From d6c181457c11522b917b3c25ff353c07b53d6916 Mon Sep 17 00:00:00 2001 From: Laura Neto <12862535+lauraneto@users.noreply.github.com> Date: Wed, 10 Sep 2025 11:20:06 +0200 Subject: [PATCH] Non existing property editor (#19997) * Initial implementation of non existing property editor * Adjust `MissingPropertyEditor` to not require registering in PropertyEditorCollection * Add `MissingPropertyEditor.name` back * Remove unused dependencies from DataTypeService * Removed reference to non existing property * Add parameterless constructor back to MissingPropertyEditor * Add validation error on document open to property with missing editor * Update labels * Removed public editor alias const * Update src/Umbraco.Web.UI.Client/src/packages/property-editors/missing/manifests.ts * Add test that checks whether the new MissingPropertyEditor is returned when an editor is not found * Also check if the editor UI alias is correct in the test * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Share property editor instances between properties * Only store missing property editors in memory in `ContentMapDefinition.MapValueViewModels()` * Add value converter for the missing property editor to always return a string (same as the Label did previously) * Small improvements to code block --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../Mapping/Content/ContentMapDefinition.cs | 49 ++++++++++--- .../Mapping/Document/DocumentMapDefinition.cs | 21 +++++- .../Document/DocumentVersionMapDefinition.cs | 19 ++++- .../Mapping/Media/MediaMapDefinition.cs | 20 +++++- .../Mapping/Member/MemberMapDefinition.cs | 17 ++++- .../PropertyEditors/MissingPropertyEditor.cs | 69 +++++++++++++++++-- .../MissingPropertyEditorValueConverter.cs | 21 ++++++ src/Umbraco.Core/Services/DataTypeService.cs | 57 +++++++-------- .../Persistence/Factories/DataTypeFactory.cs | 20 ++++-- .../Implement/DataTypeRepository.cs | 19 ++++- .../src/assets/lang/en.ts | 6 ++ .../src/assets/lang/pt.ts | 6 ++ .../packages/property-editors/manifests.ts | 2 + .../property-editors/missing/manifests.ts | 18 +++++ .../missing/modal/constants.ts | 1 + .../missing/modal/manifests.ts | 8 +++ .../modal/missing-editor-modal.element.ts | 47 +++++++++++++ .../modal/missing-editor-modal.token.ts | 17 +++++ .../property-editor-ui-missing.element.ts | 56 +++++++++++++++ .../PublishContentTypeFactoryTest.cs | 3 + .../Repositories/DocumentRepositoryTest.cs | 10 ++- .../Services/DataTypeServiceTests.cs | 33 ++++++++- 22 files changed, 451 insertions(+), 68 deletions(-) create mode 100644 src/Umbraco.Core/PropertyEditors/ValueConverters/MissingPropertyEditorValueConverter.cs create mode 100644 src/Umbraco.Web.UI.Client/src/packages/property-editors/missing/manifests.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/property-editors/missing/modal/constants.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/property-editors/missing/modal/manifests.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/property-editors/missing/modal/missing-editor-modal.element.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/property-editors/missing/modal/missing-editor-modal.token.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/property-editors/missing/property-editor-ui-missing.element.ts diff --git a/src/Umbraco.Cms.Api.Management/Mapping/Content/ContentMapDefinition.cs b/src/Umbraco.Cms.Api.Management/Mapping/Content/ContentMapDefinition.cs index 3fd3fe6a7c..06be1b91ff 100644 --- a/src/Umbraco.Cms.Api.Management/Mapping/Content/ContentMapDefinition.cs +++ b/src/Umbraco.Cms.Api.Management/Mapping/Content/ContentMapDefinition.cs @@ -1,4 +1,6 @@ -using Umbraco.Cms.Api.Management.ViewModels.Content; +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Api.Management.ViewModels.Content; +using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.PropertyEditors; @@ -12,30 +14,55 @@ public abstract class ContentMapDefinition _propertyEditorCollection = propertyEditorCollection; + protected ContentMapDefinition( + PropertyEditorCollection propertyEditorCollection, + IDataValueEditorFactory dataValueEditorFactory) + { + _propertyEditorCollection = propertyEditorCollection; + _dataValueEditorFactory = dataValueEditorFactory; + } + + [Obsolete("Please use the non-obsolete constructor. Scheduled for removal in Umbraco 18.")] + protected ContentMapDefinition(PropertyEditorCollection propertyEditorCollection) + : this( + propertyEditorCollection, + StaticServiceProvider.Instance.GetRequiredService()) + { + } protected delegate void ValueViewModelMapping(IDataEditor propertyEditor, TValueViewModel variantViewModel); protected delegate void VariantViewModelMapping(string? culture, string? segment, TVariantViewModel variantViewModel); - protected IEnumerable MapValueViewModels(IEnumerable properties, ValueViewModelMapping? additionalPropertyMapping = null, bool published = false) => - properties + protected IEnumerable MapValueViewModels( + IEnumerable properties, + ValueViewModelMapping? additionalPropertyMapping = null, + bool published = false) + { + Dictionary missingPropertyEditors = []; + return properties .SelectMany(property => property .Values .Select(propertyValue => { IDataEditor? propertyEditor = _propertyEditorCollection[property.PropertyType.PropertyEditorAlias]; - if (propertyEditor == null) + if (propertyEditor is null && !missingPropertyEditors.TryGetValue(property.PropertyType.PropertyEditorAlias, out propertyEditor)) { - return null; + // We cache the missing property editors to avoid creating multiple instances of them + propertyEditor = new MissingPropertyEditor(property.PropertyType.PropertyEditorAlias, _dataValueEditorFactory); + missingPropertyEditors[property.PropertyType.PropertyEditorAlias] = propertyEditor; } IProperty? publishedProperty = null; if (published) { publishedProperty = new Property(property.PropertyType); - publishedProperty.SetValue(propertyValue.PublishedValue, propertyValue.Culture, propertyValue.Segment); + publishedProperty.SetValue( + propertyValue.PublishedValue, + propertyValue.Culture, + propertyValue.Segment); } var variantViewModel = new TValueViewModel @@ -43,14 +70,18 @@ public abstract class ContentMapDefinition MapVariantViewModels(TContent source, VariantViewModelMapping? additionalVariantMapping = null) { diff --git a/src/Umbraco.Cms.Api.Management/Mapping/Document/DocumentMapDefinition.cs b/src/Umbraco.Cms.Api.Management/Mapping/Document/DocumentMapDefinition.cs index 0960b2d72a..fa703c134d 100644 --- a/src/Umbraco.Cms.Api.Management/Mapping/Document/DocumentMapDefinition.cs +++ b/src/Umbraco.Cms.Api.Management/Mapping/Document/DocumentMapDefinition.cs @@ -1,8 +1,10 @@ +using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Api.Management.Mapping.Content; using Umbraco.Cms.Api.Management.ViewModels.Document; using Umbraco.Cms.Api.Management.ViewModels.Document.Collection; using Umbraco.Cms.Api.Management.ViewModels.DocumentBlueprint; using Umbraco.Cms.Api.Management.ViewModels.DocumentType; +using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Mapping; @@ -15,8 +17,23 @@ public class DocumentMapDefinition : ContentMapDefinition _commonMapper = commonMapper; + public DocumentMapDefinition( + PropertyEditorCollection propertyEditorCollection, + CommonMapper commonMapper, + IDataValueEditorFactory dataValueEditorFactory) + : base(propertyEditorCollection, dataValueEditorFactory) + => _commonMapper = commonMapper; + + [Obsolete("Please use the non-obsolete constructor. Scheduled for removal in Umbraco 18.")] + public DocumentMapDefinition( + PropertyEditorCollection propertyEditorCollection, + CommonMapper commonMapper) + : this( + propertyEditorCollection, + commonMapper, + StaticServiceProvider.Instance.GetRequiredService()) + { + } public void DefineMaps(IUmbracoMapper mapper) { diff --git a/src/Umbraco.Cms.Api.Management/Mapping/Document/DocumentVersionMapDefinition.cs b/src/Umbraco.Cms.Api.Management/Mapping/Document/DocumentVersionMapDefinition.cs index 5e12e245bd..0012d244a7 100644 --- a/src/Umbraco.Cms.Api.Management/Mapping/Document/DocumentVersionMapDefinition.cs +++ b/src/Umbraco.Cms.Api.Management/Mapping/Document/DocumentVersionMapDefinition.cs @@ -1,7 +1,9 @@ -using Umbraco.Cms.Api.Management.Mapping.Content; +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Api.Management.Mapping.Content; using Umbraco.Cms.Api.Management.ViewModels; using Umbraco.Cms.Api.Management.ViewModels.Document; using Umbraco.Cms.Api.Management.ViewModels.DocumentType; +using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.PropertyEditors; @@ -11,8 +13,19 @@ namespace Umbraco.Cms.Api.Management.Mapping.Document; public class DocumentVersionMapDefinition : ContentMapDefinition, IMapDefinition { - public DocumentVersionMapDefinition(PropertyEditorCollection propertyEditorCollection) - : base(propertyEditorCollection) + public DocumentVersionMapDefinition( + PropertyEditorCollection propertyEditorCollection, + IDataValueEditorFactory dataValueEditorFactory) + : base(propertyEditorCollection, dataValueEditorFactory) + { + } + + [Obsolete("Please use the non-obsolete constructor. Scheduled for removal in Umbraco 18.")] + public DocumentVersionMapDefinition( + PropertyEditorCollection propertyEditorCollection) + : this( + propertyEditorCollection, + StaticServiceProvider.Instance.GetRequiredService()) { } diff --git a/src/Umbraco.Cms.Api.Management/Mapping/Media/MediaMapDefinition.cs b/src/Umbraco.Cms.Api.Management/Mapping/Media/MediaMapDefinition.cs index 0f0e01b597..8bd6284b18 100644 --- a/src/Umbraco.Cms.Api.Management/Mapping/Media/MediaMapDefinition.cs +++ b/src/Umbraco.Cms.Api.Management/Mapping/Media/MediaMapDefinition.cs @@ -1,7 +1,9 @@ +using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Api.Management.Mapping.Content; using Umbraco.Cms.Api.Management.ViewModels.Media; using Umbraco.Cms.Api.Management.ViewModels.Media.Collection; using Umbraco.Cms.Api.Management.ViewModels.MediaType; +using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Mapping; @@ -14,10 +16,24 @@ public class MediaMapDefinition : ContentMapDefinition _commonMapper = commonMapper; + [Obsolete("Please use the non-obsolete constructor. Scheduled for removal in Umbraco 18.")] + public MediaMapDefinition( + PropertyEditorCollection propertyEditorCollection, + CommonMapper commonMapper) + : this( + propertyEditorCollection, + commonMapper, + StaticServiceProvider.Instance.GetRequiredService()) + { + } + public void DefineMaps(IUmbracoMapper mapper) { mapper.Define((_, _) => new MediaResponseModel(), Map); diff --git a/src/Umbraco.Cms.Api.Management/Mapping/Member/MemberMapDefinition.cs b/src/Umbraco.Cms.Api.Management/Mapping/Member/MemberMapDefinition.cs index 64bc0c1e40..10e750cf9d 100644 --- a/src/Umbraco.Cms.Api.Management/Mapping/Member/MemberMapDefinition.cs +++ b/src/Umbraco.Cms.Api.Management/Mapping/Member/MemberMapDefinition.cs @@ -1,6 +1,8 @@ +using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Api.Management.Mapping.Content; using Umbraco.Cms.Api.Management.ViewModels.Member; using Umbraco.Cms.Api.Management.ViewModels.MemberType; +using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.PropertyEditors; @@ -9,8 +11,19 @@ namespace Umbraco.Cms.Api.Management.Mapping.Member; public class MemberMapDefinition : ContentMapDefinition, IMapDefinition { - public MemberMapDefinition(PropertyEditorCollection propertyEditorCollection) - : base(propertyEditorCollection) + public MemberMapDefinition( + PropertyEditorCollection propertyEditorCollection, + IDataValueEditorFactory dataValueEditorFactory) + : base(propertyEditorCollection, dataValueEditorFactory) + { + } + + [Obsolete("Please use the non-obsolete constructor. Scheduled for removal in Umbraco 18.")] + public MemberMapDefinition( + PropertyEditorCollection propertyEditorCollection) + : this( + propertyEditorCollection, + StaticServiceProvider.Instance.GetRequiredService()) { } diff --git a/src/Umbraco.Core/PropertyEditors/MissingPropertyEditor.cs b/src/Umbraco.Core/PropertyEditors/MissingPropertyEditor.cs index 2f449666bd..374efd67a7 100644 --- a/src/Umbraco.Core/PropertyEditors/MissingPropertyEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/MissingPropertyEditor.cs @@ -1,5 +1,10 @@ +using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.Composing; +using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Serialization; +using Umbraco.Cms.Core.Strings; namespace Umbraco.Cms.Core.PropertyEditors; @@ -10,19 +15,73 @@ namespace Umbraco.Cms.Core.PropertyEditors; [HideFromTypeFinder] public class MissingPropertyEditor : IDataEditor { - public string Alias => "Umbraco.Missing"; + private const string EditorAlias = "Umbraco.Missing"; + private readonly IDataValueEditorFactory _dataValueEditorFactory; + private IDataValueEditor? _valueEditor; + /// + /// Initializes a new instance of the class. + /// + public MissingPropertyEditor( + string missingEditorAlias, + IDataValueEditorFactory dataValueEditorFactory) + { + _dataValueEditorFactory = dataValueEditorFactory; + Alias = missingEditorAlias; + } + + [Obsolete("Use the non-obsolete constructor instead. Scheduled for removal in Umbraco 18.")] + public MissingPropertyEditor() + : this( + EditorAlias, + StaticServiceProvider.Instance.GetRequiredService()) + { + } + + /// + public string Alias { get; } + + /// + /// Gets the name of the editor. + /// public string Name => "Missing property editor"; + /// public bool IsDeprecated => false; - public IDictionary DefaultConfiguration => throw new NotImplementedException(); + /// + public bool SupportsReadOnly => true; - public IPropertyIndexValueFactory PropertyIndexValueFactory => throw new NotImplementedException(); + /// + public IDictionary DefaultConfiguration => new Dictionary(); + /// + public IPropertyIndexValueFactory PropertyIndexValueFactory => new DefaultPropertyIndexValueFactory(); + + /// + public IDataValueEditor GetValueEditor() => _valueEditor + ??= _dataValueEditorFactory.Create( + new DataEditorAttribute(EditorAlias)); + + /// + public IDataValueEditor GetValueEditor(object? configurationObject) => GetValueEditor(); + + /// public IConfigurationEditor GetConfigurationEditor() => new ConfigurationEditor(); - public IDataValueEditor GetValueEditor() => throw new NotImplementedException(); + // provides the property value editor + internal sealed class MissingPropertyValueEditor : DataValueEditor + { + public MissingPropertyValueEditor( + IShortStringHelper shortStringHelper, + IJsonSerializer jsonSerializer, + IIOHelper ioHelper, + DataEditorAttribute attribute) + : base(shortStringHelper, jsonSerializer, ioHelper, attribute) + { + } - public IDataValueEditor GetValueEditor(object? configurationObject) => throw new NotImplementedException(); + /// + public override bool IsReadOnly => true; + } } diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/MissingPropertyEditorValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/MissingPropertyEditorValueConverter.cs new file mode 100644 index 0000000000..da70e558cf --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/MissingPropertyEditorValueConverter.cs @@ -0,0 +1,21 @@ +using Umbraco.Cms.Core.Models.PublishedContent; + +namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters; + +/// +/// A value converter for the missing property editor, which always returns a string. +/// +[DefaultPropertyValueConverter] +public class MissingPropertyEditorValueConverter : PropertyValueConverterBase +{ + public override bool IsConverter(IPublishedPropertyType propertyType) + => "Umb.PropertyEditorUi.Missing".Equals(propertyType.EditorUiAlias); + + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) => typeof(string); + + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) + => PropertyCacheLevel.Element; + + public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview) + => source?.ToString() ?? string.Empty; +} diff --git a/src/Umbraco.Core/Services/DataTypeService.cs b/src/Umbraco.Core/Services/DataTypeService.cs index 8b1f7f953c..ef5c9abdd7 100644 --- a/src/Umbraco.Core/Services/DataTypeService.cs +++ b/src/Umbraco.Core/Services/DataTypeService.cs @@ -21,14 +21,12 @@ namespace Umbraco.Cms.Core.Services.Implement /// public class DataTypeService : RepositoryService, IDataTypeService { - private readonly IDataValueEditorFactory _dataValueEditorFactory; private readonly IDataTypeRepository _dataTypeRepository; private readonly IDataTypeContainerRepository _dataTypeContainerRepository; private readonly IContentTypeRepository _contentTypeRepository; private readonly IMediaTypeRepository _mediaTypeRepository; private readonly IMemberTypeRepository _memberTypeRepository; private readonly IAuditRepository _auditRepository; - private readonly IIOHelper _ioHelper; private readonly IDataTypeContainerService _dataTypeContainerService; private readonly IUserIdKeyResolver _userIdKeyResolver; private readonly Lazy _idKeyMap; @@ -59,6 +57,7 @@ namespace Umbraco.Cms.Core.Services.Implement { } + [Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 18.")] public DataTypeService( ICoreScopeProvider provider, ILoggerFactory loggerFactory, @@ -71,15 +70,36 @@ namespace Umbraco.Cms.Core.Services.Implement IMemberTypeRepository memberTypeRepository, IIOHelper ioHelper, Lazy idKeyMap) + : this( + provider, + loggerFactory, + eventMessagesFactory, + dataTypeRepository, + auditRepository, + contentTypeRepository, + mediaTypeRepository, + memberTypeRepository, + idKeyMap) + { + } + + public DataTypeService( + ICoreScopeProvider provider, + ILoggerFactory loggerFactory, + IEventMessagesFactory eventMessagesFactory, + IDataTypeRepository dataTypeRepository, + IAuditRepository auditRepository, + IContentTypeRepository contentTypeRepository, + IMediaTypeRepository mediaTypeRepository, + IMemberTypeRepository memberTypeRepository, + Lazy idKeyMap) : base(provider, loggerFactory, eventMessagesFactory) { - _dataValueEditorFactory = dataValueEditorFactory; _dataTypeRepository = dataTypeRepository; _auditRepository = auditRepository; _contentTypeRepository = contentTypeRepository; _mediaTypeRepository = mediaTypeRepository; _memberTypeRepository = memberTypeRepository; - _ioHelper = ioHelper; _idKeyMap = idKeyMap; // resolve dependencies for obsolete methods through the static service provider, so they don't pollute the constructor signature @@ -258,7 +278,6 @@ namespace Umbraco.Cms.Core.Services.Implement { using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true); IDataType? dataType = _dataTypeRepository.Get(Query().Where(x => x.Name == name))?.FirstOrDefault(); - ConvertMissingEditorOfDataTypeToLabel(dataType); return Task.FromResult(dataType); } @@ -275,7 +294,6 @@ namespace Umbraco.Cms.Core.Services.Implement } IDataType[] dataTypes = _dataTypeRepository.Get(query).ToArray(); - ConvertMissingEditorsOfDataTypesToLabels(dataTypes); return Task.FromResult>(dataTypes); } @@ -319,7 +337,6 @@ namespace Umbraco.Cms.Core.Services.Implement { using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true); IDataType? dataType = _dataTypeRepository.Get(id); - ConvertMissingEditorOfDataTypeToLabel(dataType); return dataType; } @@ -329,7 +346,6 @@ namespace Umbraco.Cms.Core.Services.Implement { using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true); IDataType? dataType = GetDataTypeFromRepository(id); - ConvertMissingEditorOfDataTypeToLabel(dataType); return Task.FromResult(dataType); } @@ -349,7 +365,6 @@ namespace Umbraco.Cms.Core.Services.Implement using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true); IQuery query = Query().Where(x => x.EditorAlias == propertyEditorAlias); IEnumerable dataTypes = _dataTypeRepository.Get(query).ToArray(); - ConvertMissingEditorsOfDataTypesToLabels(dataTypes); return Task.FromResult(dataTypes); } @@ -360,7 +375,6 @@ namespace Umbraco.Cms.Core.Services.Implement using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true); IQuery query = Query().Where(x => propertyEditorAlias.Contains(x.EditorAlias)); IEnumerable dataTypes = _dataTypeRepository.Get(query).ToArray(); - ConvertMissingEditorsOfDataTypesToLabels(dataTypes); return Task.FromResult(dataTypes); } @@ -370,7 +384,6 @@ namespace Umbraco.Cms.Core.Services.Implement using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true); IQuery query = Query().Where(x => x.EditorUiAlias == editorUiAlias); IEnumerable dataTypes = _dataTypeRepository.Get(query).ToArray(); - ConvertMissingEditorsOfDataTypesToLabels(dataTypes); return Task.FromResult(dataTypes); } @@ -384,32 +397,10 @@ namespace Umbraco.Cms.Core.Services.Implement { using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true); IEnumerable dataTypes = _dataTypeRepository.GetMany(ids).ToArray(); - ConvertMissingEditorsOfDataTypesToLabels(dataTypes); return dataTypes; } - private void ConvertMissingEditorOfDataTypeToLabel(IDataType? dataType) - { - if (dataType == null) - { - return; - } - - ConvertMissingEditorsOfDataTypesToLabels([dataType]); - } - - private void ConvertMissingEditorsOfDataTypesToLabels(IEnumerable dataTypes) - { - // Any data types that don't have an associated editor are created of a specific type. - // We convert them to labels to make clear to the user why the data type cannot be used. - IEnumerable dataTypesWithMissingEditors = dataTypes.Where(x => x.Editor is MissingPropertyEditor); - foreach (IDataType dataType in dataTypesWithMissingEditors) - { - dataType.Editor = new LabelPropertyEditor(_dataValueEditorFactory, _ioHelper); - } - } - public Attempt?> Move(IDataType toMove, int parentId) { Guid? containerKey = null; diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/DataTypeFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/DataTypeFactory.cs index e4509ee095..86b014a8e3 100644 --- a/src/Umbraco.Infrastructure/Persistence/Factories/DataTypeFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/Factories/DataTypeFactory.cs @@ -10,17 +10,23 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Factories; internal static class DataTypeFactory { - public static IDataType BuildEntity(DataTypeDto dto, PropertyEditorCollection editors, ILogger logger, IConfigurationEditorJsonSerializer serializer) + public static IDataType BuildEntity( + DataTypeDto dto, + PropertyEditorCollection editors, + ILogger logger, + IConfigurationEditorJsonSerializer serializer, + IDataValueEditorFactory dataValueEditorFactory) { // Check we have an editor for the data type. if (!editors.TryGet(dto.EditorAlias, out IDataEditor? editor)) { logger.LogWarning( - "Could not find an editor with alias {EditorAlias}, treating as Label. " + "The site may fail to boot and/or load data types and run.", dto.EditorAlias); - - // Create as special type, which downstream can be handled by converting to a LabelPropertyEditor to make clear - // the situation to the user. - editor = new MissingPropertyEditor(); + "Could not find an editor with alias {EditorAlias}, treating as Missing. " + "The site may fail to boot and/or load data types and run.", + dto.EditorAlias); + editor = + new MissingPropertyEditor( + dto.EditorAlias, + dataValueEditorFactory); } var dataType = new DataType(editor, serializer); @@ -41,7 +47,7 @@ internal static class DataTypeFactory dataType.SortOrder = dto.NodeDto.SortOrder; dataType.Trashed = dto.NodeDto.Trashed; dataType.CreatorId = dto.NodeDto.UserId ?? Constants.Security.UnknownUserId; - dataType.EditorUiAlias = dto.EditorUiAlias; + dataType.EditorUiAlias = editor is MissingPropertyEditor ? "Umb.PropertyEditorUi.Missing" : dto.EditorUiAlias; dataType.SetConfigurationData(editor.GetConfigurationEditor().FromDatabase(dto.Configuration, serializer)); diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DataTypeRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DataTypeRepository.cs index 6dc12aa658..8e3d71bd6b 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DataTypeRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DataTypeRepository.cs @@ -29,6 +29,7 @@ internal sealed class DataTypeRepository : EntityRepositoryBase, private readonly ILogger _dataTypeLogger; private readonly PropertyEditorCollection _editors; private readonly IConfigurationEditorJsonSerializer _serializer; + private readonly IDataValueEditorFactory _dataValueEditorFactory; public DataTypeRepository( IScopeAccessor scopeAccessor, @@ -36,11 +37,13 @@ internal sealed class DataTypeRepository : EntityRepositoryBase, PropertyEditorCollection editors, ILogger logger, ILoggerFactory loggerFactory, - IConfigurationEditorJsonSerializer serializer) + IConfigurationEditorJsonSerializer serializer, + IDataValueEditorFactory dataValueEditorFactory) : base(scopeAccessor, cache, logger) { _editors = editors; _serializer = serializer; + _dataValueEditorFactory = dataValueEditorFactory; _dataTypeLogger = loggerFactory.CreateLogger(); } @@ -262,7 +265,12 @@ internal sealed class DataTypeRepository : EntityRepositoryBase, } List? dtos = Database.Fetch(dataTypeSql); - return dtos.Select(x => DataTypeFactory.BuildEntity(x, _editors, _dataTypeLogger, _serializer)).ToArray(); + return dtos.Select(x => DataTypeFactory.BuildEntity( + x, + _editors, + _dataTypeLogger, + _serializer, + _dataValueEditorFactory)).ToArray(); } protected override IEnumerable PerformGetByQuery(IQuery query) @@ -273,7 +281,12 @@ internal sealed class DataTypeRepository : EntityRepositoryBase, List? dtos = Database.Fetch(sql); - return dtos.Select(x => DataTypeFactory.BuildEntity(x, _editors, _dataTypeLogger, _serializer)).ToArray(); + return dtos.Select(x => DataTypeFactory.BuildEntity( + x, + _editors, + _dataTypeLogger, + _serializer, + _dataValueEditorFactory)).ToArray(); } #endregion diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts index 4fd053f152..ecf128af52 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts @@ -2831,6 +2831,12 @@ export default { resetUrlMessage: 'Are you sure you want to reset this URL?', resetUrlLabel: 'Reset', }, + missingEditor: { + description: + '

Error! This property type is no longer available. Please reach out to your administrator.

', + detailsDescription: + '

This property type is no longer available.
Please contact your administrator so they can either delete this property or restore the property type.

Data:

', + }, uiCulture: { ar: 'العربية', bs: 'Bosanski', diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/pt.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/pt.ts index f94d631f89..7f702cd4a9 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/pt.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/pt.ts @@ -2830,4 +2830,10 @@ export default { resetUrlMessage: 'Tem a certeza que quer redefinir este URL?', resetUrlLabel: 'Redefinir', }, + missingEditor: { + description: + '

Erro! Este tipo de propriedade já não se encontra disponível. Por favor, contacte o administrador.

', + detailsDescription: + '

Este tipo de propriedade já não se encontra disponível.
Por favor, contacte o administrador para que ele possa apagar a propriedade ou restaurar o tipo de propriedade.

Dados:

', + }, } as UmbLocalizationDictionary; diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/manifests.ts index 092f9d72b7..a4c4dcf504 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/property-editors/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/property-editors/manifests.ts @@ -14,6 +14,7 @@ import { manifests as dropdownManifests } from './dropdown/manifests.js'; import { manifests as eyeDropperManifests } from './eye-dropper/manifests.js'; import { manifests as iconPickerManifests } from './icon-picker/manifests.js'; import { manifests as labelManifests } from './label/manifests.js'; +import { manifests as missingManifests } from './missing/manifests.js'; import { manifests as multipleTextStringManifests } from './multiple-text-string/manifests.js'; import { manifests as numberManifests } from './number/manifests.js'; import { manifests as radioButtonListManifests } from './radio-button-list/manifests.js'; @@ -32,6 +33,7 @@ export const manifests: Array = [ ...eyeDropperManifests, ...iconPickerManifests, ...labelManifests, + ...missingManifests, ...multipleTextStringManifests, ...numberManifests, ...radioButtonListManifests, diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/missing/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/missing/manifests.ts new file mode 100644 index 0000000000..0575dfc63a --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/property-editors/missing/manifests.ts @@ -0,0 +1,18 @@ +import { manifests as modalManifests } from './modal/manifests.js'; + +export const manifests: Array = [ + { + type: 'propertyEditorUi', + alias: 'Umb.PropertyEditorUi.Missing', + name: 'Missing Property Editor UI', + element: () => import('./property-editor-ui-missing.element.js'), + meta: { + label: 'Missing', + propertyEditorSchemaAlias: undefined, // By setting it to undefined, this editor won't appear in the property editor UI picker modal. + icon: 'icon-ordered-list', + group: '', + supportsReadOnly: true, + }, + }, + ...modalManifests, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/missing/modal/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/missing/modal/constants.ts new file mode 100644 index 0000000000..fb0853adfa --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/property-editors/missing/modal/constants.ts @@ -0,0 +1 @@ +export * from './missing-editor-modal.token.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/missing/modal/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/missing/modal/manifests.ts new file mode 100644 index 0000000000..3ef10f367f --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/property-editors/missing/modal/manifests.ts @@ -0,0 +1,8 @@ +export const manifests: Array = [ + { + type: 'modal', + alias: 'Umb.Modal.MissingPropertyEditor', + name: 'Missing Property Editor Modal', + element: () => import('./missing-editor-modal.element.js'), + }, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/missing/modal/missing-editor-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/missing/modal/missing-editor-modal.element.ts new file mode 100644 index 0000000000..f71d9769aa --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/property-editors/missing/modal/missing-editor-modal.element.ts @@ -0,0 +1,47 @@ +import type { UmbMissingPropertyModalData, UmbMissingPropertyModalResult } from './missing-editor-modal.token.js'; +import { html, customElement, css } from '@umbraco-cms/backoffice/external/lit'; +import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; +import { UmbModalBaseElement } from '@umbraco-cms/backoffice/modal'; +import { umbFocus } from '@umbraco-cms/backoffice/lit-element'; + +@customElement('umb-missing-property-editor-modal') +export class UmbMissingPropertyEditorModalElement extends UmbModalBaseElement< + UmbMissingPropertyModalData, + UmbMissingPropertyModalResult +> { + override render() { + return html` + + + ${this.data?.value} + + + `; + } + + static override styles = [ + UmbTextStyles, + css` + uui-dialog-layout { + max-inline-size: 60ch; + } + #codeblock { + max-height: 300px; + overflow: auto; + } + `, + ]; +} + +export { UmbMissingPropertyEditorModalElement as element }; + +declare global { + interface HTMLElementTagNameMap { + 'umb-missing-property-editor-modal': UmbMissingPropertyEditorModalElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/missing/modal/missing-editor-modal.token.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/missing/modal/missing-editor-modal.token.ts new file mode 100644 index 0000000000..9792759058 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/property-editors/missing/modal/missing-editor-modal.token.ts @@ -0,0 +1,17 @@ +import { UmbModalToken } from '@umbraco-cms/backoffice/modal'; + +export interface UmbMissingPropertyModalData { + value: string | undefined; +} + +export type UmbMissingPropertyModalResult = undefined; + +export const UMB_MISSING_PROPERTY_EDITOR_MODAL = new UmbModalToken< + UmbMissingPropertyModalData, + UmbMissingPropertyModalResult +>('Umb.Modal.MissingPropertyEditor', { + modal: { + type: 'dialog', + size: 'small', + }, +}); diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/missing/property-editor-ui-missing.element.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/missing/property-editor-ui-missing.element.ts new file mode 100644 index 0000000000..5ec66cbf83 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/property-editors/missing/property-editor-ui-missing.element.ts @@ -0,0 +1,56 @@ +import { UMB_MISSING_PROPERTY_EDITOR_MODAL } from './modal/missing-editor-modal.token.js'; +import { customElement, html } from '@umbraco-cms/backoffice/external/lit'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import { umbOpenModal } from '@umbraco-cms/backoffice/modal'; +import type { UmbPropertyEditorUiElement } from '@umbraco-cms/backoffice/property-editor'; +import { UmbFormControlMixin } from '@umbraco-cms/backoffice/validation'; + +/** + * @element umb-property-editor-ui-missing + */ +@customElement('umb-property-editor-ui-missing') +export class UmbPropertyEditorUIMissingElement + extends UmbFormControlMixin(UmbLitElement, undefined) + implements UmbPropertyEditorUiElement +{ + constructor() { + super(); + + this.addValidator( + 'customError', + () => this.localize.term('errors_propertyHasErrors'), + () => true, + ); + + this.pristine = false; + } + + async #onDetails(event: Event) { + event.stopPropagation(); + + await umbOpenModal(this, UMB_MISSING_PROPERTY_EDITOR_MODAL, { + data: { + // If the value is an object, we stringify it to make sure we can display it properly. + // If it's a primitive value, we just convert it to string. + value: typeof this.value === 'object' ? JSON.stringify(this.value, null, 2) : String(this.value), + }, + }).catch(() => undefined); + } + + override render() { + return html` + `; + } +} + +export default UmbPropertyEditorUIMissingElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-property-editor-ui-missing': UmbPropertyEditorUIMissingElement; + } +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/PublishedContent/PublishContentTypeFactoryTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/PublishedContent/PublishContentTypeFactoryTest.cs index 025281e566..aa736641c7 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/PublishedContent/PublishContentTypeFactoryTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/PublishedContent/PublishContentTypeFactoryTest.cs @@ -81,6 +81,9 @@ internal sealed class PublishContentTypeFactoryTest : UmbracoIntegrationTest { var dataType = new DataTypeBuilder() .WithId(0) + .AddEditor() + .WithAlias(Constants.PropertyEditors.Aliases.TextBox) + .Done() .Build(); dataType.EditorUiAlias = "NotUpdated"; var dataTypeCreateResult = await DataTypeService.CreateAsync(dataType, Constants.Security.SuperUserKey); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DocumentRepositoryTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DocumentRepositoryTest.cs index b794eccd86..5a6d0671ec 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DocumentRepositoryTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DocumentRepositoryTest.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; @@ -103,7 +104,14 @@ internal sealed class DocumentRepositoryTest : UmbracoIntegrationTest var ctRepository = CreateRepository(scopeAccessor, out contentTypeRepository, out TemplateRepository tr); var editors = new PropertyEditorCollection(new DataEditorCollection(() => Enumerable.Empty())); - dtdRepository = new DataTypeRepository(scopeAccessor, appCaches, editors, LoggerFactory.CreateLogger(), LoggerFactory, ConfigurationEditorJsonSerializer); + dtdRepository = new DataTypeRepository( + scopeAccessor, + appCaches, + editors, + LoggerFactory.CreateLogger(), + LoggerFactory, + ConfigurationEditorJsonSerializer, + Services.GetRequiredService()); return ctRepository; } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/DataTypeServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/DataTypeServiceTests.cs index 6870f301c5..675f52d8bc 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/DataTypeServiceTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/DataTypeServiceTests.cs @@ -1,7 +1,6 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Linq; using NUnit.Framework; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; @@ -487,4 +486,36 @@ internal sealed class DataTypeServiceTests : UmbracoIntegrationTest Assert.AreEqual("bodyText", secondResult.NodeAlias); Assert.AreEqual("Body text", secondResult.NodeName); } + + [Test] + public async Task Gets_MissingPropertyEditor_When_Editor_NotFound() + { + // Arrange + IDataType? dataType = (await DataTypeService.CreateAsync( + new DataType(new TestEditor(DataValueEditorFactory), ConfigurationEditorJsonSerializer) + { + Name = "Test Missing Editor", + DatabaseType = ValueStorageType.Ntext, + }, + Constants.Security.SuperUserKey)).Result; + + Assert.IsNotNull(dataType); + + // Act + IDataType? actual = await DataTypeService.GetAsync(dataType.Key); + + // Assert + Assert.NotNull(actual); + Assert.AreEqual(dataType.Key, actual.Key); + Assert.IsAssignableFrom(typeof(MissingPropertyEditor), actual.Editor); + Assert.AreEqual("Test Editor", actual.EditorAlias, "The alias should be the same as the original editor"); + Assert.AreEqual("Umb.PropertyEditorUi.Missing", actual.EditorUiAlias, "The editor UI alias should be the Missing Editor UI"); + } + + private class TestEditor : DataEditor + { + public TestEditor(IDataValueEditorFactory dataValueEditorFactory) + : base(dataValueEditorFactory) => + Alias = "Test Editor"; + } }