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 index fe24d54bf2..97bd186b73 100644 --- 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 @@ -17,14 +17,32 @@ class UmbEditorEntity extends UmbContextConsumerMixin(LitElement) { height: 100%; } + #header { + display: flex; + gap: 16px; + align-items: center; + } + + #footer { + display: flex; + height: 100%; + align-items: center; + gap: 16px; + } + + #actions { + display: block; + margin-left: auto; + } + 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); + border-right: 1px solid var(--uui-color-border); flex-wrap: nowrap; height: 60px; } @@ -126,27 +144,31 @@ class UmbEditorEntity extends UmbContextConsumerMixin(LitElement) { render() { return html` - - - - ${this._editorViews.map( - (view: UmbExtensionManifestEditorView) => html` - - - ${view.name} - - ` - )} - + - - + `; } 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 8b8d299dc1..00f8add784 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, property } from 'lit/decorators.js'; +import { customElement } from 'lit/decorators.js'; @customElement('umb-editor-layout') class UmbEditorLayout extends LitElement { @@ -24,11 +24,9 @@ class UmbEditorLayout extends LitElement { #header { background-color: var(--uui-color-surface); width: 100%; - display: flex; - flex: none; - gap: 16px; - align-items: center; border-bottom: 1px solid var(--uui-color-border); + box-sizing: border-box; + padding: 0 var(--uui-size-6); } #main { @@ -40,14 +38,9 @@ class UmbEditorLayout extends LitElement { } #footer { - display: flex; - flex: none; - justify-content: end; - align-items: center; height: 70px; width: 100%; - gap: 16px; - padding-right: 24px; + padding: 0 var(--uui-size-6); border-top: 1px solid var(--uui-color-border); background-color: var(--uui-color-surface); box-sizing: border-box; @@ -59,15 +52,14 @@ class UmbEditorLayout extends LitElement { return html`
`; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/editor-views/editor-view-node-edit.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/editor-views/editor-view-node-edit.element.ts index 874943398a..c850d3c92f 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/editor-views/editor-view-node-edit.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/editor-views/editor-view-node-edit.element.ts @@ -1,10 +1,13 @@ import { css, html, LitElement } from 'lit'; import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; -import { customElement, property } from 'lit/decorators.js'; -import { DocumentNode, NodeProperty } from '../../mocks/data/content.data'; +import { customElement, state } from 'lit/decorators.js'; +import { NodeEntity, NodeProperty } from '../../mocks/data/content.data'; +import { UmbContextConsumerMixin } from '../../core/context'; +import { UmbNodeContext } from '../editors/node/node.context'; +import { Subscription, distinctUntilChanged } from 'rxjs'; @customElement('umb-editor-view-node-edit') -export class UmbEditorViewNodeEdit extends LitElement { +export class UmbEditorViewNodeEdit extends UmbContextConsumerMixin(LitElement) { static styles = [ UUITextStyles, css` @@ -16,17 +19,40 @@ export class UmbEditorViewNodeEdit extends LitElement { `, ]; - @property({ type: Object }) - node?: DocumentNode; + @state() + _node?: NodeEntity; + + private _nodeContext?: UmbNodeContext; + private _nodeContextSubscription?: Subscription; + + constructor() { + super(); + + this.consumeContext('umbNodeContext', (nodeContext) => { + this._nodeContext = nodeContext; + this._useNode(); + }); + } + + private _useNode() { + this._nodeContextSubscription = this._nodeContext?.data.pipe(distinctUntilChanged()).subscribe((data) => { + this._node = data; + }); + } + + disconnectedCallback(): void { + super.disconnectedCallback(); + this._nodeContextSubscription?.unsubscribe(); + } render() { return html` - ${this.node?.properties.map( + ${this._node?.properties.map( (property: NodeProperty) => html` data.alias === property.alias)?.value}> + .value=${this._node?.data.find((data) => data.alias === property.alias)?.value}>
` )} 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 index a9ed0c2151..f114f92d1c 100644 --- 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 @@ -17,7 +17,7 @@ export class UmbDataTypeContext { } // TODO: figure out how we want to update data - public update(data: any) { + public update(data: Partial) { this._data.next({ ...this._data.getValue(), ...data }); } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/editors/document-type/document-type.context.ts b/src/Umbraco.Web.UI.Client/src/backoffice/editors/document-type/document-type.context.ts index 4d04464440..600ec7ab2f 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/editors/document-type/document-type.context.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/editors/document-type/document-type.context.ts @@ -17,7 +17,7 @@ export class UmbDocumentTypeContext { } // TODO: figure out how we want to update data - public update(data: any) { + public update(data: Partial) { this._data.next({ ...this._data.getValue(), ...data }); } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/editors/document-type/editor-document-type.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/editors/document-type/editor-document-type.element.ts index beaf73816d..d9284e1d8f 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/editors/document-type/editor-document-type.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/editors/document-type/editor-document-type.element.ts @@ -111,6 +111,10 @@ export class UmbEditorDocumentTypeElement extends UmbContextProviderMixin(UmbCon alias="Umb.Editor.DocumentType" name="${ifDefined(this._documentType?.name)}" @input="${this._handleInput}"> +
Icon
+ +
Keyboard Shortcuts
+
= []; - - @state() - private _editorViews: Array = []; - - @state() - private _currentView = ''; + _node?: NodeEntity; private _nodeStore?: UmbNodeStore; - private _nodeSubscription?: Subscription; + private _nodeStoreSubscription?: Subscription; + + private _nodeContext = new UmbNodeContext(); + private _nodeContextSubscription?: Subscription; private _notificationService?: UmbNotificationService; - private _extensionRegistry?: UmbExtensionRegistry; - private _editorViewsSubscription?: Subscription; - - private _routerFolder = ''; - constructor() { super(); @@ -84,18 +74,9 @@ export class UmbEditorNodeElement extends UmbContextConsumerMixin(LitElement) { this._notificationService = service; }); - this.consumeContext('umbExtensionRegistry', (extensionRegistry: UmbExtensionRegistry) => { - this._extensionRegistry = extensionRegistry; - this._useEditorViews(); - }); - this.addEventListener('property-value-change', this._onPropertyValueChange); - } - connectedCallback(): void { - super.connectedCallback(); - /* TODO: find a way to construct absolute urls */ - this._routerFolder = window.location.pathname.split('/view')[0]; + this.provideContext('umbNodeContext', this._nodeContext); } private _onPropertyValueChange = (e: Event) => { @@ -119,34 +100,19 @@ export class UmbEditorNodeElement extends UmbContextConsumerMixin(LitElement) { } private _useNode() { - this._nodeSubscription?.unsubscribe(); + this._nodeStoreSubscription?.unsubscribe(); - this._nodeSubscription = this._nodeStore?.getById(parseInt(this.id)).subscribe((node) => { + this._nodeStoreSubscription = this._nodeStore?.getById(parseInt(this.id)).subscribe((node) => { if (!node) return; // TODO: Handle nicely if there is no node. - this._node = node; - // TODO: merge observables - this._createRoutes(); - }); - } - private _useEditorViews() { - this._editorViewsSubscription?.unsubscribe(); + this._nodeContextSubscription?.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.Node')) - .sort((a, b) => b.meta.weight - a.meta.weight) - ) - ) - .subscribe((editorViews) => { - this._editorViews = editorViews; - // TODO: merge observables - this._createRoutes(); + this._nodeContext?.update(node); + + this._nodeContextSubscription = this._nodeContext.data.pipe(distinctUntilChanged()).subscribe((data) => { + this._node = data; }); + }); } private _onSaveAndPublish() { @@ -166,69 +132,17 @@ export class UmbEditorNodeElement extends UmbContextConsumerMixin(LitElement) { this._onSave(); } - // TODO: simplify setting up editors with views. This code has to be duplicated in each editor. - private async _createRoutes() { - if (this._node && 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.node = this._node; - 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(); - } - } - disconnectedCallback(): void { super.disconnectedCallback(); - this._nodeSubscription?.unsubscribe(); + this._nodeStoreSubscription?.unsubscribe(); + this._nodeContextSubscription?.unsubscribe(); delete this._node; } render() { return html` - - - - ${this._editorViews.map( - (view: UmbExtensionManifestEditorView) => html` - - - ${view.name} - - ` - )} - - - + +
Breadcrumbs
@@ -239,7 +153,7 @@ export class UmbEditorNodeElement extends UmbContextConsumerMixin(LitElement) { color="positive" label="Save and publish">
-
+ `; } } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/editors/node/node.context.ts b/src/Umbraco.Web.UI.Client/src/backoffice/editors/node/node.context.ts new file mode 100644 index 0000000000..530c212831 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/editors/node/node.context.ts @@ -0,0 +1,42 @@ +import { BehaviorSubject, Observable } from 'rxjs'; +import { NodeEntity } from '../../../mocks/data/content.data'; + +export class UmbNodeContext { + // TODO: figure out how fine grained we want to make our observables. + private _data: BehaviorSubject = new BehaviorSubject({ + id: -1, + key: '', + name: '', + alias: '', + icon: '', + properties: [ + { + alias: '', + label: '', + description: '', + dataTypeKey: '', + }, + ], + data: [ + { + alias: '', + value: '', + }, + ], + }); + public readonly data: Observable = this._data.asObservable(); + + constructor(node?: NodeEntity) { + if (!node) return; + this._data.next(node); + } + + // TODO: figure out how we want to update data + public update(data: Partial) { + this._data.next({ ...this._data.getValue(), ...data }); + } + + public getData() { + return this._data.getValue(); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/core/stores/node.store.ts b/src/Umbraco.Web.UI.Client/src/core/stores/node.store.ts index bd7a0cf961..da25798be4 100644 --- a/src/Umbraco.Web.UI.Client/src/core/stores/node.store.ts +++ b/src/Umbraco.Web.UI.Client/src/core/stores/node.store.ts @@ -1,11 +1,11 @@ import { BehaviorSubject, map, Observable } from 'rxjs'; -import { DocumentNode } from '../../mocks/data/content.data'; +import { NodeEntity } from '../../mocks/data/content.data'; export class UmbNodeStore { - private _nodes: BehaviorSubject> = new BehaviorSubject(>[]); - public readonly nodes: Observable> = this._nodes.asObservable(); + private _nodes: BehaviorSubject> = new BehaviorSubject(>[]); + public readonly nodes: Observable> = this._nodes.asObservable(); - getById(id: number): Observable { + getById(id: number): Observable { // fetch from server and update store fetch(`/umbraco/backoffice/content/${id}`) .then((res) => res.json()) @@ -13,14 +13,12 @@ export class UmbNodeStore { this._updateStore(data); }); - return this.nodes.pipe( - map((nodes: Array) => nodes.find((node: DocumentNode) => node.id === id) || null) - ); + return this.nodes.pipe(map((nodes: Array) => nodes.find((node: NodeEntity) => node.id === id) || null)); } // TODO: Use Node type, to not be specific about Document. // TODO: make sure UI somehow can follow the status of this action. - save(data: DocumentNode[]): Promise { + save(data: NodeEntity[]): Promise { // fetch from server and update store // TODO: use Fetcher API. let body: string; @@ -48,7 +46,7 @@ export class UmbNodeStore { private _updateStore(fetchedNodes: Array) { const storedNodes = this._nodes.getValue(); - const updated: DocumentNode[] = [...storedNodes]; + const updated: NodeEntity[] = [...storedNodes]; fetchedNodes.forEach((fetchedNode) => { const index = storedNodes.map((storedNode) => storedNode.id).indexOf(fetchedNode.id); diff --git a/src/Umbraco.Web.UI.Client/src/mocks/data/content.data.ts b/src/Umbraco.Web.UI.Client/src/mocks/data/content.data.ts index 8a4a483e18..0436ce9a54 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/data/content.data.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/data/content.data.ts @@ -1,6 +1,6 @@ import { UmbData } from './data'; -export interface DocumentNode { +export interface NodeEntity { id: number; key: string; name: string; @@ -20,7 +20,7 @@ export interface NodeProperty { export interface NodePropertyData { alias: string; - value: unknown; + value: any; } /* TODO: @@ -30,7 +30,7 @@ We would like the tree items to stay up to date, without requesting the server a If we split entityData into its own object, then that could go in the entityStore and be merged with the nodeStore (we would have a subscription on both). */ -export const data: Array = [ +export const data: Array = [ { id: 1, key: '74e4008a-ea4f-4793-b924-15e02fd380d1', @@ -135,7 +135,7 @@ export const data: Array = [ ]; // Temp mocked database -class UmbContentData extends UmbData { +class UmbContentData extends UmbData { constructor() { super(data); } 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 7159c0056f..ba97880e59 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,6 +1,6 @@ import { rest } from 'msw'; -import { DocumentNode, umbContentData } from '../data/content.data'; +import { NodeEntity, umbContentData } from '../data/content.data'; // TODO: add schema export const handlers = [ @@ -14,7 +14,7 @@ export const handlers = [ return res(ctx.status(200), ctx.json([document])); }), - rest.post('/umbraco/backoffice/content/save', (req, res, ctx) => { + rest.post('/umbraco/backoffice/content/save', (req, res, ctx) => { const data = req.body; if (!data) return;