Merge remote-tracking branch 'origin/main' into feature/swap-to-popover-container

This commit is contained in:
Jesper Møller Jensen
2023-11-23 15:02:58 +13:00
90 changed files with 3155 additions and 1089 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

@@ -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

@@ -19,6 +19,7 @@ export * from './invite-user-modal.token.js';
export * from './language-picker-modal.token.js';
export * from './link-picker-modal.token.js';
export * from './media-tree-picker-modal.token.js';
export * from './media-type-picker-modal.token.js';
export * from './property-editor-ui-picker-modal.token.js';
export * from './property-settings-modal.token.js';
export * from './search-modal.token.js';

View File

@@ -0,0 +1,16 @@
import { EntityTreeItemResponseModel } from '@umbraco-cms/backoffice/backend-api';
import { UmbModalToken, UmbPickerModalValue, UmbTreePickerModalData } from '@umbraco-cms/backoffice/modal';
export type UmbMediaTypePickerModalData = UmbTreePickerModalData<EntityTreeItemResponseModel>;
export type UmbMediaTypePickerModalValue = UmbPickerModalValue;
export const UMB_MEDIA_TYPE_PICKER_MODAL = new UmbModalToken<UmbMediaTypePickerModalData, UmbMediaTypePickerModalValue>(
'Umb.Modal.TreePicker',
{
type: 'sidebar',
size: 'small',
},
{
treeAlias: 'Umb.Tree.MediaType',
},
);

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

@@ -0,0 +1 @@
import './media-type-input/media-type-input.element.js';

View File

@@ -0,0 +1,11 @@
import { UMB_MEDIA_TYPE_ITEM_REPOSITORY_ALIAS } from '../../repository/index.js';
import { UmbPickerInputContext } from '@umbraco-cms/backoffice/picker-input';
import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api';
import { UMB_MEDIA_TYPE_PICKER_MODAL } from '@umbraco-cms/backoffice/modal';
import { MediaTypeItemResponseModel } from '@umbraco-cms/backoffice/backend-api';
export class UmbMediaTypePickerContext extends UmbPickerInputContext<MediaTypeItemResponseModel> {
constructor(host: UmbControllerHostElement) {
super(host, UMB_MEDIA_TYPE_ITEM_REPOSITORY_ALIAS, UMB_MEDIA_TYPE_PICKER_MODAL);
}
}

View File

@@ -0,0 +1,146 @@
import { UmbMediaTypePickerContext } from './media-type-input.context.js';
import { css, html, customElement, property, state, ifDefined } from '@umbraco-cms/backoffice/external/lit';
import { FormControlMixin } from '@umbraco-cms/backoffice/external/uui';
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
import type { MediaTypeItemResponseModel } from '@umbraco-cms/backoffice/backend-api';
@customElement('umb-media-type-input')
export class UmbMediaTypeInputElement extends FormControlMixin(UmbLitElement) {
/**
* This is a minimum amount of selected items in this input.
* @type {number}
* @attr
* @default 0
*/
@property({ type: Number })
public get min(): number {
return this.#pickerContext.min;
}
public set min(value: number) {
this.#pickerContext.min = value;
}
/**
* Min validation message.
* @type {boolean}
* @attr
* @default
*/
@property({ type: String, attribute: 'min-message' })
minMessage = 'This field need more items';
/**
* This is a maximum amount of selected items in this input.
* @type {number}
* @attr
* @default Infinity
*/
@property({ type: Number })
public get max(): number {
return this.#pickerContext.max;
}
public set max(value: number) {
this.#pickerContext.max = value;
}
/**
* Max validation message.
* @type {boolean}
* @attr
* @default
*/
@property({ type: String, attribute: 'min-message' })
maxMessage = 'This field exceeds the allowed amount of items';
public get selectedIds(): Array<string> {
return this.#pickerContext.getSelection();
}
public set selectedIds(ids: Array<string>) {
this.#pickerContext.setSelection(ids);
}
@property()
public set value(idsString: string) {
// Its with full purpose we don't call super.value, as thats being handled by the observation of the context selection.
this.selectedIds = idsString.split(/[ ,]+/);
}
@property()
get pickableFilter() {
return this.#pickerContext.pickableFilter;
}
set pickableFilter(newVal) {
this.#pickerContext.pickableFilter = newVal;
}
@state()
private _items?: Array<MediaTypeItemResponseModel>;
#pickerContext = new UmbMediaTypePickerContext(this);
constructor() {
super();
this.addValidator(
'rangeUnderflow',
() => this.minMessage,
() => !!this.min && this.#pickerContext.getSelection().length < this.min,
);
this.addValidator(
'rangeOverflow',
() => this.maxMessage,
() => !!this.max && this.#pickerContext.getSelection().length > this.max,
);
this.observe(this.#pickerContext.selection, (selection) => (super.value = selection.join(',')));
this.observe(this.#pickerContext.selectedItems, (selectedItems) => (this._items = selectedItems));
}
protected getFormElement() {
return undefined;
}
render() {
console.log('ITEMS', this._items);
return html`
<uui-ref-list>${this._items?.map((item) => this._renderItem(item))}</uui-ref-list>
<uui-button id="add-button" look="placeholder" @click=${() => this.#pickerContext.openPicker()} label="open"
>Add</uui-button
>
`;
}
private _renderItem(item: MediaTypeItemResponseModel) {
if (!item.id) return;
//TODO: Using uui-ref-node as we don't have a uui-ref-media-type yet.
return html`
<uui-ref-node name=${ifDefined(item.name)}>
<uui-action-bar slot="actions">
<uui-button
@click=${() => this.#pickerContext.requestRemoveItem(item.id!)}
label="Remove Media Type ${item.name}"
>Remove</uui-button
>
</uui-action-bar>
</uui-ref-node>
`;
}
static styles = [
css`
#add-button {
width: 100%;
}
`,
];
}
export default UmbMediaTypeInputElement;
declare global {
interface HTMLElementTagNameMap {
'umb-media-type-input': UmbMediaTypeInputElement;
}
}

View File

@@ -1,14 +0,0 @@
import { UmbMediaTypeRepository } from '../repository/media-type.repository.js';
import { UmbEntityActionBase } from '@umbraco-cms/backoffice/entity-action';
import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api';
export class UmbCreateMediaTypeEntityAction extends UmbEntityActionBase<UmbMediaTypeRepository> {
constructor(host: UmbControllerHostElement, repositoryAlias: string, unique: string) {
super(host, repositoryAlias, unique);
}
async execute() {
console.log(`execute for: ${this.unique}`);
alert('open create dialog');
}
}

View File

@@ -0,0 +1,26 @@
import { UmbMediaTypeDetailRepository } from '../../repository/detail/media-type-detail.repository.js';
import { UMB_MEDIA_TYPE_CREATE_OPTIONS_MODAL } from './modal/index.js';
import { UmbEntityActionBase } from '@umbraco-cms/backoffice/entity-action';
import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api';
import { UmbModalManagerContext, UMB_MODAL_MANAGER_CONTEXT_TOKEN } from '@umbraco-cms/backoffice/modal';
export class UmbCreateMediaTypeEntityAction extends UmbEntityActionBase<UmbMediaTypeDetailRepository> {
#modalManagerContext?: UmbModalManagerContext;
constructor(host: UmbControllerHostElement, repositoryAlias: string, unique: string) {
super(host, repositoryAlias, unique);
this.consumeContext(UMB_MODAL_MANAGER_CONTEXT_TOKEN, (instance) => {
this.#modalManagerContext = instance;
});
}
async execute() {
if (!this.#modalManagerContext) throw new Error('Modal manager context is not available');
if (!this.repository) throw new Error('Repository is not available');
this.#modalManagerContext?.open(UMB_MEDIA_TYPE_CREATE_OPTIONS_MODAL, {
parentKey: this.unique,
});
}
}

View File

@@ -0,0 +1,32 @@
import {
UMB_MEDIA_TYPE_ENTITY_TYPE,
UMB_MEDIA_TYPE_FOLDER_ENTITY_TYPE,
UMB_MEDIA_TYPE_ROOT_ENTITY_TYPE,
} from '../../index.js';
import { UMB_MEDIA_TYPE_DETAIL_REPOSITORY_ALIAS } from '../../repository/index.js';
import { UmbCreateMediaTypeEntityAction } from './create.action.js';
import { ManifestTypes } from '@umbraco-cms/backoffice/extension-registry';
const entityActions: Array<ManifestTypes> = [
{
type: 'entityAction',
alias: 'Umb.EntityAction.MediaType.Create',
name: 'Create Media Type Entity Action',
weight: 1000,
api: UmbCreateMediaTypeEntityAction,
meta: {
icon: 'icon-add',
label: 'Create...',
repositoryAlias: UMB_MEDIA_TYPE_DETAIL_REPOSITORY_ALIAS,
entityTypes: [UMB_MEDIA_TYPE_ENTITY_TYPE, UMB_MEDIA_TYPE_ROOT_ENTITY_TYPE, UMB_MEDIA_TYPE_FOLDER_ENTITY_TYPE],
},
},
{
type: 'modal',
alias: 'Umb.Modal.MediaTypeCreateOptions',
name: 'Media Type Create Options Modal',
js: () => import('./modal/media-type-create-options-modal.element.js'),
},
];
export const manifests = [...entityActions];

View File

@@ -0,0 +1,13 @@
import { UmbModalToken } from '@umbraco-cms/backoffice/modal';
export interface UmbMediaTypeCreateOptionsModalData {
parentKey: string | null;
}
export const UMB_MEDIA_TYPE_CREATE_OPTIONS_MODAL = new UmbModalToken<UmbMediaTypeCreateOptionsModalData>(
'Umb.Modal.MediaTypeCreateOptions',
{
type: 'sidebar',
size: 'small',
},
);

View File

@@ -0,0 +1,76 @@
import { UMB_MEDIA_TYPE_DETAIL_REPOSITORY_ALIAS } from '../../../repository/index.js';
import { UmbMediaTypeCreateOptionsModalData } from './index.js';
import { html, customElement, property } from '@umbraco-cms/backoffice/external/lit';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import {
UmbModalManagerContext,
UmbModalContext,
UMB_FOLDER_MODAL,
UMB_MODAL_MANAGER_CONTEXT_TOKEN,
} from '@umbraco-cms/backoffice/modal';
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
@customElement('umb-media-type-create-options-modal')
export class UmbDataTypeCreateOptionsModalElement extends UmbLitElement {
@property({ attribute: false })
modalContext?: UmbModalContext<UmbMediaTypeCreateOptionsModalData>;
@property({ type: Object })
data?: UmbMediaTypeCreateOptionsModalData;
#modalContext?: UmbModalManagerContext;
constructor() {
super();
this.consumeContext(UMB_MODAL_MANAGER_CONTEXT_TOKEN, (instance) => {
this.#modalContext = instance;
});
}
#onClick(event: PointerEvent) {
event.stopPropagation();
const folderModalHandler = this.#modalContext?.open(UMB_FOLDER_MODAL, {
repositoryAlias: UMB_MEDIA_TYPE_DETAIL_REPOSITORY_ALIAS,
});
folderModalHandler?.onSubmit().then(() => this.modalContext?.submit());
}
// close the modal when navigating to data type
#onNavigate() {
this.modalContext?.submit();
}
#onCancel() {
this.modalContext?.reject();
}
render() {
return html`
<umb-body-layout headline="Create Media Type">
<uui-box>
<!-- TODO: construct url -->
<uui-menu-item
href=${`section/settings/workspace/media-type/create/${this.data?.parentKey || 'null'}`}
label="New Media Type..."
@click=${this.#onNavigate}>
<uui-icon slot="icon" name="icon-autofill"></uui-icon>
</uui-menu-item>
<uui-menu-item @click=${this.#onClick} label="New Folder...">
<uui-icon slot="icon" name="icon-folder"></uui-icon>
</uui-menu-item>
</uui-box>
<uui-button slot="actions" id="cancel" label="Cancel" @click="${this.#onCancel}">Cancel</uui-button>
</umb-body-layout>
`;
}
static styles = [UmbTextStyles];
}
export default UmbDataTypeCreateOptionsModalElement;
declare global {
interface HTMLElementTagNameMap {
'umb-media-type-create-options-modal': UmbDataTypeCreateOptionsModalElement;
}
}

View File

@@ -1,12 +1,10 @@
import { UMB_MEDIA_TYPE_REPOSITORY_ALIAS } from '../repository/manifests.js';
import { UmbCreateMediaTypeEntityAction } from './create.action.js';
import UmbReloadMediaTypeEntityAction from './reload.action.js';
import { UMB_MEDIA_TYPE_ENTITY_TYPE } from '../entity.js';
import { UMB_MEDIA_TYPE_DETAIL_REPOSITORY_ALIAS } from '../repository/index.js';
import { UmbCreateMediaTypeEntityAction } from './create/create.action.js';
import { manifests as createManifests } from './create/manifests.js';
import { UmbDeleteEntityAction, UmbMoveEntityAction, UmbCopyEntityAction } from '@umbraco-cms/backoffice/entity-action';
import type { ManifestEntityAction } from '@umbraco-cms/backoffice/extension-registry';
const entityType = 'media-type';
const repositoryAlias = UMB_MEDIA_TYPE_REPOSITORY_ALIAS;
const entityActions: Array<ManifestEntityAction> = [
{
type: 'entityAction',
@@ -17,8 +15,8 @@ const entityActions: Array<ManifestEntityAction> = [
meta: {
icon: 'icon-add',
label: 'Create',
repositoryAlias,
entityTypes: [entityType],
repositoryAlias: UMB_MEDIA_TYPE_DETAIL_REPOSITORY_ALIAS,
entityTypes: [UMB_MEDIA_TYPE_ENTITY_TYPE],
},
},
{
@@ -30,8 +28,8 @@ const entityActions: Array<ManifestEntityAction> = [
meta: {
icon: 'icon-enter',
label: 'Move',
repositoryAlias,
entityTypes: [entityType],
repositoryAlias: UMB_MEDIA_TYPE_DETAIL_REPOSITORY_ALIAS,
entityTypes: [UMB_MEDIA_TYPE_ENTITY_TYPE],
},
},
{
@@ -43,8 +41,8 @@ const entityActions: Array<ManifestEntityAction> = [
meta: {
icon: 'icon-documents',
label: 'Copy',
repositoryAlias,
entityTypes: [entityType],
repositoryAlias: UMB_MEDIA_TYPE_DETAIL_REPOSITORY_ALIAS,
entityTypes: [UMB_MEDIA_TYPE_ENTITY_TYPE],
},
},
{
@@ -56,23 +54,10 @@ const entityActions: Array<ManifestEntityAction> = [
meta: {
icon: 'icon-trash',
label: 'Delete',
repositoryAlias,
entityTypes: [entityType],
},
},
{
type: 'entityAction',
alias: 'Umb.EntityAction.MediaType.Reload',
name: 'Reload Media Type Entity Action',
weight: 100,
api: UmbReloadMediaTypeEntityAction,
meta: {
icon: 'icon-refresh',
label: 'Reload',
repositoryAlias,
entityTypes: [entityType],
repositoryAlias: UMB_MEDIA_TYPE_DETAIL_REPOSITORY_ALIAS,
entityTypes: [UMB_MEDIA_TYPE_ENTITY_TYPE],
},
},
];
export const manifests = [...entityActions];
export const manifests = [...entityActions, ...createManifests];

View File

@@ -1,16 +0,0 @@
import { UmbMediaTypeRepository } from '../repository/media-type.repository.js';
import { UmbTextStyles } from "@umbraco-cms/backoffice/style";
import { UmbEntityActionBase } from '@umbraco-cms/backoffice/entity-action';
import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api';
export default class UmbReloadMediaTypeEntityAction extends UmbEntityActionBase<UmbMediaTypeRepository> {
static styles = [UmbTextStyles];
constructor(host: UmbControllerHostElement, repositoryAlias: string, unique: string) {
super(host, repositoryAlias, unique);
}
async execute() {
alert('refresh');
}
}

View File

@@ -0,0 +1,3 @@
export const UMB_MEDIA_TYPE_ROOT_ENTITY_TYPE = 'media-type-root';
export const UMB_MEDIA_TYPE_ENTITY_TYPE = 'media-type';
export const UMB_MEDIA_TYPE_FOLDER_ENTITY_TYPE = 'media-type-folder';

View File

@@ -0,0 +1,14 @@
import './components/index.js';
export {
UmbMediaTypeItemRepository,
UMB_MEDIA_TYPE_ITEM_STORE_ALIAS,
UMB_MEDIA_TYPE_DETAIL_STORE_ALIAS,
UMB_MEDIA_TYPE_DETAIL_STORE_CONTEXT,
} from './repository/index.js';
export {
UMB_MEDIA_TYPE_ROOT_ENTITY_TYPE,
UMB_MEDIA_TYPE_ENTITY_TYPE,
UMB_MEDIA_TYPE_FOLDER_ENTITY_TYPE,
} from './entity.js';

View File

@@ -1,13 +1,13 @@
import { manifests as entityActionsManifests } from './entity-actions/manifests.js';
import { manifests as menuItemManifests } from './menu-item/manifests.js';
import { manifests as repositoryManifests } from './repository/manifests.js';
import { manifests as treeManifests } from './tree/manifests.js';
import { manifests as workspaceManifests } from './workspace/manifests.js';
import { manifests as repositoryManifests } from './repository/manifests.js';
import { manifests as entityActionManifests } from './entity-actions/manifests.js';
export const manifests = [
...entityActionsManifests,
...menuItemManifests,
...treeManifests,
...repositoryManifests,
...treeManifests,
...workspaceManifests,
...entityActionManifests,
];

View File

@@ -1,3 +1,4 @@
import { UMB_MEDIA_TYPE_TREE_ALIAS } from '../tree/manifests.js';
import type { ManifestTypes } from '@umbraco-cms/backoffice/extension-registry';
const menuItem: ManifestTypes = {
@@ -9,7 +10,7 @@ const menuItem: ManifestTypes = {
meta: {
label: 'Media Types',
icon: 'icon-folder',
treeAlias: 'Umb.Tree.MediaTypes',
treeAlias: UMB_MEDIA_TYPE_TREE_ALIAS,
menus: ['Umb.Menu.Settings'],
},
};

View File

@@ -0,0 +1,3 @@
export { UmbMediaTypeDetailRepository } from './media-type-detail.repository.js';
export { UMB_MEDIA_TYPE_DETAIL_REPOSITORY_ALIAS, UMB_MEDIA_TYPE_DETAIL_STORE_ALIAS } from './manifests.js';
export { UMB_MEDIA_TYPE_DETAIL_STORE_CONTEXT } from './media-type-detail.store.js';

View File

@@ -0,0 +1,22 @@
import { UmbMediaTypeDetailRepository } from './media-type-detail.repository.js';
import { UmbMediaTypeDetailStore } from './media-type-detail.store.js';
import { ManifestRepository, ManifestStore } from '@umbraco-cms/backoffice/extension-registry';
export const UMB_MEDIA_TYPE_DETAIL_REPOSITORY_ALIAS = 'Umb.Repository.MediaType.Detail';
export const UMB_MEDIA_TYPE_DETAIL_STORE_ALIAS = 'Umb.Store.MediaType.Detail';
const detailRepository: ManifestRepository = {
type: 'repository',
alias: UMB_MEDIA_TYPE_DETAIL_REPOSITORY_ALIAS,
name: 'Media Types Repository',
api: UmbMediaTypeDetailRepository,
};
const detailStore: ManifestStore = {
type: 'store',
alias: UMB_MEDIA_TYPE_DETAIL_STORE_ALIAS,
name: 'Media Type Store',
api: UmbMediaTypeDetailStore,
};
export const manifests = [detailRepository, detailStore];

View File

@@ -0,0 +1,175 @@
import { UMB_MEDIA_TYPE_TREE_STORE_CONTEXT, UmbMediaTypeTreeStore } from '../../tree/media-type-tree.store.js';
import { UMB_MEDIA_TYPE_ITEM_STORE_CONTEXT, UmbMediaTypeItemStore } from '../item/media-type-item.store.js';
import { UmbMediaTypeServerDataSource } from './media-type-detail.server.data-source.js';
import { UmbMediaTypeDetailStore, UMB_MEDIA_TYPE_DETAIL_STORE_CONTEXT } from './media-type-detail.store.js';
import { type UmbDetailRepository } from '@umbraco-cms/backoffice/repository';
import { UmbBaseController, type UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api';
import {
CreateMediaTypeRequestModel,
MediaTypeResponseModel,
FolderTreeItemResponseModel,
UpdateMediaTypeRequestModel,
} from '@umbraco-cms/backoffice/backend-api';
import { UmbNotificationContext, UMB_NOTIFICATION_CONTEXT_TOKEN } from '@umbraco-cms/backoffice/notification';
import { UmbApi } from '@umbraco-cms/backoffice/extension-api';
type ItemType = MediaTypeResponseModel;
export class UmbMediaTypeDetailRepository
extends UmbBaseController
implements
UmbDetailRepository<CreateMediaTypeRequestModel, any, UpdateMediaTypeRequestModel, MediaTypeResponseModel>,
UmbApi
{
#init!: Promise<unknown>;
#treeStore?: UmbMediaTypeTreeStore;
#detailDataSource: UmbMediaTypeServerDataSource;
#detailStore?: UmbMediaTypeDetailStore;
#itemStore?: UmbMediaTypeItemStore;
#notificationContext?: UmbNotificationContext;
constructor(host: UmbControllerHostElement) {
super(host);
// TODO: figure out how spin up get the correct data source
this.#detailDataSource = new UmbMediaTypeServerDataSource(this);
this.#init = Promise.all([
this.consumeContext(UMB_MEDIA_TYPE_TREE_STORE_CONTEXT, (instance) => {
this.#treeStore = instance;
}),
this.consumeContext(UMB_MEDIA_TYPE_DETAIL_STORE_CONTEXT, (instance) => {
this.#detailStore = instance;
}),
this.consumeContext(UMB_MEDIA_TYPE_ITEM_STORE_CONTEXT, (instance) => {
this.#itemStore = instance;
}),
this.consumeContext(UMB_NOTIFICATION_CONTEXT_TOKEN, (instance) => {
this.#notificationContext = instance;
}),
]);
}
// DETAILS:
async createScaffold(parentId: string | null) {
if (parentId === undefined) throw new Error('Parent id is missing');
await this.#init;
const { data } = await this.#detailDataSource.createScaffold(parentId);
if (data) {
this.#detailStore?.append(data);
}
return { data };
}
async requestById(id: string) {
if (!id) throw new Error('Id is missing');
await this.#init;
const { data, error } = await this.#detailDataSource.read(id);
if (data) {
this.#detailStore?.append(data);
}
return { data, error, asObservable: () => this.#detailStore!.byId(id) };
}
async byId(id: string) {
if (!id) throw new Error('Id is missing');
await this.#init;
return this.#detailStore!.byId(id);
}
// Could potentially be general methods:
async create(mediaType: ItemType) {
if (!mediaType || !mediaType.id) throw new Error('Media Type is missing');
await this.#init;
const { error } = await this.#detailDataSource.create(mediaType);
if (!error) {
this.#detailStore?.append(mediaType);
const treeItem = createTreeItem(mediaType);
this.#treeStore?.append(treeItem);
const notification = { data: { message: `Media Type created` } };
this.#notificationContext?.peek('positive', notification);
}
return { error };
}
async save(id: string, item: UpdateMediaTypeRequestModel) {
if (!id) throw new Error('Id is missing');
if (!item) throw new Error('Item is missing');
await this.#init;
const { error } = await this.#detailDataSource.update(id, item);
if (!error) {
// TODO: we currently don't use the detail store for anything.
// Consider to look up the data before fetching from the server
// Consider notify a workspace if a template is updated in the store while someone is editing it.
this.#detailStore?.updateItem(id, item);
this.#treeStore?.updateItem(id, item);
this.#itemStore?.updateItem(id, item);
const notification = { data: { message: `Media Type saved` } };
this.#notificationContext?.peek('positive', notification);
}
return { error };
}
// General:
async delete(id: string) {
if (!id) throw new Error('Media Type id is missing');
await this.#init;
const { error } = await this.#detailDataSource.delete(id);
if (!error) {
// TODO: we currently don't use the detail store for anything.
// Consider to look up the data before fetching from the server.
// Consider notify a workspace if a template is deleted from the store while someone is editing it.
// TODO: would be nice to align the stores on methods/methodNames.
this.#detailStore?.removeItem(id);
this.#treeStore?.removeItem(id);
this.#itemStore?.removeItem(id);
const notification = { data: { message: `Media Type deleted` } };
this.#notificationContext?.peek('positive', notification);
}
return { error };
}
}
export const createTreeItem = (item: ItemType): FolderTreeItemResponseModel => {
if (!item) throw new Error('item is null or undefined');
if (!item.id) throw new Error('item.id is null or undefined');
// TODO: needs parentID, this is missing in the current model. Should be good when updated to a createModel.
return {
type: 'media-type',
parentId: null,
name: item.name,
id: item.id,
isFolder: false,
isContainer: false,
hasChildren: false,
};
};

View File

@@ -0,0 +1,147 @@
import type { UmbDataSource } from '@umbraco-cms/backoffice/repository';
import {
CreateMediaTypeRequestModel,
MediaTypeResource,
MediaTypeResponseModel,
UpdateMediaTypeRequestModel,
} from '@umbraco-cms/backoffice/backend-api';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources';
import { UmbId } from '@umbraco-cms/backoffice/id';
/**
* A data source for the Media Type that fetches data from the server
* @export
* @class UmbMediaTypeServerDataSource
* @implements {RepositoryDetailDataSource}
*/
export class UmbMediaTypeServerDataSource
implements UmbDataSource<CreateMediaTypeRequestModel, any, UpdateMediaTypeRequestModel, MediaTypeResponseModel>
{
#host: UmbControllerHost;
/**
* Creates an instance of UmbMediaServerDataSource.
* @param {UmbControllerHost} host
* @memberof UmbMediaServerDataSource
*/
constructor(host: UmbControllerHost) {
this.#host = host;
}
/**
* Fetches a Media with the given id from the server
* @param {string} id
* @return {*}
* @memberof UmbMediaTypeServerDataSource
*/
async read(id: string) {
if (!id) {
throw new Error('Id is missing');
}
return tryExecuteAndNotify(
this.#host,
MediaTypeResource.getMediaTypeById({
id: id,
}),
);
}
/**
* Creates a new Media scaffold
* @param {(string | null)} parentId
* @return {*}
* @memberof UmbMediaTypeServerDataSource
*/
async createScaffold(parentId: string | null) {
//, parentId: string | null
const data: MediaTypeResponseModel = {
id: UmbId.new(),
//parentId: parentId,
name: '',
alias: '',
description: '',
icon: 'icon-picture',
allowedAsRoot: false,
variesByCulture: false,
variesBySegment: false,
isElement: false,
allowedContentTypes: [],
compositions: [],
properties: [],
containers: [],
};
return { data };
}
/**
* Creates a new Media Type on the server
* @param {CreateMediaTypeRequestModel} mediaType
* @return {*}
* @memberof UmbMediaTypeServerDataSource
*/
async create(mediaType: CreateMediaTypeRequestModel) {
if (!mediaType) throw new Error('Media Type is missing');
return tryExecuteAndNotify(
this.#host,
MediaTypeResource.postMediaType({
requestBody: mediaType,
}),
);
}
/**
* Updates a Media Type on the server
* @param {string} id
* @param {Media} mediaType
* @return {*}
* @memberof UmbMediaTypeServerDataSource
*/
async update(id: string, mediaType: UpdateMediaTypeRequestModel) {
if (!id) throw new Error('Id is missing');
mediaType = { ...mediaType };
// TODO: Hack to remove some props that ruins the media-type post end-point.
(mediaType as any).id = undefined;
return tryExecuteAndNotify(this.#host, MediaTypeResource.putMediaTypeById({ id, requestBody: mediaType }));
}
/**
* Deletes a Template on the server
* @param {string} id
* @return {*}
* @memberof UmbMediaTypeServerDataSource
*/
async delete(id: string) {
if (!id) {
throw new Error('Id is missing');
}
// TODO: Hack the type to avoid type-error here:
return tryExecuteAndNotify(this.#host, MediaTypeResource.deleteMediaTypeById({ id })) as any;
}
/**
* Get the allowed media types for a given parent id
* @param {string} id
* @return {*}
* @memberof UmbMediaTypeServerDataSource
*/
async getAllowedChildrenOf(id: string) {
if (!id) throw new Error('Id is missing');
return tryExecuteAndNotify(
this.#host,
fetch(`/umbraco/management/api/v1/media-type/allowed-children-of/${id}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
}).then((res) => res.json()),
);
}
}

View File

@@ -1,35 +1,39 @@
import { MediaTypeResponseModel } from '@umbraco-cms/backoffice/backend-api';
import { UmbContextToken } from '@umbraco-cms/backoffice/context-api';
import { UmbArrayState } from '@umbraco-cms/backoffice/observable-api';
import { UmbStoreBase } from '@umbraco-cms/backoffice/store';
import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api';
import { UmbArrayState } from '@umbraco-cms/backoffice/observable-api';
import { MediaTypeResponseModel } from '@umbraco-cms/backoffice/backend-api';
/**
* @export
* @class UmbMediaTypeDetailStore
* @class UmbMediaTypeStore
* @extends {UmbStoreBase}
* @description - Details Data Store for Media Types
* @description - Data Store for Media Types
*/
export class UmbMediaTypeStore extends UmbStoreBase {
export class UmbMediaTypeDetailStore extends UmbStoreBase<MediaTypeResponseModel> {
/**
* Creates an instance of UmbMediaTypeStore.
* @param {UmbControllerHostElement} host
* @memberof UmbMediaTypeStore
*/
constructor(host: UmbControllerHostElement) {
super(
host,
UMB_MEDIA_TYPE_STORE_CONTEXT_TOKEN.toString(),
UMB_MEDIA_TYPE_DETAIL_STORE_CONTEXT.toString(),
new UmbArrayState<MediaTypeResponseModel>([], (x) => x.id),
);
}
/**
* @param {MediaTypeResponseModel['id']} id
* @return {*}
* @memberof UmbMediaTypeDetailStore
*/
byId(id: MediaTypeResponseModel['id']) {
return this._data.asObservablePart((x) => x.find((y) => y.id === id));
}
append(mediaType: MediaTypeResponseModel) {
this._data.append([mediaType]);
}
remove(uniques: string[]) {
this._data.remove(uniques);
}
}
export const UMB_MEDIA_TYPE_STORE_CONTEXT_TOKEN = new UmbContextToken<UmbMediaTypeStore>('UmbMediaTypeStore');
export const UMB_MEDIA_TYPE_DETAIL_STORE_CONTEXT = new UmbContextToken<UmbMediaTypeDetailStore>(
'UmbMediaTypeDetailStore',
);

View File

@@ -0,0 +1,4 @@
import { UMB_MEDIA_TYPE_ENTITY_TYPE } from '../../entity.js';
import { MediaTypeResponseModel } from '@umbraco-cms/backoffice/backend-api';
export type UmbMediaTypeDetailModel = MediaTypeResponseModel & { entityType: typeof UMB_MEDIA_TYPE_ENTITY_TYPE };

View File

@@ -0,0 +1,2 @@
export * from './item/index.js';
export * from './detail/index.js';

View File

@@ -0,0 +1,4 @@
export { UmbMediaTypeItemRepository } from './media-type-item.repository.js';
export { UMB_MEDIA_TYPE_ITEM_REPOSITORY_ALIAS, UMB_MEDIA_TYPE_ITEM_STORE_ALIAS } from './manifests.js';
export { UMB_MEDIA_TYPE_ITEM_STORE_CONTEXT } from './media-type-item.store.js';
export type { UmbMediaTypeItemModel } from './types.js';

View File

@@ -0,0 +1,22 @@
import { UmbMediaTypeItemRepository } from './media-type-item.repository.js';
import { UmbMediaTypeItemStore } from './media-type-item.store.js';
import { ManifestItemStore, ManifestRepository } from '@umbraco-cms/backoffice/extension-registry';
export const UMB_MEDIA_TYPE_ITEM_REPOSITORY_ALIAS = 'Umb.Repository.MediaType.Item';
export const UMB_MEDIA_TYPE_ITEM_STORE_ALIAS = 'Umb.Store.MediaType.Item';
const itemRepository: ManifestRepository = {
type: 'repository',
alias: UMB_MEDIA_TYPE_ITEM_REPOSITORY_ALIAS,
name: 'Media Type Item Repository',
api: UmbMediaTypeItemRepository,
};
const itemStore: ManifestItemStore = {
type: 'itemStore',
alias: UMB_MEDIA_TYPE_ITEM_STORE_ALIAS,
name: 'Media Type Item Store',
api: UmbMediaTypeItemStore,
};
export const manifests = [itemRepository, itemStore];

View File

@@ -0,0 +1,11 @@
import { UmbMediaTypeItemModel } from './types.js';
import { UmbMediaTypeItemServerDataSource } from './media-type-item.server.data-source.js';
import { UMB_MEDIA_TYPE_ITEM_STORE_CONTEXT } from './media-type-item.store.js';
import { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { UmbItemRepositoryBase } from '@umbraco-cms/backoffice/repository';
export class UmbMediaTypeItemRepository extends UmbItemRepositoryBase<UmbMediaTypeItemModel> {
constructor(host: UmbControllerHost) {
super(host, UmbMediaTypeItemServerDataSource, UMB_MEDIA_TYPE_ITEM_STORE_CONTEXT);
}
}

View File

@@ -7,7 +7,7 @@ import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources';
* A data source for Media Type items that fetches data from the server
* @export
* @class UmbMediaTypeItemServerDataSource
* @implements {DocumentTreeDataSource}
* @implements {UmbItemDataSource}
*/
export class UmbMediaTypeItemServerDataSource implements UmbItemDataSource<MediaTypeItemResponseModel> {
#host: UmbControllerHost;

View File

@@ -23,7 +23,7 @@ export class UmbMediaTypeItemStore
constructor(host: UmbControllerHostElement) {
super(
host,
UMB_MEDIA_TYPE_ITEM_STORE_CONTEXT_TOKEN.toString(),
UMB_MEDIA_TYPE_ITEM_STORE_CONTEXT.toString(),
new UmbArrayState<MediaTypeItemResponseModel>([], (x) => x.id),
);
}
@@ -33,6 +33,4 @@ export class UmbMediaTypeItemStore
}
}
export const UMB_MEDIA_TYPE_ITEM_STORE_CONTEXT_TOKEN = new UmbContextToken<UmbMediaTypeItemStore>(
'UmbMediaTypeItemStore',
);
export const UMB_MEDIA_TYPE_ITEM_STORE_CONTEXT = new UmbContextToken<UmbMediaTypeItemStore>('UmbMediaTypeItemStore');

View File

@@ -0,0 +1,3 @@
import { MediaTypeItemResponseModel } from '@umbraco-cms/backoffice/backend-api';
export type UmbMediaTypeItemModel = MediaTypeItemResponseModel;

View File

@@ -1,32 +1,4 @@
import { UmbMediaTypeRepository } from './media-type.repository.js';
import { UmbMediaTypeStore } from './media-type.detail.store.js';
import { UmbMediaTypeTreeStore } from './media-type.tree.store.js';
import type { ManifestStore, ManifestTreeStore, ManifestRepository } from '@umbraco-cms/backoffice/extension-registry';
import { manifests as detailManifests } from './detail/manifests.js';
import { manifests as itemManifests } from './item/manifests.js';
export const UMB_MEDIA_TYPE_REPOSITORY_ALIAS = 'Umb.Repository.MediaType';
const repository: ManifestRepository = {
type: 'repository',
alias: UMB_MEDIA_TYPE_REPOSITORY_ALIAS,
name: 'Media Type Repository',
api: UmbMediaTypeRepository,
};
export const UMB_MEDIA_TYPE_STORE_ALIAS = 'Umb.Store.MediaType';
export const UMB_MEDIA_TYPE_TREE_STORE_ALIAS = 'Umb.Store.MediaTypeTree';
const store: ManifestStore = {
type: 'store',
alias: UMB_MEDIA_TYPE_STORE_ALIAS,
name: 'Media Type Store',
api: UmbMediaTypeStore,
};
const treeStore: ManifestTreeStore = {
type: 'treeStore',
alias: UMB_MEDIA_TYPE_TREE_STORE_ALIAS,
name: 'Media Type Tree Store',
api: UmbMediaTypeTreeStore,
};
export const manifests = [store, treeStore, repository];
export const manifests = [...detailManifests, ...itemManifests];

View File

@@ -1,239 +0,0 @@
import { UmbMediaTypeTreeStore, UMB_MEDIA_TYPE_TREE_STORE_CONTEXT_TOKEN } from './media-type.tree.store.js';
import { UmbMediaTypeDetailServerDataSource } from './sources/media-type.detail.server.data.js';
import { UmbMediaTypeStore, UMB_MEDIA_TYPE_STORE_CONTEXT_TOKEN } from './media-type.detail.store.js';
import { UmbMediaTypeTreeServerDataSource } from './sources/media-type.tree.server.data.js';
import { UMB_MEDIA_TYPE_ITEM_STORE_CONTEXT_TOKEN, UmbMediaTypeItemStore } from './media-type-item.store.js';
import { UmbMediaTypeItemServerDataSource } from './sources/media-type-item.server.data.js';
import { UmbBaseController, UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api';
import { UmbNotificationContext, UMB_NOTIFICATION_CONTEXT_TOKEN } from '@umbraco-cms/backoffice/notification';
import {
UmbDataSource,
UmbItemRepository,
UmbDetailRepository,
UmbItemDataSource,
} from '@umbraco-cms/backoffice/repository';
import { UmbTreeRepository, UmbTreeDataSource } from '@umbraco-cms/backoffice/tree';
import {
CreateMediaTypeRequestModel,
FolderTreeItemResponseModel,
MediaTypeItemResponseModel,
MediaTypeResponseModel,
UpdateMediaTypeRequestModel,
} from '@umbraco-cms/backoffice/backend-api';
import { UmbApi } from '@umbraco-cms/backoffice/extension-api';
export class UmbMediaTypeRepository
extends UmbBaseController
implements
UmbItemRepository<MediaTypeItemResponseModel>,
UmbTreeRepository<FolderTreeItemResponseModel>,
UmbDetailRepository<CreateMediaTypeRequestModel, any, UpdateMediaTypeRequestModel, MediaTypeResponseModel>,
UmbApi
{
#init!: Promise<unknown>;
#treeSource: UmbTreeDataSource;
#treeStore?: UmbMediaTypeTreeStore;
#detailSource: UmbDataSource<CreateMediaTypeRequestModel, any, UpdateMediaTypeRequestModel, MediaTypeResponseModel>;
#detailStore?: UmbMediaTypeStore;
#itemSource: UmbItemDataSource<MediaTypeItemResponseModel>;
#itemStore?: UmbMediaTypeItemStore;
#notificationContext?: UmbNotificationContext;
constructor(host: UmbControllerHostElement) {
super(host);
// TODO: figure out how spin up get the correct data source
this.#treeSource = new UmbMediaTypeTreeServerDataSource(this);
this.#detailSource = new UmbMediaTypeDetailServerDataSource(this);
this.#itemSource = new UmbMediaTypeItemServerDataSource(this);
this.#init = Promise.all([
this.consumeContext(UMB_MEDIA_TYPE_STORE_CONTEXT_TOKEN, (instance) => {
this.#detailStore = instance;
}),
this.consumeContext(UMB_MEDIA_TYPE_TREE_STORE_CONTEXT_TOKEN, (instance) => {
this.#treeStore = instance;
}),
this.consumeContext(UMB_MEDIA_TYPE_ITEM_STORE_CONTEXT_TOKEN, (instance) => {
this.#itemStore = instance;
}),
this.consumeContext(UMB_NOTIFICATION_CONTEXT_TOKEN, (instance) => {
this.#notificationContext = instance;
}),
]);
}
async requestTreeRoot() {
await this.#init;
const data = {
id: null,
type: 'media-type-root',
name: 'Media Types',
icon: 'icon-folder',
hasChildren: true,
};
return { data };
}
async requestRootTreeItems() {
await this.#init;
const { data, error } = await this.#treeSource.getRootItems();
if (data) {
this.#treeStore?.appendItems(data.items);
}
return { data, error, asObservable: () => this.#treeStore!.rootItems };
}
async requestTreeItemsOf(parentId: string | null) {
await this.#init;
if (parentId === undefined) throw new Error('Parent id is missing');
const { data, error } = await this.#treeSource.getChildrenOf(parentId);
if (data) {
this.#treeStore?.appendItems(data.items);
}
return { data, error, asObservable: () => this.#treeStore!.childrenOf(parentId) };
}
async byId(id: string) {
if (!id) throw new Error('Key is missing');
await this.#init;
return this.#detailStore!.byId(id);
}
async requestItemsLegacy(ids: Array<string>) {
await this.#init;
if (!ids) {
throw new Error('Ids are missing');
}
const { data, error } = await this.#treeSource.getItems(ids);
return { data, error, asObservable: () => this.#treeStore!.items(ids) };
}
async rootTreeItems() {
await this.#init;
return this.#treeStore!.rootItems;
}
async treeItemsOf(parentId: string | null) {
await this.#init;
return this.#treeStore!.childrenOf(parentId);
}
async itemsLegacy(ids: Array<string>) {
await this.#init;
return this.#treeStore!.items(ids);
}
// DETAILS
async createScaffold(parentId: string | null) {
if (parentId === undefined) throw new Error('Parent id is missing');
await this.#init;
return this.#detailSource.createScaffold(parentId);
}
async requestById(id: string) {
await this.#init;
if (!id) {
throw new Error('Id is missing');
}
const { data, error } = await this.#detailSource.read(id);
if (data) {
this.#detailStore?.append(data);
}
return { data, error };
}
async requestItems(ids: Array<string>) {
if (!ids) throw new Error('Keys are missing');
await this.#init;
const { data, error } = await this.#itemSource.getItems(ids);
if (data) {
this.#itemStore?.appendItems(data);
}
return { data, error, asObservable: () => this.#itemStore!.items(ids) };
}
async items(ids: Array<string>) {
await this.#init;
return this.#itemStore!.items(ids);
}
async delete(id: string) {
await this.#init;
return this.#detailSource.delete(id);
}
async save(id: string, item: UpdateMediaTypeRequestModel) {
if (!id) throw new Error('Data Type id is missing');
if (!item) throw new Error('Media Type is missing');
await this.#init;
const { error } = await this.#detailSource.update(id, item);
if (!error) {
this.#detailStore?.append(item);
this.#treeStore?.updateItem(id, item);
const notification = { data: { message: `Media type '${item.name}' saved` } };
this.#notificationContext?.peek('positive', notification);
}
return { error };
}
async create(mediaType: CreateMediaTypeRequestModel) {
if (!mediaType || !mediaType.id) throw new Error('Document Type is missing');
await this.#init;
const { error } = await this.#detailSource.create(mediaType);
if (!error) {
//TODO: Model mismatch. FIX
this.#detailStore?.append(mediaType as unknown as MediaTypeResponseModel);
const treeItem = {
type: 'media-type',
parentId: null,
name: mediaType.name,
id: mediaType.id,
isFolder: false,
isContainer: false,
hasChildren: false,
};
this.#treeStore?.appendItems([treeItem]);
}
return { error };
}
async move() {
alert('move me!');
}
async copy() {
alert('copy me');
}
}

View File

@@ -1,107 +0,0 @@
import {
CreateMediaTypeRequestModel,
MediaTypeResource,
MediaTypeResponseModel,
UpdateMediaTypeRequestModel,
} from '@umbraco-cms/backoffice/backend-api';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources';
import { UmbDataSource } from '@umbraco-cms/backoffice/repository';
/**
* @description - A data source for the Media Type detail that fetches data from the server
* @export
* @class UmbMediaTypeDetailServerDataSource
* @implements {MediaTypeDetailDataSource}
*/
export class UmbMediaTypeDetailServerDataSource
implements UmbDataSource<CreateMediaTypeRequestModel, any, UpdateMediaTypeRequestModel, MediaTypeResponseModel>
{
#host: UmbControllerHost;
constructor(host: UmbControllerHost) {
this.#host = host;
}
/**
* @description - Creates a new MediaType scaffold
* @return {*}
* @memberof UmbMediaTypeDetailServerDataSource
*/
async createScaffold() {
const data: CreateMediaTypeRequestModel = {
name: '',
} as CreateMediaTypeRequestModel;
return { data };
}
/**
* @description - Fetches a MediaType with the given id from the server
* @param {string} id
* @return {*}
* @memberof UmbMediaTypeDetailServerDataSource
*/
async read(id: string) {
if (!id) throw new Error('Key is missing');
return tryExecuteAndNotify(
this.#host,
MediaTypeResource.getMediaTypeById({
id: id,
}),
);
}
/**
* @description - Updates a MediaType on the server
* @param {UpdateMediaTypeRequestModel} MediaType
* @return {*}
* @memberof UmbMediaTypeDetailServerDataSource
*/
async update(id: string, data: UpdateMediaTypeRequestModel) {
if (!id) throw new Error('Key is missing');
return tryExecuteAndNotify(
this.#host,
MediaTypeResource.putMediaTypeById({
id: id,
requestBody: data,
}),
);
}
/**
* @description - Inserts a new MediaType on the server
* @param {CreateMediaTypeRequestModel} data
* @return {*}
* @memberof UmbMediaTypeDetailServerDataSource
*/
async create(mediaType: CreateMediaTypeRequestModel) {
if (!mediaType) throw new Error('Media type is missing');
if (!mediaType.id) throw new Error('Media type id is missing');
return tryExecuteAndNotify(
this.#host,
MediaTypeResource.postMediaType({
requestBody: mediaType,
}),
);
}
/**
* @description - Deletes a MediaType on the server
* @param {string} id
* @return {*}
* @memberof UmbMediaTypeDetailServerDataSource
*/
async delete(id: string) {
if (!id) throw new Error('Key is missing');
return tryExecuteAndNotify(
this.#host,
MediaTypeResource.deleteMediaTypeById({
id: id,
}),
);
}
}

View File

@@ -0,0 +1,10 @@
export {
UMB_MEDIA_TYPE_TREE_ALIAS,
UMB_MEDIA_TYPE_TREE_STORE_ALIAS,
UMB_MEDIA_TYPE_TREE_REPOSITORY_ALIAS,
} from './manifests.js';
export { UmbMediaTypeTreeRepository } from './media-type-tree.repository.js';
export { UMB_MEDIA_TYPE_TREE_STORE_CONTEXT } from './media-type-tree.store.js';
export type { UmbMediaTypeTreeItemModel, UmbMediaTypeTreeRootModel } from './types.js';

View File

@@ -1,12 +1,36 @@
import { UMB_MEDIA_TYPE_REPOSITORY_ALIAS } from '../repository/manifests.js';
import type { ManifestTree, ManifestTreeItem } from '@umbraco-cms/backoffice/extension-registry';
import { UmbMediaTypeTreeRepository } from './media-type-tree.repository.js';
import { UmbMediaTypeTreeStore } from './media-type-tree.store.js';
import type {
ManifestRepository,
ManifestTree,
ManifestTreeItem,
ManifestTreeStore,
} from '@umbraco-cms/backoffice/extension-registry';
export const UMB_MEDIA_TYPE_TREE_REPOSITORY_ALIAS = 'Umb.Repository.MediaType.Tree';
export const UMB_MEDIA_TYPE_TREE_STORE_ALIAS = 'Umb.Store.MediaType.Tree';
export const UMB_MEDIA_TYPE_TREE_ALIAS = 'Umb.Tree.MediaType';
const treeRepository: ManifestRepository = {
type: 'repository',
alias: UMB_MEDIA_TYPE_TREE_REPOSITORY_ALIAS,
name: 'Media Type Tree Repository',
api: UmbMediaTypeTreeRepository,
};
const treeStore: ManifestTreeStore = {
type: 'treeStore',
alias: UMB_MEDIA_TYPE_TREE_STORE_ALIAS,
name: 'Media Type Tree Store',
api: UmbMediaTypeTreeStore,
};
const tree: ManifestTree = {
type: 'tree',
alias: 'Umb.Tree.MediaTypes',
name: 'Media Types Tree',
alias: UMB_MEDIA_TYPE_TREE_ALIAS,
name: 'Media Type Tree',
meta: {
repositoryAlias: UMB_MEDIA_TYPE_REPOSITORY_ALIAS,
repositoryAlias: UMB_MEDIA_TYPE_TREE_REPOSITORY_ALIAS,
},
};
@@ -20,4 +44,4 @@ const treeItem: ManifestTreeItem = {
},
};
export const manifests = [tree, treeItem];
export const manifests = [treeRepository, treeStore, tree, treeItem];

View File

@@ -0,0 +1,28 @@
import { UMB_MEDIA_TYPE_ROOT_ENTITY_TYPE } from '../index.js';
import { UmbMediaTypeTreeServerDataSource } from './media-type-tree.server.data-source.js';
import { UMB_MEDIA_TYPE_TREE_STORE_CONTEXT } from './media-type-tree.store.js';
import { UmbMediaTypeTreeItemModel, UmbMediaTypeTreeRootModel } from './types.js';
import { UmbTreeRepositoryBase } from '@umbraco-cms/backoffice/tree';
import { type UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { UmbApi } from '@umbraco-cms/backoffice/extension-api';
export class UmbMediaTypeTreeRepository
extends UmbTreeRepositoryBase<UmbMediaTypeTreeItemModel, UmbMediaTypeTreeRootModel>
implements UmbApi
{
constructor(host: UmbControllerHost) {
super(host, UmbMediaTypeTreeServerDataSource, UMB_MEDIA_TYPE_TREE_STORE_CONTEXT);
}
async requestTreeRoot() {
const data = {
id: null,
type: UMB_MEDIA_TYPE_ROOT_ENTITY_TYPE,
name: 'Media Types',
icon: 'icon-folder',
hasChildren: true,
};
return { data };
}
}

View File

@@ -1,10 +1,10 @@
import type { UmbTreeDataSource } from '@umbraco-cms/backoffice/tree';
import { MediaTypeResource } from '@umbraco-cms/backoffice/backend-api';
import { type UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { type UmbTreeDataSource } from '@umbraco-cms/backoffice/tree';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources';
/**
* A data source for the MediaType tree that fetches data from the server
* A data source for the Media Type tree that fetches data from the server
* @export
* @class UmbMediaTypeTreeServerDataSource
* @implements {UmbTreeDataSource}
@@ -13,9 +13,9 @@ export class UmbMediaTypeTreeServerDataSource implements UmbTreeDataSource {
#host: UmbControllerHost;
/**
* Creates an instance of MediaTypeTreeDataSource.
* Creates an instance of UmbMediaTypeTreeServerDataSource.
* @param {UmbControllerHost} host
* @memberof MediaTypeTreeDataSource
* @memberof UmbMediaTypeTreeServerDataSource
*/
constructor(host: UmbControllerHost) {
this.#host = host;
@@ -60,8 +60,8 @@ export class UmbMediaTypeTreeServerDataSource implements UmbTreeDataSource {
* @memberof UmbMediaTypeTreeServerDataSource
*/
async getItems(ids: Array<string>) {
if (!ids || ids.length === 0) {
throw new Error('Keys are missing');
if (ids) {
throw new Error('Ids are missing');
}
return tryExecuteAndNotify(

View File

@@ -1,11 +1,11 @@
import { UmbContextToken } from '@umbraco-cms/backoffice/context-api';
import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api';
import { UmbEntityTreeStore } from '@umbraco-cms/backoffice/tree';
import type { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api';
/**
* @export
* @class UmbMediaTypeTreeStore
* @extends {UmbEntityTreeStore}
* @extends {UmbStoreBase}
* @description - Tree Data Store for Media Types
*/
export class UmbMediaTypeTreeStore extends UmbEntityTreeStore {
@@ -15,10 +15,8 @@ export class UmbMediaTypeTreeStore extends UmbEntityTreeStore {
* @memberof UmbMediaTypeTreeStore
*/
constructor(host: UmbControllerHostElement) {
super(host, UMB_MEDIA_TYPE_TREE_STORE_CONTEXT_TOKEN.toString());
super(host, UMB_MEDIA_TYPE_TREE_STORE_CONTEXT.toString());
}
}
export const UMB_MEDIA_TYPE_TREE_STORE_CONTEXT_TOKEN = new UmbContextToken<UmbMediaTypeTreeStore>(
'UmbMediaTypeTreeStore',
);
export const UMB_MEDIA_TYPE_TREE_STORE_CONTEXT = new UmbContextToken<UmbMediaTypeTreeStore>('UmbMediaTypeTreeStore');

View File

@@ -0,0 +1,5 @@
import type { MediaTypeTreeItemResponseModel } from '@umbraco-cms/backoffice/backend-api';
import type { UmbEntityTreeItemModel, UmbEntityTreeRootModel } from '@umbraco-cms/backoffice/tree';
export type UmbMediaTypeTreeItemModel = MediaTypeTreeItemResponseModel & UmbEntityTreeItemModel;
export type UmbMediaTypeTreeRootModel = MediaTypeTreeItemResponseModel & UmbEntityTreeRootModel;

View File

@@ -17,17 +17,17 @@ const workspace: ManifestWorkspace = {
},
};
const workspaceViews: Array<ManifestWorkspaceEditorView> = [
const workspaceEditorViews: Array<ManifestWorkspaceEditorView> = [
{
type: 'workspaceEditorView',
alias: 'Umb.WorkspaceView.MediaType.Design',
name: 'Media Type Workspace Design View',
js: () => import('./views/details/media-type-design-workspace-view.element.js'),
weight: 90,
js: () => import('./views/design/media-type-workspace-view-edit.element.js'),
weight: 1000,
meta: {
label: 'Details',
pathname: 'details',
icon: 'document',
label: 'Design',
pathname: 'design',
icon: 'icon-document-dashed-line',
},
conditions: [
{
@@ -38,32 +38,14 @@ const workspaceViews: Array<ManifestWorkspaceEditorView> = [
},
{
type: 'workspaceEditorView',
alias: 'Umb.WorkspaceView.MediaType.ListView',
name: 'Media Type Workspace ListView View',
js: () => import('./views/details/media-type-list-view-workspace-view.element.js'),
weight: 90,
alias: 'Umb.WorkspaceView.MediaType.Structure',
name: 'Media Type Workspace Structure View',
js: () => import('./views/structure/media-type-workspace-view-structure.element.js'),
weight: 800,
meta: {
label: 'List View',
pathname: 'list-view',
icon: 'document',
},
conditions: [
{
alias: 'Umb.Condition.WorkspaceAlias',
match: workspace.alias,
},
],
},
{
type: 'workspaceEditorView',
alias: 'Umb.WorkspaceView.MediaType.Permissions',
name: 'Media Type Workspace Permissions View',
js: () => import('./views/details/media-type-permissions-workspace-view.element.js'),
weight: 90,
meta: {
label: 'Permissions',
pathname: 'permissions',
icon: 'document',
label: 'Structure',
pathname: 'structure',
icon: 'icon-mindmap',
},
conditions: [
{
@@ -94,4 +76,4 @@ const workspaceActions: Array<ManifestWorkspaceAction> = [
},
];
export const manifests = [workspace, ...workspaceViews, ...workspaceViewCollections, ...workspaceActions];
export const manifests = [workspace, ...workspaceEditorViews, ...workspaceViewCollections, ...workspaceActions];

View File

@@ -1,80 +1,118 @@
import { UmbMediaTypeRepository } from '../repository/media-type.repository.js';
import { UmbSaveableWorkspaceContextInterface, UmbEditableWorkspaceContextBase } from '@umbraco-cms/backoffice/workspace';
import { UmbMediaTypeDetailRepository } from '../repository/detail/media-type-detail.repository.js';
import { UMB_MEDIA_TYPE_ENTITY_TYPE } from '../index.js';
import {
UmbSaveableWorkspaceContextInterface,
UmbEditableWorkspaceContextBase,
} from '@umbraco-cms/backoffice/workspace';
import { UmbContentTypePropertyStructureManager } from '@umbraco-cms/backoffice/content-type';
import { type MediaTypeResponseModel } from '@umbraco-cms/backoffice/backend-api';
import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api';
import { UmbObjectState } from '@umbraco-cms/backoffice/observable-api';
import { UmbContextToken } from '@umbraco-cms/backoffice/context-api';
import { MediaTypeResponseModel } from '@umbraco-cms/backoffice/backend-api';
import { UmbBooleanState } from '@umbraco-cms/backoffice/observable-api';
type EntityType = MediaTypeResponseModel;
export class UmbMediaTypeWorkspaceContext
extends UmbEditableWorkspaceContextBase<UmbMediaTypeRepository, EntityType>
extends UmbEditableWorkspaceContextBase<UmbMediaTypeDetailRepository, EntityType>
implements UmbSaveableWorkspaceContextInterface<EntityType | undefined>
{
#data = new UmbObjectState<EntityType | undefined>(undefined);
data = this.#data.asObservable();
#getDataPromise?: Promise<any>;
// Draft is located in structure manager
name = this.#data.asObservablePart((data) => data?.name);
id = this.#data.asObservablePart((data) => data?.id);
alias = this.#data.asObservablePart((data) => data?.alias);
description = this.#data.asObservablePart((data) => data?.description);
icon = this.#data.asObservablePart((data) => data?.icon);
// General for content types:
readonly data;
readonly name;
readonly alias;
readonly description;
readonly icon;
readonly allowedAsRoot;
readonly allowedContentTypes;
readonly compositions;
readonly structure;
#isSorting = new UmbBooleanState(undefined);
isSorting = this.#isSorting.asObservable();
constructor(host: UmbControllerHostElement) {
super(host, 'Umb.Workspace.MediaType', new UmbMediaTypeRepository(host));
super(host, 'Umb.Workspace.MediaType', new UmbMediaTypeDetailRepository(host));
this.structure = new UmbContentTypePropertyStructureManager(this.host, this.repository);
// General for content types:
this.data = this.structure.ownerContentType;
this.name = this.structure.ownerContentTypeObservablePart((data) => data?.name);
this.alias = this.structure.ownerContentTypeObservablePart((data) => data?.alias);
this.description = this.structure.ownerContentTypeObservablePart((data) => data?.description);
this.icon = this.structure.ownerContentTypeObservablePart((data) => data?.icon);
this.allowedAsRoot = this.structure.ownerContentTypeObservablePart((data) => data?.allowedAsRoot);
this.allowedContentTypes = this.structure.ownerContentTypeObservablePart((data) => data?.allowedContentTypes);
this.compositions = this.structure.ownerContentTypeObservablePart((data) => data?.compositions);
}
getIsSorting() {
return this.#isSorting.getValue();
}
setIsSorting(isSorting: boolean) {
this.#isSorting.next(isSorting);
}
getData() {
return this.#data.getValue();
return this.structure.getOwnerContentType() || {};
}
getEntityId() {
return this.getData()?.id || '';
return this.getData().id;
}
getEntityType() {
return 'media-type';
return UMB_MEDIA_TYPE_ENTITY_TYPE;
}
updateProperty<PropertyName extends keyof EntityType>(propertyName: PropertyName, value: EntityType[PropertyName]) {
this.#data.update({ [propertyName]: value });
}
async load(id: string) {
this.#getDataPromise = this.repository.requestById(id);
const { data } = await this.#getDataPromise;
if (data) {
this.setIsNew(false);
this.#data.update(data);
}
this.structure.updateOwnerContentType({ [propertyName]: value });
}
async create(parentId: string | null) {
this.#getDataPromise = this.repository.createScaffold(parentId);
const { data } = await this.#getDataPromise;
if (!data) return;
const { data } = await this.structure.createScaffold(parentId);
if (!data) return undefined;
this.setIsNew(true);
//TODO: Model mismatch. FIX
this.#data.next(data as unknown as MediaTypeResponseModel);
this.setIsSorting(false);
//this.#draft.next(data);
return { data } || undefined;
// TODO: Is this wrong? should we return { data }??
}
async save() {
if (!this.#data.value) return;
if (!this.#data.value.id) return;
async load(entityId: string) {
const { data } = await this.structure.loadType(entityId);
if (!data) return undefined;
this.setIsNew(false);
this.setIsSorting(false);
//this.#draft.next(data);
return { data } || undefined;
// TODO: Is this wrong? should we return { data }??
}
/**
* Save or creates the media type, based on wether its a new one or existing.
*/
async save() {
if (this.getIsNew()) {
await this.repository.create(this.#data.value);
if ((await this.structure.create()) === true) {
this.setIsNew(false);
}
} else {
await this.repository.save(this.#data.value.id, this.#data.value);
await this.structure.save();
}
this.saveComplete(this.#data.value);
this.saveComplete(this.getData());
}
public destroy(): void {
this.#data.destroy();
this.structure.destroy();
super.destroy();
}
}
@@ -83,5 +121,6 @@ export const UMB_MEDIA_TYPE_WORKSPACE_CONTEXT = new UmbContextToken<
UmbMediaTypeWorkspaceContext
>(
'UmbWorkspaceContext',
(context): context is UmbMediaTypeWorkspaceContext => context.getEntityType?.() === 'media-type',
undefined,
(context): context is UmbMediaTypeWorkspaceContext => context.getEntityType?.() === UMB_MEDIA_TYPE_ENTITY_TYPE,
);

View File

@@ -1,9 +1,10 @@
import { UmbMediaTypeWorkspaceContext } from './media-type-workspace.context.js';
import { UmbMediaTypeWorkspaceEditorElement } from './media-type-workspace-editor.element.js';
import { UmbTextStyles } from "@umbraco-cms/backoffice/style";
import { css, html, customElement, state } from '@umbraco-cms/backoffice/external/lit';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { html, customElement, state } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
import type { UmbRoute } from '@umbraco-cms/backoffice/router';
import { UmbWorkspaceIsNewRedirectController } from '@umbraco-cms/backoffice/workspace';
@customElement('umb-media-type-workspace')
export class UmbMediaTypeWorkspaceElement extends UmbLitElement {
@@ -12,10 +13,24 @@ export class UmbMediaTypeWorkspaceElement extends UmbLitElement {
@state()
_routes: UmbRoute[] = [
{
path: 'create/:parentId',
component: import('./media-type-workspace-editor.element.js'),
setup: (_component, info) => {
const parentId = info.match.params.parentId === 'null' ? null : info.match.params.parentId;
this.#workspaceContext.create(parentId);
new UmbWorkspaceIsNewRedirectController(
this,
this.#workspaceContext,
this.shadowRoot!.querySelector('umb-router-slot')!,
);
},
},
{
path: 'edit/:id',
component: () => this.#element,
setup: (component, info) => {
setup: (_component, info) => {
const id = info.match.params.id;
this.#workspaceContext.load(id);
},
@@ -26,20 +41,7 @@ export class UmbMediaTypeWorkspaceElement extends UmbLitElement {
return html`<umb-router-slot .routes=${this._routes}></umb-router-slot>`;
}
static styles = [
UmbTextStyles,
css`
#header {
display: flex;
padding: 0 var(--uui-size-layout-1);
gap: var(--uui-size-space-4);
width: 100%;
}
uui-input {
width: 100%;
}
`,
];
static styles = [UmbTextStyles];
}
export default UmbMediaTypeWorkspaceElement;

View File

@@ -0,0 +1,214 @@
import { UmbMediaTypeWorkspaceContext } from '../../media-type-workspace.context.js';
import './media-type-workspace-view-edit-property.element.js';
import { css, html, customElement, property, state, repeat, ifDefined } from '@umbraco-cms/backoffice/external/lit';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { UmbContentTypePropertyStructureHelper, PropertyContainerTypes } from '@umbraco-cms/backoffice/content-type';
import { UmbSorterController, UmbSorterConfig } from '@umbraco-cms/backoffice/sorter';
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
import {
MediaTypePropertyTypeResponseModel,
MediaTypeResponseModel,
PropertyTypeModelBaseModel,
} from '@umbraco-cms/backoffice/backend-api';
import { UMB_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/workspace';
import { UMB_PROPERTY_SETTINGS_MODAL, UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/modal';
const SORTER_CONFIG: UmbSorterConfig<MediaTypePropertyTypeResponseModel> = {
compareElementToModel: (element: HTMLElement, model: MediaTypePropertyTypeResponseModel) => {
return element.getAttribute('data-umb-property-id') === model.id;
},
querySelectModelToElement: (container: HTMLElement, modelEntry: MediaTypePropertyTypeResponseModel) => {
return container.querySelector('data-umb-property-id=[' + modelEntry.id + ']');
},
identifier: 'content-type-property-sorter',
itemSelector: '[data-umb-property-id]',
disabledItemSelector: '[inherited]',
containerSelector: '#property-list',
};
@customElement('umb-media-type-workspace-view-edit-properties')
export class UmbMediaTypeWorkspaceViewEditPropertiesElement extends UmbLitElement {
#propertySorter = new UmbSorterController(this, {
...SORTER_CONFIG,
performItemInsert: (args) => {
let sortOrder = 0;
if (this._propertyStructure.length > 0) {
if (args.newIndex === 0) {
sortOrder = (this._propertyStructure[0].sortOrder ?? 0) - 1;
} else {
sortOrder =
(this._propertyStructure[Math.min(args.newIndex, this._propertyStructure.length - 1)].sortOrder ?? 0) + 1;
}
}
return this._propertyStructureHelper.insertProperty(args.item, sortOrder);
},
performItemRemove: (args) => {
return this._propertyStructureHelper.removeProperty(args.item.id!);
},
});
private _containerId: string | undefined;
@property({ type: String, attribute: 'container-id', reflect: false })
public get containerId(): string | undefined {
return this._containerId;
}
public set containerId(value: string | undefined) {
if (value === this._containerId) return;
const oldValue = this._containerId;
this._containerId = value;
this.requestUpdate('containerId', oldValue);
}
@property({ type: String, attribute: 'container-name', reflect: false })
public get containerName(): string | undefined {
return this._propertyStructureHelper.getContainerName();
}
public set containerName(value: string | undefined) {
this._propertyStructureHelper.setContainerName(value);
}
@property({ type: String, attribute: 'container-type', reflect: false })
public get containerType(): PropertyContainerTypes | undefined {
return this._propertyStructureHelper.getContainerType();
}
public set containerType(value: PropertyContainerTypes | undefined) {
this._propertyStructureHelper.setContainerType(value);
}
_propertyStructureHelper = new UmbContentTypePropertyStructureHelper(this);
@state()
_propertyStructure: Array<PropertyTypeModelBaseModel> = [];
@state()
_ownerMediaTypes?: MediaTypeResponseModel[];
@state()
protected _modalRouteNewProperty?: string;
@state()
_sortModeActive?: boolean;
constructor() {
super();
this.consumeContext(UMB_WORKSPACE_CONTEXT, (workspaceContext) => {
this._propertyStructureHelper.setStructureManager((workspaceContext as UmbMediaTypeWorkspaceContext).structure);
this.observe(
(workspaceContext as UmbMediaTypeWorkspaceContext).isSorting,
(isSorting) => {
this._sortModeActive = isSorting;
this.#setModel(isSorting);
},
'_observeIsSorting',
);
});
this.observe(this._propertyStructureHelper.propertyStructure, (propertyStructure) => {
this._propertyStructure = propertyStructure;
});
// Note: Route for adding a new property
new UmbModalRouteRegistrationController(this, UMB_PROPERTY_SETTINGS_MODAL)
.addAdditionalPath('new-property')
.onSetup(async () => {
const mediaTypeId = this._ownerMediaTypes?.find(
(types) => types.containers?.find((containers) => containers.id === this.containerId),
)?.id;
if (mediaTypeId === undefined) return false;
const propertyData = await this._propertyStructureHelper.createPropertyScaffold(this._containerId);
if (propertyData === undefined) return false;
return { propertyData, documentTypeId: mediaTypeId }; //TODO: Should we have a separate modal for mediaTypes?
})
.onSubmit((result) => {
this.#addProperty(result);
})
.observeRouteBuilder((routeBuilder) => {
this._modalRouteNewProperty = routeBuilder(null);
});
}
#setModel(isSorting?: boolean) {
if (isSorting) {
this.#propertySorter.setModel(this._propertyStructure);
} else {
this.#propertySorter.setModel([]);
}
}
connectedCallback(): void {
super.connectedCallback();
const mediaTypes = this._propertyStructureHelper.ownerDocumentTypes; //TODO: Should we have a separate propertyStructureHelper for mediaTypes?
if (!mediaTypes) return;
this.observe(
mediaTypes,
(medias) => {
this._ownerMediaTypes = medias;
},
'observeOwnerMediaTypes',
);
}
async #addProperty(propertyData: PropertyTypeModelBaseModel) {
const propertyPlaceholder = await this._propertyStructureHelper.addProperty(this._containerId);
if (!propertyPlaceholder) return;
this._propertyStructureHelper.partialUpdateProperty(propertyPlaceholder.id, propertyData);
}
render() {
return html`<div id="property-list">
${repeat(
this._propertyStructure,
(property) => property.id ?? '' + property.containerId ?? '' + property.sortOrder ?? '',
(property) => {
// Note: This piece might be moved into the property component
const inheritedFromMedia = this._ownerMediaTypes?.find(
(types) => types.containers?.find((containers) => containers.id === property.containerId),
);
return html`<media-type-workspace-view-edit-property
data-umb-property-id=${ifDefined(property.id)}
owner-media-type-id=${ifDefined(inheritedFromMedia?.id)}
owner-media-type-name=${ifDefined(inheritedFromMedia?.name)}
?inherited=${property.containerId !== this.containerId}
?sort-mode-active=${this._sortModeActive}
.property=${property}
@partial-property-update=${(event: CustomEvent) => {
this._propertyStructureHelper.partialUpdateProperty(property.id, event.detail);
}}
@property-delete=${() => {
this._propertyStructureHelper.removeProperty(property.id!);
}}>
</media-type-workspace-view-edit-property>`;
},
)}
</div>
${!this._sortModeActive
? html`<uui-button
label=${this.localize.term('contentTypeEditor_addProperty')}
id="add"
look="placeholder"
href=${ifDefined(this._modalRouteNewProperty)}>
<umb-localize key="contentTypeEditor_addProperty">Add property</umb-localize>
</uui-button> `
: ''} `;
}
static styles = [
UmbTextStyles,
css`
#add {
width: 100%;
}
`,
];
}
export default UmbMediaTypeWorkspaceViewEditPropertiesElement;
declare global {
interface HTMLElementTagNameMap {
'umb-media-type-workspace-view-edit-properties': UmbMediaTypeWorkspaceViewEditPropertiesElement;
}
}

View File

@@ -0,0 +1,478 @@
import { UmbDataTypeDetailRepository } from '@umbraco-cms/backoffice/data-type';
import { UUIInputElement, UUIInputEvent } from '@umbraco-cms/backoffice/external/uui';
import { css, html, customElement, property, state, ifDefined, nothing } from '@umbraco-cms/backoffice/external/lit';
import { PropertyTypeModelBaseModel } from '@umbraco-cms/backoffice/backend-api';
import {
UMB_CONFIRM_MODAL,
UMB_MODAL_MANAGER_CONTEXT_TOKEN,
UMB_PROPERTY_SETTINGS_MODAL,
UMB_WORKSPACE_MODAL,
UmbConfirmModalData,
UmbModalRouteRegistrationController,
} from '@umbraco-cms/backoffice/modal';
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
import { generateAlias } from '@umbraco-cms/backoffice/utils';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
/**
* @element media-type-workspace-view-edit-property
* @description - Element for displaying a property in an workspace.
* @slot editor - Slot for rendering the Property Editor
*/
@customElement('media-type-workspace-view-edit-property')
export class UmbMediaTypeWorkspacePropertyElement extends UmbLitElement {
private _property?: PropertyTypeModelBaseModel | undefined;
/**
* Property, the data object for the property.
* @type {PropertyTypeModelBaseModel}
* @attr
* @default undefined
*/
@property({ type: Object })
public get property(): PropertyTypeModelBaseModel | undefined {
return this._property;
}
public set property(value: PropertyTypeModelBaseModel | undefined) {
const oldValue = this._property;
this._property = value;
this.#modalRegistration.setUniquePathValue('propertyId', value?.id?.toString());
this.setDataType(this._property?.dataTypeId);
this.requestUpdate('property', oldValue);
}
/**
* Inherited, Determines if the property is part of the main media type thats being edited.
* If true, then the property is inherited from another media type, not a part of the main media type.
* @type {boolean}
* @attr
* @default undefined
*/
@property({ type: Boolean })
public inherited?: boolean;
@property({ type: Boolean, reflect: true, attribute: 'sort-mode-active' })
public sortModeActive = false;
#dataTypeDetailRepository = new UmbDataTypeDetailRepository(this);
#modalRegistration;
private _modalManagerContext?: typeof UMB_MODAL_MANAGER_CONTEXT_TOKEN.TYPE;
@state()
protected _modalRoute?: string;
@state()
protected _editMediaTypePath?: string;
@property()
public get modalRoute() {
return this._modalRoute;
}
@property({ type: String, attribute: 'owner-media-type-id' })
public ownerMediaTypeId?: string;
@property({ type: String, attribute: 'owner-media-type-name' })
public ownerMediaTypeName?: string;
@state()
private _dataTypeName?: string;
async setDataType(dataTypeId: string | undefined) {
if (!dataTypeId) return;
this.#dataTypeDetailRepository.requestById(dataTypeId).then((x) => (this._dataTypeName = x?.data?.name));
}
constructor() {
super();
this.#modalRegistration = new UmbModalRouteRegistrationController(this, UMB_PROPERTY_SETTINGS_MODAL)
.addUniquePaths(['propertyId'])
.onSetup(() => {
const mediaTypeId = this.ownerMediaTypeId;
if (mediaTypeId === undefined) return false;
const propertyData = this.property;
if (propertyData === undefined) return false;
return { propertyData, documentTypeId: mediaTypeId }; //TODO: Should we have a separate modal for mediaTypes?
})
.onSubmit((result) => {
this._partialUpdate(result);
})
.observeRouteBuilder((routeBuilder) => {
this._modalRoute = routeBuilder(null);
});
new UmbModalRouteRegistrationController(this, UMB_WORKSPACE_MODAL)
.addAdditionalPath('media-type')
.onSetup(() => {
return { entityType: 'media-type', preset: {} };
})
.observeRouteBuilder((routeBuilder) => {
this._editMediaTypePath = routeBuilder({});
});
this.consumeContext(UMB_MODAL_MANAGER_CONTEXT_TOKEN, (context) => {
this._modalManagerContext = context;
});
}
_partialUpdate(partialObject: PropertyTypeModelBaseModel) {
this.dispatchEvent(new CustomEvent('partial-property-update', { detail: partialObject }));
}
_singleValueUpdate(propertyName: string, value: string | number | boolean | null | undefined) {
const partialObject = {} as any;
partialObject[propertyName] = value;
this.dispatchEvent(new CustomEvent('partial-property-update', { detail: partialObject }));
}
@state()
private _aliasLocked = true;
#onToggleAliasLock() {
this._aliasLocked = !this._aliasLocked;
}
#requestRemove(e: Event) {
e.preventDefault();
e.stopImmediatePropagation();
if (!this.property || !this.property.id) return;
const Message: UmbConfirmModalData = {
headline: `${this.localize.term('actions_delete')} property`,
content: html`<umb-localize key="contentTypeEditor_confirmDeletePropertyMessage" .args=${[
this.property.name || this.property.id,
]}>
Are you sure you want to delete the property <strong>${this.property.name || this.property.id}</strong>
</umb-localize>
</div>`,
confirmLabel: this.localize.term('actions_delete'),
color: 'danger',
};
const modalHandler = this._modalManagerContext?.open(UMB_CONFIRM_MODAL, Message);
modalHandler
?.onSubmit()
.then(() => {
this.dispatchEvent(new CustomEvent('property-delete'));
})
.catch(() => {
// We do not need to react to cancel, so we will leave an empty method to prevent Uncaught Promise Rejection error.
return;
});
}
#onNameChange(event: UUIInputEvent) {
if (event instanceof UUIInputEvent) {
const target = event.composedPath()[0] as UUIInputElement;
if (typeof target?.value === 'string') {
const oldName = this.property?.name ?? '';
const oldAlias = this.property?.alias ?? '';
const newName = event.target.value.toString();
if (this._aliasLocked) {
const expectedOldAlias = generateAlias(oldName ?? '');
// Only update the alias if the alias matches a generated alias of the old name (otherwise the alias is considered one written by the user.)
if (expectedOldAlias === oldAlias) {
this._singleValueUpdate('alias', generateAlias(newName ?? ''));
}
}
this._singleValueUpdate('name', newName);
}
}
}
renderSortableProperty() {
if (!this.property) return;
return html`
<div class="sortable">
<uui-icon name="${this.inherited ? 'icon-merge' : 'icon-navigation'}"></uui-icon>
${this.property.name} <span style="color: var(--uui-color-disabled-contrast)">(${this.property.alias})</span>
</div>
<uui-input
type="number"
?readonly=${this.inherited}
label="sort order"
.value=${this.property.sortOrder ?? 0}></uui-input>
`;
}
renderEditableProperty() {
if (!this.property) return;
if (this.sortModeActive) {
return this.renderSortableProperty();
} else {
return html`
<div id="header">
<uui-input
name="label"
id="label-input"
placeholder=${this.localize.term('placeholders_label')}
label="label"
.value=${this.property.name}
@input=${this.#onNameChange}></uui-input>
${this.renderPropertyAlias()}
<slot name="property-action-menu"></slot>
<p>
<uui-textarea
label="description"
name="description"
id="description-input"
placeholder=${this.localize.term('placeholders_enterDescription')}
.value=${this.property.description}
@input=${(e: CustomEvent) => {
if (e.target) this._singleValueUpdate('description', (e.target as HTMLInputElement).value);
}}></uui-textarea>
</p>
</div>
<uui-button
id="editor"
label=${this.localize.term('contentTypeEditor_editorSettings')}
href=${ifDefined(this._modalRoute)}>
${this.renderPropertyTags()}
<uui-action-bar>
<uui-button label="${this.localize.term('actions_delete')}" @click="${this.#requestRemove}">
<uui-icon name="delete"></uui-icon>
</uui-button>
</uui-action-bar>
</uui-button>
`;
}
}
renderInheritedProperty() {
if (!this.property) return;
if (this.sortModeActive) {
return this.renderSortableProperty();
} else {
return html`
<div id="header">
<b>${this.property.name}</b>
<i>${this.property.alias}</i>
<p>${this.property.description}</p>
</div>
<div id="editor">
${this.renderPropertyTags()}
<uui-tag look="default" class="inherited">
<uui-icon name="icon-merge"></uui-icon>
<span
>${this.localize.term('contentTypeEditor_inheritedFrom')}
<a href=${this._editMediaTypePath + 'edit/' + this.ownerMediaTypeId}>
${this.ownerMediaTypeName ?? '??'}
</a>
</span>
</uui-tag>
</div>
`;
}
}
renderPropertyAlias() {
return this.property
? html`<uui-input
name="alias"
id="alias-input"
label="alias"
placeholder=${this.localize.term('placeholders_alias')}
.value=${this.property.alias}
?disabled=${this._aliasLocked}
@input=${(e: CustomEvent) => {
if (e.target) this._singleValueUpdate('alias', (e.target as HTMLInputElement).value);
}}>
<!-- TODO: should use UUI-LOCK-INPUT, but that does not fire an event when its locked/unlocked -->
<!-- TODO: validation for bad characters -->
<div @click=${this.#onToggleAliasLock} @keydown=${() => ''} id="alias-lock" slot="prepend">
<uui-icon name=${this._aliasLocked ? 'icon-lock' : 'icon-unlocked'}></uui-icon>
</div>
</uui-input>`
: '';
}
renderPropertyTags() {
return this.property
? html`<div class="types">
${this.property.dataTypeId ? html`<uui-tag look="default">${this._dataTypeName}</uui-tag>` : nothing}
${this.property.variesByCulture
? html`<uui-tag look="default">
<uui-icon name="icon-shuffle"></uui-icon> ${this.localize.term('contentTypeEditor_cultureVariantLabel')}
</uui-tag>`
: nothing}
${this.property.appearance?.labelOnTop == true
? html`<uui-tag look="default">
<span>${this.localize.term('contentTypeEditor_displaySettingsLabelOnTop')}</span>
</uui-tag>`
: nothing}
</div>`
: nothing;
}
render() {
// TODO: Only show alias on label if user has access to MediaType within settings:
return this.inherited ? this.renderInheritedProperty() : this.renderEditableProperty();
}
static styles = [
UmbTextStyles,
css`
:host(:not([sort-mode-active])) {
display: grid;
grid-template-columns: 200px auto;
column-gap: var(--uui-size-layout-2);
border-bottom: 1px solid var(--uui-color-divider);
padding: var(--uui-size-layout-1) 0;
container-type: inline-size;
}
:host > div {
grid-column: span 2;
}
@container (width > 600px) {
:host(:not([orientation='vertical'])) > div {
grid-column: span 1;
}
}
:host(:first-of-type) {
padding-top: 0;
}
:host(:last-of-type) {
border-bottom: none;
}
:host([sort-mode-active]) {
position: relative;
display: flex;
padding: 0;
margin-bottom: var(--uui-size-3);
}
:host([sort-mode-active]:last-of-type) {
margin-bottom: 0;
}
:host([sort-mode-active]:not([inherited])) {
cursor: grab;
}
:host([sort-mode-active]) .sortable {
flex: 1;
display: flex;
background-color: var(--uui-color-divider);
align-items: center;
padding: 0 var(--uui-size-3);
gap: var(--uui-size-3);
}
:host([sort-mode-active]) uui-input {
max-width: 75px;
}
/* Placeholder style, used when property is being dragged.*/
:host(.--umb-sorter-placeholder) > * {
visibility: hidden;
}
:host(.--umb-sorter-placeholder)::after {
content: '';
inset: 0;
position: absolute;
border: 1px dashed var(--uui-color-divider-emphasis);
border-radius: var(--uui-border-radius);
}
p {
margin-bottom: 0;
}
#header {
position: sticky;
top: var(--uui-size-space-4);
height: min-content;
z-index: 2;
}
#editor {
position: relative;
background-color: var(--uui-color-background);
}
#alias-input,
#label-input,
#description-input {
width: 100%;
}
#alias-input {
border-color: transparent;
background: var(--uui-color-surface);
}
#label-input {
font-weight: bold; /* TODO: UUI Input does not support bold text yet */
--uui-input-border-color: transparent;
}
#label-input input {
font-weight: bold;
--uui-input-border-color: transparent;
}
#alias-lock {
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
}
#alias-lock uui-icon {
margin-bottom: 2px;
/* margin: 0; */
}
#description-input {
--uui-textarea-border-color: transparent;
font-weight: 0.5rem; /* TODO: Cant change font size of UUI textarea yet */
}
.types > div uui-icon,
.inherited uui-icon {
vertical-align: sub;
}
.inherited {
position: absolute;
top: var(--uui-size-space-2);
right: var(--uui-size-space-2);
}
.types {
position: absolute;
top: var(--uui-size-space-2);
left: var(--uui-size-space-2);
display: flex;
gap: var(--uui-size-space-2);
}
#editor uui-action-bar {
position: absolute;
top: var(--uui-size-space-2);
right: var(--uui-size-space-2);
display: none;
}
#editor:hover uui-action-bar,
#editor:focus uui-action-bar {
display: block;
}
a {
color: inherit;
}
`,
];
}
declare global {
interface HTMLElementTagNameMap {
'media-type-workspace-view-edit-property': UmbMediaTypeWorkspacePropertyElement;
}
}

View File

@@ -0,0 +1,272 @@
import { UmbMediaTypeWorkspaceContext } from '../../media-type-workspace.context.js';
import { css, html, customElement, property, state, repeat, ifDefined } from '@umbraco-cms/backoffice/external/lit';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { UmbContentTypeContainerStructureHelper } from '@umbraco-cms/backoffice/content-type';
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
import { PropertyTypeContainerModelBaseModel } from '@umbraco-cms/backoffice/backend-api';
import { UMB_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/workspace';
import { UmbSorterConfig, UmbSorterController } from '@umbraco-cms/backoffice/sorter';
import './media-type-workspace-view-edit-properties.element.js';
const SORTER_CONFIG: UmbSorterConfig<PropertyTypeContainerModelBaseModel> = {
compareElementToModel: (element: HTMLElement, model: PropertyTypeContainerModelBaseModel) => {
return element.getAttribute('data-umb-group-id') === model.id;
},
querySelectModelToElement: (container: HTMLElement, modelEntry: PropertyTypeContainerModelBaseModel) => {
return container.querySelector('data-umb-group-id=[' + modelEntry.id + ']');
},
identifier: 'content-type-group-sorter',
itemSelector: '[data-umb-group-id]',
disabledItemSelector: '[inherited]',
containerSelector: '#group-list',
};
@customElement('umb-media-type-workspace-view-edit-tab')
export class UmbMediaTypeWorkspaceViewEditTabElement extends UmbLitElement {
public sorter?: UmbSorterController<PropertyTypeContainerModelBaseModel>;
config: UmbSorterConfig<PropertyTypeContainerModelBaseModel> = {
...SORTER_CONFIG,
performItemInsert: async (args) => {
if (!this._groups) return false;
const oldIndex = this._groups.findIndex((group) => group.id! === args.item.id);
if (args.newIndex === oldIndex) return true;
let sortOrder = 0;
//TODO the sortOrder set is not correct
if (this._groups.length > 0) {
if (args.newIndex === 0) {
sortOrder = (this._groups[0].sortOrder ?? 0) - 1;
} else {
sortOrder = (this._groups[Math.min(args.newIndex, this._groups.length - 1)].sortOrder ?? 0) + 1;
}
if (sortOrder !== args.item.sortOrder) {
await this._groupStructureHelper.partialUpdateContainer(args.item.id!, { sortOrder });
}
}
return true;
},
};
private _ownerTabId?: string | null;
// TODO: get rid of this:
@property({ type: String })
public get ownerTabId(): string | null | undefined {
return this._ownerTabId;
}
public set ownerTabId(value: string | null | undefined) {
if (value === this._ownerTabId) return;
const oldValue = this._ownerTabId;
this._ownerTabId = value;
this._groupStructureHelper.setOwnerId(value);
this.requestUpdate('ownerTabId', oldValue);
}
private _tabName?: string | undefined;
@property({ type: String })
public get tabName(): string | undefined {
return this._groupStructureHelper.getName();
}
public set tabName(value: string | undefined) {
if (value === this._tabName) return;
const oldValue = this._tabName;
this._tabName = value;
this._groupStructureHelper.setName(value);
this.requestUpdate('tabName', oldValue);
}
@state()
private _noTabName?: boolean;
@property({ type: Boolean })
public get noTabName(): boolean {
return this._groupStructureHelper.getIsRoot();
}
public set noTabName(value: boolean) {
this._noTabName = value;
this._groupStructureHelper.setIsRoot(value);
}
_groupStructureHelper = new UmbContentTypeContainerStructureHelper(this);
@state()
_groups: Array<PropertyTypeContainerModelBaseModel> = [];
@state()
_hasProperties = false;
@state()
_sortModeActive?: boolean;
constructor() {
super();
this.sorter = new UmbSorterController(this, this.config);
this.consumeContext(UMB_WORKSPACE_CONTEXT, (context) => {
this._groupStructureHelper.setStructureManager((context as UmbMediaTypeWorkspaceContext).structure);
this.observe(
(context as UmbMediaTypeWorkspaceContext).isSorting,
(isSorting) => {
this._sortModeActive = isSorting;
if (isSorting) {
this.sorter?.setModel(this._groups);
} else {
this.sorter?.setModel([]);
}
},
'_observeIsSorting',
);
});
this.observe(this._groupStructureHelper.containers, (groups) => {
this._groups = groups;
this.requestUpdate('_groups');
});
this.observe(this._groupStructureHelper.hasProperties, (hasProperties) => {
this._hasProperties = hasProperties;
this.requestUpdate('_hasProperties');
});
}
#onAddGroup = () => {
// Idea, maybe we can gather the sortOrder from the last group rendered and add 1 to it?
this._groupStructureHelper.addContainer(this._ownerTabId);
};
render() {
return html`
${!this._noTabName
? html`
<uui-box>
<umb-media-type-workspace-view-edit-properties
container-id=${ifDefined(this.ownerTabId === null ? undefined : this.ownerTabId)}
container-type="Tab"
container-name=${this.tabName || ''}></umb-media-type-workspace-view-edit-properties>
</uui-box>
`
: ''}
<div id="group-list">
${repeat(
this._groups,
(group) => group.id ?? '' + group.name,
(group) => html`<span data-umb-group-id=${ifDefined(group.id)}>
<uui-box>
${
this._groupStructureHelper.isOwnerChildContainer(group.id!)
? html`
<div slot="header">
<div>
${this._sortModeActive ? html`<uui-icon name="icon-navigation"></uui-icon>` : ''}
<uui-input
label="Group name"
placeholder="Enter a group name"
value=${group.name ?? ''}
@change=${(e: InputEvent) => {
const newName = (e.target as HTMLInputElement).value;
this._groupStructureHelper.updateContainerName(group.id!, group.parentId ?? null, newName);
}}>
</uui-input>
</div>
${this._sortModeActive
? html`<uui-input type="number" label="sort order" .value=${group.sortOrder ?? 0}></uui-input>`
: ''}
</div>
`
: html`<div slot="header">
<div><uui-icon name="icon-merge"></uui-icon><b>${group.name ?? ''}</b> (Inherited)</div>
${!this._sortModeActive
? html`<uui-input
readonly
type="number"
label="sort order"
.value=${group.sortOrder ?? 0}></uui-input>`
: ''}
</div>`
}
</div>
<umb-media-type-workspace-view-edit-properties
container-id=${ifDefined(group.id)}
container-type="Group"
container-name=${group.name || ''}></umb-media-type-workspace-view-edit-properties>
</uui-box></span>`,
)}
</div>
${!this._sortModeActive
? html`<uui-button
label=${this.localize.term('contentTypeEditor_addGroup')}
id="add"
look="placeholder"
@click=${this.#onAddGroup}>
${this.localize.term('contentTypeEditor_addGroup')}
</uui-button>`
: ''}
`;
}
static styles = [
UmbTextStyles,
css`
#add {
width: 100%;
}
#add:first-child {
margin-top: var(--uui-size-layout-1);
}
uui-box {
margin-bottom: var(--uui-size-layout-1);
}
[data-umb-group-id] {
display: block;
position: relative;
}
div[slot='header'] {
display: flex;
align-items: center;
justify-content: space-between;
}
div[slot='header'] > div {
display: flex;
align-items: center;
gap: var(--uui-size-3);
}
uui-input[type='number'] {
max-width: 75px;
}
.sorting {
cursor: grab;
}
.--umb-sorter-placeholder > uui-box {
visibility: hidden;
}
.--umb-sorter-placeholder::after {
content: '';
inset: 0;
position: absolute;
border-radius: var(--uui-border-radius);
border: 1px dashed var(--uui-color-divider-emphasis);
}
`,
];
}
export default UmbMediaTypeWorkspaceViewEditTabElement;
declare global {
interface HTMLElementTagNameMap {
'umb-media-type-workspace-view-edit-tab': UmbMediaTypeWorkspaceViewEditTabElement;
}
}

View File

@@ -0,0 +1,518 @@
import { UmbMediaTypeWorkspaceContext } from '../../media-type-workspace.context.js';
import type { UmbMediaTypeWorkspaceViewEditTabElement } from './media-type-workspace-view-edit-tab.element.js';
import { css, html, customElement, state, repeat, nothing, ifDefined } from '@umbraco-cms/backoffice/external/lit';
import { UUIInputElement, UUIInputEvent } from '@umbraco-cms/backoffice/external/uui';
import { UmbContentTypeContainerStructureHelper } from '@umbraco-cms/backoffice/content-type';
import { encodeFolderName, UmbRouterSlotChangeEvent, UmbRouterSlotInitEvent } from '@umbraco-cms/backoffice/router';
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
import {
MediaTypePropertyTypeContainerResponseModel,
PropertyTypeContainerModelBaseModel,
} from '@umbraco-cms/backoffice/backend-api';
import { UMB_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/workspace';
import type { UmbRoute } from '@umbraco-cms/backoffice/router';
import { UmbWorkspaceEditorViewExtensionElement } from '@umbraco-cms/backoffice/extension-registry';
import { UMB_CONFIRM_MODAL, UMB_MODAL_MANAGER_CONTEXT_TOKEN, UmbConfirmModalData } from '@umbraco-cms/backoffice/modal';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { UmbSorterConfig, UmbSorterController } from '@umbraco-cms/backoffice/sorter';
const SORTER_CONFIG: UmbSorterConfig<PropertyTypeContainerModelBaseModel> = {
compareElementToModel: (element: HTMLElement, model: MediaTypePropertyTypeContainerResponseModel) => {
return element.getAttribute('data-umb-tabs-id') === model.id;
},
querySelectModelToElement: (container: HTMLElement, modelEntry: PropertyTypeContainerModelBaseModel) => {
return container.querySelector(`[data-umb-tabs-id='` + modelEntry.id + `']`);
},
identifier: 'content-type-tabs-sorter',
itemSelector: '[data-umb-tabs-id]',
containerSelector: '#tabs-group',
disabledItemSelector: '[inherited]',
resolveVerticalDirection: () => {
return false;
},
};
@customElement('umb-media-type-workspace-view-edit')
export class UmbMediaTypeWorkspaceViewEditElement
extends UmbLitElement
implements UmbWorkspaceEditorViewExtensionElement
{
public sorter?: UmbSorterController<PropertyTypeContainerModelBaseModel>;
config: UmbSorterConfig<PropertyTypeContainerModelBaseModel> = {
...SORTER_CONFIG,
performItemInsert: async (args) => {
if (!this._tabs) return false;
const oldIndex = this._tabs.findIndex((tab) => tab.id! === args.item.id);
if (args.newIndex === oldIndex) return true;
let sortOrder = 0;
//TODO the sortOrder set is not correct
if (this._tabs.length > 0) {
if (args.newIndex === 0) {
sortOrder = (this._tabs[0].sortOrder ?? 0) - 1;
} else {
sortOrder = (this._tabs[Math.min(args.newIndex, this._tabs.length - 1)].sortOrder ?? 0) + 1;
}
if (sortOrder !== args.item.sortOrder) {
await this._tabsStructureHelper.partialUpdateContainer(args.item.id!, { sortOrder });
}
}
return true;
},
};
//private _hasRootProperties = false;
private _hasRootGroups = false;
@state()
private _routes: UmbRoute[] = [];
@state()
_tabs?: Array<PropertyTypeContainerModelBaseModel>;
@state()
private _routerPath?: string;
@state()
private _activePath = '';
@state()
private sortModeActive?: boolean;
@state()
private _buttonDisabled: boolean = false;
private _workspaceContext?: UmbMediaTypeWorkspaceContext;
private _tabsStructureHelper = new UmbContentTypeContainerStructureHelper(this);
private _modalManagerContext?: typeof UMB_MODAL_MANAGER_CONTEXT_TOKEN.TYPE;
constructor() {
super();
this.sorter = new UmbSorterController(this, this.config);
//TODO: We need to differentiate between local and composition tabs (and hybrids)
this._tabsStructureHelper.setIsRoot(true);
this._tabsStructureHelper.setContainerChildType('Tab');
this.observe(this._tabsStructureHelper.containers, (tabs) => {
this._tabs = tabs;
this._createRoutes();
});
// _hasRootProperties can be gotten via _tabsStructureHelper.hasProperties. But we do not support root properties currently.
this.consumeContext(UMB_WORKSPACE_CONTEXT, (workspaceContext) => {
this._workspaceContext = workspaceContext as UmbMediaTypeWorkspaceContext;
this._tabsStructureHelper.setStructureManager((workspaceContext as UmbMediaTypeWorkspaceContext).structure);
this.observe(
this._workspaceContext.isSorting,
(isSorting) => (this.sortModeActive = isSorting),
'_observeIsSorting',
);
this._observeRootGroups();
});
this.consumeContext(UMB_MODAL_MANAGER_CONTEXT_TOKEN, (context) => {
this._modalManagerContext = context;
});
}
private _observeRootGroups() {
if (!this._workspaceContext) return;
this.observe(
this._workspaceContext.structure.hasRootContainers('Group'),
(hasRootGroups) => {
this._hasRootGroups = hasRootGroups;
this._createRoutes();
},
'_observeGroups',
);
}
#changeMode() {
this._workspaceContext?.setIsSorting(!this.sortModeActive);
if (this.sortModeActive && this._tabs) {
this.sorter?.setModel(this._tabs);
} else {
this.sorter?.setModel([]);
}
}
private _createRoutes() {
if (!this._workspaceContext || !this._tabs) return;
const routes: UmbRoute[] = [];
if (this._tabs.length > 0) {
this._tabs?.forEach((tab) => {
const tabName = tab.name ?? '';
routes.push({
path: `tab/${encodeFolderName(tabName).toString()}`,
component: () => import('./media-type-workspace-view-edit-tab.element.js'),
setup: (component) => {
(component as UmbMediaTypeWorkspaceViewEditTabElement).tabName = tabName;
(component as UmbMediaTypeWorkspaceViewEditTabElement).ownerTabId =
this._workspaceContext?.structure.isOwnerContainer(tab.id!) ? tab.id : undefined;
},
});
});
}
routes.push({
path: 'root',
component: () => import('./media-type-workspace-view-edit-tab.element.js'),
setup: (component) => {
(component as UmbMediaTypeWorkspaceViewEditTabElement).noTabName = true;
(component as UmbMediaTypeWorkspaceViewEditTabElement).ownerTabId = null;
},
});
if (this._hasRootGroups) {
routes.push({
path: '',
redirectTo: 'root',
});
} else if (routes.length !== 0) {
routes.push({
path: '',
redirectTo: routes[0]?.path,
});
}
this._routes = routes;
}
#requestRemoveTab(tab: PropertyTypeContainerModelBaseModel | undefined) {
const Message: UmbConfirmModalData = {
headline: 'Delete tab',
content: html`<umb-localize key="contentTypeEditor_confirmDeleteTabMessage" .args=${[tab?.name ?? tab?.id]}>
Are you sure you want to delete the tab <strong>${tab?.name ?? tab?.id}</strong>
</umb-localize>
<div style="color:var(--uui-color-danger-emphasis)">
<umb-localize key="contentTypeEditor_confirmDeleteTabNotice">
This will delete all items that doesn't belong to a composition.
</umb-localize>
</div>`,
confirmLabel: this.localize.term('actions_delete'),
color: 'danger',
};
// TODO: If this tab is composed of other tabs, then notify that it will only delete the local tab.
const modalHandler = this._modalManagerContext?.open(UMB_CONFIRM_MODAL, Message);
modalHandler?.onSubmit().then(() => {
this.#remove(tab?.id);
});
}
#remove(tabId?: string) {
if (!tabId) return;
this._workspaceContext?.structure.removeContainer(null, tabId);
this._tabsStructureHelper?.isOwnerContainer(tabId)
? window.history.replaceState(null, '', this._routerPath + this._routes[0]?.path ?? '/root')
: '';
}
async #addTab() {
if (
(this.shadowRoot?.querySelector('uui-tab[active] uui-input') as UUIInputElement) &&
(this.shadowRoot?.querySelector('uui-tab[active] uui-input') as UUIInputElement).value === ''
) {
this.#focusInput();
return;
}
const tab = await this._workspaceContext?.structure.createContainer(null, null, 'Tab');
if (tab) {
const path = this._routerPath + '/tab/' + encodeFolderName(tab.name || '');
window.history.replaceState(null, '', path);
this.#focusInput();
}
}
async #focusInput() {
setTimeout(() => {
(this.shadowRoot?.querySelector('uui-tab[active] uui-input') as UUIInputElement | undefined)?.focus();
}, 100);
}
async #tabNameChanged(event: InputEvent, tab: PropertyTypeContainerModelBaseModel) {
if (this._buttonDisabled) this._buttonDisabled = !this._buttonDisabled;
let newName = (event.target as HTMLInputElement).value;
if (newName === '') {
newName = 'Unnamed';
(event.target as HTMLInputElement).value = 'Unnamed';
}
const changedName = this._workspaceContext?.structure.makeContainerNameUniqueForOwnerContentType(
newName,
'Tab',
tab.id,
);
// Check if it collides with another tab name of this same media-type, if so adjust name:
if (changedName) {
newName = changedName;
(event.target as HTMLInputElement).value = newName;
}
this._tabsStructureHelper.partialUpdateContainer(tab.id!, {
name: newName,
});
// Update the current URL, so we are still on this specific tab:
window.history.replaceState(null, '', this._routerPath + '/tab/' + encodeFolderName(newName));
}
render() {
return html`
<umb-body-layout header-fit-height>
<div id="header" slot="header">
<div id="tabs-wrapper" class="flex">
${this._routerPath ? this.renderTabsNavigation() : ''} ${this.renderAddButton()}
</div>
${this.renderActions()}
</div>
<umb-router-slot
.routes=${this._routes}
@init=${(event: UmbRouterSlotInitEvent) => {
this._routerPath = event.target.absoluteRouterPath;
}}
@change=${(event: UmbRouterSlotChangeEvent) => {
this._activePath = event.target.absoluteActiveViewPath || '';
}}>
</umb-router-slot>
</umb-body-layout>
`;
}
renderAddButton() {
if (this.sortModeActive) return;
return html`<uui-button id="add-tab" @click="${this.#addTab}" label="Add tab" compact>
<uui-icon name="icon-add"></uui-icon>
Add tab
</uui-button>`;
}
renderActions() {
const sortButtonText = this.sortModeActive
? this.localize.term('general_reorderDone')
: this.localize.term('general_reorder');
return html`<div class="tab-actions">
<uui-button look="outline" label=${this.localize.term('contentTypeEditor_compositions')} compact>
<uui-icon name="icon-merge"></uui-icon>
${this.localize.term('contentTypeEditor_compositions')}
</uui-button>
<uui-button look="outline" label=${sortButtonText} compact @click=${this.#changeMode}>
<uui-icon name="icon-navigation"></uui-icon>
${sortButtonText}
</uui-button>
</div>`;
}
renderTabsNavigation() {
if (!this._tabs) return;
return html`<div id="tabs-group" class="flex">
<uui-tab-group>
${this.renderRootTab()}
${repeat(
this._tabs,
(tab) => tab.id! + tab.name,
(tab) => this.renderTab(tab),
)}
</uui-tab-group>
</div>`;
}
renderRootTab() {
const rootTabPath = this._routerPath + '/root';
const rootTabActive = rootTabPath === this._activePath;
return html`<uui-tab
class=${this._hasRootGroups || rootTabActive ? '' : 'content-tab-is-empty'}
label=${this.localize.term('general_content')}
.active=${rootTabActive}
href=${rootTabPath}>
${this.localize.term('general_content')}
</uui-tab>`;
}
renderTab(tab: PropertyTypeContainerModelBaseModel) {
const path = this._routerPath + '/tab/' + encodeFolderName(tab.name || '');
const tabActive = path === this._activePath;
const tabInherited = !this._tabsStructureHelper.isOwnerContainer(tab.id!);
return html`<uui-tab
label=${tab.name ?? 'unnamed'}
.active=${tabActive}
href=${path}
data-umb-tabs-id=${ifDefined(tab.id)}>
${this.renderTabInner(tab, tabActive, tabInherited)}
</uui-tab>`;
}
renderTabInner(tab: PropertyTypeContainerModelBaseModel, tabActive: boolean, tabInherited: boolean) {
if (this.sortModeActive) {
return html`<div class="no-edit">
${tabInherited
? html`<uui-icon class="external" name="icon-merge"></uui-icon>${tab.name!}`
: html`<uui-icon name="icon-navigation" class="drag-${tab.id}"> </uui-icon>${tab.name!}
<uui-input
label="sort order"
type="number"
value=${ifDefined(tab.sortOrder)}
style="width:50px"
@keypress=${(e: UUIInputEvent) => this.#changeOrderNumber(tab, e)}></uui-input>`}
</div>`;
}
if (tabActive && !tabInherited) {
return html`<div class="tab">
<uui-input
id="input"
look="placeholder"
placeholder="Unnamed"
label=${tab.name!}
value="${tab.name!}"
auto-width
@change=${(e: InputEvent) => this.#tabNameChanged(e, tab)}
@blur=${(e: InputEvent) => this.#tabNameChanged(e, tab)}
@input=${() => (this._buttonDisabled = true)}
@focus=${(e: UUIInputEvent) => (e.target.value ? nothing : (this._buttonDisabled = true))}>
${this.renderDeleteFor(tab)}
</uui-input>
</div>`;
}
if (tabInherited) {
return html`<div class="no-edit"><uui-icon name="icon-merge"></uui-icon>${tab.name!}</div>`;
} else {
return html`<div class="no-edit">${tab.name!} ${this.renderDeleteFor(tab)}</div>`;
}
}
#changeOrderNumber(tab: PropertyTypeContainerModelBaseModel, e: UUIInputEvent) {
if (!e.target.value || !tab.id) return;
const sortOrder = Number(e.target.value);
this._tabsStructureHelper.partialUpdateContainer(tab.id, { sortOrder });
}
renderDeleteFor(tab: PropertyTypeContainerModelBaseModel) {
return html`<uui-button
label=${this.localize.term('actions_remove')}
class="trash"
slot="append"
?disabled=${this._buttonDisabled}
@click=${() => this.#requestRemoveTab(tab)}
compact>
<uui-icon name="icon-trash"></uui-icon>
</uui-button>`;
}
static styles = [
UmbTextStyles,
css`
#buttons-wrapper {
flex: 1;
display: flex;
align-items: center;
justify-content: space-between;
align-items: stretch;
}
:host {
position: relative;
display: flex;
flex-direction: column;
height: 100%;
--uui-tab-background: var(--uui-color-surface);
}
/* TODO: This should be replaced with a general workspace bar — naming is hard */
#header {
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: nowrap;
}
.flex {
display: flex;
}
uui-tab-group {
flex-wrap: nowrap;
}
.content-tab-is-empty {
align-self: center;
border-radius: 3px;
--uui-tab-text: var(--uui-color-text-alt);
border: dashed 1px var(--uui-color-border-emphasis);
}
uui-tab {
position: relative;
border-left: 1px hidden transparent;
border-right: 1px solid var(--uui-color-border);
}
.no-edit uui-input {
pointer-events: auto;
}
.no-edit {
pointer-events: none;
display: inline-flex;
padding-left: var(--uui-size-space-3);
border: 1px solid transparent;
align-items: center;
gap: var(--uui-size-space-3);
}
.trash {
opacity: 1;
transition: opacity 120ms;
}
uui-tab:not(:hover, :focus) .trash {
opacity: 0;
transition: opacity 120ms;
}
uui-input:not(:focus, :hover) {
border: 1px solid transparent;
}
.inherited {
vertical-align: sub;
}
.--umb-sorter-placeholder > * {
visibility: hidden;
}
.--umb-sorter-placeholder::after {
content: '';
position: absolute;
inset: 2px;
border: 1px dashed var(--uui-color-divider-emphasis);
}
`,
];
}
export default UmbMediaTypeWorkspaceViewEditElement;
declare global {
interface HTMLElementTagNameMap {
'umb-media-type-workspace-view-edit': UmbMediaTypeWorkspaceViewEditElement;
}
}

View File

@@ -1,68 +0,0 @@
import { UMB_MEDIA_TYPE_WORKSPACE_CONTEXT } from '../../media-type-workspace.context.js';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { css, html, nothing, customElement, state } from '@umbraco-cms/backoffice/external/lit';
import {
UmbModalManagerContext,
UMB_MODAL_MANAGER_CONTEXT_TOKEN,
UMB_PROPERTY_EDITOR_UI_PICKER_MODAL,
} from '@umbraco-cms/backoffice/modal';
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
import type { MediaTypeResponseModel } from '@umbraco-cms/backoffice/backend-api';
import { UmbWorkspaceEditorViewExtensionElement } from '@umbraco-cms/backoffice/extension-registry';
@customElement('umb-media-type-design-workspace-view')
export class UmbMediaTypeDesignWorkspaceViewEditElement
extends UmbLitElement
implements UmbWorkspaceEditorViewExtensionElement
{
@state()
_mediaType?: MediaTypeResponseModel;
private _workspaceContext?: typeof UMB_MEDIA_TYPE_WORKSPACE_CONTEXT.TYPE;
constructor() {
super();
this.consumeContext(UMB_MEDIA_TYPE_WORKSPACE_CONTEXT, (_instance) => {
this._workspaceContext = _instance;
this._observeMediaType();
});
}
private _observeMediaType() {
if (!this._workspaceContext) {
return;
}
this.observe(this._workspaceContext.data, (mediaType) => {
this._mediaType = mediaType;
});
}
render() {
return html`<uui-box> ${this._mediaType?.alias}</uui-box>`;
}
static styles = [
UmbTextStyles,
css`
:host {
display: block;
margin: var(--uui-size-layout-1);
padding-bottom: var(--uui-size-layout-1);
}
uui-box {
margin-top: var(--uui-size-layout-1);
}
`,
];
}
export default UmbMediaTypeDesignWorkspaceViewEditElement;
declare global {
interface HTMLElementTagNameMap {
'umb-media-type-design-workspace-view': UmbMediaTypeDesignWorkspaceViewEditElement;
}
}

View File

@@ -1,68 +0,0 @@
import { UMB_MEDIA_TYPE_WORKSPACE_CONTEXT } from '../../media-type-workspace.context.js';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { css, html, nothing, customElement, state } from '@umbraco-cms/backoffice/external/lit';
import {
UmbModalManagerContext,
UMB_MODAL_MANAGER_CONTEXT_TOKEN,
UMB_PROPERTY_EDITOR_UI_PICKER_MODAL,
} from '@umbraco-cms/backoffice/modal';
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
import type { MediaTypeResponseModel } from '@umbraco-cms/backoffice/backend-api';
import { UmbWorkspaceEditorViewExtensionElement } from '@umbraco-cms/backoffice/extension-registry';
@customElement('umb-media-type-list-view-workspace-view')
export class UmbMediaTypeListViewWorkspaceViewEditElement
extends UmbLitElement
implements UmbWorkspaceEditorViewExtensionElement
{
@state()
_mediaType?: MediaTypeResponseModel;
private _workspaceContext?: typeof UMB_MEDIA_TYPE_WORKSPACE_CONTEXT.TYPE;
constructor() {
super();
this.consumeContext(UMB_MEDIA_TYPE_WORKSPACE_CONTEXT, (_instance) => {
this._workspaceContext = _instance;
this._observeMediaType();
});
}
private _observeMediaType() {
if (!this._workspaceContext) {
return;
}
this.observe(this._workspaceContext.data, (mediaType) => {
this._mediaType = mediaType;
});
}
render() {
return html`<uui-box> List View view for ${this._mediaType?.alias}</uui-box>`;
}
static styles = [
UmbTextStyles,
css`
:host {
display: block;
margin: var(--uui-size-layout-1);
padding-bottom: var(--uui-size-layout-1);
}
uui-box {
margin-top: var(--uui-size-layout-1);
}
`,
];
}
export default UmbMediaTypeListViewWorkspaceViewEditElement;
declare global {
interface HTMLElementTagNameMap {
'umb-media-type-list-view-workspace-view': UmbMediaTypeListViewWorkspaceViewEditElement;
}
}

View File

@@ -1,68 +0,0 @@
import { UMB_MEDIA_TYPE_WORKSPACE_CONTEXT } from '../../media-type-workspace.context.js';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { css, html, nothing, customElement, state } from '@umbraco-cms/backoffice/external/lit';
import {
UmbModalManagerContext,
UMB_MODAL_MANAGER_CONTEXT_TOKEN,
UMB_PROPERTY_EDITOR_UI_PICKER_MODAL,
} from '@umbraco-cms/backoffice/modal';
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
import type { MediaTypeResponseModel } from '@umbraco-cms/backoffice/backend-api';
import { UmbWorkspaceEditorViewExtensionElement } from '@umbraco-cms/backoffice/extension-registry';
@customElement('umb-media-type-permissions-workspace-view')
export class UmbMediaTypePermissionsWorkspaceViewEditElement
extends UmbLitElement
implements UmbWorkspaceEditorViewExtensionElement
{
@state()
_mediaType?: MediaTypeResponseModel;
private _workspaceContext?: typeof UMB_MEDIA_TYPE_WORKSPACE_CONTEXT.TYPE;
constructor() {
super();
this.consumeContext(UMB_MEDIA_TYPE_WORKSPACE_CONTEXT, (_instance) => {
this._workspaceContext = _instance;
this._observeMediaType();
});
}
private _observeMediaType() {
if (!this._workspaceContext) {
return;
}
this.observe(this._workspaceContext.data, (mediaType) => {
this._mediaType = mediaType;
});
}
render() {
return html`<uui-box>Permissions View for ${this._mediaType?.alias}</uui-box>`;
}
static styles = [
UmbTextStyles,
css`
:host {
display: block;
margin: var(--uui-size-layout-1);
padding-bottom: var(--uui-size-layout-1);
}
uui-box {
margin-top: var(--uui-size-layout-1);
}
`,
];
}
export default UmbMediaTypePermissionsWorkspaceViewEditElement;
declare global {
interface HTMLElementTagNameMap {
'umb-media-type-permissions-workspace-view': UmbMediaTypePermissionsWorkspaceViewEditElement;
}
}

View File

@@ -0,0 +1,117 @@
import { UmbMediaTypeWorkspaceContext } from '../../media-type-workspace.context.js';
import type { UmbMediaTypeInputElement } from '../../../components/media-type-input/media-type-input.element.js';
import { css, html, customElement, state } from '@umbraco-cms/backoffice/external/lit';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import type { UUIToggleElement } from '@umbraco-cms/backoffice/external/uui';
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
import { UMB_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/workspace';
import { UmbWorkspaceEditorViewExtensionElement } from '@umbraco-cms/backoffice/extension-registry';
@customElement('umb-media-type-workspace-view-structure')
export class UmbMediaTypeWorkspaceViewStructureElement
extends UmbLitElement
implements UmbWorkspaceEditorViewExtensionElement
{
#workspaceContext?: UmbMediaTypeWorkspaceContext;
@state()
private _allowedAsRoot?: boolean;
@state()
private _allowedContentTypeIDs?: Array<string>;
constructor() {
super();
// TODO: Figure out if this is the best way to consume the context or if it can be strongly typed with an UmbContextToken
this.consumeContext(UMB_WORKSPACE_CONTEXT, (mediaTypeContext) => {
this.#workspaceContext = mediaTypeContext as UmbMediaTypeWorkspaceContext;
this._observeMediaType();
});
}
private _observeMediaType() {
if (!this.#workspaceContext) return;
this.observe(this.#workspaceContext.allowedAsRoot, (allowedAsRoot) => (this._allowedAsRoot = allowedAsRoot));
this.observe(this.#workspaceContext.allowedContentTypes, (allowedContentTypes) => {
const oldValue = this._allowedContentTypeIDs;
this._allowedContentTypeIDs = allowedContentTypes
?.map((x) => x.id)
.filter((x) => x !== undefined) as Array<string>;
this.requestUpdate('_allowedContentTypeIDs', oldValue);
});
}
render() {
return html`
<uui-box headline="Structure">
<umb-workspace-property-layout alias="Root" label="Allow as Root">
<div slot="description">${this.localize.term('contentTypeEditor_allowAsRootDescription')}</div>
<div slot="editor">
<uui-toggle
label=${this.localize.term('contentTypeEditor_allowAsRootHeading')}
?checked=${this._allowedAsRoot}
@change=${(e: CustomEvent) => {
this.#workspaceContext?.updateProperty('allowedAsRoot', (e.target as UUIToggleElement).checked);
}}></uui-toggle>
</div>
</umb-workspace-property-layout>
<umb-workspace-property-layout alias="ChildNodeType" label="Allowed child node types">
<div slot="description">
Allow content of the specified types to be created underneath content of this type.
</div>
<div slot="editor">
<!-- TODO: maybe we want to somehow display the hierarchy, but not necessary in the same way as old backoffice? -->
<umb-media-type-input
.selectedIds=${this._allowedContentTypeIDs ?? []}
@change="${(e: CustomEvent) => {
const sortedContentTypesList = (e.target as UmbMediaTypeInputElement).selectedIds.map((id, index) => ({
id: id,
sortOrder: index,
}));
this.#workspaceContext?.updateProperty('allowedContentTypes', sortedContentTypesList);
}}">
</umb-media-type-input>
</div>
</umb-workspace-property-layout>
</uui-box>
<uui-box headline="Presentation">
<umb-workspace-property-layout alias="Root" label="Collection view">
<div slot="description">Provides an overview of child content and hides it in the tree.</div>
<div slot="editor"><uui-toggle label="Display children in a Collection view"></uui-toggle></div>
</umb-workspace-property-layout>
</uui-box>
`;
}
static styles = [
UmbTextStyles,
css`
:host {
display: block;
margin: var(--uui-size-layout-1);
padding-bottom: var(--uui-size-layout-1); // To enforce some distance to the bottom of the scroll-container.
}
uui-box {
margin-top: var(--uui-size-layout-1);
}
uui-label,
umb-property-editor-ui-number {
display: block;
}
// TODO: is this necessary?
uui-toggle {
display: flex;
}
`,
];
}
export default UmbMediaTypeWorkspaceViewStructureElement;
declare global {
interface HTMLElementTagNameMap {
'umb-media-type-workspace-view-structure': UmbMediaTypeWorkspaceViewStructureElement;
}
}

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

@@ -1,13 +1,13 @@
import { UmbStylesheetRepository } from '../repository/stylesheet.repository.js';
import { StylesheetDetails } from '../index.js';
import type { StylesheetDetails } from '../index.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 { 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 };
@@ -18,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));
@@ -189,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

@@ -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}`;
}