diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/components/editor-entity.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/components/editor-entity.element.ts new file mode 100644 index 0000000000..fe24d54bf2 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/components/editor-entity.element.ts @@ -0,0 +1,159 @@ +import { css, html, LitElement } from 'lit'; +import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; +import { customElement, property, state } from 'lit/decorators.js'; +import { UmbContextConsumerMixin } from '../../core/context'; +import { UmbExtensionManifestEditorView, UmbExtensionRegistry } from '../../core/extension'; +import { map, Subscription } from 'rxjs'; +import { IRoute, IRoutingInfo, RouterSlot } from 'router-slot'; + +@customElement('umb-editor-entity') +class UmbEditorEntity extends UmbContextConsumerMixin(LitElement) { + static styles = [ + UUITextStyles, + css` + :host { + display: block; + width: 100%; + height: 100%; + } + + uui-input { + width: 100%; + margin-left: 16px; + } + + uui-tab-group { + --uui-tab-divider: var(--uui-color-border); + border-left: 1px solid var(--uui-color-border); + flex-wrap: nowrap; + height: 60px; + } + + uui-tab { + font-size: 0.8rem; + } + `, + ]; + + @property() + alias = ''; + + @property() + name = ''; + + @state() + private _editorViews: Array = []; + + @state() + private _currentView = ''; + + @state() + private _routes: Array = []; + + private _extensionRegistry?: UmbExtensionRegistry; + private _editorViewsSubscription?: Subscription; + private _routerFolder = ''; + + constructor() { + super(); + + this.consumeContext('umbExtensionRegistry', (extensionRegistry: UmbExtensionRegistry) => { + this._extensionRegistry = extensionRegistry; + this._useEditorViews(); + }); + } + + connectedCallback(): void { + super.connectedCallback(); + /* TODO: find a way to construct absolute urls */ + this._routerFolder = window.location.pathname.split('/view')[0]; + } + + // TODO: simplify setting up editors with views. This code has to be duplicated in each editor. + private _useEditorViews() { + this._editorViewsSubscription?.unsubscribe(); + + // TODO: how do we know which editor to show the views for? + this._editorViewsSubscription = this._extensionRegistry + ?.extensionsOfType('editorView') + .pipe( + map((extensions) => + extensions + .filter((extension) => extension.meta.editors.includes(this.alias)) + .sort((a, b) => b.meta.weight - a.meta.weight) + ) + ) + .subscribe((editorViews) => { + this._editorViews = editorViews; + this._createRoutes(); + }); + } + + private async _createRoutes() { + if (this._editorViews.length > 0) { + this._routes = []; + + this._routes = this._editorViews.map((view) => { + return { + path: `view/${view.meta.pathname}`, + component: () => document.createElement(view.elementName || 'div'), + setup: (element: HTMLElement, info: IRoutingInfo) => { + this._currentView = info.match.route.path; + }, + }; + }); + + this._routes.push({ + path: '**', + redirectTo: `view/${this._editorViews?.[0].meta.pathname}`, + }); + + this.requestUpdate(); + await this.updateComplete; + + this._forceRouteRender(); + } + } + + // TODO: Figure out why this has been necessary for this case. Come up with another case + private _forceRouteRender() { + const routerSlotEl = this.shadowRoot?.querySelector('router-slot') as RouterSlot; + if (routerSlotEl) { + routerSlotEl.render(); + } + } + + render() { + return html` + + + + + ${this._editorViews.map( + (view: UmbExtensionManifestEditorView) => html` + + + ${view.name} + + ` + )} + + + + + + + + + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'umb-editor-entity': UmbEditorEntity; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/components/editor-layout.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/components/editor-layout.element.ts index ae7376b5eb..8b8d299dc1 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/components/editor-layout.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/components/editor-layout.element.ts @@ -1,6 +1,6 @@ import { css, html, LitElement } from 'lit'; import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; -import { customElement } from 'lit/decorators.js'; +import { customElement, property } from 'lit/decorators.js'; @customElement('umb-editor-layout') class UmbEditorLayout extends LitElement { @@ -60,7 +60,7 @@ class UmbEditorLayout extends LitElement {
diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/editor-views/editor-view-data-type-edit.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/editor-views/editor-view-data-type-edit.element.ts index c3ff944b80..50f3a9ae41 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/editor-views/editor-view-data-type-edit.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/editor-views/editor-view-data-type-edit.element.ts @@ -5,18 +5,25 @@ import { ifDefined } from 'lit/directives/if-defined.js'; import { UmbContextConsumerMixin } from '../../core/context'; import type { DataTypeEntity } from '../../mocks/data/data-type.data'; import type { UmbExtensionManifestPropertyEditorUI, UmbExtensionRegistry } from '../../core/extension'; +import { Subscription } from 'rxjs'; +import { UmbDataTypeContext } from '../editors/data-type/data-type.context'; +import { UUIComboboxListElement, UUIComboboxListEvent } from '@umbraco-ui/uui'; @customElement('umb-editor-view-data-type-edit') export class UmbEditorViewDataTypeEditElement extends UmbContextConsumerMixin(LitElement) { static styles = [UUITextStyles, css``]; - @property({ type: Object }) - dataType?: DataTypeEntity; + @state() + _dataType?: DataTypeEntity; @state() private _propertyEditorUIs: Array = []; private _extensionRegistry?: UmbExtensionRegistry; + private _dataTypeContext?: UmbDataTypeContext; + + private _propertyEditorUIsSubscription?: Subscription; + private _dataTypeSubscription?: Subscription; constructor() { super(); @@ -25,23 +32,54 @@ export class UmbEditorViewDataTypeEditElement extends UmbContextConsumerMixin(Li this._extensionRegistry = registry; this._usePropertyEditorUIs(); }); + + this.consumeContext('umbDataType', (dataTypeContext) => { + this._dataTypeContext = dataTypeContext; + this._useDataType(); + }); } private _usePropertyEditorUIs() { - this._extensionRegistry?.extensionsOfType('propertyEditorUI').subscribe((propertyEditorUIs) => { - this._propertyEditorUIs = propertyEditorUIs; + this._propertyEditorUIsSubscription = this._extensionRegistry + ?.extensionsOfType('propertyEditorUI') + .subscribe((propertyEditorUIs) => { + this._propertyEditorUIs = propertyEditorUIs; + }); + } + + private _useDataType() { + this._dataTypeSubscription?.unsubscribe(); + + this._dataTypeSubscription = this._dataTypeContext?.data.subscribe((dataType: DataTypeEntity) => { + this._dataType = dataType; }); } + private _handleChange(event: UUIComboboxListEvent) { + if (!this._dataType) return; + + const target = event.composedPath()[0] as UUIComboboxListElement; + const value = target.value as string; + + this._dataTypeContext?.update({ propertyEditorUIAlias: value }); + } + + disconnectedCallback(): void { + super.disconnectedCallback(); + + this._propertyEditorUIsSubscription?.unsubscribe(); + this._dataTypeSubscription?.unsubscribe(); + } + render() { return html`

Property Editor (Model/Schema?)

- Selector goes here + Selector goes here ${this._dataType?.propertyEditorUIAlias}

Property Editor UI

- + ${this._propertyEditorUIs.map( (propertyEditorUI) => html` diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/editors/data-type/data-type.context.ts b/src/Umbraco.Web.UI.Client/src/backoffice/editors/data-type/data-type.context.ts new file mode 100644 index 0000000000..5b598f3147 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/editors/data-type/data-type.context.ts @@ -0,0 +1,26 @@ +import { BehaviorSubject, Observable } from 'rxjs'; +import { DataTypeEntity } from '../../../mocks/data/data-type.data'; + +export class UmbDataTypeContext { + // TODO: figure out how fine grained we want to make our observables. + private _data: BehaviorSubject = new BehaviorSubject({ + id: -1, + key: '', + name: '', + propertyEditorUIAlias: '', + }); + public readonly data: Observable = this._data.asObservable(); + + constructor(dataType: DataTypeEntity) { + this._data.next(dataType); + } + + // TODO: figure out how we want to update data + public update(data: any) { + this._data.next({ ...this._data.getValue(), ...data }); + } + + public getData() { + return this._data.getValue(); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/editors/editor-data-type.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/editors/editor-data-type.element.ts index 212b5b32f1..2d0e64f30f 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/editors/editor-data-type.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/editors/editor-data-type.element.ts @@ -1,21 +1,22 @@ -import { UUIButtonState, UUIComboboxListElement, UUIComboboxListEvent } from '@umbraco-ui/uui'; +import { UUIButtonState, UUIInputElement, UUIInputEvent } from '@umbraco-ui/uui'; import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; -import { css, html, LitElement } from 'lit'; +import { css, html, LitElement, nothing } from 'lit'; import { customElement, property, state } from 'lit/decorators.js'; -import { IRoute, IRoutingInfo, RouterSlot } from 'router-slot'; -import { map, Subscription } from 'rxjs'; -import { UmbContextConsumerMixin } from '../../core/context'; -import { UmbExtensionManifestEditorView, UmbExtensionRegistry } from '../../core/extension'; +import { Subscription } from 'rxjs'; +import { UmbContextProviderMixin, UmbContextConsumerMixin } from '../../core/context'; import { UmbNotificationService } from '../../core/services/notification.service'; import { UmbDataTypeStore } from '../../core/stores/data-type.store'; import { DataTypeEntity } from '../../mocks/data/data-type.data'; +import { UmbDataTypeContext } from './data-type/data-type.context'; + +import '../../backoffice/components/editor-entity.element'; // Lazy load // TODO: Make this dynamic, use load-extensions method to loop over extensions for this node. import '../editor-views/editor-view-data-type-edit.element'; @customElement('umb-editor-data-type') -export class UmbEditorDataTypeElement extends UmbContextConsumerMixin(LitElement) { +export class UmbEditorDataTypeElement extends UmbContextProviderMixin(UmbContextConsumerMixin(LitElement)) { static styles = [ UUITextStyles, css` @@ -24,22 +25,6 @@ export class UmbEditorDataTypeElement extends UmbContextConsumerMixin(LitElement width: 100%; height: 100%; } - - uui-input { - width: 100%; - margin-left: 16px; - } - - uui-tab-group { - --uui-tab-divider: var(--uui-color-border); - border-left: 1px solid var(--uui-color-border); - flex-wrap: nowrap; - height: 60px; - } - - uui-tab { - font-size: 0.8rem; - } `, ]; @@ -49,25 +34,16 @@ export class UmbEditorDataTypeElement extends UmbContextConsumerMixin(LitElement @state() private _dataType?: DataTypeEntity; - @state() - private _editorViews: Array = []; - - @state() - private _currentView = ''; - - @state() - private _routes: Array = []; - @state() private _saveButtonState?: UUIButtonState; - private _dataTypeStore?: UmbDataTypeStore; - private _dataTypeSubscription?: Subscription; - private _extensionRegistry?: UmbExtensionRegistry; - private _editorViewsSubscription?: Subscription; - private _notificationService?: UmbNotificationService; + private _dataTypeContext?: UmbDataTypeContext; + private _dataTypeContextSubscription?: Subscription; - private _routerFolder = ''; + private _dataTypeStore?: UmbDataTypeStore; + private _dataTypeStoreSubscription?: Subscription; + + private _notificationService?: UmbNotificationService; constructor() { super(); @@ -77,103 +53,30 @@ export class UmbEditorDataTypeElement extends UmbContextConsumerMixin(LitElement this._useDataType(); }); - this.consumeContext('umbExtensionRegistry', (extensionRegistry: UmbExtensionRegistry) => { - this._extensionRegistry = extensionRegistry; - this._useEditorViews(); - }); - this.consumeContext('umbNotificationService', (service: UmbNotificationService) => { this._notificationService = service; }); - - // TODO: temp solution to handle property editor UI change - this.addEventListener('change', this._handleChange); - } - - private _handleChange(event: any) { - if (!this._dataType) return; - - const target = event.composedPath()[0] as UUIComboboxListElement; - const value = target.value as string; - this._dataType.propertyEditorUIAlias = value; - } - - connectedCallback(): void { - super.connectedCallback(); - /* TODO: find a way to construct absolute urls */ - this._routerFolder = window.location.pathname.split('/view')[0]; } private _useDataType() { - this._dataTypeSubscription?.unsubscribe(); + this._dataTypeStoreSubscription?.unsubscribe(); - this._dataTypeSubscription = this._dataTypeStore?.getById(parseInt(this.id)).subscribe((dataType) => { + this._dataTypeStoreSubscription = this._dataTypeStore?.getById(parseInt(this.id)).subscribe((dataType) => { if (!dataType) return; // TODO: Handle nicely if there is no node. - this._dataType = dataType; - // TODO: merge observables - this._createRoutes(); + + // provide context to all views + // TODO: This should be done in a better way, but for now it works. + this._dataTypeContext = new UmbDataTypeContext(dataType); + + this._dataTypeContextSubscription?.unsubscribe(); + this._dataTypeContextSubscription = this._dataTypeContext.data.subscribe((data) => { + this._dataType = data; + }); + + this.provideContext('umbDataType', this._dataTypeContext); }); } - // TODO: simplify setting up editors with views. This code has to be duplicated in each editor. - private _useEditorViews() { - this._editorViewsSubscription?.unsubscribe(); - - // TODO: how do we know which editor to show the views for? - this._editorViewsSubscription = this._extensionRegistry - ?.extensionsOfType('editorView') - .pipe( - map((extensions) => - extensions - .filter((extension) => extension.meta.editors.includes('Umb.Editor.DataType')) - .sort((a, b) => b.meta.weight - a.meta.weight) - ) - ) - .subscribe((editorViews) => { - this._editorViews = editorViews; - // TODO: merge observables - this._createRoutes(); - }); - } - - private async _createRoutes() { - if (this._dataType && this._editorViews.length > 0) { - this._routes = []; - - this._routes = this._editorViews.map((view) => { - return { - path: `view/${view.meta.pathname}`, - component: () => document.createElement(view.elementName || 'div'), - setup: (element: HTMLElement, info: IRoutingInfo) => { - // TODO: make interface for EditorViews - const editorView = element as any; - // TODO: how do we pass data to views? Maybe we should use a context? - editorView.dataType = this._dataType; - this._currentView = info.match.route.path; - }, - }; - }); - - this._routes.push({ - path: '**', - redirectTo: `view/${this._editorViews?.[0].meta.pathname}`, - }); - - this.requestUpdate(); - await this.updateComplete; - - this._forceRouteRender(); - } - } - - // TODO: Fgure out why this has been necessary for this case. Come up with another case - private _forceRouteRender() { - const routerSlotEl = this.shadowRoot?.querySelector('router-slot') as RouterSlot; - if (routerSlotEl) { - routerSlotEl.render(); - } - } - private async _onSave() { // TODO: What if store is not present, what if node is not loaded.... if (!this._dataTypeStore) return; @@ -189,36 +92,37 @@ export class UmbEditorDataTypeElement extends UmbContextConsumerMixin(LitElement } } + // TODO. find a way where we don't have to do this for all editors. + private _handleInput(event: UUIInputEvent) { + if (event instanceof UUIInputEvent) { + const target = event.composedPath()[0] as UUIInputElement; + this._dataTypeContext?.update({ name: target.value }); + } + } + + disconnectedCallback(): void { + super.disconnectedCallback(); + this._dataTypeStoreSubscription?.unsubscribe(); + this._dataTypeContextSubscription?.unsubscribe(); + } + render() { return html` - - - - - ${this._editorViews.map( - (view: UmbExtensionManifestEditorView) => html` - - - ${view.name} - - ` - )} - - - - -
- -
-
+ ${this._dataType + ? html` + + +
+ +
+
+ ` + : nothing} `; } } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/editors/editor-node.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/editors/editor-node.element.ts index 8ab75482f4..74d692224e 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/editors/editor-node.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/editors/editor-node.element.ts @@ -214,7 +214,7 @@ export class UmbEditorNodeElement extends UmbContextConsumerMixin(LitElement) { return html` - + ${this._editorViews.map( (view: UmbExtensionManifestEditorView) => html`