diff --git a/src/Umbraco.Web.UI.Client/src/libs/context-api/consume/context-consumer.test.ts b/src/Umbraco.Web.UI.Client/src/libs/context-api/consume/context-consumer.test.ts index edb6435a95..aee8547152 100644 --- a/src/Umbraco.Web.UI.Client/src/libs/context-api/consume/context-consumer.test.ts +++ b/src/Umbraco.Web.UI.Client/src/libs/context-api/consume/context-consumer.test.ts @@ -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 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 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(); + }); }); }); }); diff --git a/src/Umbraco.Web.UI.Client/src/libs/context-api/consume/context-consumer.ts b/src/Umbraco.Web.UI.Client/src/libs/context-api/consume/context-consumer.ts index 777032acb1..4dff7b2a13 100644 --- a/src/Umbraco.Web.UI.Client/src/libs/context-api/consume/context-consumer.ts +++ b/src/Umbraco.Web.UI.Client/src/libs/context-api/consume/context-consumer.ts @@ -22,29 +22,32 @@ export class UmbContextConsumer; /** * 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, + contextIdentifier: string | UmbContextToken, callback?: UmbContextCallback, ) { - 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).getDiscriminator?.(); + this.#discriminator = (contextIdentifier as UmbContextToken).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 { 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); }); diff --git a/src/Umbraco.Web.UI.Client/src/libs/context-api/consume/context-request.event.ts b/src/Umbraco.Web.UI.Client/src/libs/context-api/consume/context-request.event.ts index 60640d184d..3be0970872 100644 --- a/src/Umbraco.Web.UI.Client/src/libs/context-api/consume/context-request.event.ts +++ b/src/Umbraco.Web.UI.Client/src/libs/context-api/consume/context-request.event.ts @@ -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 = (instance: T) => void; * @interface UmbContextRequestEvent */ export interface UmbContextRequestEvent extends Event { - readonly contextAlias: string | UmbContextToken; + readonly contextAlias: string; + readonly apiAlias: string; readonly callback: (context: ResultType) => boolean; } @@ -25,7 +24,8 @@ export class UmbContextRequestEventImplementation implements UmbContextRequestEvent { public constructor( - public readonly contextAlias: string | UmbContextToken, + 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 }); diff --git a/src/Umbraco.Web.UI.Client/src/libs/context-api/provide/context-provider.test.ts b/src/Umbraco.Web.UI.Client/src/libs/context-api/provide/context-provider.test.ts index e8ff8bb119..5090598136 100644 --- a/src/Umbraco.Web.UI.Client/src/libs/context-api/provide/context-provider.test.ts +++ b/src/Umbraco.Web.UI.Client/src/libs/context-api/provide/context-provider.test.ts @@ -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(); }); diff --git a/src/Umbraco.Web.UI.Client/src/libs/context-api/provide/context-provider.ts b/src/Umbraco.Web.UI.Client/src/libs/context-api/provide/context-provider.ts index 74a9f62e5f..4cd04706de 100644 --- a/src/Umbraco.Web.UI.Client/src/libs/context-api/provide/context-provider.ts +++ b/src/Umbraco.Web.UI.Client/src/libs/context-api/provide/context-provider.ts @@ -17,6 +17,7 @@ export class UmbContextProvider, + contextIdentifier: string | UmbContextToken, 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 { - const contextToken = new UmbContextToken(testContextAlias); - const typedProvider = new UmbContextProvider(document.body, contextToken, new UmbTestContextTokenClass()); - typedProvider.hostConnected(); + describe('Simple context token', () => { + const contextToken = new UmbContextToken(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(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( 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(); + }); }); }); diff --git a/src/Umbraco.Web.UI.Client/src/libs/context-api/token/context-token.ts b/src/Umbraco.Web.UI.Client/src/libs/context-api/token/context-token.ts index 0ec2b31786..2fc0f86d8e 100644 --- a/src/Umbraco.Web.UI.Client/src/libs/context-api/token/context-token.ts +++ b/src/Umbraco.Web.UI.Client/src/libs/context-api/token/context-token.ts @@ -1,10 +1,14 @@ +export type UmbContextDiscriminator = ( + instance: BaseType, +) => instance is DiscriminatorResult; -export type UmbContextDiscriminator = (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 { #discriminator: UmbContextDiscriminator | 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) { + constructor( + protected contextAlias: string, + protected apiAlias: string = 'default', + discriminator?: UmbContextDiscriminator, + ) { this.#discriminator = discriminator; } + /** + * Get the discriminator method for the token + * + * @returns the discriminator method + */ getDiscriminator(): UmbContextDiscriminator | 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; } - - } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/components/collection-view-bundle.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/components/collection-view-bundle.element.ts index 54ad3b675a..8283848981 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/collection/components/collection-view-bundle.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/components/collection-view-bundle.element.ts @@ -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` - ${this.#renderItemDisplay(this._currentView)} -
${this._views.map((view) => this.#renderItem(view))}
-
`; + return html` + + ${this.#renderItemDisplay(this._currentView)} + + + +
${this._views.map((view) => this.#renderItem(view))}
+
+
+ `; } #renderItem(view: ManifestCollectionView) { - return html`${this.#renderItemDisplay(view)}`; + return html` + + ${this.#renderItemDisplay(view)} ${view.meta.label} + + `; } #renderItemDisplay(view: ManifestCollectionView) { - return html` ${view.meta.label}`; + return html``; } 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; } `, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/index.ts index d212570fa6..99fe0873ce 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/components/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/components/index.ts @@ -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'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/popover-layout/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/popover-layout/index.ts new file mode 100644 index 0000000000..8bea3415c4 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/components/popover-layout/index.ts @@ -0,0 +1,3 @@ +import './popover-layout.element.js'; + +export * from './popover-layout.element.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/popover-layout/popover-layout.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/popover-layout/popover-layout.element.ts new file mode 100644 index 0000000000..7b0994bbf4 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/components/popover-layout/popover-layout.element.ts @@ -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``; + } + + 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; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/data-type/variant-context/data-type-variant-context.token.ts b/src/Umbraco.Web.UI.Client/src/packages/core/data-type/variant-context/data-type-variant-context.token.ts index 2bd6edee08..465fdb7ec6 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/data-type/variant-context/data-type-variant-context.token.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/data-type/variant-context/data-type-variant-context.token.ts @@ -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", isDataTypeVariantContext); + 'UmbVariantContext', + undefined, + isDataTypeVariantContext, +); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/data-type/workspace/data-type-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/data-type/workspace/data-type-workspace.context.ts index 7e249b245e..d20a5bffa0 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/data-type/workspace/data-type-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/data-type/workspace/data-type-workspace.context.ts @@ -28,17 +28,17 @@ export class UmbDataTypeWorkspaceContext { // TODO: revisit. temp solution because the create and response models are different. #data = new UmbObjectState(undefined); - data = this.#data.asObservable(); + readonly data = this.#data.asObservable(); #getDataPromise?: Promise; - 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 | undefined>(undefined); - properties: Observable | undefined> = this.#properties.asObservable(); + readonly properties: Observable | undefined> = this.#properties.asObservable(); private _propertyEditorSchemaConfigDefaultData: Array = []; private _propertyEditorUISettingsDefaultData: Array = []; @@ -53,13 +53,13 @@ export class UmbDataTypeWorkspaceContext private _propertyEditorUISettingsSchemaAlias?: string; #defaults = new UmbArrayState([], (entry) => entry.alias); - defaults = this.#defaults.asObservable(); + readonly defaults = this.#defaults.asObservable(); #propertyEditorUiIcon = new UmbStringState(null); - propertyEditorUiIcon = this.#propertyEditorUiIcon.asObservable(); + readonly propertyEditorUiIcon = this.#propertyEditorUiIcon.asObservable(); #propertyEditorUiName = new UmbStringState(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', ); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/variant-context/nameable-variant-context.token.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/variant-context/nameable-variant-context.token.ts index 307962082f..c4386e9bfe 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/variant-context/nameable-variant-context.token.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/variant-context/nameable-variant-context.token.ts @@ -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", - isNameablePropertySetContext); + 'UmbVariantContext', + undefined, + isNameablePropertySetContext, +); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/variant-context/variant-context.token.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/variant-context/variant-context.token.ts index 1827b68e81..bb33a6442a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/variant-context/variant-context.token.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/variant-context/variant-context.token.ts @@ -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"); +export const UMB_VARIANT_CONTEXT = new UmbContextToken('UmbVariantContext'); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/workspace-context/variant-workspace-context.token.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/workspace-context/variant-workspace-context.token.ts index 16ebe6b2cd..e09238e6dc 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/workspace-context/variant-workspace-context.token.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/workspace-context/variant-workspace-context.token.ts @@ -6,4 +6,8 @@ import type { UmbEntityBase } from '@umbraco-cms/backoffice/models'; export const UMB_VARIANT_WORKSPACE_CONTEXT_TOKEN = new UmbContextToken< UmbWorkspaceContextInterface, UmbVariantableWorkspaceContextInterface ->('UmbWorkspaceContext', (context): context is UmbVariantableWorkspaceContextInterface => 'variants' in context); +>( + 'UmbWorkspaceContext', + undefined, + (context): context is UmbVariantableWorkspaceContextInterface => 'variants' in context, +); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/workspace-context/workspace-context.interface.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/workspace-context/workspace-context.interface.ts index cdb9fa4b4e..e065c1adb0 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/workspace-context/workspace-context.interface.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/workspace-context/workspace-context.interface.ts @@ -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; diff --git a/src/Umbraco.Web.UI.Client/src/packages/dictionary/dictionary/workspace/dictionary-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/dictionary/dictionary/workspace/dictionary-workspace.context.ts index 492b157e66..b27f8bae0b 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/dictionary/dictionary/workspace/dictionary-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/dictionary/dictionary/workspace/dictionary-workspace.context.ts @@ -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 { #data = new UmbObjectState(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', ); diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/workspace/document-type-workspace-editor.element.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/workspace/document-type-workspace-editor.element.ts index 592fdfee6e..79469d08b8 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/workspace/document-type-workspace-editor.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/workspace/document-type-workspace-editor.element.ts @@ -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(); }); diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/workspace/document-type-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/workspace/document-type-workspace.context.ts index 82b49c3a00..c39891181c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/workspace/document-type-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/workspace/document-type-workspace.context.ts @@ -176,5 +176,6 @@ export const UMB_DOCUMENT_TYPE_WORKSPACE_CONTEXT = new UmbContextToken< UmbDocumentTypeWorkspaceContext >( 'UmbWorkspaceContext', + undefined, (context): context is UmbDocumentTypeWorkspaceContext => context.getEntityType?.() === 'document-type', ); diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/variant-context/document-variant-context.token.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/variant-context/document-variant-context.token.ts index 2d97cee2b8..e5ecd6f911 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/variant-context/document-variant-context.token.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/variant-context/document-variant-context.token.ts @@ -8,5 +8,6 @@ export const IsDocumentVariantContext = (context: UmbVariantContext): context is export const UMB_DOCUMENT_VARIANT_CONTEXT = new UmbContextToken( 'UmbVariantContext', + undefined, IsDocumentVariantContext, ); diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace.context.ts index ba169380b1..0b2f8561fd 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace.context.ts @@ -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, ); diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace.element.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace.element.ts index c1d3c4e6e2..3c6d303203 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace.element.ts @@ -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')!, ); }, }, diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/workspace/media-type-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/workspace/media-type-workspace.context.ts index 1a7272354c..104e9b5754 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/workspace/media-type-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/workspace/media-type-workspace.context.ts @@ -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, ); diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/workspace/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/workspace/manifests.ts index e0c4ae4b7c..d36fa3c4dd 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/workspace/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/workspace/manifests.ts @@ -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', }, diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/workspace/media-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/workspace/media-workspace.context.ts index 64aab3cac8..1b0575f358 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/workspace/media-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/workspace/media-workspace.context.ts @@ -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 - implements UmbSaveableWorkspaceContextInterface + implements UmbSaveableWorkspaceContextInterface, UmbApi { #data = new UmbObjectState(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', +); diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/workspace/media-workspace.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/workspace/media-workspace.element.ts index aeeb6b3531..10c2fc5ecc 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/workspace/media-workspace.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/workspace/media-workspace.element.ts @@ -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``; diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member-groups/workspace/member-group-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member-groups/workspace/member-group-workspace.context.ts index 36a48e1b06..4af1a59466 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member-groups/workspace/member-group-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member-groups/workspace/member-group-workspace.context.ts @@ -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, ); diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member-types/workspace/member-type-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member-types/workspace/member-type-workspace.context.ts index 8063754df5..ee32cbdaf9 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member-types/workspace/member-type-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member-types/workspace/member-type-workspace.context.ts @@ -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( +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', ); diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/members/workspace/member-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/members/members/workspace/member-workspace.context.ts index 986f6adc3a..55ecf02ee2 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/members/workspace/member-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/members/workspace/member-workspace.context.ts @@ -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, ); diff --git a/src/Umbraco.Web.UI.Client/src/packages/settings/languages/workspace/language/language-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/settings/languages/workspace/language/language-workspace.context.ts index 9c6626a865..e0fd57e2bc 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/settings/languages/workspace/language/language-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/settings/languages/workspace/language/language-workspace.context.ts @@ -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(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(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( +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', ); diff --git a/src/Umbraco.Web.UI.Client/src/packages/settings/relation-types/workspace/relation-type-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/settings/relation-types/workspace/relation-type-workspace.context.ts index 237d6a83a5..b28d8568d8 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/settings/relation-types/workspace/relation-type-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/settings/relation-types/workspace/relation-type-workspace.context.ts @@ -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 { #data = new UmbObjectState(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( +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', ); diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/partial-views/workspace/partial-view-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/partial-views/workspace/partial-view-workspace.context.ts index 0b925b21c6..7ea5a2601b 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/partial-views/workspace/partial-view-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/partial-views/workspace/partial-view-workspace.context.ts @@ -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(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, ); diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/scripts/entity-actions/create/create-empty.action.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/scripts/entity-actions/create/create-empty.action.ts index 7421a3c713..f6103963d6 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/scripts/entity-actions/create/create-empty.action.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/scripts/entity-actions/create/create-empty.action.ts @@ -7,6 +7,11 @@ export class UmbCreateScriptAction }> 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'}`); } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/scripts/workspace/script-workspace-edit.element.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/scripts/workspace/script-workspace-edit.element.ts index 5d7e2ab8bb..edf2b89730 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/scripts/workspace/script-workspace-edit.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/scripts/workspace/script-workspace-edit.element.ts @@ -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"> - Scripts/${this._dirName}${this._name}.js + Scripts/${this._path} diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/scripts/workspace/script-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/scripts/workspace/script-workspace.context.ts index 760d1c560f..96443c1362 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/scripts/workspace/script-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/scripts/workspace/script-workspace.context.ts @@ -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(); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/scripts/workspace/script-workspace.element.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/scripts/workspace/script-workspace.element.ts index 2c06936932..c2d4e7a2b8 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/scripts/workspace/script-workspace.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/scripts/workspace/script-workspace.element.ts @@ -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); }, }, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/workspace/stylesheet-workspace-editor.element.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/workspace/stylesheet-workspace-editor.element.ts index 2028478b43..0885a18713 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/workspace/stylesheet-workspace-editor.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/workspace/stylesheet-workspace-editor.element.ts @@ -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(); 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}"> - /css/${this._dirName}${this._name}.css + /css/${this._path}
diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/workspace/stylesheet-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/workspace/stylesheet-workspace.context.ts index 1ee3e2e599..5d0aed22ca 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/workspace/stylesheet-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/workspace/stylesheet-workspace.context.ts @@ -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(undefined); #rules = new UmbArrayState([], (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', ); diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/workspace/stylesheet-workspace.element.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/workspace/stylesheet-workspace.element.ts index c4f74d092e..1506091374 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/workspace/stylesheet-workspace.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/workspace/stylesheet-workspace.element.ts @@ -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); }, }, diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/templates/workspace/template-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/templates/workspace/template-workspace.context.ts index ae77ddbd4b..c9f1f93c24 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/templates/workspace/template-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/templates/workspace/template-workspace.context.ts @@ -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', +); diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/templates/workspace/template-workspace.element.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/templates/workspace/template-workspace.element.ts index c73caadf91..ca9068383a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/templates/workspace/template-workspace.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/templates/workspace/template-workspace.element.ts @@ -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')!, ); }, }, diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/utils.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/utils.ts index cb7253772a..57c84ea4bf 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/utils.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/utils.ts @@ -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} { - + }`; diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user-group/workspace/user-group-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user-group/workspace/user-group-workspace.context.ts index 07af16b3a1..15384293e0 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user-group/workspace/user-group-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user-group/workspace/user-group-workspace.context.ts @@ -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', ); diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/user-collection-header.element.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/user-collection-header.element.ts index 29c7bcb878..0dc081232f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/user-collection-header.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/user-collection-header.element.ts @@ -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()} +
${this.#renderCollectionActions()}
+ ${this.#renderSearch()} +
${this.#renderFilters()} ${this.#renderCollectionViews()}
`; } @@ -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` - - - : - - - -
- ${this._stateFilterOptions.map( - (option) => - html``, - )} -
-
+ + : ${this.#getStatusFilterLabel()} + + + +
+ ${this._stateFilterOptions.map( + (option) => + html``, + )} +
+
+
`; } #renderUserGroupFilter() { return html` - - - : ${this.#getUserGroupFilterLabel()} - -
- ${repeat( - this._userGroups, - (group) => group.id, - (group) => html` - - `, - )} -
-
+ + : ${this.#getUserGroupFilterLabel()} + + + +
+ ${repeat( + this._userGroups, + (group) => group.id, + (group) => html` + + `, + )} +
+
+
`; } @@ -243,7 +255,6 @@ export class UmbUserCollectionHeaderElement extends UmbLitElement { display: flex; gap: var(--uui-size-space-3); flex-direction: column; - width: fit-content; } `, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/workspace/user-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/workspace/user-workspace.context.ts index 9a3c410fa1..11ab943a6c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user/workspace/user-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/workspace/user-workspace.context.ts @@ -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, ); diff --git a/src/Umbraco.Web.UI.Client/src/shared/utils/diff.type.ts b/src/Umbraco.Web.UI.Client/src/shared/utils/diff.type.ts index cbce25da17..699ea2524f 100644 --- a/src/Umbraco.Web.UI.Client/src/shared/utils/diff.type.ts +++ b/src/Umbraco.Web.UI.Client/src/shared/utils/diff.type.ts @@ -1,5 +1,5 @@ -type FilterKeys = { +type _FilterKeys = { [K in keyof T]: K extends keyof U ? never : K; }; -export type Diff = Pick[keyof T]>; +export type Diff = Pick[keyof T]>; diff --git a/src/Umbraco.Web.UI.Client/src/shared/utils/index.ts b/src/Umbraco.Web.UI.Client/src/shared/utils/index.ts index c5d439c2e2..7b605bb5de 100644 --- a/src/Umbraco.Web.UI.Client/src/shared/utils/index.ts +++ b/src/Umbraco.Web.UI.Client/src/shared/utils/index.ts @@ -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 { diff --git a/src/Umbraco.Web.UI.Client/src/shared/utils/path-decode.function.ts b/src/Umbraco.Web.UI.Client/src/shared/utils/path-decode.function.ts new file mode 100644 index 0000000000..4858a64a5d --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/shared/utils/path-decode.function.ts @@ -0,0 +1 @@ +export const decodeFilePath = (unique: string) => decodeURIComponent(unique.replace('-', '.')); diff --git a/src/Umbraco.Web.UI.Client/src/shared/utils/path-encode.function.ts b/src/Umbraco.Web.UI.Client/src/shared/utils/path-encode.function.ts new file mode 100644 index 0000000000..ef3e8b0a2b --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/shared/utils/path-encode.function.ts @@ -0,0 +1 @@ +export const encodeFilePath = (path: string) => encodeURIComponent(path).replace('.', '-'); diff --git a/src/Umbraco.Web.UI.Client/src/shared/utils/umbraco-path.function.ts b/src/Umbraco.Web.UI.Client/src/shared/utils/umbraco-path.function.ts index b536ed8a2e..9c54a56c4e 100644 --- a/src/Umbraco.Web.UI.Client/src/shared/utils/umbraco-path.function.ts +++ b/src/Umbraco.Web.UI.Client/src/shared/utils/umbraco-path.function.ts @@ -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}`; }