move name + views slot from editor layout to entity editor + use entity editor in node editor

This commit is contained in:
Mads Rasmussen
2022-08-05 13:36:46 +02:00
parent 7ce5ce6cbe
commit f59b1e6d62
11 changed files with 166 additions and 168 deletions

View File

@@ -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`
<umb-editor-layout>
<uui-input slot="name" .value="${this.name}"></uui-input>
<uui-tab-group slot="views">
${this._editorViews.map(
(view: UmbExtensionManifestEditorView) => html`
<uui-tab
.label="${view.name}"
href="${this._routerFolder}/view/${view.meta.pathname}"
?active="${this._currentView.includes(view.meta.pathname)}">
<uui-icon slot="icon" name="${view.meta.icon}"></uui-icon>
${view.name}
</uui-tab>
`
)}
</uui-tab-group>
<div id="header" slot="header">
<slot id="icon" name="icon"></slot>
<uui-input .value="${this.name}"></uui-input>
<uui-tab-group slot="views">
${this._editorViews.map(
(view: UmbExtensionManifestEditorView) => html`
<uui-tab
.label="${view.name}"
href="${this._routerFolder}/view/${view.meta.pathname}"
?active="${this._currentView.includes(view.meta.pathname)}">
<uui-icon slot="icon" name="${view.meta.icon}"></uui-icon>
${view.name}
</uui-tab>
`
)}
</uui-tab-group>
</div>
<router-slot .routes="${this._routes}"></router-slot>
<slot></slot>
<slot name="actions" slot="actions"></slot>
<div id="footer" slot="footer">
<slot name="footer"></slot>
<slot id="actions" name="actions"></slot>
</div>
</umb-editor-layout>
`;
}

View File

@@ -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`
<div id="editor-frame">
<div id="header">
<slot name="name"></slot>
<slot name="views"></slot>
<slot name="header"></slot>
</div>
<uui-scroll-container id="main">
<slot></slot>
</uui-scroll-container>
<div id="footer">
<!-- only show footer if slot has elements -->
<slot name="actions"></slot>
<slot name="footer"></slot>
</div>
</div>
`;

View File

@@ -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`
<uui-box>
${this.node?.properties.map(
${this._node?.properties.map(
(property: NodeProperty) => html`
<umb-node-property
.property=${property}
.value=${this.node?.data.find((data) => data.alias === property.alias)?.value}></umb-node-property>
.value=${this._node?.data.find((data) => data.alias === property.alias)?.value}></umb-node-property>
<hr />
`
)}

View File

@@ -17,7 +17,7 @@ export class UmbDataTypeContext {
}
// TODO: figure out how we want to update data
public update(data: any) {
public update(data: Partial<DataTypeEntity>) {
this._data.next({ ...this._data.getValue(), ...data });
}

View File

@@ -17,7 +17,7 @@ export class UmbDocumentTypeContext {
}
// TODO: figure out how we want to update data
public update(data: any) {
public update(data: Partial<DocumentTypeEntity>) {
this._data.next({ ...this._data.getValue(), ...data });
}

View File

@@ -111,6 +111,10 @@ export class UmbEditorDocumentTypeElement extends UmbContextProviderMixin(UmbCon
alias="Umb.Editor.DocumentType"
name="${ifDefined(this._documentType?.name)}"
@input="${this._handleInput}">
<div slot="icon">Icon</div>
<div slot="footer">Keyboard Shortcuts</div>
<!-- TODO: these could be extensions points too -->
<div slot="actions">
<uui-button

View File

@@ -1,13 +1,14 @@
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 { UmbContextConsumerMixin, UmbContextProviderMixin } from '../../core/context';
import { UmbNodeStore } from '../../core/stores/node.store';
import { map, Subscription } from 'rxjs';
import { DocumentNode } from '../../mocks/data/content.data';
import { distinctUntilChanged, Subscription } from 'rxjs';
import { NodeEntity } from '../../mocks/data/content.data';
import { UmbNotificationService } from '../../core/services/notification.service';
import { UmbExtensionManifestEditorView, UmbExtensionRegistry } from '../../core/extension';
import { IRoute, IRoutingInfo, RouterSlot } from 'router-slot';
import { UmbNodeContext } from './node/node.context';
import '../components/editor-entity.element';
// Lazy load
// TODO: Make this dynamic, use load-extensions method to loop over extensions for this node.
@@ -15,7 +16,7 @@ import '../editor-views/editor-view-node-edit.element';
import '../editor-views/editor-view-node-info.element';
@customElement('umb-editor-node')
export class UmbEditorNodeElement extends UmbContextConsumerMixin(LitElement) {
export class UmbEditorNodeElement extends UmbContextProviderMixin(UmbContextConsumerMixin(LitElement)) {
static styles = [
UUITextStyles,
css`
@@ -51,27 +52,16 @@ export class UmbEditorNodeElement extends UmbContextConsumerMixin(LitElement) {
id!: string;
@state()
_node?: DocumentNode;
@state()
private _routes: Array<IRoute> = [];
@state()
private _editorViews: Array<UmbExtensionManifestEditorView> = [];
@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`
<umb-editor-layout>
<uui-input slot="name" .value="${this._node?.name}"></uui-input>
<uui-tab-group slot="views">
${this._editorViews.map(
(view: UmbExtensionManifestEditorView) => html`
<uui-tab
.label="${view.name}"
href="${this._routerFolder}/view/${view.meta.pathname}"
?active="${this._currentView.includes(view.meta.pathname)}">
<uui-icon slot="icon" name="${view.meta.icon}"></uui-icon>
${view.name}
</uui-tab>
`
)}
</uui-tab-group>
<router-slot .routes="${this._routes}"></router-slot>
<umb-editor-entity alias="Umb.Editor.Node">
<div slot="footer">Breadcrumbs</div>
<div slot="actions">
<uui-button @click=${this._onSaveAndPreview} label="Save and preview"></uui-button>
@@ -239,7 +153,7 @@ export class UmbEditorNodeElement extends UmbContextConsumerMixin(LitElement) {
color="positive"
label="Save and publish"></uui-button>
</div>
</umb-editor-layout>
</umb-editor-entity>
`;
}
}

View File

@@ -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<NodeEntity> = new BehaviorSubject({
id: -1,
key: '',
name: '',
alias: '',
icon: '',
properties: [
{
alias: '',
label: '',
description: '',
dataTypeKey: '',
},
],
data: [
{
alias: '',
value: '',
},
],
});
public readonly data: Observable<NodeEntity> = 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<NodeEntity>) {
this._data.next({ ...this._data.getValue(), ...data });
}
public getData() {
return this._data.getValue();
}
}

View File

@@ -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<Array<DocumentNode>> = new BehaviorSubject(<Array<DocumentNode>>[]);
public readonly nodes: Observable<Array<DocumentNode>> = this._nodes.asObservable();
private _nodes: BehaviorSubject<Array<NodeEntity>> = new BehaviorSubject(<Array<NodeEntity>>[]);
public readonly nodes: Observable<Array<NodeEntity>> = this._nodes.asObservable();
getById(id: number): Observable<DocumentNode | null> {
getById(id: number): Observable<NodeEntity | null> {
// 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<DocumentNode>) => nodes.find((node: DocumentNode) => node.id === id) || null)
);
return this.nodes.pipe(map((nodes: Array<NodeEntity>) => 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<void> {
save(data: NodeEntity[]): Promise<void> {
// fetch from server and update store
// TODO: use Fetcher API.
let body: string;
@@ -48,7 +46,7 @@ export class UmbNodeStore {
private _updateStore(fetchedNodes: Array<any>) {
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);

View File

@@ -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<DocumentNode> = [
export const data: Array<NodeEntity> = [
{
id: 1,
key: '74e4008a-ea4f-4793-b924-15e02fd380d1',
@@ -135,7 +135,7 @@ export const data: Array<DocumentNode> = [
];
// Temp mocked database
class UmbContentData extends UmbData<DocumentNode> {
class UmbContentData extends UmbData<NodeEntity> {
constructor() {
super(data);
}

View File

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