Single block migration (#20663)
* WiP blocklist migration * Mostly working migration * [WIP] deconstructed the migration to prefetch and process all data that requires the old definitions * Working singleblock migration * Abstracted some logic and applied it to settings elements too. * Align class and file name. * Minor code warning resolution. * More and better comments + made classes internal where it made sense --------- Co-authored-by: Andy Butland <abutland73@gmail.com>
This commit is contained in:
@@ -6,9 +6,11 @@ namespace Umbraco.Cms.Core.Cache.PropertyEditors;
|
||||
|
||||
internal sealed class BlockEditorElementTypeCache : IBlockEditorElementTypeCache
|
||||
{
|
||||
private const string CacheKey = $"{nameof(BlockEditorElementTypeCache)}_ElementTypes";
|
||||
private readonly IContentTypeService _contentTypeService;
|
||||
private readonly AppCaches _appCaches;
|
||||
|
||||
|
||||
public BlockEditorElementTypeCache(IContentTypeService contentTypeService, AppCaches appCaches)
|
||||
{
|
||||
_contentTypeService = contentTypeService;
|
||||
@@ -20,15 +22,15 @@ internal sealed class BlockEditorElementTypeCache : IBlockEditorElementTypeCache
|
||||
public IEnumerable<IContentType> GetAll()
|
||||
{
|
||||
// TODO: make this less dumb; don't fetch all elements, only fetch the items that aren't yet in the cache and amend the cache as more elements are loaded
|
||||
|
||||
const string cacheKey = $"{nameof(BlockEditorElementTypeCache)}_ElementTypes";
|
||||
IEnumerable<IContentType>? cachedElements = _appCaches.RequestCache.GetCacheItem<IEnumerable<IContentType>>(cacheKey);
|
||||
IEnumerable<IContentType>? cachedElements = _appCaches.RequestCache.GetCacheItem<IEnumerable<IContentType>>(CacheKey);
|
||||
if (cachedElements is null)
|
||||
{
|
||||
cachedElements = _contentTypeService.GetAllElementTypes();
|
||||
_appCaches.RequestCache.Set(cacheKey, cachedElements);
|
||||
_appCaches.RequestCache.Set(CacheKey, cachedElements);
|
||||
}
|
||||
|
||||
return cachedElements;
|
||||
}
|
||||
|
||||
public void ClearAll() => _appCaches.RequestCache.Remove(CacheKey);
|
||||
}
|
||||
|
||||
@@ -6,4 +6,5 @@ public interface IBlockEditorElementTypeCache
|
||||
{
|
||||
IEnumerable<IContentType> GetMany(IEnumerable<Guid> keys);
|
||||
IEnumerable<IContentType> GetAll();
|
||||
void ClearAll() { }
|
||||
}
|
||||
|
||||
@@ -139,5 +139,9 @@ public class UmbracoPlan : MigrationPlan
|
||||
To<V_17_0_0.AddDistributedJobLock>("{263075BF-F18A-480D-92B4-4947D2EAB772}");
|
||||
To<V_17_0_0.AddLastSyncedTable>("26179D88-58CE-4C92-B4A4-3CBA6E7188AC");
|
||||
To<V_17_0_0.EnsureDefaultMediaFolderHasDefaultCollection>("{8B2C830A-4FFB-4433-8337-8649B0BF52C8}");
|
||||
|
||||
// To 18.0.0
|
||||
// TODO (V18): Enable on 18 branch
|
||||
//// To<V_18_0_0.MigrateSingleBlockList>("{74332C49-B279-4945-8943-F8F00B1F5949}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,436 @@
|
||||
using System.Collections.Concurrent;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NPoco;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Cache;
|
||||
using Umbraco.Cms.Core.Cache.PropertyEditors;
|
||||
using Umbraco.Cms.Core.IO;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.Blocks;
|
||||
using Umbraco.Cms.Core.Models.Editors;
|
||||
using Umbraco.Cms.Core.PropertyEditors;
|
||||
using Umbraco.Cms.Core.Scoping;
|
||||
using Umbraco.Cms.Core.Serialization;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Web;
|
||||
using Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_18_0_0.SingleBlockList;
|
||||
using Umbraco.Cms.Infrastructure.Persistence;
|
||||
using Umbraco.Cms.Infrastructure.Persistence.Dtos;
|
||||
using Umbraco.Cms.Infrastructure.PropertyEditors;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_18_0_0;
|
||||
|
||||
public class MigrateSingleBlockList : AsyncMigrationBase
|
||||
{
|
||||
private readonly IUmbracoContextFactory _umbracoContextFactory;
|
||||
private readonly ILanguageService _languageService;
|
||||
private readonly IContentTypeService _contentTypeService;
|
||||
private readonly IMediaTypeService _mediaTypeService;
|
||||
private readonly IDataTypeService _dataTypeService;
|
||||
private readonly ICoreScopeProvider _coreScopeProvider;
|
||||
private readonly SingleBlockListProcessor _singleBlockListProcessor;
|
||||
private readonly IJsonSerializer _jsonSerializer;
|
||||
private readonly SingleBlockListConfigurationCache _blockListConfigurationCache;
|
||||
private readonly IBlockEditorElementTypeCache _elementTypeCache;
|
||||
private readonly AppCaches _appCaches;
|
||||
private readonly ILogger<MigrateSingleBlockList> _logger;
|
||||
private readonly IDataValueEditor _dummySingleBlockValueEditor;
|
||||
|
||||
public MigrateSingleBlockList(
|
||||
IMigrationContext context,
|
||||
IUmbracoContextFactory umbracoContextFactory,
|
||||
ILanguageService languageService,
|
||||
IContentTypeService contentTypeService,
|
||||
IMediaTypeService mediaTypeService,
|
||||
IDataTypeService dataTypeService,
|
||||
ILogger<MigrateSingleBlockList> logger,
|
||||
ICoreScopeProvider coreScopeProvider,
|
||||
SingleBlockListProcessor singleBlockListProcessor,
|
||||
IJsonSerializer jsonSerializer,
|
||||
SingleBlockListConfigurationCache blockListConfigurationCache,
|
||||
IDataValueEditorFactory dataValueEditorFactory,
|
||||
IIOHelper ioHelper,
|
||||
IBlockValuePropertyIndexValueFactory blockValuePropertyIndexValueFactory,
|
||||
IBlockEditorElementTypeCache elementTypeCache,
|
||||
AppCaches appCaches)
|
||||
: base(context)
|
||||
{
|
||||
_umbracoContextFactory = umbracoContextFactory;
|
||||
_languageService = languageService;
|
||||
_contentTypeService = contentTypeService;
|
||||
_mediaTypeService = mediaTypeService;
|
||||
_dataTypeService = dataTypeService;
|
||||
_logger = logger;
|
||||
_coreScopeProvider = coreScopeProvider;
|
||||
_singleBlockListProcessor = singleBlockListProcessor;
|
||||
_jsonSerializer = jsonSerializer;
|
||||
_blockListConfigurationCache = blockListConfigurationCache;
|
||||
_elementTypeCache = elementTypeCache;
|
||||
_appCaches = appCaches;
|
||||
|
||||
_dummySingleBlockValueEditor = new SingleBlockPropertyEditor(dataValueEditorFactory, jsonSerializer, ioHelper, blockValuePropertyIndexValueFactory).GetValueEditor();
|
||||
}
|
||||
|
||||
protected override async Task MigrateAsync()
|
||||
{
|
||||
// gets filled by all registered ITypedSingleBlockListProcessor
|
||||
IEnumerable<string> propertyEditorAliases = _singleBlockListProcessor.GetSupportedPropertyEditorAliases();
|
||||
|
||||
using UmbracoContextReference umbracoContextReference = _umbracoContextFactory.EnsureUmbracoContext();
|
||||
var languagesById = (await _languageService.GetAllAsync())
|
||||
.ToDictionary(language => language.Id);
|
||||
|
||||
IEnumerable<IContentType> allContentTypes = _contentTypeService.GetAll();
|
||||
IEnumerable<IPropertyType> contentPropertyTypes = allContentTypes
|
||||
.SelectMany(ct => ct.PropertyTypes);
|
||||
|
||||
IMediaType[] allMediaTypes = _mediaTypeService.GetAll().ToArray();
|
||||
IEnumerable<IPropertyType> mediaPropertyTypes = allMediaTypes
|
||||
.SelectMany(ct => ct.PropertyTypes);
|
||||
|
||||
// get all relevantPropertyTypes
|
||||
var relevantPropertyEditors =
|
||||
contentPropertyTypes.Concat(mediaPropertyTypes).DistinctBy(pt => pt.Id)
|
||||
.Where(pt => propertyEditorAliases.Contains(pt.PropertyEditorAlias))
|
||||
.GroupBy(pt => pt.PropertyEditorAlias)
|
||||
.ToDictionary(group => group.Key, group => group.ToArray());
|
||||
|
||||
// populate the cache to limit amount of db locks in recursion logic.
|
||||
var blockListsConfiguredAsSingleCount = await _blockListConfigurationCache.Populate();
|
||||
|
||||
if (blockListsConfiguredAsSingleCount == 0)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"No blocklist were configured as single, nothing to do.");
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.LogInformation(
|
||||
"Found {blockListsConfiguredAsSingleCount} number of blockListConfigurations with UseSingleBlockMode set to true",
|
||||
blockListsConfiguredAsSingleCount);
|
||||
|
||||
// we want to batch actual update calls to the database, so we are grouping them by propertyEditorAlias
|
||||
// and again by propertyType(dataType).
|
||||
var updateItemsByPropertyEditorAlias = new Dictionary<string, Dictionary<IPropertyType, List<UpdateItem>>>();
|
||||
|
||||
// For each propertyEditor, collect and process all propertyTypes and their propertyData
|
||||
foreach (var propertyEditorAlias in propertyEditorAliases)
|
||||
{
|
||||
if (relevantPropertyEditors.TryGetValue(propertyEditorAlias, out IPropertyType[]? propertyTypes) is false)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
_logger.LogInformation(
|
||||
"Migration starting for all properties of type: {propertyEditorAlias}",
|
||||
propertyEditorAlias);
|
||||
Dictionary<IPropertyType, List<UpdateItem>> updateItemsByPropertyType = await ProcessPropertyTypesAsync(propertyTypes, languagesById);
|
||||
if (updateItemsByPropertyType.Count < 1)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"No properties have been found to migrate for {propertyEditorAlias}",
|
||||
propertyEditorAlias);
|
||||
return;
|
||||
}
|
||||
|
||||
updateItemsByPropertyEditorAlias[propertyEditorAlias] = updateItemsByPropertyType;
|
||||
}
|
||||
|
||||
// update the configuration of all propertyTypes
|
||||
var singleBlockListDataTypesIds = _blockListConfigurationCache.CachedDataTypes.ToList().Select(type => type.Id).ToList();
|
||||
|
||||
string updateSql = $@"
|
||||
UPDATE umbracoDataType
|
||||
SET propertyEditorAlias = '{Constants.PropertyEditors.Aliases.SingleBlock}',
|
||||
propertyEditorUiAlias = 'Umb.PropertyEditorUi.SingleBlock'
|
||||
WHERE nodeId IN (@0)";
|
||||
await Database.ExecuteAsync(updateSql, singleBlockListDataTypesIds);
|
||||
|
||||
// we need to clear the elementTypeCache so the second part of the migration can work with the update dataTypes
|
||||
// and also the isolated/runtime Caches as that is what its build from in the default implementation
|
||||
_elementTypeCache.ClearAll();
|
||||
_appCaches.IsolatedCaches.ClearAllCaches();
|
||||
_appCaches.RuntimeCache.Clear();
|
||||
RebuildCache = true;
|
||||
|
||||
// now that we have updated the configuration of all propertyTypes, we can save the updated propertyTypes
|
||||
foreach (string propertyEditorAlias in updateItemsByPropertyEditorAlias.Keys)
|
||||
{
|
||||
if (await SavePropertyTypes(updateItemsByPropertyEditorAlias[propertyEditorAlias]))
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"Migration succeeded for all properties of type: {propertyEditorAlias}",
|
||||
propertyEditorAlias);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogError(
|
||||
"Migration failed for one or more properties of type: {propertyEditorAlias}",
|
||||
propertyEditorAlias);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<Dictionary<IPropertyType, List<UpdateItem>>> ProcessPropertyTypesAsync(IPropertyType[] propertyTypes, IDictionary<int, ILanguage> languagesById)
|
||||
{
|
||||
var updateItemsByPropertyType = new Dictionary<IPropertyType, List<UpdateItem>>();
|
||||
foreach (IPropertyType propertyType in propertyTypes)
|
||||
{
|
||||
// make sure the passed in data is valid and can be processed
|
||||
IDataType dataType = await _dataTypeService.GetAsync(propertyType.DataTypeKey)
|
||||
?? throw new InvalidOperationException("The data type could not be fetched.");
|
||||
IDataValueEditor valueEditor = dataType.Editor?.GetValueEditor()
|
||||
?? throw new InvalidOperationException(
|
||||
"The data type value editor could not be obtained.");
|
||||
|
||||
// fetch all the propertyData for the current propertyType
|
||||
Sql<ISqlContext> sql = Sql()
|
||||
.Select<PropertyDataDto>()
|
||||
.From<PropertyDataDto>()
|
||||
.InnerJoin<ContentVersionDto>()
|
||||
.On<PropertyDataDto, ContentVersionDto>((propertyData, contentVersion) =>
|
||||
propertyData.VersionId == contentVersion.Id)
|
||||
.LeftJoin<DocumentVersionDto>()
|
||||
.On<ContentVersionDto, DocumentVersionDto>((contentVersion, documentVersion) =>
|
||||
contentVersion.Id == documentVersion.Id)
|
||||
.Where<PropertyDataDto, ContentVersionDto, DocumentVersionDto>((propertyData, contentVersion, documentVersion) =>
|
||||
(contentVersion.Current == true || documentVersion.Published == true)
|
||||
&& propertyData.PropertyTypeId == propertyType.Id);
|
||||
|
||||
List<PropertyDataDto> propertyDataDtos = await Database.FetchAsync<PropertyDataDto>(sql);
|
||||
if (propertyDataDtos.Count < 1)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var updateItems = new List<UpdateItem>();
|
||||
|
||||
// process all the propertyData
|
||||
// if none of the processors modify the value, the propertyData is skipped from being saved.
|
||||
foreach (PropertyDataDto propertyDataDto in propertyDataDtos)
|
||||
{
|
||||
if (ProcessPropertyDataDto(propertyDataDto, propertyType, languagesById, valueEditor, out UpdateItem? updateItem) is false)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
updateItems.Add(updateItem!);
|
||||
}
|
||||
|
||||
updateItemsByPropertyType[propertyType] = updateItems;
|
||||
}
|
||||
|
||||
return updateItemsByPropertyType;
|
||||
}
|
||||
|
||||
private async Task<bool> SavePropertyTypes(IDictionary<IPropertyType, List<UpdateItem>> propertyTypes)
|
||||
{
|
||||
foreach (IPropertyType propertyType in propertyTypes.Keys)
|
||||
{
|
||||
// The dataType and valueEditor should be constructed as we have done this before, but we hate null values.
|
||||
IDataType dataType = await _dataTypeService.GetAsync(propertyType.DataTypeKey)
|
||||
?? throw new InvalidOperationException("The data type could not be fetched.");
|
||||
IDataValueEditor updatedValueEditor = dataType.Editor?.GetValueEditor()
|
||||
?? throw new InvalidOperationException(
|
||||
"The data type value editor could not be obtained.");
|
||||
|
||||
// batch by datatype
|
||||
var propertyDataDtos = propertyTypes[propertyType].Select(item => item.PropertyDataDto).ToList();
|
||||
|
||||
var updateBatch = propertyDataDtos.Select(propertyDataDto =>
|
||||
UpdateBatch.For(propertyDataDto, Database.StartSnapshot(propertyDataDto))).ToList();
|
||||
|
||||
var updatesToSkip = new ConcurrentBag<UpdateBatch<PropertyDataDto>>();
|
||||
|
||||
var progress = 0;
|
||||
|
||||
void HandleUpdateBatch(UpdateBatch<PropertyDataDto> update)
|
||||
{
|
||||
using UmbracoContextReference umbracoContextReference = _umbracoContextFactory.EnsureUmbracoContext();
|
||||
|
||||
progress++;
|
||||
if (progress % 100 == 0)
|
||||
{
|
||||
_logger.LogInformation(" - finíshed {progress} of {total} properties", progress, updateBatch.Count);
|
||||
}
|
||||
|
||||
PropertyDataDto propertyDataDto = update.Poco;
|
||||
|
||||
if (FinalizeUpdateItem(propertyTypes[propertyType].First(item => Equals(item.PropertyDataDto, update.Poco)), updatedValueEditor) is false)
|
||||
{
|
||||
updatesToSkip.Add(update);
|
||||
}
|
||||
}
|
||||
|
||||
if (DatabaseType == DatabaseType.SQLite)
|
||||
{
|
||||
// SQLite locks up if we run the migration in parallel, so... let's not.
|
||||
foreach (UpdateBatch<PropertyDataDto> update in updateBatch)
|
||||
{
|
||||
HandleUpdateBatch(update);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Parallel.ForEachAsync(updateBatch, async (update, token) =>
|
||||
{
|
||||
//Foreach here, but we need to suppress the flow before each task, but not the actuall await of the task
|
||||
Task task;
|
||||
using (ExecutionContext.SuppressFlow())
|
||||
{
|
||||
task = Task.Run(
|
||||
() =>
|
||||
{
|
||||
using ICoreScope scope = _coreScopeProvider.CreateCoreScope();
|
||||
scope.Complete();
|
||||
HandleUpdateBatch(update);
|
||||
},
|
||||
token);
|
||||
}
|
||||
|
||||
await task;
|
||||
}).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
updateBatch.RemoveAll(updatesToSkip.Contains);
|
||||
|
||||
if (updateBatch.Any() is false)
|
||||
{
|
||||
_logger.LogDebug(" - no properties to convert, continuing");
|
||||
continue;
|
||||
}
|
||||
|
||||
_logger.LogInformation(" - {totalConverted} properties converted, saving...", updateBatch.Count);
|
||||
var result = Database.UpdateBatch(updateBatch, new BatchOptions { BatchSize = 100 });
|
||||
if (result != updateBatch.Count)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"The database batch update was supposed to update {updateBatch.Count} property DTO entries, but it updated {result} entries.");
|
||||
}
|
||||
|
||||
_logger.LogDebug(
|
||||
"Migration completed for property type: {propertyTypeName} (id: {propertyTypeId}, alias: {propertyTypeAlias}, editor alias: {propertyTypeEditorAlias}) - {updateCount} property DTO entries updated.",
|
||||
propertyType.Name,
|
||||
propertyType.Id,
|
||||
propertyType.Alias,
|
||||
propertyType.PropertyEditorAlias,
|
||||
result);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool ProcessPropertyDataDto(
|
||||
PropertyDataDto propertyDataDto,
|
||||
IPropertyType propertyType,
|
||||
IDictionary<int, ILanguage> languagesById,
|
||||
IDataValueEditor valueEditor,
|
||||
out UpdateItem? updateItem)
|
||||
{
|
||||
// NOTE: some old property data DTOs can have variance defined, even if the property type no longer varies
|
||||
var culture = propertyType.VariesByCulture()
|
||||
&& propertyDataDto.LanguageId.HasValue
|
||||
&& languagesById.TryGetValue(propertyDataDto.LanguageId.Value, out ILanguage? language)
|
||||
? language.IsoCode
|
||||
: null;
|
||||
|
||||
if (culture is null && propertyType.VariesByCulture())
|
||||
{
|
||||
// if we end up here, the property DTO is bound to a language that no longer exists. this is an error scenario,
|
||||
// and we can't really handle it in any other way than logging; in all likelihood this is an old property version,
|
||||
// and it won't cause any runtime issues
|
||||
_logger.LogWarning(
|
||||
" - property data with id: {propertyDataId} references a language that does not exist - language id: {languageId} (property type: {propertyTypeName}, id: {propertyTypeId}, alias: {propertyTypeAlias})",
|
||||
propertyDataDto.Id,
|
||||
propertyDataDto.LanguageId,
|
||||
propertyType.Name,
|
||||
propertyType.Id,
|
||||
propertyType.Alias);
|
||||
updateItem = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
// create a fake property to be able to get a typed value and run it trough the processors.
|
||||
var segment = propertyType.VariesBySegment() ? propertyDataDto.Segment : null;
|
||||
var property = new Property(propertyType);
|
||||
property.SetValue(propertyDataDto.Value, culture, segment);
|
||||
var toEditorValue = valueEditor.ToEditor(property, culture, segment);
|
||||
|
||||
if (TryTransformValue(toEditorValue, property, out var updatedValue) is false)
|
||||
{
|
||||
_logger.LogDebug(
|
||||
" - skipping as no processor modified the data for property data with id: {propertyDataId} (property type: {propertyTypeName}, id: {propertyTypeId}, alias: {propertyTypeAlias})",
|
||||
propertyDataDto.Id,
|
||||
propertyType.Name,
|
||||
propertyType.Id,
|
||||
propertyType.Alias);
|
||||
updateItem = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
updateItem = new UpdateItem(propertyDataDto, propertyType, updatedValue);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Takes the updated value that was instanced from the db value by the old ValueEditors
|
||||
/// And runs it through the updated ValueEditors and sets it on the PropertyDataDto
|
||||
/// </summary>
|
||||
private bool FinalizeUpdateItem(UpdateItem updateItem, IDataValueEditor updatedValueEditor)
|
||||
{
|
||||
var editorValue = _jsonSerializer.Serialize(updateItem.UpdatedValue);
|
||||
var dbValue = updateItem.UpdatedValue is SingleBlockValue
|
||||
? _dummySingleBlockValueEditor.FromEditor(new ContentPropertyData(editorValue, null), null)
|
||||
: updatedValueEditor.FromEditor(new ContentPropertyData(editorValue, null), null);
|
||||
if (dbValue is not string stringValue || stringValue.DetectIsJson() is false)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
" - value editor did not yield a valid JSON string as FromEditor value property data with id: {propertyDataId} (property type: {propertyTypeName}, id: {propertyTypeId}, alias: {propertyTypeAlias})",
|
||||
updateItem.PropertyDataDto.Id,
|
||||
updateItem.PropertyType.Name,
|
||||
updateItem.PropertyType.Id,
|
||||
updateItem.PropertyType.Alias);
|
||||
return false;
|
||||
}
|
||||
|
||||
updateItem.PropertyDataDto.TextValue = stringValue;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If the value is a BlockListValue, and its datatype is configured as single
|
||||
/// We also need to convert the outer BlockListValue to a SingleBlockValue
|
||||
/// Either way, we need to run the value through the processors to possibly update nested values
|
||||
/// </summary>
|
||||
private bool TryTransformValue(object? toEditorValue, Property property, out object? value)
|
||||
{
|
||||
bool hasChanged = _singleBlockListProcessor.ProcessToEditorValue(toEditorValue);
|
||||
|
||||
if (toEditorValue is BlockListValue blockListValue
|
||||
&& _blockListConfigurationCache.IsPropertyEditorBlockListConfiguredAsSingle(property.PropertyType.DataTypeKey))
|
||||
{
|
||||
value = _singleBlockListProcessor.ConvertBlockListToSingleBlock(blockListValue);
|
||||
return true;
|
||||
}
|
||||
|
||||
value = toEditorValue;
|
||||
return hasChanged;
|
||||
}
|
||||
|
||||
private class UpdateItem
|
||||
{
|
||||
public UpdateItem(PropertyDataDto propertyDataDto, IPropertyType propertyType, object? updatedValue)
|
||||
{
|
||||
PropertyDataDto = propertyDataDto;
|
||||
PropertyType = propertyType;
|
||||
UpdatedValue = updatedValue;
|
||||
}
|
||||
|
||||
public object? UpdatedValue { get; set; }
|
||||
|
||||
public PropertyDataDto PropertyDataDto { get; set; }
|
||||
|
||||
public IPropertyType PropertyType { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using Umbraco.Cms.Core.Models.Blocks;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_18_0_0.SingleBlockList;
|
||||
|
||||
[Obsolete("Will be removed in V22")] // Available in v17, activated in v18. Migration needs to work on LTS to LTS 17=>21
|
||||
public interface ITypedSingleBlockListProcessor
|
||||
{
|
||||
/// <summary>
|
||||
/// The type of the propertyEditor expects to receive as a value to process
|
||||
/// </summary>
|
||||
public Type PropertyEditorValueType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The property (data)editor aliases that this processor supports, as defined on their DataEditor attributes
|
||||
/// </summary>
|
||||
public IEnumerable<string> PropertyEditorAliases { get; }
|
||||
|
||||
/// <summary>
|
||||
/// object?: the editorValue being processed
|
||||
/// Func<object?, bool>: the function that will be called when nested content is detected
|
||||
/// </summary>
|
||||
public Func<object?, Func<object?, bool>, Func<BlockListValue, object>, bool> Process { get; }
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Umbraco.Cms.Core.Composing;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_18_0_0.SingleBlockList;
|
||||
|
||||
[Obsolete("Will be removed in V22")] // Available in v17, activated in v18. Migration needs to work on LTS to LTS 17=>21
|
||||
internal class MigrateSingleBlockListComposer : IComposer
|
||||
{
|
||||
public void Compose(IUmbracoBuilder builder)
|
||||
{
|
||||
builder.Services.AddSingleton<ITypedSingleBlockListProcessor, SingleBlockListBlockListProcessor>();
|
||||
builder.Services.AddSingleton<ITypedSingleBlockListProcessor, SingleBlockListBlockGridProcessor>();
|
||||
builder.Services.AddSingleton<ITypedSingleBlockListProcessor, SingleBlockListRteProcessor>();
|
||||
builder.Services.AddSingleton<SingleBlockListProcessor>();
|
||||
builder.Services.AddSingleton<SingleBlockListConfigurationCache>();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
using Umbraco.Cms.Core.Models.Blocks;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_18_0_0.SingleBlockList;
|
||||
|
||||
[Obsolete("Will be removed in V22")] // Available in v17, activated in v18. Migration needs to work on LTS to LTS 17=>21
|
||||
internal abstract class SingleBlockBlockProcessorBase
|
||||
{
|
||||
private readonly SingleBlockListConfigurationCache _blockListConfigurationCache;
|
||||
|
||||
public SingleBlockBlockProcessorBase(
|
||||
SingleBlockListConfigurationCache blockListConfigurationCache)
|
||||
{
|
||||
_blockListConfigurationCache = blockListConfigurationCache;
|
||||
}
|
||||
|
||||
protected bool ProcessBlockItemDataValues(
|
||||
BlockItemData blockItemData,
|
||||
Func<object?, bool> processNested,
|
||||
Func<BlockListValue, object> processOuterValue)
|
||||
{
|
||||
var hasChanged = false;
|
||||
|
||||
foreach (BlockPropertyValue blockPropertyValue in blockItemData.Values)
|
||||
{
|
||||
if (processNested.Invoke(blockPropertyValue.Value))
|
||||
{
|
||||
hasChanged = true;
|
||||
}
|
||||
|
||||
if (_blockListConfigurationCache.IsPropertyEditorBlockListConfiguredAsSingle(
|
||||
blockPropertyValue.PropertyType!.DataTypeKey)
|
||||
&& blockPropertyValue.Value is BlockListValue blockListValue)
|
||||
{
|
||||
blockPropertyValue.Value = processOuterValue.Invoke(blockListValue);
|
||||
hasChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
return hasChanged;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Models.Blocks;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_18_0_0.SingleBlockList;
|
||||
|
||||
[Obsolete("Will be removed in V22")] // Available in v17, activated in v18. Migration needs to work on LTS to LTS 17=>21
|
||||
internal class SingleBlockListBlockGridProcessor : SingleBlockBlockProcessorBase, ITypedSingleBlockListProcessor
|
||||
{
|
||||
public SingleBlockListBlockGridProcessor(SingleBlockListConfigurationCache blockListConfigurationCache)
|
||||
: base(blockListConfigurationCache)
|
||||
{
|
||||
}
|
||||
|
||||
public Type PropertyEditorValueType => typeof(BlockGridValue);
|
||||
|
||||
public IEnumerable<string> PropertyEditorAliases => [Constants.PropertyEditors.Aliases.BlockGrid];
|
||||
|
||||
public Func<object?, Func<object?, bool>,Func<BlockListValue,object>, bool> Process => ProcessBlocks;
|
||||
|
||||
private bool ProcessBlocks(
|
||||
object? value,
|
||||
Func<object?, bool> processNested,
|
||||
Func<BlockListValue, object> processOuterValue)
|
||||
{
|
||||
if (value is not BlockGridValue blockValue)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bool hasChanged = false;
|
||||
|
||||
foreach (BlockItemData contentData in blockValue.ContentData)
|
||||
{
|
||||
if (ProcessBlockItemDataValues(contentData, processNested, processOuterValue))
|
||||
{
|
||||
hasChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (BlockItemData settingsData in blockValue.SettingsData)
|
||||
{
|
||||
if (ProcessBlockItemDataValues(settingsData, processNested, processOuterValue))
|
||||
{
|
||||
hasChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
return hasChanged;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Models.Blocks;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_18_0_0.SingleBlockList;
|
||||
|
||||
[Obsolete("Will be removed in V22")] // Available in v17, activated in v18. Migration needs to work on LTS to LTS 17=>21
|
||||
internal class SingleBlockListBlockListProcessor : SingleBlockBlockProcessorBase, ITypedSingleBlockListProcessor
|
||||
{
|
||||
public SingleBlockListBlockListProcessor(
|
||||
SingleBlockListConfigurationCache blockListConfigurationCache)
|
||||
: base(blockListConfigurationCache)
|
||||
{
|
||||
}
|
||||
|
||||
public Type PropertyEditorValueType => typeof(BlockListValue);
|
||||
|
||||
public IEnumerable<string> PropertyEditorAliases => [Constants.PropertyEditors.Aliases.BlockList];
|
||||
|
||||
public Func<object?, Func<object?, bool>, Func<BlockListValue, object>, bool> Process => ProcessBlocks;
|
||||
|
||||
private bool ProcessBlocks(
|
||||
object? value,
|
||||
Func<object?, bool> processNested,
|
||||
Func<BlockListValue, object> processOuterValue)
|
||||
{
|
||||
if (value is not BlockListValue blockValue)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bool hasChanged = false;
|
||||
|
||||
// there might be another list inside the single list so more recursion, yeeey!
|
||||
foreach (BlockItemData contentData in blockValue.ContentData)
|
||||
{
|
||||
if (ProcessBlockItemDataValues(contentData, processNested, processOuterValue))
|
||||
{
|
||||
hasChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (BlockItemData settingsData in blockValue.SettingsData)
|
||||
{
|
||||
if (ProcessBlockItemDataValues(settingsData, processNested, processOuterValue))
|
||||
{
|
||||
hasChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
return hasChanged;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.PropertyEditors;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_18_0_0.SingleBlockList;
|
||||
|
||||
/// <summary>
|
||||
/// Used by the SingleBlockList Migration and its processors to avoid having to fetch (and thus lock)
|
||||
/// data from the db multiple times during the migration.
|
||||
/// </summary>
|
||||
[Obsolete("Will be removed in V22")] // Available in v17, activated in v18. Migration needs to work on LTS to LTS 17=>21
|
||||
public class SingleBlockListConfigurationCache
|
||||
{
|
||||
private readonly IDataTypeService _dataTypeService;
|
||||
private readonly List<IDataType> _singleBlockListDataTypes = new();
|
||||
|
||||
public SingleBlockListConfigurationCache(IDataTypeService dataTypeService)
|
||||
{
|
||||
_dataTypeService = dataTypeService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Populates a cache that holds all the property editor aliases that have a BlockList configuration with UseSingleBlockMode set to true.
|
||||
/// </summary>
|
||||
/// <returns> The number of blocklists with UseSingleBlockMode set to true.</returns>
|
||||
public async Task<int> Populate()
|
||||
{
|
||||
IEnumerable<IDataType> blockListDataTypes =
|
||||
await _dataTypeService.GetByEditorAliasAsync(Constants.PropertyEditors.Aliases.BlockList);
|
||||
|
||||
foreach (IDataType dataType in blockListDataTypes)
|
||||
{
|
||||
if (dataType.ConfigurationObject is BlockListConfiguration
|
||||
{
|
||||
UseSingleBlockMode: true, ValidationLimit.Max: 1
|
||||
})
|
||||
{
|
||||
_singleBlockListDataTypes.Add(dataType);
|
||||
}
|
||||
}
|
||||
|
||||
return _singleBlockListDataTypes.Count;
|
||||
}
|
||||
|
||||
// returns whether the passed in key belongs to a blocklist with UseSingleBlockMode set to true
|
||||
public bool IsPropertyEditorBlockListConfiguredAsSingle(Guid key) =>
|
||||
_singleBlockListDataTypes.Any(dt => dt.Key == key);
|
||||
|
||||
// The list of all blocklist data types that have UseSingleBlockMode set to true
|
||||
public IEnumerable<IDataType> CachedDataTypes => _singleBlockListDataTypes.AsReadOnly();
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Models.Blocks;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_18_0_0.SingleBlockList;
|
||||
|
||||
[Obsolete("Will be removed in V22")] // Available in v17, activated in v18. Migration needs to work on LTS to LTS 17=>21
|
||||
public class SingleBlockListProcessor
|
||||
{
|
||||
private readonly IEnumerable<ITypedSingleBlockListProcessor> _processors;
|
||||
|
||||
public SingleBlockListProcessor(IEnumerable<ITypedSingleBlockListProcessor> processors) => _processors = processors;
|
||||
|
||||
public IEnumerable<string> GetSupportedPropertyEditorAliases() =>
|
||||
_processors.SelectMany(p => p.PropertyEditorAliases);
|
||||
/// <summary>
|
||||
/// The entry point of the recursive conversion
|
||||
/// Find the first processor that can handle the value and call it's Process method
|
||||
/// </summary>
|
||||
/// <returns>Whether the value was changed</returns>
|
||||
public bool ProcessToEditorValue(object? editorValue)
|
||||
{
|
||||
ITypedSingleBlockListProcessor? processor =
|
||||
_processors.FirstOrDefault(p => p.PropertyEditorValueType == editorValue?.GetType());
|
||||
|
||||
return processor is not null && processor.Process.Invoke(editorValue, ProcessToEditorValue, ConvertBlockListToSingleBlock);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates and returns the passed in BlockListValue to a SingleBlockValue
|
||||
/// Should only be called by a core processor once a BlockListValue has been found that is configured in single block mode.
|
||||
/// </summary>
|
||||
public BlockValue ConvertBlockListToSingleBlock(BlockListValue blockListValue)
|
||||
{
|
||||
IBlockLayoutItem blockListLayoutItem = blockListValue.Layout[Constants.PropertyEditors.Aliases.BlockList].First();
|
||||
|
||||
var singleBlockLayoutItem = new SingleBlockLayoutItem
|
||||
{
|
||||
ContentKey = blockListLayoutItem.ContentKey,
|
||||
SettingsKey = blockListLayoutItem.SettingsKey,
|
||||
};
|
||||
|
||||
var singleBlockValue = new SingleBlockValue(singleBlockLayoutItem)
|
||||
{
|
||||
ContentData = blockListValue.ContentData,
|
||||
SettingsData = blockListValue.SettingsData,
|
||||
Expose = blockListValue.Expose,
|
||||
};
|
||||
|
||||
return singleBlockValue;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Models.Blocks;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_18_0_0.SingleBlockList;
|
||||
|
||||
[Obsolete("Will be removed in V22")] // available in v17, activated in v18 migration needs to work on LTS to LTS 17=>21
|
||||
internal class SingleBlockListRteProcessor : SingleBlockBlockProcessorBase, ITypedSingleBlockListProcessor
|
||||
{
|
||||
public SingleBlockListRteProcessor(SingleBlockListConfigurationCache blockListConfigurationCache)
|
||||
: base(blockListConfigurationCache)
|
||||
{
|
||||
}
|
||||
|
||||
public Type PropertyEditorValueType => typeof(RichTextEditorValue);
|
||||
|
||||
public IEnumerable<string> PropertyEditorAliases =>
|
||||
[
|
||||
"Umbraco.TinyMCE", Constants.PropertyEditors.Aliases.RichText
|
||||
];
|
||||
|
||||
public Func<object?, Func<object?, bool>,Func<BlockListValue,object>, bool> Process => ProcessRichText;
|
||||
|
||||
public bool ProcessRichText(
|
||||
object? value,
|
||||
Func<object?, bool> processNested,
|
||||
Func<BlockListValue, object> processOuterValue)
|
||||
{
|
||||
if (value is not RichTextEditorValue richTextValue)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var hasChanged = false;
|
||||
|
||||
if (richTextValue.Blocks is null)
|
||||
{
|
||||
return hasChanged;
|
||||
}
|
||||
|
||||
foreach (BlockItemData contentData in richTextValue.Blocks.ContentData)
|
||||
{
|
||||
if (ProcessBlockItemDataValues(contentData, processNested, processOuterValue))
|
||||
{
|
||||
hasChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (BlockItemData settingsData in richTextValue.Blocks.SettingsData)
|
||||
{
|
||||
if (ProcessBlockItemDataValues(settingsData, processNested, processOuterValue))
|
||||
{
|
||||
hasChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
return hasChanged;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user