From b58efb6ad2a6ba6de89cc582da0bb0fc160b00b0 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Fri, 3 Jun 2022 10:15:13 +0200 Subject: [PATCH] add types for sections and dashboards and html elements --- .../backoffice-header-sections.element.ts | 19 ++---- .../src/backoffice/backoffice-main.element.ts | 20 ++---- .../src/backoffice/backoffice.element.ts | 64 +++++++++---------- .../core/context/context-consumer.mixin.ts | 7 +- .../src/core/context/context-consumer.test.ts | 5 +- .../core/context/context-provider.mixin.ts | 7 +- .../create-extension-element.function.ts | 45 +++++++------ .../extension/has-default-export.function.ts | 5 ++ .../core/extension/is-extension.function.ts | 9 +++ .../src/core/models/index.ts | 3 + .../node-editor/node-property.element.ts | 35 +++++----- .../content/content-editor.element.ts | 9 +-- .../src/mocks/domains/content.handlers.ts | 7 +- .../src/section/section-dashboards.element.ts | 24 ++----- 14 files changed, 124 insertions(+), 135 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/core/extension/has-default-export.function.ts create mode 100644 src/Umbraco.Web.UI.Client/src/core/extension/is-extension.function.ts 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 95172a66ad..5766dd00ac 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,13 +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 { isPathActive, path } from 'router-slot'; +import { map, Subscription } from 'rxjs'; import { getUserSections } from '../core/api/fetcher'; -import { UmbExtensionRegistry, UmbExtensionManifest, UmbExtensionManifestSection } from '../core/extension'; import { UmbContextConsumerMixin } from '../core/context'; +import { UmbExtensionManifestSection, UmbExtensionRegistry } from '../core/extension'; @customElement('umb-backoffice-header-sections') export class UmbBackofficeHeaderSections extends UmbContextConsumerMixin(LitElement) { @@ -95,16 +95,11 @@ export class UmbBackofficeHeaderSections extends UmbContextConsumerMixin(LitElem const { data } = await getUserSections({}); this._allowedSection = data.sections; - 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._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; }); } 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 c60ed0ef94..4ad739c565 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,15 +2,10 @@ 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, map } from 'rxjs'; +import { map, Subscription } from 'rxjs'; import { UmbContextConsumerMixin } from '../core/context'; -import { - UmbExtensionRegistry, - createExtensionElement, - UmbExtensionManifest, - UmbExtensionManifestSection, -} from '../core/extension'; +import { createExtensionElement, UmbExtensionManifestSection, UmbExtensionRegistry } from '../core/extension'; @defineElement('umb-backoffice-main') export class UmbBackofficeMain extends UmbContextConsumerMixin(LitElement) { @@ -47,14 +42,9 @@ export class UmbBackofficeMain extends UmbContextConsumerMixin(LitElement) { private _useSections() { this._sectionSubscription?.unsubscribe(); - 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) - ) - ) + 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; 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 b937761c0b..4e60c3660f 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/backoffice.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/backoffice.element.ts @@ -1,19 +1,18 @@ +import '../section/section-sidebar.element'; +import './backoffice-header.element'; +import './backoffice-main.element'; + import { defineElement } from '@umbraco-ui/uui-base/lib/registration'; import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; import { css, html, LitElement } from 'lit'; - -import './backoffice-header.element'; -import '../section/section-sidebar.element'; -import './backoffice-main.element'; - -import { UUIToastNotificationContainerElement } from '@umbraco-ui/uui'; -import { UmbContextProviderMixin } from '../core/context'; -import { UmbNodeStore } from '../core/stores/node.store'; -import { UmbDataTypeStore } from '../core/stores/data-type.store'; -import { UmbNotificationService } from '../core/service/notifications.store'; -import { Subscription } from 'rxjs'; import { state } from 'lit/decorators.js'; import { repeat } from 'lit/directives/repeat.js'; +import { Subscription } from 'rxjs'; + +import { UmbContextProviderMixin } from '../core/context'; +import { UmbNotificationService } from '../core/service/notifications.store'; +import { UmbDataTypeStore } from '../core/stores/data-type.store'; +import { UmbNodeStore } from '../core/stores/node.store'; @defineElement('umb-backoffice') export default class UmbBackoffice extends UmbContextProviderMixin(LitElement) { @@ -32,11 +31,11 @@ export default class UmbBackoffice extends UmbContextProviderMixin(LitElement) { #notifications { position: absolute; - top:0; - left:0; - right:0; + top: 0; + left: 0; + right: 0; bottom: 70px; - height:auto; + height: auto; padding: var(--uui-size-layout-1); } `, @@ -46,7 +45,7 @@ export default class UmbBackoffice extends UmbContextProviderMixin(LitElement) { private _notificationSubscribtion?: Subscription; @state() - private _notifications:any[] = []; + private _notifications: any[] = []; constructor() { super(); @@ -58,37 +57,34 @@ export default class UmbBackoffice extends UmbContextProviderMixin(LitElement) { this.provideContext('umbNotificationService', this._notificationService); } - protected firstUpdated( - _changedProperties: Map - ): void { + protected firstUpdated(_changedProperties: Map): void { super.firstUpdated(_changedProperties); - this._notificationSubscribtion = this._notificationService.notifications - .subscribe((notifications: Array) => { + this._notificationSubscribtion = this._notificationService.notifications.subscribe((notifications: Array) => { this._notifications = notifications; }); // TODO: listen to close event and remove notification from store. } + disconnectedCallback(): void { + super.disconnectedCallback(); + + this._notificationSubscribtion?.unsubscribe(); + } + render() { return html` - - ${ - repeat( - this._notifications, - (notification: any) => notification.key, - notification => html` - - + + ${repeat( + this._notifications, + (notification) => notification.key, + (notification) => html` + ` - ) - } + )} `; } diff --git a/src/Umbraco.Web.UI.Client/src/core/context/context-consumer.mixin.ts b/src/Umbraco.Web.UI.Client/src/core/context/context-consumer.mixin.ts index 4c0706d8ce..8413baa8dd 100644 --- a/src/Umbraco.Web.UI.Client/src/core/context/context-consumer.mixin.ts +++ b/src/Umbraco.Web.UI.Client/src/core/context/context-consumer.mixin.ts @@ -1,7 +1,6 @@ +import { HTMLElementConstructor } from '../models'; import { UmbContextConsumer } from './context-consumer'; -type Constructor = new (...args: any[]) => T; - export declare class UmbContextConsumerInterface { consumeContext(alias: string, callback?: (_instance: any) => void): void; whenAvailableOrChanged(contextAliases: string[], callback?: () => void): void; @@ -14,7 +13,7 @@ export declare class UmbContextConsumerInterface { * @param {Object} superClass - superclass to be extended. * @mixin */ -export const UmbContextConsumerMixin = >(superClass: T) => { +export const UmbContextConsumerMixin = (superClass: T) => { class UmbContextConsumerClass extends superClass { // all context requesters in the element _consumers: Map = new Map(); @@ -75,7 +74,7 @@ export const UmbContextConsumerMixin = >(supe } } - return UmbContextConsumerClass as unknown as Constructor & T; + return UmbContextConsumerClass as unknown as HTMLElementConstructor & T; }; declare global { diff --git a/src/Umbraco.Web.UI.Client/src/core/context/context-consumer.test.ts b/src/Umbraco.Web.UI.Client/src/core/context/context-consumer.test.ts index f3b76445e0..83da94fd38 100644 --- a/src/Umbraco.Web.UI.Client/src/core/context/context-consumer.test.ts +++ b/src/Umbraco.Web.UI.Client/src/core/context/context-consumer.test.ts @@ -1,6 +1,7 @@ import { expect, oneEvent } from '@open-wc/testing'; -import { UmbContextProvider } from './context-provider'; + import { UmbContextConsumer } from './context-consumer'; +import { UmbContextProvider } from './context-provider'; import { UmbContextRequestEventImplementation, umbContextRequestEventType } from './context-request.event'; const testContextAlias = 'my-test-context'; @@ -38,7 +39,7 @@ describe('UmbContextConsumer', () => { }); }); - it('works with UmbContextProvider', (done: any) => { + it('works with UmbContextProvider', (done) => { const provider = new UmbContextProvider(document.body, testContextAlias, new MyClass()); provider.attach(); diff --git a/src/Umbraco.Web.UI.Client/src/core/context/context-provider.mixin.ts b/src/Umbraco.Web.UI.Client/src/core/context/context-provider.mixin.ts index e4ef3626a9..d793fde40e 100644 --- a/src/Umbraco.Web.UI.Client/src/core/context/context-provider.mixin.ts +++ b/src/Umbraco.Web.UI.Client/src/core/context/context-provider.mixin.ts @@ -1,12 +1,11 @@ +import { HTMLElementConstructor } from '../models'; import { UmbContextProvider } from './context-provider'; -type Constructor = new (...args: any[]) => T; - export declare class UmbContextProviderMixinInterface { provideContext(alias: string, instance: unknown): void; } -export const UmbContextProviderMixin = (superClass: T) => { +export const UmbContextProviderMixin = (superClass: T) => { class UmbContextProviderClass extends superClass { _providers: Map = new Map(); @@ -39,7 +38,7 @@ export const UmbContextProviderMixin = (superClass: T) => } } - return UmbContextProviderClass as unknown as Constructor & T; + return UmbContextProviderClass as unknown as HTMLElementConstructor & T; }; declare global { diff --git a/src/Umbraco.Web.UI.Client/src/core/extension/create-extension-element.function.ts b/src/Umbraco.Web.UI.Client/src/core/extension/create-extension-element.function.ts index 1e4b045076..1b30d60c94 100644 --- a/src/Umbraco.Web.UI.Client/src/core/extension/create-extension-element.function.ts +++ b/src/Umbraco.Web.UI.Client/src/core/extension/create-extension-element.function.ts @@ -1,30 +1,29 @@ import { UmbExtensionManifest } from './extension.registry'; +import { hasDefaultExport } from './has-default-export.function'; +import { isExtensionType } from './is-extension.function'; import { loadExtension } from './load-extension.function'; -export function createExtensionElement(manifest: UmbExtensionManifest): Promise | Promise { +export async function createExtensionElement(manifest: UmbExtensionManifest): Promise { //TODO: Write tests for these extension options: - return loadExtension(manifest).then((js) => { - if (manifest.elementName) { - console.log('-- created by elementName', manifest.elementName); - return document.createElement(manifest.elementName as any); + const js = await loadExtension(manifest); + if (manifest.elementName) { + console.log('-- created by elementName', manifest.elementName); + return document.createElement(manifest.elementName); + } + if (js) { + if (js instanceof HTMLElement) { + console.log('-- created by manifest method providing HTMLElement', js); + return js; } - - if (js) { - if (js instanceof HTMLElement) { - console.log('-- created by manifest method providing HTMLElement', js); - return js; - } - if ((js as any).elementName) { - console.log('-- created by export elementName', (js as any).elementName); - return document.createElement((js as any).elementName); - } - if ((js as any).default) { - console.log('-- created by default class', (js as any).default); - return new (js as any).default() as HTMLElement; - } + if (isExtensionType(js)) { + console.log('-- created by export elementName', js.elementName); + return js.elementName ? document.createElement(js.elementName) : Promise.resolve(undefined); } - - console.error('-- Extension did not succeed creating an element'); - return Promise.resolve(undefined); - }); + if (hasDefaultExport(js)) { + console.log('-- created by default class', js.default); + return new js.default(); + } + } + console.error('-- Extension did not succeed creating an element'); + return Promise.resolve(undefined); } diff --git a/src/Umbraco.Web.UI.Client/src/core/extension/has-default-export.function.ts b/src/Umbraco.Web.UI.Client/src/core/extension/has-default-export.function.ts new file mode 100644 index 0000000000..8a7931479f --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/core/extension/has-default-export.function.ts @@ -0,0 +1,5 @@ +import { HTMLElementConstructor } from '../models'; + +export function hasDefaultExport(object: unknown): object is { default: HTMLElementConstructor } { + return typeof object === 'object' && object !== null && 'default' in object; +} diff --git a/src/Umbraco.Web.UI.Client/src/core/extension/is-extension.function.ts b/src/Umbraco.Web.UI.Client/src/core/extension/is-extension.function.ts new file mode 100644 index 0000000000..ab06750c87 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/core/extension/is-extension.function.ts @@ -0,0 +1,9 @@ +import { UmbExtensionManifestBase } from './extension.registry'; + +export function isExtensionType(manifest: unknown): manifest is UmbExtensionManifestBase { + return ( + typeof manifest === 'object' && + manifest !== null && + (manifest as UmbExtensionManifestBase).elementName !== undefined + ); +} diff --git a/src/Umbraco.Web.UI.Client/src/core/models/index.ts b/src/Umbraco.Web.UI.Client/src/core/models/index.ts index 618d264334..697a62f6ea 100644 --- a/src/Umbraco.Web.UI.Client/src/core/models/index.ts +++ b/src/Umbraco.Web.UI.Client/src/core/models/index.ts @@ -17,3 +17,6 @@ export type UmbracoPerformInstallDatabaseConfiguration = export type UmbracoInstallerDatabaseModel = components['schemas']['UmbracoInstallerDatabaseModel']; export type UmbracoInstallerUserModel = components['schemas']['UmbracoInstallerUserModel']; export type TelemetryModel = components['schemas']['TelemetryModel']; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type HTMLElementConstructor = new (...args: any[]) => T; diff --git a/src/Umbraco.Web.UI.Client/src/editors/node-editor/node-property.element.ts b/src/Umbraco.Web.UI.Client/src/editors/node-editor/node-property.element.ts index 42d6e21f44..e1a37095cd 100644 --- a/src/Umbraco.Web.UI.Client/src/editors/node-editor/node-property.element.ts +++ b/src/Umbraco.Web.UI.Client/src/editors/node-editor/node-property.element.ts @@ -1,11 +1,13 @@ -import { css, html, LitElement, PropertyValueMap } from 'lit'; import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; +import { css, html, LitElement, PropertyValueMap } from 'lit'; import { customElement, property, state } from 'lit/decorators.js'; -import { createExtensionElement, UmbExtensionManifest, UmbExtensionRegistry } from '../../core/extension'; -import { Subscription, map, switchMap } from 'rxjs'; +import { EMPTY, of, Subscription } from 'rxjs'; +import { switchMap } from 'rxjs/operators'; + import { UmbContextConsumerMixin } from '../../core/context'; -import { DataTypeEntity } from '../../mocks/data/content.data'; +import { createExtensionElement, UmbExtensionManifest, UmbExtensionRegistry } from '../../core/extension'; import { UmbDataTypeStore } from '../../core/stores/data-type.store'; +import { DataTypeEntity } from '../../mocks/data/content.data'; @customElement('umb-node-property') class UmbNodeProperty extends UmbContextConsumerMixin(LitElement) { @@ -63,26 +65,27 @@ class UmbNodeProperty extends UmbContextConsumerMixin(LitElement) { private _useDataType() { this._dataTypeSubscription?.unsubscribe(); if (this._property.dataTypeKey && this._extensionRegistry && this._dataTypeStore) { - this._dataTypeSubscription = this._dataTypeStore .getByKey(this._property.dataTypeKey) .pipe( - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - map((dataTypeEntity: DataTypeEntity) => { + switchMap((dataTypeEntity) => { + if (!dataTypeEntity) { + return EMPTY; + } this._dataType = dataTypeEntity; - return dataTypeEntity.propertyEditorUIAlias; - }), - switchMap((alias: string) => this._extensionRegistry?.getByAlias(alias) as any) + + return this._extensionRegistry?.getByAlias(dataTypeEntity.propertyEditorUIAlias) ?? of(null); + }) ) - .subscribe((propertyEditorUI: any) => { - this._gotData(propertyEditorUI); + .subscribe((propertyEditorUI) => { + if (propertyEditorUI) { + this._gotData(propertyEditorUI); + } }); } } private _gotData(_propertyEditorUI?: UmbExtensionManifest) { - if (!this._dataType || !_propertyEditorUI) { // TODO: if dataTypeKey didn't exist in store, we should do some nice UI. return; @@ -145,9 +148,7 @@ class UmbNodeProperty extends UmbContextConsumerMixin(LitElement) { ${this.property.label}

${this.property.description}

-
- ${this._element} -
+
${this._element}
`; } diff --git a/src/Umbraco.Web.UI.Client/src/extensions/sections/content/content-editor.element.ts b/src/Umbraco.Web.UI.Client/src/extensions/sections/content/content-editor.element.ts index bfa40b6f85..9fff3f0132 100644 --- a/src/Umbraco.Web.UI.Client/src/extensions/sections/content/content-editor.element.ts +++ b/src/Umbraco.Web.UI.Client/src/extensions/sections/content/content-editor.element.ts @@ -1,11 +1,12 @@ -import { css, html, LitElement } from 'lit'; import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; +import { css, html, LitElement } from 'lit'; import { customElement, property, state } from 'lit/decorators.js'; -import { UmbContextConsumerMixin } from '../../../core/context'; -import { UmbNodeStore } from '../../../core/stores/node.store'; import { Subscription } from 'rxjs'; -import { DocumentNode } from '../../../mocks/data/content.data'; + +import { UmbContextConsumerMixin } from '../../../core/context'; import { UmbNotificationService } from '../../../core/service/notifications.store'; +import { UmbNodeStore } from '../../../core/stores/node.store'; +import { DocumentNode } from '../../../mocks/data/content.data'; @customElement('umb-content-editor') export class UmbContentEditor extends UmbContextConsumerMixin(LitElement) { diff --git a/src/Umbraco.Web.UI.Client/src/mocks/domains/content.handlers.ts b/src/Umbraco.Web.UI.Client/src/mocks/domains/content.handlers.ts index cecdc0bf69..d390921da5 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/domains/content.handlers.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/domains/content.handlers.ts @@ -1,5 +1,6 @@ import { rest } from 'msw'; -import { umbContentData } from '../data/content.data'; + +import { DocumentNode, umbContentData } from '../data/content.data'; // TODO: add schema export const handlers = [ @@ -13,8 +14,8 @@ export const handlers = [ return res(ctx.status(200), ctx.json([document])); }), - rest.post('/umbraco/backoffice/content/save', (req, res, ctx) => { - const data = req.body as any; + rest.post('/umbraco/backoffice/content/save', (req, res, ctx) => { + const data = req.body; if (!data) return; umbContentData.save(data); diff --git a/src/Umbraco.Web.UI.Client/src/section/section-dashboards.element.ts b/src/Umbraco.Web.UI.Client/src/section/section-dashboards.element.ts index 79bc6b2c96..e25094ae53 100644 --- a/src/Umbraco.Web.UI.Client/src/section/section-dashboards.element.ts +++ b/src/Umbraco.Web.UI.Client/src/section/section-dashboards.element.ts @@ -5,12 +5,7 @@ import { IRoutingInfo } from 'router-slot'; import { map, Subscription } from 'rxjs'; import { UmbContextConsumerMixin } from '../core/context'; -import { - UmbExtensionManifestDashboard, - UmbExtensionManifest, - UmbExtensionRegistry, - createExtensionElement, -} from '../core/extension'; +import { createExtensionElement, UmbExtensionManifestDashboard, UmbExtensionRegistry } from '../core/extension'; @customElement('umb-section-dashboards') export class UmbSectionDashboards extends UmbContextConsumerMixin(LitElement) { @@ -60,23 +55,18 @@ export class UmbSectionDashboards extends UmbContextConsumerMixin(LitElement) { private _useDashboards() { this._dashboardsSubscription?.unsubscribe(); - 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._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) => { + setup: (_element: UmbExtensionManifestDashboard, info: IRoutingInfo) => { this._current = info.match.route.path; }, };