Merge remote-tracking branch 'origin/main' into feature/swap-to-popover-container
This commit is contained in:
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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',
|
||||
);
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
);
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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',
|
||||
);
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
|
||||
@@ -176,5 +176,6 @@ export const UMB_DOCUMENT_TYPE_WORKSPACE_CONTEXT = new UmbContextToken<
|
||||
UmbDocumentTypeWorkspaceContext
|
||||
>(
|
||||
'UmbWorkspaceContext',
|
||||
undefined,
|
||||
(context): context is UmbDocumentTypeWorkspaceContext => context.getEntityType?.() === 'document-type',
|
||||
);
|
||||
|
||||
@@ -8,5 +8,6 @@ export const IsDocumentVariantContext = (context: UmbVariantContext): context is
|
||||
|
||||
export const UMB_DOCUMENT_VARIANT_CONTEXT = new UmbContextToken<UmbVariantContext, UmbDocumentVariantContext>(
|
||||
'UmbVariantContext',
|
||||
undefined,
|
||||
IsDocumentVariantContext,
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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')!,
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
import './media-type-input/media-type-input.element.js';
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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];
|
||||
@@ -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',
|
||||
},
|
||||
);
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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];
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
@@ -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';
|
||||
@@ -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,
|
||||
];
|
||||
|
||||
@@ -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'],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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';
|
||||
@@ -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];
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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()),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
);
|
||||
@@ -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 };
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './item/index.js';
|
||||
export * from './detail/index.js';
|
||||
@@ -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';
|
||||
@@ -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];
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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');
|
||||
@@ -0,0 +1,3 @@
|
||||
import { MediaTypeItemResponseModel } from '@umbraco-cms/backoffice/backend-api';
|
||||
|
||||
export type UmbMediaTypeItemModel = MediaTypeItemResponseModel;
|
||||
@@ -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];
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
@@ -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];
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
}
|
||||
@@ -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(
|
||||
@@ -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');
|
||||
@@ -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;
|
||||
@@ -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];
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
},
|
||||
|
||||
@@ -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',
|
||||
);
|
||||
|
||||
@@ -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>`;
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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',
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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',
|
||||
);
|
||||
|
||||
@@ -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',
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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',
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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',
|
||||
);
|
||||
|
||||
@@ -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')!,
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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}
|
||||
{
|
||||
|
||||
|
||||
|
||||
|
||||
}`;
|
||||
|
||||
|
||||
@@ -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',
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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]>;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export const decodeFilePath = (unique: string) => decodeURIComponent(unique.replace('-', '.'));
|
||||
@@ -0,0 +1 @@
|
||||
export const encodeFilePath = (path: string) => encodeURIComponent(path).replace('.', '-');
|
||||
@@ -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}`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user