add section context + filter dashboards based on section

This commit is contained in:
Mads Rasmussen
2022-08-17 16:01:29 +02:00
parent d04cfd9f15
commit 16ac874e8b
13 changed files with 319 additions and 176 deletions

View File

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

View File

@@ -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<string> = [];
@state()
private _sections: Array<UmbExtensionManifestSection> = [];
@@ -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`
<uui-tab-group id="tabs">
${this._visibleSections.map(
(section: UmbExtensionManifestSection) => html`
<uui-tab
?active="${isPathActive(`/section/${section.meta.pathname}`, path())}"
@click="${this._handleTabClick}"
?active="${this._currentSectionAlias === section.alias}"
href="${`/section/${section.meta.pathname}`}"
label="${section.name}"></uui-tab>
label="${section.name}"
data-alias="${section.alias}"></uui-tab>
`
)}
${this._renderExtraSections()}
@@ -150,6 +156,10 @@ export class UmbBackofficeHeaderSections extends UmbContextConsumerMixin(LitElem
`
);
}
render() {
return html` ${this._renderSections()} `;
}
}
declare global {

View File

@@ -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<UmbExtensionManifestSection> = [];
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<UmbExtensionManifestSection>;
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 {

View File

@@ -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<UmbExtensionManifestDashboard> = [];
@state()
private _current = '';
@state()
private _routes: Array<any> = [];
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`
<uui-tab-group id="tabs">
${this._dashboards.map(
(dashboard: UmbExtensionManifestDashboard) => html`
<uui-tab
label=${dashboard.name}
?active="${dashboard.meta.pathname === this._current}"
@click="${(e: PointerEvent) => this._handleTabClick(e, dashboard)}"></uui-tab>
`
)}
</uui-tab-group>
<router-slot id="router-slot" .routes="${this._routes}"></router-slot>
`;
}
}
export default UmbSectionDashboards;
declare global {
interface HTMLElementTagNameMap {
'umb-section-dashboards': UmbSectionDashboards;
}
}

View File

@@ -23,7 +23,7 @@ export class UmbContentSection extends LitElement {
private _routes: Array<IRoute> = [
{
path: 'dashboard',
component: () => import('../../components/section-dashboards.element'),
component: () => import('../shared/section-dashboards.element'),
setup: () => {
this._currentNodeId = undefined;
},

View File

@@ -23,7 +23,7 @@ export class UmbMediaSection extends LitElement {
private _routes: Array<IRoute> = [
{
path: 'dashboard',
component: () => import('../../components/section-dashboards.element'),
component: () => import('../shared/section-dashboards.element'),
setup: () => {
this._currentNodeId = undefined;
},

View File

@@ -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<UmbExtensionManifestSection> = new BehaviorSubject({
type: 'section',
alias: '',
name: '',
meta: {
pathname: '',
weight: 0,
},
});
public readonly data: Observable<UmbExtensionManifestSection> = 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<UmbExtensionManifestSection>) {
this._data.next({ ...this._data.getValue(), ...data });
}
public getData() {
return this._data.getValue();
}
}

View File

@@ -11,7 +11,7 @@ export class UmbSettingsSection extends UmbContextConsumerMixin(LitElement) {
private _routes: Array<IRoute> = [
{
path: 'dashboard',
component: () => import('../../components/section-dashboards.element'),
component: () => import('../shared/section-dashboards.element'),
},
{
path: 'extensions',

View File

@@ -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<UmbExtensionManifestDashboard> = [];
@state()
private _currentDashboardPathname = '';
@state()
private _routes: Array<any> = [];
@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`
<uui-tab-group id="tabs">
${this._dashboards.map(
(dashboard: UmbExtensionManifestDashboard) => html`
<uui-tab
href="${`/section/${this._currentSectionPathname}/dashboard/${dashboard.meta.pathname}`}"
label=${dashboard.name}
?active="${dashboard.meta.pathname === this._currentDashboardPathname}"></uui-tab>
`
)}
</uui-tab-group>
<router-slot id="router-slot" .routes="${this._routes}"></router-slot>
`;
}
}
export default UmbSectionDashboards;
declare global {
interface HTMLElementTagNameMap {
'umb-section-dashboards': UmbSectionDashboards;
}
}

View File

@@ -0,0 +1,30 @@
import { map, Observable, ReplaySubject } from 'rxjs';
import { UmbExtensionRegistry } from '../extension';
export class UmbSectionStore {
private _extensionRegistry: UmbExtensionRegistry;
private _currentAlias: ReplaySubject<string> = new ReplaySubject(1);
public readonly currentAlias: Observable<string> = 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);
}
}