Merge remote-tracking branch 'origin/main' into bugfix/media-picker-type

This commit is contained in:
Jacob Overgaard
2024-03-06 10:36:35 +01:00
25 changed files with 453 additions and 340 deletions

View File

@@ -0,0 +1,6 @@
import type { UmbClassInterface } from './class.interface.js';
import type { UmbControllerHost, UmbController } from '@umbraco-cms/backoffice/controller-api';
export interface UmbClassMixinInterface extends UmbClassInterface, UmbController {
_host: UmbControllerHost;
}

View File

@@ -4,19 +4,63 @@ import type {
UmbContextProviderController,
UmbContextToken,
} from '../context-api/index.js';
import type { UmbControllerHost, UmbController } from '@umbraco-cms/backoffice/controller-api';
import type { UmbObserverController } from '@umbraco-cms/backoffice/observable-api';
import type { UmbControllerAlias, UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import type { ObserverCallback, UmbObserverController } from '@umbraco-cms/backoffice/observable-api';
import type { Observable } from '@umbraco-cms/backoffice/external/rxjs';
export interface UmbClassMixinInterface extends UmbControllerHost, UmbController {
observe<T>(
source: Observable<T> | { asObservable: () => Observable<T> },
callback: (_value: T) => void,
unique?: string,
): UmbObserverController<T>;
export interface UmbClassInterface extends UmbControllerHost {
/**
* @description Observe an Observable. An Observable is a declared source of data that can be observed. An observables is declared from a UmbState.
* @param {Observable<T>} source An Observable to observe from.
* @param {method} callback Callback method called when data is changed.
* @return {UmbObserverController} Reference to the created Observer Controller instance.
* @memberof UmbClassMixin
*/
observe<
ObservableType extends Observable<T>,
T,
SpecificT = ObservableType extends Observable<infer U>
? ObservableType extends undefined
? U | undefined
: U
: undefined,
R extends UmbObserverController<SpecificT> = UmbObserverController<SpecificT>,
SpecificR extends R | undefined = ObservableType extends undefined ? R | undefined : R,
>(
// This type dance checks if the Observable given could be undefined, if it potentially could be undefined it means that this potentially could return undefined and then call the callback with undefined. [NL]
source: ObservableType | undefined,
callback: ObserverCallback<SpecificT>,
controllerAlias?: UmbControllerAlias,
): SpecificR;
/**
* @description Provide a context API for this or child elements.
* @param {string} contextAlias
* @param {instance} instance The API instance to be exposed.
* @return {UmbContextProviderController} Reference to the created Context Provider Controller instance
* @memberof UmbClassMixin
*/
provideContext<R = unknown>(alias: string | UmbContextToken<R>, instance: R): UmbContextProviderController<R>;
/**
* @description Setup a subscription for a context. The callback is called when the context is resolved.
* @param {string} contextAlias
* @param {method} callback Callback method called when context is resolved.
* @return {UmbContextConsumerController} Reference to the created Context Consumer Controller instance
* @memberof UmbClassMixin
*/
consumeContext<BaseType = unknown, ResultType extends BaseType = BaseType>(
alias: string | UmbContextToken<BaseType, ResultType>,
callback: UmbContextCallback<ResultType>,
): UmbContextConsumerController<BaseType, ResultType>;
/**
* @description Retrieve a context. Notice this is a one time retrieving of a context, meaning if you expect this to be up to date with reality you should instead use the consumeContext method.
* @param {string} contextAlias
* @return {Promise<ContextType>} A Promise with the reference to the Context Api Instance
* @memberof UmbClassMixin
*/
getContext<BaseType = unknown, ResultType extends BaseType = BaseType>(
alias: string | UmbContextToken<BaseType, ResultType>,
): Promise<ResultType>;
}

View File

@@ -1,10 +1,9 @@
import type { UmbClassMixinInterface } from './class.interface.js';
import type { UmbClassMixinInterface } from './class-mixin.interface.js';
import type { Observable } from '@umbraco-cms/backoffice/external/rxjs';
import type { ClassConstructor } from '@umbraco-cms/backoffice/extension-api';
import {
type UmbControllerHost,
UmbControllerHostMixin,
type UmbController,
type UmbControllerAlias,
} from '@umbraco-cms/backoffice/controller-api';
import {
@@ -13,89 +12,16 @@ import {
UmbContextConsumerController,
UmbContextProviderController,
} from '@umbraco-cms/backoffice/context-api';
import { UmbObserverController } from '@umbraco-cms/backoffice/observable-api';
import { type ObserverCallback, UmbObserverController, simpleHashCode } from '@umbraco-cms/backoffice/observable-api';
type UmbClassMixinConstructor = new (
host: UmbControllerHost,
controllerAlias?: UmbControllerAlias,
) => UmbClassMixinDeclaration;
) => UmbClassMixinInterface;
// TODO: we need the interface from EventTarget to be part of the controller base. As a temp solution the UmbClassMixinDeclaration extends EventTarget.
declare class UmbClassMixinDeclaration extends EventTarget implements UmbClassMixinInterface {
_host: UmbControllerHost;
/**
* @description Observe a RxJS source of choice.
* @param {Observable<T>} source RxJS source
* @param {method} callback Callback method called when data is changed.
* @return {UmbObserverController} Reference to a Observer Controller instance
* @memberof UmbClassMixin
*/
observe<T>(
source: Observable<T>,
callback: (_value: T) => void,
controllerAlias?: UmbControllerAlias,
): UmbObserverController<T>;
/**
* @description Provide a context API for this or child elements.
* @param {string} contextAlias
* @param {instance} instance The API instance to be exposed.
* @return {UmbContextProviderController} Reference to a Context Provider Controller instance
* @memberof UmbClassMixin
*/
provideContext<
BaseType = unknown,
ResultType extends BaseType = BaseType,
InstanceType extends ResultType = ResultType,
>(
alias: string | UmbContextToken<BaseType, ResultType>,
instance: InstanceType,
): UmbContextProviderController<BaseType, ResultType, InstanceType>;
/**
* @description Setup a subscription for a context. The callback is called when the context is resolved.
* @param {string} contextAlias
* @param {method} callback Callback method called when context is resolved.
* @return {UmbContextConsumerController} Reference to a Context Consumer Controller instance
* @memberof UmbClassMixin
*/
consumeContext<BaseType = unknown, ResultType extends BaseType = BaseType>(
alias: string | UmbContextToken<BaseType, ResultType>,
callback: UmbContextCallback<ResultType>,
): UmbContextConsumerController<BaseType, ResultType>;
/**
* @description Retrieve a context. Notice this is a one time retrieving of a context, meaning if you expect this to be up to date with reality you should instead use the consumeContext method.
* @param {string} contextAlias
* @return {Promise<ContextType>} A Promise with the reference to the Context Api Instance
* @memberof UmbClassMixin
*/
getContext<BaseType = unknown, ResultType extends BaseType = BaseType>(
alias: string | UmbContextToken<BaseType, ResultType>,
): Promise<ResultType>;
hasController(controller: UmbController): boolean;
getControllers(filterMethod: (ctrl: UmbController) => boolean): UmbController[];
addController(controller: UmbController): void;
removeControllerByAlias(controllerAlias: UmbControllerAlias): void;
removeController(controller: UmbController): void;
getHostElement(): Element;
get controllerAlias(): UmbControllerAlias;
hostConnected(): void;
hostDisconnected(): void;
/**
* @description Destroys the controller and removes it from the host.
* @memberof UmbClassMixin
*/
destroy(): void;
}
export const UmbClassMixin = <T extends ClassConstructor>(superClass: T) => {
class UmbClassMixinClass extends UmbControllerHostMixin(superClass) implements UmbControllerHost {
protected _host: UmbControllerHost;
export const UmbClassMixin = <T extends ClassConstructor<EventTarget>>(superClass: T) => {
class UmbClassMixinClass extends UmbControllerHostMixin(superClass) implements UmbClassMixinInterface {
_host: UmbControllerHost;
protected _controllerAlias: UmbControllerAlias;
constructor(host: UmbControllerHost, controllerAlias?: UmbControllerAlias) {
@@ -113,12 +39,37 @@ export const UmbClassMixin = <T extends ClassConstructor>(superClass: T) => {
return this._controllerAlias;
}
observe<T>(
source: Observable<T>,
callback: (_value: T) => void,
observe<
ObservableType extends Observable<T>,
T,
SpecificT = ObservableType extends Observable<infer U>
? ObservableType extends undefined
? U | undefined
: U
: undefined,
R extends UmbObserverController<SpecificT> = UmbObserverController<SpecificT>,
SpecificR extends R | undefined = ObservableType extends undefined ? R | undefined : R,
>(
// This type dance checks if the Observable given could be undefined, if it potentially could be undefined it means that this potentially could return undefined and then call the callback with undefined. [NL]
source: ObservableType | undefined,
callback: ObserverCallback<SpecificT>,
controllerAlias?: UmbControllerAlias,
): UmbObserverController<T> {
return new UmbObserverController<T>(this, source, callback, controllerAlias);
): SpecificR {
// Fallback to use a hash of the provided method, but only if the alias is undefined.
controllerAlias ??= controllerAlias === undefined ? simpleHashCode(callback.toString()) : undefined;
if (source) {
return new UmbObserverController<T>(
this,
source,
callback as unknown as ObserverCallback<T>,
controllerAlias,
) as unknown as SpecificR;
} else {
callback(undefined as SpecificT);
this.removeControllerByAlias(controllerAlias);
return undefined as SpecificR;
}
}
provideContext<
@@ -159,5 +110,5 @@ export const UmbClassMixin = <T extends ClassConstructor>(superClass: T) => {
}
}
return UmbClassMixinClass as unknown as UmbClassMixinConstructor & UmbClassMixinDeclaration;
return UmbClassMixinClass as unknown as UmbClassMixinConstructor & T;
};

View File

@@ -1,9 +1,12 @@
import type { UmbController } from '../controller-api/controller.interface.js';
import { UmbClassMixin } from './class.mixin.js';
import type { ClassConstructor } from '@umbraco-cms/backoffice/extension-api';
/**
* This mixin enables a web-component to host controllers.
* This enables controllers to be added to the life cycle of this element.
*
*/
export abstract class UmbControllerBase extends UmbClassMixin(EventTarget) implements UmbController {}
export abstract class UmbControllerBase
extends UmbClassMixin<ClassConstructor<EventTarget>>(EventTarget)
implements UmbController {}

View File

@@ -1,4 +1,5 @@
export * from './class.interface.js';
export * from './class-mixin.interface.js';
export * from './class.mixin.js';
export * from './context.interface.js';
export * from './context-base.class.js';

View File

@@ -1,21 +1,7 @@
import type { UmbControllerAlias } from './controller-alias.type.js';
import { UmbControllerHostMixin } from './controller-host.mixin.js';
import type { UmbControllerHostElement } from './controller-host-element.interface.js';
import type { UmbController } from './controller.interface.js';
import type { UmbControllerHost } from './controller-host.interface.js';
import type { HTMLElementConstructor } from '@umbraco-cms/backoffice/extension-api';
export declare class UmbControllerHostImplementationElement extends HTMLElement implements UmbControllerHostElement {
hasController(controller: UmbController): boolean;
getControllers(filterMethod: (ctrl: UmbController) => boolean): UmbController[];
addController(controller: UmbController): void;
removeControllerByAlias(alias: UmbControllerAlias): void;
removeController(controller: UmbController): void;
getHostElement(): Element;
destroy(): void;
}
/**
* This mixin enables a web-component to host controllers.
* This enables controllers to be added to the life cycle of this element.
@@ -24,7 +10,10 @@ export declare class UmbControllerHostImplementationElement extends HTMLElement
* @mixin
*/
export const UmbControllerHostElementMixin = <T extends HTMLElementConstructor>(superClass: T) => {
class UmbControllerHostElementClass extends UmbControllerHostMixin(superClass) implements UmbControllerHost {
class UmbControllerHostElementClass
extends UmbControllerHostMixin<T>(superClass)
implements UmbControllerHostElement
{
getHostElement(): Element {
return this;
}
@@ -38,9 +27,11 @@ export const UmbControllerHostElementMixin = <T extends HTMLElementConstructor>(
super.disconnectedCallback?.();
this.hostDisconnected();
}
destroy(): void {}
}
return UmbControllerHostElementClass as unknown as HTMLElementConstructor<UmbControllerHostImplementationElement> & T;
return UmbControllerHostElementClass as unknown as HTMLElementConstructor<UmbControllerHostElement> & T;
};
declare global {

View File

@@ -2,13 +2,7 @@ import type { ClassConstructor } from '../extension-api/types/utils.js';
import type { UmbControllerHost } from './controller-host.interface.js';
import type { UmbController } from './controller.interface.js';
declare class UmbControllerHostBaseDeclaration implements Omit<UmbControllerHost, 'getHostElement'> {
hasController(controller: UmbController): boolean;
getControllers(filterMethod: (ctrl: UmbController) => boolean): UmbController[];
addController(controller: UmbController): void;
removeControllerByAlias(unique: UmbController['controllerAlias']): void;
removeController(controller: UmbController): void;
interface UmbControllerHostBaseDeclaration extends Omit<UmbControllerHost, 'getHostElement'> {
hostConnected(): void;
hostDisconnected(): void;
destroy(): void;
@@ -22,11 +16,15 @@ declare class UmbControllerHostBaseDeclaration implements Omit<UmbControllerHost
* @mixin
*/
export const UmbControllerHostMixin = <T extends ClassConstructor>(superClass: T) => {
class UmbControllerHostBaseClass extends superClass {
class UmbControllerHostBaseClass extends superClass implements UmbControllerHostBaseDeclaration {
#controllers: UmbController[] = [];
#attached = false;
getHostElement() {
return undefined as any;
}
/**
* Tests if a controller is assigned to this element.
* @param {UmbController} ctrl
@@ -123,7 +121,7 @@ export const UmbControllerHostMixin = <T extends ClassConstructor>(superClass: T
throw new Error(
`Controller with controller alias: '${ctrl.controllerAlias?.toString()}' and class name: '${
(ctrl as any).constructor.name
}', does not remove it self when destroyed. This can cause memory leaks. Please fix this issue.`,
}', does not remove it self when destroyed. This can cause memory leaks. Please fix this issue.\r\nThis usually occurs when you have a destroy() method that doesn't call super.destroy().`,
);
}
prev = ctrl;

View File

@@ -0,0 +1,11 @@
import type { UmbControllerHostElement } from '../controller-api/controller-host-element.interface.js';
import type { UmbLocalizationController } from '@umbraco-cms/backoffice/localization-api';
import type { UmbClassInterface } from '@umbraco-cms/backoffice/class-api';
export interface UmbElement extends UmbClassInterface, UmbControllerHostElement {
/**
* Use the UmbLocalizeController to localize your element.
* @see UmbLocalizationController
*/
localize: UmbLocalizationController;
}

View File

@@ -1,72 +1,50 @@
import type { UmbElement } from './element.interface.js';
import { UmbLocalizationController } from '@umbraco-cms/backoffice/localization-api';
import type { Observable } from '@umbraco-cms/backoffice/external/rxjs';
import type { HTMLElementConstructor } from '@umbraco-cms/backoffice/extension-api';
import {
UmbControllerHostElementMixin,
type UmbControllerHostImplementationElement,
} from '@umbraco-cms/backoffice/controller-api';
import { type UmbControllerAlias, UmbControllerHostElementMixin } from '@umbraco-cms/backoffice/controller-api';
import type { UmbContextToken, UmbContextCallback } from '@umbraco-cms/backoffice/context-api';
import { UmbContextConsumerController, UmbContextProviderController } from '@umbraco-cms/backoffice/context-api';
import type { ObserverCallback } from '@umbraco-cms/backoffice/observable-api';
import { UmbObserverController } from '@umbraco-cms/backoffice/observable-api';
export declare class UmbElement extends UmbControllerHostImplementationElement {
/**
* @description Observe a RxJS source of choice.
* @param {Observable<T>} source RxJS source
* @param {method} callback Callback method called when data is changed.
* @return {UmbObserverController} Reference to a Observer Controller instance
* @memberof UmbElementMixin
*/
observe<T>(
source: Observable<T> | { asObservable: () => Observable<T> },
callback: ObserverCallback<T>,
unique?: string,
): UmbObserverController<T>;
provideContext<
BaseType = unknown,
ResultType extends BaseType = BaseType,
InstanceType extends ResultType = ResultType,
>(
alias: string | UmbContextToken<BaseType, ResultType>,
instance: InstanceType,
): UmbContextProviderController<BaseType, ResultType, InstanceType>;
consumeContext<BaseType = unknown, ResultType extends BaseType = BaseType>(
alias: string | UmbContextToken<BaseType, ResultType>,
callback: UmbContextCallback<ResultType>,
): UmbContextConsumerController<BaseType, ResultType>;
getContext<BaseType = unknown, ResultType extends BaseType = BaseType>(
alias: string | UmbContextToken<BaseType, ResultType>,
): Promise<ResultType>;
/**
* Use the UmbLocalizeController to localize your element.
* @see UmbLocalizationController
*/
localize: UmbLocalizationController;
}
import { UmbObserverController, simpleHashCode } from '@umbraco-cms/backoffice/observable-api';
export const UmbElementMixin = <T extends HTMLElementConstructor>(superClass: T) => {
class UmbElementMixinClass extends UmbControllerHostElementMixin(superClass) implements UmbElement {
localize: UmbLocalizationController = new UmbLocalizationController(this);
/**
* @description Observe a RxJS source of choice.
* @param {Observable<T>} source RxJS source
* @param {method} callback Callback method called when data is changed.
* @return {UmbObserverController} Reference to a Observer Controller instance
* @memberof UmbElementMixin
*/
observe<T>(source: Observable<T>, callback: ObserverCallback<T>, unique?: string): UmbObserverController<T> {
return new UmbObserverController<T>(this, source, callback, unique);
observe<
ObservableType extends Observable<T>,
T,
SpecificT = ObservableType extends Observable<infer U>
? ObservableType extends undefined
? U | undefined
: U
: undefined,
R extends UmbObserverController<SpecificT> = UmbObserverController<SpecificT>,
SpecificR extends R | undefined = ObservableType extends undefined ? R | undefined : R,
>(
// This type dance checks if the Observable given could be undefined, if it potentially could be undefined it means that this potentially could return undefined and then call the callback with undefined. [NL]
source: ObservableType | undefined,
callback: ObserverCallback<SpecificT>,
controllerAlias?: UmbControllerAlias,
): SpecificR {
// Fallback to use a hash of the provided method, but only if the alias is undefined.
controllerAlias ??= controllerAlias === undefined ? simpleHashCode(callback.toString()) : undefined;
if (source) {
return new UmbObserverController<T>(
this,
source,
callback as unknown as ObserverCallback<T>,
controllerAlias,
) as unknown as SpecificR;
} else {
callback(undefined as SpecificT);
this.removeControllerByAlias(controllerAlias);
return undefined as SpecificR;
}
}
/**
* @description Provide a context API for this or child elements.
* @param {string} alias
* @param {instance} instance The API instance to be exposed.
* @return {UmbContextProviderController} Reference to a Context Provider Controller instance
* @memberof UmbElementMixin
*/
provideContext<
BaseType = unknown,
ResultType extends BaseType = BaseType,
@@ -78,13 +56,6 @@ export const UmbElementMixin = <T extends HTMLElementConstructor>(superClass: T)
return new UmbContextProviderController(this, alias, instance);
}
/**
* @description Setup a subscription for a context. The callback is called when the context is resolved.
* @param {string} alias
* @param {method} callback Callback method called when context is resolved.
* @return {UmbContextConsumerController} Reference to a Context Consumer Controller instance
* @memberof UmbElementMixin
*/
consumeContext<BaseType = unknown, ResultType extends BaseType = BaseType>(
alias: string | UmbContextToken<BaseType, ResultType>,
callback: UmbContextCallback<ResultType>,
@@ -92,13 +63,6 @@ export const UmbElementMixin = <T extends HTMLElementConstructor>(superClass: T)
return new UmbContextConsumerController(this, alias, callback);
}
/**
* @description Setup a subscription for a context. The callback is called when the context is resolved.
* @param {string} contextAlias
* @param {method} callback Callback method called when context is resolved.
* @return {UmbContextConsumerController} Reference to a Context Consumer Controller instance
* @memberof UmbElementMixin
*/
async getContext<BaseType = unknown, ResultType extends BaseType = BaseType>(
contextAlias: string | UmbContextToken<BaseType, ResultType>,
): Promise<ResultType> {

View File

@@ -0,0 +1,203 @@
import { expect } from '@open-wc/testing';
import { UmbElementMixin } from './element.mixin.js';
import { customElement } from '@umbraco-cms/backoffice/external/lit';
import { UmbStringState } from '@umbraco-cms/backoffice/observable-api';
@customElement('test-my-umb-element')
class UmbTestUmbElement extends UmbElementMixin(HTMLElement) {}
describe('UmbElement', () => {
let hostElement: UmbTestUmbElement;
beforeEach(() => {
hostElement = document.createElement('test-my-umb-element') as UmbTestUmbElement;
});
describe('Element general controller API', () => {
describe('methods', () => {
it('has an hasController method', () => {
expect(hostElement).to.have.property('hasController').that.is.a('function');
});
it('has an getControllers method', () => {
expect(hostElement).to.have.property('getControllers').that.is.a('function');
});
it('has an addController method', () => {
expect(hostElement).to.have.property('addController').that.is.a('function');
});
it('has an removeControllerByAlias method', () => {
expect(hostElement).to.have.property('removeControllerByAlias').that.is.a('function');
});
it('has an removeController method', () => {
expect(hostElement).to.have.property('removeController').that.is.a('function');
});
it('has an destroy method', () => {
expect(hostElement).to.have.property('destroy').that.is.a('function');
});
});
});
describe('Element helper methods API', () => {
describe('methods', () => {
it('has an hasController method', () => {
expect(hostElement).to.have.property('getHostElement').that.is.a('function');
});
it('has an hasController should return it self', () => {
expect(hostElement.getHostElement()).to.be.equal(hostElement);
});
it('has an observe method', () => {
expect(hostElement).to.have.property('observe').that.is.a('function');
});
it('has an provideContext method', () => {
expect(hostElement).to.have.property('provideContext').that.is.a('function');
});
it('has an consumeContext method', () => {
expect(hostElement).to.have.property('consumeContext').that.is.a('function');
});
it('has an getContext method', () => {
expect(hostElement).to.have.property('getContext').that.is.a('function');
});
it('has an localization class instance', () => {
expect(hostElement).to.have.property('localize').that.is.a('object');
});
});
});
describe('Controllers lifecycle', () => {
it('observe is removed when destroyed', () => {
const myState = new UmbStringState('hello');
const myObservable = myState.asObservable();
const ctrl = hostElement.observe(myObservable, () => {}, 'observer');
// The controller is now added to the host:
expect(hostElement.hasController(ctrl)).to.be.true;
ctrl.destroy();
// The controller is removed from the host:
expect(hostElement.hasController(ctrl)).to.be.false;
});
it('observe is destroyed then removed', () => {
const myState = new UmbStringState('hello');
const myObservable = myState.asObservable();
const ctrl = hostElement.observe(myObservable, () => {}, 'observer');
// The controller is now added to the host:
expect(hostElement.hasController(ctrl)).to.be.true;
hostElement.removeController(ctrl);
// The controller is removed from the host:
expect(hostElement.hasController(ctrl)).to.be.false;
});
it('observe is destroyed then removed via alias', () => {
const myState = new UmbStringState('hello');
const myObservable = myState.asObservable();
const ctrl = hostElement.observe(myObservable, () => {}, 'observer');
// The controller is now added to the host:
expect(hostElement.hasController(ctrl)).to.be.true;
hostElement.removeControllerByAlias('observer');
// The controller is removed from the host:
expect(hostElement.hasController(ctrl)).to.be.false;
});
it('observe is removed when replaced with alias', () => {
const myState = new UmbStringState('hello');
const myObservable = myState.asObservable();
const ctrl = hostElement.observe(myObservable, () => {}, 'observer');
// The controller is now added to the host:
expect(hostElement.hasController(ctrl)).to.be.true;
const ctrl2 = hostElement.observe(myObservable, () => {}, 'observer');
// The controller is removed from the host:
expect(hostElement.hasController(ctrl)).to.be.false;
// The controller is new one is there instead:
expect(hostElement.hasController(ctrl2)).to.be.true;
});
it('observe is removed when replaced with alias made of hash of callback method', () => {
const myState = new UmbStringState('hello');
const myObservable = myState.asObservable();
const ctrl = hostElement.observe(myObservable, () => {});
// The controller is now added to the host:
expect(hostElement.hasController(ctrl)).to.be.true;
const ctrl2 = hostElement.observe(myObservable, () => {});
// The controller is removed from the host:
expect(hostElement.hasController(ctrl)).to.be.false;
// The controller is new one is there instead:
expect(hostElement.hasController(ctrl2)).to.be.true;
});
it('observe is NOT removed when controller alias does not align', () => {
const myState = new UmbStringState('hello');
const myObservable = myState.asObservable();
const ctrl = hostElement.observe(myObservable, () => {});
// The controller is now added to the host:
expect(hostElement.hasController(ctrl)).to.be.true;
const ctrl2 = hostElement.observe(myObservable, (value) => {
const a = value + 'bla';
});
// The controller is not removed from the host:
expect(hostElement.hasController(ctrl)).to.be.true;
expect(hostElement.hasController(ctrl2)).to.be.true;
});
it('observe is removed when observer is undefined and using the same alias', () => {
const myState = new UmbStringState('hello');
const myObservable = myState.asObservable();
const ctrl = hostElement.observe(myObservable, () => {}, 'observer');
// The controller is now added to the host:
expect(hostElement.hasController(ctrl)).to.be.true;
const ctrl2 = hostElement.observe(
undefined,
() => {
const a = 1;
},
'observer',
);
// The controller is removed from the host, and the new one was NOT added:
expect(hostElement.hasController(ctrl)).to.be.false;
expect(ctrl2).to.be.undefined;
expect(hostElement.hasController(ctrl2)).to.be.false;
});
it('observe is removed when observer is undefined and using the same callback method', () => {
const myState = new UmbStringState('hello');
const myObservable = myState.asObservable();
const ctrl = hostElement.observe(myObservable, () => {});
// The controller is now added to the host:
expect(hostElement.hasController(ctrl)).to.be.true;
const ctrl2 = hostElement.observe(undefined, () => {});
// The controller is removed from the host, and the new one was NOT added:
expect(hostElement.hasController(ctrl)).to.be.false;
expect(ctrl2).to.be.undefined;
expect(hostElement.hasController(ctrl2)).to.be.false;
});
});
});

View File

@@ -1 +1,2 @@
export * from './element.interface.js';
export * from './element.mixin.js';

View File

@@ -1,6 +1,7 @@
import { expect } from '@open-wc/testing';
import { UmbObjectState } from './states/object-state.js';
import { UmbObserverController } from './observer.controller.js';
import { simpleHashCode } from './utils/simple-hash-code.function.js';
import { customElement } from '@umbraco-cms/backoffice/external/lit';
import { UmbControllerHostElementMixin } from '@umbraco-cms/backoffice/controller-api';
@@ -42,27 +43,21 @@ describe('UmbObserverController', () => {
expect(hostElement.hasController(secondCtrl)).to.be.true;
});
it('controller is replacing another controller when using the same callback method and no controller-alias', () => {
const state = new UmbObjectState(undefined);
const observable = state.asObservable();
const callbackMethod = (state: unknown) => {};
const firstCtrl = new UmbObserverController(hostElement, observable, callbackMethod);
const secondCtrl = new UmbObserverController(hostElement, observable, callbackMethod);
expect(hostElement.hasController(firstCtrl)).to.be.false;
expect(hostElement.hasController(secondCtrl)).to.be.true;
});
it('controller is NOT replacing another controller when using a null for controller-alias', () => {
const state = new UmbObjectState(undefined);
const observable = state.asObservable();
const callbackMethod = (state: unknown) => {};
const firstCtrl = new UmbObserverController(hostElement, observable, callbackMethod, null);
const secondCtrl = new UmbObserverController(hostElement, observable, callbackMethod, null);
// Imitates the behavior of the observe method in the UmbClassMixin
let controllerAlias1 = null;
controllerAlias1 ??= controllerAlias1 === undefined ? simpleHashCode(callbackMethod.toString()) : undefined;
const firstCtrl = new UmbObserverController(hostElement, observable, callbackMethod, controllerAlias1);
let controllerAlias2 = null;
controllerAlias2 ??= controllerAlias2 === undefined ? simpleHashCode(callbackMethod.toString()) : undefined;
const secondCtrl = new UmbObserverController(hostElement, observable, callbackMethod, controllerAlias2);
expect(hostElement.hasController(firstCtrl)).to.be.true;
expect(hostElement.hasController(secondCtrl)).to.be.true;

View File

@@ -1,5 +1,4 @@
import { type ObserverCallback, UmbObserver } from './observer.js';
import { simpleHashCode } from './utils/simple-hash-code.function.js';
import type { Observable } from '@umbraco-cms/backoffice/external/rxjs';
import type { UmbController, UmbControllerAlias, UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
@@ -15,12 +14,11 @@ export class UmbObserverController<T = unknown> extends UmbObserver<T> implement
host: UmbControllerHost,
source: Observable<T>,
callback: ObserverCallback<T>,
alias?: UmbControllerAlias | null,
alias: UmbControllerAlias,
) {
super(source, callback);
this.#host = host;
// Fallback to use a hash of the provided method, but only if the alias is undefined.
this.#alias = alias ?? (alias === undefined ? simpleHashCode(callback.toString()) : undefined);
this.#alias = alias;
// Lets check if controller is already here:
// No we don't want this, as multiple different controllers might be looking at the same source.

View File

@@ -6,7 +6,9 @@ export type ObserverCallbackStack<T> = {
complete?: () => void;
};
export type ObserverCallback<T> = ((_value: T) => void) | ObserverCallbackStack<T>;
export type ObserverCallback<T> = (_value: T) => void;
// We do not use the ObserverCallbackStack type, and it was making things more complicated than they need to be so I have taken it out..
//export type ObserverCallback<T> = ((_value: T) => void) | ObserverCallbackStack<T>;
export class UmbObserver<T> {
#source!: Observable<T>;

View File

@@ -45,6 +45,7 @@ export class UmbPropertyEditorUIBlockGridTypeConfigurationElement
this._groupsWithBlockTypes = model;
},
onEnd: () => {
// TODO: make one method for updating the blockGroupsDataSetValue:
this.#datasetContext?.setPropertyValue(
'blockGroups',
this._groupsWithBlockTypes.map((group) => ({ key: group.key, name: group.name })),
@@ -143,6 +144,8 @@ export class UmbPropertyEditorUIBlockGridTypeConfigurationElement
// TODO: Implement confirm dialog
#deleteGroup(groupKey: string) {
// TODO: make one method for updating the blockGroupsDataSetValue:
// This one that deletes might require the ability to parse what to send as an argument to the method, then a filtering can occur before.
this.#datasetContext?.setPropertyValue(
'blockGroups',
this._blockGroups.filter((group) => group.key !== groupKey),
@@ -154,6 +157,7 @@ export class UmbPropertyEditorUIBlockGridTypeConfigurationElement
#changeGroupName(e: UUIInputEvent, groupKey: string) {
const groupName = e.target.value as string;
// TODO: make one method for updating the blockGroupsDataSetValue:
this.#datasetContext?.setPropertyValue(
'blockGroups',
this._blockGroups.map((group) => (group.key === groupKey ? { ...group, name: groupName } : group)),

View File

@@ -144,22 +144,23 @@ export class UmbContentTypePropertyStructureManager<T extends UmbContentTypeMode
// Load inherited and composed types:
//this._loadContentTypeCompositions(data);// Should not be necessary as this will be done when appended to the contentTypes state. [NL]
this.#contentTypeObservers.push(
this.observe(
// Then lets start observation of the content type:
await this.#contentTypeRepository.byUnique(data.unique),
(docType) => {
if (docType) {
// TODO: Handle if there was changes made to the owner document type in this context.
/*
possible easy solutions could be to notify user wether they want to update(Discard the changes to accept the new ones).
*/
this.#contentTypes.appendOne(docType);
}
},
'observeContentType_' + data.unique,
),
const ctrl = this.observe(
// Then lets start observation of the content type:
await this.#contentTypeRepository.byUnique(data.unique),
(docType) => {
if (docType) {
// TODO: Handle if there was changes made to the owner document type in this context. [NL]
/*
possible easy solutions could be to notify user wether they want to update(Discard the changes to accept the new ones). [NL]
*/
this.#contentTypes.appendOne(docType);
}
// TODO: Do we need to handle the undefined case? [NL]
},
'observeContentType_' + data.unique,
);
this.#contentTypeObservers.push(ctrl);
}
private async _loadContentTypeCompositions(contentType: T) {
@@ -493,7 +494,7 @@ export class UmbContentTypePropertyStructureManager<T extends UmbContentTypeMode
});
}
// In future this might need to take parentName(parentId lookup) into account as well? otherwise containers that share same name and type will always be merged, but their position might be different and they should not be merged.
// In future this might need to take parentName(parentId lookup) into account as well? otherwise containers that share same name and type will always be merged, but their position might be different and they should not be merged. [NL]
containersByNameAndType(name: string, containerType: UmbPropertyContainerTypes) {
return this.#containers.asObservablePart((data) => {
return data.filter((x) => x.name === name && x.type === containerType);

View File

@@ -5,7 +5,7 @@ export const manifest: ManifestPropertyEditorSchema = {
name: 'Multi Node Tree Picker',
alias: 'Umbraco.MultiNodeTreePicker',
meta: {
defaultPropertyEditorUiAlias: 'Umb.PropertyEditorUi.MultiNodeTreePicker',
defaultPropertyEditorUiAlias: 'Umb.PropertyEditorUi.TreePicker',
settings: {
properties: [
{

View File

@@ -29,6 +29,9 @@ export interface UmbPropertyDatasetContext extends UmbContext {
// Property methods:
propertyVariantId?: (propertyAlias: string) => Promise<Observable<UmbVariantId | undefined>>;
propertyValueByAlias<ReturnType = unknown>(propertyAlias: string): Promise<Observable<ReturnType | undefined>>;
propertyValueByAlias<ReturnType = unknown>(
propertyAlias: string,
): Promise<Observable<ReturnType | undefined> | undefined>;
// TODO: Append the andCulture method as well..
setPropertyValue(propertyAlias: string, value: unknown): void;
}

View File

@@ -1,8 +1,7 @@
import { UMB_PROPERTY_DATASET_CONTEXT } from '../property-dataset/index.js';
import type { UmbVariantId } from '@umbraco-cms/backoffice/variant';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api';
import type { UmbObserverController } from '@umbraco-cms/backoffice/observable-api';
import { UmbContextBase } from '@umbraco-cms/backoffice/class-api';
import {
UmbArrayState,
UmbBasicState,
@@ -10,14 +9,12 @@ import {
UmbDeepState,
UmbStringState,
} from '@umbraco-cms/backoffice/observable-api';
import { UmbContextProviderController, UmbContextToken } from '@umbraco-cms/backoffice/context-api';
import { UmbContextToken } from '@umbraco-cms/backoffice/context-api';
import type { UmbPropertyEditorConfigProperty } from '@umbraco-cms/backoffice/property-editor';
import { UmbPropertyEditorConfigCollection } from '@umbraco-cms/backoffice/property-editor';
import type { UmbPropertyEditorUiElement } from '@umbraco-cms/backoffice/extension-registry';
export class UmbPropertyContext<ValueType = any> extends UmbControllerBase {
private _providerController: UmbContextProviderController;
export class UmbPropertyContext<ValueType = any> extends UmbContextBase<UmbPropertyContext<ValueType>> {
#alias = new UmbStringState(undefined);
public readonly alias = this.#alias.asObservable();
#label = new UmbStringState(undefined);
@@ -29,8 +26,8 @@ export class UmbPropertyContext<ValueType = any> extends UmbControllerBase {
#configValues = new UmbArrayState<UmbPropertyEditorConfigProperty>([], (x) => x.alias);
public readonly configValues = this.#configValues.asObservable();
#configCollection = new UmbClassState<UmbPropertyEditorConfigCollection | undefined>(undefined);
public readonly config = this.#configCollection.asObservable();
#config = new UmbClassState<UmbPropertyEditorConfigCollection | undefined>(undefined);
public readonly config = this.#config.asObservable();
private _editor = new UmbBasicState<UmbPropertyEditorUiElement | undefined>(undefined);
public readonly editor = this._editor.asObservable();
@@ -51,7 +48,7 @@ export class UmbPropertyContext<ValueType = any> extends UmbControllerBase {
#datasetContext?: typeof UMB_PROPERTY_DATASET_CONTEXT.TYPE;
constructor(host: UmbControllerHost) {
super(host);
super(host, UMB_PROPERTY_CONTEXT);
this.consumeContext(UMB_PROPERTY_DATASET_CONTEXT, (variantContext) => {
this.#datasetContext = variantContext;
@@ -63,10 +60,8 @@ export class UmbPropertyContext<ValueType = any> extends UmbControllerBase {
this._observeProperty();
});
this._providerController = new UmbContextProviderController(host, UMB_PROPERTY_CONTEXT, this);
this.observe(this.configValues, (configValues) => {
this.#configCollection.setValue(configValues ? new UmbPropertyEditorConfigCollection(configValues) : undefined);
this.#config.setValue(configValues ? new UmbPropertyEditorConfigCollection(configValues) : undefined);
});
this.observe(this.variantId, () => {
@@ -74,29 +69,25 @@ export class UmbPropertyContext<ValueType = any> extends UmbControllerBase {
});
}
private _observePropertyVariant?: UmbObserverController<UmbVariantId | undefined>;
private _observePropertyValue?: UmbObserverController<ValueType | undefined>;
private async _observeProperty(): Promise<void> {
const alias = this.#alias.getValue();
if (!this.#datasetContext || !alias) return;
const variantIdSubject = (await this.#datasetContext.propertyVariantId?.(alias)) ?? undefined;
this._observePropertyVariant?.destroy();
if (variantIdSubject) {
this._observePropertyVariant = this.observe(variantIdSubject, (variantId) => {
this.observe(
await this.#datasetContext.propertyVariantId?.(alias),
(variantId) => {
this.#variantId.setValue(variantId);
});
}
},
'observeVariantId',
);
// TODO: Verify if we need to optimize runtime by parsing the propertyVariantID, cause this method retrieves it again:
const subject = await this.#datasetContext.propertyValueByAlias<ValueType>(alias);
this._observePropertyValue?.destroy();
if (subject) {
this._observePropertyValue = this.observe(subject, (value) => {
this.observe(
await this.#datasetContext.propertyValueByAlias<ValueType>(alias),
(value) => {
this.#value.setValue(value);
});
}
},
'observeValue',
);
}
private _generateVariantDifferenceString() {
@@ -169,8 +160,7 @@ export class UmbPropertyContext<ValueType = any> extends UmbControllerBase {
this.#description.destroy();
this.#configValues.destroy();
this.#value.destroy();
this.#configCollection.destroy();
this._providerController.destroy(); // This would also be handled by the controller host, but if someone wanted to replace/remove this context without the host being destroyed. Then we have clean up out selfs here.
this.#config.destroy();
this.#datasetContext = undefined;
}
}

View File

@@ -101,8 +101,9 @@ export class UmbPropertyElement extends UmbLitElement {
@state()
private _element?: ManifestPropertyEditorUi['ELEMENT_TYPE'];
@state()
private _value?: unknown;
// Not begin used currently [NL]
//@state()
//private _value?: unknown;
@state()
private _alias?: string;
@@ -178,12 +179,12 @@ export class UmbPropertyElement extends UmbLitElement {
this.#propertyContext.setEditor(this._element);
if (this._element) {
// TODO: Could this be changed to change event? (or additionally support change?)
// TODO: Could this be changed to change event? (or additionally support the change event? [NL])
this._element.addEventListener('property-value-change', this._onPropertyEditorChange as any as EventListener);
// No need for a controller alias, as the clean is handled via the observer prop:
this.#valueObserver = this.observe(this.#propertyContext.value, (value) => {
this._value = value;
//this._value = value;// This was not used currently [NL]
if (this._element) {
this._element.value = value;
}

View File

@@ -22,7 +22,7 @@ export interface UmbVariantableWorkspaceContextInterface<VariantType extends Umb
propertyValueByAlias<ReturnValue = unknown>(
alias: string,
variantId?: UmbVariantId,
): Promise<Observable<ReturnValue | undefined>>;
): Promise<Observable<ReturnValue | undefined> | undefined>;
getPropertyValue<ReturnValue = unknown>(alias: string, variantId?: UmbVariantId): ReturnValue | undefined;
setPropertyValue(alias: string, value: unknown, variantId?: UmbVariantId): Promise<void>;
//propertyDataByAlias(alias: string, variantId?: UmbVariantId): Observable<ValueModelBaseModel | undefined>;

View File

@@ -3,7 +3,7 @@ import type { UmbNameablePropertyDatasetContext, UmbPropertyDatasetContext } fro
import { UMB_PROPERTY_DATASET_CONTEXT } from '@umbraco-cms/backoffice/property';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { UmbContextBase } from '@umbraco-cms/backoffice/class-api';
import { map } from '@umbraco-cms/backoffice/external/rxjs';
import { type Observable, map } from '@umbraco-cms/backoffice/external/rxjs';
import { UmbObjectState } from '@umbraco-cms/backoffice/observable-api';
import type { UmbVariantModel } from '@umbraco-cms/backoffice/variant';
import { UmbVariantId } from '@umbraco-cms/backoffice/variant';
@@ -83,21 +83,24 @@ export class UmbDocumentPropertyDataContext
/**
* TODO: Write proper JSDocs here.
* Ideally do not use these methods, its better to communicate directly with the workspace, but if you do not know the property variant id, then this will figure it out for you. So good for externals to set or get values of a property.
* Ideally do not use this method, its better to communicate directly with the workspace, but if you do not know the property variant id, then this will figure it out for you. So good for externals to set or get values of a property.
*/
async propertyValueByAlias<ReturnType = unknown>(propertyAlias: string) {
async propertyValueByAlias<ReturnType = unknown>(
propertyAlias: string,
): Promise<Observable<ReturnType | undefined> | undefined> {
await this.#workspace.isLoaded();
return (await this.#workspace.structure.propertyStructureByAlias(propertyAlias)).pipe(
map((property) =>
property?.alias
? this.#workspace.getPropertyValue<ReturnType>(property.alias, this.#createPropertyVariantId(property))
: undefined,
),
);
const structure = await this.#workspace.structure.getPropertyStructureByAlias(propertyAlias);
if (structure) {
return this.#workspace.propertyValueByAlias<ReturnType>(propertyAlias, this.#createPropertyVariantId(structure));
}
return;
}
// TODO: Refactor: Not used currently, but should investigate if we can implement this, to spare some energy.
async propertyValueByAliasAndCulture<ReturnType = unknown>(propertyAlias: string, propertyVariantId: UmbVariantId) {
async propertyValueByAliasAndCulture<ReturnType = unknown>(
propertyAlias: string,
propertyVariantId: UmbVariantId,
): Promise<Observable<ReturnType | undefined> | undefined> {
return this.#workspace.propertyValueByAlias<ReturnType>(propertyAlias, propertyVariantId);
}

View File

@@ -29,7 +29,7 @@ import {
} from '@umbraco-cms/backoffice/observable-api';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { UmbLanguageCollectionRepository, type UmbLanguageDetailModel } from '@umbraco-cms/backoffice/language';
import { firstValueFrom } from '@umbraco-cms/backoffice/external/rxjs';
import { type Observable, firstValueFrom } from '@umbraco-cms/backoffice/external/rxjs';
import { UMB_ACTION_EVENT_CONTEXT } from '@umbraco-cms/backoffice/action';
import { UmbReloadTreeItemChildrenRequestEntityActionEvent } from '@umbraco-cms/backoffice/tree';
@@ -230,7 +230,10 @@ export class UmbDocumentWorkspaceContext
return this.structure.propertyStructureById(propertyId);
}
async propertyValueByAlias<PropertyValueType = unknown>(propertyAlias: string, variantId?: UmbVariantId) {
async propertyValueByAlias<PropertyValueType = unknown>(
propertyAlias: string,
variantId?: UmbVariantId,
): Promise<Observable<PropertyValueType | undefined> | undefined> {
return this.#currentData.asObservablePart(
(data) =>
data?.values?.find((x) => x?.alias === propertyAlias && (variantId ? variantId.compare(x) : true))
@@ -254,20 +257,16 @@ export class UmbDocumentWorkspaceContext
}
return undefined;
}
async setPropertyValue<UmbDocumentValueModel = unknown>(
alias: string,
value: UmbDocumentValueModel,
variantId?: UmbVariantId,
) {
async setPropertyValue<ValueType = unknown>(alias: string, value: ValueType, variantId?: UmbVariantId) {
variantId ??= UmbVariantId.CreateInvariant();
const entry = { ...variantId.toObject(), alias, value };
const entry = { ...variantId.toObject(), alias, value } as UmbDocumentValueModel<ValueType>;
const currentData = this.getData();
if (currentData) {
const values = appendToFrozenArray(
currentData.values || [],
currentData.values ?? [],
entry,
(x) => x.alias === alias && (variantId ? variantId.compare(x) : true),
(x) => x.alias === alias && variantId!.compare(x),
);
this.#currentData.update({ values });

View File

@@ -1,20 +0,0 @@
import { Meta } from '@storybook/addon-docs';
<Meta title="Guides/Umbraco Controller" parameters={{ previewTabs: { canvas: { hidden: true } } }} />
# Umbraco Controller
This class can be used as the base of any class.
This will enable Controllers to be hosted in this class. Additionally it provides few shortcut methods for initializing core Umbraco Controllers.
```ts
observe<T>(source: Observable<T>, callback: (_value: T) => void, unique?: string): UmbObserverController<T>
provideContext<R = unknown>(alias: string | UmbContextToken<R>, instance: R): UmbContextProviderController<R>
consumeContext<R = unknown>(alias: string | UmbContextToken<R>, callback: UmbContextCallback<R>): UmbContextConsumerController<R>
```
Read about the 'observe' method in the [Store-API](?path=/docs/guides-store--docs).
Read about the 'provideContext' and 'consumeContext' methods in the [Context-API](?path=/docs/guides-context-api--docs).

View File

@@ -1,36 +0,0 @@
import { Meta } from '@storybook/addon-docs';
<Meta title="Guides/Umbraco Element" parameters={{ previewTabs: { canvas: { hidden: true } } }} />
# Umbraco Element
This element can be used as the base of any element.
This will enable Controllers to be hosted at this element. Additionally it provides few shortcut methods for initializing core Umbraco Controllers.
```ts
observe<T>(source: Observable<T>, callback: (_value: T) => void, unique?: string): UmbObserverController<T>
provideContext<R = unknown>(alias: string | UmbContextToken<R>, instance: R): UmbContextProviderController<R>
consumeContext<R = unknown>(alias: string | UmbContextToken<R>, callback: UmbContextCallback<R>): UmbContextConsumerController<R>
```
Use these for an smooth consumption, like this request for a Context API using a simple string context, where the callback value is of an unknown type:
```ts
this.consumeContext('requestThisContextAlias', (context) => {
// Notice this is a subscription, as context might change or a new one appears.
console.log("I've got the context", context);
});
```
Or use the a Context Token to get a typed context:
```ts
import { UMB_NOTIFICATION_CONTEXT } from '@umbraco-cms/backoffice/notification';
this.consumeContext(UMB_NOTIFICATION_CONTEXT, (context) => {
// Notice this is a subscription, as context might change or a new one appears, but the value is strongly typed
console.log("I've got the context", context);
});
```