Refactors backoffice sections with default element and context API (#20305)

Refactors backoffice sections

using the extensions registry to set a default/fallback
element and API for the section.
This commit is contained in:
Lee Kelleher
2025-10-07 13:40:00 +01:00
committed by GitHub
parent 034cf2894c
commit 2f712cab08
14 changed files with 128 additions and 142 deletions

View File

@@ -1,16 +1,16 @@
import { tryExecute } from '@umbraco-cms/backoffice/resources';
import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
import { ServerService } from '@umbraco-cms/backoffice/external/backend-api';
import { UmbBasicState, UmbStringState } from '@umbraco-cms/backoffice/observable-api';
import { UmbContextToken } from '@umbraco-cms/backoffice/context-api';
import { UmbContextBase } from '@umbraco-cms/backoffice/class-api';
import { UmbContextToken } from '@umbraco-cms/backoffice/context-api';
import { UmbExtensionsManifestInitializer } from '@umbraco-cms/backoffice/extension-api';
import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
import { UmbSysinfoRepository } from '@umbraco-cms/backoffice/sysinfo';
import { UMB_AUTH_CONTEXT } from '@umbraco-cms/backoffice/auth';
import { UMB_CURRENT_USER_CONTEXT } from '@umbraco-cms/backoffice/current-user';
import type { ManifestSection } from '@umbraco-cms/backoffice/section';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import type { UmbExtensionManifestInitializer } from '@umbraco-cms/backoffice/extension-api';
import { UMB_AUTH_CONTEXT } from '@umbraco-cms/backoffice/auth';
import { UMB_CURRENT_USER_CONTEXT } from '@umbraco-cms/backoffice/current-user';
import { UmbSysinfoRepository } from '@umbraco-cms/backoffice/sysinfo';
export class UmbBackofficeContext extends UmbContextBase {
#activeSectionAlias = new UmbStringState(undefined);

View File

@@ -1,15 +1,14 @@
import { UMB_BACKOFFICE_CONTEXT } from '../backoffice.context.js';
import type { UmbBackofficeContext } from '../backoffice.context.js';
import type { CSSResultGroup } from '@umbraco-cms/backoffice/external/lit';
import { css, html, customElement, state, repeat, ifDefined } from '@umbraco-cms/backoffice/external/lit';
import type { ManifestSection } from '@umbraco-cms/backoffice/section';
import { css, customElement, html, ifDefined, repeat, state } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import type { UmbExtensionManifestInitializer } from '@umbraco-cms/backoffice/extension-api';
import type { ManifestSection } from '@umbraco-cms/backoffice/section';
@customElement('umb-backoffice-header-sections')
export class UmbBackofficeHeaderSectionsElement extends UmbLitElement {
@state()
private _sections: Array<UmbExtensionManifestInitializer<ManifestSection>> = [];
private _sections: Array<ManifestSection> = [];
@state()
private _currentSectionAlias = '';
@@ -35,7 +34,7 @@ export class UmbBackofficeHeaderSectionsElement extends UmbLitElement {
this._backofficeContext.allowedSections,
(allowedSections) => {
const oldValue = this._sections;
this._sections = allowedSections;
this._sections = allowedSections.map((section) => section.manifest).filter((section) => section !== undefined);
this.requestUpdate('_sections', oldValue);
},
'observeSections',
@@ -54,10 +53,6 @@ export class UmbBackofficeHeaderSectionsElement extends UmbLitElement {
);
}
#getSectionName(section: UmbExtensionManifestInitializer<ManifestSection>) {
return section.manifest?.meta.label ? this.localize.string(section.manifest?.meta.label) : section.manifest?.name;
}
#getSectionPath(manifest: ManifestSection | undefined) {
return `section/${manifest?.meta.pathname}`;
}
@@ -107,21 +102,24 @@ export class UmbBackofficeHeaderSectionsElement extends UmbLitElement {
${repeat(
this._sections,
(section) => section.alias,
(section) => html`
<uui-tab
?active="${this._currentSectionAlias === section.alias}"
@click=${(event: PointerEvent) => this.#onSectionClick(event, section.manifest)}
href="${this.#getSectionPath(section.manifest)}"
label="${ifDefined(this.#getSectionName(section))}"
data-mark="section-link:${section.alias}"
>${this.#getSectionName(section)}</uui-tab
>
`,
(section) => this.#renderItem(section),
)}
</uui-tab-group>
`;
}
#renderItem(manifest: ManifestSection) {
const label = this.localize.string(manifest?.meta.label || manifest?.name);
return html`<uui-tab
data-mark="section-link:${manifest.alias}"
href=${this.#getSectionPath(manifest)}
label=${ifDefined(label)}
?active=${this._currentSectionAlias === manifest.alias}
@click=${(event: PointerEvent) => this.#onSectionClick(event, manifest)}
>${label}</uui-tab
>`;
}
static override styles: CSSResultGroup = [
css`
:host {
@@ -131,10 +129,10 @@ export class UmbBackofficeHeaderSectionsElement extends UmbLitElement {
height: 60px;
flex-basis: 100%;
font-size: 16px; /* specific for the header */
background-color: var(--uui-color-header-background);
--uui-tab-text: var(--uui-color-header-contrast);
--uui-tab-text-hover: var(--uui-color-header-contrast-emphasis);
--uui-tab-text-active: var(--uui-color-header-contrast-emphasis);
background-color: var(--uui-color-header-background);
--uui-tab-group-dropdown-background: var(--uui-color-header-surface);
}
`,

View File

@@ -1,12 +1,11 @@
import type { UmbBackofficeContext } from '../backoffice.context.js';
import { UMB_BACKOFFICE_CONTEXT } from '../backoffice.context.js';
import { css, html, customElement, state, nothing } from '@umbraco-cms/backoffice/external/lit';
import { UmbSectionContext, UMB_SECTION_PATH_PATTERN } from '@umbraco-cms/backoffice/section';
import type { PageComponent, UmbRoute } from '@umbraco-cms/backoffice/router';
import type { ManifestSection, UmbSectionElement } from '@umbraco-cms/backoffice/section';
import type { UmbExtensionManifestInitializer } from '@umbraco-cms/backoffice/extension-api';
import { createExtensionElement } from '@umbraco-cms/backoffice/extension-api';
import type { UmbBackofficeContext } from '../backoffice.context.js';
import { createExtensionApi, createExtensionElement } from '@umbraco-cms/backoffice/extension-api';
import { css, customElement, html, state } from '@umbraco-cms/backoffice/external/lit';
import { UmbDefaultSectionContext, UMB_SECTION_PATH_PATTERN } from '@umbraco-cms/backoffice/section';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import type { ManifestSection, UmbSectionElement } from '@umbraco-cms/backoffice/section';
import type { PageComponent, UmbRoute } from '@umbraco-cms/backoffice/router';
@customElement('umb-backoffice-main')
export class UmbBackofficeMainElement extends UmbLitElement {
@@ -14,49 +13,53 @@ export class UmbBackofficeMainElement extends UmbLitElement {
private _routes: Array<UmbRoute> = [];
@state()
private _sections: Array<UmbExtensionManifestInitializer<ManifestSection>> = [];
private _sections: Array<ManifestSection> = [];
private _backofficeContext?: UmbBackofficeContext;
private _sectionContext?: UmbSectionContext;
#backofficeContext?: UmbBackofficeContext;
constructor() {
super();
this.consumeContext(UMB_BACKOFFICE_CONTEXT, (_instance) => {
this._backofficeContext = _instance;
this.#backofficeContext = _instance;
this._observeBackoffice();
});
}
private async _observeBackoffice() {
if (this._backofficeContext) {
if (this.#backofficeContext) {
this.observe(
this._backofficeContext.allowedSections,
this.#backofficeContext.allowedSections,
(sections) => {
this._sections = sections.filter((x) => x.manifest);
this._createRoutes();
this._sections = sections.map((section) => section.manifest).filter((section) => section !== undefined);
this.#createRoutes();
},
'observeAllowedSections',
);
}
}
private _createRoutes() {
#createRoutes() {
if (!this._sections) return;
// TODO: Refactor this for re-use across the app where the routes are re-generated at any time.
const newRoutes: Array<UmbRoute> = this._sections.map((section) => {
const newRoutes: Array<UmbRoute> = this._sections.map((manifest) => {
return {
path: UMB_SECTION_PATH_PATTERN.generateLocal({ sectionName: section.manifest!.meta.pathname }),
component: () => createExtensionElement(section.manifest!, 'umb-section-default'),
setup: (component: PageComponent) => {
const manifest = section.manifest;
if (manifest) {
(component as UmbSectionElement).manifest = section.manifest;
path: UMB_SECTION_PATH_PATTERN.generateLocal({ sectionName: manifest.meta.pathname }),
component: () => createExtensionElement(manifest, 'umb-section-default'),
setup: async (component: PageComponent) => {
if (component) {
const element = component as UmbSectionElement;
this._backofficeContext?.setActiveSectionAlias(manifest.alias);
this._provideSectionContext(manifest);
element.manifest = manifest;
const api = await createExtensionApi(element, manifest, [], UmbDefaultSectionContext);
if (api) {
api.manifest = manifest;
}
}
this.#backofficeContext?.setActiveSectionAlias(manifest.alias);
},
};
});
@@ -78,17 +81,9 @@ export class UmbBackofficeMainElement extends UmbLitElement {
this._routes = newRoutes;
}
// TODO: v17. Remove this section context when we have api's as part of the section manifest.
private _provideSectionContext(sectionManifest: ManifestSection) {
if (!this._sectionContext) {
this._sectionContext = new UmbSectionContext(this);
}
this._sectionContext.setManifest(sectionManifest);
}
override render() {
return this._routes.length > 0 ? html`<umb-router-slot .routes=${this._routes}></umb-router-slot>` : nothing;
if (!this._routes.length) return;
return html`<umb-router-slot .routes=${this._routes}></umb-router-slot>`;
}
static override styles = [

View File

@@ -1,13 +1,15 @@
import type { UmbApi } from '../models/api.interface.js';
import type { ApiLoaderProperty } from '../types/utils.js';
import type { ManifestApi, ManifestElementAndApi } from '../types/base.types.js';
import { loadManifestApi } from './load-manifest-api.function.js';
import type { UmbApi } from '../models/api.interface.js';
import type { UmbApiConstructorArgumentsMethodType } from './types.js';
import { loadManifestApi } from './load-manifest-api.function.js';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
/**
* @param {UmbControllerHost} host - The controller host for this controller to be appended to
* @param {ManifestApi} manifest - The manifest of the extension
* @param {Array | UmbApiConstructorArgumentsMethodType} constructorArgs - The constructor arguments to pass to the API class
* @param {ApiLoaderProperty} fallbackApi - A fallback API loader property to use if the manifest does not have one
* @returns {Promise<UmbApi | undefined>} - The API class instance
*/
export async function createExtensionApi<ApiType extends UmbApi = UmbApi>(
@@ -16,37 +18,25 @@ export async function createExtensionApi<ApiType extends UmbApi = UmbApi>(
constructorArgs?:
| Array<unknown>
| UmbApiConstructorArgumentsMethodType<ManifestApi<ApiType> | ManifestElementAndApi<any, ApiType>>,
fallbackApi?: ApiLoaderProperty<ApiType>,
): Promise<ApiType | undefined> {
if (manifest.api) {
const apiConstructor = await loadManifestApi<ApiType>(manifest.api);
if (apiConstructor) {
const additionalArgs =
(typeof constructorArgs === 'function' ? constructorArgs(manifest) : constructorArgs) ?? [];
return new apiConstructor(host, ...additionalArgs);
} else {
console.error(
`-- Extension of alias "${manifest.alias}" did not succeed instantiate a API class via the extension manifest property 'api', using either a 'api' or 'default' export`,
manifest,
);
}
const apiPropValue = manifest.api ?? manifest.js ?? fallbackApi;
if (!apiPropValue) {
console.error(
`-- Extension of alias "${manifest.alias}" did not succeed creating an API class instance, missing a JavaScript file via the 'api' or 'js' property, using either an 'api' or 'default' export.`,
manifest,
);
return undefined;
}
if (manifest.js) {
const apiConstructor2 = await loadManifestApi<ApiType>(manifest.js);
if (apiConstructor2) {
const additionalArgs =
(typeof constructorArgs === 'function' ? constructorArgs(manifest) : constructorArgs) ?? [];
return new apiConstructor2(host, ...additionalArgs);
} else {
console.error(
`-- Extension of alias "${manifest.alias}" did not succeed instantiate a API class via the extension manifest property 'js', using either a 'api' or 'default' export`,
manifest,
);
}
const apiConstructor = await loadManifestApi<ApiType>(apiPropValue);
if (apiConstructor) {
const additionalArgs = (typeof constructorArgs === 'function' ? constructorArgs(manifest) : constructorArgs) ?? [];
return new apiConstructor(host, ...additionalArgs);
}
console.error(
`-- Extension of alias "${manifest.alias}" did not succeed creating an api class instance, missing a JavaScript file via the 'api' or 'js' property.`,
`-- Extension of alias "${manifest.alias}" did not succeed instantiating an API class instance via the extension manifest 'api' or 'js' property, using either an 'api' or 'default' export.`,
manifest,
);

View File

@@ -6,3 +6,7 @@ export class UmbDefaultSectionContext extends UmbSectionContext {
super(host);
}
}
export default UmbDefaultSectionContext;
export { UmbDefaultSectionContext as api };

View File

@@ -1,21 +1,20 @@
import type { ManifestSection, UmbSectionElement } from '../types.js';
import type { ManifestSectionRoute } from '../extensions/section-route.extension.js';
import type { UmbSectionMainViewElement } from '../section-main-views/section-main-views.element.js';
import type { ManifestSection, UmbSectionElement } from '../types.js';
import { UmbDefaultSectionContext } from './default-section.context.js';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { css, html, nothing, customElement, property, state, repeat } from '@umbraco-cms/backoffice/external/lit';
import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
import type { IRoute, IRoutingInfo, PageComponent, UmbRoute } from '@umbraco-cms/backoffice/router';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import type { UmbExtensionElementInitializer } from '@umbraco-cms/backoffice/extension-api';
import { aliasToPath } from '@umbraco-cms/backoffice/utils';
import { css, customElement, html, nothing, repeat, state } from '@umbraco-cms/backoffice/external/lit';
import {
UmbExtensionsElementInitializer,
UmbExtensionsManifestInitializer,
createExtensionApi,
createExtensionElement,
UmbExtensionsElementInitializer,
UmbExtensionsManifestInitializer,
} from '@umbraco-cms/backoffice/extension-api';
import { aliasToPath } from '@umbraco-cms/backoffice/utils';
import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { UMB_MARK_ATTRIBUTE_NAME } from '@umbraco-cms/backoffice/const';
import type { IRoute, IRoutingInfo, PageComponent, UmbRoute } from '@umbraco-cms/backoffice/router';
import type { UmbExtensionElementInitializer } from '@umbraco-cms/backoffice/extension-api';
/**
* @class UmbBaseSectionElement
@@ -23,19 +22,7 @@ import { UMB_MARK_ATTRIBUTE_NAME } from '@umbraco-cms/backoffice/const';
*/
@customElement('umb-section-default')
export class UmbDefaultSectionElement extends UmbLitElement implements UmbSectionElement {
private _manifest?: ManifestSection | undefined;
@property({ type: Object, attribute: false })
public get manifest(): ManifestSection | undefined {
return this._manifest;
}
public set manifest(value: ManifestSection | undefined) {
const oldValue = this._manifest;
if (oldValue === value) return;
this._manifest = value;
this.#api.setManifest(value);
this.requestUpdate('manifest', oldValue);
}
public manifest?: ManifestSection;
@state()
private _routes?: Array<UmbRoute>;
@@ -46,9 +33,6 @@ export class UmbDefaultSectionElement extends UmbLitElement implements UmbSectio
@state()
private _splitPanelPosition = '300px';
// TODO: v17: Move this to a manifest api. It will have to wait for a major as it will be a breaking change.
#api = new UmbDefaultSectionContext(this);
constructor() {
super();

View File

@@ -0,0 +1,19 @@
import type { UmbExtensionManifestKind } from '@umbraco-cms/backoffice/extension-registry';
export const manifest: UmbExtensionManifestKind = {
type: 'kind',
alias: 'Umb.Kind.Section.Default',
matchKind: 'default',
matchType: 'section',
manifest: {
type: 'section',
kind: 'default',
weight: 1000,
api: () => import('./default-section.context.js'),
element: () => import('./default-section.element.js'),
meta: {
label: '',
pathname: '',
},
},
};

View File

@@ -0,0 +1,2 @@
export * from './default-section.context.js';
export * from './default-section.element.js';

View File

@@ -1,20 +0,0 @@
import type { UmbExtensionManifestKind } from '@umbraco-cms/backoffice/extension-registry';
export const manifests: Array<UmbExtensionManifest | UmbExtensionManifestKind> = [
{
type: 'kind',
alias: 'Umb.Kind.Section.Default',
matchKind: 'default',
matchType: 'section',
manifest: {
type: 'section',
kind: 'default',
weight: 1000,
element: () => import('./default-section.element.js'),
meta: {
label: '',
pathname: '',
},
},
},
];

View File

@@ -1,5 +1,6 @@
import type { ManifestSection } from './section.extension.js';
import type { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api';
export interface UmbSectionElement extends HTMLElement {
export interface UmbSectionElement extends UmbControllerHostElement {
manifest?: ManifestSection;
}

View File

@@ -1,9 +1,9 @@
import type { UmbSectionContext } from '../section.context.js';
import type { UmbSectionElement } from './section-element.interface.js';
import type { ManifestElement, ManifestWithDynamicConditions } from '@umbraco-cms/backoffice/extension-api';
import type { ManifestElementAndApi, ManifestWithDynamicConditions } from '@umbraco-cms/backoffice/extension-api';
export interface ManifestSection
// TODO: v17 change to extend Element and Api
extends ManifestElement<UmbSectionElement>,
extends ManifestElementAndApi<UmbSectionElement, UmbSectionContext>,
ManifestWithDynamicConditions<UmbExtensionConditionConfig> {
type: 'section';
meta: MetaSection;

View File

@@ -1,6 +1,6 @@
export * from './components/index.js';
export * from './constants.js';
export * from './default/default-section.element.js';
export * from './default/index.js';
export * from './section-main/index.js';
export * from './section-picker-modal/section-picker-modal.token.js';
export * from './section-sidebar/index.js';

View File

@@ -1,13 +1,16 @@
import { manifest as defaultSectionManifest } from './default/default.section.kind.js';
import { manifests as sectionUserPermissionConditionManifests } from './conditions/manifests.js';
import { manifests as repositoryManifests } from './repository/manifests.js';
import type { UmbExtensionManifestKind } from '@umbraco-cms/backoffice/extension-registry';
export const manifests: Array<UmbExtensionManifest> = [
export const manifests: Array<UmbExtensionManifest | UmbExtensionManifestKind> = [
{
type: 'modal',
alias: 'Umb.Modal.SectionPicker',
name: 'Section Picker Modal',
element: () => import('./section-picker-modal/section-picker-modal.element.js'),
},
defaultSectionManifest,
...sectionUserPermissionConditionManifests,
...repositoryManifests,
];

View File

@@ -5,9 +5,10 @@ import { UmbExtensionsApiInitializer } from '@umbraco-cms/backoffice/extension-a
import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
import { UmbStringState } from '@umbraco-cms/backoffice/observable-api';
import { UmbViewContext } from '@umbraco-cms/backoffice/view';
import type { UmbApi } from '@umbraco-cms/backoffice/extension-api';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
export class UmbSectionContext extends UmbContextBase {
export class UmbSectionContext extends UmbContextBase implements UmbApi {
#manifestAlias = new UmbStringState<string | undefined>(undefined);
#manifestPathname = new UmbStringState<string | undefined>(undefined);
#manifestLabel = new UmbStringState<string | undefined>(undefined);
@@ -24,13 +25,20 @@ export class UmbSectionContext extends UmbContextBase {
this.#createSectionContextExtensions();
}
public setManifest(manifest?: ManifestSection) {
public set manifest(manifest: ManifestSection | undefined) {
this._manifest = manifest;
this.#manifestAlias.setValue(manifest?.alias);
this.#manifestPathname.setValue(manifest?.meta?.pathname);
const sectionLabel = manifest ? manifest.meta?.label || manifest.name : undefined;
this.#manifestLabel.setValue(sectionLabel);
this.#viewContext.setTitle(sectionLabel);
}
public get manifest(): ManifestSection | undefined {
return this._manifest;
}
private _manifest?: ManifestSection | undefined;
getPathname() {
return this.#manifestPathname.getValue();
@@ -50,3 +58,5 @@ export class UmbSectionContext extends UmbContextBase {
);
}
}
export default UmbSectionContext;