add types for sections and dashboards and html elements

This commit is contained in:
Jacob Overgaard
2022-06-03 10:15:13 +02:00
parent c4367b496b
commit b58efb6ad2
14 changed files with 124 additions and 135 deletions

View File

@@ -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<UmbExtensionManifest>) =>
extensions
.filter((extension) => extension.type === 'section')
.sort((a: any, b: any) => b.meta.weight - a.meta.weight)
)
)
.subscribe((sections: Array<any>) => {
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;
});
}

View File

@@ -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<UmbExtensionManifest>) =>
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<UmbExtensionManifestSection>;

View File

@@ -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<string | number | symbol, unknown>
): void {
protected firstUpdated(_changedProperties: Map<string | number | symbol, unknown>): void {
super.firstUpdated(_changedProperties);
this._notificationSubscribtion = this._notificationService.notifications
.subscribe((notifications: Array<any>) => {
this._notificationSubscribtion = this._notificationService.notifications.subscribe((notifications: Array<any>) => {
this._notifications = notifications;
});
// TODO: listen to close event and remove notification from store.
}
disconnectedCallback(): void {
super.disconnectedCallback();
this._notificationSubscribtion?.unsubscribe();
}
render() {
return html`
<umb-backoffice-header></umb-backoffice-header>
<umb-backoffice-main></umb-backoffice-main>
<uui-toast-notification-container
auto-close="7000"
bottom-up
id="notifications">
${
repeat(
this._notifications,
(notification: any) => notification.key,
notification => html`<uui-toast-notification color='positive'>
<uui-toast-notification-layout .headline=${notification.headline}>
</uui-toast-notification-layout>
<uui-toast-notification-container auto-close="7000" bottom-up id="notifications">
${repeat(
this._notifications,
(notification) => notification.key,
(notification) => html`<uui-toast-notification color="positive">
<uui-toast-notification-layout .headline=${notification.headline}> </uui-toast-notification-layout>
</uui-toast-notification>`
)
}
)}
</uui-toast-notification-container>
`;
}

View File

@@ -1,7 +1,6 @@
import { HTMLElementConstructor } from '../models';
import { UmbContextConsumer } from './context-consumer';
type Constructor<T = HTMLElement> = 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 = <T extends Constructor<HTMLElement>>(superClass: T) => {
export const UmbContextConsumerMixin = <T extends HTMLElementConstructor>(superClass: T) => {
class UmbContextConsumerClass extends superClass {
// all context requesters in the element
_consumers: Map<string, UmbContextConsumer> = new Map();
@@ -75,7 +74,7 @@ export const UmbContextConsumerMixin = <T extends Constructor<HTMLElement>>(supe
}
}
return UmbContextConsumerClass as unknown as Constructor<UmbContextConsumerInterface> & T;
return UmbContextConsumerClass as unknown as HTMLElementConstructor<UmbContextConsumerInterface> & T;
};
declare global {

View File

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

View File

@@ -1,12 +1,11 @@
import { HTMLElementConstructor } from '../models';
import { UmbContextProvider } from './context-provider';
type Constructor<T = HTMLElement> = new (...args: any[]) => T;
export declare class UmbContextProviderMixinInterface {
provideContext(alias: string, instance: unknown): void;
}
export const UmbContextProviderMixin = <T extends Constructor>(superClass: T) => {
export const UmbContextProviderMixin = <T extends HTMLElementConstructor>(superClass: T) => {
class UmbContextProviderClass extends superClass {
_providers: Map<string, UmbContextProvider> = new Map();
@@ -39,7 +38,7 @@ export const UmbContextProviderMixin = <T extends Constructor>(superClass: T) =>
}
}
return UmbContextProviderClass as unknown as Constructor<UmbContextProviderMixinInterface> & T;
return UmbContextProviderClass as unknown as HTMLElementConstructor<UmbContextProviderMixinInterface> & T;
};
declare global {

View File

@@ -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<HTMLElement> | Promise<undefined> {
export async function createExtensionElement(manifest: UmbExtensionManifest): Promise<HTMLElement | undefined> {
//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);
}

View File

@@ -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;
}

View File

@@ -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
);
}

View File

@@ -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<T = HTMLElement> = new (...args: any[]) => T;

View File

@@ -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) {
<uui-label>${this.property.label}</uui-label>
<p>${this.property.description}</p>
</div>
<div slot="editor">
${this._element}
</div>
<div slot="editor">${this._element}</div>
</umb-editor-property-layout>
`;
}

View File

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

View File

@@ -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<DocumentNode[]>('/umbraco/backoffice/content/save', (req, res, ctx) => {
const data = req.body;
if (!data) return;
umbContentData.save(data);

View File

@@ -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<UmbExtensionManifest>) =>
extensions
.filter((extension) => extension.type === 'dashboard')
.sort((a: any, b: any) => b.meta.weight - a.meta.weight)
)
)
.subscribe((dashboards: Array<UmbExtensionManifest>) => {
this._dashboards = dashboards as Array<UmbExtensionManifestDashboard>;
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;
},
};