Merge branch 'main' into feature/dynamic-root

This commit is contained in:
leekelleher
2024-01-29 09:33:28 +00:00
160 changed files with 1755 additions and 776 deletions

View File

@@ -1,8 +1,8 @@
import type { UmbAppErrorElement } from './app-error.element.js';
import { UmbAppContext } from './app.context.js';
import { UmbServerConnection } from './server-connection.js';
import type { UMB_AUTH_CONTEXT} from '@umbraco-cms/backoffice/auth';
import { UmbAuthContext } from '@umbraco-cms/backoffice/auth';
import type { UMB_AUTH_CONTEXT } from '@umbraco-cms/backoffice/auth';
import { UMB_STORAGE_REDIRECT_URL, UmbAuthContext } from '@umbraco-cms/backoffice/auth';
import { css, html, customElement, property } from '@umbraco-cms/backoffice/external/lit';
import { UUIIconRegistryEssential } from '@umbraco-cms/backoffice/external/uui';
import { UmbIconRegistry } from '@umbraco-cms/backoffice/icon';
@@ -29,13 +29,12 @@ export class UmbAppElement extends UmbLitElement {
* @attr
*/
@property({ type: String })
// TODO: get from server config
// TODO: get from base element or maybe move to UmbAuthContext.#getRedirectUrl since it is only used there
backofficePath = '/umbraco';
/**
* Bypass authentication.
*/
// TODO: this might not be the right solution
@property({ type: Boolean })
bypassAuth = false;
@@ -140,6 +139,15 @@ export class UmbAppElement extends UmbLitElement {
}
#redirect() {
// If there is a ?code parameter in the url, then we are in the middle of the oauth flow
// and we need to complete the login (the authorization notifier will redirect after this is done
// essentially hitting this method again)
const queryParams = new URLSearchParams(window.location.search);
if (queryParams.has('code')) {
this.#authContext?.completeAuthorizationRequest();
return;
}
switch (this.#serverConnection?.getStatus()) {
case RuntimeLevelModel.INSTALL:
history.replaceState(null, '', 'install');
@@ -156,17 +164,15 @@ export class UmbAppElement extends UmbLitElement {
case RuntimeLevelModel.RUN: {
const pathname = pathWithoutBasePath({ start: true, end: false });
// If we are on the installer or upgrade page, redirect to the root
// but if not, keep the current path but replace state anyway to initialize the router
let currentRoute = location.href;
const savedRoute = sessionStorage.getItem('umb:auth:redirect');
if (savedRoute) {
sessionStorage.removeItem('umb:auth:redirect');
currentRoute = savedRoute;
// If we are on installer or upgrade page, redirect to the root since we are in the RUN state
if (pathname === '/install' || pathname === '/upgrade') {
history.replaceState(null, '', '/');
break;
}
const finalPath = pathname === '/install' || pathname === '/upgrade' ? '/' : currentRoute;
history.replaceState(null, '', finalPath);
// Keep the current path but replace state anyway to initialize the router
// because the router will not initialize a wildcard route by itself
history.replaceState(null, '', location.href);
break;
}
@@ -187,11 +193,10 @@ export class UmbAppElement extends UmbLitElement {
}
// Save location.href so we can redirect to it after login
window.sessionStorage.setItem('umb:auth:redirect', location.href);
window.sessionStorage.setItem(UMB_STORAGE_REDIRECT_URL, location.href);
// Make a request to the auth server to start the auth flow
// TODO: find better name for this method
this.#authContext.login();
this.#authContext.makeAuthorizationRequest();
// Return false to prevent the route from being rendered
return false;

View File

@@ -1,4 +1,4 @@
import { UmbLocalizeController } from '@umbraco-cms/backoffice/localization-api';
import { UmbLocalizationController } from '@umbraco-cms/backoffice/localization-api';
import type { Observable } from '@umbraco-cms/backoffice/external/rxjs';
import type { HTMLElementConstructor } from '@umbraco-cms/backoffice/extension-api';
import { UmbControllerHostElementMixin, type UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api';
@@ -34,14 +34,14 @@ export declare class UmbElement extends UmbControllerHostElement {
): UmbContextConsumerController<BaseType, ResultType>;
/**
* Use the UmbLocalizeController to localize your element.
* @see UmbLocalizeController
* @see UmbLocalizationController
*/
localize: UmbLocalizeController;
localize: UmbLocalizationController;
}
export const UmbElementMixin = <T extends HTMLElementConstructor>(superClass: T) => {
class UmbElementMixinClass extends UmbControllerHostElementMixin(superClass) implements UmbElement {
localize: UmbLocalizeController = new UmbLocalizeController(this);
localize: UmbLocalizationController = new UmbLocalizationController(this);
/**
* @description Observe a RxJS source of choice.

View File

@@ -73,7 +73,7 @@ export abstract class UmbBaseExtensionInitializer<
}
protected _init() {
this.#manifestObserver = this.observe(
this.#extensionRegistry.getByAlias<ManifestType>(this.#alias),
this.#extensionRegistry.byAlias<ManifestType>(this.#alias),
async (extensionManifest) => {
this.#clearPermittedState();
this.#manifest = extensionManifest;
@@ -143,7 +143,7 @@ export abstract class UmbBaseExtensionInitializer<
if (conditionConfigs.length > 0) {
// Observes the conditions and initialize as they come in.
this.observe(
this.#extensionRegistry.getByTypeAndAliases('condition', conditionAliases),
this.#extensionRegistry.byTypeAndAliases('condition', conditionAliases),
this.#gotConditions,
'_observeConditions',
);

View File

@@ -56,8 +56,8 @@ export abstract class UmbBaseExtensionsInitializer<
}
protected _init() {
let source = Array.isArray(this.#type)
? this.#extensionRegistry.extensionsOfTypes<ManifestType>(this.#type as string[])
: this.#extensionRegistry.extensionsOfType<ManifestTypeName, ManifestType>(this.#type as ManifestTypeName);
? this.#extensionRegistry.byTypes<ManifestType>(this.#type as string[])
: this.#extensionRegistry.byType<ManifestTypeName, ManifestType>(this.#type as ManifestTypeName);
if (this.#filter) {
source = source.pipe(map((extensions: Array<ManifestType>) => extensions.filter(this.#filter!)));
}

View File

@@ -11,7 +11,7 @@ export class UmbBundleExtensionInitializer extends UmbBaseController {
constructor(host: UmbControllerHostElement, extensionRegistry: UmbExtensionRegistry<ManifestBundle>) {
super(host);
this.#extensionRegistry = extensionRegistry;
this.observe(extensionRegistry.extensionsOfType('bundle'), (bundles) => {
this.observe(extensionRegistry.byType('bundle'), (bundles) => {
// Unregister removed bundles:
this.#bundleMap.forEach((existingBundle) => {
if (!bundles.find((b) => b.alias === existingBundle.alias)) {

View File

@@ -13,7 +13,7 @@ export class UmbEntryPointExtensionInitializer extends UmbBaseController {
super(host);
this.#host = host;
this.#extensionRegistry = extensionRegistry;
this.observe(extensionRegistry.extensionsOfType('entryPoint'), (entryPoints) => {
this.observe(extensionRegistry.byType('entryPoint'), (entryPoints) => {
entryPoints.forEach((entryPoint) => {
if (this.#entryPointMap.has(entryPoint.alias)) return;
this.#entryPointMap.set(entryPoint.alias, entryPoint);

View File

@@ -47,6 +47,7 @@ describe('UmbExtensionRegistry', () => {
type: 'workspace',
name: 'test-editor-1',
alias: 'Umb.Test.Editor.1',
weight: 2,
meta: {
entityType: 'testEntity',
},
@@ -66,10 +67,20 @@ describe('UmbExtensionRegistry', () => {
expect(extensionRegistry.isRegistered('Umb.Test.Section.1')).to.be.true;
});
it('should get several extensions by type', (done) => {
extensionRegistry
.byType('section')
.subscribe((extensions) => {
expect(extensions.length).to.eq(3);
done();
})
.unsubscribe();
});
it('should get an extension by alias', (done) => {
const alias = 'Umb.Test.Section.1';
extensionRegistry
.getByTypeAndAlias('section', alias)
.byAlias(alias)
.subscribe((extension) => {
expect(extension?.alias).to.eq(alias);
done();
@@ -77,10 +88,32 @@ describe('UmbExtensionRegistry', () => {
.unsubscribe();
});
it('should get an extension by type and alias', (done) => {
const alias = 'Umb.Test.Section.1';
extensionRegistry
.byTypeAndAlias('section', alias)
.subscribe((extension) => {
expect(extension?.alias).to.eq(alias);
done();
})
.unsubscribe();
});
it('should get an extension by type and filter', (done) => {
extensionRegistry
.byTypeAndFilter('section', (ext) => ext.weight === 25)
.subscribe((extensions) => {
expect(extensions.length).to.eq(1);
expect(extensions[0].alias).to.eq('Umb.Test.Section.3');
done();
})
.unsubscribe();
});
it('should get an extension by aliases', (done) => {
const aliases = ['Umb.Test.Section.1', 'Umb.Test.Section.2'];
extensionRegistry
.getByTypeAndAliases('section', aliases)
.byTypeAndAliases('section', aliases)
.subscribe((extensions) => {
expect(extensions[0]?.alias).to.eq(aliases[1]);
expect(extensions[1]?.alias).to.eq(aliases[0]);
@@ -89,12 +122,12 @@ describe('UmbExtensionRegistry', () => {
.unsubscribe();
});
describe('getByType', () => {
describe('byType', () => {
const type = 'section';
it('should get all extensions by type', (done) => {
extensionRegistry
.extensionsOfType(type)
.byType(type)
.subscribe((extensions) => {
expect(extensions).to.have.lengthOf(3);
expect(extensions?.[0]?.type).to.eq(type);
@@ -106,7 +139,7 @@ describe('UmbExtensionRegistry', () => {
it('should return extensions ordered by weight', (done) => {
extensionRegistry
.extensionsOfType(type)
.byType(type)
.subscribe((extensions) => {
expect(extensions?.[0]?.weight).to.eq(200);
expect(extensions?.[1]?.weight).to.eq(25);
@@ -121,7 +154,7 @@ describe('UmbExtensionRegistry', () => {
let lastAmount = 0;
extensionRegistry
.extensionsOfType('section')
.byType('section')
.subscribe((extensions) => {
amountOfTimesTriggered++;
const newAmount = extensions?.length ?? 0;
@@ -169,6 +202,37 @@ describe('UmbExtensionRegistry', () => {
.unsubscribe();
});
});
describe('byTypes', () => {
const types = ['section', 'workspace'];
it('should get all extensions of the given types', (done) => {
extensionRegistry
.byTypes(types)
.subscribe((extensions) => {
expect(extensions).to.have.lengthOf(4);
expect(extensions?.[0]?.type).to.eq('section');
expect(extensions?.[1]?.type).to.eq('section');
expect(extensions?.[2]?.type).to.eq('workspace');
expect(extensions?.[3]?.type).to.eq('section');
done();
})
.unsubscribe();
});
it('should return extensions ordered by weight', (done) => {
extensionRegistry
.byTypes(types)
.subscribe((extensions) => {
expect(extensions?.[0]?.weight).to.eq(200);
expect(extensions?.[1]?.weight).to.eq(25);
expect(extensions?.[2]?.weight).to.eq(2);
expect(extensions?.[3]?.weight).to.eq(1);
done();
})
.unsubscribe();
});
});
});
describe('UmbExtensionRegistry with kinds', () => {
@@ -244,7 +308,7 @@ describe('UmbExtensionRegistry with kinds', () => {
it('should merge with kinds', (done) => {
extensionRegistry
.extensionsOfType('section')
.byType('section')
.subscribe((extensions) => {
expect(extensions).to.have.lengthOf(3);
expect(extensions?.[0]?.elementName).to.not.eq('my-kind-element');
@@ -265,7 +329,7 @@ describe('UmbExtensionRegistry with kinds', () => {
extensionRegistry.unregister('Umb.Test.Kind');
extensionRegistry
.extensionsOfType('section')
.byType('section')
.subscribe((extensions) => {
amountOfTimesTriggered++;
expect(extensions).to.have.lengthOf(3);
@@ -287,7 +351,7 @@ describe('UmbExtensionRegistry with kinds', () => {
let amountOfTimesTriggered = -1;
extensionRegistry
.extensionsOfType('section')
.byType('section')
.subscribe((extensions) => {
amountOfTimesTriggered++;
expect(extensions).to.have.lengthOf(3);

View File

@@ -1,15 +1,8 @@
import type { ManifestBase, ManifestKind } from '../types/index.js';
import type { ManifestTypeMap, SpecificManifestTypeOrManifestBase } from '../types/map.types.js';
import { UmbBasicState } from '@umbraco-cms/backoffice/observable-api';
import type {
Observable} from '@umbraco-cms/backoffice/external/rxjs';
import {
map,
distinctUntilChanged,
combineLatest,
of,
switchMap,
} from '@umbraco-cms/backoffice/external/rxjs';
import type { Observable } from '@umbraco-cms/backoffice/external/rxjs';
import { map, distinctUntilChanged, combineLatest, of, switchMap } from '@umbraco-cms/backoffice/external/rxjs';
function extensionArrayMemoization<T extends Pick<ManifestBase, 'alias'>>(
previousValue: Array<T>,
@@ -113,7 +106,7 @@ export class UmbExtensionRegistry<
}
register(manifest: ManifestTypes | ManifestKind<ManifestTypes>): void {
const isValid = this.checkExtension(manifest);
const isValid = this.#checkExtension(manifest);
if (!isValid) {
return;
}
@@ -150,14 +143,7 @@ export class UmbExtensionRegistry<
return false;
}
/*
getByAlias(alias: string) {
// TODO: make pipes prettier/simpler/reuseable
return this.extensions.pipe(map((extensions) => extensions.find((extension) => extension.alias === alias) || null));
}
*/
private checkExtension(manifest: ManifestTypes | ManifestKind<ManifestTypes>): boolean {
#checkExtension(manifest: ManifestTypes | ManifestKind<ManifestTypes>): boolean {
if (!manifest.type) {
console.error(`Extension is missing type`, manifest);
return false;
@@ -184,19 +170,23 @@ export class UmbExtensionRegistry<
return true;
}
private _kindsOfType<Key extends keyof ManifestTypeMap<ManifestTypes> | string>(type: Key) {
#kindsOfType<Key extends keyof ManifestTypeMap<ManifestTypes> | string>(type: Key) {
return this.kinds.pipe(
map((kinds) => kinds.filter((kind) => kind.matchType === type)),
distinctUntilChanged(extensionArrayMemoization),
);
}
private _extensionsOfType<Key extends keyof ManifestTypeMap<ManifestTypes> | string>(type: Key) {
#extensionsOfType<
Key extends keyof ManifestTypeMap<ManifestTypes> | string,
T extends ManifestBase = SpecificManifestTypeOrManifestBase<ManifestTypes, Key>,
>(type: Key) {
return this.extensions.pipe(
map((exts) => exts.filter((ext) => ext.type === type)),
map((exts) => exts.filter((ext) => ext.type === type) as unknown as T[]),
distinctUntilChanged(extensionArrayMemoization),
);
}
private _kindsOfTypes(types: string[]) {
#kindsOfTypes(types: string[]) {
return this.kinds.pipe(
map((kinds) => kinds.filter((kind) => types.indexOf(kind.matchType) !== -1)),
distinctUntilChanged(extensionArrayMemoization),
@@ -204,39 +194,67 @@ export class UmbExtensionRegistry<
}
// TODO: can we get rid of as unknown here
private _extensionsOfTypes<ExtensionType extends ManifestBase = ManifestBase>(
#extensionsOfTypes<ExtensionType extends ManifestBase = ManifestBase>(
types: Array<ExtensionType['type']>,
): Observable<Array<ExtensionType>> {
return this.extensions.pipe(
map((exts) => exts.filter((ext) => types.indexOf(ext.type) !== -1)),
map((exts) => exts.filter((ext) => types.indexOf(ext.type) !== -1) as unknown as Array<ExtensionType>),
distinctUntilChanged(extensionArrayMemoization),
) as unknown as Observable<Array<ExtensionType>>;
);
}
getByAlias<T extends ManifestBase = ManifestBase>(alias: string) {
#mergeExtensionWithKinds<ExtensionType extends ManifestBase, KindType extends ManifestKind<ManifestTypes>>([
ext,
kinds,
]: [ExtensionType | undefined, Array<KindType>]): ExtensionType | undefined {
// Specific Extension Meta merge (does not merge conditions)
if (ext) {
// Since we don't have the type up front in this request, we will just get all kinds here and find the matching one:
const baseManifest = kinds.find((kind) => kind.matchKind === ext.kind)?.manifest;
// TODO: This check can go away when making a find kind based on type and kind.
if (baseManifest) {
const merged = { __isMatchedWithKind: true, ...baseManifest, ...ext };
if ((baseManifest as any).meta) {
(merged as any).meta = { ...(baseManifest as any).meta, ...(ext as any).meta };
}
return merged as ExtensionType;
}
}
return ext;
}
#mergeExtensionsWithKinds<ExtensionType extends ManifestBase, KindType extends ManifestKind<ManifestTypes>>([
exts,
kinds,
]: [Array<ExtensionType>, Array<KindType>]): ExtensionType[] {
return exts
.map((ext) => {
// Specific Extension Meta merge (does not merge conditions)
const baseManifest = kinds.find((kind) => kind.matchKind === ext.kind)?.manifest;
if (baseManifest) {
const merged = { __isMatchedWithKind: true, ...baseManifest, ...ext } as any;
if ((baseManifest as any).meta) {
merged.meta = { ...(baseManifest as any).meta, ...(ext as any).meta };
}
return merged;
}
return ext;
})
.sort(sortExtensions);
}
/**
* Get an observable that provides extensions matching the given alias.
* @param alias {string} - The alias of the extensions to get.
* @returns {Observable<T | undefined>} - An observable of the extension that matches the alias.
*/
byAlias<T extends ManifestBase = ManifestBase>(alias: string) {
return this.extensions.pipe(
map((exts) => exts.find((ext) => ext.alias === alias)),
distinctUntilChanged(extensionSingleMemoization),
switchMap((ext) => {
if (ext?.kind) {
return this._kindsOfType(ext.type).pipe(
map((kinds) => {
// Specific Extension Meta merge (does not merge conditions)
if (ext) {
// Since we dont have the type up front in this request, we will just get all kinds here and find the matching one:
const baseManifest = kinds.find((kind) => kind.matchKind === ext.kind)?.manifest;
// TODO: This check can go away when making a find kind based on type and kind.
if (baseManifest) {
const merged = { __isMatchedWithKind: true, ...baseManifest, ...ext } as any;
if ((baseManifest as any).meta) {
merged.meta = { ...(baseManifest as any).meta, ...(ext as any).meta };
}
return merged;
}
}
return ext;
}),
);
return this.#kindsOfType(ext.type).pipe(map((kinds) => this.#mergeExtensionWithKinds([ext, kinds])));
}
return of(ext);
}),
@@ -244,8 +262,18 @@ export class UmbExtensionRegistry<
distinctUntilChanged(extensionAndKindMatchSingleMemoization),
) as Observable<T | undefined>;
}
/**
* @deprecated Use `byAlias` instead.
*/
getByAlias = this.byAlias.bind(this);
getByTypeAndAlias<
/**
* Get an observable that provides extensions matching the given type and alias.
* @param type {string} - The type of the extensions to get.
* @param alias {string} - The alias of the extensions to get.
* @returns {Observable<T | undefined>} - An observable of the extensions that matches the type and alias.
*/
byTypeAndAlias<
Key extends keyof ManifestTypeMap<ManifestTypes> | string,
T extends ManifestBase = SpecificManifestTypeOrManifestBase<ManifestTypes, Key>,
>(type: Key, alias: string) {
@@ -254,106 +282,94 @@ export class UmbExtensionRegistry<
map((exts) => exts.find((ext) => ext.type === type && ext.alias === alias)),
distinctUntilChanged(extensionSingleMemoization),
),
this._kindsOfType(type),
this.#kindsOfType(type),
]).pipe(
map(([ext, kinds]) => {
// TODO: share one merge function between the different methods of this class:
// Specific Extension Meta merge (does not merge conditions)
if (ext) {
const baseManifest = kinds.find((kind) => kind.matchKind === ext.kind)?.manifest;
if (baseManifest) {
const merged = { __isMatchedWithKind: true, ...baseManifest, ...ext } as any;
if ((baseManifest as any).meta) {
merged.meta = { ...(baseManifest as any).meta, ...(ext as any).meta };
}
return merged;
}
}
return ext;
}),
map(this.#mergeExtensionWithKinds),
distinctUntilChanged(extensionAndKindMatchSingleMemoization),
) as Observable<T | undefined>;
}
/**
* @deprecated Use `byTypeAndAlias` instead.
*/
getByTypeAndAlias = this.byTypeAndAlias.bind(this);
getByTypeAndAliases<
byTypeAndAliases<
Key extends keyof ManifestTypeMap<ManifestTypes> | string,
T extends ManifestBase = SpecificManifestTypeOrManifestBase<ManifestTypes, Key>,
>(type: Key, aliases: Array<string>) {
return combineLatest([
this.extensions.pipe(
map((exts) => exts.filter((ext) => ext.type === type && aliases.indexOf(ext.alias) !== -1)),
map((exts) => exts.filter((ext) => ext.type === type && aliases.indexOf(ext.alias) !== -1) as unknown as T[]),
distinctUntilChanged(extensionArrayMemoization),
),
this._kindsOfType(type),
this.#kindsOfType(type),
]).pipe(
map(([exts, kinds]) =>
exts
.map((ext) => {
// Specific Extension Meta merge (does not merge conditions)
const baseManifest = kinds.find((kind) => kind.matchKind === ext.kind)?.manifest;
if (baseManifest) {
const merged = { __isMatchedWithKind: true, ...baseManifest, ...ext } as any;
if ((baseManifest as any).meta) {
merged.meta = { ...(baseManifest as any).meta, ...(ext as any).meta };
}
return merged;
}
return ext;
})
.sort(sortExtensions),
map(this.#mergeExtensionsWithKinds),
distinctUntilChanged(extensionAndKindMatchArrayMemoization),
) as Observable<Array<T>>;
}
/**
* @deprecated Use `byTypeAndAliases` instead.
*/
getByTypeAndAliases = this.byTypeAndAliases.bind(this);
/**
* Get an observable of extensions by type and a given filter method.
* This will return the all extensions that matches the type and which filter method returns true.
* The filter method will be called for each extension manifest of the given type, and the first argument to it is the extension manifest.
* @param type {string} - The type of the extension to get
* @param filter {(ext: T): void} - The filter method to use to filter the extensions
* @returns {Observable<Array<T>>} - An observable of the extensions that matches the type and filter method
*/
byTypeAndFilter<
Key extends keyof ManifestTypeMap<ManifestTypes> | string,
T extends ManifestBase = SpecificManifestTypeOrManifestBase<ManifestTypes, Key>,
>(type: Key, filter: (ext: T) => boolean) {
return combineLatest([
this.extensions.pipe(
map((exts) => exts.filter((ext) => ext.type === type && filter(ext as unknown as T)) as unknown as T[]),
distinctUntilChanged(extensionArrayMemoization),
),
this.#kindsOfType(type),
]).pipe(
map(this.#mergeExtensionsWithKinds),
distinctUntilChanged(extensionAndKindMatchArrayMemoization),
) as Observable<Array<T>>;
}
extensionsOfType<
/**
* Get an observable that provides extensions matching the given type.
* @param type {string} - The type of the extensions to get.
* @returns {Observable<T | undefined>} - An observable of the extensions that matches the type.
*/
byType<
Key extends keyof ManifestTypeMap<ManifestTypes> | string,
T extends ManifestBase = SpecificManifestTypeOrManifestBase<ManifestTypes, Key>,
>(type: Key) {
return combineLatest([this._extensionsOfType(type), this._kindsOfType(type)]).pipe(
map(([exts, kinds]) =>
exts
.map((ext) => {
// Specific Extension Meta merge (does not merge conditions)
const baseManifest = kinds.find((kind) => kind.matchKind === ext.kind)?.manifest;
if (baseManifest) {
const merged = { __isMatchedWithKind: true, ...baseManifest, ...ext } as any;
if ((baseManifest as any).meta) {
merged.meta = { ...(baseManifest as any).meta, ...(ext as any).meta };
}
return merged;
}
return ext;
})
.sort(sortExtensions),
),
return combineLatest([this.#extensionsOfType(type), this.#kindsOfType(type)]).pipe(
map(this.#mergeExtensionsWithKinds),
distinctUntilChanged(extensionAndKindMatchArrayMemoization),
) as Observable<Array<T>>;
}
/**
* @deprecated Use `byType` instead.
*/
extensionsOfType = this.byType.bind(this);
extensionsOfTypes<ExtensionTypes extends ManifestBase = ManifestBase>(
types: string[],
): Observable<Array<ExtensionTypes>> {
return combineLatest([this._extensionsOfTypes(types), this._kindsOfTypes(types)]).pipe(
map(([exts, kinds]) =>
exts
.map((ext) => {
// Specific Extension Meta merge (does not merge conditions)
if (ext) {
const baseManifest = kinds.find((kind) => kind.matchKind === ext.kind)?.manifest;
if (baseManifest) {
const merged = { __isMatchedWithKind: true, ...baseManifest, ...ext } as any;
if ((baseManifest as any).meta) {
merged.meta = { ...(baseManifest as any).meta, ...(ext as any).meta };
}
return merged;
}
}
return ext;
})
.sort(sortExtensions),
),
/**
* Get an observable that provides extensions matching given types.
* @param type {Array<string>} - The types of the extensions to get.
* @returns {Observable<T | undefined>} - An observable of the extensions that matches the types.
*/
byTypes<ExtensionTypes extends ManifestBase = ManifestBase>(types: string[]): Observable<Array<ExtensionTypes>> {
return combineLatest([this.#extensionsOfTypes<ExtensionTypes>(types), this.#kindsOfTypes(types)]).pipe(
map(this.#mergeExtensionsWithKinds),
distinctUntilChanged(extensionAndKindMatchArrayMemoization),
) as Observable<Array<ExtensionTypes>>;
}
/**
* @deprecated Use `byTypes` instead.
*/
extensionsOfTypes = this.byTypes.bind(this);
}

View File

@@ -1,3 +1,3 @@
export * from './localize.controller.js';
export * from './localization.controller.js';
export * from './types/localization.js';
export * from './manager.js';
export * from './localization.manager.js';

View File

@@ -1,7 +1,7 @@
import { aTimeout, elementUpdated, expect, fixture, html } from '@open-wc/testing';
import type { DefaultLocalizationSet, LocalizationSet} from './manager.js';
import { registerLocalization, localizations } from './manager.js';
import { UmbLocalizeController } from './localize.controller.js';
import type { UmbLocalizationSet, UmbLocalizationSetBase } from './localization.manager.js';
import { umbLocalizationManager } from './localization.manager.js';
import { UmbLocalizationController } from './localization.controller.js';
import { LitElement, customElement, property } from '@umbraco-cms/backoffice/external/lit';
import { UmbElementMixin } from '@umbraco-cms/backoffice/element-api';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
@@ -11,7 +11,21 @@ class UmbLocalizeControllerHostElement extends UmbElementMixin(LitElement) {
@property() lang = 'en-us';
}
interface TestLocalization extends LocalizationSet {
@customElement('umb-localization-render-count')
class UmbLocalizationRenderCountElement extends UmbElementMixin(LitElement) {
amountOfUpdates = 0;
requestUpdate() {
super.requestUpdate();
this.amountOfUpdates++;
}
render() {
return html`${this.localize.term('logout')}`;
}
}
interface TestLocalization extends UmbLocalizationSetBase {
close: string;
logout: string;
withInlineToken: any;
@@ -36,20 +50,26 @@ const english: TestLocalization = {
},
};
const englishOverride: DefaultLocalizationSet = {
const englishOverride: UmbLocalizationSet = {
$code: 'en-us',
$dir: 'ltr',
close: 'Close 2',
};
const danish: DefaultLocalizationSet = {
const englishOverrideLogout: UmbLocalizationSet = {
$code: 'en-us',
$dir: 'ltr',
logout: 'Log out 2',
};
const danish: UmbLocalizationSet = {
$code: 'da',
$dir: 'ltr',
close: 'Luk',
notOnRegional: 'Not on regional',
};
const danishRegional: DefaultLocalizationSet = {
const danishRegional: UmbLocalizationSet = {
$code: 'da-dk',
$dir: 'ltr',
close: 'Luk',
@@ -57,10 +77,10 @@ const danishRegional: DefaultLocalizationSet = {
//#endregion
describe('UmbLocalizeController', () => {
let controller: UmbLocalizeController<TestLocalization>;
let controller: UmbLocalizationController;
beforeEach(async () => {
registerLocalization(english, danish, danishRegional);
umbLocalizationManager.registerManyLocalizations([english, danish, danishRegional]);
document.documentElement.lang = english.$code;
document.documentElement.dir = english.$dir;
await aTimeout(0);
@@ -72,12 +92,12 @@ describe('UmbLocalizeController', () => {
getControllers: () => [],
removeControllerByAlias: () => {},
} satisfies UmbControllerHost;
controller = new UmbLocalizeController(host);
controller = new UmbLocalizationController(host);
});
afterEach(() => {
controller.destroy();
localizations.clear();
umbLocalizationManager.localizations.clear();
});
it('should have a default language', () => {
@@ -130,9 +150,9 @@ describe('UmbLocalizeController', () => {
expect(controller.term('logout')).to.equal('Log out'); // Fallback
});
it('should override a term if new translation is registered', () => {
it('should override a term if new localization is registered', () => {
// Let the registry load the new extension
registerLocalization(englishOverride);
umbLocalizationManager.registerLocalization(englishOverride);
expect(controller.term('close')).to.equal('Close 2');
});
@@ -152,6 +172,42 @@ describe('UmbLocalizeController', () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
expect((controller.term as any)('logout', 'Hello', 'World')).to.equal('Log out');
});
it('only reacts to changes of its own localization-keys', async () => {
const element: UmbLocalizationRenderCountElement = await fixture(
html`<umb-localization-render-count></umb-localization-render-count>`,
);
// Something triggers multiple updates initially, and it varies how many it is. So we wait for a timeout to ensure that we have a clean slate and then reset the counter:
await aTimeout(20);
element.amountOfUpdates = 0;
expect(element.shadowRoot!.textContent).to.equal('Log out');
// Let the registry load the new extension
umbLocalizationManager.registerLocalization(englishOverride);
// Wait three frames is safe:
await new Promise((resolve) => requestAnimationFrame(resolve));
await new Promise((resolve) => requestAnimationFrame(resolve));
await new Promise((resolve) => requestAnimationFrame(resolve));
// This should still be the same (cause it should not be affected as the change did not change our localization key)
expect(element.amountOfUpdates).to.equal(0);
expect(element.shadowRoot!.textContent).to.equal('Log out');
// Let the registry load the new extension
umbLocalizationManager.registerLocalization(englishOverrideLogout);
// Wait three frames is safe:
await new Promise((resolve) => requestAnimationFrame(resolve));
await new Promise((resolve) => requestAnimationFrame(resolve));
await new Promise((resolve) => requestAnimationFrame(resolve));
// Now we should have gotten one update and the text should be different
expect(element.amountOfUpdates).to.equal(1);
expect(element.shadowRoot!.textContent).to.equal('Log out 2');
});
});
describe('date', () => {
@@ -226,7 +282,7 @@ describe('UmbLocalizeController', () => {
});
it('should have a localize controller', () => {
expect(element.localize).to.be.instanceOf(UmbLocalizeController);
expect(element.localize).to.be.instanceOf(UmbLocalizationController);
});
it('should update the term when the language changes', async () => {

View File

@@ -12,19 +12,16 @@ The above copyright notice and this permission notice shall be included in all c
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import type {
DefaultLocalizationSet,
UmbLocalizationSet,
FunctionParams,
LocalizationSet} from './manager.js';
import {
connectedElements,
documentDirection,
documentLanguage,
fallback,
localizations,
} from './manager.js';
UmbLocalizationSetBase,
UmbLocalizationSetKey,
} from './localization.manager.js';
import { umbLocalizationManager } from './localization.manager.js';
import type { LitElement } from '@umbraco-cms/backoffice/external/lit';
import type { UmbController, UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
const LocalizeControllerAlias = Symbol();
const LocalizationControllerAlias = Symbol();
/**
* The UmbLocalizeController enables localization for your element.
*
@@ -43,12 +40,13 @@ const LocalizeControllerAlias = Symbol();
* }
* ```
*/
export class UmbLocalizeController<LocalizationType extends LocalizationSet = DefaultLocalizationSet>
export class UmbLocalizationController<LocalizationSetType extends UmbLocalizationSetBase = UmbLocalizationSet>
implements UmbController
{
#host;
#hostEl;
controllerAlias = LocalizeControllerAlias;
#hostEl?: HTMLElement & Partial<Pick<LitElement, 'requestUpdate'>>;
readonly controllerAlias = LocalizationControllerAlias;
#usedKeys = new Array<UmbLocalizationSetKey>();
constructor(host: UmbControllerHost) {
this.#host = host;
@@ -57,15 +55,11 @@ export class UmbLocalizeController<LocalizationType extends LocalizationSet = De
}
hostConnected(): void {
if (connectedElements.has(this.#hostEl)) {
return;
}
connectedElements.add(this.#hostEl);
umbLocalizationManager.appendConsumer(this);
}
hostDisconnected(): void {
connectedElements.delete(this.#hostEl);
umbLocalizationManager.removeConsumer(this);
}
destroy(): void {
@@ -73,12 +67,24 @@ export class UmbLocalizeController<LocalizationType extends LocalizationSet = De
this.#hostEl = undefined as any;
}
documentUpdate() {
this.#hostEl?.requestUpdate?.();
}
keysChanged(changedKeys: Set<UmbLocalizationSetKey>) {
const hasOneOfTheseKeys = this.#usedKeys.find((key) => changedKeys.has(key));
if (hasOneOfTheseKeys) {
this.#hostEl?.requestUpdate?.();
}
}
/**
* Gets the host element's directionality as determined by the `dir` attribute. The return value is transformed to
* lowercase.
*/
dir() {
return `${this.#hostEl.dir || documentDirection}`.toLowerCase();
return `${this.#hostEl?.dir || umbLocalizationManager.documentDirection}`.toLowerCase();
}
/**
@@ -86,21 +92,25 @@ export class UmbLocalizeController<LocalizationType extends LocalizationSet = De
* lowercase.
*/
lang() {
return `${this.#hostEl.lang || documentLanguage}`.toLowerCase();
return `${this.#hostEl?.lang || umbLocalizationManager.documentLanguage}`.toLowerCase();
}
private getLocalizationData(lang: string) {
const locale = new Intl.Locale(lang);
const language = locale?.language.toLowerCase();
const region = locale?.region?.toLowerCase() ?? '';
const primary = <LocalizationType>localizations.get(`${language}-${region}`);
const secondary = <LocalizationType>localizations.get(language);
const primary = umbLocalizationManager.localizations.get(`${language}-${region}`) as LocalizationSetType;
const secondary = umbLocalizationManager.localizations.get(language) as LocalizationSetType;
return { locale, language, region, primary, secondary };
}
/** Outputs a translated term. */
term<K extends keyof LocalizationType>(key: K, ...args: FunctionParams<LocalizationType[K]>): string {
term<K extends keyof LocalizationSetType>(key: K, ...args: FunctionParams<LocalizationSetType[K]>): string {
if (!this.#usedKeys.includes(key)) {
this.#usedKeys.push(key);
}
const { primary, secondary } = this.getLocalizationData(this.lang());
let term: any;
@@ -109,8 +119,8 @@ export class UmbLocalizeController<LocalizationType extends LocalizationSet = De
term = primary[key];
} else if (secondary && secondary[key]) {
term = secondary[key];
} else if (fallback && fallback[key as keyof LocalizationSet]) {
term = fallback[key as keyof LocalizationSet];
} else if (umbLocalizationManager.fallback && umbLocalizationManager.fallback[key]) {
term = umbLocalizationManager.fallback[key];
} else {
return String(key);
}

View File

@@ -0,0 +1,130 @@
/*
This module is a modified copy of the original Shoelace localize package: https://github.com/shoelace-style/localize
The original license is included below.
Copyright (c) 2020 A Beautiful Site, LLC
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import type { UmbLocalizationController } from './localization.controller.js';
import type { UmbLocalizationEntry } from './types/localization.js';
export type FunctionParams<T> = T extends (...args: infer U) => string ? U : [];
export interface UmbLocalizationSetBase {
$code: string; // e.g. en, en-GB
$dir: 'ltr' | 'rtl';
}
export type UmbLocalizationSetKey = string | number | symbol;
export interface UmbLocalizationSet extends UmbLocalizationSetBase {
[key: UmbLocalizationSetKey]: UmbLocalizationEntry;
}
export const UMB_DEFAULT_LOCALIZATION_CULTURE = 'en-us';
export class UmbLocalizationManager {
connectedControllers = new Set<UmbLocalizationController<UmbLocalizationSetBase>>();
#documentElementObserver: MutationObserver;
#changedKeys: Set<UmbLocalizationSetKey> = new Set();
#requestUpdateChangedKeysId?: number = undefined;
localizations: Map<string, UmbLocalizationSetBase> = new Map();
documentDirection = document.documentElement.dir || 'ltr';
documentLanguage = document.documentElement.lang || navigator.language;
get fallback(): UmbLocalizationSet | undefined {
return this.localizations.get(UMB_DEFAULT_LOCALIZATION_CULTURE) as UmbLocalizationSet;
}
constructor() {
this.#documentElementObserver = new MutationObserver(this.updateAll);
this.#documentElementObserver.observe(document.documentElement, {
attributes: true,
attributeFilter: ['dir', 'lang'],
});
}
appendConsumer(consumer: UmbLocalizationController<UmbLocalizationSetBase>) {
if (this.connectedControllers.has(consumer)) return;
this.connectedControllers.add(consumer);
}
removeConsumer(consumer: UmbLocalizationController<UmbLocalizationSetBase>) {
this.connectedControllers.delete(consumer);
}
/** Registers one or more translations */
registerLocalization(t: UmbLocalizationSetBase) {
const code = t.$code.toLowerCase();
if (this.localizations.has(code)) {
// Merge translations that share the same language code
this.localizations.set(code, { ...this.localizations.get(code), ...t });
} else {
this.localizations.set(code, t);
}
// Declare what keys have been changed:
const keys = Object.keys(t);
for (const key of keys) {
this.#changedKeys.add(key);
}
this.#requestChangedKeysUpdate();
}
#registerLocalizationBind = this.registerLocalization.bind(this);
registerManyLocalizations(translations: Array<UmbLocalizationSetBase>) {
translations.map(this.#registerLocalizationBind);
}
/** Updates all localized elements that are currently connected */
updateAll = () => {
const newDir = document.documentElement.dir || 'ltr';
const newLang = document.documentElement.lang || navigator.language;
if (this.documentDirection === newDir && this.documentLanguage === newLang) return;
// The document direction or language did changed, so lets move on:
this.documentDirection = newDir;
this.documentLanguage = newLang;
// Check if there was any changed.
this.connectedControllers.forEach((ctrl) => {
ctrl.documentUpdate();
});
if (this.#requestUpdateChangedKeysId) {
cancelAnimationFrame(this.#requestUpdateChangedKeysId);
this.#requestUpdateChangedKeysId = undefined;
}
this.#changedKeys.clear();
};
#updateChangedKeys = () => {
this.#requestUpdateChangedKeysId = undefined;
this.connectedControllers.forEach((ctrl) => {
ctrl.keysChanged(this.#changedKeys);
});
this.#changedKeys.clear();
};
/**
* Request an update of all consumers of the keys defined in #changedKeys.
* This waits one frame, which ensures that multiple changes are collected into one.
*/
#requestChangedKeysUpdate() {
if (this.#requestUpdateChangedKeysId) return;
this.#requestUpdateChangedKeysId = requestAnimationFrame(this.#updateChangedKeys);
}
}
export const umbLocalizationManager = new UmbLocalizationManager();

View File

@@ -1,73 +0,0 @@
/*
This module is a modified copy of the original Shoelace localize package: https://github.com/shoelace-style/localize
The original license is included below.
Copyright (c) 2020 A Beautiful Site, LLC
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import type { UmbLocalizationEntry } from './types/localization.js';
import type { LitElement } from '@umbraco-cms/backoffice/external/lit';
export type FunctionParams<T> = T extends (...args: infer U) => string ? U : [];
export interface LocalizationSet {
$code: string; // e.g. en, en-GB
$dir: 'ltr' | 'rtl';
}
export interface DefaultLocalizationSet extends LocalizationSet {
[key: string]: UmbLocalizationEntry;
}
export const connectedElements = new Set<HTMLElement>();
const documentElementObserver = new MutationObserver(update);
export const localizations: Map<string, LocalizationSet> = new Map();
export let documentDirection = document.documentElement.dir || 'ltr';
export let documentLanguage = document.documentElement.lang || navigator.language;
export let fallback: LocalizationSet;
// Watch for changes on <html lang>
documentElementObserver.observe(document.documentElement, {
attributes: true,
attributeFilter: ['dir', 'lang'],
});
/** Registers one or more translations */
export function registerLocalization(...translation: LocalizationSet[]) {
translation.map((t) => {
const code = t.$code.toLowerCase();
if (localizations.has(code)) {
// Merge translations that share the same language code
localizations.set(code, { ...localizations.get(code), ...t });
} else {
localizations.set(code, t);
}
// The first translation that's registered is the fallback
if (!fallback) {
fallback = t;
}
});
update();
}
/** Updates all localized elements that are currently connected */
export function update() {
documentDirection = document.documentElement.dir || 'ltr';
documentLanguage = document.documentElement.lang || navigator.language;
[...connectedElements.keys()].map((el) => {
if (typeof (el as LitElement).requestUpdate === 'function') {
// TODO: We might want to implement a specific Umbraco method for informing about this. and then make the default UmbLitElement call requestUpdate..? Cause then others can implement their own solution?
(el as LitElement).requestUpdate();
}
});
}

View File

@@ -706,6 +706,13 @@ export const data: Array<UmbMockDataTypeModel> = [
icon: 'icon-book-alt',
groupKey: 'demo-block-group-id',
},
{
label: 'Test broken group key',
contentElementTypeKey: 'test-block-id',
editorSize: 'medium',
icon: 'icon-war',
groupKey: 'group-id-that-does-not-exist',
},
],
},
],

View File

@@ -1488,4 +1488,30 @@ export const data: Array<UmbMockDocumentTypeModel> = [
properties: [],
containers: [],
},
{
allowedTemplateIds: [],
defaultTemplateId: null,
id: 'test-block-id',
alias: 'testBlock',
name: 'Test broken group key',
description: null,
icon: 'icon-war',
allowedAsRoot: true,
variesByCulture: false,
variesBySegment: false,
isElement: true,
hasChildren: false,
isContainer: false,
parentId: null,
isFolder: false,
allowedContentTypes: [],
compositions: [],
cleanup: {
preventCleanup: false,
keepAllVersionsNewerThanDays: null,
keepLatestVersionPerDayForDays: null,
},
properties: [],
containers: [],
},
];

View File

@@ -0,0 +1,44 @@
const { rest } = window.MockServiceWorker;
import { umbDocumentTypeMockDb } from '../../data/document-type/document-type.db.js';
import { UMB_SLUG } from './slug.js';
import { umbracoPath } from '@umbraco-cms/backoffice/utils';
export const folderHandlers = [
rest.post(umbracoPath(`${UMB_SLUG}/folder`), async (req, res, ctx) => {
const requestBody = await req.json();
if (!requestBody) return res(ctx.status(400, 'no body found'));
const id = umbDocumentTypeMockDb.folder.create(requestBody);
return res(
ctx.status(201),
ctx.set({
Location: req.url.href + '/' + id,
'Umb-Generated-Resource': id,
}),
);
}),
rest.get(umbracoPath(`${UMB_SLUG}/folder/:id`), (req, res, ctx) => {
const id = req.params.id as string;
if (!id) return res(ctx.status(400));
const response = umbDocumentTypeMockDb.folder.read(id);
return res(ctx.status(200), ctx.json(response));
}),
rest.put(umbracoPath(`${UMB_SLUG}/folder/:id`), async (req, res, ctx) => {
const id = req.params.id as string;
if (!id) return res(ctx.status(400, 'no id found'));
const requestBody = await req.json();
if (!requestBody) return res(ctx.status(400, 'no body found'));
umbDocumentTypeMockDb.folder.update(id, requestBody);
return res(ctx.status(200));
}),
rest.delete(umbracoPath(`${UMB_SLUG}/folder/:id`), (req, res, ctx) => {
const id = req.params.id as string;
if (!id) return res(ctx.status(400));
umbDocumentTypeMockDb.folder.delete(id);
return res(ctx.status(200));
}),
];

View File

@@ -1,5 +1,6 @@
import { treeHandlers } from './tree.handlers.js';
import { detailHandlers } from './detail.handlers.js';
import { itemHandlers } from './item.handlers.js';
import { folderHandlers } from './folder.handlers.js';
export const handlers = [...treeHandlers, ...itemHandlers, ...detailHandlers];
export const handlers = [...treeHandlers, ...itemHandlers, ...folderHandlers, ...detailHandlers];

View File

@@ -0,0 +1,13 @@
import type { ManifestPropertyEditorUi } from '@umbraco-cms/backoffice/extension-registry';
export const manifest: ManifestPropertyEditorUi = {
type: 'propertyEditorUi',
alias: 'Umb.PropertyEditorUi.BlockTypeGroupConfiguration',
name: 'Block Grid Group Configuration Property Editor UI',
js: () => import('./property-editor-ui-block-grid-group-configuration.element.js'),
meta: {
label: '',
icon: 'icon-box-alt',
group: 'common',
},
};

View File

@@ -0,0 +1,56 @@
import { html, customElement, property, css } from '@umbraco-cms/backoffice/external/lit';
import type { UmbPropertyEditorUiElement } from '@umbraco-cms/backoffice/extension-registry';
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
import type { UmbPropertyEditorConfigCollection } from '@umbraco-cms/backoffice/property-editor';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { UmbId } from '@umbraco-cms/backoffice/id';
import type { UmbBlockGridGroupType } from '@umbraco-cms/backoffice/block';
@customElement('umb-property-editor-ui-block-grid-group-configuration')
export class UmbPropertyEditorUIBlockGridGroupConfigurationElement
extends UmbLitElement
implements UmbPropertyEditorUiElement
{
private _value: Array<UmbBlockGridGroupType> = [];
@property({ type: Array })
public get value(): Array<UmbBlockGridGroupType> {
return this._value;
}
public set value(value: Array<UmbBlockGridGroupType>) {
this._value = value || [];
}
@property({ attribute: false })
public set config(config: UmbPropertyEditorConfigCollection | undefined) {}
#addGroup() {
this.value = [...this._value, { name: 'Unnamed group', key: UmbId.new() }];
this.dispatchEvent(new CustomEvent('property-value-change'));
}
render() {
return html`
<uui-button label=${this.localize.term('blockEditor_addBlockGroup')} look="placeholder" @click=${this.#addGroup}>
${this.localize.term('blockEditor_addBlockGroup')}
</uui-button>
`;
}
static styles = [
UmbTextStyles,
css`
uui-button {
display: block;
}
`,
];
}
export default UmbPropertyEditorUIBlockGridGroupConfigurationElement;
declare global {
interface HTMLElementTagNameMap {
'umb-property-editor-ui-block-grid-group-configuration': UmbPropertyEditorUIBlockGridGroupConfigurationElement;
}
}

View File

@@ -0,0 +1,15 @@
import type { Meta, Story } from '@storybook/web-components';
import type { UmbPropertyEditorUIBlockGridGroupConfigurationElement } from './property-editor-ui-block-grid-group-configuration.element.js';
import { html } from '@umbraco-cms/backoffice/external/lit';
import './property-editor-ui-block-grid-group-configuration.element.js';
export default {
title: 'Property Editor UIs/Block Grid Group Configuration',
component: 'umb-property-editor-ui-block-grid-group-configuration',
id: 'umb-property-editor-ui-block-grid-group-configuration',
} as Meta;
export const AAAOverview: Story<UmbPropertyEditorUIBlockGridGroupConfigurationElement> = () =>
html` <umb-property-editor-ui-block-grid-group-configuration></umb-property-editor-ui-block-grid-group-configuration>`;
AAAOverview.storyName = 'Overview';

View File

@@ -1,10 +1,21 @@
import type { UmbBlockTypeBaseModel, UmbInputBlockTypeElement } from '../../../block-type/index.js';
import type { UmbBlockTypeWithGroupKey, UmbInputBlockTypeElement } from '../../../block-type/index.js';
import '../../../block-type/components/input-block-type/index.js';
import type { UmbPropertyEditorUiElement } from '@umbraco-cms/backoffice/extension-registry';
import { html, customElement, property } from '@umbraco-cms/backoffice/external/lit';
import type { UmbPropertyEditorConfigCollection } from '@umbraco-cms/backoffice/property-editor';
import { html, customElement, property, state, repeat, nothing, css } from '@umbraco-cms/backoffice/external/lit';
import {
UmbPropertyValueChangeEvent,
type UmbPropertyEditorConfigCollection,
} from '@umbraco-cms/backoffice/property-editor';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
import {
UMB_BLOCK_GRID_TYPE,
type UmbBlockGridGroupType,
type UmbBlockGridGroupTypeConfiguration,
} from '@umbraco-cms/backoffice/block';
import type { UUIInputEvent } from '@umbraco-cms/backoffice/external/uui';
import { UMB_PROPERTY_DATASET_CONTEXT, type UmbPropertyDatasetContext } from '@umbraco-cms/backoffice/property';
import { UMB_WORKSPACE_MODAL, UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/modal';
/**
* @element umb-property-editor-ui-block-grid-type-configuration
@@ -14,22 +25,150 @@ export class UmbPropertyEditorUIBlockGridTypeConfigurationElement
extends UmbLitElement
implements UmbPropertyEditorUiElement
{
#datasetContext?: UmbPropertyDatasetContext;
#blockTypeWorkspaceModalRegistration?: UmbModalRouteRegistrationController<
typeof UMB_WORKSPACE_MODAL.DATA,
typeof UMB_WORKSPACE_MODAL.VALUE
>;
private _value: Array<UmbBlockTypeWithGroupKey> = [];
@property({ attribute: false })
value: UmbBlockTypeBaseModel[] = [];
get value() {
return this._value;
}
set value(value: Array<UmbBlockTypeWithGroupKey>) {
this._value = value ?? [];
}
@property({ type: Object, attribute: false })
public config?: UmbPropertyEditorConfigCollection;
render() {
return html`<umb-input-block-type
entity-type="block-grid-type"
.value=${this.value}
@change=${(e: Event) => {
this.value = (e.target as UmbInputBlockTypeElement).value;
}}></umb-input-block-type>`;
@state()
private _blockGroups: Array<UmbBlockGridGroupType> = [];
@state()
private _mappedValuesAndGroups: Array<UmbBlockGridGroupTypeConfiguration> = [];
@state()
private _workspacePath?: string;
constructor() {
super();
this.consumeContext(UMB_PROPERTY_DATASET_CONTEXT, async (instance) => {
this.#datasetContext = instance;
this.#observeProperties();
});
this.#blockTypeWorkspaceModalRegistration?.destroy();
this.#blockTypeWorkspaceModalRegistration = new UmbModalRouteRegistrationController(this, UMB_WORKSPACE_MODAL)
.addAdditionalPath(UMB_BLOCK_GRID_TYPE)
.onSetup(() => {
return { data: { entityType: UMB_BLOCK_GRID_TYPE, preset: {} }, modal: { size: 'large' } };
})
.observeRouteBuilder((routeBuilder) => {
const newpath = routeBuilder({});
this._workspacePath = newpath;
});
}
static styles = [UmbTextStyles];
async #observeProperties() {
if (!this.#datasetContext) return;
this.observe(await this.#datasetContext.propertyValueByAlias('blockGroups'), (value) => {
this._blockGroups = (value as Array<UmbBlockGridGroupType>) ?? [];
this.#mapValuesToBlockGroups();
});
this.observe(await this.#datasetContext.propertyValueByAlias('blocks'), () => {
this.#mapValuesToBlockGroups();
});
}
#mapValuesToBlockGroups() {
// What if a block is in a group that does not exist in the block groups? Should it be removed? (Right now they will never be rendered)
const valuesWithNoGroup = this._value.filter((value) => !value.groupKey);
const valuesWithGroup = this._blockGroups.map((group) => {
return { name: group.name, key: group.key, blocks: this._value.filter((value) => value.groupKey === group.key) };
});
this._mappedValuesAndGroups = [{ blocks: valuesWithNoGroup }, ...valuesWithGroup];
}
#onChange(e: CustomEvent, groupKey?: string) {
const updatedValues = (e.target as UmbInputBlockTypeElement).value.map((value) => ({ ...value, groupKey }));
const filteredValues = this.value.filter((value) => value.groupKey !== groupKey);
this.value = [...filteredValues, ...updatedValues];
this.dispatchEvent(new UmbPropertyValueChangeEvent());
}
#onCreate(e: CustomEvent, groupKey: string | null) {
const selectedElementType = e.detail.contentElementTypeKey;
if (selectedElementType) {
this.#blockTypeWorkspaceModalRegistration?.open({}, 'create/' + selectedElementType + '/' + groupKey);
}
}
#deleteGroup(groupKey: string) {
this.#datasetContext?.setPropertyValue(
'blockGroups',
this._blockGroups.filter((group) => group.key !== groupKey),
);
// Should blocks that belonged to the removed group be deleted as well?
this.value = this._value.filter((block) => block.groupKey !== groupKey);
}
#changeGroupName(e: UUIInputEvent, groupKey: string) {
const groupName = e.target.value as string;
this.#datasetContext?.setPropertyValue(
'blockGroups',
this._blockGroups.map((group) => (group.key === groupKey ? { ...group, name: groupName } : group)),
);
}
render() {
return html`${repeat(
this._mappedValuesAndGroups,
(group) => group.key,
(group) =>
html`${group.key ? this.#renderGroupInput(group.key, group.name) : nothing}
<umb-input-block-type
.value=${group.blocks}
.workspacePath=${this._workspacePath}
@create=${(e: CustomEvent) => this.#onCreate(e, group.key ?? null)}
@change=${(e: CustomEvent) => this.#onChange(e, group.key)}></umb-input-block-type>`,
)}`;
}
#renderGroupInput(groupKey: string, groupName?: string) {
return html`<uui-input
auto-width
label="Group"
.value=${groupName ?? ''}
@change=${(e: UUIInputEvent) => this.#changeGroupName(e, groupKey)}>
<uui-button compact slot="append" label="delete" @click=${() => this.#deleteGroup(groupKey)}>
<uui-icon name="icon-trash"></uui-icon>
</uui-button>
</uui-input>`;
}
static styles = [
UmbTextStyles,
css`
uui-input {
margin-top: var(--uui-size-6);
margin-bottom: var(--uui-size-4);
}
uui-input:not(:hover, :focus) {
border: 1px solid transparent;
}
uui-input:not(:hover, :focus) uui-button {
opacity: 0;
}
`,
];
}
export default UmbPropertyEditorUIBlockGridTypeConfigurationElement;

View File

@@ -2,5 +2,12 @@ import { manifest as blockGridEditor } from './block-grid-editor/manifests.js';
import { manifest as blockGridLayoutStylesheet } from './block-grid-layout-stylesheet/manifests.js';
import { manifest as blockGridTypeConfiguration } from './block-grid-type-configuration/manifests.js';
import { manifest as blockGridColumnSpan } from './block-grid-column-span/manifests.js';
import { manifest as blockGridGroupConfiguration } from './block-grid-group-configuration/manifests.js';
export const manifests = [blockGridTypeConfiguration, blockGridEditor, blockGridLayoutStylesheet, blockGridColumnSpan];
export const manifests = [
blockGridTypeConfiguration,
blockGridEditor,
blockGridLayoutStylesheet,
blockGridColumnSpan,
blockGridGroupConfiguration,
];

View File

@@ -1,4 +1,6 @@
import type { UmbBlockTypeBaseModel } from '../block-type/index.js';
import type { UmbBlockTypeBaseModel, UmbBlockTypeWithGroupKey } from '../block-type/index.js';
export const UMB_BLOCK_GRID_TYPE = 'block-grid-type';
export interface UmbBlockGridType extends UmbBlockTypeBaseModel {
columnSpanOptions: Array<number>;
@@ -9,5 +11,13 @@ export interface UmbBlockGridType extends UmbBlockTypeBaseModel {
thumbnail?: string;
areaGridColumns?: number;
areas: Array<any>;
groupKey: null | string;
}
export interface UmbBlockGridGroupType {
name: string;
key: string;
}
export interface UmbBlockGridGroupTypeConfiguration extends Partial<UmbBlockGridGroupType> {
blocks: Array<UmbBlockTypeWithGroupKey>;
}

View File

@@ -1,10 +1,12 @@
import type { UmbBlockTypeBaseModel, UmbInputBlockTypeElement } from '../../../block-type/index.js';
import '../../../block-type/components/input-block-type/index.js';
import { UMB_BLOCK_LIST_TYPE } from '../../types.js';
import type { UmbPropertyEditorUiElement } from '@umbraco-cms/backoffice/extension-registry';
import { html, customElement, property } from '@umbraco-cms/backoffice/external/lit';
import { html, customElement, property, state } from '@umbraco-cms/backoffice/external/lit';
import type { UmbPropertyEditorConfigCollection } from '@umbraco-cms/backoffice/property-editor';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
import { UMB_WORKSPACE_MODAL, UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/modal';
/**
* @element umb-property-editor-ui-block-list-type-configuration
@@ -14,16 +16,47 @@ export class UmbPropertyEditorUIBlockListBlockConfigurationElement
extends UmbLitElement
implements UmbPropertyEditorUiElement
{
#blockTypeWorkspaceModalRegistration?: UmbModalRouteRegistrationController<
typeof UMB_WORKSPACE_MODAL.DATA,
typeof UMB_WORKSPACE_MODAL.VALUE
>;
@state()
private _workspacePath?: string;
constructor() {
super();
this.#blockTypeWorkspaceModalRegistration?.destroy();
this.#blockTypeWorkspaceModalRegistration = new UmbModalRouteRegistrationController(this, UMB_WORKSPACE_MODAL)
.addAdditionalPath(UMB_BLOCK_LIST_TYPE)
.onSetup(() => {
return { data: { entityType: UMB_BLOCK_LIST_TYPE, preset: {} }, modal: { size: 'large' } };
})
.observeRouteBuilder((routeBuilder) => {
const newpath = routeBuilder({});
this._workspacePath = newpath;
});
}
@property({ attribute: false })
value: UmbBlockTypeBaseModel[] = [];
@property({ type: Object, attribute: false })
public config?: UmbPropertyEditorConfigCollection;
#onCreate(e: CustomEvent) {
const selectedElementType = e.detail.contentElementTypeKey;
if (selectedElementType) {
this.#blockTypeWorkspaceModalRegistration?.open({}, 'create/' + selectedElementType + '/null');
}
}
render() {
return html`<umb-input-block-type
entity-type="block-list-type"
.value=${this.value}
.workspacePath=${this._workspacePath}
@create=${this.#onCreate}
@change=${(e: Event) => {
this.value = (e.target as UmbInputBlockTypeElement).value;
}}></umb-input-block-type>`;

View File

@@ -1,5 +1,7 @@
import type { UmbBlockTypeBaseModel } from '../block-type/index.js';
import type { UmbBlockLayoutBaseModel } from '../index.js';
export const UMB_BLOCK_LIST_TYPE = 'block-list-type';
export interface UmbBlockListTypeModel extends UmbBlockTypeBaseModel {}
export interface UmbBlockListLayoutModel extends UmbBlockLayoutBaseModel {}

View File

@@ -1,2 +1,2 @@
export * from './input-block-type/index.js';
export * from './block-type-card/index.js';
export * from './input-block-type/index.js';

View File

@@ -1,20 +1,16 @@
import type { UmbBlockTypeBaseModel } from '../../types.js';
import {
UMB_DOCUMENT_TYPE_PICKER_MODAL,
UMB_MODAL_MANAGER_CONTEXT,
UMB_WORKSPACE_MODAL,
UmbModalRouteRegistrationController,
} from '@umbraco-cms/backoffice/modal';
import { UMB_DOCUMENT_TYPE_PICKER_MODAL, UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal';
import '../block-type-card/index.js';
import { css, html, customElement, property, state, repeat } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
import type { UmbPropertyDatasetContext } from '@umbraco-cms/backoffice/property';
import { UMB_PROPERTY_DATASET_CONTEXT } from '@umbraco-cms/backoffice/property';
import { UmbChangeEvent } from '@umbraco-cms/backoffice/event';
@customElement('umb-input-block-type')
export class UmbInputBlockTypeElement<
BlockType extends UmbBlockTypeBaseModel = UmbBlockTypeBaseModel,
> extends UmbLitElement {
//
@property({ type: Array, attribute: false })
public get value() {
return this._items;
@@ -23,43 +19,23 @@ export class UmbInputBlockTypeElement<
this._items = items ?? [];
}
@property({ type: String, attribute: 'entity-type' })
public get entityType() {
return this.#entityType;
}
public set entityType(entityType) {
this.#entityType = entityType;
this.#blockTypeWorkspaceModalRegistration?.destroy();
if (entityType) {
// TODO: Make specific modal token that requires data.
this.#blockTypeWorkspaceModalRegistration = new UmbModalRouteRegistrationController(this, UMB_WORKSPACE_MODAL)
.addAdditionalPath(entityType)
.onSetup(() => {
return { data: { entityType: entityType, preset: {} }, modal: { size: 'large' } };
})
.observeRouteBuilder((routeBuilder) => {
const newpath = routeBuilder({});
this._workspacePath = newpath;
});
}
}
#entityType?: string;
@property({ type: String })
workspacePath?: string;
@state()
private _items: Array<BlockType> = [];
@state()
private _workspacePath?: string;
#blockTypeWorkspaceModalRegistration?: UmbModalRouteRegistrationController<
typeof UMB_WORKSPACE_MODAL.DATA,
typeof UMB_WORKSPACE_MODAL.VALUE
>;
#datasetContext?: UmbPropertyDatasetContext;
#filter: Array<UmbBlockTypeBaseModel> = [];
constructor() {
super();
this.consumeContext(UMB_PROPERTY_DATASET_CONTEXT, async (instance) => {
this.#datasetContext = instance;
this.observe(await this.#datasetContext?.propertyValueByAlias('blocks'), (value) => {
this.#filter = value as Array<UmbBlockTypeBaseModel>;
});
});
}
create() {
@@ -74,23 +50,23 @@ export class UmbInputBlockTypeElement<
// Only pick elements:
docType.isElement &&
// Prevent picking the an already used element type:
this._items.find((x) => x.contentElementTypeKey === docType.unique) === undefined,
this.#filter &&
this.#filter.find((x) => x.contentElementTypeKey === docType.unique) === undefined,
},
});
const modalValue = await modalContext?.onSubmit();
const selectedElementType = modalValue.selection[0];
if (selectedElementType) {
this.#blockTypeWorkspaceModalRegistration?.open({}, 'create/' + selectedElementType);
this.dispatchEvent(new CustomEvent('create', { detail: { contentElementTypeKey: selectedElementType } }));
}
}
});
// No need to fire a change event, as all changes are made directly to the property, via context api.
}
deleteItem(contentElementTypeKey: string) {
this._items = this._items.filter((x) => x.contentElementTypeKey !== contentElementTypeKey);
this.value = this._items.filter((x) => x.contentElementTypeKey !== contentElementTypeKey);
this.dispatchEvent(new UmbChangeEvent());
}
@@ -99,12 +75,21 @@ export class UmbInputBlockTypeElement<
}
render() {
return html`
${this._items ? repeat(this._items, (item) => item.contentElementTypeKey, this.#renderItem) : ''}
${this.#renderButton()}
`;
return html`<div>
${repeat(this.value, (block) => block.contentElementTypeKey, this.#renderItem)} ${this.#renderButton()}
</div>`;
}
#renderItem = (item: BlockType) => {
return html`
<umb-block-type-card
.workspacePath=${this.workspacePath}
.key=${item.contentElementTypeKey}
@delete=${() => this.deleteItem(item.contentElementTypeKey)}>
</umb-block-type-card>
`;
};
#renderButton() {
return html`
<uui-button id="add-button" look="placeholder" @click=${() => this.create()} label="open">
@@ -114,19 +99,9 @@ export class UmbInputBlockTypeElement<
`;
}
#renderItem = (item: BlockType) => {
return html`
<umb-block-type-card
.workspacePath=${this._workspacePath}
.key=${item.contentElementTypeKey}
@delete=${() => this.deleteItem(item.contentElementTypeKey)}>
</umb-block-type-card>
`;
};
static styles = [
css`
:host {
div {
display: grid;
gap: var(--uui-size-space-3);
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
@@ -135,6 +110,7 @@ export class UmbInputBlockTypeElement<
#add-button {
text-align: center;
min-height: 150px;
height: 100%;
}
@@ -142,6 +118,18 @@ export class UmbInputBlockTypeElement<
display: block;
margin: 0 auto;
}
uui-input {
border: none;
margin: var(--uui-size-space-6) 0 var(--uui-size-space-4);
}
uui-input:hover uui-button {
opacity: 1;
}
uui-input uui-button {
opacity: 0;
}
`,
];
}

View File

@@ -1,9 +1,10 @@
import type { UmbBlockTypeBaseModel } from '../types.js';
import type { UmbBlockTypeBaseModel, UmbBlockTypeWithGroupKey } from '../types.js';
import type { UmbPropertyDatasetContext } from '@umbraco-cms/backoffice/property';
import { UMB_PROPERTY_CONTEXT } from '@umbraco-cms/backoffice/property';
import type {
UmbInvariantableWorkspaceContextInterface,
UmbWorkspaceContextInterface} from '@umbraco-cms/backoffice/workspace';
UmbWorkspaceContextInterface,
} from '@umbraco-cms/backoffice/workspace';
import {
UmbEditableWorkspaceContextBase,
UmbInvariantWorkspacePropertyDatasetContext,
@@ -13,7 +14,7 @@ import type { UmbControllerHost, UmbControllerHostElement } from '@umbraco-cms/b
import { UmbContextToken } from '@umbraco-cms/backoffice/context-api';
import type { ManifestWorkspace, PropertyEditorConfigProperty } from '@umbraco-cms/backoffice/extension-registry';
export class UmbBlockTypeWorkspaceContext<BlockTypeData extends UmbBlockTypeBaseModel = UmbBlockTypeBaseModel>
export class UmbBlockTypeWorkspaceContext<BlockTypeData extends UmbBlockTypeWithGroupKey = UmbBlockTypeWithGroupKey>
extends UmbEditableWorkspaceContextBase<BlockTypeData>
implements UmbInvariantableWorkspaceContextInterface
{
@@ -57,9 +58,11 @@ export class UmbBlockTypeWorkspaceContext<BlockTypeData extends UmbBlockTypeBase
});
}
async create(contentElementTypeId: string) {
async create(contentElementTypeId: string, groupKey?: string | null) {
//Only set groupKey property if it exists
const data: BlockTypeData = {
contentElementTypeKey: contentElementTypeId,
...(groupKey && { groupKey: groupKey }),
} as BlockTypeData;
this.setIsNew(true);

View File

@@ -6,9 +6,9 @@ import type { UmbRoute } from '@umbraco-cms/backoffice/router';
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
import { UmbWorkspaceIsNewRedirectController } from '@umbraco-cms/backoffice/workspace';
import type { UmbApi} from '@umbraco-cms/backoffice/extension-api';
import type { UmbApi } from '@umbraco-cms/backoffice/extension-api';
import { UmbExtensionsApiInitializer, createExtensionApi } from '@umbraco-cms/backoffice/extension-api';
import type { ManifestWorkspace} from '@umbraco-cms/backoffice/extension-registry';
import type { ManifestWorkspace } from '@umbraco-cms/backoffice/extension-registry';
import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
@customElement('umb-block-type-workspace')
@@ -39,11 +39,13 @@ export class UmbBlockTypeWorkspaceElement extends UmbLitElement {
this._routes = [
{
path: 'create/:elementTypeKey',
// Would it make more sense to have groupKey before elementTypeKey?
path: 'create/:elementTypeKey/:groupKey',
component: this.#editorElement,
setup: async (_component, info) => {
const elementTypeKey = info.match.params.elementTypeKey;
this.#workspaceContext!.create(elementTypeKey);
const groupKey = info.match.params.groupKey === 'null' ? null : info.match.params.groupKey;
this.#workspaceContext!.create(elementTypeKey, groupKey);
new UmbWorkspaceIsNewRedirectController(
this,

View File

@@ -45,7 +45,6 @@ export class UmbBlockWorkspaceContext<
readonly name = this.#label.asObservable();
constructor(host: UmbControllerHost, workspaceArgs: { manifest: ManifestWorkspace }) {
// TODO: We don't need a repo here, so maybe we should not require this of the UmbEditableWorkspaceContextBase
super(host, workspaceArgs.manifest.alias);
this.#entityType = workspaceArgs.manifest.meta?.entityType;
this.workspaceAlias = workspaceArgs.manifest.alias;

View File

@@ -1,24 +0,0 @@
import { UmbControllerEvent } from '@umbraco-cms/backoffice/controller-api';
export interface UmbActionEventArgs {
unique: string;
parentUnique: string | null; // TODO: remove this when we have endpoints to support mapping a new item without reloading the parent tree item
}
export class UmbActionEvent extends UmbControllerEvent {
#args: UmbActionEventArgs;
public constructor(type: string, args: UmbActionEventArgs) {
super(type);
this.#args = args;
}
getUnique(): string {
return this.#args.unique;
}
// TODO: this can be removed when the server supports reloading a tree item without reloading the parent
getParentUnique(): string | null {
return this.#args.parentUnique;
}
}

View File

@@ -1,4 +1,3 @@
export * from './repository-action.js';
export * from './action.interface.js';
export * from './action-event.context.js';
export * from './action.event.js';

View File

@@ -1,7 +1,7 @@
import { css, html, nothing, customElement, property, state } from '@umbraco-cms/backoffice/external/lit';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { map } from '@umbraco-cms/backoffice/external/rxjs';
import type { UmbSectionSidebarContext} from '@umbraco-cms/backoffice/section';
import type { UmbSectionSidebarContext } from '@umbraco-cms/backoffice/section';
import { UMB_SECTION_SIDEBAR_CONTEXT } from '@umbraco-cms/backoffice/section';
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
@@ -44,7 +44,7 @@ export class UmbEntityActionsBundleElement extends UmbLitElement {
#observeEntityActions() {
this.observe(
umbExtensionsRegistry
.extensionsOfType('entityAction')
.byType('entityAction')
.pipe(map((actions) => actions.filter((action) => action.meta.entityTypes.includes(this.entityType!)))),
(actions) => {
this._hasActions = actions.length > 0;

View File

@@ -2,7 +2,7 @@ import { UmbInputListBaseElement } from '../input-list-base/input-list-base.js';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { css, html, nothing, customElement, state } from '@umbraco-cms/backoffice/external/lit';
import { UMB_SECTION_PICKER_MODAL } from '@umbraco-cms/backoffice/modal';
import type { ManifestSection} from '@umbraco-cms/backoffice/extension-registry';
import type { ManifestSection } from '@umbraco-cms/backoffice/extension-registry';
import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
@customElement('umb-input-section')
@@ -18,7 +18,7 @@ export class UmbInputSectionElement extends UmbInputListBaseElement {
private _observeSections() {
if (this.value.length > 0) {
this.observe(umbExtensionsRegistry.extensionsOfType('section'), (sections: Array<ManifestSection>) => {
this.observe(umbExtensionsRegistry.byType('section'), (sections: Array<ManifestSection>) => {
this._sections = sections.filter((section) => this.value.includes(section.alias));
});
} else {

View File

@@ -38,6 +38,19 @@ export class UmbInputTinyMceElement extends FormControlMixin(UmbLitElement) {
return this._editorElement?.querySelector('iframe') ?? undefined;
}
set value(newValue: FormDataEntryValue | FormData) {
super.value = newValue;
const newContent = newValue?.toString() ?? '';
if(this.#editorRef && this.#editorRef.getContent() != newContent) {
this.#editorRef.setContent(newContent);
}
}
get value(): FormDataEntryValue | FormData {
return super.value;
}
@query('#editor', true)
private _editorElement?: HTMLElement;
@@ -78,7 +91,7 @@ export class UmbInputTinyMceElement extends FormControlMixin(UmbLitElement) {
* the plugins are ready and so are not associated with the editor.
*/
async #loadPlugins() {
const observable = umbExtensionsRegistry?.extensionsOfType('tinyMcePlugin');
const observable = umbExtensionsRegistry?.byType('tinyMcePlugin');
const manifests = (await firstValueFrom(observable)) as ManifestTinyMcePlugin[];
const promises = [];

View File

@@ -12,8 +12,8 @@ import {
export class UmbCopyDataTypeEntityAction extends UmbEntityActionBase<UmbCopyDataTypeRepository> {
#modalManagerContext?: UmbModalManagerContext;
constructor(host: UmbControllerHostElement, repositoryAlias: string, unique: string) {
super(host, repositoryAlias, unique);
constructor(host: UmbControllerHostElement, repositoryAlias: string, unique: string, entityType: string) {
super(host, repositoryAlias, unique, entityType);
this.consumeContext(UMB_MODAL_MANAGER_CONTEXT, (instance) => {
this.#modalManagerContext = instance;

View File

@@ -8,8 +8,8 @@ import { UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal';
export class UmbCreateDataTypeEntityAction extends UmbEntityActionBase<UmbDataTypeDetailRepository> {
#modalManagerContext?: UmbModalManagerContext;
constructor(host: UmbControllerHostElement, repositoryAlias: string, unique: string) {
super(host, repositoryAlias, unique);
constructor(host: UmbControllerHostElement, repositoryAlias: string, unique: string, entityType: string) {
super(host, repositoryAlias, unique, entityType);
this.consumeContext(UMB_MODAL_MANAGER_CONTEXT, (instance) => {
this.#modalManagerContext = instance;

View File

@@ -12,8 +12,8 @@ import {
export class UmbMoveDataTypeEntityAction extends UmbEntityActionBase<UmbMoveDataTypeRepository> {
#modalManagerContext?: UmbModalManagerContext;
constructor(host: UmbControllerHostElement, repositoryAlias: string, unique: string) {
super(host, repositoryAlias, unique);
constructor(host: UmbControllerHostElement, repositoryAlias: string, unique: string, entityType: string) {
super(host, repositoryAlias, unique, entityType);
this.consumeContext(UMB_MODAL_MANAGER_CONTEXT, (instance) => {
this.#modalManagerContext = instance;

View File

@@ -5,13 +5,14 @@ import type { UUIInputEvent } from '@umbraco-cms/backoffice/external/uui';
import type {
UmbDataTypePickerFlowModalData,
UmbDataTypePickerFlowModalValue,
UmbModalRouteBuilder} from '@umbraco-cms/backoffice/modal';
UmbModalRouteBuilder,
} from '@umbraco-cms/backoffice/modal';
import {
UMB_DATA_TYPE_PICKER_FLOW_DATA_TYPE_PICKER_MODAL,
UmbModalBaseElement,
UmbModalRouteRegistrationController,
} from '@umbraco-cms/backoffice/modal';
import type { ManifestPropertyEditorUi} from '@umbraco-cms/backoffice/extension-registry';
import type { ManifestPropertyEditorUi } from '@umbraco-cms/backoffice/extension-registry';
import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
import type { UmbEntityTreeItemModel } from '@umbraco-cms/backoffice/tree';
import { UMB_DATATYPE_WORKSPACE_MODAL } from '@umbraco-cms/backoffice/data-type';
@@ -107,7 +108,7 @@ export class UmbDataTypePickerFlowModalElement extends UmbModalBaseElement<
'_repositoryItemsObserver',
);
this.observe(umbExtensionsRegistry.extensionsOfType('propertyEditorUi'), (propertyEditorUIs) => {
this.observe(umbExtensionsRegistry.byType('propertyEditorUi'), (propertyEditorUIs) => {
// Only include Property Editor UIs which has Property Editor Schema Alias
this.#propertyEditorUIs = propertyEditorUIs.filter(
(propertyEditorUi) => !!propertyEditorUi.meta.propertyEditorSchemaAlias,

View File

@@ -3,11 +3,10 @@ import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import type { UUIInputEvent } from '@umbraco-cms/backoffice/external/uui';
import type {
UmbPropertyEditorUIPickerModalData,
UmbPropertyEditorUIPickerModalValue} from '@umbraco-cms/backoffice/modal';
import {
UmbModalBaseElement,
UmbPropertyEditorUIPickerModalValue,
} from '@umbraco-cms/backoffice/modal';
import type { ManifestPropertyEditorUi} from '@umbraco-cms/backoffice/extension-registry';
import { UmbModalBaseElement } from '@umbraco-cms/backoffice/modal';
import type { ManifestPropertyEditorUi } from '@umbraco-cms/backoffice/extension-registry';
import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
interface GroupedPropertyEditorUIs {
@@ -38,7 +37,7 @@ export class UmbPropertyEditorUIPickerModalElement extends UmbModalBaseElement<
#usePropertyEditorUIs() {
if (!this.data) return;
this.observe(umbExtensionsRegistry.extensionsOfType('propertyEditorUi'), (propertyEditorUIs) => {
this.observe(umbExtensionsRegistry.byType('propertyEditorUi'), (propertyEditorUIs) => {
// Only include Property Editor UIs which has Property Editor Schema Alias
this._propertyEditorUIs = propertyEditorUIs.filter(
(propertyEditorUi) => !!propertyEditorUi.meta.propertyEditorSchemaAlias,

View File

@@ -1,4 +1,5 @@
import { manifests as folderManifests } from './folder/manifests.js';
import { manifests as reloadManifests } from './reload-tree-item-children/manifests.js';
import { UmbDataTypeTreeRepository } from './data-type-tree.repository.js';
import { UmbDataTypeTreeStore } from './data-type-tree.store.js';
import type {
@@ -44,4 +45,4 @@ const treeItem: ManifestTreeItem = {
},
};
export const manifests = [treeRepository, treeStore, tree, treeItem, ...folderManifests];
export const manifests = [treeRepository, treeStore, tree, treeItem, ...folderManifests, ...reloadManifests];

View File

@@ -0,0 +1,24 @@
import {
UMB_DATA_TYPE_ENTITY_TYPE,
UMB_DATA_TYPE_FOLDER_ENTITY_TYPE,
UMB_DATA_TYPE_ROOT_ENTITY_TYPE,
} from '../../entity.js';
import { UMB_DATA_TYPE_DETAIL_REPOSITORY_ALIAS } from '../../repository/index.js';
import { UmbReloadTreeItemChildrenEntityAction } from '@umbraco-cms/backoffice/tree';
import { type ManifestEntityAction } from '@umbraco-cms/backoffice/extension-registry';
export const manifests: Array<ManifestEntityAction> = [
{
type: 'entityAction',
alias: 'Umb.EntityAction.DataType.Tree.ReloadTreeItemChildren',
name: 'Reload Data Type Tree Item Children Entity Action',
weight: 10,
api: UmbReloadTreeItemChildrenEntityAction,
meta: {
icon: 'icon-refresh',
label: 'Reload children...',
repositoryAlias: UMB_DATA_TYPE_DETAIL_REPOSITORY_ALIAS,
entityTypes: [UMB_DATA_TYPE_ENTITY_TYPE, UMB_DATA_TYPE_ROOT_ENTITY_TYPE, UMB_DATA_TYPE_FOLDER_ENTITY_TYPE],
},
},
];

View File

@@ -2,8 +2,8 @@ import { UmbEntityActionBase } from '../../entity-action.js';
import type { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api';
export class UmbCopyEntityAction<T extends { copy(): Promise<void> }> extends UmbEntityActionBase<T> {
constructor(host: UmbControllerHostElement, repositoryAlias: string, unique: string) {
super(host, repositoryAlias, unique);
constructor(host: UmbControllerHostElement, repositoryAlias: string, unique: string, entityType: string) {
super(host, repositoryAlias, unique, entityType);
}
async execute() {

View File

@@ -10,8 +10,8 @@ export class UmbDeleteEntityAction<
> extends UmbEntityActionBase<T> {
#modalManager?: UmbModalManagerContext;
constructor(host: UmbControllerHostElement, repositoryAlias: string, unique: string) {
super(host, repositoryAlias, unique);
constructor(host: UmbControllerHostElement, repositoryAlias: string, unique: string, entityType: string) {
super(host, repositoryAlias, unique, entityType);
new UmbContextConsumerController(this._host, UMB_MODAL_MANAGER_CONTEXT, (instance) => {
this.#modalManager = instance;

View File

@@ -4,8 +4,8 @@ import type { UmbControllerHostElement } from '@umbraco-cms/backoffice/controlle
// TODO: investigate what we need to finish the generic move action. We would need to open a picker, which requires a modal token,
// maybe we can use kinds to make a specific manifest to the move action.
export class UmbMoveEntityAction<T extends { move(): Promise<void> }> extends UmbEntityActionBase<T> {
constructor(host: UmbControllerHostElement, repositoryAlias: string, unique: string) {
super(host, repositoryAlias, unique);
constructor(host: UmbControllerHostElement, repositoryAlias: string, unique: string, entityType: string) {
super(host, repositoryAlias, unique, entityType);
}
async execute() {

View File

@@ -8,8 +8,8 @@ import { UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal';
export class UmbRenameEntityAction extends UmbEntityActionBase<UmbRenameRepository<{ unique: string }>> {
#modalManagerContext?: UmbModalManagerContext;
constructor(host: UmbControllerHostElement, repositoryAlias: string, unique: string) {
super(host, repositoryAlias, unique);
constructor(host: UmbControllerHostElement, repositoryAlias: string, unique: string, entityType: string) {
super(host, repositoryAlias, unique, entityType);
this.consumeContext(UMB_MODAL_MANAGER_CONTEXT, (instance) => {
this.#modalManagerContext = instance;

View File

@@ -4,8 +4,8 @@ import type { UmbControllerHostElement } from '@umbraco-cms/backoffice/controlle
export class UmbSortChildrenOfEntityAction<
T extends { sortChildrenOf(): Promise<void> },
> extends UmbEntityActionBase<T> {
constructor(host: UmbControllerHostElement, repositoryAlias: string, unique: string) {
super(host, repositoryAlias, unique);
constructor(host: UmbControllerHostElement, repositoryAlias: string, unique: string, entityType: string) {
super(host, repositoryAlias, unique, entityType);
}
async execute() {

View File

@@ -10,8 +10,8 @@ export class UmbTrashEntityAction<
> extends UmbEntityActionBase<T> {
#modalContext?: UmbModalManagerContext;
constructor(host: UmbControllerHostElement, repositoryAlias: string, unique: string) {
super(host, repositoryAlias, unique);
constructor(host: UmbControllerHostElement, repositoryAlias: string, unique: string, entityType: string) {
super(host, repositoryAlias, unique, entityType);
new UmbContextConsumerController(this._host, UMB_MODAL_MANAGER_CONTEXT, (instance) => {
this.#modalContext = instance;

View File

@@ -30,7 +30,7 @@ export class UmbEntityActionListElement extends UmbLitElement {
type="entityAction"
default-element="umb-entity-action"
.filter=${this._filter}
.props=${{ unique: this.unique }}></umb-extension-slot>
.props=${{ unique: this.unique, entityType: this.entityType }}></umb-extension-slot>
`
: '';
}

View File

@@ -7,6 +7,20 @@ import { createExtensionApi } from '@umbraco-cms/backoffice/extension-api';
@customElement('umb-entity-action')
export class UmbEntityActionElement extends UmbLitElement {
private _entityType?: string | null;
@property({ type: String })
public get entityType() {
return this._entityType;
}
public set entityType(value: string | undefined | null) {
const oldValue = this._entityType;
this._entityType = value;
if (oldValue !== this._entityType) {
this.#createApi();
this.requestUpdate('entityType', oldValue);
}
}
private _unique?: string | null;
@property({ type: String })
public get unique() {
@@ -37,10 +51,17 @@ export class UmbEntityActionElement extends UmbLitElement {
}
async #createApi() {
// only create the api if we have all the required properties
if (!this._manifest) return;
if (this._unique === undefined) return;
if (!this._entityType) return;
this.#api = await createExtensionApi(this._manifest, [this, this._manifest.meta.repositoryAlias, this.unique]);
this.#api = await createExtensionApi(this._manifest, [
this,
this._manifest.meta.repositoryAlias,
this.unique,
this.entityType,
]);
// TODO: Fix so when we use a HREF it does not refresh the page?
this._href = await this.#api.getHref?.();

View File

@@ -0,0 +1,23 @@
import { UmbControllerEvent } from '@umbraco-cms/backoffice/controller-api';
export interface UmbEntityActionEventArgs {
unique: string;
entityType: string;
}
export class UmbEntityActionEvent extends UmbControllerEvent {
#args: UmbEntityActionEventArgs;
public constructor(type: string, args: UmbEntityActionEventArgs) {
super(type);
this.#args = args;
}
getEntityType(): string {
return this.#args.entityType;
}
getUnique(): string {
return this.#args.unique;
}
}

View File

@@ -7,11 +7,13 @@ export interface UmbEntityAction<RepositoryType> extends UmbAction<RepositoryTyp
}
export class UmbEntityActionBase<RepositoryType> extends UmbActionBase<RepositoryType> {
entityType: string;
unique: string;
repositoryAlias: string;
constructor(host: UmbControllerHostElement, repositoryAlias: string, unique: string) {
constructor(host: UmbControllerHostElement, repositoryAlias: string, unique: string, entityType: string) {
super(host, repositoryAlias);
this.entityType = entityType;
this.unique = unique;
this.repositoryAlias = repositoryAlias;
}

View File

@@ -2,3 +2,4 @@ export * from './entity-action-list.element.js';
export * from './entity-action.element.js';
export * from './entity-action.js';
export * from './common/index.js';
export * from './entity-action.event.js';

View File

@@ -3,7 +3,7 @@ import { css, html, customElement, query, state, property } from '@umbraco-cms/b
import type { UUIComboboxElement, UUIComboboxEvent } from '@umbraco-cms/backoffice/external/uui';
import { FormControlMixin } from '@umbraco-cms/backoffice/external/uui';
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
import type { ManifestLocalization} from '@umbraco-cms/backoffice/extension-registry';
import type { ManifestLocalization } from '@umbraco-cms/backoffice/extension-registry';
import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
interface UmbCultureInputOption {
@@ -38,7 +38,7 @@ export class UmbUiCultureInputElement extends FormControlMixin(UmbLitElement) {
#observeTranslations() {
this.observe(
umbExtensionsRegistry.extensionsOfType('localization'),
umbExtensionsRegistry.byType('localization'),
(localizationManifests) => {
this.#mapToOptions(localizationManifests);
},

View File

@@ -2,7 +2,7 @@ import { aTimeout, elementUpdated, expect, fixture, html } from '@open-wc/testin
import { UmbLocalizeElement } from './localize.element.js';
import { umbLocalizationRegistry } from '@umbraco-cms/backoffice/localization';
import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
import { UmbLocalizeController } from '@umbraco-cms/backoffice/localization-api';
import { UmbLocalizationController } from '@umbraco-cms/backoffice/localization-api';
const english = {
type: 'localization',
@@ -62,7 +62,7 @@ describe('umb-localize', () => {
});
it('should have a localize controller', () => {
expect(element.localize).to.be.instanceOf(UmbLocalizeController);
expect(element.localize).to.be.instanceOf(UmbLocalizationController);
});
it('should localize a key', async () => {

View File

@@ -1,6 +1,6 @@
import { aTimeout, expect } from '@open-wc/testing';
import { UmbLocalizationRegistry } from './localization.registry.js';
import type { ManifestLocalization} from '@umbraco-cms/backoffice/extension-registry';
import type { ManifestLocalization } from '@umbraco-cms/backoffice/extension-registry';
import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
//#region Localizations

View File

@@ -1,129 +1,111 @@
import type {
UmbLocalizationSetBase,
UmbLocalizationDictionary,
UmbLocalizationFlatDictionary,
LocalizationSet} from '@umbraco-cms/backoffice/localization-api';
import {
registerLocalization,
localizations,
} from '@umbraco-cms/backoffice/localization-api';
import { hasDefaultExport, loadManifestPlainJs } from '@umbraco-cms/backoffice/extension-api';
import type { UmbBackofficeExtensionRegistry} from '@umbraco-cms/backoffice/extension-registry';
import { umbLocalizationManager } from '@umbraco-cms/backoffice/localization-api';
import type { ManifestLocalization, UmbBackofficeExtensionRegistry } from '@umbraco-cms/backoffice/extension-registry';
import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
import {
BehaviorSubject,
Subject,
combineLatest,
map,
distinctUntilChanged,
filter,
startWith,
} from '@umbraco-cms/backoffice/external/rxjs';
import { UmbStringState } from '@umbraco-cms/backoffice/observable-api';
import { combineLatest } from '@umbraco-cms/backoffice/external/rxjs';
import { hasDefaultExport, loadManifestPlainJs } from '@umbraco-cms/backoffice/extension-api';
function addOrUpdateDictionary(
innerDictionary: UmbLocalizationFlatDictionary,
dictionaryName: string,
dictionary: UmbLocalizationDictionary['value'],
) {
for (const [key, value] of Object.entries(dictionary)) {
innerDictionary[`${dictionaryName}_${key}`] = value;
}
}
export class UmbLocalizationRegistry {
#currentLanguage = new UmbStringState(document.documentElement.lang ?? 'en-us');
readonly currentLanguage = this.#currentLanguage.asObservable();
#loadedExtAliases: Array<string> = [];
/**
* Get the current registered translations.
*/
get localizations() {
return localizations;
return umbLocalizationManager.localizations;
}
get isDefaultLoaded() {
return this.#isDefaultLoaded.asObservable();
}
#currentLanguage = new Subject<string>();
#isDefaultLoaded = new BehaviorSubject(false);
constructor(extensionRegistry: UmbBackofficeExtensionRegistry) {
const currentLanguage$ = this.#currentLanguage.pipe(
startWith(document.documentElement.lang || 'en-us'),
map((x) => x.toLowerCase()),
distinctUntilChanged(),
);
combineLatest([this.currentLanguage, extensionRegistry.byType('localization')]).subscribe(
async ([currentLanguage, extensions]) => {
const locale = new Intl.Locale(currentLanguage);
const filteredExt = extensions.filter(
(ext) =>
ext.meta.culture.toLowerCase() === locale.baseName.toLowerCase() ||
ext.meta.culture.toLowerCase() === locale.language.toLowerCase(),
);
const currentExtensions$ = extensionRegistry.extensionsOfType('localization').pipe(
filter((x) => x.length > 0),
distinctUntilChanged((prev, curr) => prev.length === curr.length && prev.every((x) => curr.includes(x))),
);
// Only get the extensions that are not already loading/loaded:
const diff = filteredExt.filter((ext) => !this.#loadedExtAliases.includes(ext.alias));
if (diff.length !== 0) {
// got new localizations to load:
const translations = await Promise.all(diff.map(this.#loadExtension));
combineLatest([currentLanguage$, currentExtensions$]).subscribe(async ([userCulture, extensions]) => {
const locale = new Intl.Locale(userCulture);
const translations = await Promise.all(
extensions
.filter(
(x) =>
x.meta.culture.toLowerCase() === locale.baseName.toLowerCase() ||
x.meta.culture.toLowerCase() === locale.language.toLowerCase(),
)
.map(async (extension) => {
const innerDictionary: UmbLocalizationFlatDictionary = {};
if (translations.length) {
umbLocalizationManager.registerManyLocalizations(translations);
// If extension contains a dictionary, add it to the inner dictionary.
if (extension.meta.localizations) {
for (const [dictionaryName, dictionary] of Object.entries(extension.meta.localizations)) {
this.#addOrUpdateDictionary(innerDictionary, dictionaryName, dictionary);
}
// Set the document language
const newLang = locale.baseName.toLowerCase();
if (document.documentElement.lang.toLowerCase() !== newLang) {
document.documentElement.lang = newLang;
}
// If extension contains a js file, load it and add the default dictionary to the inner dictionary.
if (extension.js) {
const loadedExtension = await loadManifestPlainJs(extension.js);
if (loadedExtension && hasDefaultExport<UmbLocalizationDictionary>(loadedExtension)) {
for (const [dictionaryName, dictionary] of Object.entries(loadedExtension.default)) {
this.#addOrUpdateDictionary(innerDictionary, dictionaryName, dictionary);
}
}
// Set the document direction to the direction of the primary language
const newDir = translations[0].$dir ?? 'ltr';
if (document.documentElement.dir !== newDir) {
document.documentElement.dir = newDir;
}
// Notify subscribers that the inner dictionary has changed.
return {
$code: extension.meta.culture.toLowerCase(),
$dir: extension.meta.direction ?? 'ltr',
...innerDictionary,
} satisfies LocalizationSet;
}),
);
if (translations.length) {
registerLocalization(...translations);
// Set the document language
const newLang = locale.baseName.toLowerCase();
if (document.documentElement.lang.toLowerCase() !== newLang) {
document.documentElement.lang = newLang;
}
}
// Set the document direction to the direction of the primary language
const newDir = translations[0].$dir ?? 'ltr';
if (document.documentElement.dir !== newDir) {
document.documentElement.dir = newDir;
}
}
if (!this.#isDefaultLoaded.value) {
this.#isDefaultLoaded.next(true);
this.#isDefaultLoaded.complete();
}
});
},
);
}
#loadExtension = async (extension: ManifestLocalization) => {
this.#loadedExtAliases.push(extension.alias);
const innerDictionary: UmbLocalizationFlatDictionary = {};
// If extension contains a dictionary, add it to the inner dictionary.
if (extension.meta.localizations) {
for (const [dictionaryName, dictionary] of Object.entries(extension.meta.localizations)) {
addOrUpdateDictionary(innerDictionary, dictionaryName, dictionary);
}
}
// If extension contains a js file, load it and add the default dictionary to the inner dictionary.
if (extension.js) {
const loadedExtension = await loadManifestPlainJs(extension.js);
if (loadedExtension && hasDefaultExport<UmbLocalizationDictionary>(loadedExtension)) {
for (const [dictionaryName, dictionary] of Object.entries(loadedExtension.default)) {
addOrUpdateDictionary(innerDictionary, dictionaryName, dictionary);
}
}
}
// Notify subscribers that the inner dictionary has changed.
return {
$code: extension.meta.culture.toLowerCase(),
$dir: extension.meta.direction ?? 'ltr',
...innerDictionary,
} satisfies UmbLocalizationSetBase;
};
/**
* Load a language from the extension registry.
* @param locale The locale to load.
*/
loadLanguage(locale: string) {
this.#currentLanguage.next(locale);
}
#addOrUpdateDictionary(
innerDictionary: UmbLocalizationFlatDictionary,
dictionaryName: string,
dictionary: UmbLocalizationDictionary['value'],
) {
for (const [key, value] of Object.entries(dictionary)) {
innerDictionary[`${dictionaryName}_${key}`] = value;
}
this.#currentLanguage.setValue(locale.toLowerCase());
}
}

View File

@@ -1,14 +1,10 @@
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { css, html, customElement, state } from '@umbraco-cms/backoffice/external/lit';
import { UmbSelectionManager } from '@umbraco-cms/backoffice/utils';
import type { ManifestSection} from '@umbraco-cms/backoffice/extension-registry';
import type { ManifestSection } from '@umbraco-cms/backoffice/extension-registry';
import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
import type {
UmbSectionPickerModalData,
UmbSectionPickerModalValue} from '@umbraco-cms/backoffice/modal';
import {
UmbModalBaseElement,
} from '@umbraco-cms/backoffice/modal';
import type { UmbSectionPickerModalData, UmbSectionPickerModalValue } from '@umbraco-cms/backoffice/modal';
import { UmbModalBaseElement } from '@umbraco-cms/backoffice/modal';
@customElement('umb-section-picker-modal')
export class UmbSectionPickerModalElement extends UmbModalBaseElement<
@@ -28,7 +24,7 @@ export class UmbSectionPickerModalElement extends UmbModalBaseElement<
this.#selectionManager.setSelection(this.data?.selection ?? []);
this.observe(
umbExtensionsRegistry.extensionsOfType('section'),
umbExtensionsRegistry.byType('section'),
(sections: Array<ManifestSection>) => (this._sections = sections),
),
'umbSectionsObserver';

View File

@@ -14,6 +14,6 @@ export const UMB_DATA_TYPE_PICKER_FLOW_MODAL = new UmbModalToken<
>('Umb.Modal.DataTypePickerFlow', {
modal: {
type: 'sidebar',
size: 'small',
size: 'medium',
},
});

View File

@@ -2,7 +2,7 @@ import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import type { PropertyValueMap } from '@umbraco-cms/backoffice/external/lit';
import { customElement, css, html, property, map, state } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
import type { UmbPropertyEditorUiElement} from '@umbraco-cms/backoffice/extension-registry';
import type { UmbPropertyEditorUiElement } from '@umbraco-cms/backoffice/extension-registry';
import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
import { firstValueFrom } from '@umbraco-cms/backoffice/external/rxjs';
import type { UmbPropertyEditorConfigCollection } from '@umbraco-cms/backoffice/property-editor';
@@ -78,7 +78,7 @@ export class UmbPropertyEditorUITinyMceToolbarConfigurationElement
private async getToolbarPlugins(): Promise<void> {
// Get all the toolbar plugins
const plugin$ = umbExtensionsRegistry.extensionsOfType('tinyMcePlugin');
const plugin$ = umbExtensionsRegistry.byType('tinyMcePlugin');
const plugins = await firstValueFrom(plugin$);

View File

@@ -4,7 +4,7 @@ import type { UmbObserverController } from '@umbraco-cms/backoffice/observable-a
import { UmbStringState } from '@umbraco-cms/backoffice/observable-api';
import type { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api';
import { UmbBaseController } from '@umbraco-cms/backoffice/class-api';
import type { ManifestTheme} from '@umbraco-cms/backoffice/extension-registry';
import type { ManifestTheme } from '@umbraco-cms/backoffice/extension-registry';
import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
import { loadManifestPlainCss } from '@umbraco-cms/backoffice/extension-api';
@@ -37,7 +37,7 @@ export class UmbThemeContext extends UmbBaseController {
localStorage.setItem(LOCAL_STORAGE_KEY, themeAlias);
this.#themeObserver = this.observe(
umbExtensionsRegistry
.extensionsOfType('theme')
.byType('theme')
.pipe(map((extensions) => extensions.filter((extension) => extension.alias === themeAlias))),
async (themes) => {
this.#styleElement?.remove();

View File

@@ -7,8 +7,8 @@ import { type UmbFolderRepository, UMB_FOLDER_CREATE_MODAL } from '@umbraco-cms/
export class UmbCreateFolderEntityAction<T extends UmbFolderRepository> extends UmbEntityActionBase<T> {
#modalContext?: UmbModalManagerContext;
constructor(host: UmbControllerHostElement, repositoryAlias: string, unique: string) {
super(host, repositoryAlias, unique);
constructor(host: UmbControllerHostElement, repositoryAlias: string, unique: string, entityType: string) {
super(host, repositoryAlias, unique, entityType);
new UmbContextConsumerController(this._host, UMB_MODAL_MANAGER_CONTEXT, (instance) => {
this.#modalContext = instance;

View File

@@ -8,8 +8,8 @@ import type { UmbFolderRepository } from '@umbraco-cms/backoffice/tree';
export class UmbDeleteFolderEntityAction<T extends UmbFolderRepository> extends UmbEntityActionBase<T> {
#modalContext?: UmbModalManagerContext;
constructor(host: UmbControllerHostElement, repositoryAlias: string, unique: string) {
super(host, repositoryAlias, unique);
constructor(host: UmbControllerHostElement, repositoryAlias: string, unique: string, entityType: string) {
super(host, repositoryAlias, unique, entityType);
new UmbContextConsumerController(this._host, UMB_MODAL_MANAGER_CONTEXT, (instance) => {
this.#modalContext = instance;

View File

@@ -9,8 +9,8 @@ export class UmbFolderUpdateEntityAction<
> extends UmbEntityActionBase<T> {
#modalContext?: UmbModalManagerContext;
constructor(host: UmbControllerHostElement, repositoryAlias: string, unique: string) {
super(host, repositoryAlias, unique);
constructor(host: UmbControllerHostElement, repositoryAlias: string, unique: string, entityType: string) {
super(host, repositoryAlias, unique, entityType);
new UmbContextConsumerController(this._host, UMB_MODAL_MANAGER_CONTEXT, (instance) => {
this.#modalContext = instance;

View File

@@ -22,4 +22,10 @@ export * from './data-source/index.js';
// Folder
export * from './folder/index.js';
//
export {
UmbReloadTreeItemChildrenEntityAction,
UmbReloadTreeItemChildrenRequestEntityActionEvent,
} from './reload-tree-item-children/index.js';
export { UmbTreeRepositoryBase } from './tree-repository-base.js';

View File

@@ -0,0 +1,2 @@
export { UmbReloadTreeItemChildrenEntityAction } from './reload-tree-item-children.action.js';
export { UmbReloadTreeItemChildrenRequestEntityActionEvent } from './reload-tree-item-children-request.event.js';

View File

@@ -0,0 +1,9 @@
import { UmbEntityActionEvent, type UmbEntityActionEventArgs } from '@umbraco-cms/backoffice/entity-action';
export class UmbReloadTreeItemChildrenRequestEntityActionEvent extends UmbEntityActionEvent {
static readonly TYPE = 'reload-tree-item-children-request';
constructor(args: UmbEntityActionEventArgs) {
super(UmbReloadTreeItemChildrenRequestEntityActionEvent.TYPE, args);
}
}

View File

@@ -0,0 +1,28 @@
import type { UmbCopyDataTypeRepository } from '../../data-type/repository/copy/data-type-copy.repository.js';
import { UmbEntityActionBase } from '@umbraco-cms/backoffice/entity-action';
import type { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api';
import type { UmbActionEventContext } from '@umbraco-cms/backoffice/action';
import { UMB_ACTION_EVENT_CONTEXT } from '@umbraco-cms/backoffice/action';
import { UmbReloadTreeItemChildrenRequestEntityActionEvent } from '@umbraco-cms/backoffice/tree';
export class UmbReloadTreeItemChildrenEntityAction extends UmbEntityActionBase<UmbCopyDataTypeRepository> {
#actionEventContext?: UmbActionEventContext;
constructor(host: UmbControllerHostElement, repositoryAlias: string, unique: string, entityType: string) {
super(host, repositoryAlias, unique, entityType);
this.consumeContext(UMB_ACTION_EVENT_CONTEXT, (instance) => {
this.#actionEventContext = instance;
});
}
async execute() {
if (!this.#actionEventContext) throw new Error('Action Event context is not available');
this.#actionEventContext.dispatchEvent(
new UmbReloadTreeItemChildrenRequestEntityActionEvent({
unique: this.unique,
entityType: this.entityType,
}),
);
}
}

View File

@@ -9,6 +9,9 @@ import { UmbBooleanState, UmbDeepState, UmbStringState } from '@umbraco-cms/back
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { UmbBaseController } from '@umbraco-cms/backoffice/class-api';
import { UmbContextToken } from '@umbraco-cms/backoffice/context-api';
import { UMB_ACTION_EVENT_CONTEXT, type UmbActionEventContext } from '@umbraco-cms/backoffice/action';
import type { UmbEntityActionEvent } from '@umbraco-cms/backoffice/entity-action';
import { UmbReloadTreeItemChildrenRequestEntityActionEvent } from '@umbraco-cms/backoffice/tree';
export type UmbTreeItemUniqueFunction<TreeItemType extends UmbTreeItemModelBase> = (
x: TreeItemType,
@@ -52,6 +55,7 @@ export class UmbTreeItemContextBase<TreeItemType extends UmbTreeItemModelBase>
treeContext?: UmbTreeContextBase<TreeItemType>;
#sectionContext?: UmbSectionContext;
#sectionSidebarContext?: UmbSectionSidebarContext;
#actionEventContext?: UmbActionEventContext;
#getUniqueFunction: UmbTreeItemUniqueFunction<TreeItemType>;
constructor(host: UmbControllerHost, getUniqueFunction: UmbTreeItemUniqueFunction<TreeItemType>) {
@@ -129,6 +133,18 @@ export class UmbTreeItemContextBase<TreeItemType extends UmbTreeItemModelBase>
this.#observeIsSelected();
this.#observeHasChildren();
});
this.consumeContext(UMB_ACTION_EVENT_CONTEXT, (instance) => {
this.#actionEventContext = instance;
this.#actionEventContext.removeEventListener(
UmbReloadTreeItemChildrenRequestEntityActionEvent.TYPE,
this.#onReloadRequest as EventListener,
);
this.#actionEventContext.addEventListener(
UmbReloadTreeItemChildrenRequestEntityActionEvent.TYPE,
this.#onReloadRequest as EventListener,
);
});
}
getTreeItem() {
@@ -181,7 +197,7 @@ export class UmbTreeItemContextBase<TreeItemType extends UmbTreeItemModelBase>
#observeActions() {
this.observe(
umbExtensionsRegistry
.extensionsOfType('entityAction')
.byType('entityAction')
.pipe(map((actions) => actions.filter((action) => action.meta.entityTypes.includes(this.entityType!)))),
(actions) => {
this.#hasActions.setValue(actions.length > 0);
@@ -206,10 +222,26 @@ export class UmbTreeItemContextBase<TreeItemType extends UmbTreeItemModelBase>
});
}
#onReloadRequest = (event: UmbEntityActionEvent) => {
// Only handle children request here. Root request is handled by the tree context
if (!this.unique) return;
if (event.getUnique() !== this.unique) return;
if (event.getEntityType() !== this.entityType) return;
this.requestChildren();
};
// TODO: use router context
constructPath(pathname: string, entityType: string, unique: string | null) {
return `section/${pathname}/workspace/${entityType}/edit/${unique}`;
}
destroy(): void {
this.#actionEventContext?.removeEventListener(
UmbReloadTreeItemChildrenRequestEntityActionEvent.TYPE,
this.#onReloadRequest as EventListener,
);
super.destroy();
}
}
export const UMB_TREE_ITEM_CONTEXT = new UmbContextToken<UmbTreeItemContext<any>>('UmbTreeItemContext');

View File

@@ -7,9 +7,6 @@ import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
@customElement('umb-tree-item-base')
export class UmbTreeItemBaseElement extends UmbLitElement {
@state()
private _iconAlias?: string;
@state()
private _item?: UmbTreeItemModelBase;

View File

@@ -3,10 +3,9 @@ import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
import type {
ManifestMenuItemTreeKind,
UmbBackofficeManifestKind,
UmbMenuItemElement} from '@umbraco-cms/backoffice/extension-registry';
import {
umbExtensionsRegistry,
UmbMenuItemElement,
} from '@umbraco-cms/backoffice/extension-registry';
import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
// TODO: Move to separate file:
const manifest: UmbBackofficeManifestKind = {

View File

@@ -1,5 +1,7 @@
import { UmbReloadTreeItemChildrenRequestEntityActionEvent } from './reload-tree-item-children/index.js';
import type { UmbTreeItemModelBase } from './types.js';
import type { UmbTreeRepository } from './tree-repository.interface.js';
import { type UmbActionEventContext, UMB_ACTION_EVENT_CONTEXT } from '@umbraco-cms/backoffice/action';
import type { Observable } from '@umbraco-cms/backoffice/external/rxjs';
import type { UmbPagedData } from '@umbraco-cms/backoffice/repository';
import {
@@ -12,6 +14,8 @@ import type { UmbControllerHostElement } from '@umbraco-cms/backoffice/controlle
import { UmbExtensionApiInitializer } from '@umbraco-cms/backoffice/extension-api';
import type { ProblemDetails } from '@umbraco-cms/backoffice/backend-api';
import { UmbSelectionManager } from '@umbraco-cms/backoffice/utils';
import type { UmbEntityActionEvent } from '@umbraco-cms/backoffice/entity-action';
import { UmbObjectState } from '@umbraco-cms/backoffice/observable-api';
// TODO: update interface
export interface UmbTreeContext<TreeItemType extends UmbTreeItemModelBase> extends UmbBaseController {
@@ -27,14 +31,16 @@ export class UmbTreeContextBase<TreeItemType extends UmbTreeItemModelBase>
extends UmbBaseController
implements UmbTreeContext<TreeItemType>
{
#treeRoot = new UmbObjectState<TreeItemType | undefined>(undefined);
treeRoot = this.#treeRoot.asObservable();
public repository?: UmbTreeRepository<TreeItemType>;
public selectableFilter?: (item: TreeItemType) => boolean = () => true;
public filter?: (item: TreeItemType) => boolean = () => true;
public readonly selection = new UmbSelectionManager(this._host);
#treeAlias?: string;
#actionEventContext?: UmbActionEventContext;
#initResolver?: () => void;
#initialized = false;
@@ -46,6 +52,20 @@ export class UmbTreeContextBase<TreeItemType extends UmbTreeItemModelBase>
constructor(host: UmbControllerHostElement) {
super(host);
this.provideContext('umbTreeContext', this);
this.consumeContext(UMB_ACTION_EVENT_CONTEXT, (instance) => {
this.#actionEventContext = instance;
this.#actionEventContext.removeEventListener(
UmbReloadTreeItemChildrenRequestEntityActionEvent.TYPE,
this.#onReloadRequest as EventListener,
);
this.#actionEventContext.addEventListener(
UmbReloadTreeItemChildrenRequestEntityActionEvent.TYPE,
this.#onReloadRequest as EventListener,
);
});
this.requestTreeRoot();
}
// TODO: find a generic way to do this
@@ -69,7 +89,13 @@ export class UmbTreeContextBase<TreeItemType extends UmbTreeItemModelBase>
public async requestTreeRoot() {
await this.#init;
return this.repository!.requestTreeRoot();
const { data } = await this.repository!.requestTreeRoot();
if (data) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
this.#treeRoot.setValue(data);
}
}
public async requestRootItems() {
@@ -121,4 +147,23 @@ export class UmbTreeContextBase<TreeItemType extends UmbTreeItemModelBase>
},
);
}
#onReloadRequest = (event: UmbEntityActionEvent) => {
// Only handle root request here. Items are handled by the tree item context
const treeRoot = this.#treeRoot.getValue();
if (treeRoot === undefined) return;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
if (event.getUnique() !== treeRoot.unique) return;
if (event.getEntityType() !== treeRoot.entityType) return;
this.requestRootItems();
};
destroy(): void {
this.#actionEventContext?.removeEventListener(
UmbReloadTreeItemChildrenRequestEntityActionEvent.TYPE,
this.#onReloadRequest as EventListener,
);
super.destroy();
}
}

View File

@@ -79,19 +79,21 @@ export class UmbTreeElement extends UmbLitElement {
private _treeRoot?: UmbTreeItemModelBase;
#treeContext = new UmbTreeContextBase<UmbTreeItemModelBase>(this);
#rootItemsObserver?: UmbObserverController<Array<UmbTreeItemModelBase>>;
constructor() {
super();
this.#requestTreeRoot();
this.#observeTreeRoot();
}
async #requestTreeRoot() {
if (!this.#treeContext?.requestTreeRoot) throw new Error('Tree does not support root');
const { data } = await this.#treeContext.requestTreeRoot();
this._treeRoot = data;
#observeTreeRoot() {
this.observe(
this.#treeContext.treeRoot,
(treeRoot) => {
this._treeRoot = treeRoot;
},
'umbTreeRootObserver',
);
}
async #observeRootItems() {

View File

@@ -42,6 +42,7 @@ export class UmbDashboardTranslationDictionaryElement extends UmbLitElement {
const { data } = await this.#repo.list(0, 1000);
this.#dictionaryItems = data?.items ?? [];
this.#setTableColumns();
this.#setTableItems();
}

View File

@@ -1,2 +0,0 @@
export const UMB_DICTIONARY_ROOT_ENTITY_TYPE = 'dictionary-root';
export const UMB_DICTIONARY_ENTITY_TYPE = 'dictionary-item';

View File

@@ -6,8 +6,8 @@ import type { UmbControllerHostElement } from '@umbraco-cms/backoffice/controlle
export default class UmbCreateDictionaryEntityAction extends UmbEntityActionBase<UmbDictionaryRepository> {
static styles = [UmbTextStyles];
constructor(host: UmbControllerHostElement, repositoryAlias: string, unique: string) {
super(host, repositoryAlias, unique);
constructor(host: UmbControllerHostElement, repositoryAlias: string, unique: string, entityType: string) {
super(host, repositoryAlias, unique, entityType);
}
async execute() {

View File

@@ -14,8 +14,8 @@ export default class UmbExportDictionaryEntityAction extends UmbEntityActionBase
#modalContext?: UmbModalManagerContext;
constructor(host: UmbControllerHostElement, repositoryAlias: string, unique: string) {
super(host, repositoryAlias, unique);
constructor(host: UmbControllerHostElement, repositoryAlias: string, unique: string, entityType: string) {
super(host, repositoryAlias, unique, entityType);
this.consumeContext(UMB_MODAL_MANAGER_CONTEXT, (instance) => {
this.#modalContext = instance;

View File

@@ -17,8 +17,8 @@ export default class UmbImportDictionaryEntityAction extends UmbEntityActionBase
#modalContext?: UmbModalManagerContext;
#treeStore?: UmbDictionaryTreeStore;
constructor(host: UmbControllerHostElement, repositoryAlias: string, unique: string) {
super(host, repositoryAlias, unique);
constructor(host: UmbControllerHostElement, repositoryAlias: string, unique: string, entityType: string) {
super(host, repositoryAlias, unique, entityType);
this.consumeContext(UMB_MODAL_MANAGER_CONTEXT, (instance) => {
this.#modalContext = instance;

View File

@@ -1,6 +1,5 @@
import { UMB_DICTIONARY_REPOSITORY_ALIAS } from '../repository/manifests.js';
import { UMB_DICTIONARY_ENTITY_TYPE, UMB_DICTIONARY_ROOT_ENTITY_TYPE } from '../entities.js';
import UmbReloadDictionaryEntityAction from './reload.action.js';
import { UMB_DICTIONARY_ENTITY_TYPE, UMB_DICTIONARY_ROOT_ENTITY_TYPE } from '../entity.js';
import UmbImportDictionaryEntityAction from './import/import.action.js';
import UmbExportDictionaryEntityAction from './export/export.action.js';
import UmbCreateDictionaryEntityAction from './create/create.action.js';
@@ -60,19 +59,6 @@ const entityActions: Array<ManifestEntityAction> = [
entityTypes: [UMB_DICTIONARY_ENTITY_TYPE, UMB_DICTIONARY_ROOT_ENTITY_TYPE],
},
},
{
type: 'entityAction',
alias: 'Umb.EntityAction.Dictionary.Reload',
name: 'Reload Dictionary Entity Action',
weight: 200,
api: UmbReloadDictionaryEntityAction,
meta: {
icon: 'icon-refresh',
label: 'Reload',
repositoryAlias: UMB_DICTIONARY_REPOSITORY_ALIAS,
entityTypes: [UMB_DICTIONARY_ENTITY_TYPE, UMB_DICTIONARY_ROOT_ENTITY_TYPE],
},
},
{
type: 'entityAction',
alias: 'Umb.EntityAction.Dictionary.Delete',

View File

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

View File

@@ -0,0 +1,5 @@
export const UMB_DICTIONARY_ROOT_ENTITY_TYPE = 'dictionary-root';
export const UMB_DICTIONARY_ENTITY_TYPE = 'dictionary-item';
export type UmbDictionaryEntityType = typeof UMB_DICTIONARY_ENTITY_TYPE;
export type UmbDictionaryRootEntityType = typeof UMB_DICTIONARY_ROOT_ENTITY_TYPE;

View File

@@ -1,4 +1,4 @@
import { UMB_DICTIONARY_ENTITY_TYPE } from '../entities.js';
import { UMB_DICTIONARY_ENTITY_TYPE } from '../entity.js';
import { UMB_DICTIONARY_TREE_ALIAS } from '../tree/index.js';
import type { ManifestTypes } from '@umbraco-cms/backoffice/extension-registry';

View File

@@ -1,4 +1,4 @@
import { UMB_DICTIONARY_ROOT_ENTITY_TYPE } from '../entities.js';
import { UMB_DICTIONARY_ROOT_ENTITY_TYPE } from '../entity.js';
import { UmbDictionaryTreeServerDataSource } from './dictionary-tree.server.data-source.js';
import type { UmbDictionaryTreeItemModel, UmbDictionaryTreeRootModel } from './types.js';
import { UMB_DICTIONARY_TREE_STORE_CONTEXT } from './dictionary-tree.store.js';

View File

@@ -1,6 +1,7 @@
import { UMB_DICTIONARY_ENTITY_TYPE, UMB_DICTIONARY_ROOT_ENTITY_TYPE } from '../entities.js';
import { UMB_DICTIONARY_ENTITY_TYPE, UMB_DICTIONARY_ROOT_ENTITY_TYPE } from '../entity.js';
import { UmbDictionaryTreeRepository } from './dictionary-tree.repository.js';
import { UmbDictionaryTreeStore } from './dictionary-tree.store.js';
import { manifests as reloadTreeItemChildrenManifests } from './reload-tree-item-children/manifests.js';
import type {
ManifestRepository,
ManifestTree,
@@ -45,4 +46,4 @@ const treeItem: ManifestTreeItem = {
},
};
export const manifests = [treeRepository, treeStore, tree, treeItem];
export const manifests = [treeRepository, treeStore, tree, treeItem, ...reloadTreeItemChildrenManifests];

View File

@@ -0,0 +1,20 @@
import { UMB_DICTIONARY_ROOT_ENTITY_TYPE, UMB_DICTIONARY_ENTITY_TYPE } from '../../entity.js';
import { UMB_DICTIONARY_REPOSITORY_ALIAS } from '../../repository/manifests.js';
import { UmbReloadTreeItemChildrenEntityAction } from '@umbraco-cms/backoffice/tree';
import { type ManifestEntityAction } from '@umbraco-cms/backoffice/extension-registry';
export const manifests: Array<ManifestEntityAction> = [
{
type: 'entityAction',
alias: 'Umb.EntityAction.Dictionary.Tree.ReloadTreeItemChildren',
name: 'Reload Dictionary Tree Item Children Entity Action',
weight: 10,
api: UmbReloadTreeItemChildrenEntityAction,
meta: {
icon: 'icon-refresh',
label: 'Reload children...',
repositoryAlias: UMB_DICTIONARY_REPOSITORY_ALIAS,
entityTypes: [UMB_DICTIONARY_ROOT_ENTITY_TYPE, UMB_DICTIONARY_ENTITY_TYPE],
},
},
];

View File

@@ -1,4 +1,4 @@
import { UMB_DICTIONARY_ROOT_ENTITY_TYPE } from './dictionary/entities.js';
import { UMB_DICTIONARY_ROOT_ENTITY_TYPE } from './dictionary/entity.js';
import type { ManifestDashboard, ManifestSection, ManifestTypes } from '@umbraco-cms/backoffice/extension-registry';
const sectionAlias = 'Umb.Section.Dictionary';

View File

@@ -8,8 +8,8 @@ import { UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal';
export class UmbCreateDataTypeEntityAction extends UmbEntityActionBase<UmbDocumentTypeDetailRepository> {
#modalManagerContext?: UmbModalManagerContext;
constructor(host: UmbControllerHostElement, repositoryAlias: string, unique: string) {
super(host, repositoryAlias, unique);
constructor(host: UmbControllerHostElement, repositoryAlias: string, unique: string, entityType: string) {
super(host, repositoryAlias, unique, entityType);
this.consumeContext(UMB_MODAL_MANAGER_CONTEXT, (instance) => {
this.#modalManagerContext = instance;
@@ -23,6 +23,7 @@ export class UmbCreateDataTypeEntityAction extends UmbEntityActionBase<UmbDocume
this.#modalManagerContext?.open(UMB_DOCUMENT_TYPE_CREATE_OPTIONS_MODAL, {
data: {
parentUnique: this.unique,
entityType: this.entityType,
},
});
}

View File

@@ -21,6 +21,7 @@ export class UmbDataTypeCreateOptionsModalElement extends UmbModalBaseElement<Um
// @ts-ignore
// TODO: allow null for entity actions. Some actions can be executed on the root item
this.data.parentUnique,
this.data.entityType,
);
}

View File

@@ -2,6 +2,7 @@ import { UmbModalToken } from '@umbraco-cms/backoffice/modal';
export interface UmbDocumentTypeCreateOptionsModalData {
parentUnique: string | null;
entityType: string;
}
export const UMB_DOCUMENT_TYPE_CREATE_OPTIONS_MODAL = new UmbModalToken<UmbDocumentTypeCreateOptionsModalData>(

View File

@@ -6,6 +6,7 @@ import {
import { UmbDocumentTypeTreeRepository } from './document-type-tree.repository.js';
import { UmbDocumentTypeTreeStore } from './document-type.tree.store.js';
import { manifests as folderManifests } from './folder/manifests.js';
import { manifests as reloadManifests } from './reload-tree-item-children/manifests.js';
import type {
ManifestRepository,
ManifestTree,
@@ -54,4 +55,4 @@ const treeItem: ManifestTreeItem = {
},
};
export const manifests = [treeRepository, treeStore, tree, treeItem, ...folderManifests];
export const manifests = [treeRepository, treeStore, tree, treeItem, ...folderManifests, ...reloadManifests];

View File

@@ -0,0 +1,28 @@
import {
UMB_DOCUMENT_TYPE_ROOT_ENTITY_TYPE,
UMB_DOCUMENT_TYPE_ENTITY_TYPE,
UMB_DOCUMENT_TYPE_FOLDER_ENTITY_TYPE,
} from '../../entity.js';
import { UMB_DOCUMENT_TYPE_DETAIL_REPOSITORY_ALIAS } from '../../repository/detail/manifests.js';
import { UmbReloadTreeItemChildrenEntityAction } from '@umbraco-cms/backoffice/tree';
import { type ManifestEntityAction } from '@umbraco-cms/backoffice/extension-registry';
export const manifests: Array<ManifestEntityAction> = [
{
type: 'entityAction',
alias: 'Umb.EntityAction.DocumentType.Tree.ReloadTreeItemChildren',
name: 'Reload Document Type Tree Item Children Entity Action',
weight: 10,
api: UmbReloadTreeItemChildrenEntityAction,
meta: {
icon: 'icon-refresh',
label: 'Reload children...',
repositoryAlias: UMB_DOCUMENT_TYPE_DETAIL_REPOSITORY_ALIAS,
entityTypes: [
UMB_DOCUMENT_TYPE_ROOT_ENTITY_TYPE,
UMB_DOCUMENT_TYPE_ENTITY_TYPE,
UMB_DOCUMENT_TYPE_FOLDER_ENTITY_TYPE,
],
},
},
];

View File

@@ -11,12 +11,12 @@ import type {
PropertyTypeContainerModelBaseModel,
} from '@umbraco-cms/backoffice/backend-api';
import { UMB_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/workspace';
import type { UmbRoute , UmbRouterSlotChangeEvent, UmbRouterSlotInitEvent } from '@umbraco-cms/backoffice/router';
import type { UmbRoute, UmbRouterSlotChangeEvent, UmbRouterSlotInitEvent } from '@umbraco-cms/backoffice/router';
import type { UmbWorkspaceViewElement } from '@umbraco-cms/backoffice/extension-registry';
import type { UmbConfirmModalData } from '@umbraco-cms/backoffice/modal';
import { UMB_CONFIRM_MODAL, UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import type { UmbSorterConfig} from '@umbraco-cms/backoffice/sorter';
import type { UmbSorterConfig } from '@umbraco-cms/backoffice/sorter';
import { UmbSorterController } from '@umbraco-cms/backoffice/sorter';
const SORTER_CONFIG: UmbSorterConfig<PropertyTypeContainerModelBaseModel> = {
@@ -216,7 +216,7 @@ export class UmbDocumentTypeWorkspaceViewEditElement extends UmbLitElement imple
if (!tabId) return;
this._workspaceContext?.structure.removeContainer(null, tabId);
this._tabsStructureHelper?.isOwnerContainer(tabId)
? window.history.replaceState(null, '', this._routerPath + this._routes[0]?.path ?? '/root')
? window.history.replaceState(null, '', this._routerPath + (this._routes[0]?.path ?? '/root'))
: '';
}
async #addTab() {

View File

@@ -3,8 +3,8 @@ import { UmbEntityActionBase } from '@umbraco-cms/backoffice/entity-action';
import type { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api';
export class UmbCreateDocumentBlueprintEntityAction extends UmbEntityActionBase<UmbDocumentRepository> {
constructor(host: UmbControllerHostElement, repositoryAlias: string, unique: string) {
super(host, repositoryAlias, unique);
constructor(host: UmbControllerHostElement, repositoryAlias: string, unique: string, entityType: string) {
super(host, repositoryAlias, unique, entityType);
}
async execute() {

View File

@@ -11,8 +11,8 @@ import {
export class UmbCreateDocumentEntityAction extends UmbEntityActionBase<UmbDocumentRepository> {
#modalContext?: UmbModalManagerContext;
constructor(host: UmbControllerHostElement, repositoryAlias: string, unique: string) {
super(host, repositoryAlias, unique);
constructor(host: UmbControllerHostElement, repositoryAlias: string, unique: string, entityType: string) {
super(host, repositoryAlias, unique, entityType);
this.consumeContext(UMB_MODAL_MANAGER_CONTEXT, (instance) => {
this.#modalContext = instance;

View File

@@ -3,8 +3,8 @@ import { UmbEntityActionBase } from '@umbraco-cms/backoffice/entity-action';
import type { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api';
export class UmbDocumentCultureAndHostnamesEntityAction extends UmbEntityActionBase<UmbDocumentRepository> {
constructor(host: UmbControllerHostElement, repositoryAlias: string, unique: string) {
super(host, repositoryAlias, unique);
constructor(host: UmbControllerHostElement, repositoryAlias: string, unique: string, entityType: string) {
super(host, repositoryAlias, unique, entityType);
}
async execute() {

View File

@@ -11,8 +11,8 @@ import {
export class UmbDocumentPermissionsEntityAction extends UmbEntityActionBase<UmbDocumentRepository> {
#modalManagerContext?: UmbModalManagerContext;
constructor(host: UmbControllerHostElement, repositoryAlias: string, unique: string) {
super(host, repositoryAlias, unique);
constructor(host: UmbControllerHostElement, repositoryAlias: string, unique: string, entityType: string) {
super(host, repositoryAlias, unique, entityType);
this.consumeContext(UMB_MODAL_MANAGER_CONTEXT, (instance) => {
this.#modalManagerContext = instance;

View File

@@ -3,8 +3,8 @@ import { UmbEntityActionBase } from '@umbraco-cms/backoffice/entity-action';
import type { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api';
export class UmbDocumentPublicAccessEntityAction extends UmbEntityActionBase<UmbDocumentRepository> {
constructor(host: UmbControllerHostElement, repositoryAlias: string, unique: string) {
super(host, repositoryAlias, unique);
constructor(host: UmbControllerHostElement, repositoryAlias: string, unique: string, entityType: string) {
super(host, repositoryAlias, unique, entityType);
}
async execute() {

View File

@@ -3,8 +3,8 @@ import { UmbEntityActionBase } from '@umbraco-cms/backoffice/entity-action';
import type { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api';
export class UmbPublishDocumentEntityAction extends UmbEntityActionBase<UmbDocumentRepository> {
constructor(host: UmbControllerHostElement, repositoryAlias: string, unique: string) {
super(host, repositoryAlias, unique);
constructor(host: UmbControllerHostElement, repositoryAlias: string, unique: string, entityType: string) {
super(host, repositoryAlias, unique, entityType);
}
async execute() {

View File

@@ -3,8 +3,8 @@ import { UmbEntityActionBase } from '@umbraco-cms/backoffice/entity-action';
import type { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api';
export class UmbRollbackDocumentEntityAction extends UmbEntityActionBase<UmbDocumentRepository> {
constructor(host: UmbControllerHostElement, repositoryAlias: string, unique: string) {
super(host, repositoryAlias, unique);
constructor(host: UmbControllerHostElement, repositoryAlias: string, unique: string, entityType: string) {
super(host, repositoryAlias, unique, entityType);
}
async execute() {

View File

@@ -3,8 +3,8 @@ import { UmbEntityActionBase } from '@umbraco-cms/backoffice/entity-action';
import type { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api';
export class UmbUnpublishDocumentEntityAction extends UmbEntityActionBase<UmbDocumentRepository> {
constructor(host: UmbControllerHostElement, repositoryAlias: string, unique: string) {
super(host, repositoryAlias, unique);
constructor(host: UmbControllerHostElement, repositoryAlias: string, unique: string, entityType: string) {
super(host, repositoryAlias, unique, entityType);
}
async execute() {

Some files were not shown because too many files have changed in this diff Show More