Merge branch 'main' into feature/media-types

This commit is contained in:
Mads Rasmussen
2023-11-22 10:51:27 +01:00
52 changed files with 756 additions and 428 deletions

View File

@@ -5,20 +5,22 @@ import { UmbContextConsumer } from './context-consumer.js';
import { UmbContextRequestEventImplementation, UMB_CONTENT_REQUEST_EVENT_TYPE } from './context-request.event.js';
const testContextAlias = 'my-test-context';
const testContextAliasAndApiAlias = 'my-test-context#testApi';
const testContextAliasAndNotExstingApiAlias = 'my-test-context#notExistingTestApi';
class UmbTestContextConsumerClass {
public prop: string = 'value from provider';
}
describe('UmbContextConsumer', () => {
let consumer: UmbContextConsumer;
beforeEach(() => {
// eslint-disable-next-line @typescript-eslint/no-empty-function
consumer = new UmbContextConsumer(document.body, testContextAlias, () => {});
});
describe('Public API', () => {
let consumer: UmbContextConsumer;
beforeEach(() => {
// eslint-disable-next-line @typescript-eslint/no-empty-function
consumer = new UmbContextConsumer(document.body, testContextAlias, () => {});
});
describe('properties', () => {
it('has a instance property', () => {
expect(consumer).to.have.property('instance').that.is.undefined;
@@ -45,128 +47,178 @@ describe('UmbContextConsumer', () => {
});
});
it('works with UmbContextProvider', (done) => {
const provider = new UmbContextProvider(document.body, testContextAlias, new UmbTestContextConsumerClass());
provider.hostConnected();
describe('Simple implementation', () => {
it('works with UmbContextProvider', (done) => {
const provider = new UmbContextProvider(document.body, testContextAlias, new UmbTestContextConsumerClass());
provider.hostConnected();
const element = document.createElement('div');
document.body.appendChild(element);
const element = document.createElement('div');
document.body.appendChild(element);
const localConsumer = new UmbContextConsumer(
element,
testContextAlias,
(_instance: UmbTestContextConsumerClass | undefined) => {
const localConsumer = new UmbContextConsumer(
element,
testContextAlias,
(_instance: UmbTestContextConsumerClass | undefined) => {
if (_instance) {
expect(_instance.prop).to.eq('value from provider');
done();
localConsumer.hostDisconnected();
provider.hostDisconnected();
}
},
);
localConsumer.hostConnected();
});
/*
Unprovided feature is out commented currently. I'm not sure there is a use case. So lets leave the code around until we know for sure.
it('acts to Context API disconnected', (done) => {
const provider = new UmbContextProvider(document.body, testContextAlias, new UmbTestContextConsumerClass());
provider.hostConnected();
const element = document.createElement('div');
document.body.appendChild(element);
let callbackNum = 0;
const localConsumer = new UmbContextConsumer(
element,
testContextAlias,
(_instance: UmbTestContextConsumerClass | undefined) => {
callbackNum++;
if (callbackNum === 1) {
expect(_instance?.prop).to.eq('value from provider');
// unregister.
provider.hostDisconnected();
} else if (callbackNum === 2) {
expect(_instance?.prop).to.be.undefined;
done();
}
}
);
localConsumer.hostConnected();
});
*/
});
describe('Implementation with Api Alias', () => {
it('responds when api alias matches', (done) => {
const provider = new UmbContextProvider(
document.body,
testContextAliasAndApiAlias,
new UmbTestContextConsumerClass(),
);
provider.hostConnected();
const element = document.createElement('div');
document.body.appendChild(element);
const localConsumer = new UmbContextConsumer(element, testContextAliasAndApiAlias, (_instance) => {
if (_instance) {
expect((_instance as UmbTestContextConsumerClass).prop).to.eq('value from provider');
localConsumer.hostDisconnected();
provider.hostDisconnected();
done();
}
});
localConsumer.hostConnected();
});
it('does not respond to a non existing api alias', (done) => {
const provider = new UmbContextProvider(
document.body,
testContextAliasAndApiAlias,
new UmbTestContextConsumerClass(),
);
provider.hostConnected();
const element = document.createElement('div');
document.body.appendChild(element);
const localConsumer = new UmbContextConsumer(element, testContextAliasAndNotExstingApiAlias, () => {
expect(false).to.be.true;
});
localConsumer.hostConnected();
// Delayed check to make sure the callback is not called.
Promise.resolve().then(() => {
localConsumer.hostDisconnected();
provider.hostDisconnected();
done();
});
});
});
describe('Implementation with discriminator method', () => {
type A = { prop: string };
function discriminator(instance: unknown): instance is A {
return typeof (instance as any).prop === 'string';
}
function badDiscriminator(instance: unknown): instance is A {
return typeof (instance as any).notExistingProp === 'string';
}
it('discriminator determines the instance type', async () => {
const localConsumer = new UmbContextConsumer(
document.body,
new UmbContextToken(testContextAlias, undefined, discriminator),
(instance: A) => {
console.log(instance);
},
);
localConsumer.hostConnected();
// This bit of code is not really a test but it serves as a TypeScript type test, making sure the given type is matches the one given from the Discriminator method.
type TestType = Exclude<typeof localConsumer.instance, undefined> extends A ? true : never;
const test: TestType = true;
expect(test).to.be.true;
localConsumer.destroy();
});
it('approving discriminator still fires callback', (done) => {
const provider = new UmbContextProvider(document.body, testContextAlias, new UmbTestContextConsumerClass());
provider.hostConnected();
const element = document.createElement('div');
document.body.appendChild(element);
const localConsumer = new UmbContextConsumer(
element,
new UmbContextToken(testContextAlias, undefined, discriminator),
(_instance) => {
expect(_instance.prop).to.eq('value from provider');
done();
localConsumer.hostDisconnected();
provider.hostDisconnected();
}
},
);
localConsumer.hostConnected();
});
},
);
localConsumer.hostConnected();
});
/*
Unprovided feature is out commented currently. I'm not sure there is a use case. So lets leave the code around until we know for sure.
it('acts to Context API disconnected', (done) => {
const provider = new UmbContextProvider(document.body, testContextAlias, new UmbTestContextConsumerClass());
provider.hostConnected();
it('disapproving discriminator does not fire callback', (done) => {
const provider = new UmbContextProvider(document.body, testContextAlias, new UmbTestContextConsumerClass());
provider.hostConnected();
const element = document.createElement('div');
document.body.appendChild(element);
const element = document.createElement('div');
document.body.appendChild(element);
let callbackNum = 0;
const localConsumer = new UmbContextConsumer(
element,
new UmbContextToken(testContextAlias, undefined, badDiscriminator),
(_instance) => {
expect(_instance.prop).to.eq('this must not happen!');
},
);
localConsumer.hostConnected();
const localConsumer = new UmbContextConsumer(
element,
testContextAlias,
(_instance: UmbTestContextConsumerClass | undefined) => {
callbackNum++;
if (callbackNum === 1) {
expect(_instance?.prop).to.eq('value from provider');
// unregister.
provider.hostDisconnected();
} else if (callbackNum === 2) {
expect(_instance?.prop).to.be.undefined;
done();
}
}
);
localConsumer.hostConnected();
});
*/
});
describe('UmbContextConsumer with discriminator test', () => {
type A = { prop: string };
function discriminator(instance: unknown): instance is A {
return typeof (instance as any).prop === 'string';
}
function badDiscriminator(instance: unknown): instance is A {
return typeof (instance as any).notExistingProp === 'string';
}
it('discriminator determines the instance type', async () => {
const localConsumer = new UmbContextConsumer(
document.body,
new UmbContextToken(testContextAlias, discriminator),
(instance: A) => {
console.log(instance);
},
);
localConsumer.hostConnected();
// This bit of code is not really a test but it serves as a TypeScript type test, making sure the given type is matches the one given from the Discriminator method.
type TestType = Exclude<typeof localConsumer.instance, undefined> extends A ? true : never;
const test: TestType = true;
expect(test).to.be.true;
localConsumer.destroy();
});
it('approving discriminator still fires callback', (done) => {
const provider = new UmbContextProvider(document.body, testContextAlias, new UmbTestContextConsumerClass());
provider.hostConnected();
const element = document.createElement('div');
document.body.appendChild(element);
const localConsumer = new UmbContextConsumer(
element,
new UmbContextToken(testContextAlias, discriminator),
(_instance) => {
expect(_instance.prop).to.eq('value from provider');
Promise.resolve().then(() => {
done();
localConsumer.hostDisconnected();
provider.hostDisconnected();
},
);
localConsumer.hostConnected();
});
it('disapproving discriminator does not fire callback', (done) => {
const provider = new UmbContextProvider(document.body, testContextAlias, new UmbTestContextConsumerClass());
provider.hostConnected();
const element = document.createElement('div');
document.body.appendChild(element);
const localConsumer = new UmbContextConsumer(
element,
new UmbContextToken(testContextAlias, badDiscriminator),
(_instance) => {
expect(_instance.prop).to.eq('this must not happen!');
},
);
localConsumer.hostConnected();
Promise.resolve().then(() => {
done();
localConsumer.hostDisconnected();
provider.hostDisconnected();
});
});
});
});

View File

@@ -22,29 +22,32 @@ export class UmbContextConsumer<BaseType = unknown, ResultType extends BaseType
}
#contextAlias: string;
#apiAlias: string;
#discriminator?: UmbContextDiscriminator<BaseType, ResultType>;
/**
* Creates an instance of UmbContextConsumer.
* @param {EventTarget} hostElement
* @param {string} contextAlias
* @param {string} contextIdentifier
* @param {UmbContextCallback} callback
* @memberof UmbContextConsumer
*/
constructor(
protected hostElement: EventTarget,
contextAlias: string | UmbContextToken<BaseType, ResultType>,
contextIdentifier: string | UmbContextToken<BaseType, ResultType>,
callback?: UmbContextCallback<ResultType>,
) {
this.#contextAlias = contextAlias.toString();
const idSplit = contextIdentifier.toString().split('#');
this.#contextAlias = idSplit[0];
this.#apiAlias = idSplit[1] ?? 'default';
this.#callback = callback;
this.#discriminator = (contextAlias as UmbContextToken<BaseType, ResultType>).getDiscriminator?.();
this.#discriminator = (contextIdentifier as UmbContextToken<BaseType, ResultType>).getDiscriminator?.();
}
protected _onResponse = (instance: BaseType): boolean => {
if (this.#instance === instance) {
return false;
return true;
}
if (this.#discriminator) {
// Notice if discriminator returns false, we do not want to setInstance.
@@ -68,6 +71,11 @@ export class UmbContextConsumer<BaseType = unknown, ResultType extends BaseType
}
}
/**
* @public
* @memberof UmbContextConsumer
* @description Get the context as a promise.
*/
public asPromise() {
return (
this.#promise ??
@@ -78,10 +86,12 @@ export class UmbContextConsumer<BaseType = unknown, ResultType extends BaseType
}
/**
* @public
* @memberof UmbContextConsumer
* @description Request the context from the host element.
*/
public request() {
const event = new UmbContextRequestEventImplementation(this.#contextAlias, this._onResponse);
const event = new UmbContextRequestEventImplementation(this.#contextAlias, this.#apiAlias, this._onResponse);
this.hostElement.dispatchEvent(event);
}
@@ -126,7 +136,6 @@ export class UmbContextConsumer<BaseType = unknown, ResultType extends BaseType
}
*/
// TODO: Test destroy scenarios:
public destroy() {
this.hostDisconnected();
this.#callback = undefined;

View File

@@ -9,13 +9,18 @@ describe('UmbContextRequestEvent', () => {
const event: UmbContextRequestEvent = new UmbContextRequestEventImplementation(
'my-test-context-alias',
contextRequestCallback
'my-test-api-alias',
contextRequestCallback,
);
it('has context', () => {
it('has context alias', () => {
expect(event.contextAlias).to.eq('my-test-context-alias');
});
it('has api alias', () => {
expect(event.apiAlias).to.eq('my-test-api-alias');
});
it('has a callback', () => {
expect(event.callback).to.eq(contextRequestCallback);
});

View File

@@ -1,5 +1,3 @@
import { UmbContextToken } from '../token/context-token.js';
export const UMB_CONTENT_REQUEST_EVENT_TYPE = 'umb:context-request';
export const UMB_DEBUG_CONTEXT_EVENT_TYPE = 'umb:debug-contexts';
@@ -10,7 +8,8 @@ export type UmbContextCallback<T> = (instance: T) => void;
* @interface UmbContextRequestEvent
*/
export interface UmbContextRequestEvent<ResultType = unknown> extends Event {
readonly contextAlias: string | UmbContextToken<unknown, ResultType>;
readonly contextAlias: string;
readonly apiAlias: string;
readonly callback: (context: ResultType) => boolean;
}
@@ -25,7 +24,8 @@ export class UmbContextRequestEventImplementation<ResultType = unknown>
implements UmbContextRequestEvent<ResultType>
{
public constructor(
public readonly contextAlias: string | UmbContextToken<any, ResultType>,
public readonly contextAlias: string,
public readonly apiAlias: string,
public readonly callback: (context: ResultType) => boolean,
) {
super(UMB_CONTENT_REQUEST_EVENT_TYPE, { bubbles: true, composed: true, cancelable: true });

View File

@@ -42,11 +42,12 @@ describe('UmbContextProvider', () => {
it('handles context request events', (done) => {
const event = new UmbContextRequestEventImplementation(
'my-test-context',
'default',
(_instance: UmbTestContextProviderClass) => {
expect(_instance.prop).to.eq('value from provider');
done();
return true;
}
},
);
document.body.dispatchEvent(event);
@@ -63,7 +64,7 @@ describe('UmbContextProvider', () => {
expect(_instance?.prop).to.eq('value from provider');
done();
localConsumer.hostDisconnected();
}
},
);
localConsumer.hostConnected();
});

View File

@@ -17,6 +17,7 @@ export class UmbContextProvider<BaseType = unknown, ResultType extends BaseType
protected hostElement: EventTarget;
protected _contextAlias: string;
protected _apiAlias: string;
#instance: unknown;
/**
@@ -31,17 +32,20 @@ export class UmbContextProvider<BaseType = unknown, ResultType extends BaseType
/**
* Creates an instance of UmbContextProvider.
* @param {EventTarget} host
* @param {string} contextAlias
* @param {string | UmbContextToken} contextIdentifier
* @param {*} instance
* @memberof UmbContextProvider
*/
constructor(
hostElement: EventTarget,
contextAlias: string | UmbContextToken<BaseType, ResultType>,
contextIdentifier: string | UmbContextToken<BaseType, ResultType>,
instance: ResultType,
) {
this.hostElement = hostElement;
this._contextAlias = contextAlias.toString();
const idSplit = contextIdentifier.toString().split('#');
this._contextAlias = idSplit[0];
this._apiAlias = idSplit[1] ?? 'default';
this.#instance = instance;
}
@@ -56,7 +60,8 @@ export class UmbContextProvider<BaseType = unknown, ResultType extends BaseType
// Since the alias matches, we will stop it from bubbling further up. But we still allow it to ask the other Contexts of the element. Hence not calling `event.stopImmediatePropagation();`
event.stopPropagation();
if (event.callback(this.#instance)) {
// First and importantly, check that the apiAlias matches and then call the callback. If that returns true then we can stop the event completely.
if (this._apiAlias === event.apiAlias && event.callback(this.#instance)) {
// Make sure the event not hits any more Contexts as we have found a match.
event.stopImmediatePropagation();
}

View File

@@ -4,57 +4,164 @@ import { UmbContextProvider } from '../provide/context-provider.js';
import { UmbContextToken } from './context-token.js';
const testContextAlias = 'my-test-context';
const testApiAlias = 'my-test-api';
class UmbTestContextTokenClass {
prop = 'value from provider';
}
describe('UmbContextToken', () => {
const contextToken = new UmbContextToken<UmbTestContextTokenClass>(testContextAlias);
const typedProvider = new UmbContextProvider(document.body, contextToken, new UmbTestContextTokenClass());
typedProvider.hostConnected();
describe('Simple context token', () => {
const contextToken = new UmbContextToken<UmbTestContextTokenClass>(testContextAlias);
const typedProvider = new UmbContextProvider(document.body, contextToken, new UmbTestContextTokenClass());
typedProvider.hostConnected();
after(() => {
typedProvider.hostDisconnected();
after(() => {
typedProvider.hostDisconnected();
});
it('toString returns the alias', () => {
expect(contextToken.toString()).to.eq(testContextAlias + '#' + 'default');
});
it('can be used to consume a context API', (done) => {
const element = document.createElement('div');
document.body.appendChild(element);
const localConsumer = new UmbContextConsumer(
element,
contextToken,
(_instance: UmbTestContextTokenClass | undefined) => {
expect(_instance).to.be.instanceOf(UmbTestContextTokenClass);
expect(_instance?.prop).to.eq('value from provider');
done();
localConsumer.destroy(); // We do not want to react to when the provider is disconnected.
},
);
localConsumer.hostConnected();
});
it('gives the same result when using the string alias', (done) => {
const element = document.createElement('div');
document.body.appendChild(element);
const localConsumer = new UmbContextConsumer(
element,
testContextAlias,
(_instance: UmbTestContextTokenClass | undefined) => {
expect(_instance).to.be.instanceOf(UmbTestContextTokenClass);
expect(_instance?.prop).to.eq('value from provider');
done();
localConsumer.destroy(); // We do not want to react to when the provider is disconnected.
},
);
localConsumer.hostConnected();
});
});
it('toString returns the alias', () => {
expect(contextToken.toString()).to.eq(testContextAlias);
describe('Context Token with alternative api alias', () => {
const contextToken = new UmbContextToken<UmbTestContextTokenClass>(testContextAlias, testApiAlias);
const typedProvider = new UmbContextProvider(document.body, contextToken, new UmbTestContextTokenClass());
typedProvider.hostConnected();
after(() => {
typedProvider.hostDisconnected();
});
it('toString returns the alias', () => {
expect(contextToken.toString()).to.eq(testContextAlias + '#' + testApiAlias);
});
it('can be used to consume a context API', (done) => {
const element = document.createElement('div');
document.body.appendChild(element);
const localConsumer = new UmbContextConsumer(
element,
contextToken,
(_instance: UmbTestContextTokenClass | undefined) => {
expect(_instance).to.be.instanceOf(UmbTestContextTokenClass);
expect(_instance?.prop).to.eq('value from provider');
done();
localConsumer.destroy(); // We do not want to react to when the provider is disconnected.
},
);
localConsumer.hostConnected();
});
it('gives the same result when using the string alias', (done) => {
const element = document.createElement('div');
document.body.appendChild(element);
const localConsumer = new UmbContextConsumer(
element,
testContextAlias,
(_instance: UmbTestContextTokenClass | undefined) => {
expect(_instance).to.be.instanceOf(UmbTestContextTokenClass);
expect(_instance?.prop).to.eq('value from provider');
done();
localConsumer.destroy(); // We do not want to react to when the provider is disconnected.
},
);
localConsumer.hostConnected();
});
});
it('can be used to consume a context API', (done) => {
const element = document.createElement('div');
document.body.appendChild(element);
const localConsumer = new UmbContextConsumer(
element,
contextToken,
(_instance: UmbTestContextTokenClass | undefined) => {
expect(_instance).to.be.instanceOf(UmbTestContextTokenClass);
expect(_instance?.prop).to.eq('value from provider');
done();
localConsumer.destroy(); // We do not want to react to when the provider is disconnected.
}
);
localConsumer.hostConnected();
});
it('gives the same result when using the string alias', (done) => {
const element = document.createElement('div');
document.body.appendChild(element);
const localConsumer = new UmbContextConsumer(
element,
describe('Context Token with discriminator method', () => {
const contextToken = new UmbContextToken<UmbTestContextTokenClass>(
testContextAlias,
(_instance: UmbTestContextTokenClass | undefined) => {
expect(_instance).to.be.instanceOf(UmbTestContextTokenClass);
expect(_instance?.prop).to.eq('value from provider');
done();
localConsumer.destroy(); // We do not want to react to when the provider is disconnected.
}
undefined,
(instance): instance is UmbTestContextTokenClass => instance.prop === 'value from provider',
);
const typedProvider = new UmbContextProvider(document.body, contextToken, new UmbTestContextTokenClass());
typedProvider.hostConnected();
localConsumer.hostConnected();
after(() => {
typedProvider.hostDisconnected();
});
it('toString returns the alias', () => {
expect(contextToken.toString()).to.eq(testContextAlias + '#' + 'default');
});
it('can be used to consume a context API', (done) => {
const element = document.createElement('div');
document.body.appendChild(element);
const localConsumer = new UmbContextConsumer(
element,
contextToken,
(_instance: UmbTestContextTokenClass | undefined) => {
expect(_instance).to.be.instanceOf(UmbTestContextTokenClass);
expect(_instance?.prop).to.eq('value from provider');
done();
localConsumer.destroy(); // We do not want to react to when the provider is disconnected.
},
);
localConsumer.hostConnected();
});
it('gives the same result when using the string alias', (done) => {
const element = document.createElement('div');
document.body.appendChild(element);
const localConsumer = new UmbContextConsumer(
element,
testContextAlias,
(_instance: UmbTestContextTokenClass | undefined) => {
expect(_instance).to.be.instanceOf(UmbTestContextTokenClass);
expect(_instance?.prop).to.eq('value from provider');
done();
localConsumer.destroy(); // We do not want to react to when the provider is disconnected.
},
);
localConsumer.hostConnected();
});
});
});

View File

@@ -1,10 +1,14 @@
export type UmbContextDiscriminator<BaseType, DiscriminatorResult extends BaseType> = (
instance: BaseType,
) => instance is DiscriminatorResult;
export type UmbContextDiscriminator<BaseType, DiscriminatorResult extends BaseType> = (instance: BaseType) => instance is DiscriminatorResult;
export class UmbContextToken<
BaseType = unknown,
ResultType extends BaseType = BaseType> {
/**
* @export
* @class UmbContextToken
* @template BaseType - A generic type of the API before decimated.
* @template ResultType - A concrete type of the API after decimation, use this when you apply a discriminator method. Note this is optional and defaults to the BaseType.
*/
export class UmbContextToken<BaseType = unknown, ResultType extends BaseType = BaseType> {
#discriminator: UmbContextDiscriminator<BaseType, ResultType> | undefined;
/**
* Get the type of the token
@@ -18,12 +22,23 @@ ResultType extends BaseType = BaseType> {
readonly TYPE: ResultType = undefined as never;
/**
* @param alias Unique identifier for the token
* @param contextAlias Unique identifier for the context
* @param apiAlias Unique identifier for the api
* @param discriminator A discriminator that will be used to discriminate the API — testing if the API lives up to a certain requirement. If the API does not meet the requirement then the consumer will not receive this API.
*/
constructor(protected alias: string, discriminator?: UmbContextDiscriminator<BaseType, ResultType>) {
constructor(
protected contextAlias: string,
protected apiAlias: string = 'default',
discriminator?: UmbContextDiscriminator<BaseType, ResultType>,
) {
this.#discriminator = discriminator;
}
/**
* Get the discriminator method for the token
*
* @returns the discriminator method
*/
getDiscriminator(): UmbContextDiscriminator<BaseType, ResultType> | undefined {
return this.#discriminator;
}
@@ -35,8 +50,6 @@ ResultType extends BaseType = BaseType> {
* @returns the unique alias of the token
*/
toString(): string {
return this.alias;
return this.contextAlias + '#' + this.apiAlias;
}
}

View File

@@ -12,9 +12,6 @@ export class UmbCollectionViewBundleElement extends UmbLitElement {
@state()
_currentView?: ManifestCollectionView;
@state()
private _isOpen = false;
@state()
private _collectionRootPathname = '';
@@ -33,56 +30,65 @@ export class UmbCollectionViewBundleElement extends UmbLitElement {
}
#observeCurrentView() {
this.observe(this.#collectionContext!.currentView, (view) => {
this._currentView = view;
}, 'umbCurrentCollectionViewObserver');
this.observe(
this.#collectionContext!.currentView,
(view) => {
//TODO: This is not called when the view is changed
this._currentView = view;
},
'umbCurrentCollectionViewObserver',
);
}
#observeViews() {
this.observe(this.#collectionContext!.views, (views) => {
this._views = views;
}, 'umbCollectionViewsObserver');
this.observe(
this.#collectionContext!.views,
(views) => {
this._views = views;
},
'umbCollectionViewsObserver',
);
}
#toggleDropdown() {
this._isOpen = !this._isOpen;
}
#closeDropdown() {
this._isOpen = false;
}
render() {
return html`${this.#renderLayoutButton()}`;
}
#renderLayoutButton() {
if (!this._currentView) return nothing;
return html` <umb-dropdown .open="${this._isOpen}" @close=${this.#closeDropdown}>
<uui-button slot="trigger" label="status" @click=${this.#toggleDropdown}
>${this.#renderItemDisplay(this._currentView)}</uui-button
>
<div slot="dropdown" class="filter-dropdown">${this._views.map((view) => this.#renderItem(view))}</div>
</umb-dropdown>`;
return html`
<uui-button compact popovertarget="collection-view-bundle-popover" label="status">
${this.#renderItemDisplay(this._currentView)}
</uui-button>
<uui-popover-container id="collection-view-bundle-popover" popover placement="bottom">
<umb-popover-layout>
<div class="filter-dropdown">${this._views.map((view) => this.#renderItem(view))}</div>
</umb-popover-layout>
</uui-popover-container>
`;
}
#renderItem(view: ManifestCollectionView) {
return html`<a href="${this._collectionRootPathname}/${view.meta.pathName}">${this.#renderItemDisplay(view)}</a>`;
return html`
<uui-button compact href="${this._collectionRootPathname}/${view.meta.pathName}">
${this.#renderItemDisplay(view)} <span class="label">${view.meta.label}</span>
</uui-button>
`;
}
#renderItemDisplay(view: ManifestCollectionView) {
return html`<span class="item"><uui-icon name=${view.meta.icon}></uui-icon> ${view.meta.label}</span>`;
return html`<uui-icon name=${view.meta.icon}></uui-icon>`;
}
static styles = [
UmbTextStyles,
css`
.item {
:host {
--uui-button-content-align: left;
}
a {
display: block;
.label {
margin-left: var(--uui-size-space-1);
}
.filter-dropdown {
display: flex;
gap: var(--uui-size-space-3);
flex-direction: column;
}
`,
];

View File

@@ -33,3 +33,4 @@ export * from './ref-property-editor-ui/index.js';
export * from './table/index.js';
export * from './tooltip-menu/index.js';
export * from './variant-selector/index.js';
export * from './popover-layout/index.js';

View File

@@ -0,0 +1,3 @@
import './popover-layout.element.js';
export * from './popover-layout.element.js';

View File

@@ -0,0 +1,33 @@
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { css, html, customElement } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
// TODO: maybe move this to UI Library.
// TODO: add overwrites for customization.
@customElement('umb-popover-layout')
export class UmbPopoverLayoutElement extends UmbLitElement {
render() {
return html`<slot></slot>`;
}
static styles = [
UmbTextStyles,
css`
:host {
background-color: var(--uui-color-surface);
display: block;
border: 1px solid var(--uui-color-border);
border-radius: var(--uui-border-radius);
box-shadow: var(--uui-shadow-depth-3);
padding: var(--uui-size-space-3);
overflow: clip;
}
`,
];
}
declare global {
interface HTMLElementTagNameMap {
'umb-popover-layout': UmbPopoverLayoutElement;
}
}

View File

@@ -1,8 +1,12 @@
import type { UmbDataTypeVariantContext } from "./data-type-variant-context.js";
import { UmbVariantContext } from "@umbraco-cms/backoffice/workspace";
import { UmbContextToken } from "@umbraco-cms/backoffice/context-api";
import type { UmbDataTypeVariantContext } from './data-type-variant-context.js';
import { UmbVariantContext } from '@umbraco-cms/backoffice/workspace';
import { UmbContextToken } from '@umbraco-cms/backoffice/context-api';
export const isDataTypeVariantContext = (context: UmbVariantContext): context is UmbDataTypeVariantContext => ('properties' in context && context.getType() === 'data-type');
export const isDataTypeVariantContext = (context: UmbVariantContext): context is UmbDataTypeVariantContext =>
'properties' in context && context.getType() === 'data-type';
export const UMB_DATA_TYPE_VARIANT_CONTEXT = new UmbContextToken<UmbVariantContext, UmbDataTypeVariantContext>(
"UmbVariantContext", isDataTypeVariantContext);
'UmbVariantContext',
undefined,
isDataTypeVariantContext,
);

View File

@@ -28,17 +28,17 @@ export class UmbDataTypeWorkspaceContext
{
// TODO: revisit. temp solution because the create and response models are different.
#data = new UmbObjectState<DataTypeResponseModel | undefined>(undefined);
data = this.#data.asObservable();
readonly data = this.#data.asObservable();
#getDataPromise?: Promise<any>;
name = this.#data.asObservablePart((data) => data?.name);
id = this.#data.asObservablePart((data) => data?.id);
readonly name = this.#data.asObservablePart((data) => data?.name);
readonly id = this.#data.asObservablePart((data) => data?.id);
propertyEditorUiAlias = this.#data.asObservablePart((data) => data?.propertyEditorUiAlias);
propertyEditorSchemaAlias = this.#data.asObservablePart((data) => data?.propertyEditorAlias);
readonly propertyEditorUiAlias = this.#data.asObservablePart((data) => data?.propertyEditorUiAlias);
readonly propertyEditorSchemaAlias = this.#data.asObservablePart((data) => data?.propertyEditorAlias);
#properties = new UmbObjectState<Array<PropertyEditorConfigProperty> | undefined>(undefined);
properties: Observable<Array<PropertyEditorConfigProperty> | undefined> = this.#properties.asObservable();
readonly properties: Observable<Array<PropertyEditorConfigProperty> | undefined> = this.#properties.asObservable();
private _propertyEditorSchemaConfigDefaultData: Array<PropertyEditorConfigDefaultData> = [];
private _propertyEditorUISettingsDefaultData: Array<PropertyEditorConfigDefaultData> = [];
@@ -53,13 +53,13 @@ export class UmbDataTypeWorkspaceContext
private _propertyEditorUISettingsSchemaAlias?: string;
#defaults = new UmbArrayState<PropertyEditorConfigDefaultData>([], (entry) => entry.alias);
defaults = this.#defaults.asObservable();
readonly defaults = this.#defaults.asObservable();
#propertyEditorUiIcon = new UmbStringState<string | null>(null);
propertyEditorUiIcon = this.#propertyEditorUiIcon.asObservable();
readonly propertyEditorUiIcon = this.#propertyEditorUiIcon.asObservable();
#propertyEditorUiName = new UmbStringState<string | null>(null);
propertyEditorUiName = this.#propertyEditorUiName.asObservable();
readonly propertyEditorUiName = this.#propertyEditorUiName.asObservable();
constructor(host: UmbControllerHostElement) {
super(host, 'Umb.Workspace.DataType', new UmbDataTypeDetailRepository(host));
@@ -265,5 +265,6 @@ export const UMB_DATA_TYPE_WORKSPACE_CONTEXT = new UmbContextToken<
UmbDataTypeWorkspaceContext
>(
'UmbWorkspaceContext',
undefined,
(context): context is UmbDataTypeWorkspaceContext => context.getEntityType?.() === 'data-type',
);

View File

@@ -1,9 +1,12 @@
import { type UmbVariantContext } from "./variant-context.interface.js";
import { UmbNameableVariantContext } from "./nameable-variant-context.interface.js";
import { UmbContextToken } from "@umbraco-cms/backoffice/context-api";
import { type UmbVariantContext } from './variant-context.interface.js';
import { UmbNameableVariantContext } from './nameable-variant-context.interface.js';
import { UmbContextToken } from '@umbraco-cms/backoffice/context-api';
export const isNameablePropertySetContext = (context: UmbVariantContext): context is UmbNameableVariantContext => 'setName' in context;
export const isNameablePropertySetContext = (context: UmbVariantContext): context is UmbNameableVariantContext =>
'setName' in context;
export const UMB_NAMEABLE_VARIANT_CONTEXT = new UmbContextToken<UmbVariantContext, UmbNameableVariantContext>(
"UmbVariantContext",
isNameablePropertySetContext);
'UmbVariantContext',
undefined,
isNameablePropertySetContext,
);

View File

@@ -1,4 +1,4 @@
import { type UmbVariantContext } from "./variant-context.interface.js";
import { UmbContextToken } from "@umbraco-cms/backoffice/context-api";
import { type UmbVariantContext } from './variant-context.interface.js';
import { UmbContextToken } from '@umbraco-cms/backoffice/context-api';
export const UMB_VARIANT_CONTEXT = new UmbContextToken<UmbVariantContext>("UmbVariantContext");
export const UMB_VARIANT_CONTEXT = new UmbContextToken<UmbVariantContext>('UmbVariantContext');

View File

@@ -6,4 +6,8 @@ import type { UmbEntityBase } from '@umbraco-cms/backoffice/models';
export const UMB_VARIANT_WORKSPACE_CONTEXT_TOKEN = new UmbContextToken<
UmbWorkspaceContextInterface,
UmbVariantableWorkspaceContextInterface<UmbEntityBase>
>('UmbWorkspaceContext', (context): context is UmbVariantableWorkspaceContextInterface => 'variants' in context);
>(
'UmbWorkspaceContext',
undefined,
(context): context is UmbVariantableWorkspaceContextInterface => 'variants' in context,
);

View File

@@ -1,4 +1,6 @@
export interface UmbWorkspaceContextInterface {
import type { UmbApi } from '@umbraco-cms/backoffice/extension-api';
export interface UmbWorkspaceContextInterface extends UmbApi {
readonly workspaceAlias: string;
// TODO: should we consider another name than entity type. File system files are not entities but still have this type.
getEntityType(): string;

View File

@@ -1,6 +1,9 @@
import { UmbDictionaryRepository } from '../repository/dictionary.repository.js';
import { UmbSaveableWorkspaceContextInterface, UmbEditableWorkspaceContextBase } from '@umbraco-cms/backoffice/workspace';
import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api';
import {
type UmbSaveableWorkspaceContextInterface,
UmbEditableWorkspaceContextBase,
} from '@umbraco-cms/backoffice/workspace';
import type { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api';
import { UmbObjectState } from '@umbraco-cms/backoffice/observable-api';
import { DictionaryItemResponseModel } from '@umbraco-cms/backoffice/backend-api';
import { UmbContextToken } from '@umbraco-cms/backoffice/context-api';
@@ -10,10 +13,10 @@ export class UmbDictionaryWorkspaceContext
implements UmbSaveableWorkspaceContextInterface<DictionaryItemResponseModel | undefined>
{
#data = new UmbObjectState<DictionaryItemResponseModel | undefined>(undefined);
data = this.#data.asObservable();
readonly data = this.#data.asObservable();
name = this.#data.asObservablePart((data) => data?.name);
dictionary = this.#data.asObservablePart((data) => data);
readonly name = this.#data.asObservablePart((data) => data?.name);
readonly dictionary = this.#data.asObservablePart((data) => data);
constructor(host: UmbControllerHostElement) {
super(host, 'Umb.Workspace.Dictionary', new UmbDictionaryRepository(host));
@@ -97,5 +100,6 @@ export const UMB_DICTIONARY_WORKSPACE_CONTEXT = new UmbContextToken<
UmbDictionaryWorkspaceContext
>(
'UmbWorkspaceContext',
undefined,
(context): context is UmbDictionaryWorkspaceContext => context.getEntityType?.() === 'dictionary-item',
);

View File

@@ -1,4 +1,4 @@
import { UmbDocumentTypeWorkspaceContext } from './document-type-workspace.context.js';
import { UMB_DOCUMENT_TYPE_WORKSPACE_CONTEXT } from './document-type-workspace.context.js';
import { UUIInputElement, UUIInputEvent } from '@umbraco-cms/backoffice/external/uui';
import { css, html, customElement, state, ifDefined } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
@@ -7,7 +7,6 @@ import {
UMB_MODAL_MANAGER_CONTEXT_TOKEN,
UMB_ICON_PICKER_MODAL,
} from '@umbraco-cms/backoffice/modal';
import { UMB_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/workspace';
import { generateAlias } from '@umbraco-cms/backoffice/utils';
@customElement('umb-document-type-workspace-editor')
export class UmbDocumentTypeWorkspaceEditorElement extends UmbLitElement {
@@ -27,15 +26,15 @@ export class UmbDocumentTypeWorkspaceEditorElement extends UmbLitElement {
private _iconColorAlias?: string;
// TODO: Color should be using an alias, and look up in some dictionary/key/value) of project-colors.
#workspaceContext?: UmbDocumentTypeWorkspaceContext;
#workspaceContext?: typeof UMB_DOCUMENT_TYPE_WORKSPACE_CONTEXT.TYPE;
private _modalContext?: UmbModalManagerContext;
constructor() {
super();
this.consumeContext(UMB_WORKSPACE_CONTEXT, (instance) => {
this.#workspaceContext = instance as UmbDocumentTypeWorkspaceContext;
this.consumeContext(UMB_DOCUMENT_TYPE_WORKSPACE_CONTEXT, (instance) => {
this.#workspaceContext = instance;
this.#observeDocumentType();
});

View File

@@ -176,5 +176,6 @@ export const UMB_DOCUMENT_TYPE_WORKSPACE_CONTEXT = new UmbContextToken<
UmbDocumentTypeWorkspaceContext
>(
'UmbWorkspaceContext',
undefined,
(context): context is UmbDocumentTypeWorkspaceContext => context.getEntityType?.() === 'document-type',
);

View File

@@ -8,5 +8,6 @@ export const IsDocumentVariantContext = (context: UmbVariantContext): context is
export const UMB_DOCUMENT_VARIANT_CONTEXT = new UmbContextToken<UmbVariantContext, UmbDocumentVariantContext>(
'UmbVariantContext',
undefined,
IsDocumentVariantContext,
);

View File

@@ -235,6 +235,6 @@ export const UMB_DOCUMENT_WORKSPACE_CONTEXT = new UmbContextToken<
UmbDocumentWorkspaceContext
>(
'UmbWorkspaceContext',
// TODO: Refactor: make a better generic way to identify workspaces, maybe workspaceType or workspaceAlias?.
undefined,
(context): context is UmbDocumentWorkspaceContext => context.getEntityType?.() === UMB_DOCUMENT_ENTITY_TYPE,
);

View File

@@ -1,5 +1,5 @@
import type { UmbDocumentWorkspaceContext } from './document-workspace.context.js';
import { UmbTextStyles } from "@umbraco-cms/backoffice/style";
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { html, customElement, state } from '@umbraco-cms/backoffice/external/lit';
import type { UmbRoute } from '@umbraco-cms/backoffice/router';
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
@@ -11,26 +11,18 @@ import { ManifestWorkspace, umbExtensionsRegistry } from '@umbraco-cms/backoffic
@customElement('umb-document-workspace')
export class UmbDocumentWorkspaceElement extends UmbLitElement {
#workspaceContext?: UmbDocumentWorkspaceContext;
@state()
_routes: UmbRoute[] = [];
public set manifest(manifest: ManifestWorkspace) {
console.log("got manifest", manifest.alias)
// TODO: Make context declaration.
createExtensionApi(manifest, [this]).then( (context) => {
if(context) {
createExtensionApi(manifest, [this]).then((context) => {
if (context) {
this.#gotWorkspaceContext(context);
}
})
};
});
}
#gotWorkspaceContext(context: UmbApi) {
this.#workspaceContext = context as UmbDocumentWorkspaceContext;
@@ -44,11 +36,11 @@ export class UmbDocumentWorkspaceElement extends UmbLitElement {
const parentId = info.match.params.parentId === 'null' ? null : info.match.params.parentId;
const documentTypeKey = info.match.params.documentTypeKey;
this.#workspaceContext!.create(documentTypeKey, parentId);
new UmbWorkspaceIsNewRedirectController(
this,
this.#workspaceContext!,
this.shadowRoot!.querySelector('umb-router-slot')!
this.shadowRoot!.querySelector('umb-router-slot')!,
);
},
},

View File

@@ -121,5 +121,6 @@ export const UMB_MEDIA_TYPE_WORKSPACE_CONTEXT = new UmbContextToken<
UmbMediaTypeWorkspaceContext
>(
'UmbWorkspaceContext',
undefined,
(context): context is UmbMediaTypeWorkspaceContext => context.getEntityType?.() === UMB_MEDIA_TYPE_ENTITY_TYPE,
);

View File

@@ -11,7 +11,8 @@ const workspace: ManifestWorkspace = {
type: 'workspace',
alias: 'Umb.Workspace.Media',
name: 'Media Workspace',
js: () => import('./media-workspace.element.js'),
element: () => import('./media-workspace.element.js'),
api: () => import('./media-workspace.context.js'),
meta: {
entityType: 'media',
},

View File

@@ -7,11 +7,12 @@ import {
import { appendToFrozenArray, UmbObjectState } from '@umbraco-cms/backoffice/observable-api';
import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api';
import { UmbContextToken } from '@umbraco-cms/backoffice/context-api';
import { UmbApi } from '@umbraco-cms/backoffice/extension-api';
type EntityType = UmbMediaDetailModel;
export class UmbMediaWorkspaceContext
extends UmbEditableWorkspaceContextBase<UmbMediaRepository, EntityType>
implements UmbSaveableWorkspaceContextInterface<EntityType | undefined>
implements UmbSaveableWorkspaceContextInterface<EntityType | undefined>, UmbApi
{
#data = new UmbObjectState<EntityType | undefined>(undefined);
data = this.#data.asObservable();
@@ -85,8 +86,13 @@ export class UmbMediaWorkspaceContext
this.#data.destroy();
}
}
export const api = UmbMediaWorkspaceContext;
export const UMB_MEDIA_WORKSPACE_CONTEXT = new UmbContextToken<
UmbSaveableWorkspaceContextInterface,
UmbMediaWorkspaceContext
>('UmbWorkspaceContext', (context): context is UmbMediaWorkspaceContext => context.getEntityType?.() === 'media');
>(
'UmbWorkspaceContext',
undefined,
(context): context is UmbMediaWorkspaceContext => context.getEntityType?.() === 'media',
);

View File

@@ -1,28 +1,59 @@
import { UmbMediaWorkspaceContext } from './media-workspace.context.js';
import { UmbMediaWorkspaceEditorElement } from './media-workspace-editor.element.js';
import { UmbTextStyles } from "@umbraco-cms/backoffice/style";
import { type UmbMediaWorkspaceContext } from './media-workspace.context.js';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { css, html, customElement, state } from '@umbraco-cms/backoffice/external/lit';
import type { UmbRoute } from '@umbraco-cms/backoffice/router';
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
import { type UmbApi, createExtensionApi, UmbExtensionsApiInitializer } from '@umbraco-cms/backoffice/extension-api';
import { umbExtensionsRegistry, type ManifestWorkspace } from '@umbraco-cms/backoffice/extension-registry';
import { UmbWorkspaceIsNewRedirectController } from '@umbraco-cms/backoffice/workspace';
@customElement('umb-media-workspace')
export class UmbMediaWorkspaceElement extends UmbLitElement {
public readonly workspaceAlias = 'Umb.Workspace.Media';
#workspaceContext = new UmbMediaWorkspaceContext(this);
#element = new UmbMediaWorkspaceEditorElement();
#workspaceContext?: UmbMediaWorkspaceContext;
@state()
_routes: UmbRoute[] = [
{
path: 'edit/:id',
component: () => this.#element,
setup: (_component, info) => {
const id = info.match.params.id;
this.#workspaceContext.load(id);
_routes: UmbRoute[] = [];
public set manifest(manifest: ManifestWorkspace) {
createExtensionApi(manifest, [this]).then((context) => {
if (context) {
this.#gotWorkspaceContext(context);
}
});
}
#gotWorkspaceContext(context: UmbApi) {
this.#workspaceContext = context as UmbMediaWorkspaceContext;
this._routes = [
{
path: 'create/:parentId', // /:mediaTypeKey
component: import('./media-workspace-editor.element.js'),
setup: async (_component, info) => {
// TODO: Remember the perspective of permissions here, we need to check if the user has access to create a document of this type under this parent?
const parentId = info.match.params.parentId === 'null' ? null : info.match.params.parentId;
//const mediaTypeKey = info.match.params.mediaTypeKey;
this.#workspaceContext!.create(parentId /** , mediaTypeKey */);
new UmbWorkspaceIsNewRedirectController(
this,
this.#workspaceContext!,
this.shadowRoot!.querySelector('umb-router-slot')!,
);
},
},
},
];
{
path: 'edit/:id',
component: import('./media-workspace-editor.element.js'),
setup: (_component, info) => {
const id = info.match.params.id;
this.#workspaceContext!.load(id);
},
},
];
new UmbExtensionsApiInitializer(this, umbExtensionsRegistry, 'workspaceContext', [this, this.#workspaceContext]);
}
render() {
return html`<umb-router-slot .routes=${this._routes}></umb-router-slot>`;

View File

@@ -3,10 +3,10 @@ import type { UmbMemberGroupDetailModel } from '../types.js';
import { UMB_MEMBER_GROUP_ENTITY_TYPE } from '../entity.js';
import { UMB_MEMBER_GROUP_WORKSPACE_ALIAS } from './manifests.js';
import {
UmbSaveableWorkspaceContextInterface,
type UmbSaveableWorkspaceContextInterface,
UmbEditableWorkspaceContextBase,
} from '@umbraco-cms/backoffice/workspace';
import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api';
import type { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api';
import { UmbContextToken } from '@umbraco-cms/backoffice/context-api';
export class UmbMemberGroupWorkspaceContext
@@ -47,5 +47,6 @@ export const UMB_MEMBER_GROUP_WORKSPACE_CONTEXT = new UmbContextToken<
UmbMemberGroupWorkspaceContext
>(
'UmbWorkspaceContext',
undefined,
(context): context is UmbMemberGroupWorkspaceContext => context.getEntityType?.() === UMB_MEMBER_GROUP_ENTITY_TYPE,
);

View File

@@ -1,7 +1,10 @@
import { UmbMemberTypeRepository } from '../repository/member-type.repository.js';
import { UmbSaveableWorkspaceContextInterface, UmbEditableWorkspaceContextBase } from '@umbraco-cms/backoffice/workspace';
import {
type UmbSaveableWorkspaceContextInterface,
UmbEditableWorkspaceContextBase,
} from '@umbraco-cms/backoffice/workspace';
import { UmbObjectState } from '@umbraco-cms/backoffice/observable-api';
import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api';
import type { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api';
import { UmbContextToken } from '@umbraco-cms/backoffice/context-api';
// TODO => use correct tpye
@@ -75,7 +78,11 @@ export class UmbMemberTypeWorkspaceContext
}
}
export const UMB_MEMBER_TYPE_WORKSPACE_CONTEXT = new UmbContextToken<UmbSaveableWorkspaceContextInterface, UmbMemberTypeWorkspaceContext>(
export const UMB_MEMBER_TYPE_WORKSPACE_CONTEXT = new UmbContextToken<
UmbSaveableWorkspaceContextInterface,
UmbMemberTypeWorkspaceContext
>(
'UmbWorkspaceContext',
(context): context is UmbMemberTypeWorkspaceContext => context.getEntityType?.() === 'member-type'
undefined,
(context): context is UmbMemberTypeWorkspaceContext => context.getEntityType?.() === 'member-type',
);

View File

@@ -3,7 +3,7 @@ import type { UmbMemberDetailModel } from '../types.js';
import { UMB_MEMBER_ENTITY_TYPE } from '../entity.js';
import { UMB_MEMBER_WORKSPACE_ALIAS } from './manifests.js';
import {
UmbSaveableWorkspaceContextInterface,
type UmbSaveableWorkspaceContextInterface,
UmbEditableWorkspaceContextBase,
} from '@umbraco-cms/backoffice/workspace';
import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api';
@@ -47,5 +47,6 @@ export const UMB_MEMBER_WORKSPACE_CONTEXT = new UmbContextToken<
UmbMemberWorkspaceContext
>(
'UmbWorkspaceContext',
undefined,
(context): context is UmbMemberWorkspaceContext => context.getEntityType?.() === UMB_MEMBER_ENTITY_TYPE,
);

View File

@@ -1,6 +1,9 @@
import { UmbLanguageRepository } from '../../repository/language.repository.js';
import { UmbSaveableWorkspaceContextInterface, UmbEditableWorkspaceContextBase } from '@umbraco-cms/backoffice/workspace';
import { ApiError, LanguageResponseModel } from '@umbraco-cms/backoffice/backend-api';
import {
type UmbSaveableWorkspaceContextInterface,
UmbEditableWorkspaceContextBase,
} from '@umbraco-cms/backoffice/workspace';
import { ApiError, type LanguageResponseModel } from '@umbraco-cms/backoffice/backend-api';
import { UmbObjectState } from '@umbraco-cms/backoffice/observable-api';
import type { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api';
import { UmbContextToken } from '@umbraco-cms/backoffice/context-api';
@@ -10,11 +13,11 @@ export class UmbLanguageWorkspaceContext
implements UmbSaveableWorkspaceContextInterface
{
#data = new UmbObjectState<LanguageResponseModel | undefined>(undefined);
data = this.#data.asObservable();
readonly data = this.#data.asObservable();
// TODO: this is a temp solution to bubble validation errors to the UI
#validationErrors = new UmbObjectState<any | undefined>(undefined);
validationErrors = this.#validationErrors.asObservable();
readonly validationErrors = this.#validationErrors.asObservable();
constructor(host: UmbControllerHostElement) {
super(host, 'Umb.Workspace.Language', new UmbLanguageRepository(host));
@@ -102,8 +105,11 @@ export class UmbLanguageWorkspaceContext
}
}
export const UMB_LANGUAGE_WORKSPACE_CONTEXT = new UmbContextToken<UmbSaveableWorkspaceContextInterface, UmbLanguageWorkspaceContext>(
export const UMB_LANGUAGE_WORKSPACE_CONTEXT = new UmbContextToken<
UmbSaveableWorkspaceContextInterface,
UmbLanguageWorkspaceContext
>(
'UmbWorkspaceContext',
(context): context is UmbLanguageWorkspaceContext => context.getEntityType?.() === 'language'
undefined,
(context): context is UmbLanguageWorkspaceContext => context.getEntityType?.() === 'language',
);

View File

@@ -1,8 +1,11 @@
import { UmbRelationTypeRepository } from '../repository/relation-type.repository.js';
import { UmbSaveableWorkspaceContextInterface, UmbEditableWorkspaceContextBase } from '@umbraco-cms/backoffice/workspace';
import {
type UmbSaveableWorkspaceContextInterface,
UmbEditableWorkspaceContextBase,
} from '@umbraco-cms/backoffice/workspace';
import type { RelationTypeBaseModel, RelationTypeResponseModel } from '@umbraco-cms/backoffice/backend-api';
import { UmbObjectState } from '@umbraco-cms/backoffice/observable-api';
import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api';
import type { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api';
import { UmbContextToken } from '@umbraco-cms/backoffice/context-api';
export class UmbRelationTypeWorkspaceContext
@@ -10,9 +13,9 @@ export class UmbRelationTypeWorkspaceContext
implements UmbSaveableWorkspaceContextInterface<RelationTypeResponseModel | undefined>
{
#data = new UmbObjectState<RelationTypeResponseModel | undefined>(undefined);
data = this.#data.asObservable();
name = this.#data.asObservablePart((data) => data?.name);
id = this.#data.asObservablePart((data) => data?.id);
readonly data = this.#data.asObservable();
readonly name = this.#data.asObservablePart((data) => data?.name);
readonly id = this.#data.asObservablePart((data) => data?.id);
constructor(host: UmbControllerHostElement) {
super(host, 'Umb.Workspace.RelationType', new UmbRelationTypeRepository(host));
@@ -77,9 +80,11 @@ export class UmbRelationTypeWorkspaceContext
}
}
export const UMB_RELATION_TYPE_WORKSPACE_CONTEXT = new UmbContextToken<UmbSaveableWorkspaceContextInterface, UmbRelationTypeWorkspaceContext>(
export const UMB_RELATION_TYPE_WORKSPACE_CONTEXT = new UmbContextToken<
UmbSaveableWorkspaceContextInterface,
UmbRelationTypeWorkspaceContext
>(
'UmbWorkspaceContext',
(context): context is UmbRelationTypeWorkspaceContext => context.getEntityType?.() === 'relation-type'
undefined,
(context): context is UmbRelationTypeWorkspaceContext => context.getEntityType?.() === 'relation-type',
);

View File

@@ -1,14 +1,14 @@
import { UmbPartialViewRepository } from '../repository/partial-view.repository.js';
import { UmbPartialViewDetailModel } from '../types.js';
import type { UmbPartialViewDetailModel } from '../types.js';
import { UMB_PARTIAL_VIEW_ENTITY_TYPE } from '../entity.js';
import { UmbBooleanState, UmbDeepState } from '@umbraco-cms/backoffice/observable-api';
import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api';
import type { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api';
import {
UmbSaveableWorkspaceContextInterface,
UmbEditableWorkspaceContextBase,
} from '@umbraco-cms/backoffice/workspace';
import { loadCodeEditor } from '@umbraco-cms/backoffice/code-editor';
import { UpdatePartialViewRequestModel } from '@umbraco-cms/backoffice/backend-api';
import type { UpdatePartialViewRequestModel } from '@umbraco-cms/backoffice/backend-api';
import { UmbContextToken } from '@umbraco-cms/backoffice/context-api';
export class UmbPartialViewWorkspaceContext
@@ -47,13 +47,13 @@ export class UmbPartialViewWorkspaceContext
}
#data = new UmbDeepState<UmbPartialViewDetailModel | undefined>(undefined);
data = this.#data.asObservable();
name = this.#data.asObservablePart((data) => data?.name);
content = this.#data.asObservablePart((data) => data?.content);
path = this.#data.asObservablePart((data) => data?.path);
readonly data = this.#data.asObservable();
readonly name = this.#data.asObservablePart((data) => data?.name);
readonly content = this.#data.asObservablePart((data) => data?.content);
readonly path = this.#data.asObservablePart((data) => data?.path);
#isCodeEditorReady = new UmbBooleanState(false);
isCodeEditorReady = this.#isCodeEditorReady.asObservable();
readonly isCodeEditorReady = this.#isCodeEditorReady.asObservable();
constructor(host: UmbControllerHostElement) {
super(host, 'Umb.Workspace.PartialView', new UmbPartialViewRepository(host));
@@ -107,5 +107,6 @@ export const UMB_PARTIAL_VIEW_WORKSPACE_CONTEXT = new UmbContextToken<
UmbPartialViewWorkspaceContext
>(
'UmbWorkspaceContext',
undefined,
(context): context is UmbPartialViewWorkspaceContext => context.getEntityType?.() === UMB_PARTIAL_VIEW_ENTITY_TYPE,
);

View File

@@ -7,6 +7,11 @@ export class UmbCreateScriptAction<T extends { copy(): Promise<void> }> extends
}
async execute() {
if (this.unique !== null) {
// Note: %2F is a slash (/)
this.unique = this.unique.replace(/\//g, '%2F');
}
history.pushState(null, '', `section/settings/workspace/script/create/${this.unique ?? 'null'}`);
}
}

View File

@@ -26,9 +26,6 @@ export class UmbScriptWorkspaceEditElement extends UmbLitElement {
@state()
private _path?: string | null = '';
@state()
private _dirName?: string | null = '';
@state()
private _ready?: boolean = false;
@@ -60,8 +57,7 @@ export class UmbScriptWorkspaceEditElement extends UmbLitElement {
});
this.observe(this.#scriptsWorkspaceContext.path, (path) => {
this._path = path;
this._dirName = this._path?.substring(0, this._path?.lastIndexOf('\\'))?.replace(/\\/g, '/');
this._path = path?.replace(/\\/g, '/');
});
this.observe(this.#scriptsWorkspaceContext.isNew, (isNew) => {
@@ -106,7 +102,7 @@ export class UmbScriptWorkspaceEditElement extends UmbLitElement {
.value=${this._name}
@input=${this.#onNameInput}
label="template name"></uui-input>
<small>Scripts/${this._dirName}${this._name}.js</small>
<small>Scripts/${this._path}</small>
</div>
<uui-box>
<!-- the div below in the header is to make the box display nicely with code editor -->

View File

@@ -64,8 +64,12 @@ export class UmbScriptWorkspaceContext extends UmbEditableWorkspaceContextBase<
this.setIsNew(true);
}
getEntityId(): string | undefined {
return this.getData()?.path;
getEntityId() {
const path = this.getData()?.path?.replace(/\//g, '%2F');
const name = this.getData()?.name;
// Note: %2F is a slash (/)
return path && name ? `${path}%2F${name}` : name || '';
}
public async save() {
@@ -81,7 +85,10 @@ export class UmbScriptWorkspaceContext extends UmbEditableWorkspaceContextBase<
parentPath: script.path + '/',
};
this.repository.create(createRequestBody);
const { error } = await this.repository.create(createRequestBody);
if (!error) {
this.setIsNew(false);
}
return Promise.resolve();
}
if (!script.path) return Promise.reject('There is no path');
@@ -90,7 +97,10 @@ export class UmbScriptWorkspaceContext extends UmbEditableWorkspaceContextBase<
existingPath: script.path,
content: script.content,
};
this.repository.save(script.path, updateRequestBody);
const { error } = await this.repository.save(script.path, updateRequestBody);
if (!error) {
//TODO Update the URL to the new name
}
return Promise.resolve();
}

View File

@@ -7,20 +7,20 @@ import { UmbWorkspaceIsNewRedirectController } from '@umbraco-cms/backoffice/wor
@customElement('umb-script-workspace')
export class UmbScriptWorkspaceElement extends UmbLitElement {
#scriptWorkspaceContext = new UmbScriptWorkspaceContext(this);
#workspaceContext = new UmbScriptWorkspaceContext(this);
@state()
_routes: UmbRoute[] = [
{
path: 'create/:parentKey',
component: import('./script-workspace-edit.element.js'),
setup: async (component: PageComponent, info: IRoutingInfo) => {
setup: async (_component: PageComponent, info: IRoutingInfo) => {
const parentKey = info.match.params.parentKey;
const decodePath = decodeURIComponent(parentKey);
this.#scriptWorkspaceContext.create(decodePath === 'null' ? '' : decodePath);
this.#workspaceContext.create(decodePath === 'null' ? '' : decodePath);
new UmbWorkspaceIsNewRedirectController(
this,
this.#scriptWorkspaceContext,
this.#workspaceContext,
this.shadowRoot!.querySelector('umb-router-slot')!,
);
},
@@ -31,7 +31,7 @@ export class UmbScriptWorkspaceElement extends UmbLitElement {
setup: (component: PageComponent, info: IRoutingInfo) => {
const key = info.match.params.key;
const decodePath = decodeURIComponent(key).replace('-js', '.js');
this.#scriptWorkspaceContext.load(decodePath);
this.#workspaceContext.load(decodePath);
},
},
];

View File

@@ -1,6 +1,6 @@
import { UmbStylesheetWorkspaceContext } from './stylesheet-workspace.context.js';
import { UUIInputElement, UUIInputEvent, UUITextStyles } from '@umbraco-cms/backoffice/external/uui';
import { css, html, customElement, state, PropertyValueMap } from '@umbraco-cms/backoffice/external/lit';
import { css, html, customElement, state } from '@umbraco-cms/backoffice/external/lit';
import { UMB_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/workspace';
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
import { Subject, debounceTime } from '@umbraco-cms/backoffice/external/rxjs';
@@ -23,9 +23,6 @@ export class UmbStylesheetWorkspaceEditorElement extends UmbLitElement {
@state()
private _path?: string;
@state()
private _dirName?: string;
private inputQuery$ = new Subject<string>();
constructor() {
@@ -46,14 +43,7 @@ export class UmbStylesheetWorkspaceEditorElement extends UmbLitElement {
if (!this.#workspaceContext) return;
this.observe(
this.#workspaceContext.path,
(path) => {
this._path = path;
if (this._path?.includes('.css')) {
this._dirName = this._path?.substring(0, this._path?.lastIndexOf('\\') + 1)?.replace(/\\/g, '/');
} else {
this._dirName = path + '/';
}
},
(path) => (this._path = path?.replace(/\\/g, '/')),
'_observeStylesheetPath',
);
this.observe(this.#workspaceContext.name, (name) => (this._name = name), '_observeStylesheetName');
@@ -76,7 +66,7 @@ export class UmbStylesheetWorkspaceEditorElement extends UmbLitElement {
.value=${this._name}
@input="${this.#onNameChange}">
</uui-input>
<small>/css/${this._dirName}${this._name}.css</small>
<small>/css/${this._path}</small>
</div>
<div slot="footer-info">

View File

@@ -1,10 +1,13 @@
import { UmbStylesheetRepository } from '../repository/stylesheet.repository.js';
import { StylesheetDetails } from '../index.js';
import { UmbSaveableWorkspaceContextInterface, UmbEditableWorkspaceContextBase } from '@umbraco-cms/backoffice/workspace';
import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api';
import type { StylesheetDetails } from '../index.js';
import {
type UmbSaveableWorkspaceContextInterface,
UmbEditableWorkspaceContextBase,
} from '@umbraco-cms/backoffice/workspace';
import type { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api';
import { UmbArrayState, UmbBooleanState, UmbObjectState } from '@umbraco-cms/backoffice/observable-api';
import { loadCodeEditor } from '@umbraco-cms/backoffice/code-editor';
import { RichTextRuleModel, UpdateStylesheetRequestModel } from '@umbraco-cms/backoffice/backend-api';
import type { RichTextRuleModel, UpdateStylesheetRequestModel } from '@umbraco-cms/backoffice/backend-api';
import { UmbContextToken } from '@umbraco-cms/backoffice/context-api';
export type RichTextRuleModelSortable = RichTextRuleModel & { sortOrder?: number };
@@ -15,14 +18,14 @@ export class UmbStylesheetWorkspaceContext
{
#data = new UmbObjectState<StylesheetDetails | undefined>(undefined);
#rules = new UmbArrayState<RichTextRuleModelSortable>([], (rule) => rule.name);
data = this.#data.asObservable();
rules = this.#rules.asObservable();
name = this.#data.asObservablePart((data) => data?.name);
content = this.#data.asObservablePart((data) => data?.content);
path = this.#data.asObservablePart((data) => data?.path);
readonly data = this.#data.asObservable();
readonly rules = this.#rules.asObservable();
readonly name = this.#data.asObservablePart((data) => data?.name);
readonly content = this.#data.asObservablePart((data) => data?.content);
readonly path = this.#data.asObservablePart((data) => data?.path);
#isCodeEditorReady = new UmbBooleanState(false);
isCodeEditorReady = this.#isCodeEditorReady.asObservable();
readonly isCodeEditorReady = this.#isCodeEditorReady.asObservable();
constructor(host: UmbControllerHostElement) {
super(host, 'Umb.Workspace.StyleSheet', new UmbStylesheetRepository(host));
@@ -148,7 +151,6 @@ export class UmbStylesheetWorkspaceContext
if (!error) {
this.setIsNew(false);
}
return Promise.resolve();
} else {
if (!stylesheet.path) return Promise.reject('There is no path');
@@ -157,8 +159,11 @@ export class UmbStylesheetWorkspaceContext
existingPath: stylesheet.path,
content: stylesheet.content,
};
this.repository.save(stylesheet.path, updateRequestBody);
const { error } = await this.repository.save(stylesheet.path, updateRequestBody);
if (!error) {
//TODO Update the URL to the new name
}
return Promise.resolve();
}
}
@@ -184,5 +189,6 @@ export const UMB_STYLESHEET_WORKSPACE_CONTEXT = new UmbContextToken<
UmbStylesheetWorkspaceContext
>(
'UmbWorkspaceContext',
undefined,
(context): context is UmbStylesheetWorkspaceContext => context.getEntityType?.() === 'stylesheet',
);

View File

@@ -1,10 +1,10 @@
import { serverFilePathFromUrlFriendlyPath } from '../../utils.js';
import { UmbStylesheetWorkspaceContext } from './stylesheet-workspace.context.js';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { css, html, customElement, state } from '@umbraco-cms/backoffice/external/lit';
import type { UmbRoute } from '@umbraco-cms/backoffice/router';
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
import { UmbWorkspaceIsNewRedirectController } from '@umbraco-cms/backoffice/workspace';
import { decodeFilePath } from '@umbraco-cms/backoffice/utils';
@customElement('umb-stylesheet-workspace')
export class UmbStylesheetWorkspaceElement extends UmbLitElement {
@@ -17,7 +17,7 @@ export class UmbStylesheetWorkspaceElement extends UmbLitElement {
component: import('./stylesheet-workspace-editor.element.js'),
setup: async (_component, info) => {
const path = info.match.params.path === 'null' ? null : info.match.params.path;
const serverPath = path === null ? null : serverFilePathFromUrlFriendlyPath(path);
const serverPath = path === null ? null : decodeFilePath(path);
await this.#workspaceContext.create(serverPath);
await this.#workspaceContext.setRules([]);
@@ -34,7 +34,7 @@ export class UmbStylesheetWorkspaceElement extends UmbLitElement {
setup: (_component, info) => {
this.removeControllerByAlias('_observeIsNew');
const path = info.match.params.path;
const serverPath = serverFilePathFromUrlFriendlyPath(path);
const serverPath = decodeFilePath(path);
this.#workspaceContext.load(serverPath);
},
},

View File

@@ -176,4 +176,8 @@ ${currentContent}`;
export const UMB_TEMPLATE_WORKSPACE_CONTEXT = new UmbContextToken<
UmbSaveableWorkspaceContextInterface,
UmbTemplateWorkspaceContext
>('UmbWorkspaceContext', (context): context is UmbTemplateWorkspaceContext => context.getEntityType?.() === 'template');
>(
'UmbWorkspaceContext',
undefined,
(context): context is UmbTemplateWorkspaceContext => context.getEntityType?.() === 'template',
);

View File

@@ -1,6 +1,6 @@
import { UmbTemplateWorkspaceContext } from './template-workspace.context.js';
import { html, customElement, state } from '@umbraco-cms/backoffice/external/lit';
import type { IRoutingInfo, PageComponent, UmbRoute, UmbRouterSlotInitEvent } from '@umbraco-cms/backoffice/router';
import type { IRoutingInfo, PageComponent, UmbRoute } from '@umbraco-cms/backoffice/router';
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
import '../../components/insert-menu/templating-insert-menu.element.js';
@@ -9,10 +9,6 @@ import { UmbWorkspaceIsNewRedirectController } from '@umbraco-cms/backoffice/wor
@customElement('umb-template-workspace')
export class UmbTemplateWorkspaceElement extends UmbLitElement {
public load(entityId: string) {
this.#templateWorkspaceContext.load(entityId);
}
#templateWorkspaceContext = new UmbTemplateWorkspaceContext(this);
#element = document.createElement('umb-template-workspace-editor');
@@ -29,7 +25,7 @@ export class UmbTemplateWorkspaceElement extends UmbLitElement {
new UmbWorkspaceIsNewRedirectController(
this,
this.#templateWorkspaceContext,
this.shadowRoot!.querySelector('umb-router-slot')!
this.shadowRoot!.querySelector('umb-router-slot')!,
);
},
},

View File

@@ -1,9 +1,3 @@
// TODO: we can try and make pretty urls if we want to
export const urlFriendlyPathFromServerFilePath = (path: string) => encodeURIComponent(path).replace('.', '-');
// TODO: we can try and make pretty urls if we want to
export const serverFilePathFromUrlFriendlyPath = (unique: string) => decodeURIComponent(unique.replace('-', '.'));
//Below are a copy of
export const getInsertDictionarySnippet = (nodeName: string) => {
return `@Umbraco.GetDictionaryValue("${nodeName}")`;
@@ -31,10 +25,10 @@ export const getRenderBodySnippet = () => '@RenderBody()';
export const getRenderSectionSnippet = (sectionName: string, isMandatory: boolean) =>
`@RenderSection("${sectionName}", ${isMandatory})`;
export const getAddSectionSnippet = (sectionName: string) => `@section ${sectionName}
export const getAddSectionSnippet = (sectionName: string) => `@section ${sectionName}
{
}`;

View File

@@ -1,6 +1,9 @@
import { UmbUserGroupRepository } from '../repository/user-group.repository.js';
import { UmbUserRepository } from '../../user/repository/user.repository.js';
import { UmbSaveableWorkspaceContextInterface, UmbEditableWorkspaceContextBase } from '@umbraco-cms/backoffice/workspace';
import {
UmbSaveableWorkspaceContextInterface,
UmbEditableWorkspaceContextBase,
} from '@umbraco-cms/backoffice/workspace';
import type { UserGroupResponseModel } from '@umbraco-cms/backoffice/backend-api';
import { UmbArrayState, UmbObjectState } from '@umbraco-cms/backoffice/observable-api';
import type { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api';
@@ -153,5 +156,6 @@ export const UMB_USER_GROUP_WORKSPACE_CONTEXT = new UmbContextToken<
UmbUserGroupWorkspaceContext
>(
'UmbWorkspaceContext',
undefined,
(context): context is UmbUserGroupWorkspaceContext => context.getEntityType?.() === 'user-group',
);

View File

@@ -69,15 +69,6 @@ export class UmbUserCollectionHeaderElement extends UmbLitElement {
}
}
#onDropdownClick(event: PointerEvent) {
const composedPath = event.composedPath();
const dropdown = composedPath.find((el) => el instanceof UmbDropdownElement) as UmbDropdownElement;
if (dropdown) {
dropdown.open = !dropdown.open;
}
}
private _updateSearch(event: InputEvent) {
const target = event.target as HTMLInputElement;
const filter = target.value || '';
@@ -118,8 +109,9 @@ export class UmbUserCollectionHeaderElement extends UmbLitElement {
render() {
return html`
${this.#renderCollectionActions()} ${this.#renderSearch()} ${this.#renderFilters()}
${this.#renderCollectionViews()}
<div style="display: flex; gap: var(--uui-size-space-4)">${this.#renderCollectionActions()}</div>
${this.#renderSearch()}
<div>${this.#renderFilters()} ${this.#renderCollectionViews()}</div>
`;
}
@@ -162,9 +154,27 @@ export class UmbUserCollectionHeaderElement extends UmbLitElement {
}
#getUserGroupFilterLabel() {
return this._userGroupFilterSelection.length === 0
const length = this._userGroupFilterSelection.length;
const max = 2;
//TODO: Temp solution to limit the amount of states shown
return length === 0
? this.localize.term('general_all')
: this._userGroupFilterSelection.map((group) => group.name).join(', ');
: this._userGroupFilterSelection
.slice(0, max)
.map((group) => group.name)
.join(', ') + (length > max ? ' + ' + (length - max) : '');
}
#getStatusFilterLabel() {
const length = this._stateFilterSelection.length;
const max = 2;
//TODO: Temp solution to limit the amount of states shown
return length === 0
? this.localize.term('general_all')
: this._stateFilterSelection
.slice(0, max)
.map((state) => this.localize.term('user_state' + state))
.join(', ') + (length > max ? ' + ' + (length - max) : '');
}
#renderFilters() {
@@ -173,45 +183,47 @@ export class UmbUserCollectionHeaderElement extends UmbLitElement {
#renderStatusFilter() {
return html`
<umb-dropdown class="filter">
<uui-button @click=${this.#onDropdownClick} slot="trigger" label="status">
<umb-localize key="general_status"></umb-localize>:
<umb-localize key=${'user_state' + this._stateFilterSelection}></umb-localize>
</uui-button>
<div slot="dropdown" class="filter-dropdown">
${this._stateFilterOptions.map(
(option) =>
html`<uui-checkbox
label=${this.localize.term('user_state' + option)}
@change=${this.#onStateFilterChange}
name="state"
value=${option}></uui-checkbox>`,
)}
</div>
</umb-dropdown>
<uui-button popovertarget="popover-user-status-filter" label="status">
<umb-localize key="general_status"></umb-localize>: <b>${this.#getStatusFilterLabel()}</b>
</uui-button>
<uui-popover-container id="popover-user-status-filter" popover placement="bottom">
<umb-popover-layout>
<div class="filter-dropdown">
${this._stateFilterOptions.map(
(option) =>
html`<uui-checkbox
label=${this.localize.term('user_state' + option)}
@change=${this.#onStateFilterChange}
name="state"
value=${option}></uui-checkbox>`,
)}
</div>
</umb-popover-layout>
</uui-popover-container>
`;
}
#renderUserGroupFilter() {
return html`
<umb-dropdown class="filter">
<uui-button @click=${this.#onDropdownClick} slot="trigger" label=${this.localize.term('general_groups')}>
<umb-localize key="general_groups"></umb-localize>: ${this.#getUserGroupFilterLabel()}
</uui-button>
<div slot="dropdown" class="filter-dropdown">
${repeat(
this._userGroups,
(group) => group.id,
(group) => html`
<uui-checkbox
label=${ifDefined(group.name)}
value=${ifDefined(group.id)}
@change=${this.#onUserGroupFilterChange}></uui-checkbox>
`,
)}
</div>
</umb-dropdown>
<uui-button popovertarget="popover-user-group-filter" label=${this.localize.term('general_groups')}>
<umb-localize key="general_groups"></umb-localize>: <b>${this.#getUserGroupFilterLabel()}</b>
</uui-button>
<uui-popover-container id="popover-user-group-filter" popover placement="bottom">
<umb-popover-layout>
<div class="filter-dropdown">
${repeat(
this._userGroups,
(group) => group.id,
(group) => html`
<uui-checkbox
label=${ifDefined(group.name)}
value=${ifDefined(group.id)}
@change=${this.#onUserGroupFilterChange}></uui-checkbox>
`,
)}
</div>
</umb-popover-layout>
</uui-popover-container>
`;
}
@@ -243,7 +255,6 @@ export class UmbUserCollectionHeaderElement extends UmbLitElement {
display: flex;
gap: var(--uui-size-space-3);
flex-direction: column;
width: fit-content;
}
`,
];

View File

@@ -115,5 +115,6 @@ export const UMB_USER_WORKSPACE_CONTEXT = new UmbContextToken<
UmbUserWorkspaceContext
>(
'UmbWorkspaceContext',
undefined,
(context): context is UmbUserWorkspaceContext => context.getEntityType?.() === UMB_USER_ENTITY_TYPE,
);

View File

@@ -1,5 +1,5 @@
type FilterKeys<T, U> = {
type _FilterKeys<T, U> = {
[K in keyof T]: K extends keyof U ? never : K;
};
export type Diff<U, T> = Pick<T, FilterKeys<T, U>[keyof T]>;
export type Diff<U, T> = Pick<T, _FilterKeys<T, U>[keyof T]>;

View File

@@ -4,11 +4,13 @@ export * from './ensure-path-ends-with-slash.function.js';
export * from './generate-umbraco-alias.function.js';
export * from './increment-string.function.js';
export * from './media-helper.service.js';
export * from './pagination-manager/pagination.manager.js';
export * from './path-decode.function.js';
export * from './path-encode.function.js';
export * from './path-folder-name.function.js';
export * from './selection-manager.js';
export * from './udi-service.js';
export * from './umbraco-path.function.js';
export * from './pagination-manager/pagination.manager.js';
declare global {
interface Window {

View File

@@ -0,0 +1 @@
export const decodeFilePath = (unique: string) => decodeURIComponent(unique.replace('-', '.'));

View File

@@ -0,0 +1 @@
export const encodeFilePath = (path: string) => encodeURIComponent(path).replace('.', '-');

View File

@@ -1,3 +1,4 @@
// TODO: Rename to something more obvious, naming wise this can mean anything. I suggest: umbracoManagementApiPath()
export function umbracoPath(path: string) {
return `/umbraco/management/api/v1${path}`;
}