Feature: single block property editor (#20098)
* First Go at the single block property editor based on blocklistpropertyeditor * Add simalar tests to the blocklist editor Also check whether either block of configured blocks can be picked and used from a data perspective * WIP singleblock Valiation tests * Finished first full pass off SingleBlock validation testing * Typos, Future test function * Restore accidently removed file * Introduce propertyValueConverter * Comment updates * Add singleBlock renderer * Textual improvements Comment improvements, remove licensing in file * Update DataEditorCount by 1 as we introduced a new one * Align test naming * Add ignored singleblock default renderer * Enable SingleBlock Property Indexing * Enable Partial value merging * Fix indentation --------- Co-authored-by: kjac <kja@umbraco.dk>
This commit is contained in:
@@ -5,6 +5,7 @@ using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.IO;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.PropertyEditors;
|
||||
using Umbraco.Cms.Infrastructure.PropertyEditors;
|
||||
using Umbraco.Cms.Infrastructure.Serialization;
|
||||
using Umbraco.Cms.Tests.Common.Builders.Extensions;
|
||||
using Umbraco.Cms.Tests.Common.Builders.Interfaces;
|
||||
@@ -194,6 +195,10 @@ public class DataTypeBuilder
|
||||
dataTypeBuilder.WithConfigurationEditor(
|
||||
new BlockListConfigurationEditor(ioHelper) { DefaultConfiguration = configuration });
|
||||
break;
|
||||
case Constants.PropertyEditors.Aliases.SingleBlock:
|
||||
dataTypeBuilder.WithConfigurationEditor(
|
||||
new SingleBlockConfigurationEditor(ioHelper) { DefaultConfiguration = configuration });
|
||||
break;
|
||||
case Constants.PropertyEditors.Aliases.RichText:
|
||||
dataTypeBuilder.WithConfigurationEditor(
|
||||
new RichTextConfigurationEditor(ioHelper) { DefaultConfiguration = configuration });
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.ContentEditing;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Core.Services;
|
||||
|
||||
// Disclaimer: Based on code generated by Claude code.
|
||||
// Most likely not complete, but the logic looks sound.
|
||||
public interface IContentEditingModelFactory
|
||||
{
|
||||
Task<ContentUpdateModel> CreateFromAsync(IContent content);
|
||||
}
|
||||
|
||||
public class ContentEditingModelFactory : IContentEditingModelFactory
|
||||
{
|
||||
private readonly ITemplateService _templateService;
|
||||
|
||||
public ContentEditingModelFactory(ITemplateService templateService)
|
||||
{
|
||||
_templateService = templateService;
|
||||
}
|
||||
|
||||
public async Task<ContentUpdateModel> CreateFromAsync(IContent content)
|
||||
{
|
||||
{
|
||||
var templateKey = content.TemplateId.HasValue
|
||||
? (await _templateService.GetAsync(content.TemplateId.Value))?.Key
|
||||
: null;
|
||||
var model = new ContentUpdateModel { TemplateKey = templateKey };
|
||||
var properties = new List<PropertyValueModel>();
|
||||
var variants = new List<VariantModel>();
|
||||
|
||||
MapProperties(content, properties);
|
||||
|
||||
MapNames(content, properties, variants);
|
||||
|
||||
model.Properties = properties;
|
||||
model.Variants = variants;
|
||||
return model;
|
||||
}
|
||||
}
|
||||
|
||||
private static void MapNames(IContent content, List<PropertyValueModel> properties, List<VariantModel> variants)
|
||||
{
|
||||
// Handle variants (content names per culture/segment)
|
||||
var contentVariesByCulture = content.ContentType.VariesByCulture();
|
||||
var contentVariesBySegment = content.ContentType.VariesBySegment();
|
||||
if (contentVariesByCulture || contentVariesBySegment)
|
||||
{
|
||||
// Get all unique culture/segment combinations from CultureInfos
|
||||
var cultureSegmentCombinations = new HashSet<(string? culture, string? segment)>();
|
||||
|
||||
// Add invariant combination
|
||||
cultureSegmentCombinations.Add((null, null));
|
||||
if (contentVariesByCulture)
|
||||
{
|
||||
// Add cultures
|
||||
foreach (var culture in content.AvailableCultures)
|
||||
{
|
||||
cultureSegmentCombinations.Add((culture, null));
|
||||
}
|
||||
}
|
||||
|
||||
// For segment support, we need to extract segments from property values
|
||||
// since content doesn't have "AvailableSegments" like cultures
|
||||
if (contentVariesBySegment)
|
||||
{
|
||||
var segmentsFromProperties = properties
|
||||
.Where(p => !string.IsNullOrEmpty(p.Segment))
|
||||
.Select(p => p.Segment)
|
||||
.Distinct()
|
||||
.ToList();
|
||||
foreach (var segment in segmentsFromProperties)
|
||||
{
|
||||
cultureSegmentCombinations.Add((null, segment));
|
||||
// If content also varies by culture, add culture+segment combinations
|
||||
if (contentVariesByCulture)
|
||||
{
|
||||
foreach (var culture in content.AvailableCultures)
|
||||
{
|
||||
cultureSegmentCombinations.Add((culture, segment));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create variants for each combination
|
||||
foreach (var (culture, segment) in cultureSegmentCombinations)
|
||||
{
|
||||
string? variantName;
|
||||
if (culture == null && segment == null)
|
||||
{
|
||||
// Invariant
|
||||
variantName = content.Name;
|
||||
}
|
||||
else if (culture != null && segment == null)
|
||||
{
|
||||
// Culture-specific
|
||||
variantName = content.GetCultureName(culture);
|
||||
}
|
||||
else
|
||||
{
|
||||
// For segment-specific or culture+segment combinations,
|
||||
// we'll use the invariant or culture name as segments don't have separate names
|
||||
variantName = culture != null ? content.GetCultureName(culture) : content.Name;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(variantName))
|
||||
{
|
||||
variants.Add(new VariantModel { Culture = culture, Segment = segment, Name = variantName });
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// For invariant content, add single variant
|
||||
variants.Add(new VariantModel { Culture = null, Segment = null, Name = content.Name ?? string.Empty });
|
||||
}
|
||||
}
|
||||
|
||||
private static void MapProperties(IContent content, List<PropertyValueModel> properties)
|
||||
{
|
||||
// Handle properties
|
||||
foreach (var property in content.Properties)
|
||||
{
|
||||
var propertyVariesByCulture = property.PropertyType.VariesByCulture();
|
||||
var propertyVariesBySegment = property.PropertyType.VariesBySegment();
|
||||
|
||||
// Get all property values from the property's Values collection
|
||||
foreach (var propertyValue in property.Values)
|
||||
{
|
||||
if (propertyValue.EditedValue != null)
|
||||
{
|
||||
properties.Add(new PropertyValueModel
|
||||
{
|
||||
Alias = property.Alias,
|
||||
Value = propertyValue.EditedValue,
|
||||
Culture = propertyVariesByCulture ? propertyValue.Culture : null,
|
||||
Segment = propertyVariesBySegment ? propertyValue.Segment : null,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: if no values found in the Values collection, try the traditional approach
|
||||
if (!property.Values.Any())
|
||||
{
|
||||
if (propertyVariesByCulture && content.AvailableCultures.Any())
|
||||
{
|
||||
// Handle culture variants
|
||||
foreach (var culture in content.AvailableCultures)
|
||||
{
|
||||
var cultureValue = property.GetValue(culture);
|
||||
if (cultureValue != null)
|
||||
{
|
||||
properties.Add(new PropertyValueModel
|
||||
{
|
||||
Alias = property.Alias, Value = cultureValue, Culture = culture, Segment = null,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Also add the invariant value if it exists
|
||||
var invariantValue = property.GetValue();
|
||||
if (invariantValue != null)
|
||||
{
|
||||
properties.Add(new PropertyValueModel
|
||||
{
|
||||
Alias = property.Alias, Value = invariantValue, Culture = null, Segment = null,
|
||||
});
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Handle invariant properties
|
||||
var value = property.GetValue();
|
||||
if (value != null)
|
||||
{
|
||||
properties.Add(new PropertyValueModel
|
||||
{
|
||||
Alias = property.Alias, Value = value, Culture = null, Segment = null,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user