From a8f070d834b7035f93ea00b63e846764e7ca178e Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Wed, 21 Dec 2022 14:29:59 +0100 Subject: [PATCH] Datatype configuration refactor (#13605) * Moved refactor from V12 based branch * Remove obsolete config property to avoid obsoletion warnings * Clean up test models * Make the datatype API a little less confusing by introducing explicit models for create and update * Remame "Configuration" to "Data" to make FE happy :) * Safeguard deserialization of empty configs * Less strict number handling for deserialization * Prepare for data type config migrations * Attempt to have contextual config serializers * Update OpenAPI spec with (temporary) data type controller * Fix unit tests * Update compat suppressions (allow breakage for datatype and configuration editor) * Make the duplicate JsonObjectConverter implementation private --- .../DataType/ByKeyDataTypeController.cs | 35 +++ .../DataType/CreateDataTypeController.cs | 43 ++++ .../DataType/DataTypeControllerBase.cs | 13 + .../DataType/UpdateDataTypeController.cs | 45 ++++ .../MappingBuilderExtensions.cs | 7 +- .../DataTypeViewModelMapDefinition.cs | 124 ++++++++++ src/Umbraco.Cms.Api.Management/OpenApi.json | 228 ++++++++++++++++- .../DataType/DataTypeCreateModel.cs | 6 + .../ViewModels/DataType/DataTypeModelBase.cs | 10 + .../DataType/DataTypePropertyViewModel.cs | 8 + .../DataType/DataTypeUpdateModel.cs | 5 + .../ViewModels/DataType/DataTypeViewModel.cs | 8 + src/Umbraco.Core/Cache/ValueEditorCache.cs | 4 +- .../CompatibilitySuppressions.xml | 231 ++++++++++++++++++ .../ContentApps/ListViewContentAppFactory.cs | 4 +- .../ContentEditing/ContentPropertyDisplay.cs | 5 - src/Umbraco.Core/Models/DataType.cs | 128 +++------- src/Umbraco.Core/Models/DataTypeExtensions.cs | 2 +- src/Umbraco.Core/Models/IDataType.cs | 13 +- .../Mapping/ContentPropertyDisplayMapper.cs | 7 +- .../Models/Mapping/DataTypeMapDefinition.cs | 5 +- .../Models/Mapping/MacroMapDefinition.cs | 7 +- .../Models/Mapping/PropertyTypeGroupMapper.cs | 2 +- .../Models/PropertyTagsExtensions.cs | 2 +- .../PublishedContentTypeFactory.cs | 2 +- .../PropertyEditors/ConfigurationEditor.cs | 121 ++++----- .../ConfigurationEditorOfTConfiguration.cs | 47 +--- .../ContentPickerConfigurationEditor.cs | 12 +- .../DateTimeConfigurationEditor.cs | 10 +- ...yeDropperColorPickerConfigurationEditor.cs | 36 --- .../PropertyEditors/IConfigurationEditor.cs | 85 +++---- .../LabelConfigurationEditor.cs | 22 +- .../MediaPickerConfigurationEditor.cs | 8 +- .../MultiNodePickerConfigurationEditor.cs | 33 +-- .../MultipleTextStringConfiguration.cs | 4 +- .../PropertyEditors/TagConfigurationEditor.cs | 29 +-- .../Services/DateTypeServiceExtensions.cs | 2 +- .../Services/EntityXmlSerializer.cs | 2 +- .../Services/PropertyValidationService.cs | 2 +- .../UmbracoBuilder.CoreServices.cs | 2 +- .../Migrations/Upgrade/UmbracoPlan.cs | 4 + .../V_13_0_0/MigrateDataTypeConfigurations.cs | 129 ++++++++++ .../DropDownPropertyEditorsMigration.cs | 14 +- .../MergeDateAndDateTimePropertyEditor.cs | 14 +- ...adioAndCheckboxPropertyEditorsMigration.cs | 9 +- .../Packaging/PackageDataInstallation.cs | 3 +- .../Persistence/Factories/DataTypeFactory.cs | 4 +- .../Implement/ContentRepositoryBase.cs | 2 +- .../BlockEditorPropertyValueEditor.cs | 6 +- .../ColorPickerConfigurationEditor.cs | 130 ---------- .../DropDownFlexibleConfigurationEditor.cs | 72 ------ .../ImageCropperConfigurationEditor.cs | 15 +- .../ImageCropperPropertyValueEditor.cs | 2 +- .../MediaPicker3PropertyEditor.cs | 2 +- .../MultipleTextStringConfigurationEditor.cs | 29 +-- .../MultipleTextStringPropertyEditor.cs | 2 +- .../NestedContentPropertyEditor.cs | 8 +- .../ValueListConfigurationEditor.cs | 89 ------- ...extualConfigurationEditorJsonSerializer.cs | 47 ++++ ...emTextConfigurationEditorJsonSerializer.cs | 104 ++++++++ .../Controllers/ContentControllerBase.cs | 4 +- .../Controllers/ContentTypeController.cs | 6 +- .../Controllers/DataTypeController.cs | 15 +- .../Controllers/MediaController.cs | 2 +- .../UmbracoTestDataController.cs | 2 +- .../DataTypeDefinitionRepositoryTest.cs | 24 +- .../Services/ContentServiceTagsTests.cs | 23 +- .../Filters/ContentModelValidatorTests.cs | 13 +- .../MultiValuePropertyEditorTests.cs | 121 ++++----- .../Published/NestedContentTests.cs | 46 ++-- .../PropertyValidationServiceTests.cs | 2 +- 71 files changed, 1448 insertions(+), 854 deletions(-) create mode 100644 src/Umbraco.Cms.Api.Management/Controllers/DataType/ByKeyDataTypeController.cs create mode 100644 src/Umbraco.Cms.Api.Management/Controllers/DataType/CreateDataTypeController.cs create mode 100644 src/Umbraco.Cms.Api.Management/Controllers/DataType/DataTypeControllerBase.cs create mode 100644 src/Umbraco.Cms.Api.Management/Controllers/DataType/UpdateDataTypeController.cs create mode 100644 src/Umbraco.Cms.Api.Management/Mapping/DataType/DataTypeViewModelMapDefinition.cs create mode 100644 src/Umbraco.Cms.Api.Management/ViewModels/DataType/DataTypeCreateModel.cs create mode 100644 src/Umbraco.Cms.Api.Management/ViewModels/DataType/DataTypeModelBase.cs create mode 100644 src/Umbraco.Cms.Api.Management/ViewModels/DataType/DataTypePropertyViewModel.cs create mode 100644 src/Umbraco.Cms.Api.Management/ViewModels/DataType/DataTypeUpdateModel.cs create mode 100644 src/Umbraco.Cms.Api.Management/ViewModels/DataType/DataTypeViewModel.cs create mode 100644 src/Umbraco.Infrastructure/Migrations/Upgrade/V_13_0_0/MigrateDataTypeConfigurations.cs create mode 100644 src/Umbraco.Infrastructure/Serialization/ContextualConfigurationEditorJsonSerializer.cs create mode 100644 src/Umbraco.Infrastructure/Serialization/SystemTextConfigurationEditorJsonSerializer.cs diff --git a/src/Umbraco.Cms.Api.Management/Controllers/DataType/ByKeyDataTypeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/DataType/ByKeyDataTypeController.cs new file mode 100644 index 0000000000..ec37db42f2 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/DataType/ByKeyDataTypeController.cs @@ -0,0 +1,35 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Management.ViewModels.DataType; +using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; + +namespace Umbraco.Cms.Api.Management.Controllers.DataType; + +public class ByKeyDataTypeController : DataTypeControllerBase +{ + private readonly IDataTypeService _dataTypeService; + private readonly IUmbracoMapper _umbracoMapper; + + public ByKeyDataTypeController(IDataTypeService dataTypeService, IUmbracoMapper umbracoMapper) + { + _dataTypeService = dataTypeService; + _umbracoMapper = umbracoMapper; + } + + [HttpGet("{key:guid}")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(DataTypeViewModel), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(NotFoundResult), StatusCodes.Status404NotFound)] + public async Task> ByKey(Guid key) + { + IDataType? dataType = _dataTypeService.GetDataType(key); + if (dataType == null) + { + return NotFound(); + } + + return await Task.FromResult(Ok(_umbracoMapper.Map(dataType))); + } +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/DataType/CreateDataTypeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/DataType/CreateDataTypeController.cs new file mode 100644 index 0000000000..b48b444d19 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/DataType/CreateDataTypeController.cs @@ -0,0 +1,43 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Management.ViewModels.DataType; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Membership; +using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Core.Services; + +namespace Umbraco.Cms.Api.Management.Controllers.DataType; + +public class CreateDataTypeController : DataTypeControllerBase +{ + private readonly IDataTypeService _dataTypeService; + private readonly IUmbracoMapper _umbracoMapper; + private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; + + public CreateDataTypeController(IDataTypeService dataTypeService, IUmbracoMapper umbracoMapper, IBackOfficeSecurityAccessor backOfficeSecurityAccessor) + { + _dataTypeService = dataTypeService; + _umbracoMapper = umbracoMapper; + _backOfficeSecurityAccessor = backOfficeSecurityAccessor; + } + + [HttpPost] + [MapToApiVersion("1.0")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task> Create(DataTypeCreateModel dataTypeCreateModel) + { + IDataType? created = _umbracoMapper.Map(dataTypeCreateModel); + if (created == null) + { + return BadRequest("Could not map the POSTed model to a datatype"); + } + + IUser? currentUser = _backOfficeSecurityAccessor.BackOfficeSecurity?.CurrentUser; + _dataTypeService.Save(created, currentUser?.Id ?? Constants.Security.SuperUserId); + + return await Task.FromResult(Ok(_umbracoMapper.Map(created))); + } +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/DataType/DataTypeControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/DataType/DataTypeControllerBase.cs new file mode 100644 index 0000000000..c229ed6e45 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/DataType/DataTypeControllerBase.cs @@ -0,0 +1,13 @@ +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Management.Routing; +using Umbraco.Cms.Core; + +namespace Umbraco.Cms.Api.Management.Controllers.DataType; + +[ApiController] +[VersionedApiBackOfficeRoute(Constants.UdiEntityType.DataType)] +[ApiExplorerSettings(GroupName = "Data Type")] +[ApiVersion("1.0")] +public abstract class DataTypeControllerBase : ManagementApiControllerBase +{ +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/DataType/UpdateDataTypeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/DataType/UpdateDataTypeController.cs new file mode 100644 index 0000000000..a6657abfb4 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/DataType/UpdateDataTypeController.cs @@ -0,0 +1,45 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Management.ViewModels.DataType; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Membership; +using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Core.Services; + +namespace Umbraco.Cms.Api.Management.Controllers.DataType; + +public class UpdateDataTypeController : DataTypeControllerBase +{ + private readonly IDataTypeService _dataTypeService; + private readonly IUmbracoMapper _umbracoMapper; + private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; + + public UpdateDataTypeController(IDataTypeService dataTypeService, IUmbracoMapper umbracoMapper, IBackOfficeSecurityAccessor backOfficeSecurityAccessor) + { + _dataTypeService = dataTypeService; + _umbracoMapper = umbracoMapper; + _backOfficeSecurityAccessor = backOfficeSecurityAccessor; + } + + [HttpPut("{key:guid}")] + [MapToApiVersion("1.0")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task> Update(Guid key, DataTypeUpdateModel dataTypeViewModel) + { + IDataType? current = _dataTypeService.GetDataType(key); + if (current == null) + { + return NotFound(); + } + + IDataType updated = _umbracoMapper.Map(dataTypeViewModel, current); + + IUser? currentUser = _backOfficeSecurityAccessor.BackOfficeSecurity?.CurrentUser; + _dataTypeService.Save(updated, currentUser?.Id ?? Constants.Security.SuperUserId); + + return await Task.FromResult(Ok(_umbracoMapper.Map(updated))); + } +} diff --git a/src/Umbraco.Cms.Api.Management/DependencyInjection/MappingBuilderExtensions.cs b/src/Umbraco.Cms.Api.Management/DependencyInjection/MappingBuilderExtensions.cs index 4f1d5c5842..f4d53c0111 100644 --- a/src/Umbraco.Cms.Api.Management/DependencyInjection/MappingBuilderExtensions.cs +++ b/src/Umbraco.Cms.Api.Management/DependencyInjection/MappingBuilderExtensions.cs @@ -1,6 +1,7 @@ -using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Api.Management.Mapping.Culture; +using Umbraco.Cms.Api.Management.Mapping.DataType; using Umbraco.Cms.Api.Management.Mapping.Dictionary; using Umbraco.Cms.Api.Management.Mapping.HealthCheck; using Umbraco.Cms.Api.Management.Mapping.Installer; @@ -23,7 +24,9 @@ public static class MappingBuilderExtensions .Add() .Add() .Add() - .Add(); + .Add() + .Add() + .Add(); return builder; } diff --git a/src/Umbraco.Cms.Api.Management/Mapping/DataType/DataTypeViewModelMapDefinition.cs b/src/Umbraco.Cms.Api.Management/Mapping/DataType/DataTypeViewModelMapDefinition.cs new file mode 100644 index 0000000000..0848258943 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Mapping/DataType/DataTypeViewModelMapDefinition.cs @@ -0,0 +1,124 @@ +using Umbraco.Cms.Api.Management.ViewModels.DataType; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.PropertyEditors; +using Umbraco.Cms.Core.Serialization; +using Umbraco.Cms.Core.Services; + +namespace Umbraco.Cms.Api.Management.Mapping.DataType; + +public class DataTypeViewModelMapDefinition : IMapDefinition +{ + private readonly PropertyEditorCollection _propertyEditors; + private readonly IConfigurationEditorJsonSerializer _serializer; + private readonly IDataTypeService _dataTypeService; + + public DataTypeViewModelMapDefinition( + PropertyEditorCollection propertyEditors, + IConfigurationEditorJsonSerializer serializer, + IDataTypeService dataTypeService) + { + _propertyEditors = propertyEditors; + _serializer = serializer; + _dataTypeService = dataTypeService; + } + + public void DefineMaps(IUmbracoMapper mapper) + { + mapper.Define((_, _) => new DataTypeViewModel(), Map); + + mapper.Define(InitializeDataType, Map); + + mapper.Define(InitializeDataType, Map); + + IDataType InitializeDataType(T source, MapperContext _) where T : DataTypeModelBase => + new Core.Models.DataType(_propertyEditors[source.PropertyEditorAlias], _serializer) + { + CreateDate = DateTime.Now + }; + } + + // Umbraco.Code.MapAll + private void Map(IDataType source, DataTypeViewModel target, MapperContext context) + { + target.Key = source.Key; + target.ParentKey = _dataTypeService.GetContainer(source.ParentId)?.Key; + target.Name = source.Name ?? string.Empty; + target.PropertyEditorAlias = source.EditorAlias; + + IConfigurationEditor? configurationEditor = source.Editor?.GetConfigurationEditor(); + IDictionary configuration = configurationEditor?.ToConfigurationEditor(source.ConfigurationData) + ?? new Dictionary(); + + target.Data = configuration.Select(c => + new DataTypePropertyViewModel + { + Alias = c.Key, + Value = c.Value + }).ToArray(); + } + + // Umbraco.Code.MapAll -CreateDate -DeleteDate -UpdateDate -ParentId + // Umbraco.Code.MapAll -Id -Key -Path -CreatorId -Level -SortOrder + private void Map(DataTypeUpdateModel source, IDataType target, MapperContext context) + { + IDataEditor editor = GetRequiredDataEditor(source); + + target.Name = source.Name; + target.Editor = editor; + target.DatabaseType = GetEditorValueStorageType(editor); + target.ConfigurationData = MapConfigurationData(source, editor); + } + + // Umbraco.Code.MapAll -CreateDate -DeleteDate -UpdateDate + // Umbraco.Code.MapAll -Id -Key -Path -CreatorId -Level -SortOrder + private void Map(DataTypeCreateModel source, IDataType target, MapperContext context) + { + IDataEditor editor = GetRequiredDataEditor(source); + + target.Name = source.Name; + target.ParentId = MapParentId(source.ParentKey); + target.Editor = editor; + target.DatabaseType = GetEditorValueStorageType(editor); + target.ConfigurationData = MapConfigurationData(source, editor); + } + + private IDataEditor GetRequiredDataEditor(T source) where T : DataTypeModelBase + { + if (_propertyEditors.TryGet(source.PropertyEditorAlias, out IDataEditor? editor) == false) + { + throw new InvalidOperationException($"Could not find a property editor with alias \"{source.PropertyEditorAlias}\"."); + } + + return editor; + } + + private ValueStorageType GetEditorValueStorageType(IDataEditor editor) + { + var valueType = editor.GetValueEditor().ValueType; + return ValueTypes.ToStorageType(valueType); + } + + private IDictionary MapConfigurationData(T source, IDataEditor editor) where T : DataTypeModelBase + { + var configuration = source + .Data + .Where(p => p.Value is not null) + .ToDictionary(p => p.Alias, p => p.Value!); + IConfigurationEditor? configurationEditor = editor?.GetConfigurationEditor(); + return configurationEditor?.FromConfigurationEditor(configuration) + ?? new Dictionary(); + } + + private int MapParentId(Guid? parentKey) + { + if (parentKey == null) + { + return Constants.System.Root; + } + + EntityContainer? container = _dataTypeService.GetContainer(parentKey.Value); + return container?.Id ?? throw new InvalidOperationException($"Could not find a parent container with key \"{parentKey}\"."); + } +} diff --git a/src/Umbraco.Cms.Api.Management/OpenApi.json b/src/Umbraco.Cms.Api.Management/OpenApi.json index 9f9cc9a95e..eebef272e2 100644 --- a/src/Umbraco.Cms.Api.Management/OpenApi.json +++ b/src/Umbraco.Cms.Api.Management/OpenApi.json @@ -44,6 +44,134 @@ } } }, + "/umbraco/management/api/v1/data-type": { + "post": { + "tags": [ + "Data Type" + ], + "operationId": "PostDataType", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DataTypeCreateModel" + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DataType" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + } + } + } + }, + "/umbraco/management/api/v1/data-type/{key}": { + "get": { + "tags": [ + "Data Type" + ], + "operationId": "GetDataTypeByKey", + "parameters": [ + { + "name": "key", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DataType" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundResult" + } + } + } + } + } + }, + "put": { + "tags": [ + "Data Type" + ], + "operationId": "PutDataTypeByKey", + "parameters": [ + { + "name": "key", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DataTypeUpdateModel" + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DataType" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + } + } + } + }, "/umbraco/management/api/v1/tree/data-type/children": { "get": { "tags": [ @@ -489,7 +617,7 @@ ], "operationId": "PostDictionaryUpload", "requestBody": { - "content": {} + "content": { } }, "responses": { "200": { @@ -4398,6 +4526,96 @@ }, "additionalProperties": false }, + "DataType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true + }, + "propertyEditorAlias": { + "type": "string", + "nullable": true + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DataTypeProperty" + }, + "nullable": true + }, + "key": { + "type": "string", + "format": "uuid" + }, + "parentKey": { + "type": "string", + "format": "uuid", + "nullable": true + } + }, + "additionalProperties": false + }, + "DataTypeCreateModel": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true + }, + "propertyEditorAlias": { + "type": "string", + "nullable": true + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DataTypeProperty" + }, + "nullable": true + }, + "parentKey": { + "type": "string", + "format": "uuid", + "nullable": true + } + }, + "additionalProperties": false + }, + "DataTypeProperty": { + "type": "object", + "properties": { + "alias": { + "type": "string", + "nullable": true + }, + "value": { + "nullable": true + } + }, + "additionalProperties": false + }, + "DataTypeUpdateModel": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true + }, + "propertyEditorAlias": { + "type": "string", + "nullable": true + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DataTypeProperty" + }, + "nullable": true + } + }, + "additionalProperties": false + }, "DatabaseInstall": { "required": [ "id", @@ -5318,7 +5536,7 @@ }, "providerProperties": { "type": "object", - "additionalProperties": {}, + "additionalProperties": { }, "nullable": true } }, @@ -6464,7 +6682,7 @@ "nullable": true } }, - "additionalProperties": {} + "additionalProperties": { } }, "ProfilingStatus": { "type": "object", @@ -7630,7 +7848,7 @@ "authorizationCode": { "authorizationUrl": "/umbraco/management/api/v1.0/security/back-office/authorize", "tokenUrl": "/umbraco/management/api/v1.0/security/back-office/token", - "scopes": {} + "scopes": { } } } } @@ -7638,7 +7856,7 @@ }, "security": [ { - "OAuth": [] + "OAuth": [ ] } ] } diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/DataType/DataTypeCreateModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/DataType/DataTypeCreateModel.cs new file mode 100644 index 0000000000..700f26907d --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/DataType/DataTypeCreateModel.cs @@ -0,0 +1,6 @@ +namespace Umbraco.Cms.Api.Management.ViewModels.DataType; + +public class DataTypeCreateModel : DataTypeModelBase +{ + public Guid? ParentKey { get; set; } +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/DataType/DataTypeModelBase.cs b/src/Umbraco.Cms.Api.Management/ViewModels/DataType/DataTypeModelBase.cs new file mode 100644 index 0000000000..d5f4741fdd --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/DataType/DataTypeModelBase.cs @@ -0,0 +1,10 @@ +namespace Umbraco.Cms.Api.Management.ViewModels.DataType; + +public abstract class DataTypeModelBase +{ + public string Name { get; set; } = string.Empty; + + public string PropertyEditorAlias { get; set; } = string.Empty; + + public IEnumerable Data { get; set; } = null!; +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/DataType/DataTypePropertyViewModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/DataType/DataTypePropertyViewModel.cs new file mode 100644 index 0000000000..022b2b3b06 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/DataType/DataTypePropertyViewModel.cs @@ -0,0 +1,8 @@ +namespace Umbraco.Cms.Api.Management.ViewModels.DataType; + +public class DataTypePropertyViewModel +{ + public string Alias { get; init; } = string.Empty; + + public object? Value { get; init; } +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/DataType/DataTypeUpdateModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/DataType/DataTypeUpdateModel.cs new file mode 100644 index 0000000000..3e19df8041 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/DataType/DataTypeUpdateModel.cs @@ -0,0 +1,5 @@ +namespace Umbraco.Cms.Api.Management.ViewModels.DataType; + +public class DataTypeUpdateModel : DataTypeModelBase +{ +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/DataType/DataTypeViewModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/DataType/DataTypeViewModel.cs new file mode 100644 index 0000000000..77a0a8df25 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/DataType/DataTypeViewModel.cs @@ -0,0 +1,8 @@ +namespace Umbraco.Cms.Api.Management.ViewModels.DataType; + +public class DataTypeViewModel : DataTypeModelBase +{ + public Guid Key { get; set; } + + public Guid? ParentKey { get; set; } +} diff --git a/src/Umbraco.Core/Cache/ValueEditorCache.cs b/src/Umbraco.Core/Cache/ValueEditorCache.cs index 358134ab14..f555b76f55 100644 --- a/src/Umbraco.Core/Cache/ValueEditorCache.cs +++ b/src/Umbraco.Core/Cache/ValueEditorCache.cs @@ -30,12 +30,12 @@ public class ValueEditorCache : IValueEditorCache return valueEditor; } - valueEditor = editor.GetValueEditor(dataType.Configuration); + valueEditor = editor.GetValueEditor(dataType.ConfigurationObject); dataEditorCache[dataType.Id] = valueEditor; return valueEditor; } - valueEditor = editor.GetValueEditor(dataType.Configuration); + valueEditor = editor.GetValueEditor(dataType.ConfigurationObject); _valueEditorCache[editor.Alias] = new Dictionary { [dataType.Id] = valueEditor }; return valueEditor; } diff --git a/src/Umbraco.Core/CompatibilitySuppressions.xml b/src/Umbraco.Core/CompatibilitySuppressions.xml index f580fa9458..7dd3c27092 100644 --- a/src/Umbraco.Core/CompatibilitySuppressions.xml +++ b/src/Umbraco.Core/CompatibilitySuppressions.xml @@ -28,6 +28,181 @@ lib/net7.0/Umbraco.Core.dll true + + CP0002 + M:Umbraco.Cms.Core.Models.ContentEditing.ContentPropertyDisplay.get_ConfigNullable + lib/net7.0/Umbraco.Core.dll + lib/net7.0/Umbraco.Core.dll + true + + + CP0002 + M:Umbraco.Cms.Core.Models.ContentEditing.ContentPropertyDisplay.set_ConfigNullable(System.Collections.Generic.IDictionary{System.String,System.Object}) + lib/net7.0/Umbraco.Core.dll + lib/net7.0/Umbraco.Core.dll + true + + + CP0002 + M:Umbraco.Cms.Core.Models.DataType.get_Configuration + lib/net7.0/Umbraco.Core.dll + lib/net7.0/Umbraco.Core.dll + true + + + CP0002 + M:Umbraco.Cms.Core.Models.DataType.set_Configuration(System.Object) + lib/net7.0/Umbraco.Core.dll + lib/net7.0/Umbraco.Core.dll + true + + + CP0002 + M:Umbraco.Cms.Core.Models.DataType.SetLazyConfiguration(System.String) + lib/net7.0/Umbraco.Core.dll + lib/net7.0/Umbraco.Core.dll + true + + + CP0002 + M:Umbraco.Cms.Core.Models.IDataType.get_Configuration + lib/net7.0/Umbraco.Core.dll + lib/net7.0/Umbraco.Core.dll + true + + + CP0002 + M:Umbraco.Cms.Core.Models.IDataType.set_Configuration(System.Object) + lib/net7.0/Umbraco.Core.dll + lib/net7.0/Umbraco.Core.dll + true + + + CP0002 + M:Umbraco.Cms.Core.PropertyEditors.ConfigurationEditor.FromConfigurationEditor(System.Collections.Generic.IDictionary{System.String,System.Object},System.Object) + lib/net7.0/Umbraco.Core.dll + lib/net7.0/Umbraco.Core.dll + true + + + CP0002 + M:Umbraco.Cms.Core.PropertyEditors.ConfigurationEditor.get_DefaultConfigurationObject + lib/net7.0/Umbraco.Core.dll + lib/net7.0/Umbraco.Core.dll + true + + + CP0002 + M:Umbraco.Cms.Core.PropertyEditors.ConfigurationEditor.IsConfiguration(System.Object) + lib/net7.0/Umbraco.Core.dll + lib/net7.0/Umbraco.Core.dll + true + + + CP0002 + M:Umbraco.Cms.Core.PropertyEditors.ConfigurationEditor.ToConfigurationEditor(System.Object) + lib/net7.0/Umbraco.Core.dll + lib/net7.0/Umbraco.Core.dll + true + + + CP0002 + M:Umbraco.Cms.Core.PropertyEditors.ConfigurationEditor.ToDatabase(System.Object,Umbraco.Cms.Core.Serialization.IConfigurationEditorJsonSerializer) + lib/net7.0/Umbraco.Core.dll + lib/net7.0/Umbraco.Core.dll + true + + + CP0002 + M:Umbraco.Cms.Core.PropertyEditors.ConfigurationEditor.ToValueEditor(System.Object) + lib/net7.0/Umbraco.Core.dll + lib/net7.0/Umbraco.Core.dll + true + + + CP0002 + M:Umbraco.Cms.Core.PropertyEditors.ConfigurationEditor`1.FromConfigurationEditor(System.Collections.Generic.IDictionary{System.String,System.Object},`0) + lib/net7.0/Umbraco.Core.dll + lib/net7.0/Umbraco.Core.dll + true + + + CP0002 + M:Umbraco.Cms.Core.PropertyEditors.ConfigurationEditor`1.ToConfigurationEditor(`0) + lib/net7.0/Umbraco.Core.dll + lib/net7.0/Umbraco.Core.dll + true + + + CP0002 + M:Umbraco.Cms.Core.PropertyEditors.IConfigurationEditor.FromConfigurationEditor(System.Collections.Generic.IDictionary{System.String,System.Object},System.Object) + lib/net7.0/Umbraco.Core.dll + lib/net7.0/Umbraco.Core.dll + true + + + CP0002 + M:Umbraco.Cms.Core.PropertyEditors.IConfigurationEditor.get_DefaultConfigurationObject + lib/net7.0/Umbraco.Core.dll + lib/net7.0/Umbraco.Core.dll + true + + + CP0002 + M:Umbraco.Cms.Core.PropertyEditors.IConfigurationEditor.IsConfiguration(System.Object) + lib/net7.0/Umbraco.Core.dll + lib/net7.0/Umbraco.Core.dll + true + + + CP0002 + M:Umbraco.Cms.Core.PropertyEditors.IConfigurationEditor.ToConfigurationEditor(System.Object) + lib/net7.0/Umbraco.Core.dll + lib/net7.0/Umbraco.Core.dll + true + + + CP0002 + M:Umbraco.Cms.Core.PropertyEditors.IConfigurationEditor.ToConfigurationEditorNullable(System.Object) + lib/net7.0/Umbraco.Core.dll + lib/net7.0/Umbraco.Core.dll + true + + + CP0002 + M:Umbraco.Cms.Core.PropertyEditors.IConfigurationEditor.ToValueEditor(System.Object) + lib/net7.0/Umbraco.Core.dll + lib/net7.0/Umbraco.Core.dll + true + + + CP0002 + M:Umbraco.Cms.Core.PropertyEditors.MultipleTextStringConfiguration.get_Maximum + lib/net7.0/Umbraco.Core.dll + lib/net7.0/Umbraco.Core.dll + true + + + CP0002 + M:Umbraco.Cms.Core.PropertyEditors.MultipleTextStringConfiguration.get_Minimum + lib/net7.0/Umbraco.Core.dll + lib/net7.0/Umbraco.Core.dll + true + + + CP0002 + M:Umbraco.Cms.Core.PropertyEditors.MultipleTextStringConfiguration.set_Maximum(System.Int32) + lib/net7.0/Umbraco.Core.dll + lib/net7.0/Umbraco.Core.dll + true + + + CP0002 + M:Umbraco.Cms.Core.PropertyEditors.MultipleTextStringConfiguration.set_Minimum(System.Int32) + lib/net7.0/Umbraco.Core.dll + lib/net7.0/Umbraco.Core.dll + true + CP0006 M:Umbraco.Cms.Core.Deploy.IDataTypeConfigurationConnector.FromArtifact(Umbraco.Cms.Core.Models.IDataType,System.String,Umbraco.Cms.Core.Deploy.IContextCache) @@ -70,4 +245,60 @@ lib/net7.0/Umbraco.Core.dll true + + CP0006 + M:Umbraco.Cms.Core.PropertyEditors.IConfigurationEditor.FromConfigurationEditor(System.Collections.Generic.IDictionary{System.String,System.Object}) + lib/net7.0/Umbraco.Core.dll + lib/net7.0/Umbraco.Core.dll + true + + + CP0006 + M:Umbraco.Cms.Core.PropertyEditors.IConfigurationEditor.FromConfigurationObject(System.Object,Umbraco.Cms.Core.Serialization.IConfigurationEditorJsonSerializer) + lib/net7.0/Umbraco.Core.dll + lib/net7.0/Umbraco.Core.dll + true + + + CP0006 + M:Umbraco.Cms.Core.PropertyEditors.IConfigurationEditor.ToConfigurationEditor(System.Collections.Generic.IDictionary{System.String,System.Object}) + lib/net7.0/Umbraco.Core.dll + lib/net7.0/Umbraco.Core.dll + true + + + CP0006 + M:Umbraco.Cms.Core.PropertyEditors.IConfigurationEditor.ToConfigurationObject(System.Collections.Generic.IDictionary{System.String,System.Object},Umbraco.Cms.Core.Serialization.IConfigurationEditorJsonSerializer) + lib/net7.0/Umbraco.Core.dll + lib/net7.0/Umbraco.Core.dll + true + + + CP0006 + M:Umbraco.Cms.Core.PropertyEditors.IConfigurationEditor.ToDatabase(System.Collections.Generic.IDictionary{System.String,System.Object},Umbraco.Cms.Core.Serialization.IConfigurationEditorJsonSerializer) + lib/net7.0/Umbraco.Core.dll + lib/net7.0/Umbraco.Core.dll + true + + + CP0006 + M:Umbraco.Cms.Core.PropertyEditors.IConfigurationEditor.ToValueEditor(System.Collections.Generic.IDictionary{System.String,System.Object}) + lib/net7.0/Umbraco.Core.dll + lib/net7.0/Umbraco.Core.dll + true + + + CP0006 + P:Umbraco.Cms.Core.Models.IDataType.ConfigurationData + lib/net7.0/Umbraco.Core.dll + lib/net7.0/Umbraco.Core.dll + true + + + CP0006 + P:Umbraco.Cms.Core.Models.IDataType.ConfigurationObject + lib/net7.0/Umbraco.Core.dll + lib/net7.0/Umbraco.Core.dll + true + \ No newline at end of file diff --git a/src/Umbraco.Core/ContentApps/ListViewContentAppFactory.cs b/src/Umbraco.Core/ContentApps/ListViewContentAppFactory.cs index 87c755fdc9..0e7119b858 100644 --- a/src/Umbraco.Core/ContentApps/ListViewContentAppFactory.cs +++ b/src/Umbraco.Core/ContentApps/ListViewContentAppFactory.cs @@ -80,7 +80,7 @@ public class ListViewContentAppFactory : IContentAppFactory throw new NullReferenceException("The property editor with alias " + dt.EditorAlias + " does not exist"); } - IDictionary listViewConfig = editor.GetConfigurationEditor().ToConfigurationEditorNullable(dt.Configuration); + IDictionary listViewConfig = editor.GetConfigurationEditor().ToConfigurationEditor(dt.ConfigurationData); // add the entity type to the config listViewConfig["entityType"] = entityType; @@ -122,7 +122,7 @@ public class ListViewContentAppFactory : IContentAppFactory Value = null, View = editor.GetValueEditor().View, HideLabel = true, - ConfigNullable = listViewConfig, + Config = listViewConfig, }, }; diff --git a/src/Umbraco.Core/Models/ContentEditing/ContentPropertyDisplay.cs b/src/Umbraco.Core/Models/ContentEditing/ContentPropertyDisplay.cs index 9368de8ce1..26e35270fa 100644 --- a/src/Umbraco.Core/Models/ContentEditing/ContentPropertyDisplay.cs +++ b/src/Umbraco.Core/Models/ContentEditing/ContentPropertyDisplay.cs @@ -29,14 +29,9 @@ public class ContentPropertyDisplay : ContentPropertyBasic [Required(AllowEmptyStrings = false)] public string? View { get; set; } - [Obsolete("The value type parameter of the dictionary will be made nullable in V11, use ConfigNullable instead.")] [DataMember(Name = "config")] public IDictionary? Config { get; set; } - // TODO: Obsolete in V11. - [IgnoreDataMember] - public IDictionary? ConfigNullable { get => Config!; set => Config = value!; } - [DataMember(Name = "hideLabel")] public bool HideLabel { get; set; } diff --git a/src/Umbraco.Core/Models/DataType.cs b/src/Umbraco.Core/Models/DataType.cs index 630ef338bd..9225306d3e 100644 --- a/src/Umbraco.Core/Models/DataType.cs +++ b/src/Umbraco.Core/Models/DataType.cs @@ -13,11 +13,11 @@ namespace Umbraco.Cms.Core.Models; public class DataType : TreeEntityBase, IDataType { private readonly IConfigurationEditorJsonSerializer _serializer; - private object? _configuration; - private string? _configurationJson; + private object? _configurationObject; + private IDictionary _configurationData; private ValueStorageType _databaseType; private IDataEditor? _editor; - private bool _hasConfiguration; + private bool _hasConfigurationObject; /// /// Initializes a new instance of the class. @@ -29,7 +29,7 @@ public class DataType : TreeEntityBase, IDataType ParentId = parentId; // set a default configuration - Configuration = _editor.GetConfigurationEditor().DefaultConfigurationObject; + _configurationData = _editor.GetConfigurationEditor().DefaultConfiguration; } /// @@ -45,25 +45,12 @@ public class DataType : TreeEntityBase, IDataType return; } - OnPropertyChanged(nameof(Editor)); - - // try to map the existing configuration to the new configuration - // simulate saving to db and reloading (ie go via json) - var configuration = Configuration; - var json = _serializer.Serialize(configuration); + // reset the configuration (force reload on next access using the new editor) + _configurationObject = null; + _hasConfigurationObject = false; _editor = value; - try - { - Configuration = _editor?.GetConfigurationEditor().FromDatabase(json, _serializer); - } - catch (Exception e) - { - throw new InvalidOperationException( - $"The configuration for data type {Id} : {EditorAlias} is invalid (see inner exception)." - + " Please fix the configuration and ensure it is valid. The site may fail to start and / or load data types and run.", - e); - } + OnPropertyChanged(nameof(Editor)); } } @@ -79,23 +66,38 @@ public class DataType : TreeEntityBase, IDataType set => SetPropertyValueAndDetectChanges(value, ref _databaseType, nameof(DatabaseType)); } + /// + public IDictionary ConfigurationData + { + get => _configurationData; + set + { + _configurationObject = null; + _hasConfigurationObject = false; + _configurationData = value; + + OnPropertyChanged(nameof(ConfigurationObject)); + OnPropertyChanged(nameof(ConfigurationData)); + } + } + /// [DataMember] - public object? Configuration + public object? ConfigurationObject { get { // if we know we have a configuration (which may be null), return it // if we don't have an editor, then we have no configuration, return null // else, use the editor to get the configuration object - if (_hasConfiguration) + if (_hasConfigurationObject) { - return _configuration; + return _configurationObject; } try { - _configuration = _editor?.GetConfigurationEditor().FromDatabase(_configurationJson, _serializer); + _configurationObject = _editor?.GetConfigurationEditor().ToConfigurationObject(_configurationData, _serializer); } catch (Exception e) { @@ -105,68 +107,16 @@ public class DataType : TreeEntityBase, IDataType e); } - _hasConfiguration = true; - _configurationJson = null; + _hasConfigurationObject = true; - return _configuration; - } - - set - { - if (value == null) - { - throw new ArgumentNullException(nameof(value)); - } - - // we don't support re-assigning the same object - // configurations are kinda non-mutable, mainly because detecting changes would be a pain - // reference comparison - if (_configuration == value) - { - throw new ArgumentException( - "Configurations are kinda non-mutable. Do not reassign the same object.", - nameof(value)); - } - - // validate configuration type - if (!_editor?.GetConfigurationEditor().IsConfiguration(value) ?? true) - { - throw new ArgumentException( - $"Value of type {value.GetType().Name} cannot be a configuration for editor {_editor?.Alias}, expecting.", - nameof(value)); - } - - // extract database type from configuration object, if appropriate - if (value is IConfigureValueType valueTypeConfiguration) - { - DatabaseType = ValueTypes.ToStorageType(valueTypeConfiguration.ValueType); - } - - // extract database type from dictionary, if appropriate - if (value is IDictionary dictionaryConfiguration - && dictionaryConfiguration.TryGetValue( - Constants.PropertyEditors.ConfigurationKeys.DataValueType, - out var valueTypeObject) - && valueTypeObject is string valueTypeString - && ValueTypes.IsValue(valueTypeString)) - { - DatabaseType = ValueTypes.ToStorageType(valueTypeString); - } - - _configuration = value; - _hasConfiguration = true; - _configurationJson = null; - - // it's always a change - OnPropertyChanged(nameof(Configuration)); + return _configurationObject; } } /// - /// Lazily set the configuration as a serialized json string. + /// Sets the configuration without invoking property changed events. /// /// - /// Will be de-serialized on-demand. /// /// This method is meant to be used when building entities from database, exclusively. /// It does NOT register a property change to dirty. It ignores the fact that the configuration @@ -175,11 +125,11 @@ public class DataType : TreeEntityBase, IDataType /// /// Think before using! /// - public void SetLazyConfiguration(string? configurationJson) + public void SetConfigurationData(IDictionary configurationData) { - _hasConfiguration = false; - _configuration = null; - _configurationJson = configurationJson; + _hasConfigurationObject = false; + _configurationObject = null; + _configurationData = configurationData; } /// @@ -190,26 +140,26 @@ public class DataType : TreeEntityBase, IDataType /// This method is meant to be used when creating published datatypes, exclusively. /// Think before using! /// - internal Lazy GetLazyConfiguration() + internal Lazy GetLazyConfigurationObject() { // note: in both cases, make sure we capture what we need - we don't want // to capture a reference to this full, potentially heavy, DataType instance. - if (_hasConfiguration) + if (_hasConfigurationObject) { // if configuration has already been de-serialized, return - var capturedConfiguration = _configuration; + var capturedConfiguration = _configurationObject; return new Lazy(() => capturedConfiguration); } else { // else, create a Lazy de-serializer - var capturedConfiguration = _configurationJson; + IDictionary capturedConfiguration = _configurationData; IDataEditor? capturedEditor = _editor; return new Lazy(() => { try { - return capturedEditor?.GetConfigurationEditor().FromDatabase(capturedConfiguration, _serializer); + return capturedEditor?.GetConfigurationEditor().ToConfigurationObject(capturedConfiguration, _serializer); } catch (Exception e) { diff --git a/src/Umbraco.Core/Models/DataTypeExtensions.cs b/src/Umbraco.Core/Models/DataTypeExtensions.cs index 791f7b248b..22b5aba126 100644 --- a/src/Umbraco.Core/Models/DataTypeExtensions.cs +++ b/src/Umbraco.Core/Models/DataTypeExtensions.cs @@ -60,7 +60,7 @@ public static class DataTypeExtensions throw new ArgumentNullException(nameof(dataType)); } - var configuration = dataType.Configuration; + var configuration = dataType.ConfigurationObject; switch (configuration) { diff --git a/src/Umbraco.Core/Models/IDataType.cs b/src/Umbraco.Core/Models/IDataType.cs index 64ffe6489d..5d2bb1044b 100644 --- a/src/Umbraco.Core/Models/IDataType.cs +++ b/src/Umbraco.Core/Models/IDataType.cs @@ -28,14 +28,17 @@ public interface IDataType : IUmbracoEntity, IRememberBeingDirty ValueStorageType DatabaseType { get; set; } /// - /// Gets or sets the configuration object. + /// Gets or sets the configuration data. + /// + IDictionary ConfigurationData { get; set; } + + /// + /// Gets an object representation of the configuration data. /// /// - /// The configuration object is serialized to Json and stored into the database. - /// The serialized Json is deserialized by the property editor, which by default should - /// return a Dictionary{string, object} but could return a typed object e.g. MyEditor.Configuration. + /// The object type is dictated by the underlying implementation of the . /// - object? Configuration { get; set; } + object? ConfigurationObject { get; } /// /// Creates a deep clone of the current entity with its identity/alias reset diff --git a/src/Umbraco.Core/Models/Mapping/ContentPropertyDisplayMapper.cs b/src/Umbraco.Core/Models/Mapping/ContentPropertyDisplayMapper.cs index 22407219eb..0a182968f9 100644 --- a/src/Umbraco.Core/Models/Mapping/ContentPropertyDisplayMapper.cs +++ b/src/Umbraco.Core/Models/Mapping/ContentPropertyDisplayMapper.cs @@ -38,7 +38,7 @@ internal class ContentPropertyDisplayMapper : ContentPropertyBasicMapper(configurationEditor.Fields) .WhereNotNull().ToList(); - IDictionary configurationDictionary = - configurationEditor.ToConfigurationEditor(dataType.Configuration); + IDictionary configurationDictionary = configurationEditor.ToConfigurationEditor(dataType.ConfigurationData); MapConfigurationFields(dataType, fields, configurationDictionary); diff --git a/src/Umbraco.Core/Models/Mapping/MacroMapDefinition.cs b/src/Umbraco.Core/Models/Mapping/MacroMapDefinition.cs index a042497013..62256a9bf0 100644 --- a/src/Umbraco.Core/Models/Mapping/MacroMapDefinition.cs +++ b/src/Umbraco.Core/Models/Mapping/MacroMapDefinition.cs @@ -79,11 +79,8 @@ public class MacroMapDefinition : IMapDefinition target.View = paramEditor?.GetValueEditor().View; // sets the parameter configuration to be the default configuration editor's configuration, - // ie configurationEditor.DefaultConfigurationObject, prepared for the value editor, ie - // after ToValueEditor - important to use DefaultConfigurationObject here, because depending - // on editors, ToValueEditor expects the actual strongly typed configuration - not the - // dictionary thing returned by DefaultConfiguration + // ie configurationEditor.DefaultConfigurationObject, prepared for the value editor IConfigurationEditor? configurationEditor = paramEditor?.GetConfigurationEditor(); - target.Configuration = configurationEditor?.ToValueEditor(configurationEditor.DefaultConfigurationObject); + target.Configuration = configurationEditor?.FromConfigurationEditor(configurationEditor.DefaultConfiguration); } } diff --git a/src/Umbraco.Core/Models/Mapping/PropertyTypeGroupMapper.cs b/src/Umbraco.Core/Models/Mapping/PropertyTypeGroupMapper.cs index cb77d790cd..da4383ed1d 100644 --- a/src/Umbraco.Core/Models/Mapping/PropertyTypeGroupMapper.cs +++ b/src/Umbraco.Core/Models/Mapping/PropertyTypeGroupMapper.cs @@ -255,7 +255,7 @@ public class PropertyTypeGroupMapper } IDictionary? config = propertyEditor is null || dataType is null ? new Dictionary() - : dataType.Editor?.GetConfigurationEditor().ToConfigurationEditor(dataType.Configuration); + : dataType.Editor?.GetConfigurationEditor().ToConfigurationEditor(dataType.ConfigurationData); mappedProperties.Add(new TPropertyType { diff --git a/src/Umbraco.Core/Models/PropertyTagsExtensions.cs b/src/Umbraco.Core/Models/PropertyTagsExtensions.cs index b12cbe58ef..611ca35e8a 100644 --- a/src/Umbraco.Core/Models/PropertyTagsExtensions.cs +++ b/src/Umbraco.Core/Models/PropertyTagsExtensions.cs @@ -26,7 +26,7 @@ public static class PropertyTagsExtensions var configurationObject = property.PropertyType is null ? null - : dataTypeService.GetDataType(property.PropertyType.DataTypeId)?.Configuration; + : dataTypeService.GetDataType(property.PropertyType.DataTypeId)?.ConfigurationObject; TagConfiguration? configuration = configurationObject as TagConfiguration; if (configuration is not null && configuration.Delimiter == default) diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedContentTypeFactory.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedContentTypeFactory.cs index f2b1b9bbca..733ec168e1 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedContentTypeFactory.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedContentTypeFactory.cs @@ -137,5 +137,5 @@ public class PublishedContentTypeFactory : IPublishedContentTypeFactory new PublishedPropertyType(propertyTypeAlias, dataTypeId, umbraco, variations, _propertyValueConverters, _publishedModelFactory, this); private PublishedDataType CreatePublishedDataType(IDataType dataType) - => new(dataType.Id, dataType.EditorAlias, dataType is DataType d ? d.GetLazyConfiguration() : new Lazy(() => dataType.Configuration)); + => new(dataType.Id, dataType.EditorAlias, dataType is DataType d ? d.GetLazyConfigurationObject() : new Lazy(() => dataType.ConfigurationObject)); } diff --git a/src/Umbraco.Core/PropertyEditors/ConfigurationEditor.cs b/src/Umbraco.Core/PropertyEditors/ConfigurationEditor.cs index 25aeb93418..9918dfe42e 100644 --- a/src/Umbraco.Core/PropertyEditors/ConfigurationEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/ConfigurationEditor.cs @@ -1,5 +1,6 @@ using System.Runtime.Serialization; using Umbraco.Cms.Core.Serialization; +using Umbraco.Extensions; namespace Umbraco.Cms.Core.PropertyEditors; @@ -44,15 +45,49 @@ public class ConfigurationEditor : IConfigurationEditor } /// - public virtual object? DefaultConfigurationObject => DefaultConfiguration; + public virtual IDictionary ToConfigurationEditor(IDictionary configuration) + => configuration; + + /// + public virtual IDictionary FromConfigurationEditor(IDictionary configuration) + => configuration; + + /// + public virtual IDictionary ToValueEditor(IDictionary configuration) + => ToConfigurationEditor(configuration); + + /// + public virtual object ToConfigurationObject( + IDictionary configuration, + IConfigurationEditorJsonSerializer configurationEditorJsonSerializer) => configuration; + + /// + public virtual IDictionary FromConfigurationObject( + object configuration, + IConfigurationEditorJsonSerializer configurationEditorJsonSerializer) + => configurationEditorJsonSerializer.Deserialize>(configurationEditorJsonSerializer.Serialize(configuration)) ?? new Dictionary(); + + /// + public virtual string ToDatabase( + IDictionary configuration, + IConfigurationEditorJsonSerializer configurationEditorJsonSerializer) + => configurationEditorJsonSerializer.Serialize(configuration); + + /// + public virtual IDictionary FromDatabase( + string? configuration, + IConfigurationEditorJsonSerializer configurationEditorJsonSerializer) + => configuration.IsNullOrWhiteSpace() ? new Dictionary() : configurationEditorJsonSerializer.Deserialize>(configuration) ?? new Dictionary(); /// - /// Converts a configuration object into a serialized database value. + /// Gets a field by its property name. /// - public static string? ToDatabase( - object? configuration, - IConfigurationEditorJsonSerializer configurationEditorJsonSerializer) - => configuration == null ? null : configurationEditorJsonSerializer.Serialize(configuration); + /// + /// Can be used in constructors to add infos to a field that has been defined + /// by a property marked with the . + /// + protected ConfigurationField Field(string name) + => Fields.First(x => x.PropertyName == name); /// /// Gets the configuration as a typed object. @@ -72,78 +107,4 @@ public class ConfigurationEditor : IConfigurationEditor throw new InvalidCastException( $"Cannot cast configuration of type {obj.GetType().Name} to {typeof(TConfiguration).Name}."); } - - /// - public virtual bool IsConfiguration(object obj) => obj is IDictionary; - - /// - public virtual object FromDatabase( - string? configurationJson, - IConfigurationEditorJsonSerializer configurationEditorJsonSerializer) - => string.IsNullOrWhiteSpace(configurationJson) - ? new Dictionary() - : configurationEditorJsonSerializer.Deserialize>(configurationJson)!; - - /// - public virtual object? FromConfigurationEditor(IDictionary? editorValues, object? configuration) - { - // by default, return the posted dictionary - // but only keep entries that have a non-null/empty value - // rest will fall back to default during ToConfigurationEditor() - var keys = editorValues?.Where(x => - x.Value == null || (x.Value is string stringValue && string.IsNullOrWhiteSpace(stringValue))) - .Select(x => x.Key).ToList(); - - if (keys is not null) - { - foreach (var key in keys) - { - editorValues?.Remove(key); - } - } - - return editorValues; - } - - /// - public virtual IDictionary ToConfigurationEditor(object? configuration) - { - // editors that do not override ToEditor/FromEditor have their configuration - // as a dictionary of and, by default, we merge their default - // configuration with their current configuration - if (configuration == null) - { - configuration = new Dictionary(); - } - - if (!(configuration is IDictionary c)) - { - throw new ArgumentException( - $"Expecting a {typeof(Dictionary).Name} instance but got {configuration.GetType().Name}.", - nameof(configuration)); - } - - // clone the default configuration, and apply the current configuration values - var d = new Dictionary(DefaultConfiguration); - foreach ((string key, object value) in c) - { - d[key] = value; - } - - return d; - } - - /// - public virtual IDictionary ToValueEditor(object? configuration) - => ToConfigurationEditor(configuration); - - /// - /// Gets a field by its property name. - /// - /// - /// Can be used in constructors to add infos to a field that has been defined - /// by a property marked with the . - /// - protected ConfigurationField Field(string name) - => Fields.First(x => x.PropertyName == name); } diff --git a/src/Umbraco.Core/PropertyEditors/ConfigurationEditorOfTConfiguration.cs b/src/Umbraco.Core/PropertyEditors/ConfigurationEditorOfTConfiguration.cs index f2a3afdbe3..9003116328 100644 --- a/src/Umbraco.Core/PropertyEditors/ConfigurationEditorOfTConfiguration.cs +++ b/src/Umbraco.Core/PropertyEditors/ConfigurationEditorOfTConfiguration.cs @@ -35,29 +35,20 @@ public abstract class ConfigurationEditor : ConfigurationEditor _editorConfigurationParser = editorConfigurationParser; /// - public override IDictionary DefaultConfiguration => - ToConfigurationEditor(DefaultConfigurationObject); - - /// - public override object DefaultConfigurationObject => new TConfiguration(); - - /// - public override bool IsConfiguration(object obj) - => obj is TConfiguration; - - /// - public override object FromDatabase( - string? configuration, + public override object ToConfigurationObject( + IDictionary configuration, IConfigurationEditorJsonSerializer configurationEditorJsonSerializer) { try { - if (string.IsNullOrWhiteSpace(configuration)) + if (configuration.Any() == false) { return new TConfiguration(); } - return configurationEditorJsonSerializer.Deserialize(configuration)!; + // TODO: quick fix for now (serialize to JSON, then deserialize to TConfiguration) - see if there is a better/more performant way (reverse of ObjectJsonExtensions.ToObjectDictionary) + var json = configurationEditorJsonSerializer.Serialize(configuration); + return configurationEditorJsonSerializer.Deserialize(json) ?? new TConfiguration(); } catch (Exception e) { @@ -67,32 +58,6 @@ public abstract class ConfigurationEditor : ConfigurationEditor } } - /// - public sealed override object? FromConfigurationEditor( - IDictionary? editorValues, - object? configuration) => FromConfigurationEditor(editorValues, (TConfiguration?)configuration); - - /// - /// Converts the configuration posted by the editor. - /// - /// The configuration object posted by the editor. - /// The current configuration object. - public virtual TConfiguration? FromConfigurationEditor( - IDictionary? editorValues, - TConfiguration? configuration) => - _editorConfigurationParser.ParseFromConfigurationEditor(editorValues, Fields); - - /// - public sealed override IDictionary ToConfigurationEditor(object? configuration) => - ToConfigurationEditor((TConfiguration?)configuration); - - /// - /// Converts configuration values to values for the editor. - /// - /// The configuration. - public virtual Dictionary ToConfigurationEditor(TConfiguration? configuration) => - _editorConfigurationParser.ParseToConfigurationEditor(configuration); - /// /// Discovers fields from configuration properties marked with the field attribute. /// diff --git a/src/Umbraco.Core/PropertyEditors/ContentPickerConfigurationEditor.cs b/src/Umbraco.Core/PropertyEditors/ContentPickerConfigurationEditor.cs index 3bffa4ad61..9d3e7eedf6 100644 --- a/src/Umbraco.Core/PropertyEditors/ContentPickerConfigurationEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/ContentPickerConfigurationEditor.cs @@ -17,17 +17,17 @@ internal class ContentPickerConfigurationEditor : ConfigurationEditor { { "idType", "udi" } }; - public override IDictionary ToValueEditor(object? configuration) + public override IDictionary ToValueEditor(IDictionary configuration) { // get the configuration fields - IDictionary d = base.ToValueEditor(configuration); + IDictionary config = base.ToValueEditor(configuration); // add extra fields // not part of ContentPickerConfiguration but used to configure the UI editor - d["showEditButton"] = false; - d["showPathOnHover"] = false; - d["idType"] = "udi"; + config["showEditButton"] = false; + config["showPathOnHover"] = false; + config["idType"] = "udi"; - return d; + return config; } } diff --git a/src/Umbraco.Core/PropertyEditors/DateTimeConfigurationEditor.cs b/src/Umbraco.Core/PropertyEditors/DateTimeConfigurationEditor.cs index cb238982e0..7f2c20d742 100644 --- a/src/Umbraco.Core/PropertyEditors/DateTimeConfigurationEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/DateTimeConfigurationEditor.cs @@ -29,14 +29,14 @@ public class DateTimeConfigurationEditor : ConfigurationEditor ToValueEditor(object? configuration) + public override IDictionary ToValueEditor(IDictionary configuration) { - IDictionary d = base.ToValueEditor(configuration); + IDictionary config = base.ToValueEditor(configuration); - var format = d["format"].ToString()!; + var format = config["format"].ToString()!; - d["pickTime"] = format.ContainsAny(new[] { "H", "m", "s" }); + config["pickTime"] = format.ContainsAny(new[] { "H", "m", "s" }); - return d; + return config; } } diff --git a/src/Umbraco.Core/PropertyEditors/EyeDropperColorPickerConfigurationEditor.cs b/src/Umbraco.Core/PropertyEditors/EyeDropperColorPickerConfigurationEditor.cs index 487034a6b1..4a15e1ef3b 100644 --- a/src/Umbraco.Core/PropertyEditors/EyeDropperColorPickerConfigurationEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/EyeDropperColorPickerConfigurationEditor.cs @@ -1,6 +1,5 @@ using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Services; -using Umbraco.Extensions; namespace Umbraco.Cms.Core.PropertyEditors; @@ -12,39 +11,4 @@ internal class EyeDropperColorPickerConfigurationEditor : ConfigurationEditor - public override Dictionary ToConfigurationEditor(EyeDropperColorPickerConfiguration? configuration) => - new() - { - { "showAlpha", configuration?.ShowAlpha ?? false }, { "showPalette", configuration?.ShowPalette ?? false }, - }; - - /// - public override EyeDropperColorPickerConfiguration FromConfigurationEditor( - IDictionary? editorValues, EyeDropperColorPickerConfiguration? configuration) - { - var showAlpha = true; - var showPalette = true; - - if (editorValues is not null && editorValues.TryGetValue("showAlpha", out var alpha)) - { - Attempt attempt = alpha.TryConvertTo(); - if (attempt.Success) - { - showAlpha = attempt.Result; - } - } - - if (editorValues is not null && editorValues.TryGetValue("showPalette", out var palette)) - { - Attempt attempt = palette.TryConvertTo(); - if (attempt.Success) - { - showPalette = attempt.Result; - } - } - - return new EyeDropperColorPickerConfiguration { ShowAlpha = showAlpha, ShowPalette = showPalette }; - } } diff --git a/src/Umbraco.Core/PropertyEditors/IConfigurationEditor.cs b/src/Umbraco.Core/PropertyEditors/IConfigurationEditor.cs index cbcb945c77..143897f583 100644 --- a/src/Umbraco.Core/PropertyEditors/IConfigurationEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/IConfigurationEditor.cs @@ -17,72 +17,61 @@ public interface IConfigurationEditor /// /// Gets the default configuration. /// - /// - /// - /// For basic configuration editors, this will be a dictionary of key/values. For advanced editors - /// which inherit from , this will be the dictionary - /// equivalent of an actual configuration object (ie an instance of TConfiguration, obtained - /// via . - /// - /// [DataMember(Name = "defaultConfig")] IDictionary DefaultConfiguration { get; } /// - /// Gets the default configuration object. + /// Converts the configuration data to values for the configuration editor. /// - /// - /// - /// For basic configuration editors, this will be , ie a - /// dictionary of key/values. For advanced editors which inherit from - /// , - /// this will be an actual configuration object (ie an instance of TConfiguration. - /// - /// - object? DefaultConfigurationObject { get; } + /// The configuration data. + IDictionary ToConfigurationEditor(IDictionary configuration); /// - /// Determines whether a configuration object is of the type expected by the configuration editor. - /// - bool IsConfiguration(object obj); - - // notes - // ToConfigurationEditor returns a dictionary, and FromConfigurationEditor accepts a dictionary. - // this is due to the way our front-end editors work, see DataTypeController.PostSave - // and DataTypeConfigurationFieldDisplayResolver - we are not going to change it now. - - /// - /// Converts the serialized database value into the actual configuration object. + /// Converts values from the configuration editor to configuration data. /// /// - /// Converting the configuration object to the serialized database value is - /// achieved by simply serializing the configuration. See . + /// Consider this the reverse of . /// - object FromDatabase( - string? configurationJson, + /// Values from the configuration editor. + IDictionary FromConfigurationEditor(IDictionary configuration); + + /// + /// Converts the configuration data to values for the value editor. + /// + /// The configuration data. + IDictionary ToValueEditor(IDictionary configuration); + + /// + /// Creates a configuration object from the configuration data. + /// + /// The configuration data. + /// The configuration serializer. + object ToConfigurationObject( + IDictionary configuration, IConfigurationEditorJsonSerializer configurationEditorJsonSerializer); /// - /// Converts the values posted by the configuration editor into the actual configuration object. + /// Creates configuration data from a configuration object. /// - /// The values posted by the configuration editor. - /// The current configuration object. - object? FromConfigurationEditor(IDictionary? editorValues, object? configuration); + /// The configuration object. + /// The configuration serializer. + IDictionary FromConfigurationObject( + object configuration, + IConfigurationEditorJsonSerializer configurationEditorJsonSerializer); /// - /// Converts the configuration object to values for the configuration editor. + /// Converts configuration data into a serialized database value. /// - /// The configuration. - [Obsolete("The value type parameter of the dictionary will be made nullable in V11, use ToConfigurationEditorNullable.")] - IDictionary ToConfigurationEditor(object? configuration); - - // TODO: Obsolete in V11. - IDictionary ToConfigurationEditorNullable(object? configuration) => - ToConfigurationEditor(configuration)!; + string ToDatabase( + IDictionary configuration, + IConfigurationEditorJsonSerializer configurationEditorJsonSerializer); /// - /// Converts the configuration object to values for the value editor. + /// Converts a serialized database value into configuration data. /// - /// The configuration. - IDictionary? ToValueEditor(object? configuration); + /// The serialized database value (JSON format). + /// The configuration serializer. + IDictionary FromDatabase( + string? configuration, + IConfigurationEditorJsonSerializer configurationEditorJsonSerializer); } diff --git a/src/Umbraco.Core/PropertyEditors/LabelConfigurationEditor.cs b/src/Umbraco.Core/PropertyEditors/LabelConfigurationEditor.cs index 477162bca4..b5a046b86e 100644 --- a/src/Umbraco.Core/PropertyEditors/LabelConfigurationEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/LabelConfigurationEditor.cs @@ -25,27 +25,27 @@ public class LabelConfigurationEditor : ConfigurationEditor { } - /// - public override LabelConfiguration FromConfigurationEditor( - IDictionary? editorValues, - LabelConfiguration? configuration) + public override IDictionary FromConfigurationEditor(IDictionary configuration) { - var newConfiguration = new LabelConfiguration(); + // default value + var valueType = new LabelConfiguration().ValueType; // get the value type // not simply deserializing Json because we want to validate the valueType - if (editorValues is not null && editorValues.TryGetValue( - Constants.PropertyEditors.ConfigurationKeys.DataValueType, - out var valueTypeObj) - && valueTypeObj is string stringValue) + if (configuration.TryGetValue( + Constants.PropertyEditors.ConfigurationKeys.DataValueType, + out var valueTypeObj) + && valueTypeObj is string stringValue) { // validate if (!string.IsNullOrWhiteSpace(stringValue) && ValueTypes.IsValue(stringValue)) { - newConfiguration.ValueType = stringValue; + valueType = stringValue; } } - return newConfiguration; + configuration[Constants.PropertyEditors.ConfigurationKeys.DataValueType] = valueType; + + return base.FromConfigurationEditor(configuration); } } diff --git a/src/Umbraco.Core/PropertyEditors/MediaPickerConfigurationEditor.cs b/src/Umbraco.Core/PropertyEditors/MediaPickerConfigurationEditor.cs index e4fa6f5c71..56decabc12 100644 --- a/src/Umbraco.Core/PropertyEditors/MediaPickerConfigurationEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/MediaPickerConfigurationEditor.cs @@ -32,15 +32,15 @@ public class MediaPickerConfigurationEditor : ConfigurationEditor { { "idType", "udi" } }; - public override IDictionary ToValueEditor(object? configuration) + public override IDictionary ToValueEditor(IDictionary configuration) { // get the configuration fields - IDictionary d = base.ToValueEditor(configuration); + IDictionary config = base.ToValueEditor(configuration); // add extra fields // not part of ContentPickerConfiguration but used to configure the UI editor - d["idType"] = "udi"; + config["idType"] = "udi"; - return d; + return config; } } diff --git a/src/Umbraco.Core/PropertyEditors/MultiNodePickerConfigurationEditor.cs b/src/Umbraco.Core/PropertyEditors/MultiNodePickerConfigurationEditor.cs index 3043bff9cf..60cc295210 100644 --- a/src/Umbraco.Core/PropertyEditors/MultiNodePickerConfigurationEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/MultiNodePickerConfigurationEditor.cs @@ -25,25 +25,28 @@ public class MultiNodePickerConfigurationEditor : ConfigurationEditor { { "idType", "udi" } }; - /// - public override Dictionary ToConfigurationEditor(MultiNodePickerConfiguration? configuration) + public override IDictionary ToConfigurationEditor(IDictionary configuration) { - // sanitize configuration - Dictionary output = base.ToConfigurationEditor(configuration); + IDictionary config = base.ToConfigurationEditor(configuration); + // TODO: this belongs on the client side! + if (config.TryGetValue("maxNumber", out var maxNumberValue) + && int.TryParse(maxNumberValue.ToString(), out var maxNumber) + && maxNumber > 1) + { + config["multiPicker"] = true; + } - output["multiPicker"] = configuration?.MaxNumber > 1; - - return output; + return config; } - /// - public override IDictionary ToValueEditor(object? configuration) + public override IDictionary ToValueEditor(IDictionary configuration) { - IDictionary d = base.ToValueEditor(configuration); - d["multiPicker"] = true; - d["showEditButton"] = false; - d["showPathOnHover"] = false; - d["idType"] = "udi"; - return d; + IDictionary config = base.ToValueEditor(configuration); + // TODO: this belongs on the client side! + config["multiPicker"] = true; + config["showEditButton"] = false; + config["showPathOnHover"] = false; + config["idType"] = "udi"; + return config; } } diff --git a/src/Umbraco.Core/PropertyEditors/MultipleTextStringConfiguration.cs b/src/Umbraco.Core/PropertyEditors/MultipleTextStringConfiguration.cs index 6c7f93374d..30ddde5ea5 100644 --- a/src/Umbraco.Core/PropertyEditors/MultipleTextStringConfiguration.cs +++ b/src/Umbraco.Core/PropertyEditors/MultipleTextStringConfiguration.cs @@ -6,7 +6,7 @@ namespace Umbraco.Cms.Core.PropertyEditors; public class MultipleTextStringConfiguration { // fields are configured in the editor - public int Minimum { get; set; } + public int Min { get; set; } - public int Maximum { get; set; } + public int Max { get; set; } } diff --git a/src/Umbraco.Core/PropertyEditors/TagConfigurationEditor.cs b/src/Umbraco.Core/PropertyEditors/TagConfigurationEditor.cs index 9c2a29d11a..4601f236e2 100644 --- a/src/Umbraco.Core/PropertyEditors/TagConfigurationEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/TagConfigurationEditor.cs @@ -29,34 +29,17 @@ public class TagConfigurationEditor : ConfigurationEditor Field(nameof(TagConfiguration.StorageType)).Validators.Add(new RequiredValidator(localizedTextService)); } - public override Dictionary ToConfigurationEditor(TagConfiguration? configuration) + public override IDictionary ToConfigurationEditor(IDictionary configuration) { - Dictionary dictionary = base.ToConfigurationEditor(configuration); + IDictionary config = base.ToConfigurationEditor(configuration); // the front-end editor expects the string value of the storage type - if (!dictionary.TryGetValue("storageType", out var storageType)) + // TODO: this (default value) belongs on the client side! + if (!config.ContainsKey("storageType")) { - storageType = TagsStorageType.Json; // default to Json + config["storageType"] = TagsStorageType.Json.ToString(); } - dictionary["storageType"] = storageType.ToString()!; - - return dictionary; - } - - public override TagConfiguration? FromConfigurationEditor( - IDictionary? editorValues, - TagConfiguration? configuration) - { - // the front-end editor returns the string value of the storage type - // pure Json could do with - // [JsonConverter(typeof(StringEnumConverter))] - // but here we're only deserializing to object and it's too late - if (editorValues is not null) - { - editorValues["storageType"] = Enum.Parse(typeof(TagsStorageType), (string)editorValues["storageType"]!); - } - - return base.FromConfigurationEditor(editorValues, configuration); + return config; } } diff --git a/src/Umbraco.Core/Services/DateTypeServiceExtensions.cs b/src/Umbraco.Core/Services/DateTypeServiceExtensions.cs index 476a2ddd47..c330a03772 100644 --- a/src/Umbraco.Core/Services/DateTypeServiceExtensions.cs +++ b/src/Umbraco.Core/Services/DateTypeServiceExtensions.cs @@ -15,7 +15,7 @@ public static class DateTypeServiceExtensions IDataType? dataType = dataTypeService.GetDataType(key); - if (dataType != null && dataType.Configuration is IIgnoreUserStartNodesConfig ignoreStartNodesConfig) + if (dataType != null && dataType.ConfigurationObject is IIgnoreUserStartNodesConfig ignoreStartNodesConfig) { return ignoreStartNodesConfig.IgnoreUserStartNodes; } diff --git a/src/Umbraco.Core/Services/EntityXmlSerializer.cs b/src/Umbraco.Core/Services/EntityXmlSerializer.cs index 60ad1f10ba..6e2e130770 100644 --- a/src/Umbraco.Core/Services/EntityXmlSerializer.cs +++ b/src/Umbraco.Core/Services/EntityXmlSerializer.cs @@ -213,7 +213,7 @@ internal class EntityXmlSerializer : IEntityXmlSerializer xml.Add(new XAttribute("Id", dataType.EditorAlias)); xml.Add(new XAttribute("Definition", dataType.Key)); xml.Add(new XAttribute("DatabaseType", dataType.DatabaseType.ToString())); - xml.Add(new XAttribute("Configuration", _configurationEditorJsonSerializer.Serialize(dataType.Configuration))); + xml.Add(new XAttribute("Configuration", _configurationEditorJsonSerializer.Serialize(dataType.ConfigurationObject))); var folderNames = string.Empty; var folderKeys = string.Empty; diff --git a/src/Umbraco.Core/Services/PropertyValidationService.cs b/src/Umbraco.Core/Services/PropertyValidationService.cs index fc42b13232..a3e0e63910 100644 --- a/src/Umbraco.Core/Services/PropertyValidationService.cs +++ b/src/Umbraco.Core/Services/PropertyValidationService.cs @@ -211,7 +211,7 @@ public class PropertyValidationService : IPropertyValidationService return true; } - var configuration = _dataTypeService.GetDataType(propertyType.DataTypeId)?.Configuration; + var configuration = _dataTypeService.GetDataType(propertyType.DataTypeId)?.ConfigurationObject; IDataValueEditor valueEditor = editor.GetValueEditor(configuration); return !valueEditor.Validate(value, propertyType.Mandatory, propertyType.ValidationRegExp).Any(); } diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs index a77b00814c..d6c3e95c47 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs @@ -117,7 +117,7 @@ public static partial class UmbracoBuilderExtensions builder.Services.AddScoped(); builder.Services.AddSingleton(); - builder.Services.AddSingleton(); + builder.Services.AddSingleton(); builder.Services.AddSingleton(); // register database builder diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs index 4e2728aad9..c8b9c553f3 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs @@ -3,6 +3,7 @@ using Umbraco.Cms.Core.Configuration; using Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_10_0_0; using Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_10_2_0; using Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_12_0_0; +using Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_13_0_0; namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade; @@ -84,5 +85,8 @@ public class UmbracoPlan : MigrationPlan // To 12.0.0 To("{888A0D5D-51E4-4C7E-AA0A-01306523C7FB}"); + + // To 13.0.0 + To("{5F15A1CC-353D-4889-8C7E-F303B4766196}"); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_13_0_0/MigrateDataTypeConfigurations.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_13_0_0/MigrateDataTypeConfigurations.cs new file mode 100644 index 0000000000..199cef5f39 --- /dev/null +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_13_0_0/MigrateDataTypeConfigurations.cs @@ -0,0 +1,129 @@ +using NPoco; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.PropertyEditors; +using Umbraco.Cms.Core.Serialization; +using Umbraco.Cms.Infrastructure.Migrations.PostMigrations; +using Umbraco.Cms.Infrastructure.Persistence; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Cms.Infrastructure.Serialization; +using Umbraco.Extensions; +using PropertyEditorAliases = Umbraco.Cms.Core.Constants.PropertyEditors.Aliases; + +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_13_0_0; + +public class MigrateDataTypeConfigurations : MigrationBase +{ + public MigrateDataTypeConfigurations(IMigrationContext context) + : base(context) + { + } + + // TODO: this migration is a work in progress; it will be amended for a while, thus it MUST be able to re-run several times without failing miserably + protected override void Migrate() + { + // this really should be injected, but until we can get rid of the Newtonsoft.Json based config serializer, we can't rely on + // injection during installs, so we will have to settle for new'ing it up explicitly here (at least for now). + IConfigurationEditorJsonSerializer serializer = new SystemTextConfigurationEditorJsonSerializer(); + + Sql sql = Sql() + .Select() + .From() + .Where(x => x.EditorAlias.Contains("Umbraco.")); + + List dataTypeDtos = Database.Fetch(sql); + + var refreshCache = false; + foreach (DataTypeDto dataTypeDto in dataTypeDtos) + { + Dictionary configurationData = dataTypeDto.Configuration.IsNullOrWhiteSpace() + ? new Dictionary() + : serializer.Deserialize>(dataTypeDto.Configuration) ?? new Dictionary(); + + // fix config key casing - should always be camelCase, but some have been saved as PascalCase over the years + var badlyCasedKeys = configurationData.Keys.Where(key => key.ToFirstLowerInvariant() != key).ToArray(); + var updated = badlyCasedKeys.Any(); + foreach (var incorrectKey in badlyCasedKeys) + { + configurationData[incorrectKey.ToFirstLowerInvariant()] = configurationData[incorrectKey]; + configurationData.Remove(incorrectKey); + } + + // handle special cases, i.e. missing configs (list view), weirdly serialized configs (color picker), min/max for multiple text strings, etc. etc. + updated |= dataTypeDto.EditorAlias switch + { + PropertyEditorAliases.MultipleTextstring => HandleMultipleTextstring(ref configurationData), + PropertyEditorAliases.Label => HandleLabel(ref configurationData), + PropertyEditorAliases.TextArea => HandleTextBoxAndTextArea(ref configurationData), + PropertyEditorAliases.TextBox => HandleTextBoxAndTextArea(ref configurationData), + // TODO: decide on value formats for applicable configs and re-format said configs here (i.e. color picker) + // TODO: append/enrich any missing configs here (i.e. list views are likely missing one or more config values) + _ => false + }; + + if (updated) + { + dataTypeDto.Configuration = serializer.Serialize(configurationData); + Database.Update(dataTypeDto); + refreshCache = true; + } + } + + if (refreshCache) + { + Context.AddPostMigration(); + } + } + + // convert the stored keys "minimum" and "maximum" to the expected keys "min" and "max for multiple textstrings + private static bool HandleMultipleTextstring(ref Dictionary configurationData) + { + Dictionary data = configurationData; + + bool ReplaceKey(string oldKey, string newKey) + { + if (data.ContainsKey(oldKey)) + { + data[newKey] = data[oldKey]; + data.Remove(oldKey); + return true; + } + + return false; + } + + return ReplaceKey("minimum", "min") | ReplaceKey("maximum", "max"); + } + + // enforce default "umbracoDataValueType" for label (may be empty for old data types) + private static bool HandleLabel(ref Dictionary configurationData) + { + if (configurationData.ContainsKey(Constants.PropertyEditors.ConfigurationKeys.DataValueType)) + { + if (configurationData[Constants.PropertyEditors.ConfigurationKeys.DataValueType] is string value && value.IsNullOrWhiteSpace() == false) + { + return false; + } + } + + configurationData[Constants.PropertyEditors.ConfigurationKeys.DataValueType] = ValueTypes.String; + return true; + } + + // enforce integer values for text area and text box (may be saved as string values from old times) + private static bool HandleTextBoxAndTextArea(ref Dictionary configurationData) + { + Dictionary data = configurationData; + bool ReplaceStringWithIntValue(string key) + { + if (data.ContainsKey(key) && data[key] is string stringValue && int.TryParse(stringValue, out var intValue)) + { + data[key] = intValue; + return true; + } + + return false; + } + + return ReplaceStringWithIntValue("maxChars") | ReplaceStringWithIntValue("rows"); + } +} diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DropDownPropertyEditorsMigration.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DropDownPropertyEditorsMigration.cs index 5f4c024b42..b8c438924d 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DropDownPropertyEditorsMigration.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DropDownPropertyEditorsMigration.cs @@ -58,7 +58,7 @@ public class DropDownPropertyEditorsMigration : PropertyEditorsMigrationBase foreach (DataTypeDto dataType in dataTypes) { - ValueListConfiguration config; + ValueListConfiguration config = new ValueListConfiguration(); if (!dataType.Configuration.IsNullOrWhiteSpace()) { @@ -70,9 +70,10 @@ public class DropDownPropertyEditorsMigration : PropertyEditorsMigrationBase try { - config = (ValueListConfiguration)configurationEditor.FromDatabase( - dataType.Configuration, - _configurationEditorJsonSerializer); + // this migration is obsolete, no reason to refactor this code + // config = (ValueListConfiguration)configurationEditor.FromDatabase( + // dataType.Configuration, + // _configurationEditorJsonSerializer); } catch (Exception ex) { @@ -136,8 +137,9 @@ public class DropDownPropertyEditorsMigration : PropertyEditorsMigrationBase dataType.DbType = ValueStorageType.Nvarchar.ToString(); dataType.EditorAlias = Constants.PropertyEditors.Aliases.DropDownListFlexible; - var flexConfig = new DropDownFlexibleConfiguration { Items = config.Items, Multiple = isMultiple }; - dataType.Configuration = ConfigurationEditor.ToDatabase(flexConfig, _configurationEditorJsonSerializer); + // this migration is obsolete, no reason to refactor this code + // var flexConfig = new DropDownFlexibleConfiguration { Items = config.Items, Multiple = isMultiple }; + // dataType.Configuration = ConfigurationEditor.ToDatabase(flexConfig, _configurationEditorJsonSerializer); Database.Update(dataType); } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/MergeDateAndDateTimePropertyEditor.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/MergeDateAndDateTimePropertyEditor.cs index 89658914ab..b623e67e45 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/MergeDateAndDateTimePropertyEditor.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/MergeDateAndDateTimePropertyEditor.cs @@ -43,13 +43,14 @@ public class MergeDateAndDateTimePropertyEditor : MigrationBase foreach (DataTypeDto dataType in dataTypes) { - DateTimeConfiguration config; + DateTimeConfiguration config = new DateTimeConfiguration(); try { - config = (DateTimeConfiguration)new CustomDateTimeConfigurationEditor( - _ioHelper, - _editorConfigurationParser).FromDatabase( - dataType.Configuration, _configurationEditorJsonSerializer); + // this migration is obsolete, no reason to refactor this code + // config = (DateTimeConfiguration)new CustomDateTimeConfigurationEditor( + // _ioHelper, + // _editorConfigurationParser).FromDatabase( + // dataType.Configuration, _configurationEditorJsonSerializer); // If the Umbraco.Date type is the default from V7 and it has never been updated, then the // configuration is empty, and the format stuff is handled by in JS by moment.js. - We can't do that @@ -72,7 +73,8 @@ public class MergeDateAndDateTimePropertyEditor : MigrationBase config.OffsetTime = false; dataType.EditorAlias = Constants.PropertyEditors.Aliases.DateTime; - dataType.Configuration = ConfigurationEditor.ToDatabase(config, _configurationEditorJsonSerializer); + // this migration is obsolete, no reason to refactor this code + // dataType.Configuration = ConfigurationEditor.ToDatabase(config, _configurationEditorJsonSerializer); Database.Update(dataType); } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/RadioAndCheckboxPropertyEditorsMigration.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/RadioAndCheckboxPropertyEditorsMigration.cs index fbb0b49bb0..1d0c66e788 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/RadioAndCheckboxPropertyEditorsMigration.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/RadioAndCheckboxPropertyEditorsMigration.cs @@ -63,7 +63,7 @@ public class RadioAndCheckboxPropertyEditorsMigration : PropertyEditorsMigration foreach (DataTypeDto dataType in dataTypes) { - ValueListConfiguration config; + ValueListConfiguration config = new ValueListConfiguration(); if (dataType.Configuration.IsNullOrWhiteSpace()) { @@ -78,9 +78,10 @@ public class RadioAndCheckboxPropertyEditorsMigration : PropertyEditorsMigration try { - config = (ValueListConfiguration)configurationEditor.FromDatabase( - dataType.Configuration, - _configurationEditorJsonSerializer); + // this migration is obsolete, no reason to refactor this code + // config = (ValueListConfiguration)configurationEditor.FromDatabase( + // dataType.Configuration, + // _configurationEditorJsonSerializer); } catch (Exception ex) { diff --git a/src/Umbraco.Infrastructure/Packaging/PackageDataInstallation.cs b/src/Umbraco.Infrastructure/Packaging/PackageDataInstallation.cs index 192d1a51c3..76e20a48e8 100644 --- a/src/Umbraco.Infrastructure/Packaging/PackageDataInstallation.cs +++ b/src/Umbraco.Infrastructure/Packaging/PackageDataInstallation.cs @@ -1271,8 +1271,7 @@ namespace Umbraco.Cms.Infrastructure.Packaging var configurationAttributeValue = dataTypeElement.Attribute("Configuration")?.Value; if (!string.IsNullOrWhiteSpace(configurationAttributeValue)) { - dataType.Configuration = editor.GetConfigurationEditor() - .FromDatabase(configurationAttributeValue, _serializer); + dataType.ConfigurationData = editor.GetConfigurationEditor().FromDatabase(configurationAttributeValue, _serializer); } dataTypes.Add(dataType); diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/DataTypeFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/DataTypeFactory.cs index 69862364de..182aeb1243 100644 --- a/src/Umbraco.Infrastructure/Persistence/Factories/DataTypeFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/Factories/DataTypeFactory.cs @@ -42,7 +42,7 @@ internal static class DataTypeFactory dataType.Trashed = dto.NodeDto.Trashed; dataType.CreatorId = dto.NodeDto.UserId ?? Constants.Security.UnknownUserId; - dataType.SetLazyConfiguration(dto.Configuration); + dataType.SetConfigurationData(editor.GetConfigurationEditor().FromDatabase(dto.Configuration, serializer)); // reset dirty initial properties (U4-1946) dataType.ResetDirtyProperties(false); @@ -61,7 +61,7 @@ internal static class DataTypeFactory EditorAlias = entity.EditorAlias, NodeId = entity.Id, DbType = entity.DatabaseType.ToString(), - Configuration = ConfigurationEditor.ToDatabase(entity.Configuration, serializer), + Configuration = entity.Editor?.GetConfigurationEditor().ToDatabase(entity.ConfigurationData, serializer), NodeDto = BuildNodeDto(entity), }; diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentRepositoryBase.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentRepositoryBase.cs index ce14f1ec1e..95fce7b22f 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentRepositoryBase.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentRepositoryBase.cs @@ -311,7 +311,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement continue; // not implementing IDataValueTags, continue } - object? configuration = DataTypeService.GetDataType(property.PropertyType.DataTypeId)?.Configuration; + object? configuration = DataTypeService.GetDataType(property.PropertyType.DataTypeId)?.ConfigurationObject; if (property.PropertyType.VariesByCulture()) { diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyValueEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyValueEditor.cs index fbf2239828..ca61931f38 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyValueEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyValueEditor.cs @@ -102,7 +102,7 @@ internal abstract class BlockEditorPropertyValueEditor : DataValueEditor, IDataV continue; } - object? configuration = _dataTypeService.GetDataType(prop.Value.PropertyType.DataTypeKey)?.Configuration; + object? configuration = _dataTypeService.GetDataType(prop.Value.PropertyType.DataTypeKey)?.ConfigurationObject; result.AddRange(tagsProvider.GetTags(prop.Value.Value, configuration, languageId)); } @@ -225,7 +225,7 @@ internal abstract class BlockEditorPropertyValueEditor : DataValueEditor, IDataV if (!valEditors.TryGetValue(dataType.Id, out IDataValueEditor? valEditor)) { - var tempConfig = dataType.Configuration; + var tempConfig = dataType.ConfigurationObject; valEditor = propEditor.GetValueEditor(tempConfig); valEditors.Add(dataType.Id, valEditor); @@ -246,7 +246,7 @@ internal abstract class BlockEditorPropertyValueEditor : DataValueEditor, IDataV foreach (KeyValuePair prop in row.PropertyValues) { // Fetch the property types prevalue - var propConfiguration = _dataTypeService.GetDataType(prop.Value.PropertyType.DataTypeId)?.Configuration; + var propConfiguration = _dataTypeService.GetDataType(prop.Value.PropertyType.DataTypeId)?.ConfigurationObject; // Lookup the property editor IDataEditor? propEditor = _propertyEditors[prop.Value.PropertyType.PropertyEditorAlias]; diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ColorPickerConfigurationEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/ColorPickerConfigurationEditor.cs index 8c8455ce86..edc727014c 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/ColorPickerConfigurationEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/ColorPickerConfigurationEditor.cs @@ -30,122 +30,6 @@ internal class ColorPickerConfigurationEditor : ConfigurationEditor ToConfigurationEditor(ColorPickerConfiguration? configuration) - { - List? configuredItems = configuration?.Items; // ordered - object editorItems; - - if (configuredItems == null) - { - editorItems = new object(); - } - else - { - var d = new Dictionary(); - editorItems = d; - var sortOrder = 0; - foreach (ValueListConfiguration.ValueListItem item in configuredItems) - { - d[item.Id.ToString()] = GetItemValue(item, configuration!.UseLabel, sortOrder++); - } - } - - var useLabel = configuration?.UseLabel ?? false; - - return new Dictionary { { "items", editorItems }, { "useLabel", useLabel } }; - } - - // send: { "items": { "": { "value": "", "label": "