Merge pull request #8382 from umbraco/v8/feature/block-list-data-structure-v3
V8/feature/block list data structure v3
This commit is contained in:
@@ -1,66 +1,45 @@
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Umbraco.Core.Serialization;
|
||||
|
||||
namespace Umbraco.Core.Models.Blocks
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Converted block data from json
|
||||
/// Convertable block data from json
|
||||
/// </summary>
|
||||
public class BlockEditorData
|
||||
{
|
||||
private readonly string _propertyEditorAlias;
|
||||
|
||||
public static BlockEditorData Empty { get; } = new BlockEditorData();
|
||||
|
||||
private BlockEditorData()
|
||||
{
|
||||
BlockValue = new BlockValue();
|
||||
}
|
||||
|
||||
public BlockEditorData(JToken layout, IReadOnlyList<Udi> layoutBlockReferences, IReadOnlyList<BlockItemData> blocks)
|
||||
public BlockEditorData(string propertyEditorAlias,
|
||||
IEnumerable<ContentAndSettingsReference> references,
|
||||
BlockValue blockValue)
|
||||
{
|
||||
Layout = layout ?? throw new ArgumentNullException(nameof(layout));
|
||||
LayoutBlockReferences = layoutBlockReferences ?? throw new ArgumentNullException(nameof(layoutBlockReferences));
|
||||
Blocks = blocks ?? throw new ArgumentNullException(nameof(blocks));
|
||||
}
|
||||
|
||||
public JToken Layout { get; }
|
||||
public IReadOnlyList<Udi> LayoutBlockReferences { get; } = new List<Udi>();
|
||||
public IReadOnlyList<BlockItemData> Blocks { get; } = new List<BlockItemData>();
|
||||
|
||||
internal class BlockValue
|
||||
{
|
||||
[JsonProperty("layout")]
|
||||
public IDictionary<string, JToken> Layout { get; set; }
|
||||
|
||||
[JsonProperty("data")]
|
||||
public IEnumerable<BlockItemData> Data { get; set; }
|
||||
if (string.IsNullOrWhiteSpace(propertyEditorAlias))
|
||||
throw new ArgumentException($"'{nameof(propertyEditorAlias)}' cannot be null or whitespace", nameof(propertyEditorAlias));
|
||||
_propertyEditorAlias = propertyEditorAlias;
|
||||
BlockValue = blockValue ?? throw new ArgumentNullException(nameof(blockValue));
|
||||
References = references != null ? new List<ContentAndSettingsReference>(references) : throw new ArgumentNullException(nameof(references));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a single block's data in raw form
|
||||
/// Returns the layout for this specific property editor
|
||||
/// </summary>
|
||||
public class BlockItemData
|
||||
{
|
||||
[JsonProperty("contentTypeKey")]
|
||||
public Guid ContentTypeKey { get; set; }
|
||||
public JToken Layout => BlockValue.Layout.TryGetValue(_propertyEditorAlias, out var layout) ? layout : null;
|
||||
|
||||
[JsonProperty("udi")]
|
||||
[JsonConverter(typeof(UdiJsonConverter))]
|
||||
public Udi Udi { get; set; }
|
||||
/// <summary>
|
||||
/// Returns the reference to the original BlockValue
|
||||
/// </summary>
|
||||
public BlockValue BlockValue { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The remaining properties will be serialized to a dictionary
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The JsonExtensionDataAttribute is used to put the non-typed properties into a bucket
|
||||
/// http://www.newtonsoft.com/json/help/html/DeserializeExtensionData.htm
|
||||
/// NestedContent serializes to string, int, whatever eg
|
||||
/// "stringValue":"Some String","numericValue":125,"otherNumeric":null
|
||||
/// </remarks>
|
||||
[JsonExtensionData]
|
||||
public Dictionary<string, object> RawPropertyValues { get; set; } = new Dictionary<string, object>();
|
||||
}
|
||||
public List<ContentAndSettingsReference> References { get; } = new List<ContentAndSettingsReference>();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System.Linq;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using static Umbraco.Core.Models.Blocks.BlockEditorData;
|
||||
|
||||
namespace Umbraco.Core.Models.Blocks
|
||||
{
|
||||
@@ -20,19 +18,18 @@ namespace Umbraco.Core.Models.Blocks
|
||||
_propertyEditorAlias = propertyEditorAlias;
|
||||
}
|
||||
|
||||
public BlockEditorData Convert(string json)
|
||||
public BlockEditorData Deserialize(string json)
|
||||
{
|
||||
var value = JsonConvert.DeserializeObject<BlockValue>(json);
|
||||
|
||||
if (value.Layout == null)
|
||||
return BlockEditorData.Empty;
|
||||
|
||||
if (!value.Layout.TryGetValue(_propertyEditorAlias, out var layout))
|
||||
return BlockEditorData.Empty;
|
||||
var references = value.Layout.TryGetValue(_propertyEditorAlias, out var layout)
|
||||
? GetBlockReferences(layout)
|
||||
: Enumerable.Empty<ContentAndSettingsReference>();
|
||||
|
||||
var references = GetBlockReferences(layout);
|
||||
var blocks = value.Data.ToList();
|
||||
return new BlockEditorData(layout, references, blocks);
|
||||
return new BlockEditorData(_propertyEditorAlias, references, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -40,7 +37,7 @@ namespace Umbraco.Core.Models.Blocks
|
||||
/// </summary>
|
||||
/// <param name="jsonLayout"></param>
|
||||
/// <returns></returns>
|
||||
protected abstract IReadOnlyList<Udi> GetBlockReferences(JToken jsonLayout);
|
||||
protected abstract IEnumerable<ContentAndSettingsReference> GetBlockReferences(JToken jsonLayout);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,9 +10,10 @@ namespace Umbraco.Core.Models.Blocks
|
||||
/// </summary>
|
||||
public abstract class BlockEditorModel
|
||||
{
|
||||
protected BlockEditorModel(IEnumerable<IPublishedElement> data)
|
||||
protected BlockEditorModel(IEnumerable<IPublishedElement> contentData, IEnumerable<IPublishedElement> settingsData)
|
||||
{
|
||||
Data = data ?? throw new ArgumentNullException(nameof(data));
|
||||
ContentData = contentData ?? throw new ArgumentNullException(nameof(contentData));
|
||||
SettingsData = settingsData ?? new List<IPublishedContent>();
|
||||
}
|
||||
|
||||
public BlockEditorModel()
|
||||
@@ -21,9 +22,15 @@ namespace Umbraco.Core.Models.Blocks
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// The data items of the Block List editor
|
||||
/// The content data items of the Block List editor
|
||||
/// </summary>
|
||||
[DataMember(Name = "data")]
|
||||
public IEnumerable<IPublishedElement> Data { get; set; }
|
||||
[DataMember(Name = "contentData")]
|
||||
public IEnumerable<IPublishedElement> ContentData { get; set; } = new List<IPublishedContent>();
|
||||
|
||||
/// <summary>
|
||||
/// The settings data items of the Block List editor
|
||||
/// </summary>
|
||||
[DataMember(Name = "settingsData")]
|
||||
public IEnumerable<IPublishedElement> SettingsData { get; set; } = new List<IPublishedContent>();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Umbraco.Core.Serialization;
|
||||
|
||||
namespace Umbraco.Core.Models.Blocks
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a single block's data in raw form
|
||||
/// </summary>
|
||||
public class BlockItemData
|
||||
{
|
||||
[JsonProperty("contentTypeKey")]
|
||||
public Guid ContentTypeKey { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// not serialized, manually set and used during internally
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public string ContentTypeAlias { get; set; }
|
||||
|
||||
[JsonProperty("udi")]
|
||||
[JsonConverter(typeof(UdiJsonConverter))]
|
||||
public Udi Udi { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public Guid Key => Udi != null ? ((GuidUdi)Udi).Guid : throw new InvalidOperationException("No Udi assigned");
|
||||
|
||||
/// <summary>
|
||||
/// The remaining properties will be serialized to a dictionary
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The JsonExtensionDataAttribute is used to put the non-typed properties into a bucket
|
||||
/// http://www.newtonsoft.com/json/help/html/DeserializeExtensionData.htm
|
||||
/// NestedContent serializes to string, int, whatever eg
|
||||
/// "stringValue":"Some String","numericValue":125,"otherNumeric":null
|
||||
/// </remarks>
|
||||
[JsonExtensionData]
|
||||
public Dictionary<string, object> RawPropertyValues { get; set; } = new Dictionary<string, object>();
|
||||
|
||||
/// <summary>
|
||||
/// Used during deserialization to convert the raw property data into data with a property type context
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public IDictionary<string, BlockPropertyValue> PropertyValues { get; set; } = new Dictionary<string, BlockPropertyValue>();
|
||||
|
||||
/// <summary>
|
||||
/// Used during deserialization to populate the property value/property type of a block item content property
|
||||
/// </summary>
|
||||
public class BlockPropertyValue
|
||||
{
|
||||
public object Value { get; set; }
|
||||
public PropertyType PropertyType { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,10 +13,10 @@ namespace Umbraco.Core.Models.Blocks
|
||||
{
|
||||
}
|
||||
|
||||
protected override IReadOnlyList<Udi> GetBlockReferences(JToken jsonLayout)
|
||||
protected override IEnumerable<ContentAndSettingsReference> GetBlockReferences(JToken jsonLayout)
|
||||
{
|
||||
var blockListLayout = jsonLayout.ToObject<IEnumerable<BlockListLayoutItem>>();
|
||||
return blockListLayout.Select(x => x.Udi).ToList();
|
||||
return blockListLayout.Select(x => new ContentAndSettingsReference(x.ContentUdi, x.SettingsUdi)).ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using Newtonsoft.Json;
|
||||
using Umbraco.Core.Serialization;
|
||||
using static Umbraco.Core.Models.Blocks.BlockEditorData;
|
||||
|
||||
namespace Umbraco.Core.Models.Blocks
|
||||
{
|
||||
@@ -9,11 +8,12 @@ namespace Umbraco.Core.Models.Blocks
|
||||
/// </summary>
|
||||
public class BlockListLayoutItem
|
||||
{
|
||||
[JsonProperty("settings")]
|
||||
public BlockItemData Settings { get; set; }
|
||||
|
||||
[JsonProperty("udi")]
|
||||
[JsonProperty("contentUdi", Required = Required.Always)]
|
||||
[JsonConverter(typeof(UdiJsonConverter))]
|
||||
public Udi Udi { get; set; }
|
||||
public Udi ContentUdi { get; set; }
|
||||
|
||||
[JsonProperty("settingsUdi")]
|
||||
[JsonConverter(typeof(UdiJsonConverter))]
|
||||
public Udi SettingsUdi { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,34 +8,44 @@ namespace Umbraco.Core.Models.Blocks
|
||||
/// Represents a layout item for the Block List editor
|
||||
/// </summary>
|
||||
[DataContract(Name = "blockListLayout", Namespace = "")]
|
||||
public class BlockListLayoutReference : IBlockElement<IPublishedElement>
|
||||
public class BlockListLayoutReference : IBlockReference<IPublishedElement>
|
||||
{
|
||||
public BlockListLayoutReference(Udi udi, IPublishedElement data, IPublishedElement settings)
|
||||
public BlockListLayoutReference(Udi contentUdi, IPublishedElement content, Udi settingsUdi, IPublishedElement settings)
|
||||
{
|
||||
Udi = udi ?? throw new ArgumentNullException(nameof(udi));
|
||||
Data = data ?? throw new ArgumentNullException(nameof(data));
|
||||
ContentUdi = contentUdi ?? throw new ArgumentNullException(nameof(contentUdi));
|
||||
Content = content ?? throw new ArgumentNullException(nameof(content));
|
||||
Settings = settings; // can be null
|
||||
SettingsUdi = settingsUdi; // can be null
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The Id of the data item
|
||||
/// The Id of the content data item
|
||||
/// </summary>
|
||||
[DataMember(Name = "udi")]
|
||||
public Udi Udi { get; }
|
||||
[DataMember(Name = "contentUdi")]
|
||||
public Udi ContentUdi { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The settings for the layout item
|
||||
/// The Id of the settings data item
|
||||
/// </summary>
|
||||
[DataMember(Name = "settings")]
|
||||
public IPublishedElement Settings { get; }
|
||||
[DataMember(Name = "settingsUdi")]
|
||||
public Udi SettingsUdi { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The data item referenced
|
||||
/// The content data item referenced
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is ignored from serialization since it is just a reference to the actual data element
|
||||
/// </remarks>
|
||||
[IgnoreDataMember]
|
||||
public IPublishedElement Data { get; }
|
||||
public IPublishedElement Content { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The settings data item referenced
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is ignored from serialization since it is just a reference to the actual data element
|
||||
/// </remarks>
|
||||
[IgnoreDataMember]
|
||||
public IPublishedElement Settings { get; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,8 +10,14 @@ namespace Umbraco.Core.Models.Blocks
|
||||
[DataContract(Name = "blockList", Namespace = "")]
|
||||
public class BlockListModel : BlockEditorModel
|
||||
{
|
||||
public BlockListModel(IEnumerable<IPublishedElement> data, IEnumerable<BlockListLayoutReference> layout)
|
||||
: base(data)
|
||||
public static BlockListModel Empty { get; } = new BlockListModel();
|
||||
|
||||
private BlockListModel()
|
||||
{
|
||||
}
|
||||
|
||||
public BlockListModel(IEnumerable<IPublishedElement> contentData, IEnumerable<IPublishedElement> settingsData, IEnumerable<BlockListLayoutReference> layout)
|
||||
: base(contentData, settingsData)
|
||||
{
|
||||
Layout = layout;
|
||||
}
|
||||
@@ -20,7 +26,7 @@ namespace Umbraco.Core.Models.Blocks
|
||||
/// The layout items of the Block List editor
|
||||
/// </summary>
|
||||
[DataMember(Name = "layout")]
|
||||
public IEnumerable<BlockListLayoutReference> Layout { get; }
|
||||
public IEnumerable<BlockListLayoutReference> Layout { get; } = new List<BlockListLayoutReference>();
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Umbraco.Core.Models.Blocks
|
||||
{
|
||||
public class BlockValue
|
||||
{
|
||||
[JsonProperty("layout")]
|
||||
public IDictionary<string, JToken> Layout { get; set; }
|
||||
|
||||
[JsonProperty("contentData")]
|
||||
public List<BlockItemData> ContentData { get; set; } = new List<BlockItemData>();
|
||||
|
||||
[JsonProperty("settingsData")]
|
||||
public List<BlockItemData> SettingsData { get; set; } = new List<BlockItemData>();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
|
||||
namespace Umbraco.Core.Models.Blocks
|
||||
{
|
||||
public struct ContentAndSettingsReference : IEquatable<ContentAndSettingsReference>
|
||||
{
|
||||
public ContentAndSettingsReference(Udi contentUdi, Udi settingsUdi)
|
||||
{
|
||||
ContentUdi = contentUdi ?? throw new ArgumentNullException(nameof(contentUdi));
|
||||
SettingsUdi = settingsUdi;
|
||||
}
|
||||
|
||||
public Udi ContentUdi { get; }
|
||||
public Udi SettingsUdi { get; }
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
return obj is ContentAndSettingsReference reference && Equals(reference);
|
||||
}
|
||||
|
||||
public bool Equals(ContentAndSettingsReference other)
|
||||
{
|
||||
return EqualityComparer<Udi>.Default.Equals(ContentUdi, other.ContentUdi) &&
|
||||
EqualityComparer<Udi>.Default.Equals(SettingsUdi, other.SettingsUdi);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
var hashCode = 272556606;
|
||||
hashCode = hashCode * -1521134295 + EqualityComparer<Udi>.Default.GetHashCode(ContentUdi);
|
||||
hashCode = hashCode * -1521134295 + EqualityComparer<Udi>.Default.GetHashCode(SettingsUdi);
|
||||
return hashCode;
|
||||
}
|
||||
|
||||
public static bool operator ==(ContentAndSettingsReference left, ContentAndSettingsReference right)
|
||||
{
|
||||
return left.Equals(right);
|
||||
}
|
||||
|
||||
public static bool operator !=(ContentAndSettingsReference left, ContentAndSettingsReference right)
|
||||
{
|
||||
return !(left == right);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
namespace Umbraco.Core.Models.Blocks
|
||||
{
|
||||
|
||||
// TODO: IBlockElement doesn't make sense, this is a reference to an actual element with some settings
|
||||
// and always has to do with the "Layout", should possibly be called IBlockReference or IBlockLayout or IBlockLayoutReference
|
||||
/// <summary>
|
||||
/// Represents a data item for a Block editor implementation
|
||||
/// </summary>
|
||||
/// <typeparam name="TSettings"></typeparam>
|
||||
/// <remarks>
|
||||
/// see: https://github.com/umbraco/rfcs/blob/907f3758cf59a7b6781296a60d57d537b3b60b8c/cms/0011-block-data-structure.md#strongly-typed
|
||||
/// </remarks>
|
||||
public interface IBlockElement<TSettings> : IBlockReference
|
||||
{
|
||||
TSettings Settings { get; }
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,18 @@
|
||||
namespace Umbraco.Core.Models.Blocks
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Represents a data item reference for a Block editor implementation
|
||||
/// </summary>
|
||||
/// <typeparam name="TSettings"></typeparam>
|
||||
/// <remarks>
|
||||
/// see: https://github.com/umbraco/rfcs/blob/907f3758cf59a7b6781296a60d57d537b3b60b8c/cms/0011-block-data-structure.md#strongly-typed
|
||||
/// </remarks>
|
||||
public interface IBlockReference<TSettings> : IBlockReference
|
||||
{
|
||||
TSettings Settings { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a data item reference for a Block Editor implementation
|
||||
/// </summary>
|
||||
@@ -8,6 +21,6 @@
|
||||
/// </remarks>
|
||||
public interface IBlockReference
|
||||
{
|
||||
Udi Udi { get; }
|
||||
Udi ContentUdi { get; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,8 +134,11 @@
|
||||
<Compile Include="Migrations\Upgrade\V_8_0_0\Models\PropertyTypeDto80.cs" />
|
||||
<Compile Include="Models\Blocks\BlockEditorDataConverter.cs" />
|
||||
<Compile Include="Models\Blocks\BlockEditorData.cs" />
|
||||
<Compile Include="Models\Blocks\BlockItemData.cs" />
|
||||
<Compile Include="Models\Blocks\BlockListLayoutItem.cs" />
|
||||
<Compile Include="Models\Blocks\BlockListEditorDataConverter.cs" />
|
||||
<Compile Include="Models\Blocks\BlockValue.cs" />
|
||||
<Compile Include="Models\Blocks\ContentAndSettingsReference.cs" />
|
||||
<Compile Include="Models\Blocks\IBlockReference.cs" />
|
||||
<Compile Include="Models\ContentDataIntegrityReport.cs" />
|
||||
<Compile Include="Models\ContentDataIntegrityReportEntry.cs" />
|
||||
@@ -150,7 +153,6 @@
|
||||
<Compile Include="Models\Blocks\BlockEditorModel.cs" />
|
||||
<Compile Include="Models\Blocks\BlockListLayoutReference.cs" />
|
||||
<Compile Include="Models\Blocks\BlockListModel.cs" />
|
||||
<Compile Include="Models\Blocks\IBlockElement.cs" />
|
||||
<Compile Include="Models\UpgradeResult.cs" />
|
||||
<Compile Include="Persistence\Repositories\Implement\UpgradeCheckRepository.cs" />
|
||||
<Compile Include="Persistence\Repositories\IUpgradeCheckRepository.cs" />
|
||||
|
||||
@@ -18,10 +18,14 @@ namespace Umbraco.Tests.PropertyEditors
|
||||
[TestFixture]
|
||||
public class BlockListPropertyValueConverterTests
|
||||
{
|
||||
private readonly Guid Key1 = Guid.NewGuid();
|
||||
private readonly Guid Key2 = Guid.NewGuid();
|
||||
private readonly string Alias1 = "Test1";
|
||||
private readonly string Alias2 = "Test2";
|
||||
private readonly Guid ContentKey1 = Guid.NewGuid();
|
||||
private readonly Guid ContentKey2 = Guid.NewGuid();
|
||||
private const string ContentAlias1 = "Test1";
|
||||
private const string ContentAlias2 = "Test2";
|
||||
private readonly Guid SettingKey1 = Guid.NewGuid();
|
||||
private readonly Guid SettingKey2 = Guid.NewGuid();
|
||||
private const string SettingAlias1 = "Setting1";
|
||||
private const string SettingAlias2 = "Setting2";
|
||||
|
||||
/// <summary>
|
||||
/// Setup mocks for IPublishedSnapshotAccessor
|
||||
@@ -31,15 +35,25 @@ namespace Umbraco.Tests.PropertyEditors
|
||||
{
|
||||
var test1ContentType = Mock.Of<IPublishedContentType2>(x =>
|
||||
x.IsElement == true
|
||||
&& x.Key == Key1
|
||||
&& x.Alias == Alias1);
|
||||
&& x.Key == ContentKey1
|
||||
&& x.Alias == ContentAlias1);
|
||||
var test2ContentType = Mock.Of<IPublishedContentType2>(x =>
|
||||
x.IsElement == true
|
||||
&& x.Key == Key2
|
||||
&& x.Alias == Alias2);
|
||||
&& x.Key == ContentKey2
|
||||
&& x.Alias == ContentAlias2);
|
||||
var test3ContentType = Mock.Of<IPublishedContentType2>(x =>
|
||||
x.IsElement == true
|
||||
&& x.Key == SettingKey1
|
||||
&& x.Alias == SettingAlias1);
|
||||
var test4ContentType = Mock.Of<IPublishedContentType2>(x =>
|
||||
x.IsElement == true
|
||||
&& x.Key == SettingKey2
|
||||
&& x.Alias == SettingAlias2);
|
||||
var contentCache = new Mock<IPublishedContentCache2>();
|
||||
contentCache.Setup(x => x.GetContentType(Key1)).Returns(test1ContentType);
|
||||
contentCache.Setup(x => x.GetContentType(Key2)).Returns(test2ContentType);
|
||||
contentCache.Setup(x => x.GetContentType(ContentKey1)).Returns(test1ContentType);
|
||||
contentCache.Setup(x => x.GetContentType(ContentKey2)).Returns(test2ContentType);
|
||||
contentCache.Setup(x => x.GetContentType(SettingKey1)).Returns(test3ContentType);
|
||||
contentCache.Setup(x => x.GetContentType(SettingKey2)).Returns(test4ContentType);
|
||||
var publishedSnapshot = Mock.Of<IPublishedSnapshot>(x => x.Content == contentCache.Object);
|
||||
var publishedSnapshotAccessor = Mock.Of<IPublishedSnapshotAccessor>(x => x.PublishedSnapshot == publishedSnapshot);
|
||||
return publishedSnapshotAccessor;
|
||||
@@ -60,11 +74,13 @@ namespace Umbraco.Tests.PropertyEditors
|
||||
Blocks = new[] {
|
||||
new BlockListConfiguration.BlockConfiguration
|
||||
{
|
||||
Key = Key1
|
||||
ContentElementTypeKey = ContentKey1,
|
||||
SettingsElementTypeKey = SettingKey2
|
||||
},
|
||||
new BlockListConfiguration.BlockConfiguration
|
||||
{
|
||||
Key = Key2
|
||||
ContentElementTypeKey = ContentKey2,
|
||||
SettingsElementTypeKey = SettingKey1
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -74,7 +90,7 @@ namespace Umbraco.Tests.PropertyEditors
|
||||
Blocks = new[] {
|
||||
new BlockListConfiguration.BlockConfiguration
|
||||
{
|
||||
Key = Key1
|
||||
ContentElementTypeKey = ContentKey1
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -138,14 +154,14 @@ namespace Umbraco.Tests.PropertyEditors
|
||||
var converted = editor.ConvertIntermediateToObject(publishedElement, propertyType, PropertyCacheLevel.None, json, false) as BlockListModel;
|
||||
|
||||
Assert.IsNotNull(converted);
|
||||
Assert.AreEqual(0, converted.Data.Count());
|
||||
Assert.AreEqual(0, converted.ContentData.Count());
|
||||
Assert.AreEqual(0, converted.Layout.Count());
|
||||
|
||||
json = string.Empty;
|
||||
converted = editor.ConvertIntermediateToObject(publishedElement, propertyType, PropertyCacheLevel.None, json, false) as BlockListModel;
|
||||
|
||||
Assert.IsNotNull(converted);
|
||||
Assert.AreEqual(0, converted.Data.Count());
|
||||
Assert.AreEqual(0, converted.ContentData.Count());
|
||||
Assert.AreEqual(0, converted.Layout.Count());
|
||||
}
|
||||
|
||||
@@ -161,7 +177,7 @@ namespace Umbraco.Tests.PropertyEditors
|
||||
var converted = editor.ConvertIntermediateToObject(publishedElement, propertyType, PropertyCacheLevel.None, json, false) as BlockListModel;
|
||||
|
||||
Assert.IsNotNull(converted);
|
||||
Assert.AreEqual(0, converted.Data.Count());
|
||||
Assert.AreEqual(0, converted.ContentData.Count());
|
||||
Assert.AreEqual(0, converted.Layout.Count());
|
||||
|
||||
json = @"{
|
||||
@@ -170,7 +186,7 @@ data: []}";
|
||||
converted = editor.ConvertIntermediateToObject(publishedElement, propertyType, PropertyCacheLevel.None, json, false) as BlockListModel;
|
||||
|
||||
Assert.IsNotNull(converted);
|
||||
Assert.AreEqual(0, converted.Data.Count());
|
||||
Assert.AreEqual(0, converted.ContentData.Count());
|
||||
Assert.AreEqual(0, converted.Layout.Count());
|
||||
|
||||
// Even though there is a layout, there is no data, so the conversion will result in zero elements in total
|
||||
@@ -179,18 +195,17 @@ data: []}";
|
||||
layout: {
|
||||
'" + Constants.PropertyEditors.Aliases.BlockList + @"': [
|
||||
{
|
||||
'udi': 'umb://element/e7dba547615b4e9ab4ab2a7674845bc9',
|
||||
'settings': {}
|
||||
'contentUdi': 'umb://element/e7dba547615b4e9ab4ab2a7674845bc9'
|
||||
}
|
||||
]
|
||||
},
|
||||
data: []
|
||||
contentData: []
|
||||
}";
|
||||
|
||||
converted = editor.ConvertIntermediateToObject(publishedElement, propertyType, PropertyCacheLevel.None, json, false) as BlockListModel;
|
||||
|
||||
Assert.IsNotNull(converted);
|
||||
Assert.AreEqual(0, converted.Data.Count());
|
||||
Assert.AreEqual(0, converted.ContentData.Count());
|
||||
Assert.AreEqual(0, converted.Layout.Count());
|
||||
|
||||
// Even though there is a layout and data, the data is invalid (missing required keys) so the conversion will result in zero elements in total
|
||||
@@ -199,12 +214,11 @@ data: []}";
|
||||
layout: {
|
||||
'" + Constants.PropertyEditors.Aliases.BlockList + @"': [
|
||||
{
|
||||
'udi': 'umb://element/e7dba547615b4e9ab4ab2a7674845bc9',
|
||||
'settings': {}
|
||||
'contentUdi': 'umb://element/e7dba547615b4e9ab4ab2a7674845bc9'
|
||||
}
|
||||
]
|
||||
},
|
||||
data: [
|
||||
contentData: [
|
||||
{
|
||||
'udi': 'umb://element/e7dba547615b4e9ab4ab2a7674845bc9'
|
||||
}
|
||||
@@ -214,7 +228,7 @@ data: []}";
|
||||
converted = editor.ConvertIntermediateToObject(publishedElement, propertyType, PropertyCacheLevel.None, json, false) as BlockListModel;
|
||||
|
||||
Assert.IsNotNull(converted);
|
||||
Assert.AreEqual(0, converted.Data.Count());
|
||||
Assert.AreEqual(0, converted.ContentData.Count());
|
||||
Assert.AreEqual(0, converted.Layout.Count());
|
||||
|
||||
// Everthing is ok except the udi reference in the layout doesn't match the data so it will be empty
|
||||
@@ -223,14 +237,13 @@ data: []}";
|
||||
layout: {
|
||||
'" + Constants.PropertyEditors.Aliases.BlockList + @"': [
|
||||
{
|
||||
'udi': 'umb://element/1304E1DDAC87439684FE8A399231CB3D',
|
||||
'settings': {}
|
||||
'contentUdi': 'umb://element/1304E1DDAC87439684FE8A399231CB3D'
|
||||
}
|
||||
]
|
||||
},
|
||||
data: [
|
||||
contentData: [
|
||||
{
|
||||
'contentTypeKey': '" + Key1 + @"',
|
||||
'contentTypeKey': '" + ContentKey1 + @"',
|
||||
'key': '1304E1DD-0000-4396-84FE-8A399231CB3D'
|
||||
}
|
||||
]
|
||||
@@ -239,7 +252,7 @@ data: []}";
|
||||
converted = editor.ConvertIntermediateToObject(publishedElement, propertyType, PropertyCacheLevel.None, json, false) as BlockListModel;
|
||||
|
||||
Assert.IsNotNull(converted);
|
||||
Assert.AreEqual(1, converted.Data.Count());
|
||||
Assert.AreEqual(1, converted.ContentData.Count());
|
||||
Assert.AreEqual(0, converted.Layout.Count());
|
||||
}
|
||||
|
||||
@@ -256,14 +269,13 @@ data: []}";
|
||||
layout: {
|
||||
'" + Constants.PropertyEditors.Aliases.BlockList + @"': [
|
||||
{
|
||||
'udi': 'umb://element/1304E1DDAC87439684FE8A399231CB3D',
|
||||
'settings': {}
|
||||
'contentUdi': 'umb://element/1304E1DDAC87439684FE8A399231CB3D'
|
||||
}
|
||||
]
|
||||
},
|
||||
data: [
|
||||
contentData: [
|
||||
{
|
||||
'contentTypeKey': '" + Key1 + @"',
|
||||
'contentTypeKey': '" + ContentKey1 + @"',
|
||||
'udi': 'umb://element/1304E1DDAC87439684FE8A399231CB3D'
|
||||
}
|
||||
]
|
||||
@@ -271,14 +283,14 @@ data: []}";
|
||||
var converted = editor.ConvertIntermediateToObject(publishedElement, propertyType, PropertyCacheLevel.None, json, false) as BlockListModel;
|
||||
|
||||
Assert.IsNotNull(converted);
|
||||
Assert.AreEqual(1, converted.Data.Count());
|
||||
var item0 = converted.Data.ElementAt(0);
|
||||
Assert.AreEqual(1, converted.ContentData.Count());
|
||||
var item0 = converted.ContentData.ElementAt(0);
|
||||
Assert.AreEqual(Guid.Parse("1304E1DD-AC87-4396-84FE-8A399231CB3D"), item0.Key);
|
||||
Assert.AreEqual("Test1", item0.ContentType.Alias);
|
||||
Assert.AreEqual(1, converted.Layout.Count());
|
||||
var layout0 = converted.Layout.ElementAt(0);
|
||||
Assert.IsNull(layout0.Settings);
|
||||
Assert.AreEqual(Udi.Parse("umb://element/1304E1DDAC87439684FE8A399231CB3D"), layout0.Udi);
|
||||
Assert.AreEqual(Udi.Parse("umb://element/1304E1DDAC87439684FE8A399231CB3D"), layout0.ContentUdi);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -294,44 +306,142 @@ data: []}";
|
||||
layout: {
|
||||
'" + Constants.PropertyEditors.Aliases.BlockList + @"': [
|
||||
{
|
||||
'udi': 'umb://element/1304E1DDAC87439684FE8A399231CB3D',
|
||||
'settings': {}
|
||||
'contentUdi': 'umb://element/1304E1DDAC87439684FE8A399231CB3D',
|
||||
'settingsUdi': 'umb://element/1F613E26CE274898908A561437AF5100'
|
||||
},
|
||||
{
|
||||
'udi': 'umb://element/0A4A416E547D464FABCC6F345C17809A',
|
||||
'settings': {}
|
||||
'contentUdi': 'umb://element/0A4A416E547D464FABCC6F345C17809A',
|
||||
'settingsUdi': 'umb://element/63027539B0DB45E7B70459762D4E83DD'
|
||||
}
|
||||
]
|
||||
},
|
||||
data: [
|
||||
contentData: [
|
||||
{
|
||||
'contentTypeKey': '" + Key1 + @"',
|
||||
'contentTypeKey': '" + ContentKey1 + @"',
|
||||
'udi': 'umb://element/1304E1DDAC87439684FE8A399231CB3D'
|
||||
},
|
||||
{
|
||||
'contentTypeKey': '" + Key2 + @"',
|
||||
'contentTypeKey': '" + ContentKey2 + @"',
|
||||
'udi': 'umb://element/E05A034704424AB3A520E048E6197E79'
|
||||
},
|
||||
{
|
||||
'contentTypeKey': '" + Key2 + @"',
|
||||
'contentTypeKey': '" + ContentKey2 + @"',
|
||||
'udi': 'umb://element/0A4A416E547D464FABCC6F345C17809A'
|
||||
}
|
||||
]
|
||||
],
|
||||
settingsData: [
|
||||
{
|
||||
'contentTypeKey': '" + SettingKey1 + @"',
|
||||
'udi': 'umb://element/63027539B0DB45E7B70459762D4E83DD'
|
||||
},
|
||||
{
|
||||
'contentTypeKey': '" + SettingKey2 + @"',
|
||||
'udi': 'umb://element/1F613E26CE274898908A561437AF5100'
|
||||
},
|
||||
{
|
||||
'contentTypeKey': '" + SettingKey2 + @"',
|
||||
'udi': 'umb://element/BCF4BA3DA40C496C93EC58FAC85F18B9'
|
||||
}
|
||||
],
|
||||
}";
|
||||
|
||||
var converted = editor.ConvertIntermediateToObject(publishedElement, propertyType, PropertyCacheLevel.None, json, false) as BlockListModel;
|
||||
|
||||
Assert.IsNotNull(converted);
|
||||
Assert.AreEqual(3, converted.Data.Count());
|
||||
Assert.AreEqual(3, converted.ContentData.Count());
|
||||
Assert.AreEqual(3, converted.SettingsData.Count());
|
||||
Assert.AreEqual(2, converted.Layout.Count());
|
||||
|
||||
var item0 = converted.Layout.ElementAt(0);
|
||||
Assert.AreEqual(Guid.Parse("1304E1DD-AC87-4396-84FE-8A399231CB3D"), item0.Data.Key);
|
||||
Assert.AreEqual("Test1", item0.Data.ContentType.Alias);
|
||||
Assert.AreEqual(Guid.Parse("1304E1DD-AC87-4396-84FE-8A399231CB3D"), item0.Content.Key);
|
||||
Assert.AreEqual("Test1", item0.Content.ContentType.Alias);
|
||||
Assert.AreEqual(Guid.Parse("1F613E26CE274898908A561437AF5100"), item0.Settings.Key);
|
||||
Assert.AreEqual("Setting2", item0.Settings.ContentType.Alias);
|
||||
|
||||
var item1 = converted.Layout.ElementAt(1);
|
||||
Assert.AreEqual(Guid.Parse("0A4A416E-547D-464F-ABCC-6F345C17809A"), item1.Data.Key);
|
||||
Assert.AreEqual("Test2", item1.Data.ContentType.Alias);
|
||||
Assert.AreEqual(Guid.Parse("0A4A416E-547D-464F-ABCC-6F345C17809A"), item1.Content.Key);
|
||||
Assert.AreEqual("Test2", item1.Content.ContentType.Alias);
|
||||
Assert.AreEqual(Guid.Parse("63027539B0DB45E7B70459762D4E83DD"), item1.Settings.Key);
|
||||
Assert.AreEqual("Setting1", item1.Settings.ContentType.Alias);
|
||||
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Data_Item_Removed_If_Removed_From_Config()
|
||||
{
|
||||
var editor = CreateConverter();
|
||||
|
||||
// The data below expects that ContentKey1 + ContentKey2 + SettingsKey1 + SettingsKey2 exist but only ContentKey2 exists so
|
||||
// the data should all be filtered.
|
||||
var config = new BlockListConfiguration
|
||||
{
|
||||
Blocks = new[] {
|
||||
new BlockListConfiguration.BlockConfiguration
|
||||
{
|
||||
ContentElementTypeKey = ContentKey2,
|
||||
SettingsElementTypeKey = null
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var propertyType = GetPropertyType(config);
|
||||
var publishedElement = Mock.Of<IPublishedElement>();
|
||||
|
||||
var json = @"
|
||||
{
|
||||
layout: {
|
||||
'" + Constants.PropertyEditors.Aliases.BlockList + @"': [
|
||||
{
|
||||
'contentUdi': 'umb://element/1304E1DDAC87439684FE8A399231CB3D',
|
||||
'settingsUdi': 'umb://element/1F613E26CE274898908A561437AF5100'
|
||||
},
|
||||
{
|
||||
'contentUdi': 'umb://element/0A4A416E547D464FABCC6F345C17809A',
|
||||
'settingsUdi': 'umb://element/63027539B0DB45E7B70459762D4E83DD'
|
||||
}
|
||||
]
|
||||
},
|
||||
contentData: [
|
||||
{
|
||||
'contentTypeKey': '" + ContentKey1 + @"',
|
||||
'udi': 'umb://element/1304E1DDAC87439684FE8A399231CB3D'
|
||||
},
|
||||
{
|
||||
'contentTypeKey': '" + ContentKey2 + @"',
|
||||
'udi': 'umb://element/E05A034704424AB3A520E048E6197E79'
|
||||
},
|
||||
{
|
||||
'contentTypeKey': '" + ContentKey2 + @"',
|
||||
'udi': 'umb://element/0A4A416E547D464FABCC6F345C17809A'
|
||||
}
|
||||
],
|
||||
settingsData: [
|
||||
{
|
||||
'contentTypeKey': '" + SettingKey1 + @"',
|
||||
'udi': 'umb://element/63027539B0DB45E7B70459762D4E83DD'
|
||||
},
|
||||
{
|
||||
'contentTypeKey': '" + SettingKey2 + @"',
|
||||
'udi': 'umb://element/1F613E26CE274898908A561437AF5100'
|
||||
},
|
||||
{
|
||||
'contentTypeKey': '" + SettingKey2 + @"',
|
||||
'udi': 'umb://element/BCF4BA3DA40C496C93EC58FAC85F18B9'
|
||||
}
|
||||
],
|
||||
}";
|
||||
|
||||
var converted = editor.ConvertIntermediateToObject(publishedElement, propertyType, PropertyCacheLevel.None, json, false) as BlockListModel;
|
||||
|
||||
Assert.IsNotNull(converted);
|
||||
Assert.AreEqual(2, converted.ContentData.Count());
|
||||
Assert.AreEqual(0, converted.SettingsData.Count());
|
||||
Assert.AreEqual(1, converted.Layout.Count());
|
||||
|
||||
var item0 = converted.Layout.ElementAt(0);
|
||||
Assert.AreEqual(Guid.Parse("0A4A416E-547D-464F-ABCC-6F345C17809A"), item0.Content.Key);
|
||||
Assert.AreEqual("Test2", item0.Content.ContentType.Alias);
|
||||
Assert.IsNull(item0.Settings);
|
||||
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -517,7 +517,7 @@
|
||||
}
|
||||
|
||||
function handleHttpException(err) {
|
||||
if (!err.status) {
|
||||
if (err && !err.status) {
|
||||
$exceptionHandler(err);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
if (!elementModel || !elementModel.variants || !elementModel.variants.length) { return; }
|
||||
|
||||
var variant = elementModel.variants[0];
|
||||
|
||||
|
||||
for (var t = 0; t < variant.tabs.length; t++) {
|
||||
var tab = variant.tabs[t];
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -44,11 +44,11 @@
|
||||
* needs to stay simple to avoid deep watching.
|
||||
*/
|
||||
function mapToPropertyModel(elementModel, dataModel) {
|
||||
|
||||
|
||||
if (!elementModel || !elementModel.variants || !elementModel.variants.length) { return; }
|
||||
|
||||
var variant = elementModel.variants[0];
|
||||
|
||||
|
||||
for (var t = 0; t < variant.tabs.length; t++) {
|
||||
var tab = variant.tabs[t];
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -98,13 +98,13 @@
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Generate label for Block, uses either the labelInterpolator or falls back to the contentTypeName.
|
||||
* @param {Object} blockObject BlockObject to recive data values from.
|
||||
*/
|
||||
function getBlockLabel(blockObject) {
|
||||
if(blockObject.labelInterpolator !== undefined) {
|
||||
if (blockObject.labelInterpolator !== undefined) {
|
||||
// We are just using the data model, since its a key/value object that is live synced. (if we need to add additional vars, we could make a shallow copy and apply those.)
|
||||
return blockObject.labelInterpolator(blockObject.data);
|
||||
}
|
||||
@@ -133,10 +133,10 @@
|
||||
// But we like to sync non-primitive values as well! Yes, and this does happen, just not through this code, but through the nature of JavaScript.
|
||||
// Non-primitive values act as references to the same data and are therefor synced.
|
||||
blockObject.__watchers.push(isolatedScope.$watch("blockObjects._" + blockObject.key + "." + field + ".variants[0].tabs[" + t + "].properties[" + p + "].value", watcherCreator(blockObject, prop)));
|
||||
|
||||
|
||||
// We also like to watch our data model to be able to capture changes coming from other places.
|
||||
if (forSettings === true) {
|
||||
blockObject.__watchers.push(isolatedScope.$watch("blockObjects._" + blockObject.key + "." + "layout.settings" + "." + prop.alias, createLayoutSettingsModelWatcher(blockObject, prop)));
|
||||
blockObject.__watchers.push(isolatedScope.$watch("blockObjects._" + blockObject.key + "." + "settingsData" + "." + prop.alias, createLayoutSettingsModelWatcher(blockObject, prop)));
|
||||
} else {
|
||||
blockObject.__watchers.push(isolatedScope.$watch("blockObjects._" + blockObject.key + "." + "data" + "." + prop.alias, createDataModelWatcher(blockObject, prop)));
|
||||
}
|
||||
@@ -151,8 +151,8 @@
|
||||
/**
|
||||
* Used to create a prop watcher for the data in the property editor data model.
|
||||
*/
|
||||
function createDataModelWatcher(blockObject, prop) {
|
||||
return function() {
|
||||
function createDataModelWatcher(blockObject, prop) {
|
||||
return function () {
|
||||
if (prop.value !== blockObject.data[prop.alias]) {
|
||||
|
||||
// sync data:
|
||||
@@ -165,11 +165,11 @@
|
||||
/**
|
||||
* Used to create a prop watcher for the settings in the property editor data model.
|
||||
*/
|
||||
function createLayoutSettingsModelWatcher(blockObject, prop) {
|
||||
return function() {
|
||||
if (prop.value !== blockObject.layout.settings[prop.alias]) {
|
||||
function createLayoutSettingsModelWatcher(blockObject, prop) {
|
||||
return function () {
|
||||
if (prop.value !== blockObject.settingsData[prop.alias]) {
|
||||
// sync data:
|
||||
prop.value = blockObject.layout.settings[prop.alias];
|
||||
prop.value = blockObject.settingsData[prop.alias];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -177,8 +177,8 @@
|
||||
/**
|
||||
* Used to create a scoped watcher for a content property on a blockObject.
|
||||
*/
|
||||
function createContentModelPropWatcher(blockObject, prop) {
|
||||
return function() {
|
||||
function createContentModelPropWatcher(blockObject, prop) {
|
||||
return function () {
|
||||
if (blockObject.data[prop.alias] !== prop.value) {
|
||||
// sync data:
|
||||
blockObject.data[prop.alias] = prop.value;
|
||||
@@ -191,11 +191,11 @@
|
||||
/**
|
||||
* Used to create a scoped watcher for a settings property on a blockObject.
|
||||
*/
|
||||
function createSettingsModelPropWatcher(blockObject, prop) {
|
||||
return function() {
|
||||
if (blockObject.layout.settings[prop.alias] !== prop.value) {
|
||||
function createSettingsModelPropWatcher(blockObject, prop) {
|
||||
return function () {
|
||||
if (blockObject.settingsData[prop.alias] !== prop.value) {
|
||||
// sync data:
|
||||
blockObject.layout.settings[prop.alias] = prop.value;
|
||||
blockObject.settingsData[prop.alias] = prop.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -245,7 +245,8 @@
|
||||
// ensure basic part of data-structure is in place:
|
||||
this.value = propertyModelValue;
|
||||
this.value.layout = this.value.layout || {};
|
||||
this.value.data = this.value.data || [];
|
||||
this.value.contentData = this.value.contentData || [];
|
||||
this.value.settingsData = this.value.settingsData || [];
|
||||
|
||||
this.propertyEditorAlias = propertyEditorAlias;
|
||||
this.blockConfigurations = blockConfigurations;
|
||||
@@ -254,17 +255,17 @@
|
||||
|
||||
this.isolatedScope = scopeOfExistance.$new(true);
|
||||
this.isolatedScope.blockObjects = {};
|
||||
|
||||
|
||||
this.__watchers.push(this.isolatedScope.$on("$destroy", this.destroy.bind(this)));
|
||||
this.__watchers.push(propertyEditorScope.$on("postFormSubmitting", this.sync.bind(this)));
|
||||
|
||||
};
|
||||
|
||||
|
||||
BlockEditorModelObject.prototype = {
|
||||
|
||||
update: function (propertyModelValue, propertyEditorScope) {
|
||||
// clear watchers
|
||||
this.__watchers.forEach(w => { w(); });
|
||||
this.__watchers.forEach(w => { w(); });
|
||||
delete this.__watchers;
|
||||
|
||||
// clear block objects
|
||||
@@ -292,7 +293,7 @@
|
||||
* @param {string} key contentTypeKey to recive the configuration model for.
|
||||
* @returns {Object | null} Configuration model for the that specific block. Or ´null´ if the contentTypeKey isnt available in the current block configurations.
|
||||
*/
|
||||
getBlockConfiguration: function(key) {
|
||||
getBlockConfiguration: function (key) {
|
||||
return this.blockConfigurations.find(bc => bc.contentTypeKey === key) || null;
|
||||
},
|
||||
|
||||
@@ -304,7 +305,7 @@
|
||||
* @param {Object} blockObject BlockObject to receive data values from.
|
||||
* @returns {Promise} A Promise object which resolves when all scaffold models are loaded.
|
||||
*/
|
||||
load: function() {
|
||||
load: function () {
|
||||
var tasks = [];
|
||||
|
||||
var scaffoldKeys = [];
|
||||
@@ -338,7 +339,7 @@
|
||||
* @description Retrive a list of aliases that are available for content of blocks in this property editor, does not contain aliases of block settings.
|
||||
* @return {Array} array of strings representing alias.
|
||||
*/
|
||||
getAvailableAliasesForBlockContent: function() {
|
||||
getAvailableAliasesForBlockContent: function () {
|
||||
return this.blockConfigurations.map(blockConfiguration => this.getScaffoldFromKey(blockConfiguration.contentTypeKey).contentTypeAlias);
|
||||
},
|
||||
|
||||
@@ -350,13 +351,13 @@
|
||||
* The purpose of this data is to provide it for the Block Picker.
|
||||
* @return {Array} array of objects representing available blocks, each object containing properties blockConfigModel and elementTypeModel.
|
||||
*/
|
||||
getAvailableBlocksForBlockPicker: function() {
|
||||
getAvailableBlocksForBlockPicker: function () {
|
||||
|
||||
var blocks = [];
|
||||
|
||||
this.blockConfigurations.forEach(blockConfiguration => {
|
||||
var scaffold = this.getScaffoldFromKey(blockConfiguration.contentTypeKey);
|
||||
if(scaffold) {
|
||||
if (scaffold) {
|
||||
blocks.push({
|
||||
blockConfigModel: blockConfiguration,
|
||||
elementTypeModel: scaffold.documentType
|
||||
@@ -375,7 +376,7 @@
|
||||
* @param {string} key contentTypeKey to recive the scaffold model for.
|
||||
* @returns {Object | null} Scaffold model for the that content type. Or null if the scaffolding model dosnt exist in this context.
|
||||
*/
|
||||
getScaffoldFromKey: function(contentTypeKey) {
|
||||
getScaffoldFromKey: function (contentTypeKey) {
|
||||
return this.scaffolds.find(o => o.contentTypeKey === contentTypeKey);
|
||||
},
|
||||
|
||||
@@ -387,7 +388,7 @@
|
||||
* @param {string} alias contentTypeAlias to recive the scaffold model for.
|
||||
* @returns {Object | null} Scaffold model for the that content type. Or null if the scaffolding model dosnt exist in this context.
|
||||
*/
|
||||
getScaffoldFromAlias: function(contentTypeAlias) {
|
||||
getScaffoldFromAlias: function (contentTypeAlias) {
|
||||
return this.scaffolds.find(o => o.contentTypeAlias === contentTypeAlias);
|
||||
},
|
||||
|
||||
@@ -412,14 +413,14 @@
|
||||
* @param {Object} layoutEntry the layout entry object to build the block model from.
|
||||
* @return {Object | null} The BlockObject for the given layout entry. Or null if data or configuration wasnt found for this block.
|
||||
*/
|
||||
getBlockObject: function(layoutEntry) {
|
||||
getBlockObject: function (layoutEntry) {
|
||||
|
||||
var udi = layoutEntry.udi;
|
||||
var udi = layoutEntry.contentUdi;
|
||||
|
||||
var dataModel = this._getDataByUdi(udi);
|
||||
|
||||
if (dataModel === null) {
|
||||
console.error("Couldnt find content model of " + udi)
|
||||
console.error("Couldn't find content model of " + udi)
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -427,11 +428,12 @@
|
||||
var contentScaffold;
|
||||
|
||||
if (blockConfiguration === null) {
|
||||
console.error("The block entry of "+udi+" is not begin initialized cause its contentTypeKey is not allowed for this PropertyEditor");
|
||||
} else {
|
||||
console.error("The block entry of " + udi + " is not being initialized because its contentTypeKey is not allowed for this PropertyEditor");
|
||||
}
|
||||
else {
|
||||
contentScaffold = this.getScaffoldFromKey(blockConfiguration.contentTypeKey);
|
||||
if(contentScaffold === null) {
|
||||
console.error("The block entry of "+udi+" is not begin initialized cause its Element Type was not loaded.");
|
||||
if (contentScaffold === null) {
|
||||
console.error("The block entry of " + udi + " is not begin initialized cause its Element Type was not loaded.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -442,12 +444,12 @@
|
||||
unsupported: true
|
||||
};
|
||||
contentScaffold = {};
|
||||
|
||||
|
||||
}
|
||||
|
||||
var blockObject = {};
|
||||
// Set an angularJS cloneNode method, to avoid this object begin cloned.
|
||||
blockObject.cloneNode = function() {
|
||||
blockObject.cloneNode = function () {
|
||||
return null;// angularJS accept this as a cloned value as long as the
|
||||
}
|
||||
blockObject.key = String.CreateGuid().replace(/-/g, "");
|
||||
@@ -464,7 +466,7 @@
|
||||
this.__scope.$evalAsync();
|
||||
}
|
||||
}.bind(blockObject)
|
||||
, 10);
|
||||
, 10);
|
||||
|
||||
// make basics from scaffold
|
||||
blockObject.content = Utilities.copy(contentScaffold);
|
||||
@@ -482,20 +484,29 @@
|
||||
var settingsScaffold = this.getScaffoldFromKey(blockConfiguration.settingsElementTypeKey);
|
||||
if (settingsScaffold !== null) {
|
||||
|
||||
layoutEntry.settings = layoutEntry.settings || {};
|
||||
|
||||
blockObject.settingsData = layoutEntry.settings;
|
||||
if (!layoutEntry.settingsUdi) {
|
||||
// if this block does not have settings data, then create it. This could happen because settings model has been added later than this content was created.
|
||||
layoutEntry.settingsUdi = this._createSettingsEntry(blockConfiguration.settingsElementTypeKey);
|
||||
}
|
||||
|
||||
var settingsUdi = layoutEntry.settingsUdi;
|
||||
|
||||
var settingsData = this._getSettingsByUdi(settingsUdi);
|
||||
if (settingsData === null) {
|
||||
console.error("Couldnt find content settings data of " + settingsUdi)
|
||||
return null;
|
||||
}
|
||||
|
||||
blockObject.settingsData = settingsData;
|
||||
|
||||
// make basics from scaffold
|
||||
blockObject.settings = Utilities.copy(settingsScaffold);
|
||||
layoutEntry.settings = layoutEntry.settings || {};
|
||||
if (!layoutEntry.settings.key) { layoutEntry.settings.key = String.CreateGuid(); }
|
||||
if (!layoutEntry.settings.contentTypeKey) { layoutEntry.settings.contentTypeKey = blockConfiguration.settingsElementTypeKey; }
|
||||
mapToElementModel(blockObject.settings, layoutEntry.settings);
|
||||
blockObject.settings.udi = settingsUdi;
|
||||
mapToElementModel(blockObject.settings, settingsData);
|
||||
}
|
||||
}
|
||||
|
||||
blockObject.retrieveValuesFrom = function(content, settings) {
|
||||
blockObject.retrieveValuesFrom = function (content, settings) {
|
||||
if (this.content !== null) {
|
||||
mapElementValues(content, this.content);
|
||||
}
|
||||
@@ -510,7 +521,7 @@
|
||||
mapToPropertyModel(this.content, this.data);
|
||||
}
|
||||
if (this.config.settingsElementTypeKey !== null) {
|
||||
mapToPropertyModel(this.settings, this.layout.settings);
|
||||
mapToPropertyModel(this.settings, this.settingsData);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -532,9 +543,10 @@
|
||||
|
||||
delete this.layout;
|
||||
delete this.data;
|
||||
delete this.settingsData;
|
||||
delete this.content;
|
||||
delete this.settings;
|
||||
|
||||
|
||||
// remove model from isolatedScope.
|
||||
delete this.__scope.blockObjects["_" + this.key];
|
||||
// NOTE: It seems like we should call this.__scope.$destroy(); since that is the only way to remove a scope from it's parent,
|
||||
@@ -544,7 +556,7 @@
|
||||
|
||||
// removes this method, making it impossible to destroy again.
|
||||
delete this.destroy;
|
||||
|
||||
|
||||
// lets remove the key to make things blow up if this is still referenced:
|
||||
delete this.key;
|
||||
}
|
||||
@@ -563,8 +575,15 @@
|
||||
*/
|
||||
removeDataAndDestroyModel: function (blockObject) {
|
||||
var udi = blockObject.content.udi;
|
||||
var settingsUdi = null;
|
||||
if (blockObject.settings) {
|
||||
settingsUdi = blockObject.settings.udi;
|
||||
}
|
||||
this.destroyBlockObject(blockObject);
|
||||
this.removeDataByUdi(udi);
|
||||
if (settingsUdi) {
|
||||
this.removeSettingsByUdi(settingsUdi);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -574,7 +593,7 @@
|
||||
* @description Destroys the Block Model, but all data is kept.
|
||||
* @param {Object} blockObject The BlockObject to be destroyed.
|
||||
*/
|
||||
destroyBlockObject: function(blockObject) {
|
||||
destroyBlockObject: function (blockObject) {
|
||||
blockObject.destroy();
|
||||
},
|
||||
|
||||
@@ -586,13 +605,13 @@
|
||||
* @param {object} defaultStructure if no data exist the layout of your poerty editor will be set to this object.
|
||||
* @return {Object} Layout object, structure depends on the model of your property editor.
|
||||
*/
|
||||
getLayout: function(defaultStructure) {
|
||||
getLayout: function (defaultStructure) {
|
||||
if (!this.value.layout[this.propertyEditorAlias]) {
|
||||
this.value.layout[this.propertyEditorAlias] = defaultStructure;
|
||||
}
|
||||
return this.value.layout[this.propertyEditorAlias];
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* @ngdoc method
|
||||
* @name create
|
||||
@@ -601,21 +620,21 @@
|
||||
* @param {string} contentTypeKey the contentTypeKey of the block you wish to create, if contentTypeKey is not avaiable in the block configuration then ´null´ will be returned.
|
||||
* @return {Object | null} Layout entry object, to be inserted at a decired location in the layout object. Or null if contentTypeKey is unavaiaible.
|
||||
*/
|
||||
create: function(contentTypeKey) {
|
||||
|
||||
create: function (contentTypeKey) {
|
||||
|
||||
var blockConfiguration = this.getBlockConfiguration(contentTypeKey);
|
||||
if(blockConfiguration === null) {
|
||||
if (blockConfiguration === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var entry = {
|
||||
udi: this._createDataEntry(contentTypeKey)
|
||||
contentUdi: this._createDataEntry(contentTypeKey)
|
||||
}
|
||||
|
||||
if (blockConfiguration.settingsElementTypeKey != null) {
|
||||
entry.settings = { key: String.CreateGuid(), contentTypeKey: blockConfiguration.settingsElementTypeKey };
|
||||
entry.settingsUdi = this._createSettingsEntry(blockConfiguration.settingsElementTypeKey)
|
||||
}
|
||||
|
||||
|
||||
return entry;
|
||||
},
|
||||
|
||||
@@ -626,19 +645,19 @@
|
||||
* @description Insert data from ElementType Model
|
||||
* @return {Object | null} Layout entry object, to be inserted at a decired location in the layout object. Or ´null´ if the given ElementType isnt supported by the block configuration.
|
||||
*/
|
||||
createFromElementType: function(elementTypeDataModel) {
|
||||
createFromElementType: function (elementTypeDataModel) {
|
||||
|
||||
elementTypeDataModel = Utilities.copy(elementTypeDataModel);
|
||||
|
||||
var contentTypeKey = elementTypeDataModel.contentTypeKey;
|
||||
|
||||
var layoutEntry = this.create(contentTypeKey);
|
||||
if(layoutEntry === null) {
|
||||
if (layoutEntry === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var dataModel = this._getDataByUdi(layoutEntry.udi);
|
||||
if(dataModel === null) {
|
||||
if (dataModel === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -654,7 +673,7 @@
|
||||
* @methodOf umbraco.services.blockEditorModelObject
|
||||
* @description Force immidiate update of the blockobject models to the property model.
|
||||
*/
|
||||
sync: function() {
|
||||
sync: function () {
|
||||
for (const key in this.isolatedScope.blockObjects) {
|
||||
this.isolatedScope.blockObjects[key].sync();
|
||||
}
|
||||
@@ -662,32 +681,64 @@
|
||||
|
||||
// private
|
||||
// TODO: Then this can just be a method in the outer scope
|
||||
_createDataEntry: function(elementTypeKey) {
|
||||
_createDataEntry: function (elementTypeKey) {
|
||||
var content = {
|
||||
contentTypeKey: elementTypeKey,
|
||||
udi: udiService.create("element")
|
||||
};
|
||||
this.value.data.push(content);
|
||||
this.value.contentData.push(content);
|
||||
return content.udi;
|
||||
},
|
||||
// private
|
||||
// TODO: Then this can just be a method in the outer scope
|
||||
_getDataByUdi: function(udi) {
|
||||
return this.value.data.find(entry => entry.udi === udi) || null;
|
||||
_getDataByUdi: function (udi) {
|
||||
return this.value.contentData.find(entry => entry.udi === udi) || null;
|
||||
},
|
||||
|
||||
/**
|
||||
* @ngdoc method
|
||||
* @name removeDataByUdi
|
||||
* @methodOf umbraco.services.blockEditorModelObject
|
||||
* @description Removes the data of a given UDI.
|
||||
* Notice this method does not remove the block from your layout, this will need to be handlede by the Property Editor since this services donst know about your layout structure.
|
||||
* @param {string} udi The UDI of the data to be removed.
|
||||
* @description Removes the content data of a given UDI.
|
||||
* Notice this method does not remove the block from your layout, this will need to be handled by the Property Editor since this services don't know about your layout structure.
|
||||
* @param {string} udi The UDI of the content data to be removed.
|
||||
*/
|
||||
removeDataByUdi: function(udi) {
|
||||
const index = this.value.data.findIndex(o => o.udi === udi);
|
||||
removeDataByUdi: function (udi) {
|
||||
const index = this.value.contentData.findIndex(o => o.udi === udi);
|
||||
if (index !== -1) {
|
||||
this.value.data.splice(index, 1);
|
||||
this.value.contentData.splice(index, 1);
|
||||
}
|
||||
},
|
||||
|
||||
// private
|
||||
// TODO: Then this can just be a method in the outer scope
|
||||
_createSettingsEntry: function (elementTypeKey) {
|
||||
var settings = {
|
||||
contentTypeKey: elementTypeKey,
|
||||
udi: udiService.create("element")
|
||||
};
|
||||
this.value.settingsData.push(settings);
|
||||
return settings.udi;
|
||||
},
|
||||
|
||||
// private
|
||||
// TODO: Then this can just be a method in the outer scope
|
||||
_getSettingsByUdi: function (udi) {
|
||||
return this.value.settingsData.find(entry => entry.udi === udi) || null;
|
||||
},
|
||||
|
||||
/**
|
||||
* @ngdoc method
|
||||
* @name removeSettingsByUdi
|
||||
* @methodOf umbraco.services.blockEditorModelObject
|
||||
* @description Removes the settings data of a given UDI.
|
||||
* Notice this method does not remove the settingsUdi from your layout, this will need to be handled by the Property Editor since this services don't know about your layout structure.
|
||||
* @param {string} udi The UDI of the settings data to be removed.
|
||||
*/
|
||||
removeSettingsByUdi: function (udi) {
|
||||
const index = this.value.settingsData.findIndex(o => o.udi === udi);
|
||||
if (index !== -1) {
|
||||
this.value.settingsData.splice(index, 1);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -697,13 +748,13 @@
|
||||
* @methodOf umbraco.services.blockEditorModelObject
|
||||
* @description Notice you should not need to destroy the BlockEditorModelObject since it will automaticly be destroyed when the scope of existance gets destroyed.
|
||||
*/
|
||||
destroy: function() {
|
||||
destroy: function () {
|
||||
|
||||
this.__watchers.forEach(w => { w(); });
|
||||
for (const key in this.isolatedScope.blockObjects) {
|
||||
this.destroyBlockObject(this.isolatedScope.blockObjects[key]);
|
||||
}
|
||||
|
||||
|
||||
delete this.__watchers;
|
||||
delete this.value;
|
||||
delete this.propertyEditorAlias;
|
||||
|
||||
+25
-4
@@ -132,7 +132,13 @@
|
||||
|
||||
// Called when we save the value, the server may return an updated data and our value is re-synced
|
||||
// we need to deal with that here so that our model values are all in sync so we basically re-initialize.
|
||||
function onServerValueChanged(newVal, oldVal) {
|
||||
function onServerValueChanged(newVal, oldVal) {
|
||||
|
||||
// We need to ensure that the property model value is an object, this is needed for modelObject to recive a reference and keep that updated.
|
||||
if (typeof newVal !== 'object' || newVal === null) {// testing if we have null or undefined value or if the value is set to another type than Object.
|
||||
newVal = {};
|
||||
}
|
||||
|
||||
modelObject.update(newVal, $scope);
|
||||
onLoaded();
|
||||
}
|
||||
@@ -148,6 +154,8 @@
|
||||
// Store a reference to the layout model, because we need to maintain this model.
|
||||
vm.layout = modelObject.getLayout([]);
|
||||
|
||||
var invalidLayoutItems = [];
|
||||
|
||||
// Append the blockObjects to our layout.
|
||||
vm.layout.forEach(entry => {
|
||||
// $block must have the data property to be a valid BlockObject, if not its considered as a destroyed blockObject.
|
||||
@@ -155,9 +163,22 @@
|
||||
var block = getBlockObject(entry);
|
||||
|
||||
// If this entry was not supported by our property-editor it would return 'null'.
|
||||
if(block !== null) {
|
||||
if (block !== null) {
|
||||
entry.$block = block;
|
||||
}
|
||||
else {
|
||||
// then we need to filter this out and also update the underlying model. This could happen if the data
|
||||
// is invalid for some reason or the data structure has changed.
|
||||
invalidLayoutItems.push(entry);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// remove the ones that are invalid
|
||||
invalidLayoutItems.forEach(entry => {
|
||||
var index = vm.layout.findIndex(x => x === entry);
|
||||
if (index >= 0) {
|
||||
vm.layout.splice(index, 1);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -240,7 +261,7 @@
|
||||
|
||||
function deleteBlock(block) {
|
||||
|
||||
var layoutIndex = vm.layout.findIndex(entry => entry.udi === block.content.udi);
|
||||
var layoutIndex = vm.layout.findIndex(entry => entry.contentUdi === block.content.udi);
|
||||
if (layoutIndex === -1) {
|
||||
throw new Error("Could not find layout entry of block with udi: "+block.content.udi)
|
||||
}
|
||||
@@ -250,7 +271,7 @@
|
||||
var removed = vm.layout.splice(layoutIndex, 1);
|
||||
removed.forEach(x => {
|
||||
// remove any server validation errors associated
|
||||
var guid = udiService.getKey(x.udi);
|
||||
var guid = udiService.getKey(x.contentUdi);
|
||||
serverValidationManager.removePropertyError(guid, vm.umbProperty.property.culture, vm.umbProperty.property.segment, "", { matchType: "contains" });
|
||||
});
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
describe('blockEditorService tests', function () {
|
||||
|
||||
var key = "6A1F5BDD-67EF-4173-B061-D6348ED07094";
|
||||
var udi = "umb://element/6A1F5BDD67EF4173B061D6348ED07094";
|
||||
var contentKey = "6A1F5BDD-67EF-4173-B061-D6348ED07094";
|
||||
var contentUdi = "umb://element/6A1F5BDD67EF4173B061D6348ED07094";
|
||||
var settingsKey = "2AF42343-C8A2-400D-BA43-4818C2B3CDC5";
|
||||
var settingsUdi = "umb://element/2AF42343C8A2400DBA434818C2B3CDC5";
|
||||
|
||||
var blockEditorService, contentResource, $rootScope, $scope;
|
||||
|
||||
@@ -9,7 +11,7 @@
|
||||
beforeEach(module('umbraco.resources'));
|
||||
beforeEach(module('umbraco.mocks'));
|
||||
beforeEach(module('umbraco'));
|
||||
|
||||
|
||||
beforeEach(inject(function ($injector, mocksUtils, _$rootScope_) {
|
||||
|
||||
mocksUtils.disableAuth();
|
||||
@@ -20,7 +22,7 @@
|
||||
contentResource = $injector.get("contentResource");
|
||||
spyOn(contentResource, "getScaffoldByKey").and.callFake(
|
||||
function () {
|
||||
return Promise.resolve(mocksUtils.getMockVariantContent(1234, key, udi))
|
||||
return Promise.resolve(mocksUtils.getMockVariantContent(1234, contentKey, contentUdi))
|
||||
}
|
||||
);
|
||||
|
||||
@@ -29,27 +31,53 @@
|
||||
}));
|
||||
|
||||
|
||||
var blockConfigurationMock = { contentTypeKey: "7C5B74D1-E2F9-45A3-AE4B-FC7A829BF8AB", label:"Test label", settingsElementTypeKey: null, view: "testview.html"};
|
||||
var blockConfigurationMock = { contentTypeKey: "7C5B74D1-E2F9-45A3-AE4B-FC7A829BF8AB", label: "Test label", settingsElementTypeKey: null, view: "testview.html" };
|
||||
|
||||
var propertyModelMock = {
|
||||
layout: {
|
||||
"Umbraco.TestBlockEditor": [
|
||||
{
|
||||
udi: udi
|
||||
contentUdi: contentUdi
|
||||
}
|
||||
]
|
||||
},
|
||||
data: [
|
||||
contentData: [
|
||||
{
|
||||
udi: udi,
|
||||
udi: contentUdi,
|
||||
contentTypeKey: "7C5B74D1-E2F9-45A3-AE4B-FC7A829BF8AB",
|
||||
testproperty: "myTestValue"
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
var blockWithSettingsConfigurationMock = { contentTypeKey: "7C5B74D1-E2F9-45A3-AE4B-FC7A829BF8AB", label: "Test label", settingsElementTypeKey: "7C5B74D1-E2F9-45A3-AE4B-FC7A829BF8AB", view: "testview.html" };
|
||||
var propertyModelWithSettingsMock = {
|
||||
layout: {
|
||||
"Umbraco.TestBlockEditor": [
|
||||
{
|
||||
contentUdi: contentUdi,
|
||||
settingsUdi: settingsUdi
|
||||
}
|
||||
]
|
||||
},
|
||||
contentData: [
|
||||
{
|
||||
udi: contentUdi,
|
||||
contentTypeKey: "7C5B74D1-E2F9-45A3-AE4B-FC7A829BF8AB",
|
||||
testproperty: "myTestValue"
|
||||
}
|
||||
],
|
||||
settingsData: [
|
||||
{
|
||||
udi: settingsUdi,
|
||||
contentTypeKey: "7C5B74D1-E2F9-45A3-AE4B-FC7A829BF8AB",
|
||||
testproperty: "myTestValueForSettings"
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
describe('init blockEditorModelObject', function () {
|
||||
|
||||
|
||||
it('fail if no model value', function () {
|
||||
function createWithNoModelValue() {
|
||||
blockEditorService.createModelObject(null, "Umbraco.TestBlockEditor", [], $scope, $scope);
|
||||
@@ -66,29 +94,29 @@
|
||||
|
||||
it('getBlockConfiguration provide the requested block configurtion', function () {
|
||||
var modelObject = blockEditorService.createModelObject({}, "Umbraco.TestBlockEditor", [blockConfigurationMock], $scope, $scope);
|
||||
|
||||
|
||||
expect(modelObject.getBlockConfiguration(blockConfigurationMock.contentTypeKey).label).toBe(blockConfigurationMock.label);
|
||||
});
|
||||
|
||||
it('load provides data for itemPicker', function (done) {
|
||||
var modelObject = blockEditorService.createModelObject({}, "Umbraco.TestBlockEditor", [blockConfigurationMock], $scope, $scope);
|
||||
|
||||
|
||||
modelObject.load().then(() => {
|
||||
var itemPickerOptions = modelObject.getAvailableBlocksForBlockPicker();
|
||||
expect(itemPickerOptions.length).toBe(1);
|
||||
expect(itemPickerOptions[0].blockConfigModel.contentTypeKey).toBe(blockConfigurationMock.contentTypeKey);
|
||||
done();
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
|
||||
it('getLayoutEntry has values', function (done) {
|
||||
|
||||
|
||||
|
||||
var modelObject = blockEditorService.createModelObject(propertyModelMock, "Umbraco.TestBlockEditor", [blockConfigurationMock], $scope, $scope);
|
||||
|
||||
|
||||
modelObject.load().then(() => {
|
||||
|
||||
|
||||
var layout = modelObject.getLayout();
|
||||
|
||||
expect(layout).not.toBeUndefined();
|
||||
@@ -98,42 +126,42 @@
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
|
||||
it('getBlockObject has values', function (done) {
|
||||
|
||||
|
||||
|
||||
var modelObject = blockEditorService.createModelObject(propertyModelMock, "Umbraco.TestBlockEditor", [blockConfigurationMock], $scope, $scope);
|
||||
|
||||
|
||||
modelObject.load().then(() => {
|
||||
|
||||
|
||||
try {
|
||||
var layout = modelObject.getLayout();
|
||||
|
||||
var blockObject = modelObject.getBlockObject(layout[0]);
|
||||
|
||||
expect(blockObject).not.toBeUndefined();
|
||||
expect(blockObject.data.udi).toBe(propertyModelMock.data[0].udi);
|
||||
expect(blockObject.content.variants[0].tabs[0].properties[0].value).toBe(propertyModelMock.data[0].testproperty);
|
||||
expect(blockObject.data.udi).toBe(propertyModelMock.contentData[0].udi);
|
||||
expect(blockObject.content.variants[0].tabs[0].properties[0].value).toBe(propertyModelMock.contentData[0].testproperty);
|
||||
|
||||
done();
|
||||
} catch (e) {
|
||||
done.fail(e);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
||||
it('getBlockObject syncs primitive values', function (done) {
|
||||
|
||||
var propertyModel = angular.copy(propertyModelMock);
|
||||
|
||||
var modelObject = blockEditorService.createModelObject(propertyModel, "Umbraco.TestBlockEditor", [blockConfigurationMock], $scope, $scope);
|
||||
|
||||
|
||||
modelObject.load().then(() => {
|
||||
|
||||
|
||||
try {
|
||||
var layout = modelObject.getLayout();
|
||||
|
||||
@@ -143,31 +171,31 @@
|
||||
|
||||
$rootScope.$digest();// invoke angularJS Store.
|
||||
|
||||
expect(blockObject.data).toBe(propertyModel.data[0]);
|
||||
expect(blockObject.data).toEqual(propertyModel.contentData[0]);
|
||||
expect(blockObject.data.testproperty).toBe("anotherTestValue");
|
||||
expect(propertyModel.data[0].testproperty).toBe("anotherTestValue");
|
||||
expect(propertyModel.contentData[0].testproperty).toBe("anotherTestValue");
|
||||
|
||||
done();
|
||||
} catch (e) {
|
||||
done.fail(e);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
||||
it('getBlockObject syncs values of object', function (done) {
|
||||
|
||||
var propertyModel = angular.copy(propertyModelMock);
|
||||
|
||||
var complexValue = {"list": ["A", "B", "C"]};
|
||||
propertyModel.data[0].testproperty = complexValue;
|
||||
var complexValue = { "list": ["A", "B", "C"] };
|
||||
propertyModel.contentData[0].testproperty = complexValue;
|
||||
|
||||
|
||||
var modelObject = blockEditorService.createModelObject(propertyModel, "Umbraco.TestBlockEditor", [blockConfigurationMock], $scope, $scope);
|
||||
|
||||
|
||||
modelObject.load().then(() => {
|
||||
|
||||
|
||||
try {
|
||||
var layout = modelObject.getLayout();
|
||||
|
||||
@@ -178,15 +206,15 @@
|
||||
|
||||
$rootScope.$digest();// invoke angularJS Store.
|
||||
|
||||
expect(propertyModel.data[0].testproperty.list[0]).toBe("AA");
|
||||
expect(propertyModel.data[0].testproperty.list.length).toBe(4);
|
||||
expect(propertyModel.contentData[0].testproperty.list[0]).toBe("AA");
|
||||
expect(propertyModel.contentData[0].testproperty.list.length).toBe(4);
|
||||
|
||||
done();
|
||||
} catch (e) {
|
||||
done.fail(e);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
it('layout is referencing layout of propertyModel', function (done) {
|
||||
@@ -194,9 +222,9 @@
|
||||
var propertyModel = angular.copy(propertyModelMock);
|
||||
|
||||
var modelObject = blockEditorService.createModelObject(propertyModel, "Umbraco.TestBlockEditor", [blockConfigurationMock], $scope, $scope);
|
||||
|
||||
|
||||
modelObject.load().then(() => {
|
||||
|
||||
|
||||
var layout = modelObject.getLayout();
|
||||
|
||||
// remove from layout;
|
||||
@@ -207,7 +235,7 @@
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
it('removeDataAndDestroyModel removes data', function (done) {
|
||||
@@ -215,9 +243,9 @@
|
||||
var propertyModel = angular.copy(propertyModelMock);
|
||||
|
||||
var modelObject = blockEditorService.createModelObject(propertyModel, "Umbraco.TestBlockEditor", [blockConfigurationMock], $scope, $scope);
|
||||
|
||||
|
||||
modelObject.load().then(() => {
|
||||
|
||||
|
||||
try {
|
||||
var layout = modelObject.getLayout();
|
||||
|
||||
@@ -232,8 +260,8 @@
|
||||
// remove from data;
|
||||
modelObject.removeDataAndDestroyModel(blockObject);
|
||||
|
||||
expect(propertyModel.data.length).toBe(0);
|
||||
expect(propertyModel.data[0]).toBeUndefined();
|
||||
expect(propertyModel.contentData.length).toBe(0);
|
||||
expect(propertyModel.contentData[0]).toBeUndefined();
|
||||
expect(propertyModel.layout["Umbraco.TestBlockEditor"].length).toBe(0);
|
||||
expect(propertyModel.layout["Umbraco.TestBlockEditor"][0]).toBeUndefined();
|
||||
|
||||
@@ -242,10 +270,105 @@
|
||||
done.fail(e);
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
it('getBlockObject of block with settings has values', function (done) {
|
||||
|
||||
var propertyModel = angular.copy(propertyModelWithSettingsMock);
|
||||
|
||||
var modelObject = blockEditorService.createModelObject(propertyModel, "Umbraco.TestBlockEditor", [blockWithSettingsConfigurationMock], $scope, $scope);
|
||||
|
||||
modelObject.load().then(() => {
|
||||
|
||||
var layout = modelObject.getLayout();
|
||||
|
||||
var blockObject = modelObject.getBlockObject(layout[0]);
|
||||
|
||||
expect(blockObject).not.toBeUndefined();
|
||||
expect(blockObject.data.udi).toBe(propertyModel.contentData[0].udi);
|
||||
expect(blockObject.content.variants[0].tabs[0].properties[0].value).toBe(propertyModel.contentData[0].testproperty);
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
it('getBlockObject of block with settings syncs primative values', function (done) {
|
||||
|
||||
var propertyModel = angular.copy(propertyModelWithSettingsMock);
|
||||
|
||||
var modelObject = blockEditorService.createModelObject(propertyModel, "Umbraco.TestBlockEditor", [blockWithSettingsConfigurationMock], $scope, $scope);
|
||||
|
||||
modelObject.load().then(() => {
|
||||
|
||||
var layout = modelObject.getLayout();
|
||||
|
||||
var blockObject = modelObject.getBlockObject(layout[0]);
|
||||
|
||||
blockObject.content.variants[0].tabs[0].properties[0].value = "anotherTestValue";
|
||||
blockObject.settings.variants[0].tabs[0].properties[0].value = "anotherTestValueForSettings";
|
||||
|
||||
$rootScope.$digest();// invoke angularJS Store.
|
||||
|
||||
expect(blockObject.data).toEqual(propertyModel.contentData[0]);
|
||||
expect(blockObject.data.testproperty).toBe("anotherTestValue");
|
||||
expect(propertyModel.contentData[0].testproperty).toBe("anotherTestValue");
|
||||
|
||||
expect(blockObject.settingsData).toEqual(propertyModel.settingsData[0]);
|
||||
expect(blockObject.settingsData.testproperty).toBe("anotherTestValueForSettings");
|
||||
expect(propertyModel.settingsData[0].testproperty).toBe("anotherTestValueForSettings");
|
||||
|
||||
//
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
it('getBlockObject of block with settings syncs values of object', function (done) {
|
||||
|
||||
var propertyModel = angular.copy(propertyModelWithSettingsMock);
|
||||
|
||||
var complexValue = { "list": ["A", "B", "C"] };
|
||||
propertyModel.contentData[0].testproperty = complexValue;
|
||||
|
||||
var complexSettingsValue = { "list": ["A", "B", "C"] };
|
||||
propertyModel.settingsData[0].testproperty = complexSettingsValue;
|
||||
|
||||
var modelObject = blockEditorService.createModelObject(propertyModel, "Umbraco.TestBlockEditor", [blockWithSettingsConfigurationMock], $scope, $scope);
|
||||
|
||||
modelObject.load().then(() => {
|
||||
|
||||
try {
|
||||
var layout = modelObject.getLayout();
|
||||
|
||||
var blockObject = modelObject.getBlockObject(layout[0]);
|
||||
|
||||
blockObject.content.variants[0].tabs[0].properties[0].value.list[0] = "AA";
|
||||
blockObject.content.variants[0].tabs[0].properties[0].value.list.push("D");
|
||||
|
||||
blockObject.settings.variants[0].tabs[0].properties[0].value.list[0] = "settingsValue";
|
||||
blockObject.settings.variants[0].tabs[0].properties[0].value.list.push("settingsNewValue");
|
||||
|
||||
$rootScope.$digest();// invoke angularJS Store.
|
||||
|
||||
expect(propertyModel.contentData[0].testproperty.list[0]).toBe("AA");
|
||||
expect(propertyModel.contentData[0].testproperty.list.length).toBe(4);
|
||||
|
||||
expect(propertyModel.settingsData[0].testproperty.list[0]).toBe("settingsValue");
|
||||
expect(propertyModel.settingsData[0].testproperty.list.length).toBe(4);
|
||||
|
||||
done();
|
||||
} catch (e) {
|
||||
done.fail(e);
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -11,6 +11,7 @@ using Umbraco.Core.Models.Editors;
|
||||
using Umbraco.Core.PropertyEditors;
|
||||
using Umbraco.Core.Services;
|
||||
using static Umbraco.Core.Models.Blocks.BlockEditorData;
|
||||
using static Umbraco.Core.Models.Blocks.BlockItemData;
|
||||
|
||||
namespace Umbraco.Web.PropertyEditors
|
||||
{
|
||||
@@ -63,8 +64,12 @@ namespace Umbraco.Web.PropertyEditors
|
||||
var rawJson = value == null ? string.Empty : value is string str ? str : value.ToString();
|
||||
|
||||
var result = new List<UmbracoEntityReference>();
|
||||
var blockEditorData = _blockEditorValues.DeserializeAndClean(rawJson);
|
||||
if (blockEditorData == null)
|
||||
return Enumerable.Empty<UmbracoEntityReference>();
|
||||
|
||||
foreach (var row in _blockEditorValues.GetPropertyValues(rawJson))
|
||||
// TODO: What about Settings?
|
||||
foreach (var row in blockEditorData.BlockValue.ContentData)
|
||||
{
|
||||
foreach (var prop in row.PropertyValues)
|
||||
{
|
||||
@@ -83,6 +88,132 @@ namespace Umbraco.Web.PropertyEditors
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
#region Convert database // editor
|
||||
|
||||
// note: there is NO variant support here
|
||||
|
||||
/// <summary>
|
||||
/// Ensure that sub-editor values are translated through their ToEditor methods
|
||||
/// </summary>
|
||||
/// <param name="property"></param>
|
||||
/// <param name="dataTypeService"></param>
|
||||
/// <param name="culture"></param>
|
||||
/// <param name="segment"></param>
|
||||
/// <returns></returns>
|
||||
public override object ToEditor(Property property, IDataTypeService dataTypeService, string culture = null, string segment = null)
|
||||
{
|
||||
var val = property.GetValue(culture, segment);
|
||||
|
||||
BlockEditorData blockEditorData;
|
||||
try
|
||||
{
|
||||
blockEditorData = _blockEditorValues.DeserializeAndClean(val);
|
||||
}
|
||||
catch (JsonSerializationException)
|
||||
{
|
||||
// if this occurs it means the data is invalid, shouldn't happen but has happened if we change the data format.
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
if (blockEditorData == null || blockEditorData.BlockValue.ContentData.Count == 0)
|
||||
return string.Empty;
|
||||
|
||||
foreach (var row in blockEditorData.BlockValue.ContentData)
|
||||
{
|
||||
foreach (var prop in row.PropertyValues)
|
||||
{
|
||||
try
|
||||
{
|
||||
// create a temp property with the value
|
||||
// - force it to be culture invariant as the block editor can't handle culture variant element properties
|
||||
prop.Value.PropertyType.Variations = ContentVariation.Nothing;
|
||||
var tempProp = new Property(prop.Value.PropertyType);
|
||||
|
||||
tempProp.SetValue(prop.Value.Value);
|
||||
|
||||
// convert that temp property, and store the converted value
|
||||
var propEditor = _propertyEditors[prop.Value.PropertyType.PropertyEditorAlias];
|
||||
if (propEditor == null)
|
||||
{
|
||||
// NOTE: This logic was borrowed from Nested Content and I'm unsure why it exists.
|
||||
// if the property editor doesn't exist I think everything will break anyways?
|
||||
// update the raw value since this is what will get serialized out
|
||||
row.RawPropertyValues[prop.Key] = tempProp.GetValue()?.ToString();
|
||||
continue;
|
||||
}
|
||||
|
||||
var tempConfig = dataTypeService.GetDataType(prop.Value.PropertyType.DataTypeId).Configuration;
|
||||
var valEditor = propEditor.GetValueEditor(tempConfig);
|
||||
var convValue = valEditor.ToEditor(tempProp, dataTypeService);
|
||||
|
||||
// update the raw value since this is what will get serialized out
|
||||
row.RawPropertyValues[prop.Key] = convValue;
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
// deal with weird situations by ignoring them (no comment)
|
||||
row.PropertyValues.Remove(prop.Key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// return json convertable object
|
||||
return blockEditorData.BlockValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensure that sub-editor values are translated through their FromEditor methods
|
||||
/// </summary>
|
||||
/// <param name="editorValue"></param>
|
||||
/// <param name="currentValue"></param>
|
||||
/// <returns></returns>
|
||||
public override object FromEditor(ContentPropertyData editorValue, object currentValue)
|
||||
{
|
||||
if (editorValue.Value == null || string.IsNullOrWhiteSpace(editorValue.Value.ToString()))
|
||||
return null;
|
||||
|
||||
BlockEditorData blockEditorData;
|
||||
try
|
||||
{
|
||||
blockEditorData = _blockEditorValues.DeserializeAndClean(editorValue.Value);
|
||||
}
|
||||
catch (JsonSerializationException)
|
||||
{
|
||||
// if this occurs it means the data is invalid, shouldn't happen but has happened if we change the data format.
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
if (blockEditorData == null || blockEditorData.BlockValue.ContentData.Count == 0)
|
||||
return string.Empty;
|
||||
|
||||
foreach (var row in blockEditorData.BlockValue.ContentData)
|
||||
{
|
||||
foreach (var prop in row.PropertyValues)
|
||||
{
|
||||
// Fetch the property types prevalue
|
||||
var propConfiguration = _dataTypeService.GetDataType(prop.Value.PropertyType.DataTypeId).Configuration;
|
||||
|
||||
// Lookup the property editor
|
||||
var propEditor = _propertyEditors[prop.Value.PropertyType.PropertyEditorAlias];
|
||||
if (propEditor == null) continue;
|
||||
|
||||
// Create a fake content property data object
|
||||
var contentPropData = new ContentPropertyData(prop.Value.Value, propConfiguration);
|
||||
|
||||
// Get the property editor to do it's conversion
|
||||
var newValue = propEditor.GetValueEditor().FromEditor(contentPropData, prop.Value.Value);
|
||||
|
||||
// update the raw value since this is what will get serialized out
|
||||
row.RawPropertyValues[prop.Key] = newValue;
|
||||
}
|
||||
}
|
||||
|
||||
// return json
|
||||
return JsonConvert.SerializeObject(blockEditorData.BlockValue);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
internal class BlockEditorValidator : ComplexEditorValidator
|
||||
@@ -96,19 +227,26 @@ namespace Umbraco.Web.PropertyEditors
|
||||
|
||||
protected override IEnumerable<ElementTypeValidationModel> GetElementTypeValidation(object value)
|
||||
{
|
||||
foreach (var row in _blockEditorValues.GetPropertyValues(value))
|
||||
var blockEditorData = _blockEditorValues.DeserializeAndClean(value);
|
||||
if (blockEditorData != null)
|
||||
{
|
||||
var elementValidation = new ElementTypeValidationModel(row.ContentTypeAlias, row.Id);
|
||||
foreach (var prop in row.PropertyValues)
|
||||
foreach (var row in blockEditorData.BlockValue.ContentData)
|
||||
{
|
||||
elementValidation.AddPropertyTypeValidation(
|
||||
new PropertyTypeValidationModel(prop.Value.PropertyType, prop.Value.Value));
|
||||
var elementValidation = new ElementTypeValidationModel(row.ContentTypeAlias, row.Key);
|
||||
foreach (var prop in row.PropertyValues)
|
||||
{
|
||||
elementValidation.AddPropertyTypeValidation(
|
||||
new PropertyTypeValidationModel(prop.Value.PropertyType, prop.Value.Value));
|
||||
}
|
||||
yield return elementValidation;
|
||||
}
|
||||
yield return elementValidation;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Used to deserialize json values and clean up any values based on the existence of element types and layout structure
|
||||
/// </summary>
|
||||
internal class BlockEditorValues
|
||||
{
|
||||
private readonly Lazy<Dictionary<Guid, IContentType>> _contentTypes;
|
||||
@@ -126,81 +264,77 @@ namespace Umbraco.Web.PropertyEditors
|
||||
return contentType;
|
||||
}
|
||||
|
||||
public IReadOnlyList<BlockValue> GetPropertyValues(object propertyValue)
|
||||
public BlockEditorData DeserializeAndClean(object propertyValue)
|
||||
{
|
||||
if (propertyValue == null || string.IsNullOrWhiteSpace(propertyValue.ToString()))
|
||||
return new List<BlockValue>();
|
||||
return null;
|
||||
|
||||
var converted = _dataConverter.Convert(propertyValue.ToString());
|
||||
var blockEditorData = _dataConverter.Deserialize(propertyValue.ToString());
|
||||
|
||||
if (converted.Blocks.Count == 0)
|
||||
return new List<BlockValue>();
|
||||
|
||||
var contentTypePropertyTypes = new Dictionary<string, Dictionary<string, PropertyType>>();
|
||||
var result = new List<BlockValue>();
|
||||
|
||||
foreach(var block in converted.Blocks)
|
||||
if (blockEditorData.BlockValue.ContentData.Count == 0)
|
||||
{
|
||||
var contentType = GetElementType(block);
|
||||
if (contentType == null)
|
||||
continue;
|
||||
|
||||
// get the prop types for this content type but keep a dictionary of found ones so we don't have to keep re-looking and re-creating
|
||||
// objects on each iteration.
|
||||
if (!contentTypePropertyTypes.TryGetValue(contentType.Alias, out var propertyTypes))
|
||||
propertyTypes = contentTypePropertyTypes[contentType.Alias] = contentType.CompositionPropertyTypes.ToDictionary(x => x.Alias, x => x);
|
||||
|
||||
var propValues = new Dictionary<string, BlockPropertyValue>();
|
||||
|
||||
// find any keys that are not real property types and remove them
|
||||
foreach (var prop in block.RawPropertyValues.ToList())
|
||||
{
|
||||
// doesn't exist so remove it
|
||||
if (!propertyTypes.TryGetValue(prop.Key, out var propType))
|
||||
{
|
||||
block.RawPropertyValues.Remove(prop.Key);
|
||||
}
|
||||
else
|
||||
{
|
||||
// set the value to include the resolved property type
|
||||
propValues[prop.Key] = new BlockPropertyValue
|
||||
{
|
||||
PropertyType = propType,
|
||||
Value = prop.Value
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
result.Add(new BlockValue
|
||||
{
|
||||
ContentTypeAlias = contentType.Alias,
|
||||
PropertyValues = propValues,
|
||||
Id = ((GuidUdi)block.Udi).Guid
|
||||
});
|
||||
// if there's no content ensure there's no settings too
|
||||
blockEditorData.BlockValue.SettingsData.Clear();
|
||||
return null;
|
||||
}
|
||||
|
||||
return result;
|
||||
var contentTypePropertyTypes = new Dictionary<string, Dictionary<string, PropertyType>>();
|
||||
|
||||
// filter out any content that isn't referenced in the layout references
|
||||
foreach(var block in blockEditorData.BlockValue.ContentData.Where(x => blockEditorData.References.Any(r => r.ContentUdi == x.Udi)))
|
||||
{
|
||||
ResolveBlockItemData(block, contentTypePropertyTypes);
|
||||
}
|
||||
// filter out any settings that isn't referenced in the layout references
|
||||
foreach (var block in blockEditorData.BlockValue.SettingsData.Where(x => blockEditorData.References.Any(r => r.SettingsUdi == x.Udi)))
|
||||
{
|
||||
ResolveBlockItemData(block, contentTypePropertyTypes);
|
||||
}
|
||||
|
||||
// remove blocks that couldn't be resolved
|
||||
blockEditorData.BlockValue.ContentData.RemoveAll(x => x.ContentTypeAlias.IsNullOrWhiteSpace());
|
||||
blockEditorData.BlockValue.SettingsData.RemoveAll(x => x.ContentTypeAlias.IsNullOrWhiteSpace());
|
||||
|
||||
return blockEditorData;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Used during deserialization to populate the property value/property type of a nested content row property
|
||||
/// </summary>
|
||||
internal class BlockPropertyValue
|
||||
private bool ResolveBlockItemData(BlockItemData block, Dictionary<string, Dictionary<string, PropertyType>> contentTypePropertyTypes)
|
||||
{
|
||||
public object Value { get; set; }
|
||||
public PropertyType PropertyType { get; set; }
|
||||
}
|
||||
var contentType = GetElementType(block);
|
||||
if (contentType == null)
|
||||
return false;
|
||||
|
||||
/// <summary>
|
||||
/// Used during deserialization to populate the content type alias and property values of a block
|
||||
/// </summary>
|
||||
internal class BlockValue
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public string ContentTypeAlias { get; set; }
|
||||
public IDictionary<string, BlockPropertyValue> PropertyValues { get; set; } = new Dictionary<string, BlockPropertyValue>();
|
||||
}
|
||||
// get the prop types for this content type but keep a dictionary of found ones so we don't have to keep re-looking and re-creating
|
||||
// objects on each iteration.
|
||||
if (!contentTypePropertyTypes.TryGetValue(contentType.Alias, out var propertyTypes))
|
||||
propertyTypes = contentTypePropertyTypes[contentType.Alias] = contentType.CompositionPropertyTypes.ToDictionary(x => x.Alias, x => x);
|
||||
|
||||
var propValues = new Dictionary<string, BlockPropertyValue>();
|
||||
|
||||
// find any keys that are not real property types and remove them
|
||||
foreach (var prop in block.RawPropertyValues.ToList())
|
||||
{
|
||||
// doesn't exist so remove it
|
||||
if (!propertyTypes.TryGetValue(prop.Key, out var propType))
|
||||
{
|
||||
block.RawPropertyValues.Remove(prop.Key);
|
||||
}
|
||||
else
|
||||
{
|
||||
// set the value to include the resolved property type
|
||||
propValues[prop.Key] = new BlockPropertyValue
|
||||
{
|
||||
PropertyType = propType,
|
||||
Value = prop.Value
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
block.ContentTypeAlias = contentType.Alias;
|
||||
block.PropertyValues = propValues;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -27,11 +27,12 @@ namespace Umbraco.Web.PropertyEditors
|
||||
[JsonProperty("thumbnail")]
|
||||
public string Thumbnail { get; set; }
|
||||
|
||||
// TODO: This is named inconsistently in JS but renaming it needs to be done in quite a lot of places, this should be contentElementTypeKey
|
||||
[JsonProperty("contentTypeKey")]
|
||||
public Guid Key { get; set; }
|
||||
public Guid ContentElementTypeKey { get; set; }
|
||||
|
||||
[JsonProperty("settingsElementTypeKey")]
|
||||
public string SettingsElementTypeKey { get; set; }
|
||||
public Guid? SettingsElementTypeKey { get; set; }
|
||||
|
||||
[JsonProperty("view")]
|
||||
public string View { get; set; }
|
||||
|
||||
@@ -133,13 +133,20 @@ namespace Umbraco.Web.PropertyEditors
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
|
||||
|
||||
#region Convert database // editor
|
||||
|
||||
// note: there is NO variant support here
|
||||
|
||||
// TODO: What does this do?
|
||||
/// <summary>
|
||||
/// Ensure that sub-editor values are translated through their ToEditor methods
|
||||
/// </summary>
|
||||
/// <param name="property"></param>
|
||||
/// <param name="dataTypeService"></param>
|
||||
/// <param name="culture"></param>
|
||||
/// <param name="segment"></param>
|
||||
/// <returns></returns>
|
||||
public override object ToEditor(Property property, IDataTypeService dataTypeService, string culture = null, string segment = null)
|
||||
{
|
||||
var val = property.GetValue(culture, segment);
|
||||
@@ -186,11 +193,16 @@ namespace Umbraco.Web.PropertyEditors
|
||||
}
|
||||
}
|
||||
|
||||
// return json
|
||||
// return the object, there's a native json converter for this so it will serialize correctly
|
||||
return rows;
|
||||
}
|
||||
|
||||
// TODO: What does this do?
|
||||
/// <summary>
|
||||
/// Ensure that sub-editor values are translated through their FromEditor methods
|
||||
/// </summary>
|
||||
/// <param name="editorValue"></param>
|
||||
/// <param name="currentValue"></param>
|
||||
/// <returns></returns>
|
||||
public override object FromEditor(ContentPropertyData editorValue, object currentValue)
|
||||
{
|
||||
if (editorValue.Value == null || string.IsNullOrWhiteSpace(editorValue.Value.ToString()))
|
||||
|
||||
@@ -24,7 +24,7 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters
|
||||
}
|
||||
|
||||
public IPublishedElement ConvertToElement(
|
||||
BlockEditorData.BlockItemData data,
|
||||
BlockItemData data,
|
||||
PropertyCacheLevel referenceCacheLevel, bool preview)
|
||||
{
|
||||
// hack! we need to cast, we have no choice beacuse we cannot make breaking changes.
|
||||
|
||||
+46
-24
@@ -18,11 +18,13 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters
|
||||
{
|
||||
private readonly IProfilingLogger _proflog;
|
||||
private readonly BlockEditorConverter _blockConverter;
|
||||
private readonly BlockListEditorDataConverter _blockListEditorDataConverter;
|
||||
|
||||
public BlockListPropertyValueConverter(IProfilingLogger proflog, BlockEditorConverter blockConverter)
|
||||
{
|
||||
_proflog = proflog;
|
||||
_blockConverter = blockConverter;
|
||||
_blockListEditorDataConverter = new BlockListEditorDataConverter();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -50,59 +52,79 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters
|
||||
using (_proflog.DebugDuration<BlockListPropertyValueConverter>($"ConvertPropertyToBlockList ({propertyType.DataType.Id})"))
|
||||
{
|
||||
var configuration = propertyType.DataType.ConfigurationAs<BlockListConfiguration>();
|
||||
var contentTypes = configuration.Blocks;
|
||||
var contentTypeMap = contentTypes.ToDictionary(x => x.Key);
|
||||
var blockConfigMap = configuration.Blocks.ToDictionary(x => x.ContentElementTypeKey);
|
||||
var validSettingElementTypes = blockConfigMap.Values.Select(x => x.SettingsElementTypeKey).Where(x => x.HasValue).Distinct().ToList();
|
||||
|
||||
var elements = new Dictionary<Guid, IPublishedElement>();
|
||||
var contentPublishedElements = new Dictionary<Guid, IPublishedElement>();
|
||||
var settingsPublishedElements = new Dictionary<Guid, IPublishedElement>();
|
||||
|
||||
var layout = new List<BlockListLayoutReference>();
|
||||
var model = new BlockListModel(elements.Values, layout);
|
||||
|
||||
var value = (string)inter;
|
||||
if (string.IsNullOrWhiteSpace(value)) return model;
|
||||
if (string.IsNullOrWhiteSpace(value)) return BlockListModel.Empty;
|
||||
|
||||
var converter = new BlockListEditorDataConverter();
|
||||
var converted = converter.Convert(value);
|
||||
if (converted.Blocks.Count == 0) return model;
|
||||
var converted = _blockListEditorDataConverter.Deserialize(value);
|
||||
if (converted.BlockValue.ContentData.Count == 0) return BlockListModel.Empty;
|
||||
|
||||
var blockListLayout = converted.Layout.ToObject<IEnumerable<BlockListLayoutItem>>();
|
||||
|
||||
// parse the data elements
|
||||
foreach (var data in converted.Blocks)
|
||||
// convert the content data
|
||||
foreach (var data in converted.BlockValue.ContentData)
|
||||
{
|
||||
if (!blockConfigMap.ContainsKey(data.ContentTypeKey)) continue;
|
||||
|
||||
var element = _blockConverter.ConvertToElement(data, referenceCacheLevel, preview);
|
||||
if (element == null) continue;
|
||||
elements[element.Key] = element;
|
||||
contentPublishedElements[element.Key] = element;
|
||||
}
|
||||
// convert the settings data
|
||||
foreach (var data in converted.BlockValue.SettingsData)
|
||||
{
|
||||
if (!validSettingElementTypes.Contains(data.ContentTypeKey)) continue;
|
||||
|
||||
var element = _blockConverter.ConvertToElement(data, referenceCacheLevel, preview);
|
||||
if (element == null) continue;
|
||||
settingsPublishedElements[element.Key] = element;
|
||||
}
|
||||
|
||||
// if there's no elements just return since if there's no data it doesn't matter what is stored in layout
|
||||
if (elements.Count == 0) return model;
|
||||
if (contentPublishedElements.Count == 0) return BlockListModel.Empty;
|
||||
|
||||
foreach (var layoutItem in blockListLayout)
|
||||
{
|
||||
// the result of this can be null, that's ok
|
||||
var element = layoutItem.Settings != null ? _blockConverter.ConvertToElement(layoutItem.Settings, referenceCacheLevel, preview) : null;
|
||||
|
||||
var guidUdi = (GuidUdi)layoutItem.Udi;
|
||||
|
||||
// get the data reference
|
||||
if (!elements.TryGetValue(guidUdi.Guid, out var data))
|
||||
// get the content reference
|
||||
var contentGuidUdi = (GuidUdi)layoutItem.ContentUdi;
|
||||
if (!contentPublishedElements.TryGetValue(contentGuidUdi.Guid, out var contentData))
|
||||
continue;
|
||||
|
||||
if (!data.ContentType.TryGetKey(out var contentTypeKey))
|
||||
// get the setting reference
|
||||
IPublishedElement settingsData = null;
|
||||
var settingGuidUdi = layoutItem.SettingsUdi != null ? (GuidUdi)layoutItem.SettingsUdi : null;
|
||||
if (settingGuidUdi != null)
|
||||
settingsPublishedElements.TryGetValue(settingGuidUdi.Guid, out settingsData);
|
||||
|
||||
if (!contentData.ContentType.TryGetKey(out var contentTypeKey))
|
||||
throw new InvalidOperationException("The content type was not of type " + typeof(IPublishedContentType2));
|
||||
|
||||
if (!contentTypeMap.TryGetValue(contentTypeKey, out var blockConfig))
|
||||
if (!blockConfigMap.TryGetValue(contentTypeKey, out var blockConfig))
|
||||
continue;
|
||||
|
||||
// this can happen if they have a settings type, save content, remove the settings type, and display the front-end page before saving the content again
|
||||
if (element != null && string.IsNullOrWhiteSpace(blockConfig.SettingsElementTypeKey))
|
||||
element = null;
|
||||
// we also ensure that the content type's match since maybe the settings type has been changed after this has been persisted.
|
||||
if (settingsData != null)
|
||||
{
|
||||
if (!settingsData.ContentType.TryGetKey(out var settingsElementTypeKey))
|
||||
throw new InvalidOperationException("The settings element type was not of type " + typeof(IPublishedContentType2));
|
||||
|
||||
var layoutRef = new BlockListLayoutReference(layoutItem.Udi, data, element);
|
||||
if (!blockConfig.SettingsElementTypeKey.HasValue || settingsElementTypeKey != blockConfig.SettingsElementTypeKey)
|
||||
settingsData = null;
|
||||
}
|
||||
|
||||
var layoutRef = new BlockListLayoutReference(contentGuidUdi, contentData, settingGuidUdi, settingsData);
|
||||
layout.Add(layoutRef);
|
||||
}
|
||||
|
||||
var model = new BlockListModel(contentPublishedElements.Values, settingsPublishedElements.Values, layout);
|
||||
return model;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user