Migrate data type configurations from V7.0+ (#15733)
* Migrate data type configurations from V7.0+ * Update listview defaults to match the install ones
This commit is contained in:
@@ -1,8 +1,11 @@
|
||||
using NPoco;
|
||||
using System.Text.Json.Nodes;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NPoco;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.PropertyEditors;
|
||||
using Umbraco.Cms.Core.Serialization;
|
||||
using Umbraco.Cms.Infrastructure.Migrations.PostMigrations;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Infrastructure.Persistence;
|
||||
using Umbraco.Cms.Infrastructure.Persistence.Dtos;
|
||||
using Umbraco.Cms.Infrastructure.Serialization;
|
||||
@@ -11,63 +14,442 @@ using PropertyEditorAliases = Umbraco.Cms.Core.Constants.PropertyEditors.Aliases
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_14_0_0;
|
||||
|
||||
// TODO: this migration is a work in progress; it will be amended for a while, thus it MUST be able to re-run several times without failing miserably
|
||||
public class MigrateDataTypeConfigurations : MigrationBase
|
||||
{
|
||||
public MigrateDataTypeConfigurations(IMigrationContext context)
|
||||
private readonly IContentTypeService _contentTypeService;
|
||||
private readonly IMediaTypeService _mediaTypeService;
|
||||
private readonly IMemberTypeService _memberTypeService;
|
||||
private readonly ILogger<MigrateDataTypeConfigurations> _logger;
|
||||
private readonly IConfigurationEditorJsonSerializer _configurationEditorJsonSerializer;
|
||||
|
||||
public MigrateDataTypeConfigurations(
|
||||
IMigrationContext context,
|
||||
IContentTypeService contentTypeService,
|
||||
IMediaTypeService mediaTypeService,
|
||||
IMemberTypeService memberTypeService,
|
||||
ILogger<MigrateDataTypeConfigurations> logger)
|
||||
: base(context)
|
||||
{
|
||||
_contentTypeService = contentTypeService;
|
||||
_mediaTypeService = mediaTypeService;
|
||||
_logger = logger;
|
||||
_memberTypeService = memberTypeService;
|
||||
|
||||
// TODO: inject this once we're rid of the Newtonsoft.Json based config serializer
|
||||
_configurationEditorJsonSerializer = new SystemTextConfigurationEditorJsonSerializer();
|
||||
}
|
||||
|
||||
// TODO: this migration is a work in progress; it will be amended for a while, thus it MUST be able to re-run several times without failing miserably
|
||||
protected override void Migrate()
|
||||
{
|
||||
// this really should be injected, but until we can get rid of the Newtonsoft.Json based config serializer, we can't rely on
|
||||
// injection during installs, so we will have to settle for new'ing it up explicitly here (at least for now).
|
||||
IConfigurationEditorJsonSerializer serializer = new SystemTextConfigurationEditorJsonSerializer();
|
||||
IContentType[] allContentTypes = _contentTypeService.GetAll().ToArray();
|
||||
IMediaType[] allMediaTypes = _mediaTypeService.GetAll().ToArray();
|
||||
IMemberType[] allMemberTypes = _memberTypeService.GetAll().ToArray();
|
||||
|
||||
Sql<ISqlContext> sql = Sql()
|
||||
.Select<DataTypeDto>()
|
||||
.AndSelect<NodeDto>()
|
||||
.From<DataTypeDto>()
|
||||
.InnerJoin<NodeDto>()
|
||||
.On<DataTypeDto, NodeDto>(left => left.NodeId, right => right.NodeId)
|
||||
.Where<DataTypeDto>(x => x.EditorAlias.Contains("Umbraco."));
|
||||
|
||||
List<DataTypeDto> dataTypeDtos = Database.Fetch<DataTypeDto>(sql);
|
||||
|
||||
foreach (DataTypeDto dataTypeDto in dataTypeDtos)
|
||||
{
|
||||
Dictionary<string, object> configurationData = dataTypeDto.Configuration.IsNullOrWhiteSpace()
|
||||
? new Dictionary<string, object>()
|
||||
: serializer.Deserialize<Dictionary<string, object>>(dataTypeDto.Configuration) ?? new Dictionary<string, object>();
|
||||
|
||||
// fix config key casing - should always be camelCase, but some have been saved as PascalCase over the years
|
||||
var badlyCasedKeys = configurationData.Keys.Where(key => key.ToFirstLowerInvariant() != key).ToArray();
|
||||
var updated = badlyCasedKeys.Any();
|
||||
foreach (var incorrectKey in badlyCasedKeys)
|
||||
var updated = false;
|
||||
Dictionary<string, object>? configurationData = null;
|
||||
try
|
||||
{
|
||||
configurationData[incorrectKey.ToFirstLowerInvariant()] = configurationData[incorrectKey];
|
||||
configurationData.Remove(incorrectKey);
|
||||
configurationData = dataTypeDto.Configuration.IsNullOrWhiteSpace()
|
||||
? new Dictionary<string, object>()
|
||||
: _configurationEditorJsonSerializer
|
||||
.Deserialize<Dictionary<string, object?>>(dataTypeDto.Configuration)?
|
||||
.Where(item => item.Value is not null)
|
||||
.ToDictionary(item => item.Key, item => item.Value!)
|
||||
?? new Dictionary<string, object>();
|
||||
|
||||
// do not attempt to migrate the configuration data twice (it *will* fail for some editors)
|
||||
if (configurationData.ContainsKey("umbMigrationV14"))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// fix config key casing - should always be camelCase, but some have been saved as PascalCase over the years
|
||||
var badlyCasedKeys = configurationData.Keys.Where(key => key.ToFirstLowerInvariant() != key).ToArray();
|
||||
updated = badlyCasedKeys.Any();
|
||||
foreach (var incorrectKey in badlyCasedKeys)
|
||||
{
|
||||
configurationData[incorrectKey.ToFirstLowerInvariant()] = configurationData[incorrectKey];
|
||||
configurationData.Remove(incorrectKey);
|
||||
}
|
||||
|
||||
// handle special cases, i.e. missing configs (list view), weirdly serialized configs (color picker), min/max for multiple text strings, etc. etc.
|
||||
updated |= dataTypeDto.EditorAlias switch
|
||||
{
|
||||
PropertyEditorAliases.Boolean => HandleBoolean(ref configurationData),
|
||||
PropertyEditorAliases.ColorPicker => HandleColorPicker(ref configurationData),
|
||||
PropertyEditorAliases.ContentPicker => HandleContentPicker(ref configurationData),
|
||||
PropertyEditorAliases.DateTime => HandleDateTime(ref configurationData),
|
||||
PropertyEditorAliases.DropDownListFlexible => HandleDropDown(ref configurationData),
|
||||
PropertyEditorAliases.EmailAddress => HandleEmailAddress(ref configurationData),
|
||||
PropertyEditorAliases.Label => HandleLabel(ref configurationData),
|
||||
PropertyEditorAliases.ListView => HandleListView(ref configurationData),
|
||||
PropertyEditorAliases.MediaPicker3 => HandleMediaPicker(ref configurationData, allMediaTypes),
|
||||
PropertyEditorAliases.MultiNodeTreePicker => HandleMultiNodeTreePicker(ref configurationData, allContentTypes, allMediaTypes, allMemberTypes),
|
||||
PropertyEditorAliases.MultiUrlPicker => HandleMultiUrlPicker(ref configurationData),
|
||||
PropertyEditorAliases.MultipleTextstring => HandleMultipleTextstring(ref configurationData),
|
||||
PropertyEditorAliases.RadioButtonList => HandleRadioButton(ref configurationData),
|
||||
PropertyEditorAliases.RichText => HandleRichText(ref configurationData),
|
||||
PropertyEditorAliases.TextBox => HandleTextBoxAndTextArea(ref configurationData),
|
||||
PropertyEditorAliases.TextArea => HandleTextBoxAndTextArea(ref configurationData),
|
||||
PropertyEditorAliases.TinyMce => HandleRichText(ref configurationData),
|
||||
PropertyEditorAliases.UploadField => HandleUploadField(ref configurationData),
|
||||
_ => false
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Migration failed for data type: {dataTypeName} (id: {dataTypeId}, editor alias: {dataTypeEditorAlias})", dataTypeDto.NodeDto?.Text, dataTypeDto.NodeId, dataTypeDto.EditorAlias);
|
||||
}
|
||||
|
||||
// handle special cases, i.e. missing configs (list view), weirdly serialized configs (color picker), min/max for multiple text strings, etc. etc.
|
||||
updated |= dataTypeDto.EditorAlias switch
|
||||
if (updated && configurationData is not null)
|
||||
{
|
||||
PropertyEditorAliases.MultipleTextstring => HandleMultipleTextstring(ref configurationData),
|
||||
PropertyEditorAliases.Label => HandleLabel(ref configurationData),
|
||||
PropertyEditorAliases.TextArea => HandleTextBoxAndTextArea(ref configurationData),
|
||||
PropertyEditorAliases.TextBox => HandleTextBoxAndTextArea(ref configurationData),
|
||||
// TODO: decide on value formats for applicable configs and re-format said configs here (i.e. color picker)
|
||||
// TODO: append/enrich any missing configs here (i.e. list views are likely missing one or more config values)
|
||||
_ => false
|
||||
};
|
||||
|
||||
if (updated)
|
||||
{
|
||||
dataTypeDto.Configuration = serializer.Serialize(configurationData);
|
||||
// tag the configuration data as migrated, so we don't attempt to migrate it twice
|
||||
configurationData["umbMigrationV14"] = DateTimeOffset.UtcNow;
|
||||
dataTypeDto.Configuration = _configurationEditorJsonSerializer.Serialize(configurationData);
|
||||
Database.Update(dataTypeDto);
|
||||
RebuildCache = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// translate "default value" to a proper boolean value
|
||||
private bool HandleBoolean(ref Dictionary<string, object> configurationData)
|
||||
=> ReplaceIntegerStringWithBoolean(ref configurationData, "default");
|
||||
|
||||
// translate "allowed colors" configuration from multiple old formats
|
||||
private bool HandleColorPicker(ref Dictionary<string, object> configurationData)
|
||||
{
|
||||
if (configurationData.Any() is false)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// convert "useLabel" from 0/1 to false/true
|
||||
var changed = false;
|
||||
var useLabel = ConfigurationValue(configurationData, "useLabel");
|
||||
if (useLabel is not null && int.TryParse(useLabel, out var intValue))
|
||||
{
|
||||
configurationData["useLabel"] = intValue == 1;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (configurationData.All(item => item.Value is string { Length: 3 or 6 }))
|
||||
{
|
||||
// V7.0 format:
|
||||
// {
|
||||
// "0": "FFF",
|
||||
// "1": "F00",
|
||||
// "2": "0F0",
|
||||
// ...
|
||||
// }
|
||||
|
||||
configurationData["items"] = configurationData.Select(item =>
|
||||
{
|
||||
var hex = ((string)item.Value).ToLowerInvariant();
|
||||
if (hex.Length is 3)
|
||||
{
|
||||
hex = string.Join(string.Empty, hex.Select(c => $"{c}{c}"));
|
||||
}
|
||||
|
||||
return new ColorPickerItem
|
||||
{
|
||||
Label = hex,
|
||||
Value = hex
|
||||
};
|
||||
}).ToArray();
|
||||
|
||||
configurationData.RemoveAll(item => item.Key is not "items");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (configurationData.ContainsKey("items") is false && configurationData.Any(item => int.TryParse(item.Key, out _) && item.Value is JsonObject))
|
||||
{
|
||||
// V7.15 format:
|
||||
// {
|
||||
// "useLabel": "0",
|
||||
// "0": {
|
||||
// "value": "000000",
|
||||
// "label": "Black",
|
||||
// "sortOrder": 0
|
||||
// },
|
||||
// "1": {
|
||||
// "value": "ff0000",
|
||||
// "label": "Red",
|
||||
// "sortOrder": 1
|
||||
// },
|
||||
// ...
|
||||
// }
|
||||
|
||||
configurationData["items"] = configurationData
|
||||
.Where(item => int.TryParse(item.Key, out _) && item.Value is JsonObject)
|
||||
.Select(item =>
|
||||
{
|
||||
// we'll wrap this in an explicit try/catch here because we really can't be entirely sure of the validity of
|
||||
// each item in the data - but we want to port over as many as we can
|
||||
try
|
||||
{
|
||||
return _configurationEditorJsonSerializer.Deserialize<ColorPickerItem>(((JsonObject)item.Value)
|
||||
.ToJsonString());
|
||||
}
|
||||
catch
|
||||
{
|
||||
// silently ignore
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.WhereNotNull()
|
||||
.ToArray();
|
||||
|
||||
configurationData.RemoveAll(item => int.TryParse(item.Key, out _));
|
||||
return true;
|
||||
}
|
||||
|
||||
// V8.0+ format (yes, it became a "value list" where the values are serialized objects of "value" and "label")
|
||||
// {
|
||||
// "useLabel": true,
|
||||
// "items": [
|
||||
// {
|
||||
// "id": 1,
|
||||
// "value": "{\"value\":\"000000\",\"label\":\"Black\"}"
|
||||
// },
|
||||
// {
|
||||
// "id": 2,
|
||||
// "value": "{\"value\":\"ff0000\",\"label\":\"Red\"}"
|
||||
// },
|
||||
// ...
|
||||
// ]
|
||||
// }
|
||||
// ... OR potentially (mentioned in the old codebase, but yet to be seen in an actual test database):
|
||||
// {
|
||||
// "useLabel": false,
|
||||
// "items": [
|
||||
// {
|
||||
// "id": 1,
|
||||
// "value": "000000"
|
||||
// },
|
||||
// {
|
||||
// "id": 2,
|
||||
// "value": "ff0000"
|
||||
// },
|
||||
// ...
|
||||
// ]
|
||||
// }
|
||||
|
||||
var items = ConfigurationValue(configurationData, "items", true);
|
||||
if (items is null)
|
||||
{
|
||||
return changed;
|
||||
}
|
||||
|
||||
ValueListItem[]? valueListItems = _configurationEditorJsonSerializer.Deserialize<ValueListItem[]>(items);
|
||||
if (valueListItems is null)
|
||||
{
|
||||
// this exception is caught by the calling method, which logs the error and continues with the rest of the data type migration
|
||||
throw new InvalidOperationException("The color picker \"items\" configuration could not be parsed.");
|
||||
}
|
||||
|
||||
ColorPickerItem[] colorPickerValues = valueListItems
|
||||
.Select(item => item.Value.DetectIsJson()
|
||||
? _configurationEditorJsonSerializer.Deserialize<ColorPickerItem>(item.Value)
|
||||
: new ColorPickerItem { Label = item.Value, Value = item.Value })
|
||||
.WhereNotNull()
|
||||
.ToArray();
|
||||
|
||||
configurationData["items"] = colorPickerValues;
|
||||
return true;
|
||||
}
|
||||
|
||||
// translate start node from UDI
|
||||
private bool HandleContentPicker(ref Dictionary<string, object> configurationData)
|
||||
=> ReplaceUdiWithKey(ref configurationData, "startNodeId");
|
||||
|
||||
// translate "offsetTime" and "defaultEmpty" to a property boolean value
|
||||
private bool HandleDateTime(ref Dictionary<string, object> configurationData)
|
||||
=> ReplaceIntegerStringWithBoolean(ref configurationData, "offsetTime")
|
||||
| ReplaceIntegerStringWithBoolean(ref configurationData, "defaultEmpty");
|
||||
|
||||
// translate "selectable items" from old "value list" format to string array
|
||||
private bool HandleDropDown(ref Dictionary<string, object> configurationData)
|
||||
=> ReplaceValueListArrayWithStringArray(ref configurationData, "items");
|
||||
|
||||
// remove old (obsolete) "isRequired" configuration
|
||||
private bool HandleEmailAddress(ref Dictionary<string, object> configurationData)
|
||||
=> configurationData.Remove("isRequired");
|
||||
|
||||
// enforce default "umbracoDataValueType" for label (may be empty for old data types)
|
||||
private static bool HandleLabel(ref Dictionary<string, object> configurationData)
|
||||
{
|
||||
if (configurationData.ContainsKey(Constants.PropertyEditors.ConfigurationKeys.DataValueType))
|
||||
{
|
||||
if (configurationData[Constants.PropertyEditors.ConfigurationKeys.DataValueType] is string value && value.IsNullOrWhiteSpace() == false)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
configurationData[Constants.PropertyEditors.ConfigurationKeys.DataValueType] = ValueTypes.String;
|
||||
return true;
|
||||
}
|
||||
|
||||
// ensure that list view configs have all configurations, as some have never been added by means of migration.
|
||||
// also performs a re-formatting of "layouts" and "includeProperties" to a V14 format
|
||||
private bool HandleListView(ref Dictionary<string, object> configurationData)
|
||||
{
|
||||
var layoutsValue = ConfigurationValue(configurationData, "layouts", true);
|
||||
if (layoutsValue is not null)
|
||||
{
|
||||
OldListViewLayout[]? layouts = _configurationEditorJsonSerializer.Deserialize<OldListViewLayout[]>(layoutsValue);
|
||||
if (layouts is null)
|
||||
{
|
||||
// this exception is caught by the calling method, which logs the error and continues with the rest of the data type migration
|
||||
throw new InvalidOperationException("The list view \"layouts\" configuration could not be parsed.");
|
||||
}
|
||||
|
||||
configurationData["layouts"] = layouts.Select(layout => new NewListViewLayout
|
||||
{
|
||||
Name = layout.Name,
|
||||
IsSystem = layout.IsSystem == 1,
|
||||
Selected = layout.Selected,
|
||||
Icon = layout.Icon,
|
||||
// TODO: this will be changed - likely into "Component", with some default translation of core layout paths (pending LKE)
|
||||
Path = layout.Path
|
||||
}).ToArray();
|
||||
}
|
||||
else
|
||||
{
|
||||
configurationData["layouts"] = new[]
|
||||
{
|
||||
new NewListViewLayout
|
||||
{
|
||||
Name = "List",
|
||||
// TODO: this will be changed - figure out the defaults (pending LKE)
|
||||
Path = "views/propertyeditors/listview/layouts/list/list.html",
|
||||
Icon = "icon-list",
|
||||
IsSystem = true,
|
||||
Selected = true
|
||||
},
|
||||
new NewListViewLayout
|
||||
{
|
||||
Name = "Grid",
|
||||
// TODO: this will be changed - figure out the defaults (pending LKE)
|
||||
Path = "views/propertyeditors/listview/layouts/grid/grid.html",
|
||||
Icon = "icon-thumbnails-small",
|
||||
IsSystem = true,
|
||||
Selected = true
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
var includePropertiesValue = ConfigurationValue(configurationData, "includeProperties", true);
|
||||
if (includePropertiesValue is not null)
|
||||
{
|
||||
OldListViewProperty[]? properties = _configurationEditorJsonSerializer.Deserialize<OldListViewProperty[]>(includePropertiesValue);
|
||||
if (properties is null)
|
||||
{
|
||||
// this exception is caught by the calling method, which logs the error and continues with the rest of the data type migration
|
||||
throw new InvalidOperationException("The list view \"includePropertiesValue\" configuration could not be parsed.");
|
||||
}
|
||||
|
||||
configurationData["includeProperties"] = properties.Select(property => new NewListViewProperty
|
||||
{
|
||||
// the "owner" property alias is "creator" from V14
|
||||
Alias = property.Alias is "owner" ? "creator" : property.Alias,
|
||||
Header = property.Header ?? property.Alias switch
|
||||
{
|
||||
"email" => "Email",
|
||||
"username" => "User name",
|
||||
"createDate" => "Created at",
|
||||
"published" => "Published at",
|
||||
"updater" => "Edited by",
|
||||
_ => string.Empty
|
||||
},
|
||||
NameTemplate = property.NameTemplate,
|
||||
IsSystem = property.IsSystem == 1
|
||||
}).ToArray();
|
||||
}
|
||||
else
|
||||
{
|
||||
configurationData["includeProperties"] = new[]
|
||||
{
|
||||
new NewListViewProperty { Alias = "sortOrder", Header = "Sort order", IsSystem = true },
|
||||
new NewListViewProperty { Alias = "updateDate", Header = "Last edited", IsSystem = true },
|
||||
new NewListViewProperty { Alias = "creator", Header = "Created by", IsSystem = true }
|
||||
};
|
||||
}
|
||||
|
||||
configurationData.TryAdd("bulkActionPermissions", new ListViewDefaults.BulkActionPermissions());
|
||||
configurationData.TryAdd("icon", ListViewDefaults.Icon);
|
||||
configurationData.TryAdd("showContentFirst", ListViewDefaults.ShowContentFirst);
|
||||
configurationData.TryAdd("useInfiniteEditor", ListViewDefaults.UseInfiniteEditor);
|
||||
configurationData.TryAdd("pageSize", ListViewDefaults.PageSize);
|
||||
configurationData.TryAdd("orderDirection", ListViewDefaults.OrderDirection);
|
||||
configurationData.TryAdd("orderBy", ListViewDefaults.OrderBy);
|
||||
|
||||
// with the reformatting of "layouts" and "includeProperties", the list view configs will always have changed
|
||||
return true;
|
||||
}
|
||||
|
||||
// translate start node from UDI to key and replace docType aliases with their keys
|
||||
private bool HandleMediaPicker(ref Dictionary<string, object> configurationData, IMediaType[] allMediaTypes)
|
||||
=> ReplaceUdiWithKey(ref configurationData, "startNodeId")
|
||||
| ReplaceContentTypeAliasesWithKeys(ref configurationData, "filter", allMediaTypes);
|
||||
|
||||
// translate start node from UDI and replace docType aliases with their keys
|
||||
private bool HandleMultiNodeTreePicker(ref Dictionary<string, object> configurationData, IContentType[] allContentTypes, IMediaType[] allMediaTypes, IMemberType[] allMemberTypes)
|
||||
{
|
||||
var changed = false;
|
||||
|
||||
var startNodeValue = ConfigurationValue(configurationData, "startNode", true);
|
||||
OldTreeSource? treeSource = startNodeValue is not null
|
||||
? _configurationEditorJsonSerializer.Deserialize<OldTreeSource>(startNodeValue)
|
||||
: null;
|
||||
if (treeSource is not null)
|
||||
{
|
||||
configurationData["startNode"] = new NewTreeSource
|
||||
{
|
||||
Type = treeSource.Type,
|
||||
Id = treeSource.Id?.Guid,
|
||||
DynamicRoot = treeSource.DynamicRoot
|
||||
};
|
||||
changed = true;
|
||||
}
|
||||
|
||||
changed |= ReplaceContentTypeAliasesWithKeys(
|
||||
ref configurationData,
|
||||
"filter",
|
||||
treeSource?.Type.ToLowerInvariant() switch
|
||||
{
|
||||
"media" => allMediaTypes,
|
||||
"member" => allMemberTypes,
|
||||
_ => allContentTypes
|
||||
});
|
||||
|
||||
// old, server-side calculated property that should never be part of config
|
||||
changed |= configurationData.Remove("multiPicker");
|
||||
|
||||
changed |= ReplaceIntegerStringWithBoolean(ref configurationData, "ignoreUserStartNodes")
|
||||
| ReplaceIntegerStringWithBoolean(ref configurationData, "showOpenButton");
|
||||
|
||||
return changed;
|
||||
}
|
||||
|
||||
// replace "ignoreUserStartNodes" with a proper boolean value
|
||||
private bool HandleMultiUrlPicker(ref Dictionary<string, object> configurationData)
|
||||
=> ReplaceIntegerStringWithBoolean(ref configurationData, "ignoreUserStartNodes");
|
||||
|
||||
// convert the stored keys "minimum" and "maximum" to the expected keys "min" and "max for multiple textstrings
|
||||
private static bool HandleMultipleTextstring(ref Dictionary<string, object> configurationData)
|
||||
{
|
||||
@@ -88,18 +470,58 @@ public class MigrateDataTypeConfigurations : MigrationBase
|
||||
return ReplaceKey("minimum", "min") | ReplaceKey("maximum", "max");
|
||||
}
|
||||
|
||||
// enforce default "umbracoDataValueType" for label (may be empty for old data types)
|
||||
private static bool HandleLabel(ref Dictionary<string, object> configurationData)
|
||||
// translate "selectable items" from old "value list" format to string array
|
||||
private bool HandleRadioButton(ref Dictionary<string, object> configurationData)
|
||||
=> ReplaceValueListArrayWithStringArray(ref configurationData, "items");
|
||||
|
||||
|
||||
// translate media parent UDI and split "editor" value into separate configuration data values
|
||||
private bool HandleRichText(ref Dictionary<string, object> configurationData)
|
||||
{
|
||||
if (configurationData.ContainsKey(Constants.PropertyEditors.ConfigurationKeys.DataValueType))
|
||||
var changed = ReplaceUdiWithKey(ref configurationData, "mediaParentId");
|
||||
|
||||
var editor = ConfigurationValue(configurationData, "editor", true);
|
||||
if (editor is null)
|
||||
{
|
||||
if (configurationData[Constants.PropertyEditors.ConfigurationKeys.DataValueType] is string value && value.IsNullOrWhiteSpace() == false)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
configurationData[Constants.PropertyEditors.ConfigurationKeys.DataValueType] = ValueTypes.String;
|
||||
RichTextEditorConfiguration? richTextEditorConfiguration = _configurationEditorJsonSerializer.Deserialize<RichTextEditorConfiguration>(editor);
|
||||
if (richTextEditorConfiguration is null)
|
||||
{
|
||||
// this exception is caught by the calling method, which logs the error and continues with the rest of the data type migration
|
||||
throw new InvalidOperationException("The rich text \"editor\" configuration could not be parsed.");
|
||||
}
|
||||
|
||||
if (richTextEditorConfiguration.Toolbar is not null && richTextEditorConfiguration.Toolbar.Any())
|
||||
{
|
||||
configurationData["toolbar"] = richTextEditorConfiguration.Toolbar;
|
||||
}
|
||||
|
||||
if (richTextEditorConfiguration.Stylesheets is not null && richTextEditorConfiguration.Stylesheets.Any())
|
||||
{
|
||||
configurationData["stylesheets"] = richTextEditorConfiguration.Stylesheets;
|
||||
}
|
||||
|
||||
if (richTextEditorConfiguration.Mode.IsNullOrWhiteSpace() is false)
|
||||
{
|
||||
configurationData["mode"] = richTextEditorConfiguration.Mode.ToFirstUpperInvariant();
|
||||
}
|
||||
|
||||
if (richTextEditorConfiguration.MaxImageSize is not null)
|
||||
{
|
||||
configurationData["maxImageSize"] = richTextEditorConfiguration.MaxImageSize;
|
||||
}
|
||||
|
||||
if (richTextEditorConfiguration.Dimensions is not null)
|
||||
{
|
||||
configurationData["dimensions"] = richTextEditorConfiguration.Dimensions;
|
||||
}
|
||||
|
||||
configurationData.Remove("editor");
|
||||
|
||||
ReplaceIntegerStringWithBoolean(ref configurationData, "ignoreUserStartNodes");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -120,4 +542,223 @@ public class MigrateDataTypeConfigurations : MigrationBase
|
||||
|
||||
return ReplaceStringWithIntValue("maxChars") | ReplaceStringWithIntValue("rows");
|
||||
}
|
||||
|
||||
// translate "allowed file extensions" from old "value list" format to string array
|
||||
private bool HandleUploadField(ref Dictionary<string, object> configurationData)
|
||||
=> ReplaceValueListArrayWithStringArray(ref configurationData, "fileExtensions");
|
||||
|
||||
private string? ConfigurationValue(IReadOnlyDictionary<string, object> configurationData, string key, bool mustBeJson = false)
|
||||
{
|
||||
if (configurationData.TryGetValue(key, out var configurationValue) is false)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var value = configurationValue.ToString();
|
||||
if (value.IsNullOrWhiteSpace() || (mustBeJson && value.DetectIsJson() is false))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
private bool ReplaceUdiWithKey(ref Dictionary<string, object> configurationData, string key)
|
||||
{
|
||||
var configurationValue = ConfigurationValue(configurationData, key);
|
||||
if (configurationValue is null || UdiParser.TryParse(configurationValue, out GuidUdi? udi) is false)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
configurationData[key] = udi.Guid;
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool ReplaceContentTypeAliasesWithKeys(ref Dictionary<string, object> configurationData, string key, IEnumerable<IContentTypeBase> allContentTypes)
|
||||
{
|
||||
var value = ConfigurationValue(configurationData, key);
|
||||
if (value is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var aliases = value.Split(Constants.CharArrays.Comma).ToArray();
|
||||
Guid[] keys = aliases
|
||||
.Select(alias => allContentTypes.FirstOrDefault(c => c.Alias.InvariantEquals(alias))?.Key)
|
||||
.Where(contentTypeKey => contentTypeKey.HasValue)
|
||||
.Select(contentTypeKey => contentTypeKey!.Value)
|
||||
.ToArray();
|
||||
|
||||
configurationData[key] = string.Join(",", keys);
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool ReplaceValueListArrayWithStringArray(ref Dictionary<string, object> configurationData, string key)
|
||||
{
|
||||
var items = ConfigurationValue(configurationData, key, true);
|
||||
if (items is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
ValueListItem[]? valueListItems = _configurationEditorJsonSerializer.Deserialize<ValueListItem[]>(items);
|
||||
if (valueListItems is null)
|
||||
{
|
||||
// this exception is caught by the calling method, which logs the error and continues with the rest of the data type migration
|
||||
throw new InvalidOperationException($"The configuration key \"{key}\" configuration could not be parsed as value list items.");
|
||||
}
|
||||
|
||||
configurationData[key] = valueListItems.Select(item => item.Value).ToArray();
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool ReplaceIntegerStringWithBoolean(ref Dictionary<string, object> configurationData, string key)
|
||||
{
|
||||
var value = ConfigurationValue(configurationData, key);
|
||||
if (value.IsNullOrWhiteSpace())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (value)
|
||||
{
|
||||
case "0":
|
||||
configurationData[key] = false;
|
||||
return true;
|
||||
case "1":
|
||||
configurationData[key] = true;
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private class RichTextEditorConfiguration
|
||||
{
|
||||
public string[]? Toolbar { get; set; }
|
||||
|
||||
public string[]? Stylesheets { get; set; }
|
||||
|
||||
public int? MaxImageSize { get; set; }
|
||||
|
||||
public string? Mode { get; set; }
|
||||
|
||||
public EditorDimensions? Dimensions { get; set; }
|
||||
|
||||
public class EditorDimensions
|
||||
{
|
||||
public int? Width { get; set; }
|
||||
|
||||
public int? Height { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
private class ValueListItem
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
public required string Value { get; set; }
|
||||
}
|
||||
|
||||
private class ColorPickerItem
|
||||
{
|
||||
public required string Value { get; set; }
|
||||
|
||||
public required string Label { get; set; }
|
||||
}
|
||||
|
||||
private class OldTreeSource
|
||||
{
|
||||
public required string Type { get; set; }
|
||||
|
||||
public GuidUdi? Id { get; set; }
|
||||
|
||||
public object? DynamicRoot { get; set; }
|
||||
}
|
||||
|
||||
private class NewTreeSource
|
||||
{
|
||||
public required string Type { get; set; }
|
||||
|
||||
public Guid? Id { get; set; }
|
||||
|
||||
public object? DynamicRoot { get; set; }
|
||||
}
|
||||
|
||||
private class ListViewDefaults
|
||||
{
|
||||
public class BulkActionPermissions
|
||||
{
|
||||
public bool AllowBulkPublish { get; } = true;
|
||||
|
||||
public bool AllowBulkUnpublish { get; } = true;
|
||||
|
||||
public bool AllowBulkCopy { get; } = true;
|
||||
|
||||
public bool AllowBulkMove { get; } = true;
|
||||
|
||||
public bool AllowBulkDelete { get; } = true;
|
||||
}
|
||||
|
||||
public const string Icon = "icon-badge color-black";
|
||||
|
||||
public const bool ShowContentFirst = false;
|
||||
|
||||
public const bool UseInfiniteEditor = false;
|
||||
|
||||
public const string OrderBy = "updateDate";
|
||||
|
||||
public const string OrderDirection = "desc";
|
||||
|
||||
public const int PageSize = 10;
|
||||
}
|
||||
|
||||
private class OldListViewLayout
|
||||
{
|
||||
public string? Name { get; set; }
|
||||
|
||||
public string? Path { get; set; }
|
||||
|
||||
public string? Icon { get; set; }
|
||||
|
||||
public int IsSystem { get; set; }
|
||||
|
||||
public bool Selected { get; set; }
|
||||
}
|
||||
|
||||
private class NewListViewLayout
|
||||
{
|
||||
public string? Name { get; set; }
|
||||
|
||||
public string? Path { get; set; }
|
||||
|
||||
public string? Icon { get; set; }
|
||||
|
||||
public bool IsSystem { get; set; }
|
||||
|
||||
public bool Selected { get; set; }
|
||||
}
|
||||
|
||||
private class OldListViewProperty
|
||||
{
|
||||
public required string Alias { get; set; }
|
||||
|
||||
public string? Header { get; set; }
|
||||
|
||||
public string? NameTemplate { get; set; }
|
||||
|
||||
public required int IsSystem { get; set; }
|
||||
}
|
||||
|
||||
private class NewListViewProperty
|
||||
{
|
||||
public required string Alias { get; set; }
|
||||
|
||||
public required string Header { get; set; }
|
||||
|
||||
public string? NameTemplate { get; set; }
|
||||
|
||||
public required bool IsSystem { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user