diff --git a/src/Umbraco.Core/ContentExtensions.cs b/src/Umbraco.Core/ContentExtensions.cs index 7bce23e98e..eb6339741a 100644 --- a/src/Umbraco.Core/ContentExtensions.cs +++ b/src/Umbraco.Core/ContentExtensions.cs @@ -119,6 +119,15 @@ namespace Umbraco.Core return false; } + /// + /// Returns all properties based on the editorAlias + /// + /// + /// + /// + public static IEnumerable GetPropertiesByEditor(this IContentBase content, string editorAlias) + => content.Properties.Where(x => x.PropertyType.PropertyEditorAlias == editorAlias); + /// /// Returns properties that do not belong to a group /// diff --git a/src/Umbraco.Core/Models/Blocks/BlockEditorDataConverter.cs b/src/Umbraco.Core/Models/Blocks/BlockEditorDataConverter.cs index 22e364c0f8..802e8c2ee3 100644 --- a/src/Umbraco.Core/Models/Blocks/BlockEditorDataConverter.cs +++ b/src/Umbraco.Core/Models/Blocks/BlockEditorDataConverter.cs @@ -18,10 +18,35 @@ namespace Umbraco.Core.Models.Blocks _propertyEditorAlias = propertyEditorAlias; } + public BlockEditorData ConvertFrom(JToken json) + { + var value = json.ToObject(); + return Convert(value); + } + + public bool TryDeserialize(string json, out BlockEditorData blockEditorData) + { + try + { + var value = JsonConvert.DeserializeObject(json); + blockEditorData = Convert(value); + return true; + } + catch (System.Exception) + { + blockEditorData = null; + return false; + } + } + public BlockEditorData Deserialize(string json) { var value = JsonConvert.DeserializeObject(json); + return Convert(value); + } + private BlockEditorData Convert(BlockValue value) + { if (value.Layout == null) return BlockEditorData.Empty; diff --git a/src/Umbraco.Core/Models/Blocks/BlockListLayoutItem.cs b/src/Umbraco.Core/Models/Blocks/BlockListLayoutItem.cs index 3453ff2a78..5de44e16c1 100644 --- a/src/Umbraco.Core/Models/Blocks/BlockListLayoutItem.cs +++ b/src/Umbraco.Core/Models/Blocks/BlockListLayoutItem.cs @@ -12,7 +12,7 @@ namespace Umbraco.Core.Models.Blocks [JsonConverter(typeof(UdiJsonConverter))] public Udi ContentUdi { get; set; } - [JsonProperty("settingsUdi")] + [JsonProperty("settingsUdi", NullValueHandling = NullValueHandling.Ignore)] [JsonConverter(typeof(UdiJsonConverter))] public Udi SettingsUdi { get; set; } } diff --git a/src/Umbraco.Core/Models/PropertyCollection.cs b/src/Umbraco.Core/Models/PropertyCollection.cs index c587a45424..13fbf949d6 100644 --- a/src/Umbraco.Core/Models/PropertyCollection.cs +++ b/src/Umbraco.Core/Models/PropertyCollection.cs @@ -7,6 +7,7 @@ using System.Runtime.Serialization; namespace Umbraco.Core.Models { + /// /// Represents a collection of property values. /// diff --git a/src/Umbraco.Core/PropertyEditors/ComplexPropertyEditorContentEventHandler.cs b/src/Umbraco.Core/PropertyEditors/ComplexPropertyEditorContentEventHandler.cs new file mode 100644 index 0000000000..2b819d4555 --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/ComplexPropertyEditorContentEventHandler.cs @@ -0,0 +1,98 @@ +using System; +using System.Collections.Generic; +using Umbraco.Core.Events; +using Umbraco.Core.Models; +using Umbraco.Core.Services; +using Umbraco.Core.Services.Implement; + +namespace Umbraco.Core.PropertyEditors +{ + /// + /// Utility class for dealing with Copying/Saving events for complex editors + /// + internal class ComplexPropertyEditorContentEventHandler : IDisposable + { + private readonly string _editorAlias; + private readonly Func _formatPropertyValue; + private bool _disposedValue; + + public ComplexPropertyEditorContentEventHandler(string editorAlias, + Func formatPropertyValue) + { + _editorAlias = editorAlias; + _formatPropertyValue = formatPropertyValue; + ContentService.Copying += ContentService_Copying; + ContentService.Saving += ContentService_Saving; + } + + /// + /// Copying event handler + /// + /// + /// + private void ContentService_Copying(IContentService sender, CopyEventArgs e) + { + var props = e.Copy.GetPropertiesByEditor(_editorAlias); + UpdatePropertyValues(props, false); + } + + /// + /// Saving event handler + /// + /// + /// + private void ContentService_Saving(IContentService sender, ContentSavingEventArgs e) + { + foreach (var entity in e.SavedEntities) + { + var props = entity.GetPropertiesByEditor(_editorAlias); + UpdatePropertyValues(props, true); + } + } + + private void UpdatePropertyValues(IEnumerable props, bool onlyMissingKeys) + { + foreach (var prop in props) + { + // A Property may have one or more values due to cultures + var propVals = prop.Values; + foreach (var cultureVal in propVals) + { + // Remove keys from published value & any nested properties + var updatedPublishedVal = _formatPropertyValue(cultureVal.PublishedValue?.ToString(), onlyMissingKeys); + cultureVal.PublishedValue = updatedPublishedVal; + + // Remove keys from edited/draft value & any nested properties + var updatedEditedVal = _formatPropertyValue(cultureVal.EditedValue?.ToString(), onlyMissingKeys); + cultureVal.EditedValue = updatedEditedVal; + } + } + } + + /// + /// Unbinds from events + /// + /// + protected virtual void Dispose(bool disposing) + { + if (!_disposedValue) + { + if (disposing) + { + ContentService.Copying -= ContentService_Copying; + ContentService.Saving -= ContentService_Saving; + } + _disposedValue = true; + } + } + + /// + /// Unbinds from events + /// + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + } +} diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 19f9d25369..f80784f12a 100755 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -148,6 +148,7 @@ + diff --git a/src/Umbraco.Tests/PropertyEditors/BlockEditorComponentTests.cs b/src/Umbraco.Tests/PropertyEditors/BlockEditorComponentTests.cs new file mode 100644 index 0000000000..bfd8b8c77b --- /dev/null +++ b/src/Umbraco.Tests/PropertyEditors/BlockEditorComponentTests.cs @@ -0,0 +1,261 @@ +using Newtonsoft.Json; +using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.Linq; +using Umbraco.Core; +using Umbraco.Web.Compose; + +namespace Umbraco.Tests.PropertyEditors +{ + [TestFixture] + public class BlockEditorComponentTests + { + private readonly JsonSerializerSettings _serializerSettings = new JsonSerializerSettings + { + Formatting = Formatting.None, + NullValueHandling = NullValueHandling.Ignore, + + }; + + private const string _contentGuid1 = "036ce82586a64dfba2d523a99ed80f58"; + private const string _contentGuid2 = "48288c21a38a40ef82deb3eda90a58f6"; + private const string _settingsGuid1 = "ffd35c4e2eea4900abfa5611b67b2492"; + private const string _subContentGuid1 = "4c44ce6b3a5c4f5f8f15e3dc24819a9e"; + private const string _subContentGuid2 = "a062c06d6b0b44ac892b35d90309c7f8"; + private const string _subSettingsGuid1 = "4d998d980ffa4eee8afdc23c4abd6d29"; + + [Test] + public void Cannot_Have_Null_Udi() + { + var component = new BlockEditorComponent(); + var json = GetBlockListJson(null, string.Empty); + Assert.Throws(() => component.ReplaceBlockListUdis(json)); + } + + [Test] + public void No_Nesting() + { + var guids = Enumerable.Range(0, 3).Select(x => Guid.NewGuid()).ToList(); + var guidCounter = 0; + Func guidFactory = () => guids[guidCounter++]; + + var json = GetBlockListJson(null); + + var expected = ReplaceGuids(json, guids, _contentGuid1, _contentGuid2, _settingsGuid1); + + var component = new BlockEditorComponent(); + var result = component.ReplaceBlockListUdis(json, guidFactory); + + var expectedJson = JsonConvert.SerializeObject(JsonConvert.DeserializeObject(expected, _serializerSettings), _serializerSettings); + var resultJson = JsonConvert.SerializeObject(JsonConvert.DeserializeObject(result, _serializerSettings), _serializerSettings); + Console.WriteLine(expectedJson); + Console.WriteLine(resultJson); + Assert.AreEqual(expectedJson, resultJson); + } + + [Test] + public void One_Level_Nesting_Escaped() + { + var guids = Enumerable.Range(0, 6).Select(x => Guid.NewGuid()).ToList(); + + var guidCounter = 0; + Func guidFactory = () => guids[guidCounter++]; + + var innerJson = GetBlockListJson(null, _subContentGuid1, _subContentGuid2, _subSettingsGuid1); + + // we need to ensure the escaped json is consistent with how it will be re-escaped after parsing + // and this is how to do that, the result will also include quotes around it. + var innerJsonEscaped = JsonConvert.ToString(innerJson); + + // get the json with the subFeatures as escaped + var json = GetBlockListJson(innerJsonEscaped); + + var component = new BlockEditorComponent(); + var result = component.ReplaceBlockListUdis(json, guidFactory); + + // the expected result is that the subFeatures data is no longer escaped + var expected = ReplaceGuids(GetBlockListJson(innerJson), guids, + _contentGuid1, _contentGuid2, _settingsGuid1, + _subContentGuid1, _subContentGuid2, _subSettingsGuid1); + + var expectedJson = JsonConvert.SerializeObject(JsonConvert.DeserializeObject(expected, _serializerSettings), _serializerSettings); + var resultJson = JsonConvert.SerializeObject(JsonConvert.DeserializeObject(result, _serializerSettings), _serializerSettings); + Console.WriteLine(expectedJson); + Console.WriteLine(resultJson); + Assert.AreEqual(expectedJson, resultJson); + } + + [Test] + public void One_Level_Nesting_Unescaped() + { + var guids = Enumerable.Range(0, 6).Select(x => Guid.NewGuid()).ToList(); + var guidCounter = 0; + Func guidFactory = () => guids[guidCounter++]; + + // nested blocks without property value escaping used in the conversion + var innerJson = GetBlockListJson(null, _subContentGuid1, _subContentGuid2, _subSettingsGuid1); + + // get the json with the subFeatures as unescaped + var json = GetBlockListJson(innerJson); + + var expected = ReplaceGuids(GetBlockListJson(innerJson), guids, + _contentGuid1, _contentGuid2, _settingsGuid1, + _subContentGuid1, _subContentGuid2, _subSettingsGuid1); + + var component = new BlockEditorComponent(); + var result = component.ReplaceBlockListUdis(json, guidFactory); + + var expectedJson = JsonConvert.SerializeObject(JsonConvert.DeserializeObject(expected, _serializerSettings), _serializerSettings); + var resultJson = JsonConvert.SerializeObject(JsonConvert.DeserializeObject(result, _serializerSettings), _serializerSettings); + Console.WriteLine(expectedJson); + Console.WriteLine(resultJson); + Assert.AreEqual(expectedJson, resultJson); + } + + [Test] + public void Nested_In_Complex_Editor_Escaped() + { + var guids = Enumerable.Range(0, 6).Select(x => Guid.NewGuid()).ToList(); + var guidCounter = 0; + Func guidFactory = () => guids[guidCounter++]; + + var innerJson = GetBlockListJson(null, _subContentGuid1, _subContentGuid2, _subSettingsGuid1); + + // we need to ensure the escaped json is consistent with how it will be re-escaped after parsing + // and this is how to do that, the result will also include quotes around it. + var innerJsonEscaped = JsonConvert.ToString(innerJson); + + // Complex editor such as the grid + var complexEditorJsonEscaped = GetGridJson(innerJsonEscaped); + + var json = GetBlockListJson(complexEditorJsonEscaped); + + var component = new BlockEditorComponent(); + var result = component.ReplaceBlockListUdis(json, guidFactory); + + // the expected result is that the subFeatures data is no longer escaped + var expected = ReplaceGuids(GetBlockListJson(GetGridJson(innerJson)), guids, + _contentGuid1, _contentGuid2, _settingsGuid1, + _subContentGuid1, _subContentGuid2, _subSettingsGuid1); + + var expectedJson = JsonConvert.SerializeObject(JsonConvert.DeserializeObject(expected, _serializerSettings), _serializerSettings); + var resultJson = JsonConvert.SerializeObject(JsonConvert.DeserializeObject(result, _serializerSettings), _serializerSettings); + Console.WriteLine(expectedJson); + Console.WriteLine(resultJson); + Assert.AreEqual(expectedJson, resultJson); + } + + private string GetBlockListJson(string subFeatures, + string contentGuid1 = _contentGuid1, + string contentGuid2 = _contentGuid2, + string settingsGuid1 = _settingsGuid1) + { + return @"{ + ""layout"": + { + ""Umbraco.BlockList"": [ + { + ""contentUdi"": """ + (contentGuid1.IsNullOrWhiteSpace() ? string.Empty : GuidUdi.Create(Constants.UdiEntityType.Element, Guid.Parse(contentGuid1)).ToString()) + @""" + }, + { + ""contentUdi"": ""umb://element/" + contentGuid2 + @""", + ""settingsUdi"": ""umb://element/" + settingsGuid1 + @""" + } + ] + }, + ""contentData"": [ + { + ""contentTypeKey"": ""d6ce4a86-91a2-45b3-a99c-8691fc1fb020"", + ""udi"": """ + (contentGuid1.IsNullOrWhiteSpace() ? string.Empty : GuidUdi.Create(Constants.UdiEntityType.Element, Guid.Parse(contentGuid1)).ToString()) + @""", + ""featureName"": ""Hello"", + ""featureDetails"": ""World"" + }, + { + ""contentTypeKey"": ""d6ce4a86-91a2-45b3-a99c-8691fc1fb020"", + ""udi"": ""umb://element/" + contentGuid2 + @""", + ""featureName"": ""Another"", + ""featureDetails"": ""Feature""" + (subFeatures == null ? string.Empty : (@", ""subFeatures"": " + subFeatures)) + @" + } + ], + ""settingsData"": [ + { + ""contentTypeKey"": ""d6ce4a86-91a2-45b3-a99c-8691fc1fb020"", + ""udi"": ""umb://element/" + settingsGuid1 + @""", + ""featureName"": ""Setting 1"", + ""featureDetails"": ""Setting 2"" + }, + ] +}"; + } + + private string GetGridJson(string subBlockList) + { + return @"{ + ""name"": ""1 column layout"", + ""sections"": [ + { + ""grid"": ""12"", + ""rows"": [ + { + ""name"": ""Article"", + ""id"": ""b4f6f651-0de3-ef46-e66a-464f4aaa9c57"", + ""areas"": [ + { + ""grid"": ""4"", + ""controls"": [ + { + ""value"": ""I am quote"", + ""editor"": { + ""alias"": ""quote"", + ""view"": ""textstring"" + }, + ""styles"": null, + ""config"": null + }], + ""styles"": null, + ""config"": null + }, + { + ""grid"": ""8"", + ""controls"": [ + { + ""value"": ""Header"", + ""editor"": { + ""alias"": ""headline"", + ""view"": ""textstring"" + }, + ""styles"": null, + ""config"": null + }, + { + ""value"": " + subBlockList + @", + ""editor"": { + ""alias"": ""madeUpNestedContent"", + ""view"": ""madeUpNestedContentInGrid"" + }, + ""styles"": null, + ""config"": null + }], + ""styles"": null, + ""config"": null + }], + ""styles"": null, + ""config"": null + }] + }] +}"; + } + + private string ReplaceGuids(string json, List newGuids, params string[] oldGuids) + { + for (var i = 0; i < oldGuids.Length; i++) + { + var old = oldGuids[i]; + json = json.Replace(old, newGuids[i].ToString("N")); + } + return json; + } + + } +} diff --git a/src/Umbraco.Tests/PropertyEditors/NestedContentPropertyComponentTests.cs b/src/Umbraco.Tests/PropertyEditors/NestedContentPropertyComponentTests.cs index 1b83c048d2..5b7e220123 100644 --- a/src/Umbraco.Tests/PropertyEditors/NestedContentPropertyComponentTests.cs +++ b/src/Umbraco.Tests/PropertyEditors/NestedContentPropertyComponentTests.cs @@ -9,6 +9,7 @@ using Umbraco.Web.Compose; namespace Umbraco.Tests.PropertyEditors { + [TestFixture] public class NestedContentPropertyComponentTests { diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index f803f1cd1d..004945bb46 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -147,6 +147,7 @@ + diff --git a/src/Umbraco.Web.UI.Client/src/common/services/blockeditor.service.js b/src/Umbraco.Web.UI.Client/src/common/services/blockeditor.service.js index ffb1971169..dfa0eae297 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/blockeditor.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/blockeditor.service.js @@ -4,39 +4,113 @@ * * @description * Added in Umbraco 8.7. Service for dealing with Block Editors. - * + * * Block Editor Service provides the basic features for a block editor. * The main feature is the ability to create a Model Object which takes care of your data for your Block Editor. - * - * + * + * * ##Samples * * ####Instantiate a Model Object for your property editor: - * + * *
  *     modelObject = blockEditorService.createModelObject(vm.model.value, vm.model.editor, vm.model.config.blocks, $scope);
  *     modelObject.load().then(onLoaded);
  * 
* - * + * * See {@link umbraco.services.blockEditorModelObject BlockEditorModelObject} for more samples. - * + * */ (function () { 'use strict'; + + /** + * When performing a runtime copy of Block Editors entries, we copy the ElementType Data Model and inner IDs are kept identical, to ensure new IDs are changed on paste we need to provide a resolver for the ClipboardService. + */ + angular.module('umbraco').run(['clipboardService', 'udiService', function (clipboardService, udiService) { + + function replaceUdi(obj, key, dataObject) { + var udi = obj[key]; + var newUdi = udiService.create("element"); + obj[key] = newUdi; + dataObject.forEach((data) => { + if (data.udi === udi) { + data.udi = newUdi; + } + }); + } + function replaceUdisOfObject(obj, propValue) { + for (var k in obj) { + if(k === "contentUdi") { + replaceUdi(obj, k, propValue.contentData); + } else if(k === "settingsUdi") { + replaceUdi(obj, k, propValue.settingsData); + } else { + // lets crawl through all properties of layout to make sure get captured all `contentUdi` and `settingsUdi` properties. + var propType = typeof obj[k]; + if(propType === "object" || propType === "array") { + replaceUdisOfObject(obj[k], propValue) + } + } + } + } + function replaceElementTypeBlockListUDIsResolver(obj, propClearingMethod) { + replaceRawBlockListUDIsResolver(obj.value, propClearingMethod); + } + + clipboardService.registerPastePropertyResolver(replaceElementTypeBlockListUDIsResolver, clipboardService.TYPES.ELEMENT_TYPE); + + + function replaceRawBlockListUDIsResolver(value, propClearingMethod) { + if (typeof value === "object") { + + // we got an object, and it has these three props then we are most likely dealing with a Block Editor. + if ((value.layout !== undefined && value.contentData !== undefined && value.settingsData !== undefined)) { + + replaceUdisOfObject(value.layout, value); + + // replace UDIs for inner properties of this Block Editors content data. + if(value.contentData.length > 0) { + value.contentData.forEach((item) => { + for (var k in item) { + propClearingMethod(item[k], clipboardService.TYPES.RAW); + } + }); + } + // replace UDIs for inner properties of this Block Editors settings data. + if(value.settingsData.length > 0) { + value.settingsData.forEach((item) => { + for (var k in item) { + propClearingMethod(item[k], clipboardService.TYPES.RAW); + } + }); + } + + } + } + } + + clipboardService.registerPastePropertyResolver(replaceRawBlockListUDIsResolver, clipboardService.TYPES.RAW); + + }]); + + + + function blockEditorService(blockEditorModelObject) { /** * @ngdocs function * @name createModelObject * @methodOf umbraco.services.blockEditorService - * + * * @description * Create a new Block Editor Model Object. * See {@link umbraco.services.blockEditorModelObject blockEditorModelObject} - * + * * @see umbraco.services.blockEditorModelObject * @param {object} propertyModelValue data object of the property editor, usually model.value. * @param {string} propertyEditorAlias alias of the property. 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 920ba1c58d..868b8baba7 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 @@ -13,8 +13,7 @@ (function () { 'use strict'; - - function blockEditorModelObjectFactory($interpolate, $q, udiService, contentResource, localizationService, umbRequestHelper) { + function blockEditorModelObjectFactory($interpolate, $q, udiService, contentResource, localizationService, umbRequestHelper, clipboardService) { /** * Simple mapping from property model content entry to editing model, @@ -231,7 +230,8 @@ var notSupportedProperties = [ "Umbraco.Tags", "Umbraco.UploadField", - "Umbraco.ImageCropper" + "Umbraco.ImageCropper", + "Umbraco.NestedContent" ]; @@ -524,12 +524,11 @@ } var blockConfiguration = this.getBlockConfiguration(dataModel.contentTypeKey); - var contentScaffold; + var contentScaffold = null; if (blockConfiguration === null) { - console.error("The block of " + contentUdi + " is not being initialized because its contentTypeKey('" + dataModel.contentTypeKey + "') is not allowed for this PropertyEditor"); - } - else { + console.warn("The block of " + contentUdi + " is not being initialized because its contentTypeKey('" + dataModel.contentTypeKey + "') is not allowed for this PropertyEditor"); + } else { contentScaffold = this.getScaffoldFromKey(blockConfiguration.contentElementTypeKey); if (contentScaffold === null) { console.error("The block of " + contentUdi + " is not begin initialized cause its Element Type was not loaded."); @@ -539,10 +538,9 @@ if (blockConfiguration === null || contentScaffold === null) { blockConfiguration = { - label: "Unsupported Block", + label: "Unsupported", unsupported: true }; - contentScaffold = {}; } var blockObject = {}; @@ -567,10 +565,14 @@ , 10); // make basics from scaffold - blockObject.content = Utilities.copy(contentScaffold); - ensureUdiAndKey(blockObject.content, contentUdi); + if(contentScaffold !== null) {// We might not have contentScaffold + blockObject.content = Utilities.copy(contentScaffold); + ensureUdiAndKey(blockObject.content, contentUdi); - mapToElementModel(blockObject.content, dataModel); + mapToElementModel(blockObject.content, dataModel); + } else { + blockObject.content = null; + } blockObject.data = dataModel; blockObject.layout = layoutEntry; @@ -672,11 +674,8 @@ * @param {Object} blockObject The BlockObject to be removed and destroyed. */ removeDataAndDestroyModel: function (blockObject) { - var udi = blockObject.content.udi; - var settingsUdi = null; - if (blockObject.settings) { - settingsUdi = blockObject.settings.udi; - } + var udi = blockObject.layout.contentUdi; + var settingsUdi = blockObject.layout.settingsUdi || null; this.destroyBlockObject(blockObject); this.removeDataByUdi(udi); if (settingsUdi) { @@ -745,7 +744,7 @@ */ createFromElementType: function (elementTypeDataModel) { - elementTypeDataModel = Utilities.copy(elementTypeDataModel); + elementTypeDataModel = clipboardService.parseContentForPaste(elementTypeDataModel, clipboardService.TYPES.ELEMENT_TYPE); var contentElementTypeKey = elementTypeDataModel.contentTypeKey; diff --git a/src/Umbraco.Web.UI.Client/src/common/services/clipboard.service.js b/src/Umbraco.Web.UI.Client/src/common/services/clipboard.service.js index 0d2ca6623b..58ed07367e 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/clipboard.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/clipboard.service.js @@ -13,7 +13,28 @@ function clipboardService(notificationsService, eventsService, localStorageService, iconHelper) { - var clearPropertyResolvers = []; + const TYPES = {}; + TYPES.ELEMENT_TYPE = "elementType"; + TYPES.RAW = "raw"; + + var clearPropertyResolvers = {}; + var pastePropertyResolvers = {}; + var clipboardTypeResolvers = {}; + + clipboardTypeResolvers[TYPES.ELEMENT_TYPE] = function(data, propMethod) { + for (var t = 0; t < data.variants[0].tabs.length; t++) { + var tab = data.variants[0].tabs[t]; + for (var p = 0; p < tab.properties.length; p++) { + var prop = tab.properties[p]; + propMethod(prop, TYPES.ELEMENT_TYPE); + } + } + } + clipboardTypeResolvers[TYPES.RAW] = function(data, propMethod) { + for (var p = 0; p < data.length; p++) { + propMethod(data[p], TYPES.RAW); + } + } var STORAGE_KEY = "umbClipboardService"; @@ -57,28 +78,29 @@ function clipboardService(notificationsService, eventsService, localStorageServi } - function clearPropertyForStorage(prop) { + function resolvePropertyForStorage(prop, type) { - for (var i=0; i prepareEntryForStorage(data, firstLevelClearupMethod)); + var copiedDatas = datas.map(data => prepareEntryForStorage(type, data, firstLevelClearupMethod)); // remove previous copies of this entry: storage.entries = storage.entries.filter( diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockeditor/blockeditor.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockeditor/blockeditor.controller.js index a08a05b0f7..88cda027a8 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockeditor/blockeditor.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockeditor/blockeditor.controller.js @@ -7,8 +7,8 @@ angular.module("umbraco") vm.tabs = []; localizationService.localizeMany([ - vm.model.liveEditing ? "prompt_discardChanges" : "general_close", - vm.model.liveEditing ? "buttons_confirmActionConfirm" : "buttons_submitChanges" + vm.model.createFlow ? "general_cancel" : (vm.model.liveEditing ? "prompt_discardChanges" : "general_close"), + vm.model.createFlow ? "general_create" : (vm.model.liveEditing ? "buttons_confirmActionConfirm" : "buttons_submitChanges") ]).then(function (data) { vm.closeLabel = data[0]; vm.submitLabel = data[1]; @@ -68,16 +68,16 @@ angular.module("umbraco") // * It would have a 'commit' method to commit the removed errors - which we would call in the formHelper.submitForm when it's successful // * It would have a 'rollback' method to reset the removed errors - which we would call here - - if (vm.blockForm.$dirty === true) { - localizationService.localizeMany(["prompt_discardChanges", "blockEditor_blockHasChanges"]).then(function (localizations) { + if (vm.model.createFlow === true || vm.blockForm.$dirty === true) { + var labels = vm.model.createFlow === true ? ["blockEditor_confirmCancelBlockCreationHeadline", "blockEditor_confirmCancelBlockCreationMessage"] : ["prompt_discardChanges", "blockEditor_blockHasChanges"]; + localizationService.localizeMany(labels).then(function (localizations) { const confirm = { title: localizations[0], view: "default", content: localizations[1], submitButtonLabelKey: "general_discard", submitButtonStyle: "danger", - closeButtonLabelKey: "general_cancel", + closeButtonLabelKey: "prompt_stay", submit: function () { overlayService.close(); vm.model.close(vm.model); @@ -88,11 +88,10 @@ angular.module("umbraco") }; overlayService.open(confirm); }); - - return; + } else { + vm.model.close(vm.model); } - // TODO: check if content/settings has changed and ask user if they are sure. - vm.model.close(vm.model); + } } diff --git a/src/Umbraco.Web.UI.Client/src/views/content/content.create.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/content.create.controller.js index 487a73f948..0e6dc76725 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/content.create.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/content.create.controller.js @@ -2,7 +2,7 @@ * @ngdoc controller * @name Umbraco.Editors.Content.CreateController * @function - * + * * @description * The controller for the content creation dialog */ @@ -22,20 +22,17 @@ function contentCreateController($scope, function initialize() { $scope.loading = true; $scope.allowedTypes = null; - + var getAllowedTypes = contentTypeResource.getAllowedTypes($scope.currentNode.id).then(function (data) { $scope.allowedTypes = iconHelper.formatContentTypeIcons(data); - if ($scope.allowedTypes.length === 0) { - contentTypeResource.getCount().then(function(count) { - $scope.countTypes = count; - }); - } }); var getCurrentUser = authResource.getCurrentUser().then(function (currentUser) { - if (currentUser.allowedSections.indexOf("settings") > -1) { - $scope.hasSettingsAccess = true; + + $scope.hasSettingsAccess = currentUser.allowedSections.indexOf("settings") > -1; + if ($scope.hasSettingsAccess) { + if ($scope.currentNode.id > -1) { - contentResource.getById($scope.currentNode.id).then(function (data) { + return contentResource.getById($scope.currentNode.id).then(function (data) { $scope.contentTypeId = data.contentTypeId; }); } @@ -43,6 +40,12 @@ function contentCreateController($scope, }); $q.all([getAllowedTypes, getCurrentUser]).then(function() { + if ($scope.hasSettingsAccess === true && $scope.allowedTypes.length === 0) { + return contentTypeResource.getCount().then(function(count) { + $scope.countTypes = count; + }); + } + }).then(function() { $scope.loading = false; }); @@ -60,13 +63,13 @@ function contentCreateController($scope, .path("/content/content/edit/" + $scope.currentNode.id) .search("doctype", docType.alias) .search("create", "true") - /* when we create a new node we want to make sure it uses the same + /* when we create a new node we want to make sure it uses the same language as what is selected in the tree */ .search("cculture", mainCulture) - /* when we create a new node we must make sure that any previously + /* when we create a new node we must make sure that any previously opened segments is reset */ .search("csegment", null) - /* when we create a new node we must make sure that any previously + /* when we create a new node we must make sure that any previously used blueprint is reset */ .search("blueprintId", null); close(); @@ -92,8 +95,6 @@ function contentCreateController($scope, } else { createBlank(docType); } - - navigationService.hideDialog(); } function createFromBlueprint(blueprintId) { @@ -127,7 +128,7 @@ function contentCreateController($scope, $scope.createOrSelectBlueprintIfAny = createOrSelectBlueprintIfAny; $scope.createFromBlueprint = createFromBlueprint; - // the current node changes behind the scenes when the context menu is clicked without closing + // the current node changes behind the scenes when the context menu is clicked without closing // the default menu first, so we must watch the current node and re-initialize accordingly var unbindModelWatcher = $scope.$watch("currentNode", initialize); $scope.$on('$destroy', function () { diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/unsupportedblock/unsupportedblock.editor.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/unsupportedblock/unsupportedblock.editor.html index d860b44b60..0317105a66 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/unsupportedblock/unsupportedblock.editor.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/unsupportedblock/unsupportedblock.editor.html @@ -4,9 +4,11 @@ {{block.label}}
- This Block is no longer supported in this context.
+ This content is no longer supported in this context.
You might want to remove this block, or contact your developer to take actions for making this block available again.

- Learn about this circumstance +
Block data:

     
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.controller.js index ba0d4415f5..7c608cb3f4 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.controller.js @@ -50,7 +50,7 @@ unsubscribe.push(eventsService.on("editors.documentType.saved", updateUsedElementTypes)); vm.requestRemoveBlockByIndex = function (index) { - localizationService.localizeMany(["general_delete", "blockEditor_confirmDeleteBlockMessage", "blockEditor_confirmDeleteBlockNotice"]).then(function (data) { + localizationService.localizeMany(["general_delete", "blockEditor_confirmDeleteBlockTypeMessage", "blockEditor_confirmDeleteBlockTypeNotice"]).then(function (data) { var contentElementType = vm.getElementTypeByKey($scope.model.value[index].contentElementTypeKey); overlayService.confirmDelete({ title: data[0], diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umb-block-list-property-editor.less b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umb-block-list-property-editor.less index a2c124a6ea..5a8c21250b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umb-block-list-property-editor.less +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umb-block-list-property-editor.less @@ -38,6 +38,45 @@ opacity: 1; } } + + &.--show-validation { + ng-form.ng-invalid-val-server-match-content > & { + border: 2px solid @formErrorText; + border-radius: @baseBorderRadius; + &::after { + content: "!"; + position: absolute; + top: -12px; + right: -12px; + display: inline-flex; + align-items: center; + justify-content: center; + width: 18px; + height: 18px; + border-radius: 50%; + font-size: 13px; + text-align: center; + font-weight: bold; + background-color: @errorBackground; + color: @errorText; + border: 2px solid @white; + font-weight: 900; + + animation-duration: 1.4s; + animation-iteration-count: infinite; + animation-name: umb-block-list__block--content--badge-bounce; + animation-timing-function: ease; + @keyframes umb-block-list__block--content--badge-bounce { + 0% { transform: translateY(0); } + 20% { transform: translateY(-6px); } + 40% { transform: translateY(0); } + 55% { transform: translateY(-3px); } + 70% { transform: translateY(0); } + 100% { transform: translateY(0); } + } + } + } + } } ng-form.ng-invalid-val-server-match-settings > .umb-block-list__block > .umb-block-list__block--actions { opacity: 1; @@ -109,44 +148,6 @@ ng-form.ng-invalid-val-server-match-settings > .umb-block-list__block > .umb-blo box-sizing: border-box; } - &.--show-validation { - ng-form.ng-invalid-val-server-match-content > .umb-block-list__block > & > div { - border: 2px solid @formErrorText; - border-radius: @baseBorderRadius; - &::after { - content: "!"; - position: absolute; - top: -12px; - right: -12px; - display: inline-flex; - align-items: center; - justify-content: center; - width: 18px; - height: 18px; - border-radius: 50%; - font-size: 13px; - text-align: center; - font-weight: bold; - background-color: @errorBackground; - color: @errorText; - border: 2px solid @white; - font-weight: 900; - - animation-duration: 1.4s; - animation-iteration-count: infinite; - animation-name: umb-block-list__block--content--badge-bounce; - animation-timing-function: ease; - @keyframes umb-block-list__block--content--badge-bounce { - 0% { transform: translateY(0); } - 20% { transform: translateY(-6px); } - 40% { transform: translateY(0); } - 55% { transform: translateY(-3px); } - 70% { transform: translateY(0); } - 100% { transform: translateY(0); } - } - } - } - } } .blockelement__draggable-element { @@ -187,25 +188,29 @@ ng-form.ng-invalid-val-server-match-settings > .umb-block-list__block > .umb-blo > .__plus { position: absolute; - pointer-events: none; // lets stop avoiding the mouse values in JS move event. - width: 24px; - height: 24px; - padding: 0; - border-radius: 3em; - border: 2px solid @blueMid; display: flex; justify-content: center; align-items: center; + pointer-events: none; // lets stop avoiding the mouse values in JS move event. + box-sizing: border-box; + width: 28px; + height: 28px; + margin-left: -16px - 8px; + margin-top: -16px; + padding: 0; + border-radius: 3em; + border: 2px solid @blueMid; color: @blueMid; + line-height: 22px; font-size: 20px; font-weight: 800; background-color: rgba(255, 255, 255, .96); box-shadow: 0 0 0 2px rgba(255, 255, 255, .96); - transform: scale(0) translate(-80%, -50%); + transform: scale(0); transition: transform 240ms ease-in; - animation: umb-block-list__block--create-button_after 800ms ease-in-out infinite; + animation: umb-block-list__block--create-button__plus 400ms ease-in-out infinite; - @keyframes umb-block-list__block--create-button_after { + @keyframes umb-block-list__block--create-button__plus { 0% { color: rgba(@blueMid, 0.8); } 50% { color: rgba(@blueMid, 1); } 100% { color: rgba(@blueMid, 0.8); } @@ -223,7 +228,7 @@ ng-form.ng-invalid-val-server-match-settings > .umb-block-list__block > .umb-blo transition-duration: 120ms; > .__plus { - transform: scale(1) translate(-80%, -50%); + transform: scale(1); transition-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1.275); } } diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umb-block-list-row.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umb-block-list-row.html index c2657985cf..c692c81eaf 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umb-block-list-row.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umb-block-list-row.html @@ -1,9 +1,9 @@ -
+
entry.contentUdi === block.content.udi); + var layoutIndex = vm.layout.findIndex(entry => entry.contentUdi === block.layout.contentUdi); if (layoutIndex === -1) { - throw new Error("Could not find layout entry of block with udi: "+block.content.udi) + throw new Error("Could not find layout entry of block with udi: "+block.layout.contentUdi) } setDirty(); @@ -310,7 +326,9 @@ blockObject.active = true; } - function editBlock(blockObject, openSettings, blockIndex, parentForm) { + function editBlock(blockObject, openSettings, blockIndex, parentForm, options) { + + options = options || {}; // this must be set if (blockIndex === undefined) { @@ -344,6 +362,7 @@ $parentForm: parentForm || vm.propertyForm, // pass in a $parentForm, this maintains the FormController hierarchy with the infinite editing view (if it contains a form) hideContent: blockObject.hideContentInOverlay, openSettings: openSettings === true, + createFlow: options.createFlow === true, liveEditing: liveEditing, title: blockObject.label, view: "views/common/infiniteeditors/blockeditor/blockeditor.html", @@ -358,15 +377,17 @@ blockObject.active = false; editorService.close(); }, - close: function() { - - if (liveEditing === true) { - // revert values when closing in liveediting mode. - blockObject.retrieveValuesFrom(blockContentClone, blockSettingsClone); - } - - if (wasNotActiveBefore === true) { - blockObject.active = false; + close: function(blockEditorModel) { + if (blockEditorModel.createFlow) { + deleteBlock(blockObject); + } else { + if (liveEditing === true) { + // revert values when closing in liveediting mode. + blockObject.retrieveValuesFrom(blockContentClone, blockSettingsClone); + } + if (wasNotActiveBefore === true) { + blockObject.active = false; + } } editorService.close(); } @@ -406,7 +427,7 @@ size: (amountOfAvailableTypes > 8 ? "medium" : "small"), filter: (amountOfAvailableTypes > 8), clickPasteItem: function(item, mouseEvent) { - if (item.type === "elementTypeArray") { + if (Array.isArray(item.pasteData)) { var indexIncrementor = 0; item.pasteData.forEach(function (entry) { if (requestPasteFromClipboard(createIndex + indexIncrementor, entry)) { @@ -432,7 +453,7 @@ if (inlineEditing === true) { activateBlock(vm.layout[createIndex].$block); } else if (inlineEditing === false && vm.layout[createIndex].$block.hideContentInOverlay !== true) { - editBlock(vm.layout[createIndex].$block, false, createIndex, blockPickerModel.$parentForm); + editBlock(vm.layout[createIndex].$block, false, createIndex, blockPickerModel.$parentForm, {createFlow: true}); } } } @@ -448,42 +469,28 @@ }; blockPickerModel.clickClearClipboard = function ($event) { - clipboardService.clearEntriesOfType("elementType", vm.availableContentTypesAliases); - clipboardService.clearEntriesOfType("elementTypeArray", vm.availableContentTypesAliases); + clipboardService.clearEntriesOfType(clipboardService.TYPES.ELEMENT_TYPE, vm.availableContentTypesAliases); }; blockPickerModel.clipboardItems = []; - var singleEntriesForPaste = clipboardService.retriveEntriesOfType("elementType", vm.availableContentTypesAliases); - singleEntriesForPaste.forEach(function (entry) { - blockPickerModel.clipboardItems.push( - { - type: "elementType", - date: entry.date, - pasteData: entry.data, - blockConfigModel: modelObject.getScaffoldFromAlias(entry.alias), - elementTypeModel: { - name: entry.label, - icon: entry.icon - } + var entriesForPaste = clipboardService.retriveEntriesOfType(clipboardService.TYPES.ELEMENT_TYPE, vm.availableContentTypesAliases); + entriesForPaste.forEach(function (entry) { + var pasteEntry = { + type: clipboardService.TYPES.ELEMENT_TYPE, + date: entry.date, + pasteData: entry.data, + elementTypeModel: { + name: entry.label, + icon: entry.icon } - ); - }); - - var arrayEntriesForPaste = clipboardService.retriveEntriesOfType("elementTypeArray", vm.availableContentTypesAliases); - arrayEntriesForPaste.forEach(function (entry) { - blockPickerModel.clipboardItems.push( - { - type: "elementTypeArray", - date: entry.date, - pasteData: entry.data, - blockConfigModel: {}, // no block configuration for paste items of elementTypeArray. - elementTypeModel: { - name: entry.label, - icon: entry.icon - } - } - ); + } + if(Array.isArray(pasteEntry.data) === false) { + pasteEntry.blockConfigModel = modelObject.getScaffoldFromAlias(entry.alias); + } else { + pasteEntry.blockConfigModel = {}; + } + blockPickerModel.clipboardItems.push(pasteEntry); }); blockPickerModel.clipboardItems.sort( (a, b) => { @@ -513,11 +520,11 @@ } localizationService.localize("clipboard_labelForArrayOfItemsFrom", [vm.model.label, contentNodeName]).then(function(localizedLabel) { - clipboardService.copyArray("elementTypeArray", aliases, elementTypesToCopy, localizedLabel, "icon-thumbnail-list", vm.model.id); + clipboardService.copyArray(clipboardService.TYPES.ELEMENT_TYPE, aliases, elementTypesToCopy, localizedLabel, "icon-thumbnail-list", vm.model.id); }); } function copyBlock(block) { - clipboardService.copy("elementType", block.content.contentTypeAlias, block.content, block.label); + clipboardService.copy(clipboardService.TYPES.ELEMENT_TYPE, block.content.contentTypeAlias, block.content, block.label); } function requestPasteFromClipboard(index, pasteEntry) { diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umbblocklistblock.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umbblocklistblock.component.js index 206fb0bb3a..4531612cba 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umbblocklistblock.component.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umbblocklistblock.component.js @@ -41,6 +41,9 @@ // Guess we'll leave it for now but means all things need to be copied to the $scope and then all // primitives need to be watched. + // let the Block know about its form + model.block.setParentForm(model.parentForm); + $scope.block = model.block; $scope.api = model.api; $scope.index = model.index; diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.controller.js index 4158c977be..0f4dcc180e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.controller.js @@ -22,18 +22,18 @@ // Loop through all inner properties: for (var k in obj) { - propClearingMethod(obj[k]); + propClearingMethod(obj[k], clipboardService.TYPES.RAW); } } } } - clipboardService.registrerClearPropertyResolver(clearNestedContentPropertiesForStorage) + clipboardService.registerClearPropertyResolver(clearNestedContentPropertiesForStorage, clipboardService.TYPES.ELEMENT_TYPE) function clearInnerNestedContentPropertiesForStorage(prop, propClearingMethod) { - // if we got an array, and it has a entry with ncContentTypeAlias this meants that we are dealing with a NestedContent property inside a NestedContent property. + // if we got an array, and it has a entry with ncContentTypeAlias this meants that we are dealing with a NestedContent property data. if ((Array.isArray(prop) && prop.length > 0 && prop[0].ncContentTypeAlias !== undefined)) { for (var i = 0; i < prop.length; i++) { @@ -44,13 +44,13 @@ // Loop through all inner properties: for (var k in obj) { - propClearingMethod(obj[k]); + propClearingMethod(obj[k], clipboardService.TYPES.RAW); } } } } - clipboardService.registrerClearPropertyResolver(clearInnerNestedContentPropertiesForStorage) + clipboardService.registerClearPropertyResolver(clearInnerNestedContentPropertiesForStorage, clipboardService.TYPES.RAW) }]); angular @@ -130,7 +130,7 @@ } localizationService.localize("clipboard_labelForArrayOfItemsFrom", [model.label, nodeName]).then(function (data) { - clipboardService.copyArray("elementTypeArray", aliases, vm.nodes, data, "icon-thumbnail-list", model.id, clearNodeForCopy); + clipboardService.copyArray(clipboardService.TYPES.ELEMENT_TYPE, aliases, vm.nodes, data, "icon-thumbnail-list", model.id, clearNodeForCopy); }); } @@ -210,7 +210,7 @@ size: availableItems.length > 6 ? "medium" : "small", availableItems: availableItems, clickPasteItem: function (item) { - if (item.type === "elementTypeArray") { + if (Array.isArray(item.data)) { _.each(item.data, function (entry) { pasteFromClipboard(entry); }); @@ -238,21 +238,9 @@ dialog.pasteItems = []; - var singleEntriesForPaste = clipboardService.retriveEntriesOfType("elementType", contentTypeAliases); - _.each(singleEntriesForPaste, function (entry) { + var entriesForPaste = clipboardService.retriveEntriesOfType(clipboardService.TYPES.ELEMENT_TYPE, contentTypeAliases); + _.each(entriesForPaste, function (entry) { dialog.pasteItems.push({ - type: "elementType", - date: entry.date, - name: entry.label, - data: entry.data, - icon: entry.icon - }); - }); - - var arrayEntriesForPaste = clipboardService.retriveEntriesOfType("elementTypeArray", contentTypeAliases); - _.each(arrayEntriesForPaste, function (entry) { - dialog.pasteItems.push({ - type: "elementTypeArray", date: entry.date, name: entry.label, data: entry.data, @@ -270,8 +258,7 @@ dialog.clickClearPaste = function ($event) { $event.stopPropagation(); $event.preventDefault(); - clipboardService.clearEntriesOfType("elementType", contentTypeAliases); - clipboardService.clearEntriesOfType("elementTypeArray", contentTypeAliases); + clipboardService.clearEntriesOfType(clipboardService.TYPES.ELEMENT_TYPE, contentTypeAliases); dialog.pasteItems = [];// This dialog is not connected via the clipboardService events, so we need to update manually. dialog.hideHeader = false; }; @@ -464,7 +451,7 @@ syncCurrentNode(); - clipboardService.copy("elementType", node.contentTypeAlias, node, null, null, null, clearNodeForCopy); + clipboardService.copy(clipboardService.TYPES.ELEMENT_TYPE, node.contentTypeAlias, node, null, null, null, clearNodeForCopy); $event.stopPropagation(); } @@ -475,6 +462,8 @@ return; } + newNode = clipboardService.parseContentForPaste(newNode, clipboardService.TYPES.ELEMENT_TYPE); + // generate a new key. newNode.key = String.CreateGuid(); @@ -486,7 +475,7 @@ } function checkAbilityToPasteContent() { - vm.showPaste = clipboardService.hasEntriesOfType("elementType", contentTypeAliases) || clipboardService.hasEntriesOfType("elementTypeArray", contentTypeAliases); + vm.showPaste = clipboardService.hasEntriesOfType(clipboardService.TYPES.ELEMENT_TYPE, contentTypeAliases); } eventsService.on("clipboardService.storageUpdate", checkAbilityToPasteContent); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textarea/textarea.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textarea/textarea.controller.js index 4a7fff99f8..4f3cb6770b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textarea/textarea.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textarea/textarea.controller.js @@ -1,42 +1,38 @@ function textAreaController($scope, validationMessageService) { // macro parameter editor doesn't contains a config object, - // so we create a new one to hold any properties + // so we create a new one to hold any properties if (!$scope.model.config) { $scope.model.config = {}; } - $scope.model.count = 0; - if (!$scope.model.config.maxChars) { - $scope.model.config.maxChars = false; - } - - $scope.model.maxlength = false; - if ($scope.model.config && $scope.model.config.maxChars) { - $scope.model.maxlength = true; - } + $scope.maxChars = $scope.model.config.maxChars || 0; + $scope.maxCharsLimit = ($scope.model.config && $scope.model.config.maxChars > 0); + $scope.charsCount = 0; + $scope.nearMaxLimit = false; + $scope.validLength = true; $scope.$on("formSubmitting", function() { - if ($scope.isLengthValid()) { + if ($scope.validLength) { $scope.textareaFieldForm.textarea.$setValidity("maxChars", true); } else { $scope.textareaFieldForm.textarea.$setValidity("maxChars", false); } }); - $scope.isLengthValid = function() { - if (!$scope.model.maxlength) { - return true; - } - return $scope.model.config.maxChars >= $scope.model.count; + function checkLengthVadility() { + $scope.validLength = !($scope.maxCharsLimit === true && $scope.charsCount > $scope.maxChars); } - $scope.model.change = function () { + $scope.change = function () { if ($scope.model.value) { - $scope.model.count = $scope.model.value.length; + $scope.charsCount = $scope.model.value.length; + checkLengthVadility(); + $scope.nearMaxLimit = $scope.maxCharsLimit === true && $scope.validLength === true && $scope.charsCount > Math.max($scope.maxChars*.8, $scope.maxChars-50); } } - $scope.model.change(); + $scope.model.onValueChanged = $scope.change; + $scope.change(); // Set the message to use for when a mandatory field isn't completed. // Will either use the one provided on the property type or a localised default. diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textarea/textarea.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textarea/textarea.html index 87f6ffeac9..2f183c29f0 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textarea/textarea.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textarea/textarea.html @@ -1,17 +1,29 @@
- + {{mandatoryMessage}} {{textareaFieldForm.textarea.errorMsg}} -
- %0% characters left. +
+

{{model.label}} %0% characters left.

+
-
- Maximum %0% characters, %1% too many. +
+

{{model.label}} Maximum %0% characters, %1% too many.

+
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textbox/textbox.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textbox/textbox.controller.js index b47c3584b3..b7c740e749 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textbox/textbox.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textbox/textbox.controller.js @@ -4,37 +4,41 @@ function textboxController($scope, validationMessageService) { if (!$scope.model.config) { $scope.model.config = {}; } - $scope.model.count = 0; - if (!$scope.model.config.maxChars) { - // 500 is the maximum number that can be stored - // in the database, so set it to the max, even - // if no max is specified in the config - $scope.model.config.maxChars = 500; - } + + // 512 is the maximum number that can be stored + // in the database, so set it to the max, even + // if no max is specified in the config + $scope.maxChars = Math.min($scope.model.config.maxChars || 512, 512); + $scope.charsCount = 0; + $scope.nearMaxLimit = false; + $scope.validLength = true; $scope.$on("formSubmitting", function() { - if ($scope.isLengthValid()) { + if ($scope.validLength === true) { $scope.textboxFieldForm.textbox.$setValidity("maxChars", true); } else { $scope.textboxFieldForm.textbox.$setValidity("maxChars", false); } }); - $scope.isLengthValid = function() { - return $scope.model.config.maxChars >= $scope.model.count; + function checkLengthVadility() { + $scope.validLength = $scope.charsCount <= $scope.maxChars; } - $scope.model.change = function () { + $scope.change = function () { if ($scope.model.value) { - $scope.model.count = $scope.model.value.length; + $scope.charsCount = $scope.model.value.length; + checkLengthVadility(); + $scope.nearMaxLimit = $scope.validLength && $scope.charsCount > Math.max($scope.maxChars*.8, $scope.maxChars-25); } } - $scope.model.change(); + $scope.model.onValueChanged = $scope.change; + $scope.change(); // Set the message to use for when a mandatory field isn't completed. // Will either use the one provided on the property type or a localised default. validationMessageService.getMandatoryMessage($scope.model.validation).then(function(value) { $scope.mandatoryMessage = value; - }); + }); } angular.module('umbraco').controller("Umbraco.PropertyEditors.textboxController", textboxController); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textbox/textbox.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textbox/textbox.html index 5d86259e93..5e135ea7d9 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textbox/textbox.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textbox/textbox.html @@ -1,27 +1,30 @@
- - + ng-keyup="change()" /> +

{{model.label}} {{textboxFieldForm.textbox.errorMsg}}

{{mandatoryMessage}}

-
-

{{model.label}} %0% characters left.

-

%0% characters left.

+
+

{{model.label}} %0% characters left.

+
-
-

{{model.label}} Maximum %0% characters, %1% too many.

- +
+

{{model.label}} Maximum %0% characters, %1% too many.

+
diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml index dff3615883..9610d7ab57 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml @@ -1845,8 +1845,9 @@ Mange hilsner fra Umbraco robotten Tilføj speciel visning Tilføj instillinger Overskriv label form - %0%.]]> - Indhold der benytter sig af denne blok vil gå bort. + %0%.]]> + %0%.]]> + Indholdet vil stadigt eksistere, men redigering af dette indhold vil ikke være muligt. Indholdet vil blive vist som ikke understøttet indhold. Billede Tilføj billede @@ -1856,6 +1857,8 @@ Mange hilsner fra Umbraco robotten Avanceret Skjuld indholds editoren Du har lavet ændringer til dette indhold. Er du sikker på at du vil kassere dem? + Annuller oprettelse? + diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml index 13f076a492..a7479a3392 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml @@ -2475,8 +2475,9 @@ To manage your website, simply open the Umbraco back office and start adding con Add custom view Add settings Overwrite label template - %0%.]]> - Content using this block will be lost. + %0%.]]> + %0%.]]> + The content of this block will still be present, editing of this content will no longer be available and will be shown as unsupported content. Thumbnail Add thumbnail @@ -2486,6 +2487,8 @@ To manage your website, simply open the Umbraco back office and start adding con Advanced Force hide content editor You have made changes to this content. Are you sure you want to discard them? + Discard creation? + What are Content Templates? diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml index c44b7d15a8..d00af6fe5e 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml @@ -2497,8 +2497,9 @@ To manage your website, simply open the Umbraco back office and start adding con Add custom view Add settings Overwrite label template - %0%.]]> - Content using this block will be lost. + %0%.]]> + %0%.]]> + The content of this block will still be present, editing of this content will no longer be available and will be shown as unsupported content. Thumbnail Add thumbnail @@ -2508,6 +2509,8 @@ To manage your website, simply open the Umbraco back office and start adding con Advanced Force hide content editor You have made changes to this content. Are you sure you want to discard them? + Discard creation? + What are Content Templates? diff --git a/src/Umbraco.Web/BlockListTemplateExtensions.cs b/src/Umbraco.Web/BlockListTemplateExtensions.cs index 413584bc8e..6e105a24d6 100644 --- a/src/Umbraco.Web/BlockListTemplateExtensions.cs +++ b/src/Umbraco.Web/BlockListTemplateExtensions.cs @@ -1,9 +1,9 @@ using System; -using System.Linq; using System.Web.Mvc; using System.Web.Mvc.Html; using Umbraco.Core.Models.Blocks; using Umbraco.Core.Models.PublishedContent; +using System.Web; namespace Umbraco.Web { @@ -12,7 +12,7 @@ namespace Umbraco.Web public const string DefaultFolder = "BlockList/"; public const string DefaultTemplate = "Default"; - public static MvcHtmlString GetBlockListHtml(this HtmlHelper html, BlockListModel model, string template = DefaultTemplate) + public static IHtmlString GetBlockListHtml(this HtmlHelper html, BlockListModel model, string template = DefaultTemplate) { if (model?.Count == 0) return new MvcHtmlString(string.Empty); @@ -20,11 +20,11 @@ namespace Umbraco.Web return html.Partial(view, model); } - public static MvcHtmlString GetBlockListHtml(this HtmlHelper html, IPublishedProperty property, string template = DefaultTemplate) => GetBlockListHtml(html, property?.GetValue() as BlockListModel, template); + public static IHtmlString GetBlockListHtml(this HtmlHelper html, IPublishedProperty property, string template = DefaultTemplate) => GetBlockListHtml(html, property?.GetValue() as BlockListModel, template); - public static MvcHtmlString GetBlockListHtml(this HtmlHelper html, IPublishedContent contentItem, string propertyAlias) => GetBlockListHtml(html, contentItem, propertyAlias, DefaultTemplate); + public static IHtmlString GetBlockListHtml(this HtmlHelper html, IPublishedContent contentItem, string propertyAlias) => GetBlockListHtml(html, contentItem, propertyAlias, DefaultTemplate); - public static MvcHtmlString GetBlockListHtml(this HtmlHelper html, IPublishedContent contentItem, string propertyAlias, string template) + public static IHtmlString GetBlockListHtml(this HtmlHelper html, IPublishedContent contentItem, string propertyAlias, string template) { if (propertyAlias == null) throw new ArgumentNullException(nameof(propertyAlias)); if (string.IsNullOrWhiteSpace(propertyAlias)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(propertyAlias)); @@ -34,11 +34,5 @@ namespace Umbraco.Web return GetBlockListHtml(html, prop?.GetValue() as BlockListModel, template); } - - public static MvcHtmlString GetBlockListHtml(this IPublishedProperty property, HtmlHelper html, string template = DefaultTemplate) => GetBlockListHtml(html, property?.GetValue() as BlockListModel, template); - - public static MvcHtmlString GetBlockListHtml(this IPublishedContent contentItem, HtmlHelper html, string propertyAlias) => GetBlockListHtml(html, contentItem, propertyAlias, DefaultTemplate); - - public static MvcHtmlString GetBlockListHtml(this IPublishedContent contentItem, HtmlHelper html, string propertyAlias, string template) => GetBlockListHtml(html, contentItem, propertyAlias, template); } } diff --git a/src/Umbraco.Web/Compose/BlockEditorComponent.cs b/src/Umbraco.Web/Compose/BlockEditorComponent.cs new file mode 100644 index 0000000000..a8b4cfb8ca --- /dev/null +++ b/src/Umbraco.Web/Compose/BlockEditorComponent.cs @@ -0,0 +1,202 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Umbraco.Core; +using Umbraco.Core.Composing; +using Umbraco.Core.Models.Blocks; +using Umbraco.Core.PropertyEditors; + +namespace Umbraco.Web.Compose +{ + /// + /// A component for Block editors used to bind to events + /// + public class BlockEditorComponent : IComponent + { + private ComplexPropertyEditorContentEventHandler _handler; + private readonly BlockListEditorDataConverter _converter = new BlockListEditorDataConverter(); + + public void Initialize() + { + _handler = new ComplexPropertyEditorContentEventHandler( + Constants.PropertyEditors.Aliases.BlockList, + ReplaceBlockListUdis); + } + + public void Terminate() => _handler?.Dispose(); + + private string ReplaceBlockListUdis(string rawJson, bool onlyMissingUdis) + { + // the block editor doesn't ever have missing UDIs so when this is true there's nothing to process + if (onlyMissingUdis) return rawJson; + + return ReplaceBlockListUdis(rawJson, null); + } + + // internal for tests + internal string ReplaceBlockListUdis(string rawJson, Func createGuid = null) + { + // used so we can test nicely + if (createGuid == null) + createGuid = () => Guid.NewGuid(); + + if (string.IsNullOrWhiteSpace(rawJson) || !rawJson.DetectIsJson()) + return rawJson; + + // Parse JSON + // This will throw a FormatException if there are null UDIs (expected) + var blockListValue = _converter.Deserialize(rawJson); + + UpdateBlockListRecursively(blockListValue, createGuid); + + return JsonConvert.SerializeObject(blockListValue.BlockValue); + } + + private void UpdateBlockListRecursively(BlockEditorData blockListData, Func createGuid) + { + var oldToNew = new Dictionary(); + MapOldToNewUdis(oldToNew, blockListData.BlockValue.ContentData, createGuid); + MapOldToNewUdis(oldToNew, blockListData.BlockValue.SettingsData, createGuid); + + for (var i = 0; i < blockListData.References.Count; i++) + { + var reference = blockListData.References[i]; + var hasContentMap = oldToNew.TryGetValue(reference.ContentUdi, out var contentMap); + Udi settingsMap = null; + var hasSettingsMap = reference.SettingsUdi != null && oldToNew.TryGetValue(reference.SettingsUdi, out settingsMap); + + if (hasContentMap) + { + // replace the reference + blockListData.References.RemoveAt(i); + blockListData.References.Insert(i, new ContentAndSettingsReference(contentMap, hasSettingsMap ? settingsMap : null)); + } + } + + // build the layout with the new UDIs + var layout = (JArray)blockListData.Layout; + layout.Clear(); + foreach (var reference in blockListData.References) + { + layout.Add(JObject.FromObject(new BlockListLayoutItem + { + ContentUdi = reference.ContentUdi, + SettingsUdi = reference.SettingsUdi + })); + } + + + RecursePropertyValues(blockListData.BlockValue.ContentData, createGuid); + RecursePropertyValues(blockListData.BlockValue.SettingsData, createGuid); + } + + private void RecursePropertyValues(IEnumerable blockData, Func createGuid) + { + foreach (var data in blockData) + { + // check if we need to recurse (make a copy of the dictionary since it will be modified) + foreach (var propertyAliasToBlockItemData in new Dictionary(data.RawPropertyValues)) + { + if (propertyAliasToBlockItemData.Value is JToken jtoken) + { + if (ProcessJToken(jtoken, createGuid, out var result)) + { + // need to re-save this back to the RawPropertyValues + data.RawPropertyValues[propertyAliasToBlockItemData.Key] = result; + } + } + else + { + var asString = propertyAliasToBlockItemData.Value?.ToString(); + + if (asString != null && asString.DetectIsJson()) + { + // this gets a little ugly because there could be some other complex editor that contains another block editor + // and since we would have no idea how to parse that, all we can do is try JSON Path to find another block editor + // of our type + var json = JToken.Parse(asString); + if (ProcessJToken(json, createGuid, out var result)) + { + // need to re-save this back to the RawPropertyValues + data.RawPropertyValues[propertyAliasToBlockItemData.Key] = result; + } + } + } + } + } + } + + private bool ProcessJToken(JToken json, Func createGuid, out JToken result) + { + var updated = false; + result = json; + + // select all tokens (flatten) + var allProperties = json.SelectTokens("$..*").Select(x => x.Parent as JProperty).WhereNotNull().ToList(); + foreach (var prop in allProperties) + { + if (prop.Name == Constants.PropertyEditors.Aliases.BlockList) + { + // get it's parent 'layout' and it's parent's container + var layout = prop.Parent?.Parent as JProperty; + if (layout != null && layout.Parent is JObject layoutJson) + { + // recurse + var blockListValue = _converter.ConvertFrom(layoutJson); + UpdateBlockListRecursively(blockListValue, createGuid); + + // set new value + if (layoutJson.Parent != null) + { + // we can replace the object + layoutJson.Replace(JObject.FromObject(blockListValue.BlockValue)); + updated = true; + } + else + { + // if there is no parent it means that this json property was the root, in which case we just return + result = JObject.FromObject(blockListValue.BlockValue); + return true; + } + } + } + else if (prop.Name != "layout" && prop.Name != "contentData" && prop.Name != "settingsData" && prop.Name != "contentTypeKey") + { + // this is an arbitrary property that could contain a nested complex editor + var propVal = prop.Value?.ToString(); + // check if this might contain a nested Block Editor + if (!propVal.IsNullOrWhiteSpace() && propVal.DetectIsJson() && propVal.InvariantContains(Constants.PropertyEditors.Aliases.BlockList)) + { + if (_converter.TryDeserialize(propVal, out var nestedBlockData)) + { + // recurse + UpdateBlockListRecursively(nestedBlockData, createGuid); + // set the value to the updated one + prop.Value = JObject.FromObject(nestedBlockData.BlockValue); + updated = true; + } + } + } + } + + return updated; + } + + private void MapOldToNewUdis(Dictionary oldToNew, IEnumerable blockData, Func createGuid) + { + foreach (var data in blockData) + { + // This should never happen since a FormatException will be thrown if one is empty but we'll keep this here + if (data.Udi == null) + throw new InvalidOperationException("Block data cannot contain a null UDI"); + + // replace the UDIs + var newUdi = GuidUdi.Create(Constants.UdiEntityType.Element, createGuid()); + oldToNew[data.Udi] = newUdi; + data.Udi = newUdi; + } + } + } +} diff --git a/src/Umbraco.Web/Compose/BlockEditorComposer.cs b/src/Umbraco.Web/Compose/BlockEditorComposer.cs new file mode 100644 index 0000000000..e281bcb19f --- /dev/null +++ b/src/Umbraco.Web/Compose/BlockEditorComposer.cs @@ -0,0 +1,12 @@ +using Umbraco.Core; +using Umbraco.Core.Composing; + +namespace Umbraco.Web.Compose +{ + /// + /// A composer for Block editors to run a component + /// + [RuntimeLevel(MinLevel = RuntimeLevel.Run)] + public class BlockEditorComposer : ComponentComposer, ICoreComposer + { } +} diff --git a/src/Umbraco.Web/Compose/NestedContentPropertyComponent.cs b/src/Umbraco.Web/Compose/NestedContentPropertyComponent.cs index 5794a2734e..633e814bd9 100644 --- a/src/Umbraco.Web/Compose/NestedContentPropertyComponent.cs +++ b/src/Umbraco.Web/Compose/NestedContentPropertyComponent.cs @@ -6,67 +6,32 @@ using Umbraco.Core; using Umbraco.Core.Composing; using Umbraco.Core.Events; using Umbraco.Core.Models; +using Umbraco.Core.PropertyEditors; using Umbraco.Core.Services; using Umbraco.Core.Services.Implement; using Umbraco.Web.PropertyEditors; namespace Umbraco.Web.Compose { + + /// + /// A component for NestedContent used to bind to events + /// public class NestedContentPropertyComponent : IComponent { + private ComplexPropertyEditorContentEventHandler _handler; + public void Initialize() { - ContentService.Copying += ContentService_Copying; - ContentService.Saving += ContentService_Saving; + _handler = new ComplexPropertyEditorContentEventHandler( + Constants.PropertyEditors.Aliases.NestedContent, + CreateNestedContentKeys); } - private void ContentService_Copying(IContentService sender, CopyEventArgs e) - { - // When a content node contains nested content property - // Check if the copied node contains a nested content - var nestedContentProps = e.Copy.Properties.Where(x => x.PropertyType.PropertyEditorAlias == Constants.PropertyEditors.Aliases.NestedContent); - UpdateNestedContentProperties(nestedContentProps, false); - } + public void Terminate() => _handler?.Dispose(); - private void ContentService_Saving(IContentService sender, ContentSavingEventArgs e) - { - // One or more content nodes could be saved in a bulk publish - foreach (var entity in e.SavedEntities) - { - // When a content node contains nested content property - // Check if the copied node contains a nested content - var nestedContentProps = entity.Properties.Where(x => x.PropertyType.PropertyEditorAlias == Constants.PropertyEditors.Aliases.NestedContent); - UpdateNestedContentProperties(nestedContentProps, true); - } - } + private string CreateNestedContentKeys(string rawJson, bool onlyMissingKeys) => CreateNestedContentKeys(rawJson, onlyMissingKeys, null); - public void Terminate() - { - ContentService.Copying -= ContentService_Copying; - ContentService.Saving -= ContentService_Saving; - } - - private void UpdateNestedContentProperties(IEnumerable nestedContentProps, bool onlyMissingKeys) - { - // Each NC Property on a doctype - foreach (var nestedContentProp in nestedContentProps) - { - // A NC Prop may have one or more values due to cultures - var propVals = nestedContentProp.Values; - foreach (var cultureVal in propVals) - { - // Remove keys from published value & any nested NC's - var updatedPublishedVal = CreateNestedContentKeys(cultureVal.PublishedValue?.ToString(), onlyMissingKeys); - cultureVal.PublishedValue = updatedPublishedVal; - - // Remove keys from edited/draft value & any nested NC's - var updatedEditedVal = CreateNestedContentKeys(cultureVal.EditedValue?.ToString(), onlyMissingKeys); - cultureVal.EditedValue = updatedEditedVal; - } - } - } - - // internal for tests internal string CreateNestedContentKeys(string rawJson, bool onlyMissingKeys, Func createGuid = null) { @@ -77,7 +42,7 @@ namespace Umbraco.Web.Compose if (string.IsNullOrWhiteSpace(rawJson) || !rawJson.DetectIsJson()) return rawJson; - // Parse JSON + // Parse JSON var complexEditorValue = JToken.Parse(rawJson); UpdateNestedContentKeysRecursively(complexEditorValue, onlyMissingKeys, createGuid); @@ -98,7 +63,6 @@ namespace Umbraco.Web.Compose { // get it's sibling 'key' property var ncKeyVal = prop.Parent["key"] as JValue; - // TODO: This bool seems odd, if the key is null, shouldn't we fill it in regardless of onlyMissingKeys? if ((onlyMissingKeys && ncKeyVal == null) || (!onlyMissingKeys && ncKeyVal != null)) { // create or replace diff --git a/src/Umbraco.Web/Compose/NestedContentPropertyComposer.cs b/src/Umbraco.Web/Compose/NestedContentPropertyComposer.cs index 4c9d9dee1c..8e4cfbfffc 100644 --- a/src/Umbraco.Web/Compose/NestedContentPropertyComposer.cs +++ b/src/Umbraco.Web/Compose/NestedContentPropertyComposer.cs @@ -3,6 +3,9 @@ using Umbraco.Core.Composing; namespace Umbraco.Web.Compose { + /// + /// A composer for nested content to run a component + /// [RuntimeLevel(MinLevel = RuntimeLevel.Run)] public class NestedContentPropertyComposer : ComponentComposer, ICoreComposer { } diff --git a/src/Umbraco.Web/Install/InstallHelper.cs b/src/Umbraco.Web/Install/InstallHelper.cs index b74a20c27c..a998a172fc 100644 --- a/src/Umbraco.Web/Install/InstallHelper.cs +++ b/src/Umbraco.Web/Install/InstallHelper.cs @@ -30,6 +30,18 @@ namespace Umbraco.Web.Install private readonly IInstallationService _installationService; private InstallationType? _installationType; + + [Obsolete("Use the constructor with IInstallationService injected.")] + public InstallHelper( + IUmbracoContextAccessor umbracoContextAccessor, + DatabaseBuilder databaseBuilder, + ILogger logger, + IGlobalSettings globalSettings) + : this(umbracoContextAccessor, databaseBuilder, logger, globalSettings, Current.Factory.GetInstance()) + { + + } + public InstallHelper(IUmbracoContextAccessor umbracoContextAccessor, DatabaseBuilder databaseBuilder, ILogger logger, IGlobalSettings globalSettings, IInstallationService installationService) diff --git a/src/Umbraco.Web/PropertyEditors/TextboxConfiguration.cs b/src/Umbraco.Web/PropertyEditors/TextboxConfiguration.cs index c9f15e2e2f..641cc8e42a 100644 --- a/src/Umbraco.Web/PropertyEditors/TextboxConfiguration.cs +++ b/src/Umbraco.Web/PropertyEditors/TextboxConfiguration.cs @@ -7,7 +7,7 @@ namespace Umbraco.Web.PropertyEditors /// public class TextboxConfiguration { - [ConfigurationField("maxChars", "Maximum allowed characters", "textstringlimited", Description = "If empty, 500 character limit")] + [ConfigurationField("maxChars", "Maximum allowed characters", "textstringlimited", Description = "If empty, 512 character limit")] public int? MaxChars { get; set; } } } diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index b2068290f6..33051671ee 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -133,6 +133,8 @@ + +