Extensions: Adds @provideContext and @consumeContext decorators for a better developer experience (#20510)
* feat: adds first draft of a context consume decorator * feat: uses an options pattern * feat: changes approach to use `addInitializer` and `queueMicroTask` instead * feat: adds extra warning if context is consumed on disconnected controllers * feat: example implementation of consume decorator * feat: adds support for 'subscribe' * feat: initial work on provide decorator * docs: adds license to consume decorator * feat: adds support for umbraco controllers with `hostConnected` * feat: uses asPromise to handle one-time subscription instead * test: adds unit tests for consume decorator * feat: adds support for controllers through hostConnected injection * feat: adds support for controllers through hostConnected injection * test: adds unit tests for provide decorator * docs: adds more documentation around usage and adds a few warnings in console when it detects wrong usage * feat: removes unused controllerMap * docs: adds wording on standard vs legacy decorators * docs: clarifies usage around internal state * feat: adds proper return types for decorators * docs: adds more types * feat: makes element optional * feat: makes element optional * feat: uses @consume in the log viewer to showcase * chore: cleans up debug info * feat: renames to `consumeContext` and `provideContext` to stay inline with our own methods * chore: removes unneeded typings * chore: removes not needed check * chore: removes not needed check * test: adds test for rendered value * feat: splits up code into several smaller functions * Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * docs: augments code example for creating a context * Update src/Umbraco.Web.UI.Client/src/packages/log-viewer/workspace/views/search/components/log-viewer-search-input.element.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
import { UmbContextToken } from '../token/context-token.js';
|
||||
import type { UmbContextMinimal } from '../types.js';
|
||||
import { UmbContextProvider } from '../provide/context-provider.js';
|
||||
import { consumeContext } from './context-consume.decorator.js';
|
||||
import { aTimeout, elementUpdated, expect, fixture } from '@open-wc/testing';
|
||||
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
|
||||
import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api';
|
||||
import { html, state } from '@umbraco-cms/backoffice/external/lit';
|
||||
|
||||
class UmbTestContextConsumerClass implements UmbContextMinimal {
|
||||
public prop: string = 'value from provider';
|
||||
getHostElement() {
|
||||
return undefined as any;
|
||||
}
|
||||
}
|
||||
|
||||
const testToken = new UmbContextToken<UmbTestContextConsumerClass>('my-test-context');
|
||||
|
||||
class MyTestElement extends UmbLitElement {
|
||||
@consumeContext({
|
||||
context: testToken,
|
||||
})
|
||||
@state()
|
||||
contextValue?: UmbTestContextConsumerClass;
|
||||
|
||||
override render() {
|
||||
return html`<div>${this.contextValue?.prop ?? 'no context'}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('my-consume-test-element', MyTestElement);
|
||||
|
||||
describe('@consume decorator', () => {
|
||||
let provider: UmbContextProvider;
|
||||
let element: MyTestElement;
|
||||
|
||||
beforeEach(async () => {
|
||||
provider = new UmbContextProvider(document.body, testToken, new UmbTestContextConsumerClass());
|
||||
provider.hostConnected();
|
||||
|
||||
element = await fixture<MyTestElement>(`<my-consume-test-element></my-consume-test-element>`);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
provider.destroy();
|
||||
(provider as any) = undefined;
|
||||
});
|
||||
|
||||
it('should receive a context value when provided on the host', () => {
|
||||
expect(element.contextValue).to.equal(provider.providerInstance());
|
||||
expect(element.contextValue?.prop).to.equal('value from provider');
|
||||
});
|
||||
|
||||
it('should render the value from the context', async () => {
|
||||
expect(element).shadowDom.to.equal('<div>value from provider</div>');
|
||||
});
|
||||
|
||||
it('should work when the decorator is used in a controller', async () => {
|
||||
class MyController extends UmbControllerBase {
|
||||
@consumeContext({ context: testToken })
|
||||
contextValue?: UmbTestContextConsumerClass;
|
||||
}
|
||||
|
||||
const controller = new MyController(element);
|
||||
|
||||
await elementUpdated(element);
|
||||
|
||||
expect(element.contextValue).to.equal(provider.providerInstance());
|
||||
expect(controller.contextValue).to.equal(provider.providerInstance());
|
||||
});
|
||||
|
||||
it('should have called the callback first', async () => {
|
||||
let callbackCalled = false;
|
||||
|
||||
class MyCallbackTestElement extends UmbLitElement {
|
||||
@consumeContext({
|
||||
context: testToken,
|
||||
callback: () => {
|
||||
callbackCalled = true;
|
||||
},
|
||||
})
|
||||
contextValue?: UmbTestContextConsumerClass;
|
||||
}
|
||||
|
||||
customElements.define('my-callback-consume-test-element', MyCallbackTestElement);
|
||||
|
||||
const callbackElement = await fixture<MyCallbackTestElement>(
|
||||
`<my-callback-consume-test-element></my-callback-consume-test-element>`,
|
||||
);
|
||||
|
||||
await elementUpdated(callbackElement);
|
||||
|
||||
expect(callbackCalled).to.be.true;
|
||||
expect(callbackElement.contextValue).to.equal(provider.providerInstance());
|
||||
});
|
||||
|
||||
it('should update the context value when the provider instance changes', async () => {
|
||||
const newProviderInstance = new UmbTestContextConsumerClass();
|
||||
newProviderInstance.prop = 'new value from provider';
|
||||
|
||||
const newProvider = new UmbContextProvider(element, testToken, newProviderInstance);
|
||||
newProvider.hostConnected();
|
||||
|
||||
await elementUpdated(element);
|
||||
|
||||
expect(element.contextValue).to.equal(newProvider.providerInstance());
|
||||
expect(element.contextValue?.prop).to.equal(newProviderInstance.prop);
|
||||
});
|
||||
|
||||
it('should be able to consume without subscribing', async () => {
|
||||
class MyNoSubscribeTestController extends UmbControllerBase {
|
||||
@consumeContext({ context: testToken, subscribe: false })
|
||||
contextValue?: UmbTestContextConsumerClass;
|
||||
}
|
||||
|
||||
const controller = new MyNoSubscribeTestController(element);
|
||||
await aTimeout(0); // Wait a tick for promise to resolve
|
||||
|
||||
expect(controller.contextValue).to.equal(provider.providerInstance());
|
||||
|
||||
const newProviderInstance = new UmbTestContextConsumerClass();
|
||||
newProviderInstance.prop = 'new value from provider';
|
||||
|
||||
const newProvider = new UmbContextProvider(element, testToken, newProviderInstance);
|
||||
newProvider.hostConnected();
|
||||
|
||||
await aTimeout(0); // Wait a tick for promise to resolve
|
||||
|
||||
// Should still be the old value
|
||||
expect(controller.contextValue).to.not.equal(newProvider.providerInstance());
|
||||
expect(controller.contextValue?.prop).to.equal('value from provider');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,289 @@
|
||||
/*
|
||||
* Portions of this code are adapted from @lit/context
|
||||
* Copyright 2017 Google LLC
|
||||
* SPDX-License-Identifier: BSD-3-Clause
|
||||
*
|
||||
* Original source: https://github.com/lit/lit/tree/main/packages/context
|
||||
*
|
||||
* @license BSD-3-Clause
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import type { UmbContextToken } from '../token/index.js';
|
||||
import type { UmbContextMinimal } from '../types.js';
|
||||
import { UmbContextConsumerController } from './context-consumer.controller.js';
|
||||
import type { UmbContextCallback } from './context-request.event.js';
|
||||
|
||||
export interface UmbConsumeOptions<
|
||||
BaseType extends UmbContextMinimal = UmbContextMinimal,
|
||||
ResultType extends BaseType = BaseType,
|
||||
> {
|
||||
/**
|
||||
* The context to consume, either as a string alias or an UmbContextToken.
|
||||
*/
|
||||
context: string | UmbContextToken<BaseType, ResultType>;
|
||||
|
||||
/**
|
||||
* An optional callback that is invoked when the context value is set or changes.
|
||||
* Note, the class instance is probably not fully constructed when this is first invoked.
|
||||
* If you need to ensure the class is fully constructed, consider using a setter on the property instead.
|
||||
*/
|
||||
callback?: UmbContextCallback<ResultType>;
|
||||
|
||||
/**
|
||||
* If true, the context consumer will stay active and invoke the callback on context changes.
|
||||
* If false, the context consumer will use asPromise() to get the value once and then clean up.
|
||||
* @default true
|
||||
*/
|
||||
subscribe?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* A property decorator that adds an UmbContextConsumerController to the component
|
||||
* which will try and retrieve a value for the property via the Umbraco Context API.
|
||||
*
|
||||
* This decorator supports both modern "standard" decorators (Stage 3 TC39 proposal) and
|
||||
* legacy TypeScript experimental decorators for backward compatibility.
|
||||
*
|
||||
* @param {UmbConsumeOptions} options Configuration object containing context, callback, and subscribe options
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import {consumeContext} from '@umbraco-cms/backoffice/context-api';
|
||||
* import {UMB_WORKSPACE_CONTEXT} from './workspace.context-token.js';
|
||||
*
|
||||
* class MyElement extends UmbLitElement {
|
||||
* // Standard decorators (with 'accessor' keyword) - Modern approach
|
||||
* @consumeContext({context: UMB_WORKSPACE_CONTEXT})
|
||||
* accessor workspaceContext?: UmbWorkspaceContext;
|
||||
*
|
||||
* // Legacy decorators (without 'accessor') - Works with @state/@property
|
||||
* @consumeContext({context: UMB_USER_CONTEXT, subscribe: false})
|
||||
* @state()
|
||||
* currentUser?: UmbUserContext;
|
||||
* }
|
||||
* ```
|
||||
* @returns {ConsumeDecorator<ResultType>} A property decorator function
|
||||
*/
|
||||
export function consumeContext<
|
||||
BaseType extends UmbContextMinimal = UmbContextMinimal,
|
||||
ResultType extends BaseType = BaseType,
|
||||
>(options: UmbConsumeOptions<BaseType, ResultType>): ConsumeDecorator<ResultType> {
|
||||
const { context, callback, subscribe = true } = options;
|
||||
|
||||
return ((protoOrTarget: any, nameOrContext: PropertyKey | ClassAccessorDecoratorContext<any, ResultType>) => {
|
||||
if (typeof nameOrContext === 'object') {
|
||||
setupStandardDecorator(protoOrTarget, nameOrContext, context, callback, subscribe);
|
||||
return;
|
||||
}
|
||||
|
||||
setupLegacyDecorator(protoOrTarget, nameOrContext as string, context, callback, subscribe);
|
||||
}) as ConsumeDecorator<ResultType>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up a standard decorator (Stage 3 TC39 proposal) for auto-accessors.
|
||||
* This branch is used when decorating with the 'accessor' keyword.
|
||||
* Example: @consumeContext({context: TOKEN}) accessor myProp?: Type;
|
||||
*
|
||||
* The decorator receives a ClassAccessorDecoratorContext object which provides
|
||||
* addInitializer() to run code during class construction.
|
||||
*
|
||||
* This is the modern, standardized decorator API that will be the standard
|
||||
* when Lit 4.x is released.
|
||||
*
|
||||
* Note: Standard decorators currently don't work with @state()/@property()
|
||||
* decorators, which is why we still need the legacy branch.
|
||||
*/
|
||||
function setupStandardDecorator<BaseType extends UmbContextMinimal, ResultType extends BaseType>(
|
||||
protoOrTarget: any,
|
||||
decoratorContext: ClassAccessorDecoratorContext<any, ResultType>,
|
||||
context: string | UmbContextToken<BaseType, ResultType>,
|
||||
callback: UmbContextCallback<ResultType> | undefined,
|
||||
subscribe: boolean,
|
||||
): void {
|
||||
if (!('addInitializer' in decoratorContext)) {
|
||||
console.warn(
|
||||
'@consumeContext decorator: Standard decorator context does not support addInitializer. ' +
|
||||
'This should not happen with modern decorators.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
decoratorContext.addInitializer(function () {
|
||||
queueMicrotask(() => {
|
||||
if (subscribe) {
|
||||
// Continuous subscription - stays active and updates property on context changes
|
||||
new UmbContextConsumerController(this, context, (value) => {
|
||||
protoOrTarget.set.call(this, value);
|
||||
callback?.(value);
|
||||
});
|
||||
} else {
|
||||
// One-time consumption - uses asPromise() to get the value once and then cleans up
|
||||
const controller = new UmbContextConsumerController(this, context, callback);
|
||||
controller.asPromise().then((value) => {
|
||||
protoOrTarget.set.call(this, value);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up a legacy decorator (TypeScript experimental) for regular properties.
|
||||
* This branch is used when decorating without the 'accessor' keyword.
|
||||
* Example: @consumeContext({context: TOKEN}) @state() myProp?: Type;
|
||||
*
|
||||
* The decorator receives:
|
||||
* - protoOrTarget: The class prototype
|
||||
* - propertyKey: The property name (string)
|
||||
*
|
||||
* This is the older TypeScript experimental decorator API, still widely used
|
||||
* in Umbraco because it works with @state() and @property() decorators.
|
||||
* The 'accessor' keyword is not compatible with these decorators yet.
|
||||
*
|
||||
* We support three initialization strategies:
|
||||
* 1. addInitializer (if available, e.g., on LitElement classes)
|
||||
* 2. hostConnected wrapper (for UmbController classes)
|
||||
* 3. Warning (if neither is available)
|
||||
*/
|
||||
function setupLegacyDecorator<BaseType extends UmbContextMinimal, ResultType extends BaseType>(
|
||||
protoOrTarget: any,
|
||||
propertyKey: string,
|
||||
context: string | UmbContextToken<BaseType, ResultType>,
|
||||
callback: UmbContextCallback<ResultType> | undefined,
|
||||
subscribe: boolean,
|
||||
): void {
|
||||
const constructor = protoOrTarget.constructor as any;
|
||||
|
||||
// Strategy 1: Use addInitializer if available (LitElement classes)
|
||||
if (constructor.addInitializer) {
|
||||
constructor.addInitializer((element: any): void => {
|
||||
queueMicrotask(() => {
|
||||
if (subscribe) {
|
||||
// Continuous subscription
|
||||
new UmbContextConsumerController(element, context, (value) => {
|
||||
element[propertyKey] = value;
|
||||
callback?.(value);
|
||||
});
|
||||
} else {
|
||||
// One-time consumption using asPromise()
|
||||
const controller = new UmbContextConsumerController(element, context, callback);
|
||||
controller.asPromise().then((value) => {
|
||||
element[propertyKey] = value;
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Strategy 2: Wrap hostConnected for UmbController classes without addInitializer
|
||||
if ('hostConnected' in protoOrTarget && typeof protoOrTarget.hostConnected === 'function') {
|
||||
const originalHostConnected = protoOrTarget.hostConnected;
|
||||
|
||||
protoOrTarget.hostConnected = function (this: any) {
|
||||
// Set up consumer once, using a flag to prevent multiple setups
|
||||
if (!this.__consumeControllers) {
|
||||
this.__consumeControllers = new Map();
|
||||
}
|
||||
|
||||
if (!this.__consumeControllers.has(propertyKey)) {
|
||||
if (subscribe) {
|
||||
// Continuous subscription
|
||||
const controller = new UmbContextConsumerController(this, context, (value) => {
|
||||
this[propertyKey] = value;
|
||||
callback?.(value);
|
||||
});
|
||||
this.__consumeControllers.set(propertyKey, controller);
|
||||
} else {
|
||||
// One-time consumption using asPromise()
|
||||
const controller = new UmbContextConsumerController(this, context, callback);
|
||||
controller.asPromise().then((value) => {
|
||||
this[propertyKey] = value;
|
||||
});
|
||||
// Don't store in map since it cleans itself up
|
||||
}
|
||||
}
|
||||
|
||||
// Call original hostConnected if it exists
|
||||
originalHostConnected?.call(this);
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
// Strategy 3: No supported initialization method available
|
||||
console.warn(
|
||||
`@consumeContext applied to ${constructor.name}.${propertyKey} but neither addInitializer nor hostConnected is available. ` +
|
||||
`Make sure the class extends UmbLitElement, UmbControllerBase, or implements UmbController with hostConnected.`,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a public interface type that removes private and protected fields.
|
||||
* This allows accepting otherwise incompatible versions of the type (e.g. from
|
||||
* multiple copies of the same package in `node_modules`).
|
||||
*/
|
||||
type Interface<T> = {
|
||||
[K in keyof T]: T[K];
|
||||
};
|
||||
|
||||
declare class ReactiveElement {
|
||||
static addInitializer?: (initializer: (instance: any) => void) => void;
|
||||
}
|
||||
|
||||
declare class ReactiveController {
|
||||
hostConnected?: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* A type representing the base class of which the decorator should work
|
||||
* requiring either addInitializer (UmbLitElement) or hostConnected (UmbController).
|
||||
*/
|
||||
type ReactiveEntity = ReactiveElement | ReactiveController;
|
||||
|
||||
type ConsumeDecorator<ValueType> = {
|
||||
// legacy
|
||||
<K extends PropertyKey, Proto extends Interface<ReactiveEntity>>(
|
||||
protoOrDescriptor: Proto,
|
||||
name?: K,
|
||||
): FieldMustMatchProvidedType<Proto, K, ValueType>;
|
||||
|
||||
// standard
|
||||
<C extends Interface<ReactiveEntity>, V extends ValueType>(
|
||||
value: ClassAccessorDecoratorTarget<C, V>,
|
||||
context: ClassAccessorDecoratorContext<C, V>,
|
||||
): void;
|
||||
};
|
||||
|
||||
// Note TypeScript requires the return type of a decorator to be `void | any`
|
||||
type DecoratorReturn = void | any;
|
||||
|
||||
type FieldMustMatchProvidedType<Obj, Key extends PropertyKey, ProvidedType> =
|
||||
// First we check whether the object has the property as a required field
|
||||
Obj extends Record<Key, infer ConsumingType>
|
||||
? // Ok, it does, just check whether it's ok to assign the
|
||||
// provided type to the consuming field
|
||||
[ProvidedType] extends [ConsumingType]
|
||||
? DecoratorReturn
|
||||
: {
|
||||
message: 'provided type not assignable to consuming field';
|
||||
provided: ProvidedType;
|
||||
consuming: ConsumingType;
|
||||
}
|
||||
: // Next we check whether the object has the property as an optional field
|
||||
Obj extends Partial<Record<Key, infer ConsumingType>>
|
||||
? // Check assignability again. Note that we have to include undefined
|
||||
// here on the consuming type because it's optional.
|
||||
[ProvidedType] extends [ConsumingType | undefined]
|
||||
? DecoratorReturn
|
||||
: {
|
||||
message: 'provided type not assignable to consuming field';
|
||||
provided: ProvidedType;
|
||||
consuming: ConsumingType | undefined;
|
||||
}
|
||||
: // Ok, the field isn't present, so either someone's using consume
|
||||
// manually, i.e. not as a decorator (maybe don't do that! but if you do,
|
||||
// you're on your own for your type checking, sorry), or the field is
|
||||
// private, in which case we can't check it.
|
||||
DecoratorReturn;
|
||||
@@ -191,13 +191,21 @@ export class UmbContextConsumer<
|
||||
cancelAnimationFrame(this.#raf);
|
||||
}
|
||||
|
||||
const hostElement = this._retrieveHost();
|
||||
|
||||
// Add connection check to prevent requesting on disconnected elements
|
||||
if (hostElement && !hostElement.isConnected) {
|
||||
console.warn('UmbContextConsumer: Attempting to request context on disconnected element', hostElement);
|
||||
return;
|
||||
}
|
||||
|
||||
const event = new UmbContextRequestEventImplementation(
|
||||
this.#contextAlias,
|
||||
this.#apiAlias,
|
||||
this._onResponse,
|
||||
this.#stopAtContextMatch,
|
||||
);
|
||||
(this.#skipHost ? this._retrieveHost()?.parentNode : this._retrieveHost())?.dispatchEvent(event);
|
||||
(this.#skipHost ? hostElement?.parentNode : hostElement)?.dispatchEvent(event);
|
||||
|
||||
if (this.#promiseResolver && this.#promiseOptions?.preventTimeout !== true) {
|
||||
this.#raf = requestAnimationFrame(() => {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from './context-consume.decorator.js';
|
||||
export * from './context-consumer.controller.js';
|
||||
export * from './context-consumer.js';
|
||||
export * from './context-request.event.js';
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
import { UmbContextToken } from '../token/context-token.js';
|
||||
import type { UmbContextMinimal } from '../types.js';
|
||||
import { provideContext } from './context-provide.decorator.js';
|
||||
import { aTimeout, elementUpdated, expect, fixture } from '@open-wc/testing';
|
||||
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
|
||||
import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api';
|
||||
|
||||
class UmbTestContextConsumerClass implements UmbContextMinimal {
|
||||
public prop: string;
|
||||
|
||||
constructor(initialValue = 'value from provider') {
|
||||
this.prop = initialValue;
|
||||
}
|
||||
|
||||
getHostElement() {
|
||||
return document.body;
|
||||
}
|
||||
}
|
||||
|
||||
const testToken = new UmbContextToken<UmbTestContextConsumerClass>('my-test-context', 'testApi');
|
||||
|
||||
class MyTestRootElement extends UmbLitElement {
|
||||
@provideContext({ context: testToken })
|
||||
providerInstance = new UmbTestContextConsumerClass();
|
||||
}
|
||||
|
||||
customElements.define('my-provide-test-element', MyTestRootElement);
|
||||
|
||||
class MyTestElement extends UmbLitElement {
|
||||
contextValue?: UmbTestContextConsumerClass;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.consumeContext(testToken, (value) => {
|
||||
this.contextValue = value;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('my-consume-test-element', MyTestElement);
|
||||
|
||||
describe('@provide decorator', () => {
|
||||
let rootElement: MyTestRootElement;
|
||||
let element: MyTestElement;
|
||||
|
||||
beforeEach(async () => {
|
||||
rootElement = await fixture<MyTestRootElement>(
|
||||
`<my-provide-test-element><my-consume-test-element></my-consume-test-element></my-provide-test-element>`,
|
||||
);
|
||||
element = rootElement.querySelector('my-consume-test-element') as MyTestElement;
|
||||
});
|
||||
|
||||
afterEach(() => {});
|
||||
|
||||
it('should receive a context value when provided on the host', () => {
|
||||
expect(element.contextValue).to.equal(rootElement.providerInstance);
|
||||
});
|
||||
|
||||
it('should work when the decorator is used in a controller', async () => {
|
||||
class MyController extends UmbControllerBase {
|
||||
@provideContext({ context: testToken })
|
||||
providerInstance = new UmbTestContextConsumerClass('new value');
|
||||
}
|
||||
|
||||
const controller = new MyController(element);
|
||||
|
||||
await elementUpdated(element);
|
||||
|
||||
expect(element.contextValue).to.equal(controller.providerInstance);
|
||||
expect(controller.providerInstance.prop).to.equal('new value');
|
||||
});
|
||||
|
||||
it('should not update the instance when the property changes', async () => {
|
||||
// we do not support setting a new value on a provided property
|
||||
// as it would require a lot more logic to handle updating the context consumers
|
||||
// So for now we just warn the user that this is not supported
|
||||
// This might be revisited in the future if there is a need for it
|
||||
|
||||
const originalProviderInstance = rootElement.providerInstance;
|
||||
|
||||
const newProviderInstance = new UmbTestContextConsumerClass('new value from provider');
|
||||
rootElement.providerInstance = newProviderInstance;
|
||||
|
||||
await aTimeout(0);
|
||||
|
||||
expect(element.contextValue).to.equal(originalProviderInstance);
|
||||
expect(element.contextValue?.prop).to.equal(originalProviderInstance.prop);
|
||||
});
|
||||
|
||||
it('should update the context value when the provider instance is replaced', async () => {
|
||||
const newProviderInstance = new UmbTestContextConsumerClass();
|
||||
newProviderInstance.prop = 'new value from provider';
|
||||
|
||||
class MyUpdateTestElement extends UmbLitElement {
|
||||
@provideContext({ context: testToken })
|
||||
providerInstance = newProviderInstance;
|
||||
}
|
||||
customElements.define('my-update-provide-test-element', MyUpdateTestElement);
|
||||
|
||||
const newProvider = await fixture<MyUpdateTestElement>(
|
||||
`<my-update-provide-test-element><my-consume-test-element></my-consume-test-element></my-update-provide-test-element>`,
|
||||
);
|
||||
const element = newProvider.querySelector('my-consume-test-element') as MyTestElement;
|
||||
|
||||
await elementUpdated(element);
|
||||
|
||||
expect(element.contextValue).to.equal(newProviderInstance);
|
||||
expect(element.contextValue?.prop).to.equal(newProviderInstance.prop);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,258 @@
|
||||
/*
|
||||
* Portions of this code are adapted from @lit/context
|
||||
* Copyright 2017 Google LLC
|
||||
* SPDX-License-Identifier: BSD-3-Clause
|
||||
*
|
||||
* Original source: https://github.com/lit/lit/tree/main/packages/context
|
||||
*
|
||||
* @license BSD-3-Clause
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import type { UmbContextToken } from '../token/index.js';
|
||||
import type { UmbContextMinimal } from '../types.js';
|
||||
import { UmbContextProviderController } from './context-provider.controller.js';
|
||||
|
||||
export interface UmbProvideOptions<BaseType extends UmbContextMinimal, ResultType extends BaseType> {
|
||||
context: string | UmbContextToken<BaseType, ResultType>;
|
||||
}
|
||||
|
||||
/**
|
||||
* A property decorator that creates an UmbContextProviderController to provide
|
||||
* a context value to child elements via the Umbraco Context API.
|
||||
*
|
||||
* This decorator supports both modern "standard" decorators (Stage 3 TC39 proposal) and
|
||||
* legacy TypeScript experimental decorators for backward compatibility.
|
||||
*
|
||||
* The provider is created once during initialization with the property's initial value.
|
||||
* To update the provided value dynamically, keep a state inside the provided context instance
|
||||
* and update that state as needed. The context instance itself should remain the same.
|
||||
* You can use any of the Umb{*}State classes.
|
||||
*
|
||||
* @param {UmbProvideOptions} options Configuration object containing the context token
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import {provideContext} from '@umbraco-cms/backoffice/context-api';
|
||||
* import {UMB_WORKSPACE_CONTEXT} from './workspace.context-token.js';
|
||||
*
|
||||
* class MyWorkspaceElement extends UmbLitElement {
|
||||
* // Standard decorators - requires 'accessor' keyword
|
||||
* @provideContext({context: UMB_WORKSPACE_CONTEXT})
|
||||
* accessor workspaceContext = new UmbWorkspaceContext(this);
|
||||
*
|
||||
* // Legacy decorators - works without 'accessor'
|
||||
* @provideContext({context: UMB_WORKSPACE_CONTEXT})
|
||||
* workspaceContext = new UmbWorkspaceContext(this);
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // For dynamic updates, store the state inside the context instance
|
||||
* class MyContext extends UmbControllerBase {
|
||||
* someProperty = new UmbStringState('initial value');
|
||||
* }
|
||||
*
|
||||
* class MyElement extends UmbLitElement {
|
||||
* @provideContext({context: MY_CONTEXT})
|
||||
* private _myContext = new MyContext(this);
|
||||
*
|
||||
* updateValue(newValue: string) {
|
||||
* this._myContext.someProperty.setValue(newValue);
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @returns {ProvideDecorator<InstanceType>} A property decorator function
|
||||
*/
|
||||
export function provideContext<
|
||||
BaseType extends UmbContextMinimal = UmbContextMinimal,
|
||||
ResultType extends BaseType = BaseType,
|
||||
InstanceType extends ResultType = ResultType,
|
||||
>(options: UmbProvideOptions<BaseType, ResultType>): ProvideDecorator<InstanceType> {
|
||||
const { context } = options;
|
||||
|
||||
return ((
|
||||
protoOrTarget: any,
|
||||
nameOrContext: PropertyKey | ClassAccessorDecoratorContext<any, InstanceType>,
|
||||
): void | any => {
|
||||
if (typeof nameOrContext === 'object') {
|
||||
return setupStandardDecorator(protoOrTarget, context);
|
||||
}
|
||||
|
||||
setupLegacyDecorator(protoOrTarget, nameOrContext as string, context);
|
||||
}) as ProvideDecorator<ResultType>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up a standard decorator (Stage 3 TC39 proposal) for auto-accessors.
|
||||
* This branch is used when decorating with the 'accessor' keyword.
|
||||
* Example: @provideContext({context: TOKEN}) accessor myProp = new MyContext();
|
||||
*
|
||||
* The decorator receives a ClassAccessorDecoratorContext object and returns
|
||||
* an accessor descriptor that intercepts the property initialization.
|
||||
*
|
||||
* This is the modern, standardized decorator API that will be the standard
|
||||
* when Lit 4.x is released.
|
||||
*
|
||||
* Note: Standard decorators currently don't work with @state()/@property()
|
||||
* decorators, which is why we still need the legacy branch.
|
||||
*/
|
||||
function setupStandardDecorator<
|
||||
BaseType extends UmbContextMinimal,
|
||||
ResultType extends BaseType,
|
||||
InstanceType extends ResultType,
|
||||
>(protoOrTarget: any, context: string | UmbContextToken<BaseType, ResultType>) {
|
||||
return {
|
||||
get(this: any) {
|
||||
return protoOrTarget.get.call(this);
|
||||
},
|
||||
set(this: any, value: InstanceType) {
|
||||
return protoOrTarget.set.call(this, value);
|
||||
},
|
||||
init(this: any, value: InstanceType) {
|
||||
// Defer controller creation to avoid timing issues with private fields
|
||||
queueMicrotask(() => {
|
||||
new UmbContextProviderController<BaseType, ResultType, InstanceType>(this, context, value);
|
||||
});
|
||||
return value;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up a legacy decorator (TypeScript experimental) for regular properties.
|
||||
* This branch is used when decorating without the 'accessor' keyword.
|
||||
* Example: @provideContext({context: TOKEN}) myProp = new MyContext();
|
||||
*
|
||||
* The decorator receives:
|
||||
* - protoOrTarget: The class prototype
|
||||
* - propertyKey: The property name (string)
|
||||
*
|
||||
* This is the older TypeScript experimental decorator API, still widely used
|
||||
* in Umbraco because it works with @state() and @property() decorators.
|
||||
* The 'accessor' keyword is not compatible with these decorators yet.
|
||||
*
|
||||
* We support three initialization strategies:
|
||||
* 1. addInitializer (if available, e.g., on LitElement classes)
|
||||
* 2. hostConnected wrapper (for UmbController classes)
|
||||
* 3. Warning (if neither is available)
|
||||
*/
|
||||
function setupLegacyDecorator<
|
||||
BaseType extends UmbContextMinimal,
|
||||
ResultType extends BaseType,
|
||||
InstanceType extends ResultType,
|
||||
>(protoOrTarget: any, propertyKey: string, context: string | UmbContextToken<BaseType, ResultType>): void {
|
||||
const constructor = protoOrTarget.constructor as any;
|
||||
|
||||
// Strategy 1: Use addInitializer if available (LitElement classes)
|
||||
if (constructor.addInitializer) {
|
||||
constructor.addInitializer((element: any): void => {
|
||||
// Defer controller creation to avoid timing issues with private fields
|
||||
queueMicrotask(() => {
|
||||
const initialValue = element[propertyKey];
|
||||
new UmbContextProviderController<BaseType, ResultType, InstanceType>(element, context, initialValue);
|
||||
});
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Strategy 2: Wrap hostConnected for UmbController classes without addInitializer
|
||||
if ('hostConnected' in protoOrTarget && typeof protoOrTarget.hostConnected === 'function') {
|
||||
const originalHostConnected = protoOrTarget.hostConnected;
|
||||
|
||||
protoOrTarget.hostConnected = function (this: any) {
|
||||
// Set up provider once, using a flag to prevent multiple setups
|
||||
if (!this.__provideControllers) {
|
||||
this.__provideControllers = new Map();
|
||||
}
|
||||
|
||||
if (!this.__provideControllers.has(propertyKey)) {
|
||||
const initialValue = this[propertyKey];
|
||||
new UmbContextProviderController<BaseType, ResultType, InstanceType>(this, context, initialValue);
|
||||
// Mark as set up to prevent duplicate providers
|
||||
this.__provideControllers.set(propertyKey, true);
|
||||
}
|
||||
|
||||
// Call original hostConnected if it exists
|
||||
originalHostConnected?.call(this);
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
// Strategy 3: No supported initialization method available
|
||||
console.warn(
|
||||
`@provideContext applied to ${constructor.name}.${propertyKey} but neither addInitializer nor hostConnected is available. ` +
|
||||
`Make sure the class extends UmbLitElement, UmbControllerBase, or implements UmbController with hostConnected.`,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a public interface type that removes private and protected fields.
|
||||
* This allows accepting otherwise compatible versions of the type (e.g. from
|
||||
* multiple copies of the same package in `node_modules`).
|
||||
*/
|
||||
type Interface<T> = {
|
||||
[K in keyof T]: T[K];
|
||||
};
|
||||
|
||||
declare class ReactiveElement {
|
||||
static addInitializer?: (initializer: (instance: any) => void) => void;
|
||||
}
|
||||
|
||||
declare class ReactiveController {
|
||||
hostConnected?: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* A type representing the base class of which the decorator should work
|
||||
* requiring either addInitializer (UmbLitElement) or hostConnected (UmbController).
|
||||
*/
|
||||
type ReactiveEntity = ReactiveElement | ReactiveController;
|
||||
|
||||
type ProvideDecorator<ContextType> = {
|
||||
// legacy
|
||||
<K extends PropertyKey, Proto extends Interface<ReactiveEntity>>(
|
||||
protoOrDescriptor: Proto,
|
||||
name?: K,
|
||||
): FieldMustMatchContextType<Proto, K, ContextType>;
|
||||
|
||||
// standard
|
||||
<C extends Interface<ReactiveEntity>, V extends ContextType>(
|
||||
value: ClassAccessorDecoratorTarget<C, V>,
|
||||
context: ClassAccessorDecoratorContext<C, V>,
|
||||
): void;
|
||||
};
|
||||
|
||||
// Note TypeScript requires the return type of a decorator to be `void | any`
|
||||
type DecoratorReturn = void | any;
|
||||
|
||||
type FieldMustMatchContextType<Obj, Key extends PropertyKey, ContextType> =
|
||||
// First we check whether the object has the property as a required field
|
||||
Obj extends Record<Key, infer ProvidingType>
|
||||
? // Ok, it does, just check whether it's ok to assign the
|
||||
// provided type to the consuming field
|
||||
[ProvidingType] extends [ContextType]
|
||||
? DecoratorReturn
|
||||
: {
|
||||
message: 'providing field not assignable to context';
|
||||
context: ContextType;
|
||||
provided: ProvidingType;
|
||||
}
|
||||
: // Next we check whether the object has the property as an optional field
|
||||
Obj extends Partial<Record<Key, infer Providing>>
|
||||
? // Check assignability again. Note that we have to include undefined
|
||||
// here on the providing type because it's optional.
|
||||
[Providing | undefined] extends [ContextType]
|
||||
? DecoratorReturn
|
||||
: {
|
||||
message: 'providing field not assignable to context';
|
||||
context: ContextType;
|
||||
consuming: Providing | undefined;
|
||||
}
|
||||
: // Ok, the field isn't present, so either someone's using provide
|
||||
// manually, i.e. not as a decorator (maybe don't do that! but if you do,
|
||||
// you're on your own for your type checking, sorry), or the field is
|
||||
// private, in which case we can't check it.
|
||||
DecoratorReturn;
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from './context-provide.decorator.js';
|
||||
export * from './context-boundary.js';
|
||||
export * from './context-boundary.controller.js';
|
||||
export * from './context-provide.event.js';
|
||||
|
||||
@@ -44,7 +44,7 @@ class UmbLogViewerMessagesData extends UmbMockDBBase<LogMessageResponseModel> {
|
||||
}
|
||||
|
||||
getLevelCount() {
|
||||
const levels = this.data.map((log) => log.level ?? 'unknown');
|
||||
const levels = this.data.map((log) => log.level?.toLowerCase() ?? 'unknown');
|
||||
const counts = {};
|
||||
levels.forEach((level: string) => {
|
||||
//eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import type { UmbLogViewerDateRange, UmbLogViewerWorkspaceContext } from '../workspace/logviewer-workspace.context.js';
|
||||
import type { UmbLogViewerDateRange } from '../workspace/logviewer-workspace.context.js';
|
||||
import { UMB_APP_LOG_VIEWER_CONTEXT } from '../workspace/logviewer-workspace.context-token.js';
|
||||
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
|
||||
import { css, html, customElement, property, state } from '@umbraco-cms/backoffice/external/lit';
|
||||
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
|
||||
import { query as getQuery, path, toQueryString } from '@umbraco-cms/backoffice/router';
|
||||
import type { UUIInputEvent } from '@umbraco-cms/backoffice/external/uui';
|
||||
import { consumeContext } from '@umbraco-cms/backoffice/context-api';
|
||||
|
||||
@customElement('umb-log-viewer-date-range-selector')
|
||||
export class UmbLogViewerDateRangeSelectorElement extends UmbLitElement {
|
||||
@@ -17,20 +18,20 @@ export class UmbLogViewerDateRangeSelectorElement extends UmbLitElement {
|
||||
@property({ type: Boolean, reflect: true })
|
||||
horizontal = false;
|
||||
|
||||
#logViewerContext?: UmbLogViewerWorkspaceContext;
|
||||
#logViewerContext?: typeof UMB_APP_LOG_VIEWER_CONTEXT.TYPE;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.consumeContext(UMB_APP_LOG_VIEWER_CONTEXT, (instance) => {
|
||||
this.#logViewerContext = instance;
|
||||
this.#observeStuff();
|
||||
});
|
||||
@consumeContext({ context: UMB_APP_LOG_VIEWER_CONTEXT })
|
||||
private set _logViewerContext(value) {
|
||||
this.#logViewerContext = value;
|
||||
this.#observeStuff();
|
||||
}
|
||||
private get _logViewerContext() {
|
||||
return this.#logViewerContext;
|
||||
}
|
||||
|
||||
#observeStuff() {
|
||||
if (!this.#logViewerContext) return;
|
||||
this.observe(
|
||||
this.#logViewerContext.dateRange,
|
||||
this._logViewerContext?.dateRange,
|
||||
(dateRange: UmbLogViewerDateRange) => {
|
||||
this._startDate = dateRange.startDate;
|
||||
this._endDate = dateRange.endDate;
|
||||
@@ -50,7 +51,7 @@ export class UmbLogViewerDateRangeSelectorElement extends UmbLitElement {
|
||||
}
|
||||
|
||||
#updateFiltered() {
|
||||
this.#logViewerContext?.setDateRange({ startDate: this._startDate, endDate: this._endDate });
|
||||
this._logViewerContext?.setDateRange({ startDate: this._startDate, endDate: this._endDate });
|
||||
|
||||
const query = getQuery();
|
||||
const qs = toQueryString({
|
||||
@@ -71,7 +72,7 @@ export class UmbLogViewerDateRangeSelectorElement extends UmbLitElement {
|
||||
id="start-date"
|
||||
type="date"
|
||||
label="From"
|
||||
.max=${this.#logViewerContext?.today ?? ''}
|
||||
.max=${this._logViewerContext?.today ?? ''}
|
||||
.value=${this._startDate}></umb-input-date>
|
||||
</div>
|
||||
<div class="input-container">
|
||||
@@ -82,7 +83,7 @@ export class UmbLogViewerDateRangeSelectorElement extends UmbLitElement {
|
||||
type="date"
|
||||
label="To"
|
||||
.min=${this._startDate}
|
||||
.max=${this.#logViewerContext?.today ?? ''}
|
||||
.max=${this._logViewerContext?.today ?? ''}
|
||||
.value=${this._endDate}></umb-input-date>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -1,23 +1,26 @@
|
||||
import type { UmbLogViewerWorkspaceContext } from '../../../logviewer-workspace.context.js';
|
||||
import { UMB_APP_LOG_VIEWER_CONTEXT } from '../../../logviewer-workspace.context-token.js';
|
||||
import { html, nothing, customElement, property, state } from '@umbraco-cms/backoffice/external/lit';
|
||||
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
|
||||
import type { LoggerResponseModel } from '@umbraco-cms/backoffice/external/backend-api';
|
||||
import { consumeContext } from '@umbraco-cms/backoffice/context-api';
|
||||
|
||||
@customElement('umb-log-viewer-log-level-overview')
|
||||
export class UmbLogViewerLogLevelOverviewElement extends UmbLitElement {
|
||||
#logViewerContext?: UmbLogViewerWorkspaceContext;
|
||||
constructor() {
|
||||
super();
|
||||
this.consumeContext(UMB_APP_LOG_VIEWER_CONTEXT, (instance) => {
|
||||
this.#logViewerContext = instance;
|
||||
this.#logViewerContext?.getSavedSearches();
|
||||
this.#observeLogLevels();
|
||||
});
|
||||
#logViewerContext?: typeof UMB_APP_LOG_VIEWER_CONTEXT.TYPE;
|
||||
|
||||
@consumeContext({ context: UMB_APP_LOG_VIEWER_CONTEXT })
|
||||
private set _logViewerContext(value) {
|
||||
this.#logViewerContext = value;
|
||||
this.#logViewerContext?.getSavedSearches();
|
||||
this.#observeLogLevels();
|
||||
}
|
||||
private get _logViewerContext() {
|
||||
return this.#logViewerContext;
|
||||
}
|
||||
|
||||
@state()
|
||||
private _loggers: LoggerResponseModel[] = [];
|
||||
|
||||
/**
|
||||
* The name of the logger to get the level for. Defaults to 'Global'.
|
||||
* @memberof UmbLogViewerLogLevelOverviewElement
|
||||
@@ -26,8 +29,7 @@ export class UmbLogViewerLogLevelOverviewElement extends UmbLitElement {
|
||||
loggerName = 'Global';
|
||||
|
||||
#observeLogLevels() {
|
||||
if (!this.#logViewerContext) return;
|
||||
this.observe(this.#logViewerContext.loggers, (loggers) => {
|
||||
this.observe(this._logViewerContext?.loggers, (loggers) => {
|
||||
this._loggers = loggers ?? [];
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
import type { UmbLogViewerWorkspaceContext } from '../../../logviewer-workspace.context.js';
|
||||
import { UMB_APP_LOG_VIEWER_CONTEXT } from '../../../logviewer-workspace.context-token.js';
|
||||
import { css, html, customElement, state } from '@umbraco-cms/backoffice/external/lit';
|
||||
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
|
||||
import type { LogLevelCountsReponseModel } from '@umbraco-cms/backoffice/external/backend-api';
|
||||
import { consumeContext } from '@umbraco-cms/backoffice/context-api';
|
||||
|
||||
@customElement('umb-log-viewer-log-types-chart')
|
||||
export class UmbLogViewerLogTypesChartElement extends UmbLitElement {
|
||||
#logViewerContext?: UmbLogViewerWorkspaceContext;
|
||||
constructor() {
|
||||
super();
|
||||
this.consumeContext(UMB_APP_LOG_VIEWER_CONTEXT, (instance) => {
|
||||
this.#logViewerContext = instance;
|
||||
this.#logViewerContext?.getLogCount();
|
||||
this.#observeStuff();
|
||||
});
|
||||
#logViewerContext?: typeof UMB_APP_LOG_VIEWER_CONTEXT.TYPE;
|
||||
|
||||
@consumeContext({ context: UMB_APP_LOG_VIEWER_CONTEXT })
|
||||
private set _logViewerContext(value) {
|
||||
this.#logViewerContext = value;
|
||||
this.#logViewerContext?.getLogCount();
|
||||
this.#observeStuff();
|
||||
}
|
||||
private get _logViewerContext() {
|
||||
return this.#logViewerContext;
|
||||
}
|
||||
|
||||
@state()
|
||||
@@ -47,8 +49,7 @@ export class UmbLogViewerLogTypesChartElement extends UmbLitElement {
|
||||
}
|
||||
|
||||
#observeStuff() {
|
||||
if (!this.#logViewerContext) return;
|
||||
this.observe(this.#logViewerContext.logCount, (logLevel) => {
|
||||
this.observe(this._logViewerContext?.logCount, (logLevel) => {
|
||||
this._logLevelCountResponse = logLevel ?? null;
|
||||
this.setLogLevelCount();
|
||||
});
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { UmbLogViewerWorkspaceContext } from '../../../logviewer-workspace.context.js';
|
||||
import { UMB_APP_LOG_VIEWER_CONTEXT } from '../../../logviewer-workspace.context-token.js';
|
||||
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
|
||||
import { css, html, customElement, state, nothing } from '@umbraco-cms/backoffice/external/lit';
|
||||
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
|
||||
import type { LogTemplateResponseModel } from '@umbraco-cms/backoffice/external/backend-api';
|
||||
import type { UUIPaginationEvent } from '@umbraco-cms/backoffice/external/uui';
|
||||
import { consumeContext } from '@umbraco-cms/backoffice/context-api';
|
||||
|
||||
@customElement('umb-log-viewer-message-templates-overview')
|
||||
export class UmbLogViewerMessageTemplatesOverviewElement extends UmbLitElement {
|
||||
@@ -17,19 +17,20 @@ export class UmbLogViewerMessageTemplatesOverviewElement extends UmbLitElement {
|
||||
@state()
|
||||
private _messageTemplates: Array<LogTemplateResponseModel> = [];
|
||||
|
||||
#logViewerContext?: UmbLogViewerWorkspaceContext;
|
||||
constructor() {
|
||||
super();
|
||||
this.consumeContext(UMB_APP_LOG_VIEWER_CONTEXT, (instance) => {
|
||||
this.#logViewerContext = instance;
|
||||
this.#logViewerContext?.getMessageTemplates(0, this.#itemsPerPage);
|
||||
this.#observeStuff();
|
||||
});
|
||||
#logViewerContext?: typeof UMB_APP_LOG_VIEWER_CONTEXT.TYPE;
|
||||
|
||||
@consumeContext({ context: UMB_APP_LOG_VIEWER_CONTEXT })
|
||||
private set _logViewerContext(value) {
|
||||
this.#logViewerContext = value;
|
||||
this.#getMessageTemplates();
|
||||
this.#observeStuff();
|
||||
}
|
||||
private get _logViewerContext() {
|
||||
return this.#logViewerContext;
|
||||
}
|
||||
|
||||
#observeStuff() {
|
||||
if (!this.#logViewerContext) return;
|
||||
this.observe(this.#logViewerContext.messageTemplates, (templates) => {
|
||||
this.observe(this._logViewerContext?.messageTemplates, (templates) => {
|
||||
this._messageTemplates = templates?.items ?? [];
|
||||
this._total = templates?.total ?? 0;
|
||||
});
|
||||
@@ -37,7 +38,7 @@ export class UmbLogViewerMessageTemplatesOverviewElement extends UmbLitElement {
|
||||
|
||||
#getMessageTemplates() {
|
||||
const skip = this.#currentPage * this.#itemsPerPage - this.#itemsPerPage;
|
||||
this.#logViewerContext?.getMessageTemplates(skip, this.#itemsPerPage);
|
||||
this._logViewerContext?.getMessageTemplates(skip, this.#itemsPerPage);
|
||||
}
|
||||
|
||||
#onChangePage(event: UUIPaginationEvent) {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { UmbLogViewerWorkspaceContext } from '../../../logviewer-workspace.context.js';
|
||||
import { UMB_APP_LOG_VIEWER_CONTEXT } from '../../../logviewer-workspace.context-token.js';
|
||||
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
|
||||
import { css, html, customElement, state, nothing } from '@umbraco-cms/backoffice/external/lit';
|
||||
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
|
||||
import type { SavedLogSearchResponseModel } from '@umbraco-cms/backoffice/external/backend-api';
|
||||
import type { UUIPaginationEvent } from '@umbraco-cms/backoffice/external/uui';
|
||||
import { consumeContext } from '@umbraco-cms/backoffice/context-api';
|
||||
|
||||
@customElement('umb-log-viewer-saved-searches-overview')
|
||||
export class UmbLogViewerSavedSearchesOverviewElement extends UmbLitElement {
|
||||
@@ -17,20 +17,20 @@ export class UmbLogViewerSavedSearchesOverviewElement extends UmbLitElement {
|
||||
@state()
|
||||
private _total = 0;
|
||||
|
||||
#logViewerContext?: UmbLogViewerWorkspaceContext;
|
||||
#logViewerContext?: typeof UMB_APP_LOG_VIEWER_CONTEXT.TYPE;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.consumeContext(UMB_APP_LOG_VIEWER_CONTEXT, (instance) => {
|
||||
this.#logViewerContext = instance;
|
||||
this.#logViewerContext?.getSavedSearches({ skip: 0, take: this.#itemsPerPage });
|
||||
this.#observeStuff();
|
||||
});
|
||||
@consumeContext({ context: UMB_APP_LOG_VIEWER_CONTEXT })
|
||||
private set _logViewerContext(value) {
|
||||
this.#logViewerContext = value;
|
||||
this.#getSavedSearches();
|
||||
this.#observeStuff();
|
||||
}
|
||||
private get _logViewerContext() {
|
||||
return this.#logViewerContext;
|
||||
}
|
||||
|
||||
#observeStuff() {
|
||||
if (!this.#logViewerContext) return;
|
||||
this.observe(this.#logViewerContext.savedSearches, (savedSearches) => {
|
||||
this.observe(this._logViewerContext?.savedSearches, (savedSearches) => {
|
||||
this._savedSearches = savedSearches?.items ?? [];
|
||||
this._total = savedSearches?.total ?? 0;
|
||||
});
|
||||
@@ -38,7 +38,7 @@ export class UmbLogViewerSavedSearchesOverviewElement extends UmbLitElement {
|
||||
|
||||
#getSavedSearches() {
|
||||
const skip = this.#currentPage * this.#itemsPerPage - this.#itemsPerPage;
|
||||
this.#logViewerContext?.getSavedSearches({ skip, take: this.#itemsPerPage });
|
||||
this._logViewerContext?.getSavedSearches({ skip, take: this.#itemsPerPage });
|
||||
}
|
||||
|
||||
#onChangePage(event: UUIPaginationEvent) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { UmbLogViewerWorkspaceContext } from '../../logviewer-workspace.context.js';
|
||||
import { UMB_APP_LOG_VIEWER_CONTEXT } from '../../logviewer-workspace.context-token.js';
|
||||
import { css, html, customElement, state } from '@umbraco-cms/backoffice/external/lit';
|
||||
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
|
||||
import { consumeContext } from '@umbraco-cms/backoffice/context-api';
|
||||
|
||||
//TODO: add a disabled attribute to the show more button when the total number of items is correctly returned from the endpoint
|
||||
@customElement('umb-log-viewer-overview-view')
|
||||
@@ -12,28 +12,29 @@ export class UmbLogViewerOverviewViewElement extends UmbLitElement {
|
||||
@state()
|
||||
private _canShowLogs = false;
|
||||
|
||||
#logViewerContext?: UmbLogViewerWorkspaceContext;
|
||||
constructor() {
|
||||
super();
|
||||
this.consumeContext(UMB_APP_LOG_VIEWER_CONTEXT, (instance) => {
|
||||
this.#logViewerContext = instance;
|
||||
this.#observeErrorCount();
|
||||
this.#observeCanShowLogs();
|
||||
this.#logViewerContext?.getLogLevels(0, 100);
|
||||
});
|
||||
#logViewerContext?: typeof UMB_APP_LOG_VIEWER_CONTEXT.TYPE;
|
||||
|
||||
@consumeContext({
|
||||
context: UMB_APP_LOG_VIEWER_CONTEXT,
|
||||
})
|
||||
private set _logViewerContext(value) {
|
||||
this.#logViewerContext = value;
|
||||
this.#observeErrorCount();
|
||||
this.#observeCanShowLogs();
|
||||
value?.getLogLevels(0, 100);
|
||||
}
|
||||
private get _logViewerContext() {
|
||||
return this.#logViewerContext;
|
||||
}
|
||||
|
||||
#observeErrorCount() {
|
||||
if (!this.#logViewerContext) return;
|
||||
|
||||
this.observe(this.#logViewerContext.logCount, (logLevelCount) => {
|
||||
this.observe(this._logViewerContext?.logCount, (logLevelCount) => {
|
||||
this._errorCount = logLevelCount?.error;
|
||||
});
|
||||
}
|
||||
|
||||
#observeCanShowLogs() {
|
||||
if (!this.#logViewerContext) return;
|
||||
this.observe(this.#logViewerContext.canShowLogs, (canShowLogs) => {
|
||||
this.observe(this._logViewerContext?.canShowLogs, (canShowLogs) => {
|
||||
this._canShowLogs = canShowLogs ?? false;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { UmbLogViewerWorkspaceContext } from '../../../logviewer-workspace.context.js';
|
||||
import { UMB_APP_LOG_VIEWER_CONTEXT } from '../../../logviewer-workspace.context-token.js';
|
||||
import type { UUICheckboxElement } from '@umbraco-cms/backoffice/external/uui';
|
||||
import { css, html, customElement, queryAll, state } from '@umbraco-cms/backoffice/external/lit';
|
||||
@@ -6,6 +5,7 @@ import { debounce } from '@umbraco-cms/backoffice/utils';
|
||||
import { LogLevelModel } from '@umbraco-cms/backoffice/external/backend-api';
|
||||
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
|
||||
import { path, query, toQueryString } from '@umbraco-cms/backoffice/router';
|
||||
import { consumeContext } from '@umbraco-cms/backoffice/context-api';
|
||||
|
||||
@customElement('umb-log-viewer-log-level-filter-menu')
|
||||
export class UmbLogViewerLogLevelFilterMenuElement extends UmbLitElement {
|
||||
@@ -15,27 +15,24 @@ export class UmbLogViewerLogLevelFilterMenuElement extends UmbLitElement {
|
||||
@state()
|
||||
private _logLevelFilter: LogLevelModel[] = [];
|
||||
|
||||
#logViewerContext?: UmbLogViewerWorkspaceContext;
|
||||
#logViewerContext?: typeof UMB_APP_LOG_VIEWER_CONTEXT.TYPE;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.consumeContext(UMB_APP_LOG_VIEWER_CONTEXT, (instance) => {
|
||||
this.#logViewerContext = instance;
|
||||
this.#observeLogLevelFilter();
|
||||
});
|
||||
@consumeContext({ context: UMB_APP_LOG_VIEWER_CONTEXT })
|
||||
private set _logViewerContext(value) {
|
||||
this.#logViewerContext = value;
|
||||
this.#observeLogLevelFilter();
|
||||
}
|
||||
private get _logViewerContext() {
|
||||
return this.#logViewerContext;
|
||||
}
|
||||
|
||||
#observeLogLevelFilter() {
|
||||
if (!this.#logViewerContext) return;
|
||||
|
||||
this.observe(this.#logViewerContext.logLevelsFilter, (levelsFilter) => {
|
||||
this.observe(this._logViewerContext?.logLevelsFilter, (levelsFilter) => {
|
||||
this._logLevelFilter = levelsFilter ?? [];
|
||||
});
|
||||
}
|
||||
|
||||
#setLogLevel() {
|
||||
if (!this.#logViewerContext) return;
|
||||
|
||||
const logLevels = Array.from(this._logLevelSelectorCheckboxes)
|
||||
.filter((checkbox) => checkbox.checked)
|
||||
.map((checkbox) => checkbox.value as LogLevelModel);
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { UmbLogViewerWorkspaceContext } from '../../../logviewer-workspace.context.js';
|
||||
import { UMB_APP_LOG_VIEWER_CONTEXT } from '../../../logviewer-workspace.context-token.js';
|
||||
import type { UUIScrollContainerElement, UUIPaginationElement } from '@umbraco-cms/backoffice/external/uui';
|
||||
import { css, html, customElement, query, state } from '@umbraco-cms/backoffice/external/lit';
|
||||
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
|
||||
import type { LogMessageResponseModel } from '@umbraco-cms/backoffice/external/backend-api';
|
||||
import { DirectionModel } from '@umbraco-cms/backoffice/external/backend-api';
|
||||
import { consumeContext } from '@umbraco-cms/backoffice/context-api';
|
||||
|
||||
@customElement('umb-log-viewer-messages-list')
|
||||
export class UmbLogViewerMessagesListElement extends UmbLitElement {
|
||||
@@ -23,46 +23,45 @@ export class UmbLogViewerMessagesListElement extends UmbLitElement {
|
||||
@state()
|
||||
private _isLoading = true;
|
||||
|
||||
#logViewerContext?: UmbLogViewerWorkspaceContext;
|
||||
#logViewerContext?: typeof UMB_APP_LOG_VIEWER_CONTEXT.TYPE;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.consumeContext(UMB_APP_LOG_VIEWER_CONTEXT, (instance) => {
|
||||
this.#logViewerContext = instance;
|
||||
this.#observeLogs();
|
||||
});
|
||||
@consumeContext({ context: UMB_APP_LOG_VIEWER_CONTEXT })
|
||||
private set _logViewerContext(value) {
|
||||
this.#logViewerContext = value;
|
||||
this.#observeLogs();
|
||||
}
|
||||
private get _logViewerContext() {
|
||||
return this.#logViewerContext;
|
||||
}
|
||||
|
||||
#observeLogs() {
|
||||
if (!this.#logViewerContext) return;
|
||||
|
||||
this.observe(this.#logViewerContext.logs, (logs) => {
|
||||
this.observe(this._logViewerContext?.logs, (logs) => {
|
||||
this._logs = logs ?? [];
|
||||
});
|
||||
|
||||
this.observe(this.#logViewerContext.isLoadingLogs, (isLoading) => {
|
||||
this._isLoading = isLoading === null ? this._isLoading : isLoading;
|
||||
this.observe(this._logViewerContext?.isLoadingLogs, (isLoading) => {
|
||||
this._isLoading = isLoading ?? this._isLoading;
|
||||
});
|
||||
|
||||
this.observe(this.#logViewerContext.logsTotal, (total) => {
|
||||
this.observe(this._logViewerContext?.logsTotal, (total) => {
|
||||
this._logsTotal = total ?? 0;
|
||||
});
|
||||
|
||||
this.observe(this.#logViewerContext.sortingDirection, (direction) => {
|
||||
this._sortingDirection = direction;
|
||||
this.observe(this._logViewerContext?.sortingDirection, (direction) => {
|
||||
this._sortingDirection = direction ?? this._sortingDirection;
|
||||
});
|
||||
}
|
||||
|
||||
#sortLogs() {
|
||||
this.#logViewerContext?.toggleSortOrder();
|
||||
this.#logViewerContext?.setCurrentPage(1);
|
||||
this.#logViewerContext?.getLogs();
|
||||
this._logViewerContext?.toggleSortOrder();
|
||||
this._logViewerContext?.setCurrentPage(1);
|
||||
this._logViewerContext?.getLogs();
|
||||
}
|
||||
|
||||
#onPageChange(event: Event): void {
|
||||
const current = (event.target as UUIPaginationElement).current;
|
||||
this.#logViewerContext?.setCurrentPage(current);
|
||||
this.#logViewerContext?.getLogs();
|
||||
this._logViewerContext?.setCurrentPage(current);
|
||||
this._logViewerContext?.getLogs();
|
||||
this._logsScrollContainer.scrollTop = 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
import type {
|
||||
UmbPoolingConfig,
|
||||
UmbPoolingInterval,
|
||||
UmbLogViewerWorkspaceContext,
|
||||
} from '../../../logviewer-workspace.context.js';
|
||||
import type { UmbPoolingConfig, UmbPoolingInterval } from '../../../logviewer-workspace.context.js';
|
||||
import { UMB_APP_LOG_VIEWER_CONTEXT } from '../../../logviewer-workspace.context-token.js';
|
||||
import { css, html, customElement, query, state } from '@umbraco-cms/backoffice/external/lit';
|
||||
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
|
||||
import type { UmbDropdownElement } from '@umbraco-cms/backoffice/components';
|
||||
import { consumeContext } from '@umbraco-cms/backoffice/context-api';
|
||||
|
||||
@customElement('umb-log-viewer-polling-button')
|
||||
export class UmbLogViewerPollingButtonElement extends UmbLitElement {
|
||||
@@ -18,30 +15,29 @@ export class UmbLogViewerPollingButtonElement extends UmbLitElement {
|
||||
|
||||
#pollingIntervals: UmbPoolingInterval[] = [2000, 5000, 10000, 20000, 30000];
|
||||
|
||||
#logViewerContext?: UmbLogViewerWorkspaceContext;
|
||||
#logViewerContext?: typeof UMB_APP_LOG_VIEWER_CONTEXT.TYPE;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.consumeContext(UMB_APP_LOG_VIEWER_CONTEXT, (instance) => {
|
||||
this.#logViewerContext = instance;
|
||||
this.#observePoolingConfig();
|
||||
});
|
||||
@consumeContext({ context: UMB_APP_LOG_VIEWER_CONTEXT })
|
||||
private set _logViewerContext(value) {
|
||||
this.#logViewerContext = value;
|
||||
this.#observePoolingConfig();
|
||||
}
|
||||
private get _logViewerContext() {
|
||||
return this.#logViewerContext;
|
||||
}
|
||||
|
||||
#observePoolingConfig() {
|
||||
if (!this.#logViewerContext) return;
|
||||
|
||||
this.observe(this.#logViewerContext.polling, (poolingConfig) => {
|
||||
this._poolingConfig = { ...poolingConfig };
|
||||
this.observe(this._logViewerContext?.polling, (poolingConfig) => {
|
||||
this._poolingConfig = poolingConfig ? { ...poolingConfig } : { enabled: false, interval: 0 };
|
||||
});
|
||||
}
|
||||
|
||||
#togglePolling() {
|
||||
this.#logViewerContext?.togglePolling();
|
||||
this._logViewerContext?.togglePolling();
|
||||
}
|
||||
|
||||
#setPolingInterval = (interval: UmbPoolingInterval) => {
|
||||
this.#logViewerContext?.setPollingInterval(interval);
|
||||
this._logViewerContext?.setPollingInterval(interval);
|
||||
|
||||
this.#closePoolingPopover();
|
||||
};
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { UmbLogViewerWorkspaceContext } from '../../../logviewer-workspace.context.js';
|
||||
import { UMB_APP_LOG_VIEWER_CONTEXT } from '../../../logviewer-workspace.context-token.js';
|
||||
import { UMB_LOG_VIEWER_SAVE_SEARCH_MODAL } from './log-viewer-search-input-modal.modal-token.js';
|
||||
import { css, html, customElement, query, state } from '@umbraco-cms/backoffice/external/lit';
|
||||
@@ -12,6 +11,7 @@ import type { UmbDropdownElement } from '@umbraco-cms/backoffice/components';
|
||||
import type { UUIInputElement } from '@umbraco-cms/backoffice/external/uui';
|
||||
|
||||
import './log-viewer-search-input-modal.element.js';
|
||||
import { consumeContext } from '@umbraco-cms/backoffice/context-api';
|
||||
|
||||
@customElement('umb-log-viewer-search-input')
|
||||
export class UmbLogViewerSearchInputElement extends UmbLitElement {
|
||||
@@ -33,15 +33,20 @@ export class UmbLogViewerSearchInputElement extends UmbLitElement {
|
||||
// TODO: Revisit this code, to not use RxJS directly:
|
||||
#inputQuery$ = new Subject<string>();
|
||||
|
||||
#logViewerContext?: UmbLogViewerWorkspaceContext;
|
||||
#logViewerContext?: typeof UMB_APP_LOG_VIEWER_CONTEXT.TYPE;
|
||||
|
||||
@consumeContext({ context: UMB_APP_LOG_VIEWER_CONTEXT })
|
||||
private set _logViewerContext(value) {
|
||||
this.#logViewerContext = value;
|
||||
this.#observeStuff();
|
||||
this.#logViewerContext?.getSavedSearches();
|
||||
}
|
||||
private get _logViewerContext() {
|
||||
return this.#logViewerContext;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.consumeContext(UMB_APP_LOG_VIEWER_CONTEXT, (instance) => {
|
||||
this.#logViewerContext = instance;
|
||||
this.#observeStuff();
|
||||
this.#logViewerContext?.getSavedSearches();
|
||||
});
|
||||
|
||||
this.#inputQuery$
|
||||
.pipe(
|
||||
@@ -49,7 +54,7 @@ export class UmbLogViewerSearchInputElement extends UmbLitElement {
|
||||
debounceTime(250),
|
||||
)
|
||||
.subscribe((query) => {
|
||||
this.#logViewerContext?.setFilterExpression(query);
|
||||
this._logViewerContext?.setFilterExpression(query);
|
||||
this.#persist(query);
|
||||
this._isQuerySaved = this._savedSearches.some((search) => search.query === query);
|
||||
this._showLoader = false;
|
||||
@@ -57,15 +62,14 @@ export class UmbLogViewerSearchInputElement extends UmbLitElement {
|
||||
}
|
||||
|
||||
#observeStuff() {
|
||||
if (!this.#logViewerContext) return;
|
||||
this.observe(this.#logViewerContext.savedSearches, (savedSearches) => {
|
||||
this.observe(this._logViewerContext?.savedSearches, (savedSearches) => {
|
||||
this._savedSearches = savedSearches?.items ?? [];
|
||||
this._isQuerySaved = this._savedSearches.some((search) => search.query === this._inputQuery);
|
||||
});
|
||||
|
||||
this.observe(this.#logViewerContext.filterExpression, (query) => {
|
||||
this._inputQuery = query;
|
||||
this._isQuerySaved = this._savedSearches.some((search) => search.query === query);
|
||||
this.observe(this._logViewerContext?.filterExpression, (query) => {
|
||||
this._inputQuery = query ?? '';
|
||||
this._isQuerySaved = this._savedSearches.some((search) => search.query === this._inputQuery);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -92,11 +96,11 @@ export class UmbLogViewerSearchInputElement extends UmbLitElement {
|
||||
|
||||
#clearQuery() {
|
||||
this.#inputQuery$.next('');
|
||||
this.#logViewerContext?.setFilterExpression('');
|
||||
this._logViewerContext?.setFilterExpression('');
|
||||
}
|
||||
|
||||
#saveSearch(savedSearch: SavedLogSearchResponseModel) {
|
||||
this.#logViewerContext?.saveSearch(savedSearch);
|
||||
this._logViewerContext?.saveSearch(savedSearch);
|
||||
}
|
||||
|
||||
async #removeSearch(name: string) {
|
||||
@@ -107,7 +111,7 @@ export class UmbLogViewerSearchInputElement extends UmbLitElement {
|
||||
confirmLabel: 'Delete',
|
||||
});
|
||||
|
||||
this.#logViewerContext?.removeSearch({ name });
|
||||
this._logViewerContext?.removeSearch({ name });
|
||||
//this.dispatchEvent(new UmbDeleteEvent());
|
||||
}
|
||||
|
||||
|
||||
@@ -1,32 +1,32 @@
|
||||
import type { UmbLogViewerWorkspaceContext } from '../../logviewer-workspace.context.js';
|
||||
import { UMB_APP_LOG_VIEWER_CONTEXT } from '../../logviewer-workspace.context-token.js';
|
||||
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
|
||||
import { css, html, customElement, state } from '@umbraco-cms/backoffice/external/lit';
|
||||
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
|
||||
import type { UmbObserverController } from '@umbraco-cms/backoffice/observable-api';
|
||||
import { consumeContext } from '@umbraco-cms/backoffice/context-api';
|
||||
|
||||
@customElement('umb-log-viewer-search-view')
|
||||
export class UmbLogViewerSearchViewElement extends UmbLitElement {
|
||||
@state()
|
||||
private _canShowLogs = true;
|
||||
|
||||
#logViewerContext?: UmbLogViewerWorkspaceContext;
|
||||
#logViewerContext?: typeof UMB_APP_LOG_VIEWER_CONTEXT.TYPE;
|
||||
|
||||
@consumeContext({ context: UMB_APP_LOG_VIEWER_CONTEXT })
|
||||
private set _logViewerContext(value) {
|
||||
this.#logViewerContext = value;
|
||||
this.#observeCanShowLogs();
|
||||
}
|
||||
private get _logViewerContext() {
|
||||
return this.#logViewerContext;
|
||||
}
|
||||
|
||||
#canShowLogsObserver?: UmbObserverController<boolean | null>;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.consumeContext(UMB_APP_LOG_VIEWER_CONTEXT, (instance) => {
|
||||
this.#logViewerContext = instance;
|
||||
this.#observeCanShowLogs();
|
||||
});
|
||||
}
|
||||
|
||||
#observeCanShowLogs() {
|
||||
if (this.#canShowLogsObserver) this.#canShowLogsObserver.destroy();
|
||||
if (!this.#logViewerContext) return;
|
||||
|
||||
this.#canShowLogsObserver = this.observe(this.#logViewerContext.canShowLogs, (canShowLogs) => {
|
||||
this.#canShowLogsObserver = this.observe(this._logViewerContext?.canShowLogs, (canShowLogs) => {
|
||||
this._canShowLogs = canShowLogs ?? this._canShowLogs;
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user