diff --git a/src/Umbraco.Web.UI.Client/.env b/src/Umbraco.Web.UI.Client/.env index 76438bfb23..dadbe466c0 100644 --- a/src/Umbraco.Web.UI.Client/.env +++ b/src/Umbraco.Web.UI.Client/.env @@ -2,3 +2,4 @@ VITE_UMBRACO_USE_MSW=on # on = turns on MSW, off = disables all mock handlers VITE_UMBRACO_API_URL=http://localhost:11000 VITE_UMBRACO_INSTALL_STATUS=running # running or must-install or must-upgrade +VITE_MSW_QUIET=off # on = turns off MSW console logs, off = turns on MSW console logs diff --git a/src/Umbraco.Web.UI.Client/libs/extensions-api/create-extension-element-or-fallback.function.ts b/src/Umbraco.Web.UI.Client/libs/extensions-api/create-extension-element-or-fallback.function.ts new file mode 100644 index 0000000000..8b207b1191 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/libs/extensions-api/create-extension-element-or-fallback.function.ts @@ -0,0 +1,10 @@ +import { createExtensionElement } from './create-extension-element.function'; +import { isManifestElementableType } from './is-manifest-elementable-type.function'; + +export async function createExtensionElementOrFallback(manifest: any, fallbackElementName: string): Promise { + if (isManifestElementableType(manifest)) { + return createExtensionElement(manifest); + } + + return Promise.resolve(document.createElement(fallbackElementName)); +} diff --git a/src/Umbraco.Web.UI.Client/libs/extensions-api/index.ts b/src/Umbraco.Web.UI.Client/libs/extensions-api/index.ts index bd0c6626ca..4104a16115 100644 --- a/src/Umbraco.Web.UI.Client/libs/extensions-api/index.ts +++ b/src/Umbraco.Web.UI.Client/libs/extensions-api/index.ts @@ -8,5 +8,6 @@ export * from './is-manifest-elementable-type.function'; export * from './is-manifest-js-type.function'; export * from './is-manifest-loader-type.function'; export * from './load-extension.function'; +export * from './create-extension-element-or-fallback.function'; export const umbExtensionsRegistry = new UmbExtensionRegistry(); diff --git a/src/Umbraco.Web.UI.Client/libs/extensions-api/registry/extension.registry.ts b/src/Umbraco.Web.UI.Client/libs/extensions-api/registry/extension.registry.ts index 146003b0c6..7e562cb17b 100644 --- a/src/Umbraco.Web.UI.Client/libs/extensions-api/registry/extension.registry.ts +++ b/src/Umbraco.Web.UI.Client/libs/extensions-api/registry/extension.registry.ts @@ -1,5 +1,5 @@ import { BehaviorSubject, map, Observable } from 'rxjs'; -import type { ManifestTypes, ManifestTypeMap, ManifestBase } from '../../models'; +import type { ManifestTypes, ManifestTypeMap, ManifestBase, ManifestWithLoader, ManifestEntrypoint } from '../../models'; import { hasDefaultExport } from '../has-default-export.function'; import { loadExtension } from '../load-extension.function'; @@ -13,7 +13,7 @@ export class UmbExtensionRegistry { private _extensions = new BehaviorSubject>([]); public readonly extensions = this._extensions.asObservable(); - register(manifest: ManifestTypes & { loader?: () => Promise }): void { + register(manifest: ManifestTypes): void { const extensionsValues = this._extensions.getValue(); const extension = extensionsValues.find((extension) => extension.alias === manifest.alias); @@ -26,7 +26,7 @@ export class UmbExtensionRegistry { // If entrypoint extension, we should load and run it immediately if (manifest.type === 'entrypoint') { - loadExtension(manifest).then((js) => { + loadExtension(manifest as ManifestEntrypoint).then((js) => { if (hasDefaultExport(js)) { new js.default(); } else { diff --git a/src/Umbraco.Web.UI.Client/libs/extensions-registry/models.ts b/src/Umbraco.Web.UI.Client/libs/extensions-registry/models.ts index d8762da1e0..02856862df 100644 --- a/src/Umbraco.Web.UI.Client/libs/extensions-registry/models.ts +++ b/src/Umbraco.Web.UI.Client/libs/extensions-registry/models.ts @@ -18,6 +18,7 @@ import type { ManifestCollectionBulkAction } from './collection-bulk-action.mode import type { ManifestCollectionView } from './collection-view.models'; import type { ManifestHealthCheck } from './health-check.models'; import type { ManifestSidebarMenuItem } from './sidebar-menu-item.models'; +import type { ManifestTheme } from './theme.models'; export * from './header-app.models'; export * from './section.models'; @@ -39,6 +40,7 @@ export * from './collection-bulk-action.models'; export * from './collection-view.models'; export * from './health-check.models'; export * from './sidebar-menu-item.models'; +export * from './theme.models'; export type ManifestTypes = | ManifestCustom @@ -63,7 +65,8 @@ export type ManifestTypes = | ManifestCollectionBulkAction | ManifestCollectionView | ManifestHealthCheck - | ManifestSidebarMenuItem; + | ManifestSidebarMenuItem + | ManifestTheme; export type ManifestStandardTypes = ManifestTypes['type']; @@ -78,14 +81,29 @@ export interface ManifestBase { weight?: number; } -export interface ManifestElement extends ManifestBase { +export interface ManifestWithLoader extends ManifestBase { + loader?: () => Promise; +} + +export interface ManifestElement extends ManifestWithLoader { type: ManifestStandardTypes; js?: string; elementName?: string; - loader?: () => Promise; + //loader?: () => Promise; meta?: any; } +export interface ManifestWithView extends ManifestElement { + meta: MetaManifestWithView; +} + +export interface MetaManifestWithView { + pathname: string; + label: string; + icon: string; +} + + export interface ManifestElementWithElementName extends ManifestElement { elementName: string; } diff --git a/src/Umbraco.Web.UI.Client/libs/extensions-registry/theme.models.ts b/src/Umbraco.Web.UI.Client/libs/extensions-registry/theme.models.ts new file mode 100644 index 0000000000..ff2cdcb3f9 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/libs/extensions-registry/theme.models.ts @@ -0,0 +1,8 @@ +import type { ManifestWithLoader } from "./models"; + + +// TODO: make or find type for JS Module with default export: Would be nice to support css file directly. +export interface ManifestTheme extends ManifestWithLoader { + type: 'theme'; + css?: string; +} diff --git a/src/Umbraco.Web.UI.Client/libs/extensions-registry/workspace-view.models.ts b/src/Umbraco.Web.UI.Client/libs/extensions-registry/workspace-view.models.ts index 066b37b3ef..acfc5e9062 100644 --- a/src/Umbraco.Web.UI.Client/libs/extensions-registry/workspace-view.models.ts +++ b/src/Umbraco.Web.UI.Client/libs/extensions-registry/workspace-view.models.ts @@ -1,6 +1,6 @@ -import type { ManifestElement } from './models'; +import type { ManifestWithView } from './models'; -export interface ManifestWorkspaceView extends ManifestElement { +export interface ManifestWorkspaceView extends ManifestWithView { type: 'workspaceView'; meta: MetaEditorView; } diff --git a/src/Umbraco.Web.UI.Client/libs/router/index.ts b/src/Umbraco.Web.UI.Client/libs/router/index.ts new file mode 100644 index 0000000000..9ba378560a --- /dev/null +++ b/src/Umbraco.Web.UI.Client/libs/router/index.ts @@ -0,0 +1,3 @@ +export * from './router-slot.element'; +export * from './router-slot-change.event'; +export * from './router-slot-init.event'; diff --git a/src/Umbraco.Web.UI.Client/libs/router/rollup.config.js b/src/Umbraco.Web.UI.Client/libs/router/rollup.config.js new file mode 100644 index 0000000000..945c0afe88 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/libs/router/rollup.config.js @@ -0,0 +1,4 @@ +import config from '../../utils/rollup.config.js'; +export default { + ...config, +}; diff --git a/src/Umbraco.Web.UI.Client/libs/router/router-slot-change.event.ts b/src/Umbraco.Web.UI.Client/libs/router/router-slot-change.event.ts new file mode 100644 index 0000000000..d5004c1d27 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/libs/router/router-slot-change.event.ts @@ -0,0 +1,7 @@ +import { UUIEvent } from '@umbraco-ui/uui-base/lib/events'; +import type { UmbRouterSlotElement } from './router-slot.element'; +export class UmbRouterSlotChangeEvent extends UUIEvent { + constructor() { + super('change'); + } +} diff --git a/src/Umbraco.Web.UI.Client/libs/router/router-slot-init.event.ts b/src/Umbraco.Web.UI.Client/libs/router/router-slot-init.event.ts new file mode 100644 index 0000000000..896abe1825 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/libs/router/router-slot-init.event.ts @@ -0,0 +1,7 @@ +import { UUIEvent } from '@umbraco-ui/uui-base/lib/events'; +import type { UmbRouterSlotElement } from './router-slot.element'; +export class UmbRouterSlotInitEvent extends UUIEvent { + constructor() { + super('init'); + } +} diff --git a/src/Umbraco.Web.UI.Client/libs/router/router-slot.element.ts b/src/Umbraco.Web.UI.Client/libs/router/router-slot.element.ts new file mode 100644 index 0000000000..26fbbfd9bb --- /dev/null +++ b/src/Umbraco.Web.UI.Client/libs/router/router-slot.element.ts @@ -0,0 +1,79 @@ +import { LitElement, PropertyValueMap } from 'lit'; +import { customElement, property } from 'lit/decorators.js'; +import { IRoute, RouterSlot } from 'router-slot'; +import { UmbRouterSlotChangeEvent, UmbRouterSlotInitEvent } from '@umbraco-cms/router'; + +/** + * @element umb-router-slot-element + * @description - Component for wrapping Router Slot element, providing some local events for implementation. + * @extends UmbRouterSlotElement + */ +@customElement('umb-router-slot') +export class UmbRouterSlotElement extends LitElement { + #router: RouterSlot; + #listening = false; + + @property() + public get routes(): IRoute[] | undefined { + return (this.#router as any).routes; + } + public set routes(value: IRoute[] | undefined) { + (this.#router as any).routes = value; + } + + private _routerPath?: string; + public get absoluteRouterPath() { + return this._routerPath; + } + + private _activeLocalPath?: string; + public get localActiveViewPath() { + return this._activeLocalPath; + } + + public get absoluteActiveViewPath() { + return this._routerPath + '/' + this._activeLocalPath; + } + + constructor() { + super(); + this.#router = document.createElement('router-slot'); + } + + connectedCallback() { + super.connectedCallback(); + if (this.#listening === false) { + window.addEventListener('navigationsuccess', this._onNavigationChanged); + this.#listening = true; + } + } + + disconnectedCallback() { + super.disconnectedCallback(); + window.removeEventListener('navigationsuccess', this._onNavigationChanged); + this.#listening = false; + } + + protected firstUpdated(_changedProperties: PropertyValueMap | Map): void { + super.firstUpdated(_changedProperties); + this._routerPath = this.#router.constructAbsolutePath('') || ''; + this.dispatchEvent(new UmbRouterSlotInitEvent()); + } + + private _onNavigationChanged = (event?: any) => { + if (event.detail.slot === this.#router) { + this._activeLocalPath = event.detail.match.route.path; + this.dispatchEvent(new UmbRouterSlotChangeEvent()); + } + }; + + render() { + return this.#router; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'umb-router-slot': UmbRouterSlotElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/libs/utils/umbraco-path.ts b/src/Umbraco.Web.UI.Client/libs/utils/umbraco-path.ts index 5e0043d3b2..b536ed8a2e 100644 --- a/src/Umbraco.Web.UI.Client/libs/utils/umbraco-path.ts +++ b/src/Umbraco.Web.UI.Client/libs/utils/umbraco-path.ts @@ -1,4 +1,3 @@ -import type { Path } from 'msw'; -export function umbracoPath(path: string): Path { +export function umbracoPath(path: string) { return `/umbraco/management/api/v1${path}`; } 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 1d632380f0..056599ad87 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/backoffice.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/backoffice.element.ts @@ -34,10 +34,12 @@ import { UmbDataTypeDetailStore } from './settings/data-types/data-type.detail.s import { UmbDataTypeTreeStore } from './settings/data-types/tree/data-type.tree.store'; import { UmbTemplateTreeStore } from './templating/templates/tree/data/template.tree.store'; import { UmbTemplateDetailStore } from './templating/templates/workspace/data/template.detail.store'; +import { UmbThemeContext } from './themes/theme.context'; import { UmbLanguageStore } from './settings/languages/language.store'; -import { UmbThemeService, UMB_THEME_SERVICE_CONTEXT_TOKEN } from './themes/theme.service'; import { UmbNotificationService, UMB_NOTIFICATION_SERVICE_CONTEXT_TOKEN } from '@umbraco-cms/notification'; +import '@umbraco-cms/router'; + // Domains import './settings'; import './documents'; @@ -102,7 +104,7 @@ export class UmbBackofficeElement extends UmbLitElement { this.provideContext(UMB_BACKOFFICE_CONTEXT_TOKEN, new UmbBackofficeContext()); this.provideContext(UMB_CURRENT_USER_HISTORY_STORE_CONTEXT_TOKEN, new UmbCurrentUserHistoryStore()); - this.provideContext(UMB_THEME_SERVICE_CONTEXT_TOKEN, new UmbThemeService()); + new UmbThemeContext(this); } render() { diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/dashboards/redirect-management/dashboard-redirect-management.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/dashboards/redirect-management/dashboard-redirect-management.element.ts index 595da98b54..ff30f6b5d5 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/documents/dashboards/redirect-management/dashboard-redirect-management.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/dashboards/redirect-management/dashboard-redirect-management.element.ts @@ -14,9 +14,9 @@ export class UmbDashboardRedirectManagementElement extends UmbLitElement { css` .actions { display: flex; - gap: 4px; + gap: var(--uui-size-space-1); justify-content: space-between; - margin-bottom: 12px; + margin-bottom: var(--uui-size-space-4); } .actions uui-icon { @@ -56,7 +56,7 @@ export class UmbDashboardRedirectManagementElement extends UmbLitElement { } .trackerDisabled::after { content: ''; - background-color: rgba(250, 250, 250, 0.7); + background-color: var(--uui-color-disabled); position: absolute; border-radius: 2px; left: 0; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/dashboards/examine-management/views/modal-views/fields-settings.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/dashboards/examine-management/views/modal-views/fields-settings.element.ts index ab1e999c0d..ce43b2b8cd 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/settings/dashboards/examine-management/views/modal-views/fields-settings.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/dashboards/examine-management/views/modal-views/fields-settings.element.ts @@ -21,7 +21,7 @@ export class UmbModalLayoutFieldsSettingsElement extends UmbModalLayoutElement ${this._renderPropertyEditorUI()} + ${this._renderPropertyEditorUI()} ${this._renderConfig()} `; } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/backoffice-frame/backoffice-header.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/backoffice-frame/backoffice-header.element.ts index f27e0a9d5d..6d66febbe2 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/backoffice-frame/backoffice-header.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/backoffice-frame/backoffice-header.element.ts @@ -26,6 +26,7 @@ export class UmbBackofficeHeader extends LitElement { --uui-button-padding-top-factor: 1; --uui-button-padding-bottom-factor: 0.5; margin-right: var(--uui-size-space-2); + --uui-button-background-color: transparent; } #logo img { diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/backoffice-frame/backoffice-main.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/backoffice-frame/backoffice-main.element.ts index d876d97a8e..e2c04a8cfe 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/backoffice-frame/backoffice-main.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/backoffice-frame/backoffice-main.element.ts @@ -4,11 +4,10 @@ import { css, html } from 'lit'; import { state } from 'lit/decorators.js'; import { IRoutingInfo } from 'router-slot'; import { UmbSectionContext, UMB_SECTION_CONTEXT_TOKEN } from '../section/section.context'; -import { UmbSectionElement } from '../section/section.element'; import { UmbBackofficeContext, UMB_BACKOFFICE_CONTEXT_TOKEN } from './backoffice.context'; -import { createExtensionElement } from '@umbraco-cms/extensions-api'; import type { ManifestSection } from '@umbraco-cms/models'; import { UmbLitElement } from '@umbraco-cms/element'; +import { createExtensionElementOrFallback } from '@umbraco-cms/extensions-api'; @defineElement('umb-backoffice-main') export class UmbBackofficeMain extends UmbLitElement { @@ -45,15 +44,18 @@ export class UmbBackofficeMain extends UmbLitElement { this._backofficeContext = _instance; this._observeBackoffice(); }); - } private async _observeBackoffice() { - if(this._backofficeContext) { - this.observe(this._backofficeContext.getAllowedSections(), (sections) => { - this._sections = sections; - this._createRoutes(); - }, 'observeAllowedSections'); + if (this._backofficeContext) { + this.observe( + this._backofficeContext.getAllowedSections(), + (sections) => { + this._sections = sections; + this._createRoutes(); + }, + 'observeAllowedSections' + ); } } @@ -64,8 +66,10 @@ export class UmbBackofficeMain extends UmbLitElement { this._routes = this._sections.map((section) => { return { path: this._routePrefix + section.meta.pathname, - component: () => this._getSectionElement(section), - setup: this._onRouteSetup, // TODO: sometimes we can end up in a state where this callback doesn't get called. It could look like a bug in the router-slot. + component: () => createExtensionElementOrFallback(section, 'umb-section'), + setup: this._onRouteSetup, + // TODO: sometimes we can end up in a state where this callback doesn't get called. It could look like a bug in the router-slot. + // Niels: Could this be because _backofficeContext is not available at that state? }; }); @@ -75,15 +79,6 @@ export class UmbBackofficeMain extends UmbLitElement { }); } - // TODO: Make this a common shared method on @umbraco-cms/extensions-api - private _getSectionElement(section: ManifestSection) { - if (!section.loader || !section.elementName || !section.js) { - return UmbSectionElement; - } - - return createExtensionElement(section); - } - private _onRouteSetup = (_component: HTMLElement, info: IRoutingInfo) => { const currentPath = info.match.route.path; const section = this._sections.find((s) => this._routePrefix + s.meta.pathname === currentPath); @@ -102,7 +97,7 @@ export class UmbBackofficeMain extends UmbLitElement { } render() { - return html``; + return html``; } } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/body-layout/body-layout.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/body-layout/body-layout.element.ts index 6a3b5f9d68..92f2b23cfc 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/body-layout/body-layout.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/body-layout/body-layout.element.ts @@ -57,7 +57,7 @@ export class UmbBodyLayout extends LitElement { #actions { display: flex; - gap: 6px; + gap: var(--uui-size-space-2); margin: 0 var(--uui-size-layout-1); margin-left: auto; } @@ -89,18 +89,39 @@ export class UmbBodyLayout extends LitElement { render() { return html` -