diff --git a/src/Umbraco.Web.UI.Client/src/libs/extension-api/controller/base-extensions-initializer.controller.ts b/src/Umbraco.Web.UI.Client/src/libs/extension-api/controller/base-extensions-initializer.controller.ts index e7492a6a8dd7..57258cf300a5 100644 --- a/src/Umbraco.Web.UI.Client/src/libs/extension-api/controller/base-extensions-initializer.controller.ts +++ b/src/Umbraco.Web.UI.Client/src/libs/extension-api/controller/base-extensions-initializer.controller.ts @@ -177,6 +177,7 @@ export abstract class UmbBaseExtensionsInitializer< this.#exposedPermittedExts = approvedExtensions; + // TODO: Option to first resolve once all have responded are completed. [NL] if (this.#exposedPermittedExts.length > 0) { this.#promiseResolvers.forEach((x) => x()); this.#promiseResolvers = []; diff --git a/src/Umbraco.Web.UI.Client/src/libs/extension-api/functions/create-extension-api.function.ts b/src/Umbraco.Web.UI.Client/src/libs/extension-api/functions/create-extension-api.function.ts index a168758d3ab6..82006ce8f461 100644 --- a/src/Umbraco.Web.UI.Client/src/libs/extension-api/functions/create-extension-api.function.ts +++ b/src/Umbraco.Web.UI.Client/src/libs/extension-api/functions/create-extension-api.function.ts @@ -5,10 +5,10 @@ import type { UmbApiConstructorArgumentsMethodType } from './types.js'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; /** - * - * @param host - * @param manifest - * @param constructorArgs + * @param {UmbControllerHost} host - The controller host for this controller to be appended to + * @param {ManifestApi} manifest - The manifest of the extension + * @param {Array | UmbApiConstructorArgumentsMethodType} constructorArgs - The constructor arguments to pass to the API class + * @returns {Promise} - The API class instance */ export async function createExtensionApi( host: UmbControllerHost, diff --git a/src/Umbraco.Web.UI.Client/src/libs/extension-api/registry/extension.registry.ts b/src/Umbraco.Web.UI.Client/src/libs/extension-api/registry/extension.registry.ts index 037f2e7f5b2b..15d828a62415 100644 --- a/src/Umbraco.Web.UI.Client/src/libs/extension-api/registry/extension.registry.ts +++ b/src/Umbraco.Web.UI.Client/src/libs/extension-api/registry/extension.registry.ts @@ -467,7 +467,9 @@ export class UmbExtensionRegistry< return []; } const kinds = this._kinds.getValue(); - return exts.map((ext) => (ext?.kind ? (this.#mergeExtensionWithKinds([ext, kinds]) ?? ext) : ext)); + return exts + .map((ext) => (ext?.kind ? (this.#mergeExtensionWithKinds([ext, kinds]) ?? ext) : ext)) + .sort(sortExtensions); } /** diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/context/block-grid-entries.context.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/context/block-grid-entries.context.ts index 3c1d1fee5fb4..bb1fe4cc3702 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/context/block-grid-entries.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/context/block-grid-entries.context.ts @@ -436,7 +436,7 @@ export class UmbBlockGridEntriesContext originData?: UmbBlockGridWorkspaceOriginData, ) { await this._retrieveManager; - return this._manager?.create(contentElementTypeKey, partialLayoutEntry, originData); + return await this._manager?.createWithPresets(contentElementTypeKey, partialLayoutEntry, originData); } // insert Block? diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/context/block-grid-manager.context.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/context/block-grid-manager.context.ts index ed58c645e75f..7fb16cd57374 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/context/block-grid-manager.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/context/block-grid-manager.context.ts @@ -10,7 +10,7 @@ import { import { transformServerPathToClientPath } from '@umbraco-cms/backoffice/utils'; import { UmbBlockManagerContext } from '@umbraco-cms/backoffice/block'; import { UMB_APP_CONTEXT } from '@umbraco-cms/backoffice/app'; -import type { UmbBlockDataModel } from '@umbraco-cms/backoffice/block'; +import type { UmbBlockDataModel, UmbBlockDataObjectModel } from '@umbraco-cms/backoffice/block'; import type { UmbBlockTypeGroup } from '@umbraco-cms/backoffice/block-type'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import type { UmbNumberRangeValueType } from '@umbraco-cms/backoffice/models'; @@ -91,15 +91,30 @@ export class UmbBlockGridManagerContext< this.#serverUrl = appContext.getServerUrl(); }); } - + /** + * @deprecated Use createWithPresets instead. Will be removed in v.17. + */ create( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + contentElementTypeKey: string, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + partialLayoutEntry?: Omit, + // This property is used by some implementations, but not used in this. Do not remove. [NL] + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _originData?: UmbBlockGridWorkspaceOriginData, + ) { + throw new Error('Method deparecated use createWithPresets'); + return {} as UmbBlockDataObjectModel; + } + + async createWithPresets( contentElementTypeKey: string, partialLayoutEntry?: Omit, // This property is used by some implementations, but not used in this. // eslint-disable-next-line @typescript-eslint/no-unused-vars originData?: UmbBlockGridWorkspaceOriginData, ) { - return super._createBlockData(contentElementTypeKey, partialLayoutEntry); + return await super._createBlockData(contentElementTypeKey, partialLayoutEntry); } /** diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-list/context/block-list-entries.context.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-list/context/block-list-entries.context.ts index 9cc4369ecd5d..e625f4d29899 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-list/context/block-list-entries.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-list/context/block-list-entries.context.ts @@ -168,7 +168,7 @@ export class UmbBlockListEntriesContext extends UmbBlockEntriesContext< originData?: UmbBlockListWorkspaceOriginData, ) { await this._retrieveManager; - return this._manager?.create(contentElementTypeKey, partialLayoutEntry, originData); + return await this._manager?.createWithPresets(contentElementTypeKey, partialLayoutEntry, originData); } // insert Block? diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-list/context/block-list-manager.context.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-list/context/block-list-manager.context.ts index ccc98f903ac7..6dd0267cb3f2 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-list/context/block-list-manager.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-list/context/block-list-manager.context.ts @@ -2,7 +2,7 @@ import type { UmbBlockListLayoutModel, UmbBlockListTypeModel } from '../types.js import type { UmbBlockListWorkspaceOriginData } from '../index.js'; import type { UmbBlockDataModel } from '../../block/types.js'; import { UmbBooleanState } from '@umbraco-cms/backoffice/observable-api'; -import { UmbBlockManagerContext } from '@umbraco-cms/backoffice/block'; +import { UmbBlockManagerContext, type UmbBlockDataObjectModel } from '@umbraco-cms/backoffice/block'; /** * A implementation of the Block Manager specifically for the Block List Editor. @@ -21,14 +21,29 @@ export class UmbBlockListManagerContext< return this.#inlineEditingMode.getValue(); } + /** + * @deprecated Use createWithPresets instead. Will be removed in v.17. + */ create( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + contentElementTypeKey: string, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + partialLayoutEntry?: Omit, + // This property is used by some implementations, but not used in this. Do not remove. [NL] + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _originData?: UmbBlockListWorkspaceOriginData, + ) { + throw new Error('Method deparecated use createWithPresets'); + return {} as UmbBlockDataObjectModel; + } + async createWithPresets( contentElementTypeKey: string, partialLayoutEntry?: Omit, // This property is used by some implementations, but not used in this. Do not remove. [NL] // eslint-disable-next-line @typescript-eslint/no-unused-vars _originData?: UmbBlockListWorkspaceOriginData, ) { - return super._createBlockData(contentElementTypeKey, partialLayoutEntry); + return await super._createBlockData(contentElementTypeKey, partialLayoutEntry); } insert( diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-rte/context/block-rte-entries.context.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-rte/context/block-rte-entries.context.ts index 492ab213fa65..1724dd6b1ebe 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-rte/context/block-rte-entries.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-rte/context/block-rte-entries.context.ts @@ -111,7 +111,7 @@ export class UmbBlockRteEntriesContext extends UmbBlockEntriesContext< originData?: UmbBlockRteWorkspaceOriginData, ) { await this._retrieveManager; - return this._manager?.create(contentElementTypeKey, partialLayoutEntry, originData); + return await this._manager?.createWithPresets(contentElementTypeKey, partialLayoutEntry, originData); } // insert Block? diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-rte/context/block-rte-manager.context.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-rte/context/block-rte-manager.context.ts index c56cfeebd5f1..30458896c18e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-rte/context/block-rte-manager.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-rte/context/block-rte-manager.context.ts @@ -1,7 +1,7 @@ import type { UmbBlockRteWorkspaceOriginData } from '../workspace/block-rte-workspace.modal-token.js'; import type { UmbBlockRteLayoutModel, UmbBlockRteTypeModel } from '../types.js'; import type { UmbBlockDataModel } from '../../block/types.js'; -import { UmbBlockManagerContext } from '@umbraco-cms/backoffice/block'; +import { UmbBlockManagerContext, type UmbBlockDataObjectModel } from '@umbraco-cms/backoffice/block'; import '../components/block-rte-entry/index.js'; @@ -18,14 +18,29 @@ export class UmbBlockRteManagerContext< this._layouts.remove(contentKeys); } + /** + * @deprecated Use createWithPresets instead. Will be removed in v.17. + */ create( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + contentElementTypeKey: string, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + partialLayoutEntry?: Omit, + // This property is used by some implementations, but not used in this. Do not remove. [NL] + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _originData?: UmbBlockRteWorkspaceOriginData, + ) { + throw new Error('Method deparecated use createWithPresets'); + return {} as UmbBlockDataObjectModel; + } + async createWithPresets( contentElementTypeKey: string, partialLayoutEntry?: Omit, // This property is used by some implementations, but not used in this, do not remove. [NL] // eslint-disable-next-line @typescript-eslint/no-unused-vars _originData?: UmbBlockRteWorkspaceOriginData, ) { - const data = super._createBlockData(contentElementTypeKey, partialLayoutEntry); + const data = await super._createBlockData(contentElementTypeKey, partialLayoutEntry); // Find block type. const blockType = this.getBlockTypes().find((x) => x.contentElementTypeKey === contentElementTypeKey); diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block/context/block-entry.context.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block/context/block-entry.context.ts index 7fd3f5248933..7bea42e67b3e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block/context/block-entry.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block/context/block-entry.context.ts @@ -398,11 +398,11 @@ export abstract class UmbBlockEntryContext< this.observe( observeMultiple([this.settingsKey, this.blockType]), - ([settingsKey, blockType]) => { + async ([settingsKey, blockType]) => { if (!this.#contentKey || settingsKey === undefined || !blockType) return; if (settingsKey == null && blockType.settingsElementTypeKey) { // We have a settings ElementType in config but not in data, so lets create the scaffold for that: [NL] - const settingsData = this._manager!.createBlockSettingsData(blockType.contentElementTypeKey); // Yes its on purpose we use the contentElementTypeKey here, as this is our identifier for a BlockType. [NL] + const settingsData = await this._manager!.createBlockSettingsData(blockType.contentElementTypeKey); // Yes its on purpose we use the contentElementTypeKey here, as this is our identifier for a BlockType. [NL] this._manager?.setOneSettings(settingsData); this._layout.update({ settingsKey: settingsData.key } as Partial); } else if (settingsKey && blockType.settingsElementTypeKey === undefined) { diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block/context/block-manager.context.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block/context/block-manager.context.ts index a679c1cd815c..2ed85ca7c095 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block/context/block-manager.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block/context/block-manager.context.ts @@ -18,6 +18,13 @@ import type { UmbPropertyEditorConfigCollection } from '@umbraco-cms/backoffice/ import type { UmbVariantId } from '@umbraco-cms/backoffice/variant'; import type { UmbBlockTypeBaseModel } from '@umbraco-cms/backoffice/block-type'; import { UmbReadOnlyVariantStateManager } from '@umbraco-cms/backoffice/utils'; +import { + UmbPropertyValuePresetVariantBuilderController, + type UmbPropertyTypePresetModel, + type UmbPropertyTypePresetModelTypeModel, +} from '@umbraco-cms/backoffice/property'; +import { UMB_APP_LANGUAGE_CONTEXT } from '@umbraco-cms/backoffice/language'; +import { UmbDataTypeDetailRepository } from '@umbraco-cms/backoffice/data-type'; export type UmbBlockDataObjectModel = { layout: LayoutEntryType; @@ -317,13 +324,25 @@ export abstract class UmbBlockManagerContext< ); } + /** + * @deprecated Use `createWithPresets` instead. Which is Async. Will be removed in v.17 + * @param contentElementTypeKey + * @param partialLayoutEntry + * @param originData + */ abstract create( contentElementTypeKey: string, partialLayoutEntry?: Omit, originData?: BlockOriginDataType, ): UmbBlockDataObjectModel | undefined; - public createBlockSettingsData(contentElementTypeKey: string) { + abstract createWithPresets( + contentElementTypeKey: string, + partialLayoutEntry?: Omit, + originData?: BlockOriginDataType, + ): Promise | undefined>; + + public async createBlockSettingsData(contentElementTypeKey: string) { const blockType = this.#blockTypes.value.find((x) => x.contentElementTypeKey === contentElementTypeKey); if (!blockType) { throw new Error(`Cannot create block settings, missing block type for ${contentElementTypeKey}`); @@ -332,6 +351,8 @@ export abstract class UmbBlockManagerContext< throw new Error(`Cannot create block settings, missing settings element type for ${contentElementTypeKey}`); } + // TODO: Handle presets here [NL] + return { key: UmbId.new(), contentTypeKey: blockType.settingsElementTypeKey, @@ -339,15 +360,71 @@ export abstract class UmbBlockManagerContext< }; } - protected _createBlockElementData(key: string, elementTypeKey: string) { + protected async _createBlockElementData(key: string, contentTypeKey: string) { + // + const appLanguage = await this.getContext(UMB_APP_LANGUAGE_CONTEXT); + + const contentStructure = this.getStructure(contentTypeKey); + if (!contentStructure) { + throw new Error(`Cannot create Preset for Block, missing content structure for ${contentTypeKey}`); + } + + // Set culture and segment for all values: + const cutlures = contentStructure.variesByCulture ? await appLanguage.getCultures() : []; + if (cutlures.length === 0) { + throw new Error('Could not retrieve app cultures.'); + } + // TODO: Receive the segments from somewhere. [NL] + const segments: Array | undefined = contentStructure.variesBySegment ? [] : undefined; + + const repo = new UmbDataTypeDetailRepository(this); + + const propertyTypes = await contentStructure.getContentTypeProperties(); + const valueDefinitions = await Promise.all( + propertyTypes.map(async (property) => { + // TODO: Implement caching for data-type requests. [NL] + const dataType = (await repo.requestByUnique(property.dataType.unique)).data; + // This means if its not loaded this will never resolve and the error below will never happen. + if (!dataType) { + throw new Error(`DataType of "${property.dataType.unique}" not found.`); + } + if (!dataType.editorUiAlias) { + throw new Error(`DataType of "${property.dataType.unique}" did not have a editorUiAlias.`); + } + + return { + alias: property.alias, + propertyEditorUiAlias: dataType.editorUiAlias, + propertyEditorSchemaAlias: dataType.editorAlias, + config: dataType.values, + typeArgs: { + variesByCulture: property.variesByCulture, + variesBySegment: property.variesBySegment, + } as UmbPropertyTypePresetModelTypeModel, + } as UmbPropertyTypePresetModel; + }), + ); + + const controller = new UmbPropertyValuePresetVariantBuilderController(this); + controller.setCultures(cutlures); + if (segments) { + controller.setSegments(segments); + } + const values = await controller.create(valueDefinitions); + + // Set culture and segment for all values: + return { - key: key, - contentTypeKey: elementTypeKey, - values: [], + key, + contentTypeKey, + values, }; } - protected _createBlockData(contentElementTypeKey: string, partialLayoutEntry?: Omit) { + protected async _createBlockData( + contentElementTypeKey: string, + partialLayoutEntry?: Omit, + ) { // Find block type. const blockType = this.#blockTypes.value.find((x) => x.contentElementTypeKey === contentElementTypeKey); if (!blockType) { @@ -360,12 +437,12 @@ export abstract class UmbBlockManagerContext< ...(partialLayoutEntry as Partial), } as BlockLayoutType; - const content = this._createBlockElementData(layout.contentKey, contentElementTypeKey); + const content = await this._createBlockElementData(layout.contentKey, contentElementTypeKey); let settings: UmbBlockDataModel | undefined = undefined; if (blockType.settingsElementTypeKey) { layout.settingsKey = UmbId.new(); - settings = this._createBlockElementData(layout.settingsKey, blockType.settingsElementTypeKey); + settings = await this._createBlockElementData(layout.settingsKey, blockType.settingsElementTypeKey); } return { diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/block-element-manager.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/block-element-manager.ts index bac6c7d5bd8b..4a5c0ab0c205 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/block-element-manager.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/block-element-manager.ts @@ -133,10 +133,7 @@ export class UmbBlockElementManager extends UmbControllerBase { - #init!: Promise; + #initResolver?: () => void; + #init = new Promise((resolve) => { + this.#initResolver = resolve; + }); #repository?: UmbDetailRepository; #initRepositoryResolver?: () => void; @@ -48,6 +51,11 @@ export class UmbContentTypeStructureManager< } }); + async whenLoaded() { + await this.#init; + return true; + } + #ownerContentTypeUnique?: string; #contentTypeObservers = new Array(); @@ -68,6 +76,9 @@ export class UmbContentTypeStructureManager< // Notice this may need to use getValue to avoid resetting it self. [NL] return contentTypes.flatMap((x) => x.properties ?? []); }); + async getContentTypeProperties() { + return await this.observe(this.contentTypeProperties).asPromise(); + } readonly contentTypeDataTypeUniques = this.#contentTypes.asObservablePart((contentTypes) => { // Notice this may need to use getValue to avoid resetting it self. [NL] return contentTypes @@ -122,16 +133,17 @@ export class UmbContentTypeStructureManager< * @returns {Promise} - Promise resolved */ public async loadType(unique?: string) { - //if (!unique) return; - //if (this.#ownerContentTypeUnique === unique) return; + if (this.#ownerContentTypeUnique === unique) { + // Its the same, but we do not know if its done loading jet, so we will wait for the load promise to finish. [NL] + await this.#init; + return; + } this.#clear(); - this.#ownerContentTypeUnique = unique; - - const promise = this.#loadType(unique); - this.#init = promise; - await this.#init; - return promise; + if (!unique) return; + const result = await this.#loadType(unique); + this.#initResolver?.(); + return result; } public async createScaffold(preset?: Partial) { @@ -145,6 +157,7 @@ export class UmbContentTypeStructureManager< // Add the new content type to the list of content types, this holds our draft state of this scaffold. this.#contentTypes.appendOne(data); + this.#initResolver?.(); return { data }; } @@ -776,6 +789,9 @@ export class UmbContentTypeStructureManager< } #clear() { + this.#init = new Promise((resolve) => { + this.#initResolver = resolve; + }); this.#contentTypes.setValue([]); this.#contentTypeObservers.forEach((observer) => observer.destroy()); this.#contentTypeObservers = []; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/content/workspace/content-detail-workspace-base.ts b/src/Umbraco.Web.UI.Client/src/packages/core/content/workspace/content-detail-workspace-base.ts index 4fecb6c53689..363a73bdf3d8 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/content/workspace/content-detail-workspace-base.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/content/workspace/content-detail-workspace-base.ts @@ -25,7 +25,7 @@ import { type UmbEntityVariantOptionModel, } from '@umbraco-cms/backoffice/variant'; import { UmbDeprecation, UmbReadOnlyVariantStateManager } from '@umbraco-cms/backoffice/utils'; -import { UmbDataTypeItemRepositoryManager } from '@umbraco-cms/backoffice/data-type'; +import { UmbDataTypeDetailRepository, UmbDataTypeItemRepositoryManager } from '@umbraco-cms/backoffice/data-type'; import { appendToFrozenArray, mergeObservables, UmbArrayState } from '@umbraco-cms/backoffice/observable-api'; import { UmbLanguageCollectionRepository, type UmbLanguageDetailModel } from '@umbraco-cms/backoffice/language'; import type { Observable } from '@umbraco-cms/backoffice/external/rxjs'; @@ -47,6 +47,11 @@ import { UmbRequestReloadStructureForEntityEvent, } from '@umbraco-cms/backoffice/entity-action'; import type { ClassConstructor } from '@umbraco-cms/backoffice/extension-api'; +import { + UmbPropertyValuePresetVariantBuilderController, + type UmbPropertyTypePresetModel, + type UmbPropertyTypePresetModelTypeModel, +} from '@umbraco-cms/backoffice/property'; export interface UmbContentDetailWorkspaceContextArgs< DetailModelType extends UmbContentDetailModel, @@ -60,6 +65,7 @@ export interface UmbContentDetailWorkspaceContextArgs< contentValidationRepository?: ClassConstructor>; skipValidationOnSubmit?: boolean; contentVariantScaffold: VariantModelType; + contentTypePropertyName: string; saveModalToken?: UmbModalToken, UmbContentVariantPickerValue>; } @@ -145,6 +151,7 @@ export abstract class UmbContentDetailWorkspaceContextBase< #validationRepository?: UmbContentValidationRepository; #saveModalToken?: UmbModalToken, UmbContentVariantPickerValue>; + #contentTypePropertyName: string; constructor( host: UmbControllerHost, @@ -159,6 +166,7 @@ export abstract class UmbContentDetailWorkspaceContextBase< this._data.setVariantScaffold(args.contentVariantScaffold); this.#saveModalToken = args.saveModalToken; + this.#contentTypePropertyName = args.contentTypePropertyName; const contentTypeDetailRepository = new args.contentTypeDetailRepository(this); this.#validationRepositoryClass = args.contentValidationRepository; @@ -245,6 +253,56 @@ export abstract class UmbContentDetailWorkspaceContextBase< this.#languages.setValue(data?.items ?? []); } + protected override async _scaffoldProcessData(data: DetailModelType): Promise { + // Load the content type structure, usually this comes from the data, but in this case we are making the data, and we need this to be able to complete the data. [NL] + await this.structure.loadType((data as any)[this.#contentTypePropertyName].unique); + + // Set culture and segment for all values: + const cutlures = this.#languages.getValue().map((x) => x.unique); + + if (this.structure.variesBySegment) { + console.warn('Segments are not yet implemented for preset'); + } + const segments: Array | undefined = this.structure.variesBySegment ? [] : undefined; + + const repo = new UmbDataTypeDetailRepository(this); + + const propertyTypes = await this.structure.getContentTypeProperties(); + const valueDefinitions = await Promise.all( + propertyTypes.map(async (property) => { + // TODO: Implement caching for data-type requests. [NL] + const dataType = (await repo.requestByUnique(property.dataType.unique)).data; + // This means if its not loaded this will never resolve and the error below will never happen. + if (!dataType) { + throw new Error(`DataType of "${property.dataType.unique}" not found.`); + } + if (!dataType.editorUiAlias) { + throw new Error(`DataType of "${property.dataType.unique}" did not have a editorUiAlias.`); + } + + return { + alias: property.alias, + propertyEditorUiAlias: dataType.editorUiAlias, + propertyEditorSchemaAlias: dataType.editorAlias, + config: dataType.values, + typeArgs: { + variesByCulture: property.variesByCulture, + variesBySegment: property.variesBySegment, + } as UmbPropertyTypePresetModelTypeModel, + } as UmbPropertyTypePresetModel; + }), + ); + + const controller = new UmbPropertyValuePresetVariantBuilderController(this); + controller.setCultures(cutlures); + if (segments) { + controller.setSegments(segments); + } + data.values = await controller.create(valueDefinitions); + + return data; + } + /** * Get the name of a variant * @param {UmbVariantId } [variantId] - The variant id @@ -672,20 +730,26 @@ export abstract class UmbContentDetailWorkspaceContextBase< throw new Error('Error creating content'); } - this._data.setPersisted(data); - - const currentData = this._data.getCurrent(); - const variantIdsIncludingInvariant = [...variantIds, UmbVariantId.CreateInvariant()]; - // Retrieve a data set which only contains updates from the selected variants + invariant. [NL] + // Only update the variants that was chosen to be saved: + const persistedData = this._data.getCurrent(); + const newPersistedData = await new UmbMergeContentVariantDataController(this).process( + persistedData, + data, + variantIds, + variantIdsIncludingInvariant, + ); + this._data.setPersisted(newPersistedData); + + // Only update the variants that was chosen to be saved: + const currentData = this._data.getCurrent(); const newCurrentData = await new UmbMergeContentVariantDataController(this).process( currentData, data, variantIds, variantIdsIncludingInvariant, ); - this._data.setCurrent(newCurrentData); const eventContext = await this.getContext(UMB_ACTION_EVENT_CONTEXT); @@ -707,12 +771,20 @@ export abstract class UmbContentDetailWorkspaceContextBase< throw new Error('Error saving content'); } - this._data.setPersisted(data); - // TODO: Only update the variants that was chosen to be saved: - const currentData = this._data.getCurrent(); - const variantIdsIncludingInvariant = [...variantIds, UmbVariantId.CreateInvariant()]; + // Only update the variants that was chosen to be saved: + const persistedData = this._data.getCurrent(); + const newPersistedData = await new UmbMergeContentVariantDataController(this).process( + persistedData, + data, + variantIds, + variantIdsIncludingInvariant, + ); + this._data.setPersisted(newPersistedData); + + // Only update the variants that was chosen to be saved: + const currentData = this._data.getCurrent(); const newCurrentData = await new UmbMergeContentVariantDataController(this).process( currentData, data, diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/config/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/config/index.ts index be254977addb..40cba6de6ff1 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/config/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/config/index.ts @@ -1,2 +1 @@ -export type * from './property-editor-config.type.js'; export * from './property-editor-config-collection.class.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/config/property-editor-config-collection.class.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/config/property-editor-config-collection.class.ts index af5c4448a63f..bda33b19f9ef 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/config/property-editor-config-collection.class.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/config/property-editor-config-collection.class.ts @@ -1,4 +1,4 @@ -import type { UmbPropertyEditorConfigProperty, UmbPropertyEditorConfig } from '../index.js'; +import type { UmbPropertyEditorConfigProperty, UmbPropertyEditorConfig } from '../types.js'; import type { DataTypePropertyPresentationModel } from '@umbraco-cms/backoffice/external/backend-api'; /** diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/config/types.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/config/types.ts new file mode 100644 index 000000000000..516b77df2455 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/config/types.ts @@ -0,0 +1 @@ +export type * from './property-editor-config.type.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/extensions/property-editor.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/extensions/property-editor.extension.ts index c9f0f82d5e70..2ef67d3fa90c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/extensions/property-editor.extension.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/extensions/property-editor.extension.ts @@ -42,6 +42,7 @@ export interface MetaPropertyEditorSchema { export interface PropertyEditorSettings { properties: PropertyEditorSettingsProperty[]; // default data is kept separate from the properties, to give the ability for Property Editor UI to overwrite default values for the property editor settings. + // TODO: Deprecate defaultData in the future and rename to preset. [NL] defaultData?: PropertyEditorSettingsDefaultData[]; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/index.ts index 116b486efdc8..6f36bd603767 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/index.ts @@ -3,4 +3,4 @@ export * from './config/index.js'; export * from './constants.js'; export * from './events/index.js'; export * from './ui-picker-modal/index.js'; -export type * from './extensions/types.js'; +export type * from './types.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/types.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/types.ts new file mode 100644 index 000000000000..17fbe12e7a43 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/types.ts @@ -0,0 +1,2 @@ +export type * from './config/types.js'; +export type * from './extensions/types.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property/index.ts index 3e9e25957511..e5d236cee9be 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property/index.ts @@ -1,5 +1,6 @@ export * from './components/index.js'; export * from './conditions/index.js'; -export * from './controllers/property-value-clone.controller.js'; export * from './property-dataset/index.js'; +export * from './property-value-cloner/property-value-clone.controller.js'; +export * from './property-value-preset/index.js'; export type * from './types.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property/controllers/property-value-clone.controller.test.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property/property-value-cloner/property-value-clone.controller.test.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/core/property/controllers/property-value-clone.controller.test.ts rename to src/Umbraco.Web.UI.Client/src/packages/core/property/property-value-cloner/property-value-clone.controller.test.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property/controllers/property-value-clone.controller.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property/property-value-cloner/property-value-clone.controller.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/core/property/controllers/property-value-clone.controller.ts rename to src/Umbraco.Web.UI.Client/src/packages/core/property/property-value-cloner/property-value-clone.controller.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property/property-value-preset/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property/property-value-preset/index.ts new file mode 100644 index 000000000000..4218dccc1c42 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property/property-value-preset/index.ts @@ -0,0 +1,4 @@ +export type * from './property-value-preset.extension.js'; +export * from './property-value-preset-builder.controller.js'; +export * from './property-value-preset-variant-builder.controller.js'; +export type * from './types.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property/property-value-preset/property-value-preset-builder.controller.test.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property/property-value-preset/property-value-preset-builder.controller.test.ts new file mode 100644 index 000000000000..55a274478504 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property/property-value-preset/property-value-preset-builder.controller.test.ts @@ -0,0 +1,325 @@ +import { expect } from '@open-wc/testing'; +import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; +import { customElement } from '@umbraco-cms/backoffice/external/lit'; +import { UmbControllerHostElementMixin } from '@umbraco-cms/backoffice/controller-api'; +import type { + ManifestPropertyValuePreset, + UmbPropertyTypePresetModel, + UmbPropertyTypePresetWithSchemaAliasModel, + UmbPropertyValuePreset, +} from './types.js'; +import type { UmbPropertyEditorConfig } from '@umbraco-cms/backoffice/property-editor'; +import { UmbPropertyValuePresetBuilderController } from './property-value-preset-builder.controller.js'; + +@customElement('umb-test-controller-host') +export class UmbTestControllerHostElement extends UmbControllerHostElementMixin(HTMLElement) {} + +// TODO: Write test with config, investigate oppertunity to retrieve Config Object, for an simpler DX. [NL] + +// Test with async APIs, espcially where the first one is slower than the last one. +export class TestPropertyValuePresetFirstApi implements UmbPropertyValuePreset { + async processValue(value: undefined | string, config: UmbPropertyEditorConfig) { + return value ? value + '_first' : 'first'; + } + + destroy(): void {} +} + +export class TestPropertyValuePresetSecondApi implements UmbPropertyValuePreset { + async processValue(value: undefined | string, config: UmbPropertyEditorConfig) { + return value ? value + '_second' : 'second'; + } + + destroy(): void {} +} + +export class TestPropertyValuePresetAsyncApi implements UmbPropertyValuePreset { + async processValue(value: undefined | string, config: UmbPropertyEditorConfig) { + await new Promise((resolve) => setTimeout(resolve, 10)); + return value ? value + '_async' : 'async'; + } + + destroy(): void {} +} + +describe('UmbPropertyValuePresetBuilderController', () => { + describe('Create with a single preset', () => { + beforeEach(async () => { + const manifestFirstPreset: ManifestPropertyValuePreset = { + type: 'propertyValuePreset', + name: 'test-preset-1', + alias: 'Umb.Test.Preset.1', + api: TestPropertyValuePresetFirstApi, + forPropertyEditorUiAlias: 'test-editor-ui', + }; + + umbExtensionsRegistry.register(manifestFirstPreset); + }); + afterEach(async () => { + umbExtensionsRegistry.unregister('Umb.Test.Preset.1'); + }); + + it('creates value', async () => { + const ctrlHost = new UmbTestControllerHostElement(); + const ctrl = new UmbPropertyValuePresetBuilderController(ctrlHost); + + const propertyTypes: Array = [ + { + alias: 'test', + propertyEditorUiAlias: 'test-editor-ui', + config: [], + typeArgs: {}, + }, + ]; + + const result = await ctrl.create(propertyTypes); + + expect(result.length).to.be.equal(1); + expect(result[0]?.value).to.be.equal('first'); + }); + }); + + describe('Create with a two presets', () => { + beforeEach(async () => { + const manifestFirstPreset: ManifestPropertyValuePreset = { + type: 'propertyValuePreset', + name: 'test-preset-1', + alias: 'Umb.Test.Preset.1', + weight: 20, + api: TestPropertyValuePresetFirstApi, + forPropertyEditorUiAlias: 'test-editor-ui', + }; + + const manifestSecondPreset: ManifestPropertyValuePreset = { + type: 'propertyValuePreset', + name: 'test-preset-2', + alias: 'Umb.Test.Preset.2', + weight: 10, + api: TestPropertyValuePresetSecondApi, + forPropertyEditorUiAlias: 'test-editor-ui', + }; + + umbExtensionsRegistry.register(manifestSecondPreset); + umbExtensionsRegistry.register(manifestFirstPreset); + }); + afterEach(async () => { + umbExtensionsRegistry.unregister('Umb.Test.Preset.1'); + umbExtensionsRegistry.unregister('Umb.Test.Preset.2'); + }); + + it('creates a combined value', async () => { + const ctrlHost = new UmbTestControllerHostElement(); + const ctrl = new UmbPropertyValuePresetBuilderController(ctrlHost); + + const propertyTypes: Array = [ + { + alias: 'test', + propertyEditorUiAlias: 'test-editor-ui', + config: [], + typeArgs: {}, + }, + ]; + + const result = await ctrl.create(propertyTypes); + + expect(result.length).to.be.equal(1); + expect(result[0]?.value).to.be.equal('first_second'); + }); + }); + + describe('Different weight gets a different execution order', () => { + beforeEach(async () => { + const manifestFirstPreset: ManifestPropertyValuePreset = { + type: 'propertyValuePreset', + name: 'test-preset-1', + alias: 'Umb.Test.Preset.1', + weight: 20, + api: TestPropertyValuePresetFirstApi, + forPropertyEditorUiAlias: 'test-editor-ui', + }; + + const manifestSecondPreset: ManifestPropertyValuePreset = { + type: 'propertyValuePreset', + name: 'test-preset-2', + alias: 'Umb.Test.Preset.2', + weight: 3000, + api: TestPropertyValuePresetSecondApi, + forPropertyEditorUiAlias: 'test-editor-ui', + }; + + umbExtensionsRegistry.register(manifestSecondPreset); + umbExtensionsRegistry.register(manifestFirstPreset); + }); + afterEach(async () => { + umbExtensionsRegistry.unregister('Umb.Test.Preset.1'); + umbExtensionsRegistry.unregister('Umb.Test.Preset.2'); + }); + + it('creates a combined value', async () => { + const ctrlHost = new UmbTestControllerHostElement(); + const ctrl = new UmbPropertyValuePresetBuilderController(ctrlHost); + + const propertyTypes: Array = [ + { + alias: 'test', + propertyEditorUiAlias: 'test-editor-ui', + config: [], + typeArgs: {}, + }, + ]; + + const result = await ctrl.create(propertyTypes); + + expect(result.length).to.be.equal(1); + expect(result[0]?.value).to.be.equal('second_first'); + }); + }); + + describe('weigthed async presets keeps the right order', () => { + beforeEach(async () => { + const manifestFirstPreset: ManifestPropertyValuePreset = { + type: 'propertyValuePreset', + name: 'test-preset-1', + alias: 'Umb.Test.Preset.1', + weight: 3, + api: TestPropertyValuePresetFirstApi, + forPropertyEditorUiAlias: 'test-editor-ui', + }; + + const manifestAsyncPreset: ManifestPropertyValuePreset = { + type: 'propertyValuePreset', + name: 'test-preset-3', + alias: 'Umb.Test.Preset.3', + weight: 2, + api: TestPropertyValuePresetAsyncApi, + forPropertyEditorUiAlias: 'test-editor-ui', + }; + + const manifestSecondPreset: ManifestPropertyValuePreset = { + type: 'propertyValuePreset', + name: 'test-preset-2', + alias: 'Umb.Test.Preset.2', + weight: 1, + api: TestPropertyValuePresetSecondApi, + forPropertyEditorUiAlias: 'test-editor-ui', + }; + + umbExtensionsRegistry.register(manifestSecondPreset); + umbExtensionsRegistry.register(manifestAsyncPreset); + umbExtensionsRegistry.register(manifestFirstPreset); + }); + afterEach(async () => { + umbExtensionsRegistry.unregister('Umb.Test.Preset.1'); + umbExtensionsRegistry.unregister('Umb.Test.Preset.2'); + umbExtensionsRegistry.unregister('Umb.Test.Preset.3'); + }); + + it('creates a combined value', async () => { + const ctrlHost = new UmbTestControllerHostElement(); + const ctrl = new UmbPropertyValuePresetBuilderController(ctrlHost); + + const propertyTypes: Array = [ + { + alias: 'test', + propertyEditorUiAlias: 'test-editor-ui', + config: [], + typeArgs: {}, + }, + { + alias: 'test2', + propertyEditorUiAlias: 'test-editor-ui', + config: [], + typeArgs: {}, + }, + ]; + + const result = await ctrl.create(propertyTypes); + + expect(result.length).to.be.equal(2); + expect(result[0]?.alias).to.be.equal('test'); + expect(result[0]?.value).to.be.equal('first_async_second'); + expect(result[1]?.alias).to.be.equal('test2'); + expect(result[1]?.value).to.be.equal('first_async_second'); + }); + }); + + describe('combine use of UI and Schema Presets', () => { + beforeEach(async () => { + const manifestFirstPreset: ManifestPropertyValuePreset = { + type: 'propertyValuePreset', + name: 'test-preset-1', + alias: 'Umb.Test.Preset.1', + weight: 3, + api: TestPropertyValuePresetFirstApi, + forPropertyEditorSchemaAlias: 'test-editor-schema', + }; + + const manifestAsyncPreset: ManifestPropertyValuePreset = { + type: 'propertyValuePreset', + name: 'test-preset-3', + alias: 'Umb.Test.Preset.3', + weight: 2, + api: TestPropertyValuePresetAsyncApi, + forPropertyEditorUiAlias: 'test-editor-ui', + forPropertyEditorSchemaAlias: 'test-editor-schema', + }; + + const manifestSecondPreset: ManifestPropertyValuePreset = { + type: 'propertyValuePreset', + name: 'test-preset-2', + alias: 'Umb.Test.Preset.2', + weight: 1, + api: TestPropertyValuePresetSecondApi, + forPropertyEditorUiAlias: 'test-editor-ui', + }; + + umbExtensionsRegistry.register(manifestSecondPreset); + umbExtensionsRegistry.register(manifestAsyncPreset); + umbExtensionsRegistry.register(manifestFirstPreset); + }); + afterEach(async () => { + umbExtensionsRegistry.unregister('Umb.Test.Preset.1'); + umbExtensionsRegistry.unregister('Umb.Test.Preset.2'); + umbExtensionsRegistry.unregister('Umb.Test.Preset.3'); + }); + + it('creates only presets that fits the configuration', async () => { + const ctrlHost = new UmbTestControllerHostElement(); + const ctrl = new UmbPropertyValuePresetBuilderController(ctrlHost); + + const propertyTypes: Array = [ + { + alias: 'test', + propertyEditorUiAlias: 'test-editor-ui', + config: [], + typeArgs: {}, + }, + { + alias: 'test2', + propertyEditorUiAlias: 'test-editor-ui', + propertyEditorSchemaAlias: 'test-editor-schema', + config: [], + typeArgs: {}, + }, + { + alias: 'test3', + propertyEditorUiAlias: 'some-other-ui', + propertyEditorSchemaAlias: 'test-editor-schema', + config: [], + typeArgs: {}, + }, + ]; + + const result = await ctrl.create(propertyTypes); + + // Test that only the right presets are used: + expect(result.length).to.be.equal(3); + expect(result[0]?.alias).to.be.equal('test'); + expect(result[0]?.value).to.be.equal('async_second'); + expect(result[1]?.alias).to.be.equal('test2'); + expect(result[1]?.value).to.be.equal('first_async_second'); + expect(result[2]?.alias).to.be.equal('test3'); + expect(result[2]?.value).to.be.equal('first_async'); + }); + }); +}); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property/property-value-preset/property-value-preset-builder.controller.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property/property-value-preset/property-value-preset-builder.controller.ts new file mode 100644 index 000000000000..6f94a67dd08a --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property/property-value-preset/property-value-preset-builder.controller.ts @@ -0,0 +1,110 @@ +import type { UmbPropertyValueData, UmbPropertyValueDataPotentiallyWithEditorAlias } from '../index.js'; +import type { + ManifestPropertyValuePreset, + UmbPropertyTypePresetModel, + UmbPropertyTypePresetWithSchemaAliasModel, + UmbPropertyValuePreset, + UmbPropertyValuePresetApiCallArgs, +} from './types.js'; +import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; +import { createExtensionApi } from '@umbraco-cms/backoffice/extension-api'; +import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; + +const EMPTY_CALL_ARGS = Object.freeze({}); + +export class UmbPropertyValuePresetBuilderController< + ReturnType = UmbPropertyValueData | UmbPropertyValueDataPotentiallyWithEditorAlias, +> extends UmbControllerBase { + /** + * Clones the property data. + * @param {UmbPropertyValueDataPotentiallyWithEditorAlias} propertyTypes - Data about the properties to make a preset for. + * @returns {Promise} - A promise that resolves to the cloned property data. + */ + async create( + propertyTypes: Array, + ): Promise> { + const result = await Promise.all(propertyTypes.map(this.#createPropertyPreset)); + + //Merge all the values into a single array: + const values = result.flatMap((x) => x); + + this.destroy(); + + return values; + } + + #createPropertyPreset = async ( + propertyType: UmbPropertyTypePresetModel | UmbPropertyTypePresetWithSchemaAliasModel, + ): Promise> => { + const editorAlias: string | undefined = (propertyType as UmbPropertyTypePresetWithSchemaAliasModel) + .propertyEditorSchemaAlias; + + const editorUiAlias = propertyType.propertyEditorUiAlias; + if (!editorUiAlias) { + throw new Error(`propertyEditorUiAlias was not defined in ${propertyType}`); + } + + let filter: (x: ManifestPropertyValuePreset) => boolean; + if (editorAlias && editorUiAlias) { + filter = (x) => x.forPropertyEditorSchemaAlias === editorAlias || x.forPropertyEditorUiAlias === editorUiAlias; + } else { + filter = (x) => x.forPropertyEditorUiAlias === editorUiAlias; + } + + // Find a preset for this editor alias: + const manifests = umbExtensionsRegistry.getByTypeAndFilter('propertyValuePreset', filter); + + const apis = (await Promise.all(manifests.map((x) => createExtensionApi(this, x)))).filter( + (x) => x !== undefined, + ) as Array; + + const result = await this._generatePropertyValues(apis, propertyType); + + for (const api of apis) { + api.destroy(); + } + + return result; + }; + + protected async _generatePropertyValues( + apis: Array, + propertyType: UmbPropertyTypePresetModel | UmbPropertyTypePresetWithSchemaAliasModel, + ): Promise> { + const property = await this._generatePropertyValue(apis, propertyType, EMPTY_CALL_ARGS); + return property ? [property] : []; + } + + protected async _generatePropertyValue( + apis: Array, + propertyType: UmbPropertyTypePresetModel | UmbPropertyTypePresetWithSchemaAliasModel, + callArgs: UmbPropertyValuePresetApiCallArgs, + ): Promise { + let value: unknown = undefined; + // Important to use a inline for loop, to secure that each entry is processed(asynchronously) in order + for (const api of apis) { + if (!api.processValue) { + throw new Error(`'processValue()' method is not defined in the api: ${api.constructor.name}`); + } + + value = await api.processValue(value, propertyType.config, propertyType.typeArgs, callArgs); + } + + if (!value) { + return; + } + + if ((propertyType as UmbPropertyTypePresetWithSchemaAliasModel).propertyEditorSchemaAlias) { + return { + editorAlias: (propertyType as UmbPropertyTypePresetWithSchemaAliasModel).propertyEditorSchemaAlias, + alias: propertyType.alias, + value, + } satisfies UmbPropertyValueDataPotentiallyWithEditorAlias as ReturnType; + } else { + return { + alias: propertyType.alias, + value, + } satisfies UmbPropertyValueData as ReturnType; + } + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property/property-value-preset/property-value-preset-variant-builder.controller.test.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property/property-value-preset/property-value-preset-variant-builder.controller.test.ts new file mode 100644 index 000000000000..74f04d849eff --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property/property-value-preset/property-value-preset-variant-builder.controller.test.ts @@ -0,0 +1,183 @@ +import { expect } from '@open-wc/testing'; +import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; +import { customElement } from '@umbraco-cms/backoffice/external/lit'; +import { UmbControllerHostElementMixin } from '@umbraco-cms/backoffice/controller-api'; +import type { + ManifestPropertyValuePreset, + UmbPropertyTypePresetModel, + UmbPropertyTypePresetModelTypeModel, + UmbPropertyTypePresetWithSchemaAliasModel, + UmbPropertyValuePreset, + UmbPropertyValuePresetApiCallArgs, +} from './types.js'; +import type { UmbPropertyEditorConfig } from '@umbraco-cms/backoffice/property-editor'; +import { UmbPropertyValuePresetVariantBuilderController } from './property-value-preset-variant-builder.controller.js'; + +@customElement('umb-test-controller-host') +export class UmbTestControllerHostElement extends UmbControllerHostElementMixin(HTMLElement) {} + +// Test with async APIs, espcially where the first one is slower than the last one. +export class TestPropertyValuePresetFirstApi implements UmbPropertyValuePreset { + async processValue( + value: undefined | string, + config: UmbPropertyEditorConfig, + typeArgs: UmbPropertyTypePresetModelTypeModel, + callArgs: UmbPropertyValuePresetApiCallArgs, + ): Promise { + return value + ? value + '_' + 'value for variant ' + callArgs.variantId!.toString() + : 'value for culture ' + callArgs.variantId?.toString(); + } + + destroy(): void {} +} + +describe('UmbPropertyValuePresetVariantBuilderController', () => { + describe('Create with a variant preset', () => { + beforeEach(async () => { + const manifestFirstPreset: ManifestPropertyValuePreset = { + type: 'propertyValuePreset', + name: 'test-preset-1', + alias: 'Umb.Test.Preset.1', + api: TestPropertyValuePresetFirstApi, + forPropertyEditorUiAlias: 'test-editor-ui', + }; + + umbExtensionsRegistry.register(manifestFirstPreset); + }); + afterEach(async () => { + umbExtensionsRegistry.unregister('Umb.Test.Preset.1'); + }); + + it('creates culture variant values', async () => { + const ctrlHost = new UmbTestControllerHostElement(); + const ctrl = new UmbPropertyValuePresetVariantBuilderController(ctrlHost); + ctrl.setCultures(['cultureA', 'cultureB']); + + const propertyTypes: Array = [ + { + alias: 'test', + propertyEditorUiAlias: 'test-editor-ui', + config: [], + typeArgs: { variesByCulture: true }, + }, + ]; + + const result = await ctrl.create(propertyTypes); + + expect(result.length).to.be.equal(2); + expect(result[0]?.value).to.be.equal('value for culture cultureA'); + expect(result[0]?.culture).to.be.equal('cultureA'); + expect(result[1]?.value).to.be.equal('value for culture cultureB'); + expect(result[1]?.culture).to.be.equal('cultureB'); + }); + + it('creates culture variant values when no cultures available should fail', async () => { + const ctrlHost = new UmbTestControllerHostElement(); + const ctrl = new UmbPropertyValuePresetVariantBuilderController(ctrlHost); + + const propertyTypes: Array = [ + { + alias: 'test', + propertyEditorUiAlias: 'test-editor-ui', + config: [], + typeArgs: { variesByCulture: true }, + }, + ]; + + try { + await ctrl.create(propertyTypes); + expect.fail('Expected to fail'); + } catch (e) {} + }); + + it('creates segmented values', async () => { + const ctrlHost = new UmbTestControllerHostElement(); + const ctrl = new UmbPropertyValuePresetVariantBuilderController(ctrlHost); + ctrl.setSegments(['segmentA', 'segmentB']); + + const propertyTypes: Array = [ + { + alias: 'test', + propertyEditorUiAlias: 'test-editor-ui', + config: [], + typeArgs: { variesBySegment: true }, + }, + ]; + + const result = await ctrl.create(propertyTypes); + + expect(result.length).to.be.equal(3); + expect(result[0]?.value).to.be.equal('value for culture invariant'); + expect(result[0]?.culture).to.be.null; + expect(result[0]?.segment).to.be.null; + expect(result[1]?.value).to.be.equal('value for culture invariant_segmentA'); + expect(result[1]?.culture).to.be.null; + expect(result[1]?.segment).to.be.equal('segmentA'); + expect(result[2]?.value).to.be.equal('value for culture invariant_segmentB'); + expect(result[2]?.segment).to.be.equal('segmentB'); + expect(result[2]?.culture).to.be.null; + }); + + it('creates segmented values when no segments available', async () => { + const ctrlHost = new UmbTestControllerHostElement(); + const ctrl = new UmbPropertyValuePresetVariantBuilderController(ctrlHost); + + const propertyTypes: Array = [ + { + alias: 'test', + propertyEditorUiAlias: 'test-editor-ui', + config: [], + typeArgs: { variesBySegment: true }, + }, + ]; + + const result = await ctrl.create(propertyTypes); + + expect(result.length).to.be.equal(1); + expect(result[0]?.value).to.be.equal('value for culture invariant'); + expect(result[0]?.culture).to.be.null; + expect(result[0]?.segment).to.be.null; + }); + + it('creates culture variant and segmented values', async () => { + const ctrlHost = new UmbTestControllerHostElement(); + const ctrl = new UmbPropertyValuePresetVariantBuilderController(ctrlHost); + ctrl.setCultures(['cultureA', 'cultureB']); + ctrl.setSegments(['segmentA', 'segmentB']); + + const propertyTypes: Array = [ + { + alias: 'test', + propertyEditorUiAlias: 'test-editor-ui', + config: [], + typeArgs: { variesByCulture: true, variesBySegment: true }, + }, + ]; + + const result = await ctrl.create(propertyTypes); + + expect(result.length).to.be.equal(6); + + expect(result[0]?.value).to.be.equal('value for culture cultureA'); + expect(result[0]?.culture).to.be.equal('cultureA'); + expect(result[0]?.segment).to.be.null; + expect(result[1]?.value).to.be.equal('value for culture cultureA_segmentA'); + expect(result[1]?.culture).to.be.equal('cultureA'); + expect(result[1]?.segment).to.be.equal('segmentA'); + expect(result[2]?.value).to.be.equal('value for culture cultureA_segmentB'); + expect(result[2]?.culture).to.be.equal('cultureA'); + expect(result[2]?.segment).to.be.equal('segmentB'); + + expect(result[3]?.value).to.be.equal('value for culture cultureB'); + expect(result[3]?.culture).to.be.equal('cultureB'); + expect(result[3]?.segment).to.be.null; + expect(result[4]?.value).to.be.equal('value for culture cultureB_segmentA'); + expect(result[4]?.culture).to.be.equal('cultureB'); + expect(result[4]?.segment).to.be.equal('segmentA'); + expect(result[5]?.value).to.be.equal('value for culture cultureB_segmentB'); + expect(result[5]?.culture).to.be.equal('cultureB'); + expect(result[5]?.segment).to.be.equal('segmentB'); + }); + }); +}); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property/property-value-preset/property-value-preset-variant-builder.controller.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property/property-value-preset/property-value-preset-variant-builder.controller.ts new file mode 100644 index 000000000000..ab5a4aa358fc --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property/property-value-preset/property-value-preset-variant-builder.controller.ts @@ -0,0 +1,86 @@ +import { UmbVariantId } from '../../variant/variant-id.class.js'; +import { UmbPropertyValuePresetBuilderController } from './property-value-preset-builder.controller.js'; +import type { + UmbPropertyTypePresetModel, + UmbPropertyTypePresetWithSchemaAliasModel, + UmbPropertyValuePreset, +} from './types.js'; +import type { UmbElementValueModel } from '@umbraco-cms/backoffice/content'; + +type ReturnType = UmbElementValueModel; + +export class UmbPropertyValuePresetVariantBuilderController extends UmbPropertyValuePresetBuilderController { + #cultures: Array = []; + // Always declare the default segment (null) + #segments: Array = [null]; + + setCultures(cultures: Array): void { + this.#cultures = cultures; + } + setSegments(segments: Array): void { + // No matter how many segments are present, always include the default segment (null) + this.#segments = [null, ...segments]; + } + + protected override async _generatePropertyValues( + apis: Array, + propertyType: UmbPropertyTypePresetModel | UmbPropertyTypePresetWithSchemaAliasModel, + ): Promise> { + const values: Array = []; + + if (propertyType.typeArgs.variesBySegment && propertyType.typeArgs.variesByCulture) { + if (this.#cultures.length === 0) { + throw new Error('Cultures must be set when varying by culture.'); + } + + for (const culture of this.#cultures) { + for (const segment of this.#segments) { + const value = await this._generatePropertyValue(apis, propertyType, { + variantId: new UmbVariantId(culture, segment), + }); + if (value) { + value.culture = culture; + value.segment = segment; + values.push(value); + } + } + } + } else if (propertyType.typeArgs.variesByCulture) { + if (this.#cultures.length === 0) { + throw new Error('Cultures must be set when varying by culture.'); + } + + for (const culture of this.#cultures) { + const value = await this._generatePropertyValue(apis, propertyType, { + variantId: new UmbVariantId(culture), + }); + if (value) { + value.culture = culture; + value.segment = null; + values.push(value); + } + } + } else if (propertyType.typeArgs.variesBySegment) { + for (const segment of this.#segments) { + const value = await this._generatePropertyValue(apis, propertyType, { + variantId: new UmbVariantId(null, segment), + }); + if (value) { + // Be aware this maybe should have been the default culture? + value.culture = null; + value.segment = segment; + values.push(value); + } + } + } else { + const value = await this._generatePropertyValue(apis, propertyType, {}); + if (value) { + // Be aware this maybe should have been the default culture? + value.culture = null; + value.segment = null; + values.push(value); + } + } + return values; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property/property-value-preset/property-value-preset.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property/property-value-preset/property-value-preset.extension.ts new file mode 100644 index 000000000000..88813e393017 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property/property-value-preset/property-value-preset.extension.ts @@ -0,0 +1,14 @@ +import type { UmbPropertyValuePreset } from './types.js'; +import type { ManifestApi } from '@umbraco-cms/backoffice/extension-api'; + +export interface ManifestPropertyValuePreset extends ManifestApi> { + type: 'propertyValuePreset'; + forPropertyEditorSchemaAlias?: string; + forPropertyEditorUiAlias?: string; +} + +declare global { + interface UmbExtensionManifestMap { + ManifestPropertyValuePreset: ManifestPropertyValuePreset; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property/property-value-preset/types.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property/property-value-preset/types.ts new file mode 100644 index 000000000000..de87eed76680 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property/property-value-preset/types.ts @@ -0,0 +1,42 @@ +import type { UmbVariantId } from '../../variant/variant-id.class.js'; +import type { UmbPropertyEditorConfig } from '@umbraco-cms/backoffice/property-editor'; +import type { UmbApi } from '@umbraco-cms/backoffice/extension-api'; + +export type * from './property-value-preset.extension.js'; + +export interface UmbPropertyValuePreset< + ValueType = unknown, + ConfigType extends UmbPropertyEditorConfig = UmbPropertyEditorConfig, +> extends UmbApi { + processValue: UmbPropertyValuePresetValuesProcessor; +} + +export type UmbPropertyValuePresetValuesProcessor< + ValueType = unknown, + ConfigType extends UmbPropertyEditorConfig = UmbPropertyEditorConfig, +> = ( + value: undefined | ValueType, + config: ConfigType, + typeArgs: UmbPropertyTypePresetModelTypeModel, + callArgs: UmbPropertyValuePresetApiCallArgs, +) => PromiseLike; + +export interface UmbPropertyTypePresetModel { + alias: string; + propertyEditorUiAlias: string; + config: UmbPropertyEditorConfig; + typeArgs: UmbPropertyTypePresetModelTypeModel; +} + +export interface UmbPropertyTypePresetModelTypeModel { + isMandatory?: boolean; + variesByCulture?: boolean; + variesBySegment?: boolean; +} +export interface UmbPropertyValuePresetApiCallArgs { + variantId?: UmbVariantId; +} + +export interface UmbPropertyTypePresetWithSchemaAliasModel extends UmbPropertyTypePresetModel { + propertyEditorSchemaAlias: string; +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property/property-value-resolver/property-value-resolver.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property/property-value-resolver/property-value-resolver.extension.ts index 9499e2dde022..bb15c27b6a93 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property/property-value-resolver/property-value-resolver.extension.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property/property-value-resolver/property-value-resolver.extension.ts @@ -9,7 +9,7 @@ export interface ManifestPropertyValueResolver extends ManifestApi { + return data; + } + async submit() { await this.#init; const currentData = this.getData(); @@ -355,6 +361,7 @@ export abstract class UmbEntityDetailWorkspaceContextBase< /* TODO: temp removal of discard changes in workspace modals. The modal closes before the discard changes dialog is resolved.*/ + // TODO: I think this can go away now??? if (newUrl.includes('/modal/umb-modal-workspace/')) { return true; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/document-blueprints/workspace/document-blueprint-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/document-blueprints/workspace/document-blueprint-workspace.context.ts index d1213629f5a4..aacaad252d7e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/document-blueprints/workspace/document-blueprint-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/document-blueprints/workspace/document-blueprint-workspace.context.ts @@ -44,6 +44,7 @@ export class UmbDocumentBlueprintWorkspaceContext detailRepositoryAlias: UMB_DOCUMENT_BLUEPRINT_DETAIL_REPOSITORY_ALIAS, contentTypeDetailRepository: UmbDocumentTypeDetailRepository, contentVariantScaffold: UMB_DOCUMENT_DETAIL_MODEL_VARIANT_SCAFFOLD, + contentTypePropertyName: 'documentType', }); this.observe(this.contentTypeUnique, (unique) => this.structure.loadType(unique), null); diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace.context.ts index b0c6e04ff6a9..547247c7b746 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace.context.ts @@ -82,6 +82,7 @@ export class UmbDocumentWorkspaceContext contentValidationRepository: UmbDocumentValidationRepository, skipValidationOnSubmit: true, contentVariantScaffold: UMB_DOCUMENT_DETAIL_MODEL_VARIANT_SCAFFOLD, + contentTypePropertyName: 'documentType', saveModalToken: UMB_DOCUMENT_SAVE_MODAL, }); diff --git a/src/Umbraco.Web.UI.Client/src/packages/language/global-contexts/app-language.context.ts b/src/Umbraco.Web.UI.Client/src/packages/language/global-contexts/app-language.context.ts index 2f0a6e354dc9..acf0b9f19109 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/language/global-contexts/app-language.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/language/global-contexts/app-language.context.ts @@ -13,12 +13,11 @@ import { UMB_CURRENT_USER_CONTEXT } from '@umbraco-cms/backoffice/current-user'; // TODO: Implement default language end-point, in progress at backend team, so we can avoid getting all languages. export class UmbAppLanguageContext extends UmbContextBase implements UmbApi { #languages = new UmbArrayState([], (x) => x.unique); - - #appLanguage = new UmbObjectState(undefined); - public readonly appLanguage = this.#appLanguage.asObservable(); - public readonly appLanguageCulture = this.#appLanguage.asObservablePart((x) => x?.unique); - - public readonly appLanguageReadOnlyState = new UmbReadOnlyStateManager(this); + public readonly languages = this.#languages.asObservable(); + public readonly cultures = this.#languages.asObservablePart((x) => x.map((y) => y.unique)); + async getCultures() { + return (await this.observe(this.languages).asPromise()).map((x) => x.unique); + } public readonly appDefaultLanguage = this.#languages.asObservablePart((languages) => languages.find((language) => language.isDefault), @@ -26,6 +25,12 @@ export class UmbAppLanguageContext extends UmbContextBase public readonly moreThanOneLanguage = this.#languages.asObservablePart((x) => x.length > 1); + #appLanguage = new UmbObjectState(undefined); + public readonly appLanguage = this.#appLanguage.asObservable(); + public readonly appLanguageCulture = this.#appLanguage.asObservablePart((x) => x?.unique); + + public readonly appLanguageReadOnlyState = new UmbReadOnlyStateManager(this); + #languageCollectionRepository = new UmbLanguageCollectionRepository(this); #currentUserAllowedLanguages: Array = []; #currentUserHasAccessToAllLanguages = false; diff --git a/src/Umbraco.Web.UI.Client/src/packages/markdown-editor/components/input-markdown-editor/input-markdown.element.ts b/src/Umbraco.Web.UI.Client/src/packages/markdown-editor/components/input-markdown-editor/input-markdown.element.ts index b85eb539fcb7..f0a0f64d7ec5 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/markdown-editor/components/input-markdown-editor/input-markdown.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/markdown-editor/components/input-markdown-editor/input-markdown.element.ts @@ -33,7 +33,9 @@ interface UmbMarkdownEditorAction extends monaco.editor.IActionDescriptor { * @fires change - when the value of the input changes */ @customElement('umb-input-markdown') -export class UmbInputMarkdownElement extends UmbFormControlMixin(UmbLitElement, '') { +export class UmbInputMarkdownElement extends UmbFormControlMixin( + UmbLitElement, +) { protected override getFormElement() { return this._codeEditor; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/markdown-editor/property-editors/markdown-editor/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/markdown-editor/property-editors/markdown-editor/manifests.ts index 11be0a8d8764..10f5ef989073 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/markdown-editor/property-editors/markdown-editor/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/markdown-editor/property-editors/markdown-editor/manifests.ts @@ -1,6 +1,13 @@ import { manifest as schemaManifest } from './Umbraco.MarkdownEditor.js'; export const manifests: Array = [ + { + type: 'propertyValuePreset', + forPropertyEditorSchemaAlias: 'Umbraco.MarkdownEditor', + alias: 'Umb.PropertyValuePreset.MarkdownEditor', + name: 'Markdown Editor Property Value Preset', + api: () => import('./markdown-editor-property-value-preset.js'), + }, { type: 'propertyEditorUi', alias: 'Umb.PropertyEditorUi.MarkdownEditor', diff --git a/src/Umbraco.Web.UI.Client/src/packages/markdown-editor/property-editors/markdown-editor/markdown-editor-property-value-preset.ts b/src/Umbraco.Web.UI.Client/src/packages/markdown-editor/property-editors/markdown-editor/markdown-editor-property-value-preset.ts new file mode 100644 index 000000000000..95951de39183 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/markdown-editor/property-editors/markdown-editor/markdown-editor-property-value-preset.ts @@ -0,0 +1,14 @@ +import type { UmbPropertyValuePreset } from '@umbraco-cms/backoffice/property'; +import type { UmbPropertyEditorConfig } from '@umbraco-cms/backoffice/property-editor'; +import type { UmbMarkdownPropertyEditorUiValue } from './types.js'; + +export class UmbMarkdownPropertyValuePreset implements UmbPropertyValuePreset { + async processValue(value: undefined | UmbMarkdownPropertyEditorUiValue, config: UmbPropertyEditorConfig) { + const defaultValue = config.find((x) => x.alias === 'defaultValue')?.value as string | undefined; + return value !== undefined ? value : defaultValue; + } + + destroy(): void {} +} + +export { UmbMarkdownPropertyValuePreset as api }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/markdown-editor/property-editors/markdown-editor/property-editor-ui-markdown-editor.element.ts b/src/Umbraco.Web.UI.Client/src/packages/markdown-editor/property-editors/markdown-editor/property-editor-ui-markdown-editor.element.ts index a49d71053bc2..3c54910cd6fb 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/markdown-editor/property-editors/markdown-editor/property-editor-ui-markdown-editor.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/markdown-editor/property-editors/markdown-editor/property-editor-ui-markdown-editor.element.ts @@ -10,14 +10,13 @@ import type { UUIModalSidebarSize } from '@umbraco-cms/backoffice/external/uui'; import '../../components/input-markdown-editor/index.js'; -const elementName = 'umb-property-editor-ui-markdown-editor'; /** * @element umb-property-editor-ui-markdown-editor */ -@customElement(elementName) +@customElement('umb-property-editor-ui-markdown-editor') export class UmbPropertyEditorUIMarkdownEditorElement extends UmbLitElement implements UmbPropertyEditorUiElement { @property() - value = ''; + value?: string; /** * Sets the input to readonly mode, meaning value cannot be changed but still able to read and select its content. @@ -39,12 +38,6 @@ export class UmbPropertyEditorUIMarkdownEditorElement extends UmbLitElement impl this._preview = config.getValueByAlias('preview'); this._overlaySize = config.getValueByAlias('overlaySize') ?? 'small'; - - // TODO: To be removed once the "Property Value Presets" feature has been implemented. - const defaultValue = config.getValueByAlias('defaultValue'); - if (defaultValue && this.value === undefined) { - this.value = defaultValue; - } } #onChange(event: Event & { target: UmbInputMarkdownElement }) { @@ -55,7 +48,7 @@ export class UmbPropertyEditorUIMarkdownEditorElement extends UmbLitElement impl override render() { return html` this.structure.loadType(unique), null); diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/member/member-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/member/member-workspace.context.ts index 121de10724ea..40c0643e5a92 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/member/member-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/member/member-workspace.context.ts @@ -41,6 +41,7 @@ export class UmbMemberWorkspaceContext // TODO: Enable Validation Repository when we have UI for showing validation issues on other tabs. [NL] //contentValidationRepository: UmbMemberValidationRepository, contentVariantScaffold: UMB_MEMBER_DETAIL_MODEL_VARIANT_SCAFFOLD, + contentTypePropertyName: 'memberType', }); this.observe(this.contentTypeUnique, (unique) => this.structure.loadType(unique), null); diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/index.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/index.ts new file mode 100644 index 000000000000..06c33f562f4e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/property-editors/index.ts @@ -0,0 +1 @@ +export type * from './types.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/slider/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/slider/manifests.ts index fd4519a59c19..d313ac784957 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/property-editors/slider/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/property-editors/slider/manifests.ts @@ -1,6 +1,13 @@ import { manifest as sliderSchemaManifest } from './Umbraco.Slider.js'; export const manifests: Array = [ + { + type: 'propertyValuePreset', + forPropertyEditorSchemaAlias: 'Umbraco.Slider', + alias: 'Umb.PropertyValuePreset.Slider', + name: 'Property Editor Schema Slider Preset for Initial Values', + api: () => import('./slider-property-value-preset.js'), + }, { type: 'propertyEditorUi', alias: 'Umb.PropertyEditorUi.Slider', diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/slider/property-editor-ui-slider.element.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/slider/property-editor-ui-slider.element.ts index d9a719922c6b..f61e4e83b624 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/property-editors/slider/property-editor-ui-slider.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/property-editors/slider/property-editor-ui-slider.element.ts @@ -1,3 +1,4 @@ +import type { UmbSliderPropertyEditorUiValue } from './types.js'; import type { UmbInputSliderElement } from '@umbraco-cms/backoffice/components'; import { customElement, html, property, state } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; @@ -8,15 +9,13 @@ import type { UmbPropertyEditorUiElement, } from '@umbraco-cms/backoffice/property-editor'; -export type UmbSliderValue = { from: number; to: number } | undefined; - /** * @element umb-property-editor-ui-slider */ @customElement('umb-property-editor-ui-slider') export class UmbPropertyEditorUISliderElement extends UmbLitElement implements UmbPropertyEditorUiElement { @property({ type: Object }) - value: UmbSliderValue | undefined; + value: UmbSliderPropertyEditorUiValue | undefined; /** * Sets the input to readonly mode, meaning value cannot be changed but still able to read and select its content. diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/slider/slider-property-value-preset.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/slider/slider-property-value-preset.ts new file mode 100644 index 000000000000..8c27b41147d3 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/property-editors/slider/slider-property-value-preset.ts @@ -0,0 +1,36 @@ +import type { UmbSliderPropertyEditorUiValue } from './types.js'; +import type { UmbPropertyValuePreset } from '@umbraco-cms/backoffice/property'; +import type { UmbPropertyEditorConfig } from '@umbraco-cms/backoffice/property-editor'; + +export class UmbSliderPropertyValuePreset + implements UmbPropertyValuePreset +{ + async processValue(value: undefined | UmbSliderPropertyEditorUiValue, config: UmbPropertyEditorConfig) { + const enableRange = Boolean(config.find((x) => x.alias === 'enableRange') ?? false); + + /* + const min = Number(config.find((x) => x.alias === 'minVal') ?? 0); + const max = Number(config.find((x) => x.alias === 'maxVal') ?? 100); + const minVerified = isNaN(min) ? undefined : min; + const maxVerified = isNaN(max) ? undefined : max; + */ + + const step = (config.find((x) => x.alias === 'step') as number | undefined) ?? 0; + const stepVerified = step > 0 ? step : 1; + + const initValueMin = Number(config.find((x) => x.alias === 'initVal1')?.value) || 0; + const initValueMinVerified = isNaN(initValueMin) ? 0 : initValueMin; + + const initValueMax = Number(config.find((x) => x.alias === 'initVal2')?.value) || 0; + const initValueMaxVerified = isNaN(initValueMax) ? initValueMinVerified + stepVerified : initValueMax; + + const initialState = enableRange + ? { from: initValueMinVerified, to: initValueMaxVerified } + : { from: initValueMinVerified, to: initValueMinVerified }; + return value !== undefined ? value : initialState; + } + + destroy(): void {} +} + +export { UmbSliderPropertyValuePreset as api }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/slider/types.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/slider/types.ts new file mode 100644 index 000000000000..3d6d1fd39aff --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/property-editors/slider/types.ts @@ -0,0 +1,6 @@ +export type UmbSliderPropertyEditorUiValue = { from: number; to: number } | undefined; + +/** + * @deprecated this type will be removed in v.17.0, use `UmbPropertyEditorUISliderValue` instead + */ +export type UmbSliderValue = UmbSliderPropertyEditorUiValue; diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/toggle/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/toggle/manifests.ts index ee3c9d5a5afe..efd7f2292d2a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/property-editors/toggle/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/property-editors/toggle/manifests.ts @@ -1,6 +1,13 @@ import { manifest as trueFalseSchemaManifest } from './Umbraco.TrueFalse.js'; export const manifests: Array = [ + { + type: 'propertyValuePreset', + forPropertyEditorSchemaAlias: 'Umbraco.TrueFalse', + alias: 'Umb.PropertyValuePreset.TrueFalse', + name: 'Property Editor Schema True/False Preset for Initial State', + api: () => import('./true-false-property-value-preset.js'), + }, { type: 'propertyEditorUi', alias: 'Umb.PropertyEditorUi.Toggle', @@ -19,24 +26,22 @@ export const manifests: Array = [ label: 'Preset value', propertyEditorUiAlias: 'Umb.PropertyEditorUi.Toggle', config: [ - { - alias: "ariaLabel", - value: 'toggle for the initial state of this data type' - } - ] + alias: 'ariaLabel', + value: 'toggle for the initial state of this data type', + }, + ], }, { alias: 'showLabels', label: 'Show on/off labels', propertyEditorUiAlias: 'Umb.PropertyEditorUi.Toggle', config: [ - { - alias: "ariaLabel", - value: 'toggle for weather if label should be displayed' - } - ] + alias: 'ariaLabel', + value: 'toggle for weather if label should be displayed', + }, + ], }, { alias: 'labelOn', diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/toggle/property-editor-ui-toggle.element.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/toggle/property-editor-ui-toggle.element.ts index 4160cfc53b09..f0e34bb06522 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/property-editors/toggle/property-editor-ui-toggle.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/property-editors/toggle/property-editor-ui-toggle.element.ts @@ -1,3 +1,4 @@ +import type { UmbTogglePropertyEditorUiValue } from './types.js'; import type { UmbInputToggleElement } from '@umbraco-cms/backoffice/components'; import { customElement, html, property, state } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; @@ -10,7 +11,7 @@ import { UMB_VALIDATION_FALSE_LOCALIZATION_KEY, UmbFormControlMixin } from '@umb @customElement('umb-property-editor-ui-toggle') export class UmbPropertyEditorUIToggleElement - extends UmbFormControlMixin(UmbLitElement) + extends UmbFormControlMixin(UmbLitElement) implements UmbPropertyEditorUiElement { @property({ type: String }) @@ -47,8 +48,6 @@ export class UmbPropertyEditorUIToggleElement public set config(config: UmbPropertyEditorConfigCollection | undefined) { if (!config) return; - // TODO: Do not set value here, but use future feature of Value Presets. [NL] - this.value ??= config.getValueByAlias('default') ?? false; this._labelOff = config.getValueByAlias('labelOff'); this._labelOn = config.getValueByAlias('labelOn'); this._showLabels = Boolean(config.getValueByAlias('showLabels')); @@ -60,7 +59,8 @@ export class UmbPropertyEditorUIToggleElement } #onChange(event: CustomEvent & { target: UmbInputToggleElement }) { - this.value = event.target.checked; + const checked = event.target.checked; + this.value = this.mandatory ? (checked ?? null) : checked; this.dispatchEvent(new UmbPropertyValueChangeEvent()); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/toggle/true-false-property-value-preset.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/toggle/true-false-property-value-preset.ts new file mode 100644 index 000000000000..8d3681d1d70c --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/property-editors/toggle/true-false-property-value-preset.ts @@ -0,0 +1,16 @@ +import type { UmbTogglePropertyEditorUiValue } from './types.js'; +import type { UmbPropertyValuePreset } from '@umbraco-cms/backoffice/property'; +import type { UmbPropertyEditorConfig } from '@umbraco-cms/backoffice/property-editor'; + +export class UmbTrueFalsePropertyValuePreset + implements UmbPropertyValuePreset +{ + async processValue(value: undefined | UmbTogglePropertyEditorUiValue, config: UmbPropertyEditorConfig) { + const initialState = (config.find((x) => x.alias === 'default')?.value as boolean | undefined) ?? false; + return value !== undefined ? value : initialState; + } + + destroy(): void {} +} + +export { UmbTrueFalsePropertyValuePreset as api }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/toggle/types.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/toggle/types.ts new file mode 100644 index 000000000000..d2a6e1a2b4b1 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/property-editors/toggle/types.ts @@ -0,0 +1 @@ +export type UmbTogglePropertyEditorUiValue = boolean; diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/types.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/types.ts new file mode 100644 index 000000000000..1213e71e2859 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/property-editors/types.ts @@ -0,0 +1,2 @@ +export type * from './toggle/types.js'; +export type * from './slider/types.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user-group/workspace/user-group/user-group-workspace-editor.element.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user-group/workspace/user-group/user-group-workspace-editor.element.ts index aae6d579e995..e44a4a6a7cb1 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user-group/workspace/user-group/user-group-workspace-editor.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user-group/workspace/user-group/user-group-workspace-editor.element.ts @@ -244,7 +244,9 @@ export class UmbUserGroupWorkspaceEditorElement extends UmbLitElement {
- +