initial property context concept
This commit is contained in:
@@ -10,6 +10,11 @@ const isDataTypeDetails = (dataType: DataTypeDetails | FolderTreeItem): dataType
|
||||
// TODO: can we make is easy to reuse store methods across different stores?
|
||||
|
||||
export type UmbDataTypeStoreItemType = DataTypeDetails | FolderTreeItem;
|
||||
|
||||
// TODO: research how we write names of global consts.
|
||||
export const STORE_ALIAS = 'umbDataTypeStore';
|
||||
|
||||
|
||||
/**
|
||||
* @export
|
||||
* @class UmbDataTypesStore
|
||||
@@ -18,7 +23,7 @@ export type UmbDataTypeStoreItemType = DataTypeDetails | FolderTreeItem;
|
||||
*/
|
||||
export class UmbDataTypeStore extends UmbDataStoreBase<UmbDataTypeStoreItemType> {
|
||||
|
||||
public readonly storeAlias = 'umbDataTypeStore';
|
||||
public readonly storeAlias = STORE_ALIAS;
|
||||
|
||||
/**
|
||||
* @description - Request a Data Type by key. The Data Type is added to the store and is returned as an Observable.
|
||||
|
||||
@@ -2,11 +2,9 @@ import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
|
||||
import { css, html } from 'lit';
|
||||
import { ifDefined } from 'lit-html/directives/if-defined.js';
|
||||
import { customElement, property, state } from 'lit/decorators.js';
|
||||
import { EMPTY, of, switchMap } from 'rxjs';
|
||||
|
||||
import { UmbDataTypeStore } from '../../../settings/data-types/data-type.store';
|
||||
import type { ContentProperty, ManifestTypes } from '@umbraco-cms/models';
|
||||
import { umbExtensionsRegistry } from '@umbraco-cms/extensions-registry';
|
||||
import type { ContentProperty } from '@umbraco-cms/models';
|
||||
|
||||
import '../entity-property/entity-property.element';
|
||||
import { UmbLitElement } from '@umbraco-cms/element';
|
||||
@@ -33,7 +31,7 @@ export class UmbContentPropertyElement extends UmbLitElement {
|
||||
}
|
||||
|
||||
@property()
|
||||
value?: string;
|
||||
value?: object;
|
||||
|
||||
@state()
|
||||
private _propertyEditorUIAlias?: string;
|
||||
@@ -56,17 +54,10 @@ export class UmbContentPropertyElement extends UmbLitElement {
|
||||
if (!this._dataTypeStore || !this._property) return;
|
||||
|
||||
this.observe(
|
||||
this._dataTypeStore.getByKey(this._property.dataTypeKey).pipe(
|
||||
switchMap((dataType) => {
|
||||
if (!dataType?.propertyEditorUIAlias) return EMPTY;
|
||||
this._dataTypeData = dataType.data;
|
||||
return umbExtensionsRegistry.getByAlias(dataType.propertyEditorUIAlias) ?? of(null);
|
||||
})
|
||||
),
|
||||
(manifest) => {
|
||||
if (manifest?.type === 'propertyEditorUI') {
|
||||
this._propertyEditorUIAlias = manifest.alias;
|
||||
}
|
||||
this._dataTypeStore.getByKey(this._property.dataTypeKey),
|
||||
(dataType) => {
|
||||
this._dataTypeData = dataType?.data;
|
||||
this._propertyEditorUIAlias = dataType?.propertyEditorUIAlias || undefined;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
|
||||
import { css, html, PropertyValueMap } from 'lit';
|
||||
import { css, html } from 'lit';
|
||||
import { customElement, property, state } from 'lit/decorators.js';
|
||||
import { ifDefined } from 'lit-html/directives/if-defined.js';
|
||||
import { UmbWorkspacePropertyContext } from './workspace-property.context';
|
||||
import { createExtensionElement } from '@umbraco-cms/extensions-api';
|
||||
import { umbExtensionsRegistry } from '@umbraco-cms/extensions-registry';
|
||||
import type { ManifestPropertyEditorUI, ManifestTypes } from '@umbraco-cms/models';
|
||||
import type { DataTypePropertyData, ManifestPropertyEditorUI, ManifestTypes } from '@umbraco-cms/models';
|
||||
|
||||
import '../../property-actions/shared/property-action-menu/property-action-menu.element';
|
||||
import 'src/backoffice/shared/components/workspace/workspace-property-layout/workspace-property-layout.element';
|
||||
@@ -48,6 +49,12 @@ export class UmbEntityPropertyElement extends UmbLitElement {
|
||||
`,
|
||||
];
|
||||
|
||||
@state()
|
||||
private _label?:string;
|
||||
|
||||
@state()
|
||||
private _description?:string;
|
||||
|
||||
/**
|
||||
* Label. Name of the property
|
||||
* @type {string}
|
||||
@@ -55,7 +62,9 @@ export class UmbEntityPropertyElement extends UmbLitElement {
|
||||
* @default ''
|
||||
*/
|
||||
@property({ type: String })
|
||||
public label = '';
|
||||
public set label(label: string) {
|
||||
this._propertyContext.setLabel(label);
|
||||
}
|
||||
|
||||
/**
|
||||
* Description: render a description underneath the label.
|
||||
@@ -64,7 +73,9 @@ export class UmbEntityPropertyElement extends UmbLitElement {
|
||||
* @default ''
|
||||
*/
|
||||
@property({ type: String })
|
||||
public description = '';
|
||||
public set description(description: string) {
|
||||
this._propertyContext.setDescription(description);
|
||||
}
|
||||
|
||||
/**
|
||||
* Alias
|
||||
@@ -74,7 +85,9 @@ export class UmbEntityPropertyElement extends UmbLitElement {
|
||||
* @default ''
|
||||
*/
|
||||
@property({ type: String })
|
||||
public alias = '';
|
||||
public set alias(alias: string) {
|
||||
this._propertyContext.setAlias(alias);
|
||||
}
|
||||
|
||||
/**
|
||||
* Property Editor UI Alias. Render the Property Editor UI registered for this alias.
|
||||
@@ -101,7 +114,9 @@ export class UmbEntityPropertyElement extends UmbLitElement {
|
||||
* @default ''
|
||||
*/
|
||||
@property({ type: Object, attribute: false })
|
||||
public value?: any;
|
||||
public set value(value: object) {
|
||||
this._propertyContext.setValue(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Config. Configuration to pass to the Property Editor UI. This is also the configuration data stored on the Data Type.
|
||||
@@ -111,15 +126,16 @@ export class UmbEntityPropertyElement extends UmbLitElement {
|
||||
* @default ''
|
||||
*/
|
||||
@property({ type: Object, attribute: false })
|
||||
public config?: any;
|
||||
public set config(value: DataTypePropertyData[]) {
|
||||
this._propertyContext.setConfig(value);
|
||||
}
|
||||
|
||||
// TODO: make interface for UMBPropertyEditorElement
|
||||
@state()
|
||||
private _element?: { value?: any; config?: any } & HTMLElement; // TODO: invent interface for propertyEditorUI.
|
||||
|
||||
|
||||
// TODO: How to get proper default value?
|
||||
private _propertyContext = new UmbWorkspacePropertyContext<string>("");
|
||||
private _propertyContext = new UmbWorkspacePropertyContext(this);
|
||||
|
||||
private propertyEditorUIObserver?: UmbObserverController<ManifestTypes>;
|
||||
|
||||
@@ -127,66 +143,73 @@ export class UmbEntityPropertyElement extends UmbLitElement {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.provideContext('umbPropertyContext', this._propertyContext);
|
||||
|
||||
this._observePropertyEditorUI();
|
||||
|
||||
this.observe(this._propertyContext.label, (label) => {
|
||||
console.log("_propertyContext replied with label", label)
|
||||
this._label = label;
|
||||
});
|
||||
this.observe(this._propertyContext.label, (description) => {
|
||||
this._description = description;
|
||||
});
|
||||
|
||||
// TODO: move event to context. maybe rename to property-editor-value-change.
|
||||
this.addEventListener('property-editor-change', this._onPropertyEditorChange as any as EventListener);
|
||||
|
||||
|
||||
}
|
||||
|
||||
private _observePropertyEditorUI() {
|
||||
this.propertyEditorUIObserver?.destroy();
|
||||
this.propertyEditorUIObserver = this.observe(umbExtensionsRegistry.getByTypeAndAlias('propertyEditorUI', this.propertyEditorUIAlias), (manifest) => {
|
||||
this._gotEditor(manifest);
|
||||
this._gotEditorUI(manifest);
|
||||
});
|
||||
}
|
||||
|
||||
private _gotEditor(propertyEditorUIManifest?: ManifestPropertyEditorUI | null) {
|
||||
if (!propertyEditorUIManifest) {
|
||||
// TODO: if dataTypeKey didn't exist in store, we should do some nice UI.
|
||||
private _gotEditorUI(manifest?: ManifestPropertyEditorUI | null) {
|
||||
if (!manifest) {
|
||||
// TODO: if propertyEditorUIAlias didn't exist in store, we should do some nice fail UI.
|
||||
return;
|
||||
}
|
||||
|
||||
createExtensionElement(propertyEditorUIManifest)
|
||||
createExtensionElement(manifest)
|
||||
.then((el) => {
|
||||
const oldValue = this._element;
|
||||
this._element = el;
|
||||
|
||||
if (this._element) {
|
||||
this._element.value = this.value; // Be aware its duplicated code
|
||||
this._element.config = this.config; // Be aware its duplicated code
|
||||
}
|
||||
this.observe(this._propertyContext.value, (value) => {
|
||||
if(this._element) {
|
||||
this._element.value = value;
|
||||
}
|
||||
});
|
||||
this.observe(this._propertyContext.config, (config) => {
|
||||
if(this._element) {
|
||||
this._element.config = config;
|
||||
}
|
||||
});
|
||||
|
||||
this.requestUpdate('element', oldValue);
|
||||
|
||||
})
|
||||
.catch(() => {
|
||||
// TODO: loading JS failed so we should do some nice UI. (This does only happen if extension has a js prop, otherwise we concluded that no source was needed resolved the load.)
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
private _onPropertyEditorChange = (e: CustomEvent) => {
|
||||
const target = e.composedPath()[0] as any;
|
||||
this.value = target.value;
|
||||
|
||||
// TODO: update context.
|
||||
|
||||
this.dispatchEvent(new CustomEvent('property-value-change', { bubbles: true, composed: true }));
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
/** Lit does not currently handle dynamic tag names, therefor we are doing some manual rendering */
|
||||
// TODO: Refactor into a base class for dynamic-tag element? we will be using this a lot for extensions.
|
||||
// This could potentially hook into Lit and parse all properties defined in the specific class on to the dynamic-element. (see static elementProperties: PropertyDeclarationMap;)
|
||||
willUpdate(changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>) {
|
||||
super.willUpdate(changedProperties);
|
||||
|
||||
if (changedProperties.has('value') && this._element) {
|
||||
this._element.value = this.value; // Be aware its duplicated code
|
||||
}
|
||||
|
||||
if (changedProperties.has('config') && this._element) {
|
||||
this._element.config = this.config; // Be aware its duplicated code
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<umb-workspace-property-layout id="layout" label="${this.label}" description="${this.description}">
|
||||
<umb-workspace-property-layout id="layout" label="${ifDefined(this._label)}" description="${ifDefined(this._description)}">
|
||||
${this._renderPropertyActionMenu()}
|
||||
<div slot="editor">${this._element}</div>
|
||||
</umb-workspace-property-layout>
|
||||
@@ -198,7 +221,7 @@ export class UmbEntityPropertyElement extends UmbLitElement {
|
||||
? html`<umb-property-action-menu
|
||||
slot="property-action-menu"
|
||||
id="property-action-menu"
|
||||
.propertyEditorUIAlias="${this.propertyEditorUIAlias}"
|
||||
.propertyEditorUIAlias="${this._propertyEditorUIAlias}"
|
||||
.value="${this.value}"></umb-property-action-menu>`
|
||||
: ''}`;
|
||||
}
|
||||
|
||||
@@ -1,60 +1,114 @@
|
||||
import { BehaviorSubject, Observable } from "rxjs";
|
||||
import { distinctUntilChanged, map, Observable, of, shareReplay } from "rxjs";
|
||||
import type { DataTypeDetails } from "@umbraco-cms/models";
|
||||
import { UmbControllerHostInterface } from "src/core/controller/controller-host.mixin";
|
||||
import { naiveObjectComparison, UniqueBehaviorSubject } from "src/core/observable-api/unique-behavior-subject";
|
||||
import { UmbContextProviderController } from "src/core/context-api/provide/context-provider.controller";
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
//TODO: Property-Context: move these methods out:
|
||||
type MappingFunction<T, R> = (mappable: T) => R;
|
||||
type MemoizationFunction<R> = (previousResult: R, currentResult: R) => boolean;
|
||||
|
||||
function defaultMemoization(previousValue: any, currentValue: any): boolean {
|
||||
if (typeof previousValue === 'object' && typeof currentValue === 'object') {
|
||||
return naiveObjectComparison(previousValue, currentValue);
|
||||
}
|
||||
return previousValue === currentValue;
|
||||
}
|
||||
//TODO: Property-Context: rename this method.
|
||||
export function select$<T, R> (
|
||||
source$: Observable<T>,
|
||||
mappingFunction: MappingFunction<T, R>,
|
||||
memoizationFunction?: MemoizationFunction<R>
|
||||
): Observable<R> {
|
||||
return source$.pipe(
|
||||
map(mappingFunction),
|
||||
distinctUntilChanged(memoizationFunction || defaultMemoization),
|
||||
shareReplay(1) // TODO: investigate what happens if this was removed.
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
export type WorkspacePropertyData<ValueType> = {
|
||||
alias?: string | null;
|
||||
label?: string | null;
|
||||
value?: ValueType | null;
|
||||
alias?: string;
|
||||
label?: string;
|
||||
description?: string;
|
||||
value?: ValueType | null;
|
||||
config?: DataTypeDetails['data'];// This could potentially then come from hardcoded JS object and not the DataType store.
|
||||
};
|
||||
|
||||
|
||||
|
||||
export class UmbWorkspacePropertyContext<ValueType> {
|
||||
|
||||
//private _host: UmbControllerHostInterface;
|
||||
|
||||
private _data: BehaviorSubject<WorkspacePropertyData<ValueType>>;
|
||||
public readonly data: Observable<WorkspacePropertyData<ValueType>>;
|
||||
private _data: UniqueBehaviorSubject<WorkspacePropertyData<ValueType>>;
|
||||
|
||||
public readonly alias: Observable<WorkspacePropertyData<ValueType>['alias']>;
|
||||
public readonly label: Observable<WorkspacePropertyData<ValueType>['label']>;
|
||||
public readonly description: Observable<WorkspacePropertyData<ValueType>['description']>;
|
||||
public readonly value: Observable<WorkspacePropertyData<ValueType>['value']>;
|
||||
public readonly config: Observable<WorkspacePropertyData<ValueType>['config']>;
|
||||
|
||||
|
||||
#defaultValue!: ValueType | null;
|
||||
constructor(host:UmbControllerHostInterface) {
|
||||
|
||||
|
||||
constructor(defaultValue: ValueType | null) {
|
||||
|
||||
this.#defaultValue = defaultValue;
|
||||
//this._host = host;
|
||||
|
||||
// TODO: How do we connect this value with parent context?
|
||||
// Ensuring the property editor value-property is updated...
|
||||
// How about consuming a workspace context? When received maybe assuming these will fit or test if it likes to accept this property..
|
||||
|
||||
this._data = new BehaviorSubject({value: defaultValue} as WorkspacePropertyData<ValueType>);
|
||||
this.data = this._data.asObservable();
|
||||
this._data = new UniqueBehaviorSubject({} as WorkspacePropertyData<ValueType>);
|
||||
|
||||
this.alias = select$(this._data, data => data.alias);
|
||||
this.label = select$(this._data, data => data.label);
|
||||
this.description = select$(this._data, data => data.description);
|
||||
this.value = select$(this._data, data => data.value);
|
||||
this.config = select$(this._data, data => data.config);
|
||||
|
||||
|
||||
new UmbContextProviderController(host, 'umbPropertyContext', this);
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
hostConnected() {
|
||||
|
||||
}
|
||||
|
||||
hostDisconnected() {
|
||||
|
||||
}
|
||||
*/
|
||||
|
||||
public getData() {
|
||||
/*public getData() {
|
||||
return this._data.getValue();
|
||||
}*/
|
||||
|
||||
|
||||
|
||||
public update(data: Partial<WorkspacePropertyData<ValueType>>) {
|
||||
this._data.next({ ...this._data.getValue(), ...data });
|
||||
}
|
||||
|
||||
|
||||
public update(data: Partial<WorkspacePropertyData<ValueType>>) {
|
||||
this._data.next({ ...this.getData(), ...data });
|
||||
public setAlias(alias: WorkspacePropertyData<ValueType>['alias']) {
|
||||
this.update({alias: alias});
|
||||
}
|
||||
public setLabel(label: WorkspacePropertyData<ValueType>['label']) {
|
||||
this.update({label: label});
|
||||
}
|
||||
public setDescription(description: WorkspacePropertyData<ValueType>['description']) {
|
||||
this.update({description: description});
|
||||
}
|
||||
public setValue(value: WorkspacePropertyData<ValueType>['value']) {
|
||||
this.update({value: value});
|
||||
}
|
||||
public setConfig(config: WorkspacePropertyData<ValueType>['config']) {
|
||||
this.update({config: config});
|
||||
}
|
||||
|
||||
public resetValue() {
|
||||
console.log("property context reset")
|
||||
|
||||
this.update({value: this.#defaultValue})
|
||||
this.update({value: null});
|
||||
}
|
||||
|
||||
|
||||
// TODO: how can we make sure to call this.
|
||||
public destroy(): void {
|
||||
this._data.unsubscribe();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { css, html } from 'lit';
|
||||
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
import { UmbWorkspacePropertyContext } from 'src/backoffice/shared/components/entity-property/workspace-property.context';
|
||||
import type { UmbWorkspacePropertyContext } from 'src/backoffice/shared/components/entity-property/workspace-property.context';
|
||||
import { UmbLitElement } from '@umbraco-cms/element';
|
||||
|
||||
@customElement('umb-property-editor-ui-textarea')
|
||||
@@ -26,8 +26,11 @@ export class UmbPropertyEditorUITextareaElement extends UmbLitElement {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.consumeContext('umbPropertyContext', (instance) => {
|
||||
this.consumeContext('umbPropertyContext', (instance: UmbWorkspacePropertyContext<string>) => {
|
||||
this.propertyContext = instance;
|
||||
this.observe(this.propertyContext.value, (value) => {
|
||||
console.log("Context says value changed", value)
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -40,7 +43,8 @@ export class UmbPropertyEditorUITextareaElement extends UmbLitElement {
|
||||
return html`
|
||||
<uui-textarea .value=${this.value} @input=${this.onInput}></uui-textarea>
|
||||
${this.config?.map((property: any) => html`<div>${property.alias}: ${property.value}</div>`)}
|
||||
<button @click=${() => this.propertyContext?.resetValue()}>Reset</button>`;
|
||||
<button @click=${() => this.propertyContext?.resetValue()}>Reset</button>
|
||||
<button @click=${() => this.propertyContext?.setLabel('random' + Math.random()*10)}>Label change</button>`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ export const data: Array<DataTypeDetails> = [
|
||||
parentKey: null,
|
||||
isFolder: false,
|
||||
propertyEditorModelAlias: 'Umbraco.TextArea',
|
||||
propertyEditorUIAlias: 'Umb.PropertyEditorUI.Textarea',
|
||||
propertyEditorUIAlias: 'Umb.PropertyEditorUI.TextArea',
|
||||
data: [
|
||||
{
|
||||
alias: 'maxChars',
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
|
||||
|
||||
function deepFreeze<T>(inObj: T): T {
|
||||
Object.freeze(inObj);
|
||||
|
||||
Object.getOwnPropertyNames(inObj).forEach(function (prop) {
|
||||
// eslint-disable-next-line no-prototype-builtins
|
||||
if ((inObj as any).hasOwnProperty(prop)
|
||||
&& (inObj as any)[prop] != null
|
||||
&& typeof (inObj as any)[prop] === 'object'
|
||||
&& !Object.isFrozen((inObj as any)[prop])) {
|
||||
deepFreeze((inObj as any)[prop]);
|
||||
}
|
||||
});
|
||||
return inObj;
|
||||
}
|
||||
|
||||
|
||||
export function naiveObjectComparison(objOne: any, objTwo: any): boolean {
|
||||
return JSON.stringify(objOne) === JSON.stringify(objTwo);
|
||||
}
|
||||
|
||||
|
||||
|
||||
export class UniqueBehaviorSubject<T> extends BehaviorSubject<T> {
|
||||
constructor(initialData: T) {
|
||||
super(deepFreeze(initialData));
|
||||
}
|
||||
|
||||
next(newData: T): void {
|
||||
const frozenData = deepFreeze(newData);
|
||||
if (!naiveObjectComparison(frozenData, this.getValue())) {
|
||||
super.next(frozenData);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user