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>
This commit is contained in:
Laura Neto
2025-09-10 11:20:06 +02:00
committed by GitHub
parent c93d4d9318
commit d6c181457c
22 changed files with 451 additions and 68 deletions

View File

@@ -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;
/// <summary>
/// Initializes a new instance of the <see cref="MissingPropertyEditor"/> class.
/// </summary>
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<IDataValueEditorFactory>())
{
}
/// <inheritdoc />
public string Alias { get; }
/// <summary>
/// Gets the name of the editor.
/// </summary>
public string Name => "Missing property editor";
/// <inheritdoc />
public bool IsDeprecated => false;
public IDictionary<string, object> DefaultConfiguration => throw new NotImplementedException();
/// <inheritdoc />
public bool SupportsReadOnly => true;
public IPropertyIndexValueFactory PropertyIndexValueFactory => throw new NotImplementedException();
/// <inheritdoc />
public IDictionary<string, object> DefaultConfiguration => new Dictionary<string, object>();
/// <inheritdoc />
public IPropertyIndexValueFactory PropertyIndexValueFactory => new DefaultPropertyIndexValueFactory();
/// <inheritdoc />
public IDataValueEditor GetValueEditor() => _valueEditor
??= _dataValueEditorFactory.Create<MissingPropertyValueEditor>(
new DataEditorAttribute(EditorAlias));
/// <inheritdoc />
public IDataValueEditor GetValueEditor(object? configurationObject) => GetValueEditor();
/// <inheritdoc />
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();
/// <inheritdoc />
public override bool IsReadOnly => true;
}
}

View File

@@ -0,0 +1,21 @@
using Umbraco.Cms.Core.Models.PublishedContent;
namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters;
/// <summary>
/// A value converter for the missing property editor, which always returns a string.
/// </summary>
[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;
}

View File

@@ -21,14 +21,12 @@ namespace Umbraco.Cms.Core.Services.Implement
/// </summary>
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<IIdKeyMap> _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<IIdKeyMap> 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<IIdKeyMap> 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<IDataType>().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<IEnumerable<IDataType>>(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<IDataType> query = Query<IDataType>().Where(x => x.EditorAlias == propertyEditorAlias);
IEnumerable<IDataType> 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<IDataType> query = Query<IDataType>().Where(x => propertyEditorAlias.Contains(x.EditorAlias));
IEnumerable<IDataType> 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<IDataType> query = Query<IDataType>().Where(x => x.EditorUiAlias == editorUiAlias);
IEnumerable<IDataType> 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<IDataType> dataTypes = _dataTypeRepository.GetMany(ids).ToArray();
ConvertMissingEditorsOfDataTypesToLabels(dataTypes);
return dataTypes;
}
private void ConvertMissingEditorOfDataTypeToLabel(IDataType? dataType)
{
if (dataType == null)
{
return;
}
ConvertMissingEditorsOfDataTypesToLabels([dataType]);
}
private void ConvertMissingEditorsOfDataTypesToLabels(IEnumerable<IDataType> 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<IDataType> dataTypesWithMissingEditors = dataTypes.Where(x => x.Editor is MissingPropertyEditor);
foreach (IDataType dataType in dataTypesWithMissingEditors)
{
dataType.Editor = new LabelPropertyEditor(_dataValueEditorFactory, _ioHelper);
}
}
public Attempt<OperationResult<MoveOperationStatusType>?> Move(IDataType toMove, int parentId)
{
Guid? containerKey = null;