Merge branch 'main' into chore/folder-structure

This commit is contained in:
Mads Rasmussen
2023-01-02 13:18:49 +01:00
25 changed files with 291 additions and 60 deletions

View File

@@ -41,11 +41,11 @@ export class UmbCollectionContext<
}
connectedCallback() {
this._storeConsumer.attach();
this._storeConsumer.hostConnected();
}
disconnectedCallback() {
this._storeConsumer.detach();
this._storeConsumer.hostDisconnected();
}
public getData() {

View File

@@ -1,22 +1,26 @@
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
import { css, html, LitElement, PropertyValueMap } from 'lit';
import { customElement, property, state } from 'lit/decorators.js';
import { UmbObserverMixin } from '@umbraco-cms/observable-api';
import { UmbWorkspacePropertyContext } from './workspace-property.context';
import { createExtensionElement } from '@umbraco-cms/extensions-api';
import { umbExtensionsRegistry } from '@umbraco-cms/extensions-registry';
import { UmbContextConsumerMixin } from '@umbraco-cms/context-api';
import type { ManifestPropertyEditorUI, ManifestTypes } from '@umbraco-cms/models';
import '../../property-actions/shared/property-action-menu/property-action-menu.element';
import '../workspace/workspace-property-layout/workspace-property-layout.element';
import 'src/backoffice/core/components/workspace/workspace-property-layout/workspace-property-layout.element';
import { UmbContextProviderController } from 'src/core/context-api/provide/context-provider.controller';
import { UmbControllerHostMixin } from 'src/core/controller/controller-host.mixin';
import { UmbObserverController } from 'src/core/observable-api/observer.controller';
/**
* @element umb-entity-property
* @description - Component for displaying a entity property. The Element will render a Property Editor based on the Property Editor UI alias passed to the element.
* The element will also render all Property Actions related to the Property Editor.
*/
// TODO: get rid of the other mixins:
@customElement('umb-entity-property')
export class UmbEntityPropertyElement extends UmbContextConsumerMixin(UmbObserverMixin(LitElement)) {
export class UmbEntityPropertyElement extends UmbControllerHostMixin(LitElement) {
static styles = [
UUITextStyles,
css`
@@ -114,14 +118,26 @@ export class UmbEntityPropertyElement extends UmbContextConsumerMixin(UmbObserve
@state()
private _element?: { value?: any; config?: any } & HTMLElement; // TODO: invent interface for propertyEditorUI.
connectedCallback(): void {
super.connectedCallback();
// TODO: How to get proper default value?
private _propertyContext = new UmbWorkspacePropertyContext<string>("");
private propertyEditorUIObserver?: UmbObserverController;
constructor() {
super();
// TODO: make it easier to create a provider, unless a context should just extends a provider-controller?
new UmbContextProviderController(this, 'umbPropertyContext', this._propertyContext);
this._observePropertyEditorUI();
this.addEventListener('property-editor-change', this._onPropertyEditorChange as any as EventListener);
}
private _observePropertyEditorUI() {
this.observe<ManifestTypes>(umbExtensionsRegistry.getByAlias(this.propertyEditorUIAlias), (manifest) => {
this.propertyEditorUIObserver?.destroy();
this.propertyEditorUIObserver = new UmbObserverController<ManifestTypes>(this, umbExtensionsRegistry.getByAlias(this.propertyEditorUIAlias), (manifest) => {
if (manifest?.type === 'propertyEditorUI') {
this._gotData(manifest);
}

View File

@@ -0,0 +1,64 @@
import { BehaviorSubject, Observable } from "rxjs";
export type WorkspacePropertyData<ValueType> = {
alias?: string | null;
label?: string | null;
value?: ValueType | null;
};
export class UmbWorkspacePropertyContext<ValueType> {
private _data: BehaviorSubject<WorkspacePropertyData<ValueType>>;
public readonly data: Observable<WorkspacePropertyData<ValueType>>;
#defaultValue!: ValueType | null;
constructor(defaultValue: ValueType | null) {
this.#defaultValue = defaultValue;
// TODO: How do we connect this value with parent context?
// Ensuring the property editor value-property is updated...
this._data = new BehaviorSubject({value: defaultValue} as WorkspacePropertyData<ValueType>);
this.data = this._data.asObservable();
}
/*
hostConnected() {
}
hostDisconnected() {
}
*/
public getData() {
return this._data.getValue();
}
public update(data: Partial<WorkspacePropertyData<ValueType>>) {
this._data.next({ ...this.getData(), ...data });
}
public resetValue() {
console.log("property context reset")
this.update({value: this.#defaultValue})
}
// TODO: how can we make sure to call this.
public destroy(): void {
this._data.unsubscribe();
}
}

View File

@@ -1,9 +1,9 @@
import { UmbNotificationService } from '../../../services/notification';
import { UmbNotificationDefaultData } from '../../../services/notification/layouts/default';
import { UmbWorkspaceWithStoreContext } from './workspace-with-store.context';
import { UmbNodeStoreBase } from 'src/backoffice/core/stores/store';
import { ContentTreeItem } from '@umbraco-cms/backend-api';
import { UmbContextConsumer } from '@umbraco-cms/context-api';
import { UmbWorkspaceWithStoreContext } from './workspace-with-store.context';
// TODO: Consider if its right to have this many class-inheritance of WorkspaceContext
export class UmbWorkspaceNodeContext<
@@ -39,12 +39,12 @@ export class UmbWorkspaceNodeContext<
connectedCallback() {
super.connectedCallback();
this._notificationConsumer.attach();
this._notificationConsumer.hostConnected();
}
disconnectedCallback() {
super.connectedCallback();
this._notificationConsumer.detach();
this._notificationConsumer.hostDisconnected();
}
protected _onStoreSubscription(): void {
@@ -67,3 +67,4 @@ export class UmbWorkspaceNodeContext<
});
}
}

View File

@@ -29,11 +29,11 @@ export abstract class UmbWorkspaceWithStoreContext<
}
connectedCallback() {
this._storeConsumer.attach();
this._storeConsumer.hostConnected();
}
disconnectedCallback() {
this._storeConsumer.detach();
this._storeConsumer.hostDisconnected();
}
protected abstract _onStoreSubscription(): void;
@@ -63,8 +63,8 @@ export abstract class UmbWorkspaceWithStoreContext<
public destroy(): void {
super.destroy();
if (this._storeConsumer) {
this._storeConsumer.detach();
if(this._storeConsumer) {
this._storeConsumer.hostDisconnected();
}
if (this._dataObserver) {
// I want to make sure that we unsubscribe, also if store(observer source) changes.

View File

@@ -1,8 +1,8 @@
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
import { css, html, LitElement } from 'lit';
import { customElement, state } from 'lit/decorators.js';
import { UmbResourceController } from '@umbraco-cms/resources';
import { ProfilingResource } from '@umbraco-cms/backend-api';
import { UmbResourceController } from '@umbraco-cms/controllers';
@customElement('umb-dashboard-performance-profiling')
export class UmbDashboardPerformanceProfilingElement extends LitElement {

View File

@@ -1,9 +1,12 @@
import { css, html, LitElement } from 'lit';
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
import { customElement, property } from 'lit/decorators.js';
import { UmbControllerHostMixin } from 'src/core/controller/controller-host.mixin';
import { UmbContextConsumerController } from 'src/core/context-api/consume/context-consumer.controller';
import { UmbWorkspacePropertyContext } from 'src/backoffice/core/components/entity-property/workspace-property.context';
@customElement('umb-property-editor-ui-textarea')
export class UmbPropertyEditorUITextareaElement extends LitElement {
export class UmbPropertyEditorUITextareaElement extends UmbControllerHostMixin(LitElement) {
static styles = [
UUITextStyles,
css`
@@ -19,14 +22,26 @@ export class UmbPropertyEditorUITextareaElement extends LitElement {
@property({ type: Array, attribute: false })
config = [];
private propertyContext?: UmbWorkspacePropertyContext<string>;
constructor() {
super();
new UmbContextConsumerController(this, 'umbPropertyContext', (instance) => {
this.propertyContext = instance;
});
}
private onInput(e: InputEvent) {
this.value = (e.target as HTMLInputElement).value;
this.dispatchEvent(new CustomEvent('property-editor-change', { bubbles: true, composed: true }));
}
render() {
return html`<uui-textarea .value=${this.value} @input=${this.onInput}></uui-textarea>
${this.config?.map((property: any) => html`<div>${property.alias}: ${property.value}</div>`)} `;
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>`;
}
}

View File

@@ -0,0 +1,14 @@
import { UmbContextConsumer } from './context-consumer';
import { UmbContextCallback } from './context-request.event';
import type { UmbController } from 'src/core/controller/controller.interface';
import { UmbControllerHostInterface } from 'src/core/controller/controller-host.mixin';
export class UmbContextConsumerController extends UmbContextConsumer implements UmbController {
constructor(host:UmbControllerHostInterface, contextAlias: string, callback: UmbContextCallback) {
super(host, contextAlias, callback);
host.addController(this);
}
}

View File

@@ -74,7 +74,7 @@ export const UmbContextConsumerMixin = <T extends HTMLElementConstructor>(superC
}
if (this._attached) {
consumer.attach();
consumer.hostConnected();
}
});
}
@@ -84,13 +84,13 @@ export const UmbContextConsumerMixin = <T extends HTMLElementConstructor>(superC
connectedCallback() {
super.connectedCallback?.();
this._attached = true;
this._consumers.forEach((consumers) => consumers.forEach((consumer) => consumer.attach()));
this._consumers.forEach((consumers) => consumers.forEach((consumer) => consumer.hostConnected()));
}
disconnectedCallback() {
super.disconnectedCallback?.();
this._attached = false;
this._consumers.forEach((consumers) => consumers.forEach((consumer) => consumer.detach()));
this._consumers.forEach((consumers) => consumers.forEach((consumer) => consumer.hostDisconnected()));
this._resolved.clear();
}

View File

@@ -28,7 +28,7 @@ describe('UmbContextConsumer', () => {
it('dispatches context request event when constructed', async () => {
const listener = oneEvent(window, umbContextRequestEventType);
consumer.attach();
consumer.hostConnected();
const event = (await listener) as unknown as UmbContextRequestEventImplementation;
expect(event).to.exist;
@@ -40,7 +40,7 @@ describe('UmbContextConsumer', () => {
it('works with UmbContextProvider', (done) => {
const provider = new UmbContextProvider(document.body, testContextAlias, new MyClass());
provider.attach();
provider.hostConnected();
const element = document.createElement('div');
document.body.appendChild(element);
@@ -49,8 +49,8 @@ describe('UmbContextConsumer', () => {
expect(_instance.prop).to.eq('value from provider');
done();
});
localConsumer.attach();
localConsumer.hostConnected();
provider.detach();
provider.hostDisconnected();
});
});

View File

@@ -1,4 +1,4 @@
import { isUmbContextProvideEvent, umbContextProvideEventType } from '../provide/context-provide.event';
import { isUmbContextProvideEventType, umbContextProvideEventType } from '../provide/context-provide.event';
import { UmbContextRequestEventImplementation, UmbContextCallback } from './context-request.event';
/**
@@ -23,19 +23,19 @@ export class UmbContextConsumer {
this.target.dispatchEvent(event);
}
public attach() {
public hostConnected() {
// TODO: We need to use closets application element. We need this in order to have separate Backoffice running within or next to each other.
window.addEventListener(umbContextProvideEventType, this._handleNewProvider);
this.request();
}
public detach() {
public hostDisconnected() {
// TODO: We need to use closets application element. We need this in order to have separate Backoffice running within or next to each other.
window.removeEventListener(umbContextProvideEventType, this._handleNewProvider);
}
private _handleNewProvider = (event: Event) => {
if (!isUmbContextProvideEvent(event)) return;
if (!isUmbContextProvideEventType(event)) return;
if (this._contextAlias === event.contextAlias) {
this.request();

View File

@@ -20,6 +20,6 @@ export class UmbContextProvideEventImplementation extends Event implements UmbCo
}
}
export const isUmbContextProvideEvent = (event: Event): event is UmbContextProvideEventImplementation => {
export const isUmbContextProvideEventType = (event: Event): event is UmbContextProvideEventImplementation => {
return event.type === umbContextProvideEventType;
};

View File

@@ -0,0 +1,13 @@
import { UmbContextProvider } from './context-provider';
import type { UmbController } from 'src/core/controller/controller.interface';
import { UmbControllerHostInterface } from 'src/core/controller/controller-host.mixin';
export class UmbContextProviderController extends UmbContextProvider implements UmbController {
constructor(host:UmbControllerHostInterface, contextAlias: string, instance: unknown) {
super(host, contextAlias, instance);
host.addController(this);
}
}

View File

@@ -15,14 +15,14 @@ export const UmbContextProviderMixin = <T extends HTMLElementConstructor>(superC
// TODO: Consider if its right to re-publish the context?
const existingProvider = this._providers.get(alias);
if (existingProvider) {
existingProvider.detach();
existingProvider.hostDisconnected();
}
const provider = new UmbContextProvider(this, alias, instance);
this._providers.set(alias, provider);
// TODO: if already connected then attach the new one.
if (this._attached) {
provider.attach();
provider.hostConnected();
}
}
@@ -31,13 +31,13 @@ export const UmbContextProviderMixin = <T extends HTMLElementConstructor>(superC
connectedCallback() {
super.connectedCallback?.();
this._attached = true;
this._providers.forEach((provider) => provider.attach());
this._providers.forEach((provider) => provider.hostConnected());
}
disconnectedCallback() {
super.disconnectedCallback?.();
this._attached = false;
this._providers.forEach((provider) => provider.detach());
this._providers.forEach((provider) => provider.hostDisconnected());
}
}

View File

@@ -12,11 +12,11 @@ describe('UmbContextProvider', () => {
beforeEach(() => {
provider = new UmbContextProvider(document.body, 'my-test-context', new MyClass());
provider.attach();
provider.hostConnected();
});
afterEach(async () => {
provider.detach();
provider.hostDisconnected();
});
describe('Public API', () => {
@@ -28,11 +28,11 @@ describe('UmbContextProvider', () => {
describe('methods', () => {
it('has an attach method', () => {
expect(provider).to.have.property('attach').that.is.a('function');
expect(provider).to.have.property('hostConnected').that.is.a('function');
});
it('has a detach method', () => {
expect(provider).to.have.property('detach').that.is.a('function');
expect(provider).to.have.property('hostDisconnected').that.is.a('function');
});
});
});
@@ -53,8 +53,8 @@ describe('UmbContextProvider', () => {
const localConsumer = new UmbContextConsumer(element, 'my-test-context', (_instance: MyClass) => {
expect(_instance.prop).to.eq('value from provider');
done();
localConsumer.detach();
localConsumer.hostDisconnected();
});
localConsumer.attach();
localConsumer.hostConnected();
});
});

View File

@@ -26,7 +26,7 @@ export class UmbContextProvider {
/**
* @memberof UmbContextProvider
*/
public attach() {
public hostConnected() {
this.host.addEventListener(umbContextRequestEventType, this._handleContextRequest);
this.host.dispatchEvent(new UmbContextProvideEventImplementation(this._contextAlias));
}
@@ -34,7 +34,7 @@ export class UmbContextProvider {
/**
* @memberof UmbContextProvider
*/
public detach() {
public hostDisconnected() {
this.host.removeEventListener(umbContextRequestEventType, this._handleContextRequest);
// TODO: fire unprovided event.
}

View File

@@ -0,0 +1,56 @@
import type { HTMLElementConstructor } from '../models';
import { UmbController } from './controller.interface';
export declare class UmbControllerHostInterface extends HTMLElement {
//#controllers:UmbController[];
//#attached:boolean;
addController(controller:UmbController): void;
}
/**
* This mixin enables the component to host controllers.
* This is done by calling the `consumeContext` method.
*
* @param {Object} superClass - superclass to be extended.
* @mixin
*/
export const UmbControllerHostMixin = <T extends HTMLElementConstructor>(superClass: T) => {
class UmbContextConsumerClass extends superClass {
#controllers: UmbController[] = [];
#attached = false;
/**
* Append a controller to this element.
* @param {UmbController} ctrl
*/
addController(ctrl: UmbController): void {
this.#controllers.push(ctrl);
if(this.#attached) {
ctrl.hostConnected();
}
}
connectedCallback() {
super.connectedCallback?.();
this.#attached = true;
this.#controllers.forEach((ctrl: UmbController) => ctrl.hostConnected());
}
disconnectedCallback() {
super.disconnectedCallback?.();
this.#attached = false;
this.#controllers.forEach((ctrl: UmbController) => ctrl.hostDisconnected());
}
}
return UmbContextConsumerClass as unknown as HTMLElementConstructor<UmbControllerHostInterface> & T;
};
declare global {
interface HTMLElement {
connectedCallback(): void;
disconnectedCallback(): void;
}
}

View File

@@ -0,0 +1,4 @@
export interface UmbController {
hostConnected(): void;
hostDisconnected(): void;
}

View File

@@ -0,0 +1,18 @@
import { Observable } from 'rxjs';
import { UmbObserver } from './observer';
import type { UmbController } from 'src/core/controller/controller.interface';
import { UmbControllerHostInterface } from 'src/core/controller/controller-host.mixin';
export class UmbObserverController<Y = any> extends UmbObserver<Y> implements UmbController {
constructor(host:UmbControllerHostInterface, source: Observable<any>, callback: (_value: Y) => void) {
super(source, callback);
host.addController(this);
}
hostConnected() {
return;
}
}

View File

@@ -10,10 +10,14 @@ export const UmbObserverMixin = <T extends HTMLElementConstructor>(superClass: T
_subscriptions: Map<Observable<any>, Subscription> = new Map();
observe<Y = any>(source: Observable<any>, callback: (_value: Y) => void): ()=>void {
// TODO: can be transferred to something using alias?
/*
if (this._subscriptions.has(source)) {
const subscription = this._subscriptions.get(source);
subscription?.unsubscribe();
}
*/
const subscription = source.subscribe((value) => callback(value));
this._subscriptions.set(source, subscription);

View File

@@ -0,0 +1,30 @@
import { Observable, Subscription } from 'rxjs';
export class UmbObserver<Y = any> {
#subscription!: Subscription;
constructor(source: Observable<any>, callback: (_value: Y) => void) {
// TODO: can be transferred to something using alias?
/*
if (this._subscriptions.has(source)) {
const subscription = this._subscriptions.get(source);
subscription?.unsubscribe();
}
*/
this.#subscription = source.subscribe((value) => callback(value));
}
// Notice controller class implements empty hostConnected().
hostDisconnected() {
this.#subscription.unsubscribe();
}
destroy(): void {
this.#subscription.unsubscribe();
}
};

View File

@@ -1,26 +1,23 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { ReactiveController, ReactiveControllerHost } from 'lit';
import { UmbController } from '../controller/controller.interface';
import { UmbControllerHostInterface } from '../controller/controller-host.mixin';
import { UmbContextConsumerController } from '../context-api/consume/context-consumer.controller';
import { ApiError, CancelablePromise, ProblemDetails } from '@umbraco-cms/backend-api';
import { UmbNotificationOptions, UmbNotificationService } from 'src/backoffice/core/services/notification';
import { UmbNotificationDefaultData } from 'src/backoffice/core/services/notification/layouts/default';
import { UmbContextConsumer } from '@umbraco-cms/context-api';
export class UmbResourceController implements UmbController {
export class UmbResourceController implements ReactiveController {
host: ReactiveControllerHost;
#promises: Promise<any>[] = [];
#notificationConsumer: UmbContextConsumer;
#notificationService?: UmbNotificationService;
constructor(host: ReactiveControllerHost) {
(this.host = host).addController(this);
this.#notificationConsumer = new UmbContextConsumer(
host as unknown as EventTarget,
'umbNotificationService',
constructor(host: UmbControllerHostInterface) {
host.addController(this);
new UmbContextConsumerController(host, 'umbNotificationService',
(_instance: UmbNotificationService) => {
this.#notificationService = _instance;
}
@@ -28,13 +25,12 @@ export class UmbResourceController implements ReactiveController {
}
hostConnected() {
this.#promises.length = 0;
this.#notificationConsumer.attach();
// TODO: Make sure we do the right thing here, as connected can be called multiple times without disconnected invoked.
//this.#promises.length = 0;
}
hostDisconnected() {
this.cancelAllResources();
this.#notificationConsumer.detach();
}
addResource(promise: Promise<any>): void {

View File

@@ -28,7 +28,7 @@
"@umbraco-cms/observable-api": ["src/core/observable-api"],
"@umbraco-cms/utils": ["src/core/utils"],
"@umbraco-cms/test-utils": ["src/core/test-utils"],
"@umbraco-cms/controllers": ["src/core/controllers"],
"@umbraco-cms/resources": ["src/core/resources"],
"@umbraco-cms/services": ["src/core/services"],
"@umbraco-cms/components/*": ["src/backoffice/components/*"],
"@umbraco-cms/stores/*": ["src/core/stores/*"],

View File

@@ -19,7 +19,7 @@ export default {
'@umbraco-cms/resource-api': './src/core/resource-api',
'@umbraco-cms/utils': './src/core/utils/index.ts',
'@umbraco-cms/test-utils': './src/core/test-utils/index.ts',
'@umbraco-cms/controllers': './src/core/controllers',
'@umbraco-cms/resources': './src/core/resources',
'@umbraco-cms/services': './src/core/services',
'@umbraco-cms/extensions-registry': './src/core/extensions-registry/index.ts',
},