From f7a831f05486d36e540870820c8175573d259b97 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 23 Jul 2020 16:03:35 +1000 Subject: [PATCH] Fixes some JS issues with new data format, streamlines the blockeditor data format serialization in c#, implements To/FromEditor methods --- .../Models/Blocks/BlockEditorData.cs | 68 ++--- .../Models/Blocks/BlockEditorDataConverter.cs | 17 +- .../Models/Blocks/BlockItemData.cs | 56 ++++ .../Blocks/BlockListEditorDataConverter.cs | 2 +- src/Umbraco.Core/Models/Blocks/BlockValue.cs | 18 ++ src/Umbraco.Core/Umbraco.Core.csproj | 2 + .../components/content/edit.controller.js | 2 +- .../blockeditormodelobject.service.js | 125 ++++---- .../services/servervalidationmgr.service.js | 1 - .../umbBlockListPropertyEditor.component.js | 27 +- .../BlockEditorPropertyEditor.cs | 274 +++++++++++++----- .../NestedContentPropertyEditor.cs | 20 +- .../ValueConverters/BlockEditorConverter.cs | 2 +- .../BlockListPropertyValueConverter.cs | 8 +- 14 files changed, 415 insertions(+), 207 deletions(-) create mode 100644 src/Umbraco.Core/Models/Blocks/BlockItemData.cs create mode 100644 src/Umbraco.Core/Models/Blocks/BlockValue.cs diff --git a/src/Umbraco.Core/Models/Blocks/BlockEditorData.cs b/src/Umbraco.Core/Models/Blocks/BlockEditorData.cs index ba9f22d945..5ee609b148 100644 --- a/src/Umbraco.Core/Models/Blocks/BlockEditorData.cs +++ b/src/Umbraco.Core/Models/Blocks/BlockEditorData.cs @@ -1,74 +1,44 @@ -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 { /// - /// Converted block data from json + /// Convertable block data from json /// public class BlockEditorData { + private readonly string _propertyEditorAlias; + public static BlockEditorData Empty { get; } = new BlockEditorData(); private BlockEditorData() { } - public BlockEditorData(JToken layout, - IReadOnlyList references, - IReadOnlyList contentData, - IReadOnlyList settingsData) + public BlockEditorData(string propertyEditorAlias, + IEnumerable references, + BlockValue blockValue) { - Layout = layout ?? throw new ArgumentNullException(nameof(layout)); - References = references ?? throw new ArgumentNullException(nameof(references)); - ContentData = contentData ?? throw new ArgumentNullException(nameof(contentData)); - SettingsData = settingsData ?? throw new ArgumentNullException(nameof(settingsData)); - } - - public JToken Layout { get; } - public IReadOnlyList References { get; } = new List(); - public IReadOnlyList ContentData { get; } = new List(); - public IReadOnlyList SettingsData { get; } = new List(); - - internal class BlockValue - { - [JsonProperty("layout")] - public IDictionary Layout { get; set; } - - [JsonProperty("contentData")] - public IEnumerable ContentData { get; set; } = new List(); - - [JsonProperty("settingsData")] - public IEnumerable SettingsData { get; set; } = new List(); + 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(references) : throw new ArgumentNullException(nameof(references)); } /// - /// Represents a single block's data in raw form + /// Returns the layout for this specific property editor /// - 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; } + /// + /// Returns the reference to the original BlockValue + /// + public BlockValue BlockValue { get; } - /// - /// The remaining properties will be serialized to a dictionary - /// - /// - /// 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 - /// - [JsonExtensionData] - public Dictionary RawPropertyValues { get; set; } = new Dictionary(); - } + public List References { get; } = new List(); } } diff --git a/src/Umbraco.Core/Models/Blocks/BlockEditorDataConverter.cs b/src/Umbraco.Core/Models/Blocks/BlockEditorDataConverter.cs index f045a00401..22e364c0f8 100644 --- a/src/Umbraco.Core/Models/Blocks/BlockEditorDataConverter.cs +++ b/src/Umbraco.Core/Models/Blocks/BlockEditorDataConverter.cs @@ -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,21 +18,18 @@ namespace Umbraco.Core.Models.Blocks _propertyEditorAlias = propertyEditorAlias; } - public BlockEditorData Convert(string json) + public BlockEditorData Deserialize(string json) { var value = JsonConvert.DeserializeObject(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(); - var references = GetBlockReferences(layout); - var contentData = value.ContentData.ToList(); - var settingsData = value.SettingsData.ToList(); - - return new BlockEditorData(layout, references, contentData, settingsData); + return new BlockEditorData(_propertyEditorAlias, references, value); } /// @@ -42,7 +37,7 @@ namespace Umbraco.Core.Models.Blocks /// /// /// - protected abstract IReadOnlyList GetBlockReferences(JToken jsonLayout); + protected abstract IEnumerable GetBlockReferences(JToken jsonLayout); } } diff --git a/src/Umbraco.Core/Models/Blocks/BlockItemData.cs b/src/Umbraco.Core/Models/Blocks/BlockItemData.cs new file mode 100644 index 0000000000..12a636771e --- /dev/null +++ b/src/Umbraco.Core/Models/Blocks/BlockItemData.cs @@ -0,0 +1,56 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using Umbraco.Core.Serialization; + +namespace Umbraco.Core.Models.Blocks +{ + /// + /// Represents a single block's data in raw form + /// + public class BlockItemData + { + [JsonProperty("contentTypeKey")] + public Guid ContentTypeKey { get; set; } + + /// + /// not serialized, manually set and used during internally + /// + [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"); + + /// + /// The remaining properties will be serialized to a dictionary + /// + /// + /// 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 + /// + [JsonExtensionData] + public Dictionary RawPropertyValues { get; set; } = new Dictionary(); + + /// + /// Used during deserialization to convert the raw property data into data with a property type context + /// + [JsonIgnore] + public IDictionary PropertyValues { get; set; } = new Dictionary(); + + /// + /// Used during deserialization to populate the property value/property type of a block item content property + /// + public class BlockPropertyValue + { + public object Value { get; set; } + public PropertyType PropertyType { get; set; } + } + } +} diff --git a/src/Umbraco.Core/Models/Blocks/BlockListEditorDataConverter.cs b/src/Umbraco.Core/Models/Blocks/BlockListEditorDataConverter.cs index 0dec05bc54..23f69922d9 100644 --- a/src/Umbraco.Core/Models/Blocks/BlockListEditorDataConverter.cs +++ b/src/Umbraco.Core/Models/Blocks/BlockListEditorDataConverter.cs @@ -13,7 +13,7 @@ namespace Umbraco.Core.Models.Blocks { } - protected override IReadOnlyList GetBlockReferences(JToken jsonLayout) + protected override IEnumerable GetBlockReferences(JToken jsonLayout) { var blockListLayout = jsonLayout.ToObject>(); return blockListLayout.Select(x => new ContentAndSettingsReference(x.ContentUdi, x.SettingsUdi)).ToList(); diff --git a/src/Umbraco.Core/Models/Blocks/BlockValue.cs b/src/Umbraco.Core/Models/Blocks/BlockValue.cs new file mode 100644 index 0000000000..4700ddfd3b --- /dev/null +++ b/src/Umbraco.Core/Models/Blocks/BlockValue.cs @@ -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 Layout { get; set; } + + [JsonProperty("contentData")] + public List ContentData { get; set; } = new List(); + + [JsonProperty("settingsData")] + public List SettingsData { get; set; } = new List(); + } +} diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index d7a1251f20..73af567cbc 100755 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -134,8 +134,10 @@ + + diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js index 68b19abf08..e3c2913e4e 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js @@ -517,7 +517,7 @@ } function handleHttpException(err) { - if (!err.status) { + if (err && !err.status) { $exceptionHandler(err); } } diff --git a/src/Umbraco.Web.UI.Client/src/common/services/blockeditormodelobject.service.js b/src/Umbraco.Web.UI.Client/src/common/services/blockeditormodelobject.service.js index 7496c15b52..e3e1e31a53 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/blockeditormodelobject.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/blockeditormodelobject.service.js @@ -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,7 +133,7 @@ // 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 + "." + "settingsData" + "." + prop.alias, createLayoutSettingsModelWatcher(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,8 +165,8 @@ /** * Used to create a prop watcher for the settings in the property editor data model. */ - function createLayoutSettingsModelWatcher(blockObject, prop) { - return function() { + function createLayoutSettingsModelWatcher(blockObject, prop) { + return function () { if (prop.value !== blockObject.settingsData[prop.alias]) { // sync data: 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,8 +191,8 @@ /** * Used to create a scoped watcher for a settings property on a blockObject. */ - function createSettingsModelPropWatcher(blockObject, prop) { - return function() { + function createSettingsModelPropWatcher(blockObject, prop) { + return function () { if (blockObject.settingsData[prop.alias] !== prop.value) { // sync data: blockObject.settingsData[prop.alias] = prop.value; @@ -255,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 @@ -293,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; }, @@ -305,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 = []; @@ -339,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); }, @@ -351,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 @@ -376,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); }, @@ -388,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); }, @@ -413,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; } @@ -428,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."); } } @@ -443,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, ""); @@ -465,7 +466,7 @@ this.__scope.$evalAsync(); } }.bind(blockObject) - , 10); + , 10); // make basics from scaffold blockObject.content = Utilities.copy(contentScaffold); @@ -505,7 +506,7 @@ } } - blockObject.retrieveValuesFrom = function(content, settings) { + blockObject.retrieveValuesFrom = function (content, settings) { if (this.content !== null) { mapElementValues(content, this.content); } @@ -545,7 +546,7 @@ 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, @@ -555,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; } @@ -580,7 +581,7 @@ } this.destroyBlockObject(blockObject); this.removeDataByUdi(udi); - if(settingsUdi) { + if (settingsUdi) { this.removeSettingsByUdi(settingsUdi); } }, @@ -592,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(); }, @@ -604,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 @@ -619,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.settingsUdi = this._createSettingsEntry(blockConfiguration.settingsElementTypeKey) } - + return entry; }, @@ -644,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; } @@ -672,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(); } @@ -680,7 +681,7 @@ // 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") @@ -690,7 +691,7 @@ }, // private // TODO: Then this can just be a method in the outer scope - _getDataByUdi: function(udi) { + _getDataByUdi: function (udi) { return this.value.contentData.find(entry => entry.udi === udi) || null; }, @@ -699,10 +700,10 @@ * @name removeDataByUdi * @methodOf umbraco.services.blockEditorModelObject * @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 handlede by the Property Editor since this services donst know about your layout structure. + * 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) { + removeDataByUdi: function (udi) { const index = this.value.contentData.findIndex(o => o.udi === udi); if (index !== -1) { this.value.contentData.splice(index, 1); @@ -711,7 +712,7 @@ // private // TODO: Then this can just be a method in the outer scope - _createSettingsEntry: function(elementTypeKey) { + _createSettingsEntry: function (elementTypeKey) { var settings = { contentTypeKey: elementTypeKey, udi: udiService.create("element") @@ -722,7 +723,7 @@ // private // TODO: Then this can just be a method in the outer scope - _getSettingsByUdi: function(udi) { + _getSettingsByUdi: function (udi) { return this.value.settingsData.find(entry => entry.udi === udi) || null; }, @@ -731,10 +732,10 @@ * @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 handlede by the Property Editor since this services donst know about your layout structure. + * 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) { + removeSettingsByUdi: function (udi) { const index = this.value.settingsData.findIndex(o => o.udi === udi); if (index !== -1) { this.value.settingsData.splice(index, 1); @@ -747,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; diff --git a/src/Umbraco.Web.UI.Client/src/common/services/servervalidationmgr.service.js b/src/Umbraco.Web.UI.Client/src/common/services/servervalidationmgr.service.js index b2fdf37aa4..7f8212f2c6 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/servervalidationmgr.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/servervalidationmgr.service.js @@ -504,7 +504,6 @@ function serverValidationManager($timeout) { // add a generic error for the property addPropertyError(propertyValidationKey, culture, htmlFieldReference, value && Array.isArray(value) && value.length > 0 ? value[0] : null, segment); - hasPropertyErrors = true; } else { diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umbBlockListPropertyEditor.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umbBlockListPropertyEditor.component.js index aa66472778..45ba6ddaa7 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umbBlockListPropertyEditor.component.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umbBlockListPropertyEditor.component.js @@ -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) } diff --git a/src/Umbraco.Web/PropertyEditors/BlockEditorPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/BlockEditorPropertyEditor.cs index 866bd38e05..c20bd08204 100644 --- a/src/Umbraco.Web/PropertyEditors/BlockEditorPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/BlockEditorPropertyEditor.cs @@ -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(); + var blockEditorData = _blockEditorValues.DeserializeAndClean(rawJson); + if (blockEditorData == null) + return Enumerable.Empty(); - 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 + + /// + /// Ensure that sub-editor values are translated through their ToEditor methods + /// + /// + /// + /// + /// + /// + 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; + } + + /// + /// Ensure that sub-editor values are translated through their FromEditor methods + /// + /// + /// + /// + 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 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; } } } + /// + /// Used to deserialize json values and clean up any values based on the existence of element types and layout structure + /// internal class BlockEditorValues { private readonly Lazy> _contentTypes; @@ -126,81 +264,77 @@ namespace Umbraco.Web.PropertyEditors return contentType; } - public IReadOnlyList GetPropertyValues(object propertyValue) + public BlockEditorData DeserializeAndClean(object propertyValue) { if (propertyValue == null || string.IsNullOrWhiteSpace(propertyValue.ToString())) - return new List(); + return null; - var converted = _dataConverter.Convert(propertyValue.ToString()); + var blockEditorData = _dataConverter.Deserialize(propertyValue.ToString()); - if (converted.ContentData.Count == 0) - return new List(); - - var contentTypePropertyTypes = new Dictionary>(); - var result = new List(); - - foreach(var block in converted.ContentData) + 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(); - - // 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>(); + + // 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; } - /// - /// Used during deserialization to populate the property value/property type of a nested content row property - /// - internal class BlockPropertyValue + private bool ResolveBlockItemData(BlockItemData block, Dictionary> contentTypePropertyTypes) { - public object Value { get; set; } - public PropertyType PropertyType { get; set; } - } + var contentType = GetElementType(block); + if (contentType == null) + return false; - /// - /// Used during deserialization to populate the content type alias and property values of a block - /// - internal class BlockValue - { - public Guid Id { get; set; } - public string ContentTypeAlias { get; set; } - public IDictionary PropertyValues { get; set; } = new Dictionary(); - } + // 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(); + + // 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 diff --git a/src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs index 4767dc19cd..ac9e0624bb 100644 --- a/src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs @@ -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? + /// + /// Ensure that sub-editor values are translated through their ToEditor methods + /// + /// + /// + /// + /// + /// 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? + /// + /// Ensure that sub-editor values are translated through their FromEditor methods + /// + /// + /// + /// public override object FromEditor(ContentPropertyData editorValue, object currentValue) { if (editorValue.Value == null || string.IsNullOrWhiteSpace(editorValue.Value.ToString())) diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/BlockEditorConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/BlockEditorConverter.cs index 83c612e9f7..f043c8e66e 100644 --- a/src/Umbraco.Web/PropertyEditors/ValueConverters/BlockEditorConverter.cs +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/BlockEditorConverter.cs @@ -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. diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs index 2b04106288..4d972f7a33 100644 --- a/src/Umbraco.Web/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs @@ -62,20 +62,20 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters if (string.IsNullOrWhiteSpace(value)) return BlockListModel.Empty; var converter = new BlockListEditorDataConverter(); - var converted = converter.Convert(value); - if (converted.ContentData.Count == 0) return BlockListModel.Empty; + var converted = converter.Deserialize(value); + if (converted.BlockValue.ContentData.Count == 0) return BlockListModel.Empty; var blockListLayout = converted.Layout.ToObject>(); // convert the content data - foreach (var data in converted.ContentData) + foreach (var data in converted.BlockValue.ContentData) { var element = _blockConverter.ConvertToElement(data, referenceCacheLevel, preview); if (element == null) continue; contentPublishedElements[element.Key] = element; } // convert the settings data - foreach (var data in converted.SettingsData) + foreach (var data in converted.BlockValue.SettingsData) { var element = _blockConverter.ConvertToElement(data, referenceCacheLevel, preview); if (element == null) continue;