diff --git a/src/Umbraco.Infrastructure/Models/Blocks/BlockListLayoutReference.cs b/src/Umbraco.Core/Models/Blocks/BlockListItem.cs
similarity index 65%
rename from src/Umbraco.Infrastructure/Models/Blocks/BlockListLayoutReference.cs
rename to src/Umbraco.Core/Models/Blocks/BlockListItem.cs
index f576bd927f..f4b5c489e7 100644
--- a/src/Umbraco.Infrastructure/Models/Blocks/BlockListLayoutReference.cs
+++ b/src/Umbraco.Core/Models/Blocks/BlockListItem.cs
@@ -7,10 +7,10 @@ namespace Umbraco.Core.Models.Blocks
///
/// Represents a layout item for the Block List editor
///
- [DataContract(Name = "blockListLayout", Namespace = "")]
- public class BlockListLayoutReference : IBlockReference
+ [DataContract(Name = "block", Namespace = "")]
+ public class BlockListItem : IBlockReference
{
- public BlockListLayoutReference(Udi contentUdi, IPublishedElement content, Udi settingsUdi, IPublishedElement settings)
+ public BlockListItem(Udi contentUdi, IPublishedElement content, Udi settingsUdi, IPublishedElement settings)
{
ContentUdi = contentUdi ?? throw new ArgumentNullException(nameof(contentUdi));
Content = content ?? throw new ArgumentNullException(nameof(content));
@@ -33,19 +33,13 @@ namespace Umbraco.Core.Models.Blocks
///
/// The content data item referenced
///
- ///
- /// This is ignored from serialization since it is just a reference to the actual data element
- ///
- [IgnoreDataMember]
+ [DataMember(Name = "content")]
public IPublishedElement Content { get; }
///
/// The settings data item referenced
///
- ///
- /// This is ignored from serialization since it is just a reference to the actual data element
- ///
- [IgnoreDataMember]
+ [DataMember(Name = "settings")]
public IPublishedElement Settings { get; }
}
}
diff --git a/src/Umbraco.Infrastructure/Models/Blocks/IBlockReference.cs b/src/Umbraco.Core/Models/Blocks/IBlockReference.cs
similarity index 100%
rename from src/Umbraco.Infrastructure/Models/Blocks/IBlockReference.cs
rename to src/Umbraco.Core/Models/Blocks/IBlockReference.cs
diff --git a/src/Umbraco.Infrastructure/Models/Blocks/BlockEditorModel.cs b/src/Umbraco.Infrastructure/Models/Blocks/BlockEditorModel.cs
deleted file mode 100644
index fa5a29fece..0000000000
--- a/src/Umbraco.Infrastructure/Models/Blocks/BlockEditorModel.cs
+++ /dev/null
@@ -1,36 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Runtime.Serialization;
-using Umbraco.Core.Models.PublishedContent;
-
-namespace Umbraco.Core.Models.Blocks
-{
- ///
- /// The base class for any strongly typed model for a Block editor implementation
- ///
- public abstract class BlockEditorModel
- {
- protected BlockEditorModel(IEnumerable contentData, IEnumerable settingsData)
- {
- ContentData = contentData ?? throw new ArgumentNullException(nameof(contentData));
- SettingsData = settingsData ?? new List();
- }
-
- public BlockEditorModel()
- {
- }
-
-
- ///
- /// The content data items of the Block List editor
- ///
- [DataMember(Name = "contentData")]
- public IEnumerable ContentData { get; set; } = new List();
-
- ///
- /// The settings data items of the Block List editor
- ///
- [DataMember(Name = "settingsData")]
- public IEnumerable SettingsData { get; set; } = new List();
- }
-}
diff --git a/src/Umbraco.Infrastructure/Models/Blocks/BlockItemData.cs b/src/Umbraco.Infrastructure/Models/Blocks/BlockItemData.cs
index e929a3568a..4459341adc 100644
--- a/src/Umbraco.Infrastructure/Models/Blocks/BlockItemData.cs
+++ b/src/Umbraco.Infrastructure/Models/Blocks/BlockItemData.cs
@@ -49,8 +49,14 @@ namespace Umbraco.Core.Models.Blocks
///
public class BlockPropertyValue
{
- public object Value { get; set; }
- public IPropertyType PropertyType { get; set; }
+ public BlockPropertyValue(object value, IPropertyType propertyType)
+ {
+ Value = value;
+ PropertyType = propertyType ?? throw new ArgumentNullException(nameof(propertyType));
+ }
+
+ public object Value { get; }
+ public IPropertyType PropertyType { get; }
}
}
}
diff --git a/src/Umbraco.Infrastructure/Models/Blocks/BlockListModel.cs b/src/Umbraco.Infrastructure/Models/Blocks/BlockListModel.cs
index 0492cf0d73..9a5a3af22a 100644
--- a/src/Umbraco.Infrastructure/Models/Blocks/BlockListModel.cs
+++ b/src/Umbraco.Infrastructure/Models/Blocks/BlockListModel.cs
@@ -1,4 +1,7 @@
-using System.Collections.Generic;
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
using System.Runtime.Serialization;
using Umbraco.Core.Models.PublishedContent;
@@ -8,26 +11,54 @@ namespace Umbraco.Core.Models.Blocks
/// The strongly typed model for the Block List editor
///
[DataContract(Name = "blockList", Namespace = "")]
- public class BlockListModel : BlockEditorModel
+ public class BlockListModel : IReadOnlyList
{
+ private readonly IReadOnlyList _layout = new List();
+
public static BlockListModel Empty { get; } = new BlockListModel();
private BlockListModel()
{
}
- public BlockListModel(IEnumerable contentData, IEnumerable settingsData, IEnumerable layout)
- : base(contentData, settingsData)
+ public BlockListModel(IEnumerable layout)
{
- Layout = layout;
+ _layout = layout.ToList();
}
- ///
- /// The layout items of the Block List editor
- ///
- [DataMember(Name = "layout")]
- public IEnumerable Layout { get; } = new List();
+ public int Count => _layout.Count;
+
+ ///
+ /// Get the block by index
+ ///
+ ///
+ ///
+ public BlockListItem this[int index] => _layout[index];
+
+ ///
+ /// Get the block by content Guid
+ ///
+ ///
+ ///
+ public BlockListItem this[Guid contentKey] => _layout.FirstOrDefault(x => x.Content.Key == contentKey);
+
+ ///
+ /// Get the block by content element Udi
+ ///
+ ///
+ ///
+ public BlockListItem this[Udi contentUdi]
+ {
+ get
+ {
+ if (!(contentUdi is GuidUdi guidUdi)) return null;
+ return _layout.FirstOrDefault(x => x.Content.Key == guidUdi.Guid);
+ }
+ }
+
+ public IEnumerator GetEnumerator() => _layout.GetEnumerator();
+
+ IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
-
}
}
diff --git a/src/Umbraco.Infrastructure/Models/Mapping/CommonMapper.cs b/src/Umbraco.Infrastructure/Models/Mapping/CommonMapper.cs
index 18741ae2bc..6df8408edb 100644
--- a/src/Umbraco.Infrastructure/Models/Mapping/CommonMapper.cs
+++ b/src/Umbraco.Infrastructure/Models/Mapping/CommonMapper.cs
@@ -47,17 +47,9 @@ namespace Umbraco.Web.Models.Mapping
public ContentTypeBasic GetContentType(IContentBase source, MapperContext context)
{
-
- var user = _umbracoContextAccessor.UmbracoContext?.Security?.CurrentUser;
- if (user?.AllowedSections.Any(x => x.Equals(Constants.Applications.Settings)) ?? false)
- {
- var contentType = _contentTypeBaseServiceProvider.GetContentTypeOf(source);
- var contentTypeBasic = context.Map(contentType);
-
- return contentTypeBasic;
- }
- //no access
- return null;
+ var contentType = _contentTypeBaseServiceProvider.GetContentTypeOf(source);
+ var contentTypeBasic = context.Map(contentType);
+ return contentTypeBasic;
}
public IEnumerable GetContentApps(IUmbracoEntity source)
diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyEditor.cs
index ba9bc07038..b78cae7ac4 100644
--- a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyEditor.cs
+++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyEditor.cs
@@ -54,7 +54,7 @@ namespace Umbraco.Web.PropertyEditors
internal class BlockEditorPropertyValueEditor : DataValueEditor, IDataValueReference
{
private readonly PropertyEditorCollection _propertyEditors;
- private readonly IDataTypeService _dataTypeService; // TODO: Not used yet but we'll need it to fill in the FromEditor/ToEditor
+ private readonly IDataTypeService _dataTypeService;
private readonly ILogger _logger;
private readonly BlockEditorValues _blockEditorValues;
@@ -65,7 +65,7 @@ namespace Umbraco.Web.PropertyEditors
_dataTypeService = dataTypeService;
_logger = logger;
_blockEditorValues = new BlockEditorValues(new BlockListEditorDataConverter(), contentTypeService, _logger);
- Validators.Add(new BlockEditorValidator(_blockEditorValues, propertyEditors, dataTypeService, textService));
+ Validators.Add(new BlockEditorValidator(_blockEditorValues, propertyEditors, dataTypeService, textService, contentTypeService));
Validators.Add(new MinMaxValidator(_blockEditorValues, textService));
}
@@ -246,19 +246,33 @@ namespace Umbraco.Web.PropertyEditors
public IEnumerable Validate(object value, string valueType, object dataTypeConfiguration)
{
var blockConfig = (BlockListConfiguration)dataTypeConfiguration;
+ if (blockConfig == null) yield break;
+
+ var validationLimit = blockConfig.ValidationLimit;
+ if (validationLimit == null) yield break;
+
var blockEditorData = _blockEditorValues.DeserializeAndClean(value);
- if ((blockEditorData == null && blockConfig?.ValidationLimit?.Min > 0)
- || (blockEditorData != null && blockEditorData.Layout.Count() < blockConfig?.ValidationLimit?.Min))
+
+ if ((blockEditorData == null && validationLimit.Min.HasValue && validationLimit.Min > 0)
+ || (blockEditorData != null && validationLimit.Min.HasValue && blockEditorData.Layout.Count() < validationLimit.Min))
{
yield return new ValidationResult(
- _textService.Localize("validation/entriesShort", new[] { blockConfig.ValidationLimit.Min.ToString(), (blockConfig.ValidationLimit.Min - blockEditorData.Layout.Count()).ToString() }),
+ _textService.Localize("validation/entriesShort", new[]
+ {
+ validationLimit.Min.ToString(),
+ (validationLimit.Min - blockEditorData.Layout.Count()).ToString()
+ }),
new[] { "minCount" });
}
- if (blockEditorData != null && blockEditorData.Layout.Count() > blockConfig?.ValidationLimit?.Max)
+ if (blockEditorData != null && validationLimit.Max.HasValue && blockEditorData.Layout.Count() > validationLimit.Max)
{
yield return new ValidationResult(
- _textService.Localize("validation/entriesExceed", new[] { blockConfig.ValidationLimit.Max.ToString(), (blockEditorData.Layout.Count() - blockConfig.ValidationLimit.Max).ToString() }),
+ _textService.Localize("validation/entriesExceed", new[]
+ {
+ validationLimit.Max.ToString(),
+ (blockEditorData.Layout.Count() - validationLimit.Max).ToString()
+ }),
new[] { "maxCount" });
}
}
@@ -267,10 +281,13 @@ namespace Umbraco.Web.PropertyEditors
internal class BlockEditorValidator : ComplexEditorValidator
{
private readonly BlockEditorValues _blockEditorValues;
+ private readonly IContentTypeService _contentTypeService;
- public BlockEditorValidator(BlockEditorValues blockEditorValues, PropertyEditorCollection propertyEditors, IDataTypeService dataTypeService, ILocalizedTextService textService) : base(propertyEditors, dataTypeService, textService)
+ public BlockEditorValidator(BlockEditorValues blockEditorValues, PropertyEditorCollection propertyEditors, IDataTypeService dataTypeService, ILocalizedTextService textService, IContentTypeService contentTypeService)
+ : base(propertyEditors, dataTypeService, textService)
{
_blockEditorValues = blockEditorValues;
+ _contentTypeService = contentTypeService;
}
protected override IEnumerable GetElementTypeValidation(object value)
@@ -278,8 +295,28 @@ namespace Umbraco.Web.PropertyEditors
var blockEditorData = _blockEditorValues.DeserializeAndClean(value);
if (blockEditorData != null)
{
- foreach (var row in blockEditorData.BlockValue.ContentData.Concat(blockEditorData.BlockValue.SettingsData))
+ // There is no guarantee that the client will post data for every property defined in the Element Type but we still
+ // need to validate that data for each property especially for things like 'required' data to work.
+ // Lookup all element types for all content/settings and then we can populate any empty properties.
+ var allElements = blockEditorData.BlockValue.ContentData.Concat(blockEditorData.BlockValue.SettingsData).ToList();
+ var allElementTypes = _contentTypeService.GetAll(allElements.Select(x => x.ContentTypeKey).ToArray()).ToDictionary(x => x.Key);
+
+ foreach (var row in allElements)
{
+ if (!allElementTypes.TryGetValue(row.ContentTypeKey, out var elementType))
+ throw new InvalidOperationException($"No element type found with key {row.ContentTypeKey}");
+
+ // now ensure missing properties
+ foreach (var elementTypeProp in elementType.CompositionPropertyTypes)
+ {
+ if (!row.PropertyValues.ContainsKey(elementTypeProp.Alias))
+ {
+ // set values to null
+ row.PropertyValues[elementTypeProp.Alias] = new BlockPropertyValue(null, elementTypeProp);
+ row.RawPropertyValues[elementTypeProp.Alias] = null;
+ }
+ }
+
var elementValidation = new ElementTypeValidationModel(row.ContentTypeAlias, row.Key);
foreach (var prop in row.PropertyValues)
{
@@ -331,7 +368,7 @@ namespace Umbraco.Web.PropertyEditors
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)))
+ foreach (var block in blockEditorData.BlockValue.ContentData.Where(x => blockEditorData.References.Any(r => r.ContentUdi == x.Udi)))
{
ResolveBlockItemData(block, contentTypePropertyTypes);
}
@@ -374,11 +411,7 @@ namespace Umbraco.Web.PropertyEditors
else
{
// set the value to include the resolved property type
- propValues[prop.Key] = new BlockPropertyValue
- {
- PropertyType = propType,
- Value = prop.Value
- };
+ propValues[prop.Key] = new BlockPropertyValue(prop.Value, propType);
}
}
diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockListConfiguration.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockListConfiguration.cs
index bfbbc80179..e461da40dc 100644
--- a/src/Umbraco.Infrastructure/PropertyEditors/BlockListConfiguration.cs
+++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockListConfiguration.cs
@@ -27,8 +27,7 @@ namespace Umbraco.Web.PropertyEditors
[JsonProperty("thumbnail")]
public string Thumbnail { get; set; }
- // TODO: This is named inconsistently in JS but renaming it needs to be done in quite a lot of places, this should be contentElementTypeKey
- [JsonProperty("contentTypeKey")]
+ [JsonProperty("contentElementTypeKey")]
public Guid ContentElementTypeKey { get; set; }
[JsonProperty("settingsElementTypeKey")]
diff --git a/src/Umbraco.Infrastructure/PropertyEditors/NestedContentPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/NestedContentPropertyEditor.cs
index 617dfea095..6ad465ecd5 100644
--- a/src/Umbraco.Infrastructure/PropertyEditors/NestedContentPropertyEditor.cs
+++ b/src/Umbraco.Infrastructure/PropertyEditors/NestedContentPropertyEditor.cs
@@ -90,11 +90,12 @@ namespace Umbraco.Web.PropertyEditors
_dataTypeService = dataTypeService;
_logger = logger;
_nestedContentValues = new NestedContentValues(contentTypeService);
- Validators.Add(new NestedContentValidator(_nestedContentValues, propertyEditors, dataTypeService, localizedTextService));
+ Validators.Add(new NestedContentValidator(_nestedContentValues, propertyEditors, dataTypeService, localizedTextService, contentTypeService));
_contentTypes = new Lazy>(() =>
_contentTypeService.GetAll().ToDictionary(c => c.Alias)
);
+
}
///
@@ -300,16 +301,47 @@ namespace Umbraco.Web.PropertyEditors
internal class NestedContentValidator : ComplexEditorValidator
{
private readonly NestedContentValues _nestedContentValues;
+ private readonly IContentTypeService _contentTypeService;
- public NestedContentValidator(NestedContentValues nestedContentValues, PropertyEditorCollection propertyEditors, IDataTypeService dataTypeService, ILocalizedTextService textService)
+ public NestedContentValidator(NestedContentValues nestedContentValues, PropertyEditorCollection propertyEditors, IDataTypeService dataTypeService, ILocalizedTextService textService, IContentTypeService contentTypeService)
: base(propertyEditors, dataTypeService, textService)
{
_nestedContentValues = nestedContentValues;
+ _contentTypeService = contentTypeService;
}
protected override IEnumerable GetElementTypeValidation(object value)
{
- foreach (var row in _nestedContentValues.GetPropertyValues(value))
+ var rows = _nestedContentValues.GetPropertyValues(value);
+ if (rows.Count == 0) yield break;
+
+ // There is no guarantee that the client will post data for every property defined in the Element Type but we still
+ // need to validate that data for each property especially for things like 'required' data to work.
+ // Lookup all element types for all content/settings and then we can populate any empty properties.
+ var allElementAliases = rows.Select(x => x.ContentTypeAlias).ToList();
+ // unfortunately we need to get all content types and post filter - but they are cached so its ok, there's
+ // no overload to lookup by many aliases.
+ var allElementTypes = _contentTypeService.GetAll().Where(x => allElementAliases.Contains(x.Alias)).ToDictionary(x => x.Alias);
+
+ foreach (var row in rows)
{
+ if (!allElementTypes.TryGetValue(row.ContentTypeAlias, out var elementType))
+ throw new InvalidOperationException($"No element type found with alias {row.ContentTypeAlias}");
+
+ // now ensure missing properties
+ foreach (var elementTypeProp in elementType.CompositionPropertyTypes)
+ {
+ if (!row.PropertyValues.ContainsKey(elementTypeProp.Alias))
+ {
+ // set values to null
+ row.PropertyValues[elementTypeProp.Alias] = new NestedContentValues.NestedContentPropertyValue
+ {
+ PropertyType = elementTypeProp,
+ Value = null
+ };
+ row.RawPropertyValues[elementTypeProp.Alias] = null;
+ }
+ }
+
var elementValidation = new ElementTypeValidationModel(row.ContentTypeAlias, row.Id);
foreach (var prop in row.PropertyValues)
{
diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs
index 25b22e1a9c..0c90a41fbd 100644
--- a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs
+++ b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs
@@ -58,7 +58,7 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters
var contentPublishedElements = new Dictionary();
var settingsPublishedElements = new Dictionary();
- var layout = new List();
+ var layout = new List();
var value = (string)inter;
if (string.IsNullOrWhiteSpace(value)) return BlockListModel.Empty;
@@ -120,11 +120,11 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters
settingsData = null;
}
- var layoutRef = new BlockListLayoutReference(contentGuidUdi, contentData, settingGuidUdi, settingsData);
+ var layoutRef = new BlockListItem(contentGuidUdi, contentData, settingGuidUdi, settingsData);
layout.Add(layoutRef);
}
- var model = new BlockListModel(contentPublishedElements.Values, settingsPublishedElements.Values, layout);
+ var model = new BlockListModel(layout);
return model;
}
}
diff --git a/src/Umbraco.Tests/PropertyEditors/BlockListPropertyValueConverterTests.cs b/src/Umbraco.Tests/PropertyEditors/BlockListPropertyValueConverterTests.cs
index 122ce1ff26..5d664c9c76 100644
--- a/src/Umbraco.Tests/PropertyEditors/BlockListPropertyValueConverterTests.cs
+++ b/src/Umbraco.Tests/PropertyEditors/BlockListPropertyValueConverterTests.cs
@@ -154,15 +154,13 @@ namespace Umbraco.Tests.PropertyEditors
var converted = editor.ConvertIntermediateToObject(publishedElement, propertyType, PropertyCacheLevel.None, json, false) as BlockListModel;
Assert.IsNotNull(converted);
- Assert.AreEqual(0, converted.ContentData.Count());
- Assert.AreEqual(0, converted.Layout.Count());
+ Assert.AreEqual(0, converted.Count);
json = string.Empty;
converted = editor.ConvertIntermediateToObject(publishedElement, propertyType, PropertyCacheLevel.None, json, false) as BlockListModel;
Assert.IsNotNull(converted);
- Assert.AreEqual(0, converted.ContentData.Count());
- Assert.AreEqual(0, converted.Layout.Count());
+ Assert.AreEqual(0, converted.Count);
}
[Test]
@@ -177,8 +175,7 @@ namespace Umbraco.Tests.PropertyEditors
var converted = editor.ConvertIntermediateToObject(publishedElement, propertyType, PropertyCacheLevel.None, json, false) as BlockListModel;
Assert.IsNotNull(converted);
- Assert.AreEqual(0, converted.ContentData.Count());
- Assert.AreEqual(0, converted.Layout.Count());
+ Assert.AreEqual(0, converted.Count);
json = @"{
layout: {},
@@ -186,8 +183,7 @@ data: []}";
converted = editor.ConvertIntermediateToObject(publishedElement, propertyType, PropertyCacheLevel.None, json, false) as BlockListModel;
Assert.IsNotNull(converted);
- Assert.AreEqual(0, converted.ContentData.Count());
- Assert.AreEqual(0, converted.Layout.Count());
+ Assert.AreEqual(0, converted.Count);
// Even though there is a layout, there is no data, so the conversion will result in zero elements in total
json = @"
@@ -205,8 +201,7 @@ data: []}";
converted = editor.ConvertIntermediateToObject(publishedElement, propertyType, PropertyCacheLevel.None, json, false) as BlockListModel;
Assert.IsNotNull(converted);
- Assert.AreEqual(0, converted.ContentData.Count());
- Assert.AreEqual(0, converted.Layout.Count());
+ Assert.AreEqual(0, converted.Count);
// Even though there is a layout and data, the data is invalid (missing required keys) so the conversion will result in zero elements in total
json = @"
@@ -228,8 +223,7 @@ data: []}";
converted = editor.ConvertIntermediateToObject(publishedElement, propertyType, PropertyCacheLevel.None, json, false) as BlockListModel;
Assert.IsNotNull(converted);
- Assert.AreEqual(0, converted.ContentData.Count());
- Assert.AreEqual(0, converted.Layout.Count());
+ Assert.AreEqual(0, converted.Count);
// Everthing is ok except the udi reference in the layout doesn't match the data so it will be empty
json = @"
@@ -252,8 +246,7 @@ data: []}";
converted = editor.ConvertIntermediateToObject(publishedElement, propertyType, PropertyCacheLevel.None, json, false) as BlockListModel;
Assert.IsNotNull(converted);
- Assert.AreEqual(1, converted.ContentData.Count());
- Assert.AreEqual(0, converted.Layout.Count());
+ Assert.AreEqual(0, converted.Count);
}
[Test]
@@ -283,14 +276,12 @@ data: []}";
var converted = editor.ConvertIntermediateToObject(publishedElement, propertyType, PropertyCacheLevel.None, json, false) as BlockListModel;
Assert.IsNotNull(converted);
- Assert.AreEqual(1, converted.ContentData.Count());
- var item0 = converted.ContentData.ElementAt(0);
+ Assert.AreEqual(1, converted.Count);
+ var item0 = converted[0].Content;
Assert.AreEqual(Guid.Parse("1304E1DD-AC87-4396-84FE-8A399231CB3D"), item0.Key);
Assert.AreEqual("Test1", item0.ContentType.Alias);
- Assert.AreEqual(1, converted.Layout.Count());
- var layout0 = converted.Layout.ElementAt(0);
- Assert.IsNull(layout0.Settings);
- Assert.AreEqual(UdiParser.Parse("umb://element/1304E1DDAC87439684FE8A399231CB3D"), layout0.ContentUdi);
+ Assert.IsNull(converted[0].Settings);
+ Assert.AreEqual(UdiParser.Parse("umb://element/1304E1DDAC87439684FE8A399231CB3D"), converted[0].ContentUdi);
}
[Test]
@@ -348,17 +339,15 @@ data: []}";
var converted = editor.ConvertIntermediateToObject(publishedElement, propertyType, PropertyCacheLevel.None, json, false) as BlockListModel;
Assert.IsNotNull(converted);
- Assert.AreEqual(3, converted.ContentData.Count());
- Assert.AreEqual(3, converted.SettingsData.Count());
- Assert.AreEqual(2, converted.Layout.Count());
+ Assert.AreEqual(2, converted.Count);
- var item0 = converted.Layout.ElementAt(0);
+ var item0 = converted[0];
Assert.AreEqual(Guid.Parse("1304E1DD-AC87-4396-84FE-8A399231CB3D"), item0.Content.Key);
Assert.AreEqual("Test1", item0.Content.ContentType.Alias);
Assert.AreEqual(Guid.Parse("1F613E26CE274898908A561437AF5100"), item0.Settings.Key);
Assert.AreEqual("Setting2", item0.Settings.ContentType.Alias);
- var item1 = converted.Layout.ElementAt(1);
+ var item1 = converted[1];
Assert.AreEqual(Guid.Parse("0A4A416E-547D-464F-ABCC-6F345C17809A"), item1.Content.Key);
Assert.AreEqual("Test2", item1.Content.ContentType.Alias);
Assert.AreEqual(Guid.Parse("63027539B0DB45E7B70459762D4E83DD"), item1.Settings.Key);
@@ -434,11 +423,9 @@ data: []}";
var converted = editor.ConvertIntermediateToObject(publishedElement, propertyType, PropertyCacheLevel.None, json, false) as BlockListModel;
Assert.IsNotNull(converted);
- Assert.AreEqual(2, converted.ContentData.Count());
- Assert.AreEqual(0, converted.SettingsData.Count());
- Assert.AreEqual(1, converted.Layout.Count());
+ Assert.AreEqual(1, converted.Count);
- var item0 = converted.Layout.ElementAt(0);
+ var item0 = converted[0];
Assert.AreEqual(Guid.Parse("0A4A416E-547D-464F-ABCC-6F345C17809A"), item0.Content.Key);
Assert.AreEqual("Test2", item0.Content.ContentType.Alias);
Assert.IsNull(item0.Settings);
diff --git a/src/Umbraco.Web.BackOffice/Umbraco.Web.BackOffice.csproj b/src/Umbraco.Web.BackOffice/Umbraco.Web.BackOffice.csproj
index 56bdbe6be9..6711d507e8 100644
--- a/src/Umbraco.Web.BackOffice/Umbraco.Web.BackOffice.csproj
+++ b/src/Umbraco.Web.BackOffice/Umbraco.Web.BackOffice.csproj
@@ -3,7 +3,7 @@
netcoreapp3.1Library
- 8
+ latest
diff --git a/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj b/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj
index 979764af7e..2c8ce61d9a 100644
--- a/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj
+++ b/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj
@@ -3,7 +3,7 @@
netcoreapp3.1Library
- 8
+ latest
diff --git a/src/Umbraco.Web.UI.Client/package-lock.json b/src/Umbraco.Web.UI.Client/package-lock.json
index be9ea2b358..be96697a7d 100644
--- a/src/Umbraco.Web.UI.Client/package-lock.json
+++ b/src/Umbraco.Web.UI.Client/package-lock.json
@@ -8614,9 +8614,9 @@
}
},
"lodash": {
- "version": "4.17.15",
- "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
- "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==",
+ "version": "4.17.19",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz",
+ "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==",
"dev": true
},
"lodash._basecopy": {
@@ -9326,9 +9326,9 @@
"dev": true
},
"nouislider": {
- "version": "14.4.0",
- "resolved": "https://registry.npmjs.org/nouislider/-/nouislider-14.4.0.tgz",
- "integrity": "sha512-D1aYsT73yWrSNcRfqcovE//htpfFqQwd+m+9UCIVSsRupwD7kodSj6j/DTJur5mqnv5HckSJvjHekyVZKLi6dA=="
+ "version": "14.6.0",
+ "resolved": "https://registry.npmjs.org/nouislider/-/nouislider-14.6.0.tgz",
+ "integrity": "sha512-KY0jH2pU4G/55wpoS5Ynyrc5xpOMZ10/Xr51sMYG/JxmYoPJGy3fG8mOMio0MJXerKp5Go3elwcODk3lX6mFMQ=="
},
"now-and-later": {
"version": "2.0.1",
@@ -15339,9 +15339,9 @@
"integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q=="
},
"tinymce": {
- "version": "4.9.10",
- "resolved": "https://registry.npmjs.org/tinymce/-/tinymce-4.9.10.tgz",
- "integrity": "sha512-vyzGG04Q44Y7zWIKA4c+G7MxMCsed6JkrhU+k0TaDs9XKAiS+e+D3Fzz5OIJ7p5keF7lbRK5czgI8T1JtouZqw=="
+ "version": "4.9.11",
+ "resolved": "https://registry.npmjs.org/tinymce/-/tinymce-4.9.11.tgz",
+ "integrity": "sha512-nkSLsax+VY5DBRjMFnHFqPwTnlLEGHCco82FwJF2JNH6W+5/ClvNC1P4uhD5lXPDNiDykSHR0XJdEh7w/ICHzA=="
},
"tmp": {
"version": "0.0.33",
diff --git a/src/Umbraco.Web.UI.Client/package.json b/src/Umbraco.Web.UI.Client/package.json
index 21445e3cb3..454d081fe7 100644
--- a/src/Umbraco.Web.UI.Client/package.json
+++ b/src/Umbraco.Web.UI.Client/package.json
@@ -38,11 +38,11 @@
"lazyload-js": "1.0.0",
"moment": "2.22.2",
"ng-file-upload": "12.2.13",
- "nouislider": "14.4.0",
+ "nouislider": "14.6.0",
"npm": "^6.14.7",
"signalr": "2.4.0",
"spectrum-colorpicker": "1.8.0",
- "tinymce": "4.9.10",
+ "tinymce": "4.9.11",
"typeahead.js": "0.11.1",
"underscore": "1.9.1"
},
@@ -78,7 +78,7 @@
"karma-phantomjs-launcher": "1.0.4",
"karma-spec-reporter": "0.0.32",
"less": "3.10.3",
- "lodash": "4.17.15",
+ "lodash": "4.17.19",
"marked": "^0.7.0",
"merge-stream": "2.0.0",
"run-sequence": "2.2.1"
diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umblogin.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umblogin.directive.js
index a63c30fb9e..3e287a6d6c 100644
--- a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umblogin.directive.js
+++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umblogin.directive.js
@@ -168,6 +168,7 @@
vm.inviteStep = 2;
}, function (err) {
+ formHelper.resetForm({ scope: $scope, hasErrors: true });
formHelper.handleError(err);
vm.invitedUserPasswordModel.buttonState = "error";
});
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 1d503dd3bc..9e51c2565b 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
@@ -582,6 +582,7 @@
eventsService.emit("content.unpublished", { content: $scope.content });
overlayService.close();
}, function (err) {
+ formHelper.resetForm({ scope: $scope, hasErrors: true });
$scope.page.buttonGroupState = 'error';
handleHttpException(err);
});
diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorheader.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorheader.directive.js
index 58f799e5af..76687dc0d6 100644
--- a/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorheader.directive.js
+++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorheader.directive.js
@@ -188,6 +188,7 @@ Use this directive to construct a header inside the main editor window.
@param {string} name The content name.
+@param {boolean=} nameRequired Require name to be defined. (True by default)
@param {array=} tabs Array of tabs. See example above.
@param {array=} navigation Array of sub views. See example above.
@param {boolean=} nameLocked Set to true to lock the name.
@@ -199,7 +200,7 @@ Use this directive to construct a header inside the main editor window.
@param {boolean=} hideAlias Set to true to hide alias.
@param {string=} description Add a description to the content.
@param {boolean=} hideDescription Set to true to hide description.
-@param {boolean=} setpagetitle If true the page title will be set to reflect the type of data the header is working with
+@param {boolean=} setpagetitle If true the page title will be set to reflect the type of data the header is working with
@param {string=} editorfor The localization to use to aid accessibility on the edit and create screen
**/
@@ -207,7 +208,7 @@ Use this directive to construct a header inside the main editor window.
'use strict';
function EditorHeaderDirective(editorService, localizationService, editorState, $rootScope) {
-
+
function link(scope, $injector) {
scope.vm = {};
@@ -329,11 +330,11 @@ Use this directive to construct a header inside the main editor window.
}
scope.accessibility.a11yMessageVisible = !isEmptyOrSpaces(scope.accessibility.a11yMessage);
scope.accessibility.a11yNameVisible = !isEmptyOrSpaces(scope.accessibility.a11yName);
-
+
});
}
-
+
function isEmptyOrSpaces(str) {
return str === null || str===undefined || str.trim ==='';
@@ -348,7 +349,7 @@ Use this directive to construct a header inside the main editor window.
});
}
-
+
var directive = {
transclude: true,
@@ -358,6 +359,7 @@ Use this directive to construct a header inside the main editor window.
scope: {
name: "=",
nameLocked: "=",
+ nameRequired: "=?",
menu: "=",
hideActionsMenu: "",
icon: "=",
diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valformmanager.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valformmanager.directive.js
index 47f1145600..77c26e066e 100644
--- a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valformmanager.directive.js
+++ b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valformmanager.directive.js
@@ -159,9 +159,6 @@ function valFormManager(serverValidationManager, $rootScope, $timeout, $location
element.removeClass(SHOW_VALIDATION_CLASS_NAME);
scope.showValidation = false;
notifySubView();
- //clear form state as at this point we retrieve new data from the server
- //and all validation will have cleared at this point
- formCtrl.$setPristine();
}));
var confirmed = false;
@@ -238,6 +235,8 @@ function valFormManager(serverValidationManager, $rootScope, $timeout, $location
}
});
+ // TODO: I'm unsure why this exists, i believe this may be a hack for something like tinymce which might automatically
+ // change a form value on load but we need it to be $pristine?
$timeout(function () {
formCtrl.$setPristine();
}, 1000);
diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertymsg.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertymsg.directive.js
index 979ac23bb3..26c0403f85 100644
--- a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertymsg.directive.js
+++ b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertymsg.directive.js
@@ -306,7 +306,15 @@ function valPropertyMsg(serverValidationManager, localizationService, angularHel
formCtrl.$setValidity('valPropertyMsg', false);
startWatch();
-
+ // This check is required in order to be able to reset ourselves and is typically for complex editor
+ // scenarios where the umb-property itself doesn't contain any ng-model controls which means that the
+ // above serverValidityResetter technique will not work to clear valPropertyMsg errors.
+ // In order for this to work we rely on the current form controller's $pristine state. This means that anytime
+ // the form is submitted whether there are validation errors or not the state must be reset... this is automatically
+ // taken care of with the formHelper.resetForm method that should be used in all cases. $pristine is required because it's
+ // a value that is cascaded to all form controls based on the hierarchy of child ng-model controls. This allows us to easily
+ // know if a value has changed. The alternative is what we used to do which was to put a deep $watch on the entire complex value
+ // which is hugely inefficient.
if (propertyErrors.length === 1 && hadError && !formCtrl.$pristine) {
var propertyValidationPath = umbPropCtrl.getValidationPath();
serverValidationManager.removePropertyError(propertyValidationPath, currentCulture, "", currentSegment);
diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valservermatch.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valservermatch.directive.js
index 1b4c593735..5f8600c8c0 100644
--- a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valservermatch.directive.js
+++ b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valservermatch.directive.js
@@ -54,6 +54,8 @@ function valServerMatch(serverValidationManager) {
function bindCallback(validationKey, matchVal, matchType) {
+ if (!matchVal) return;
+
if (Utilities.isString(matchVal)) {
matchVal = [matchVal]; // normalize to an array since the value can also natively be an array
}
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 fe57534ffb..5cc717be46 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
@@ -4,11 +4,11 @@
*
* @description
* Added in Umbraco 8.7. Model Object for dealing with data of Block Editors.
- *
+ *
* Block Editor Model Object provides the basic features for editing data of a block editor.
* Use the Block Editor Service to instantiate the Model Object.
* See {@link umbraco.services.blockEditorService blockEditorService}
- *
+ *
*/
(function () {
'use strict';
@@ -236,7 +236,7 @@
/**
- * Formats the content apps and ensures unsupported property's have the notsupported view (returns a promise)
+ * Formats the content apps and ensures unsupported property's have the notsupported view
* @param {any} scaffold
*/
function formatScaffoldData(scaffold) {
@@ -255,7 +255,7 @@
// could be empty in tests
if (!scaffold.apps) {
console.warn("No content apps found in scaffold");
- return $q.resolve(scaffold);
+ return scaffold;
}
// replace view of content app
@@ -271,22 +271,27 @@
scaffold.apps.splice(infoAppIndex, 1);
}
+ return scaffold;
+ }
+
+ /**
+ * Creates a settings content app, we only want to do this if settings is present on the specific block.
+ * @param {any} contentModel
+ */
+ function appendSettingsContentApp(contentModel, settingsName) {
+ if (!contentModel.apps) {
+ return
+ }
+
// add the settings app
- return localizationService.localize("blockEditor_tabBlockSettings").then(
- function (settingsName) {
-
- var settingsTab = {
- "name": settingsName,
- "alias": "settings",
- "icon": "icon-settings",
- "view": "views/common/infiniteeditors/blockeditor/blockeditor.settings.html",
- "hasError": false
- };
- scaffold.apps.push(settingsTab);
-
- return scaffold;
- }
- );
+ var settingsTab = {
+ "name": settingsName,
+ "alias": "settings",
+ "icon": "icon-settings",
+ "view": "views/common/infiniteeditors/blockeditor/blockeditor.settings.html",
+ "hasError": false
+ };
+ contentModel.apps.push(settingsTab);
}
/**
@@ -309,6 +314,8 @@
this.__watchers = [];
+ this.__labels = {};
+
// ensure basic part of data-structure is in place:
this.value = propertyModelValue;
this.value.layout = this.value.layout || {};
@@ -324,7 +331,7 @@
this.isolatedScope.blockObjects = {};
this.__watchers.push(this.isolatedScope.$on("$destroy", this.destroy.bind(this)));
- this.__watchers.push(propertyEditorScope.$on("postFormSubmitting", this.sync.bind(this)));
+ this.__watchers.push(propertyEditorScope.$on("formSubmittingFinalPhase", this.sync.bind(this)));
};
@@ -344,24 +351,25 @@
// update our values
this.value = propertyModelValue;
this.value.layout = this.value.layout || {};
- this.value.data = this.value.data || [];
+ this.value.contentData = this.value.contentData || [];
+ this.value.settingsData = this.value.settingsData || [];
// re-create the watchers
this.__watchers = [];
this.__watchers.push(this.isolatedScope.$on("$destroy", this.destroy.bind(this)));
- this.__watchers.push(propertyEditorScope.$on("postFormSubmitting", this.sync.bind(this)));
+ this.__watchers.push(propertyEditorScope.$on("formSubmittingFinalPhase", this.sync.bind(this)));
},
/**
* @ngdoc method
* @name getBlockConfiguration
* @methodOf umbraco.services.blockEditorModelObject
- * @description Get block configuration object for a given contentTypeKey.
- * @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.
+ * @description Get block configuration object for a given contentElementTypeKey.
+ * @param {string} key contentElementTypeKey to recive the configuration model for.
+ * @returns {Object | null} Configuration model for the that specific block. Or ´null´ if the contentElementTypeKey isnt available in the current block configurations.
*/
getBlockConfiguration: function (key) {
- return this.blockConfigurations.find(bc => bc.contentTypeKey === key) || null;
+ return this.blockConfigurations.find(bc => bc.contentElementTypeKey === key) || null;
},
/**
@@ -373,12 +381,24 @@
* @returns {Promise} A Promise object which resolves when all scaffold models are loaded.
*/
load: function () {
+
+ var self = this;
+
var tasks = [];
+ tasks.push(localizationService.localize("blockEditor_tabBlockSettings").then(
+ function (settingsName) {
+ // self.__labels might not exists anymore, this happens if this instance has been destroyed before the load is complete.
+ if(self.__labels) {
+ self.__labels.settingsName = settingsName;
+ }
+ }
+ ));
+
var scaffoldKeys = [];
this.blockConfigurations.forEach(blockConfiguration => {
- scaffoldKeys.push(blockConfiguration.contentTypeKey);
+ scaffoldKeys.push(blockConfiguration.contentElementTypeKey);
if (blockConfiguration.settingsElementTypeKey != null) {
scaffoldKeys.push(blockConfiguration.settingsElementTypeKey);
}
@@ -387,19 +407,11 @@
// removing duplicates.
scaffoldKeys = scaffoldKeys.filter((value, index, self) => self.indexOf(value) === index);
- var self = this;
-
scaffoldKeys.forEach(contentTypeKey => {
tasks.push(contentResource.getScaffoldByKey(-20, contentTypeKey).then(scaffold => {
// self.scaffolds might not exists anymore, this happens if this instance has been destroyed before the load is complete.
if (self.scaffolds) {
- return formatScaffoldData(scaffold).then(s => {
- self.scaffolds.push(s);
- return s;
- });
- }
- else {
- return $q.resolve(scaffold);
+ self.scaffolds.push(formatScaffoldData(scaffold));
}
}));
});
@@ -415,7 +427,7 @@
* @return {Array} array of strings representing alias.
*/
getAvailableAliasesForBlockContent: function () {
- return this.blockConfigurations.map(blockConfiguration => this.getScaffoldFromKey(blockConfiguration.contentTypeKey).contentTypeAlias);
+ return this.blockConfigurations.map(blockConfiguration => this.getScaffoldFromKey(blockConfiguration.contentElementTypeKey).contentTypeAlias);
},
/**
@@ -431,7 +443,7 @@
var blocks = [];
this.blockConfigurations.forEach(blockConfiguration => {
- var scaffold = this.getScaffoldFromKey(blockConfiguration.contentTypeKey);
+ var scaffold = this.getScaffoldFromKey(blockConfiguration.contentElementTypeKey);
if (scaffold) {
blocks.push({
blockConfigModel: blockConfiguration,
@@ -503,12 +515,12 @@
var contentScaffold;
if (blockConfiguration === null) {
- console.error("The block entry of " + contentUdi + " is not being initialized because its contentTypeKey is not allowed for this PropertyEditor");
+ console.error("The block of " + contentUdi + " is not being initialized because its contentTypeKey('" + dataModel.contentTypeKey + "') is not allowed for this PropertyEditor");
}
else {
- contentScaffold = this.getScaffoldFromKey(blockConfiguration.contentTypeKey);
+ contentScaffold = this.getScaffoldFromKey(blockConfiguration.contentElementTypeKey);
if (contentScaffold === null) {
- console.error("The block entry of " + contentUdi + " is not begin initialized cause its Element Type was not loaded.");
+ console.error("The block of " + contentUdi + " is not begin initialized cause its Element Type was not loaded.");
}
}
@@ -525,7 +537,7 @@
var blockObject = {};
// Set an angularJS cloneNode method, to avoid this object begin cloned.
blockObject.cloneNode = function () {
- return null;// angularJS accept this as a cloned value as long as the
+ return null;// angularJS accept this as a cloned value as long as the
}
blockObject.key = String.CreateGuid().replace(/-/g, "");
blockObject.config = Utilities.copy(blockConfiguration);
@@ -577,6 +589,9 @@
ensureUdiAndKey(blockObject.settings, settingsUdi);
mapToElementModel(blockObject.settings, settingsData);
+
+ // add settings content-app
+ appendSettingsContentApp(blockObject.content, this.__labels.settingsName);
}
}
@@ -623,7 +638,7 @@
// 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,
+ // NOTE: It seems like we should call this.__scope.$destroy(); since that is the only way to remove a scope from it's parent,
// however that is not the case since __scope is actually this.isolatedScope which gets cleaned up when the outer scope is
// destroyed. If we do that here it breaks the scope chain and validation.
delete this.__scope;
@@ -691,18 +706,18 @@
* @name create
* @methodOf umbraco.services.blockEditorModelObject
* @description Create a empty layout entry, notice the layout entry is not added to the property editors model layout object, since the layout sturcture depends on the property editor.
- * @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.
+ * @param {string} contentElementTypeKey the contentElementTypeKey of the block you wish to create, if contentElementTypeKey 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 contentElementTypeKey is unavaiaible.
*/
- create: function (contentTypeKey) {
+ create: function (contentElementTypeKey) {
- var blockConfiguration = this.getBlockConfiguration(contentTypeKey);
+ var blockConfiguration = this.getBlockConfiguration(contentElementTypeKey);
if (blockConfiguration === null) {
return null;
}
var entry = {
- contentUdi: createDataEntry(contentTypeKey, this.value.contentData)
+ contentUdi: createDataEntry(contentElementTypeKey, this.value.contentData)
}
if (blockConfiguration.settingsElementTypeKey != null) {
@@ -723,14 +738,14 @@
elementTypeDataModel = Utilities.copy(elementTypeDataModel);
- var contentTypeKey = elementTypeDataModel.contentTypeKey;
+ var contentElementTypeKey = elementTypeDataModel.contentTypeKey;
- var layoutEntry = this.create(contentTypeKey);
+ var layoutEntry = this.create(contentElementTypeKey);
if (layoutEntry === null) {
return null;
}
- var dataModel = getDataByUdi(layoutEntry.udi, this.value.contentData);
+ var dataModel = getDataByUdi(layoutEntry.contentUdi, this.value.contentData);
if (dataModel === null) {
return null;
}
diff --git a/src/Umbraco.Web.UI.Client/src/common/services/contenteditinghelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/contenteditinghelper.service.js
index cda10ee6df..4ffd0c3c0b 100644
--- a/src/Umbraco.Web.UI.Client/src/common/services/contenteditinghelper.service.js
+++ b/src/Umbraco.Web.UI.Client/src/common/services/contenteditinghelper.service.js
@@ -117,6 +117,9 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, editorSt
return $q.resolve(data);
}, function (err) {
+
+ formHelper.resetForm({ scope: args.scope, hasErrors: true });
+
self.handleSaveError({
showNotifications: args.showNotifications,
softRedirect: args.softRedirect,
diff --git a/src/Umbraco.Web.UI.Client/src/common/services/formhelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/formhelper.service.js
index 962961729b..5866e28b1e 100644
--- a/src/Umbraco.Web.UI.Client/src/common/services/formhelper.service.js
+++ b/src/Umbraco.Web.UI.Client/src/common/services/formhelper.service.js
@@ -17,10 +17,10 @@ function formHelper(angularHelper, serverValidationManager, notificationsService
* @function
*
* @description
- * Called by controllers when submitting a form - this ensures that all client validation is checked,
+ * Called by controllers when submitting a form - this ensures that all client validation is checked,
* server validation is cleared, that the correct events execute and status messages are displayed.
* This returns true if the form is valid, otherwise false if form submission cannot continue.
- *
+ *
* @param {object} args An object containing arguments for form submission
*/
submitForm: function (args) {
@@ -45,7 +45,10 @@ function formHelper(angularHelper, serverValidationManager, notificationsService
args.scope.$broadcast("formSubmitting", { scope: args.scope, action: args.action });
// Some property editors need to perform an action after all property editors have reacted to the formSubmitting.
- args.scope.$broadcast("postFormSubmitting", { scope: args.scope, action: args.action });
+ args.scope.$broadcast("formSubmittingFinalPhase", { scope: args.scope, action: args.action });
+
+ // Set the form state to submitted
+ currentForm.$setSubmitted();
//then check if the form is valid
if (!args.skipValidation) {
@@ -73,18 +76,32 @@ function formHelper(angularHelper, serverValidationManager, notificationsService
*
* @description
* Called by controllers when a form has been successfully submitted, this ensures the correct events are raised.
- *
+ *
* @param {object} args An object containing arguments for form submission
*/
resetForm: function (args) {
+
+ var currentForm;
+
if (!args) {
throw "args cannot be null";
}
if (!args.scope) {
throw "args.scope cannot be null";
}
+ if (!args.formCtrl) {
+ //try to get the closest form controller
+ currentForm = angularHelper.getRequiredCurrentForm(args.scope);
+ }
+ else {
+ currentForm = args.formCtrl;
+ }
- args.scope.$broadcast("formSubmitted", { scope: args.scope });
+ // Set the form state to pristine
+ currentForm.$setPristine();
+ currentForm.$setUntouched();
+
+ args.scope.$broadcast(args.hasErrors ? "formSubmittedValidationFailed" : "formSubmitted", { scope: args.scope });
},
showNotifications: function (args) {
@@ -109,7 +126,7 @@ function formHelper(angularHelper, serverValidationManager, notificationsService
* @description
* Needs to be called when a form submission fails, this will wire up all server validation errors in ModelState and
* add the correct messages to the notifications. If a server error has occurred this will show a ysod.
- *
+ *
* @param {object} err The error object returned from the http promise
*/
handleError: function (err) {
@@ -148,7 +165,7 @@ function formHelper(angularHelper, serverValidationManager, notificationsService
*
* @description
* This wires up all of the server validation model state so that valServer and valServerField directives work
- *
+ *
* @param {object} err The error object returned from the http promise
*/
handleServerValidation: function (modelState) {
diff --git a/src/Umbraco.Web.UI.Client/src/less/alerts.less b/src/Umbraco.Web.UI.Client/src/less/alerts.less
index 3907b59f58..3539e21064 100644
--- a/src/Umbraco.Web.UI.Client/src/less/alerts.less
+++ b/src/Umbraco.Web.UI.Client/src/less/alerts.less
@@ -7,6 +7,7 @@
// -------------------------
.alert {
+ position: relative;
padding: 8px 35px 8px 14px;
margin-bottom: @baseLineHeight;
background-color: @warningBackground;
@@ -98,3 +99,29 @@
.alert-block p + p {
margin-top: 5px;
}
+
+
+// Property error alerts
+// -------------------------
+.alert.property-error {
+
+ display: inline-block;
+ font-size: 14px;
+ padding: 6px 16px 6px 12px;
+ margin-bottom: 6px;
+
+ &::after {
+ content:'';
+ position: absolute;
+ bottom:-6px;
+ left: 6px;
+ width: 0;
+ height: 0;
+ border-left: 6px solid transparent;
+ border-right: 6px solid transparent;
+ border-top: 6px solid;
+ }
+ &.alert-error::after {
+ border-top-color: @errorBackground;
+ }
+}
diff --git a/src/Umbraco.Web.UI.Client/src/less/components/editor/umb-variant-switcher.less b/src/Umbraco.Web.UI.Client/src/less/components/editor/umb-variant-switcher.less
index 8dbc070856..eae25b273c 100644
--- a/src/Umbraco.Web.UI.Client/src/less/components/editor/umb-variant-switcher.less
+++ b/src/Umbraco.Web.UI.Client/src/less/components/editor/umb-variant-switcher.less
@@ -50,6 +50,19 @@ button.umb-variant-switcher__toggle {
font-weight: bold;
background-color: @errorBackground;
color: @errorText;
+
+ animation-duration: 1.4s;
+ animation-iteration-count: infinite;
+ animation-name: umb-variant-switcher__toggle--badge-bounce;
+ animation-timing-function: ease;
+ @keyframes umb-variant-switcher__toggle--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); }
+ }
}
}
}
@@ -226,6 +239,19 @@ button.umb-variant-switcher__toggle {
font-weight: bold;
background-color: @errorBackground;
color: @errorText;
+
+ animation-duration: 1.4s;
+ animation-iteration-count: infinite;
+ animation-name: umb-variant-switcher__name--badge-bounce;
+ animation-timing-function: ease;
+ @keyframes umb-variant-switcher__name--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); }
+ }
}
}
}
diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-editor-navigation-item.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-editor-navigation-item.less
index cb673e3c6f..2fc705b11b 100644
--- a/src/Umbraco.Web.UI.Client/src/less/components/umb-editor-navigation-item.less
+++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-editor-navigation-item.less
@@ -53,6 +53,39 @@
height: 4px;
}
}
+
+ // Validation
+ .show-validation &.-has-error {
+ color: @red;
+
+ &:hover {
+ color: @red !important;
+ }
+
+ &::before {
+ background-color: @red;
+ }
+
+ &:not(.is-active) {
+ .badge {
+ animation-duration: 1.4s;
+ animation-iteration-count: infinite;
+ animation-name: umb-sub-views-nav-item--badge-bounce;
+ animation-timing-function: ease;
+ @keyframes umb-sub-views-nav-item--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); }
+ }
+ }
+ .badge.--error-badge {
+ display: block;
+ }
+ }
+ }
}
&__action:active,
@@ -101,6 +134,10 @@
height: 12px;
min-width: 12px;
}
+ &.--error-badge {
+ display: none;
+ font-weight: 900;
+ }
}
&-text {
@@ -182,13 +219,3 @@
}
}
}
-
-// Validation
-.show-validation .umb-sub-views-nav-item__action.-has-error,
-.show-validation .umb-sub-views-nav-item > a.-has-error {
- color: @red;
-
- &::before {
- background-color: @red;
- }
-}
diff --git a/src/Umbraco.Web.UI.Client/src/less/main.less b/src/Umbraco.Web.UI.Client/src/less/main.less
index ce20b8dc88..2354e96d38 100644
--- a/src/Umbraco.Web.UI.Client/src/less/main.less
+++ b/src/Umbraco.Web.UI.Client/src/less/main.less
@@ -272,6 +272,7 @@ label:not([for]) {
/* CONTROL VALIDATION */
.umb-control-required {
color: @controlRequiredColor;
+ font-weight: 900;
}
.controls-row {
diff --git a/src/Umbraco.Web.UI.Client/src/less/mixins.less b/src/Umbraco.Web.UI.Client/src/less/mixins.less
index a87080a326..9739a90dae 100644
--- a/src/Umbraco.Web.UI.Client/src/less/mixins.less
+++ b/src/Umbraco.Web.UI.Client/src/less/mixins.less
@@ -138,7 +138,9 @@
// additional targetting of the ng-invalid class.
.formFieldState(@textColor: @gray-4, @borderColor: @gray-7, @backgroundColor: @gray-10) {
// Set the text color
- .control-label,
+ > .control-label,
+ > .umb-el-wrap > .control-label,
+ > .umb-el-wrap > .control-header > .control-label,
.help-block,
.help-inline {
color: @textColor;
diff --git a/src/Umbraco.Web.UI.Client/src/less/variables.less b/src/Umbraco.Web.UI.Client/src/less/variables.less
index 2f627f3ab3..840c6d529f 100644
--- a/src/Umbraco.Web.UI.Client/src/less/variables.less
+++ b/src/Umbraco.Web.UI.Client/src/less/variables.less
@@ -481,7 +481,7 @@
@formErrorText: @errorBackground;
@formErrorBackground: lighten(@errorBackground, 55%);
-@formErrorBorder: darken(spin(@errorBackground, -10), 3%);
+@formErrorBorder: @red;
@formSuccessText: @successBackground;
@formSuccessBackground: lighten(@successBackground, 48%);
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 f515cbb4ba..a08a05b0f7 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
@@ -1,6 +1,6 @@
angular.module("umbraco")
.controller("Umbraco.Editors.BlockEditorController",
- function ($scope, localizationService, formHelper) {
+ function ($scope, localizationService, formHelper, overlayService) {
var vm = this;
vm.model = $scope.model;
@@ -23,17 +23,14 @@ angular.module("umbraco")
if (contentApp) {
if (vm.model.hideContent) {
apps.splice(apps.indexOf(contentApp), 1);
- } else if (vm.model.openSettings !== true) {
- contentApp.active = true;
}
+ contentApp.active = (vm.model.openSettings !== true);
}
if (vm.model.settings && vm.model.settings.variants) {
var settingsApp = apps.find(entry => entry.alias === "settings");
if (settingsApp) {
- if (vm.model.openSettings) {
- settingsApp.active = true;
- }
+ settingsApp.active = (vm.model.openSettings === true);
}
}
@@ -42,6 +39,7 @@ angular.module("umbraco")
vm.submitAndClose = function () {
if (vm.model && vm.model.submit) {
+
// always keep server validations since this will be a nested editor and server validations are global
if (formHelper.submitForm({
scope: $scope,
@@ -49,13 +47,16 @@ angular.module("umbraco")
keepServerValidation: true
})) {
vm.model.submit(vm.model);
+ vm.saveButtonState = "success";
+ } else {
+ vm.saveButtonState = "error";
}
}
}
vm.close = function () {
if (vm.model && vm.model.close) {
- // TODO: At this stage there could very well have been server errors that have been cleared
+ // TODO: At this stage there could very well have been server errors that have been cleared
// but if we 'close' we are basically cancelling the value changes which means we'd want to cancel
// all of the server errors just cleared. It would be possible to do that but also quite annoying.
// The rudimentary way would be to:
@@ -67,6 +68,29 @@ 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) {
+ const confirm = {
+ title: localizations[0],
+ view: "default",
+ content: localizations[1],
+ submitButtonLabelKey: "general_discard",
+ submitButtonStyle: "danger",
+ closeButtonLabelKey: "general_cancel",
+ submit: function () {
+ overlayService.close();
+ vm.model.close(vm.model);
+ },
+ close: function () {
+ overlayService.close();
+ }
+ };
+ overlayService.open(confirm);
+ });
+
+ return;
+ }
// 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/common/infiniteeditors/blockeditor/blockeditor.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockeditor/blockeditor.html
index de18f13d2c..285e554c68 100644
--- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockeditor/blockeditor.html
+++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockeditor/blockeditor.html
@@ -6,6 +6,7 @@
diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/templatesections/templatesections.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/templatesections/templatesections.html
index 045a1403e2..b4e8d7fbe8 100644
--- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/templatesections/templatesections.html
+++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/templatesections/templatesections.html
@@ -45,7 +45,9 @@
s && s.vm && s.vm.constructor.name === "umbVariantContentController");
@@ -84,7 +84,7 @@
vm.validationLimit = vm.model.config.validationLimit;
vm.listWrapperStyles = {};
-
+
if (vm.model.config.maxPropertyWidth) {
vm.listWrapperStyles['max-width'] = vm.model.config.maxPropertyWidth;
}
@@ -100,10 +100,6 @@
} else if(vm.umbElementEditorContent && vm.umbElementEditorContent.getScope) {
scopeOfExistence = vm.umbElementEditorContent.getScope();
}
-
- // Create Model Object, to manage our data for this Block Editor.
- modelObject = blockEditorService.createModelObject(vm.model.value, vm.model.editor, vm.model.config.blocks, scopeOfExistence, $scope);
- modelObject.load().then(onLoaded);
copyAllBlocksAction = {
labelKey: "clipboard_labelForCopyAllEntries",
@@ -124,15 +120,20 @@
copyAllBlocksAction,
deleteAllBlocksAction
];
-
+
if (vm.umbProperty) {
vm.umbProperty.setPropertyActions(propertyActions);
}
+
+ // Create Model Object, to manage our data for this Block Editor.
+ modelObject = blockEditorService.createModelObject(vm.model.value, vm.model.editor, vm.model.config.blocks, scopeOfExistence, $scope);
+ modelObject.load().then(onLoaded);
+
};
// 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.
@@ -142,13 +143,13 @@
modelObject.update(newVal, $scope);
onLoaded();
}
-
+
function setDirty() {
if (vm.propertyForm) {
vm.propertyForm.$setDirty();
}
}
-
+
function onLoaded() {
// Store a reference to the layout model, because we need to maintain this model.
@@ -161,7 +162,7 @@
// $block must have the data property to be a valid BlockObject, if not its considered as a destroyed blockObject.
if (entry.$block === undefined || entry.$block === null || entry.$block.data === undefined) {
var block = getBlockObject(entry);
-
+
// If this entry was not supported by our property-editor it would return 'null'.
if (block !== null) {
entry.$block = block;
@@ -192,7 +193,7 @@
}
function getDefaultViewForBlock(block) {
-
+
if (block.config.unsupported === true)
return "views/propertyeditors/blocklist/blocklistentryeditors/unsupportedblock/unsupportedblock.editor.html";
@@ -201,39 +202,53 @@
return "views/propertyeditors/blocklist/blocklistentryeditors/labelblock/labelblock.editor.html";
}
+ /**
+ * Ensure that the containing content variant languag and current property culture is transfered along
+ * to the scaffolded content object representing this block.
+ * This is required for validation along with ensuring that the umb-property inheritance is constently maintained.
+ * @param {any} content
+ */
+ function ensureCultureData(content) {
+
+ if (!content) return;
+
+ if (vm.umbVariantContent.editor.content.language) {
+ // set the scaffolded content's language to the language of the current editor
+ content.language = vm.umbVariantContent.editor.content.language;
+ }
+ // currently we only ever deal with invariant content for blocks so there's only one
+ content.variants[0].tabs.forEach(tab => {
+ tab.properties.forEach(prop => {
+ // set the scaffolded property to the culture of the containing property
+ prop.culture = vm.umbProperty.property.culture;
+ });
+ });
+ }
+
function getBlockObject(entry) {
var block = modelObject.getBlockObject(entry);
if (block === null) return null;
- // ensure that the containing content variant language/culture is transfered along
- // to the scaffolded content object representing this block. This is required for validation
- // along with ensuring that the umb-property inheritance is constently maintained.
- if (vm.umbVariantContent.editor.content.language) {
- block.content.language = vm.umbVariantContent.editor.content.language;
- // currently we only ever deal with invariant content for blocks so there's only one
- block.content.variants[0].tabs.forEach(tab => {
- tab.properties.forEach(prop => {
- prop.culture = vm.umbVariantContent.editor.content.language.culture;
- });
- });
- }
+ ensureCultureData(block.content);
+ ensureCultureData(block.settings);
// TODO: Why is there a '/' prefixed? that means this will never work with virtual directories
block.view = (block.config.view ? "/" + block.config.view : getDefaultViewForBlock(block));
+ block.showValidation = block.config.view ? true : false;
block.hideContentInOverlay = block.config.forceHideContentEditorInOverlay === true || inlineEditing === true;
block.showSettings = block.config.settingsElementTypeKey != null;
- block.showCopy = vm.supportCopy && block.config.contentTypeKey != null;// if we have content, otherwise it doesn't make sense to copy.
+ block.showCopy = vm.supportCopy && block.config.contentElementTypeKey != null;// if we have content, otherwise it doesn't make sense to copy.
return block;
}
- function addNewBlock(index, contentTypeKey) {
+ function addNewBlock(index, contentElementTypeKey) {
// Create layout entry. (not added to property model jet.)
- var layoutEntry = modelObject.create(contentTypeKey);
+ var layoutEntry = modelObject.create(contentElementTypeKey);
if (layoutEntry === null) {
return false;
}
@@ -243,7 +258,7 @@
if (blockObject === null) {
return false;
}
-
+
// If we reach this line, we are good to add the layoutEntry and blockObject to our models.
// Add the Block Object to our layout entry.
@@ -251,7 +266,7 @@
// add layout entry at the decired location in layout.
vm.layout.splice(index, 0, layoutEntry);
-
+
// lets move focus to this new block.
vm.setBlockFocus(blockObject);
@@ -271,8 +286,12 @@
var removed = vm.layout.splice(layoutIndex, 1);
removed.forEach(x => {
// remove any server validation errors associated
- var guid = udiService.getKey(x.contentUdi);
- serverValidationManager.removePropertyError(guid, vm.umbProperty.property.culture, vm.umbProperty.property.segment, "", { matchType: "contains" });
+ var guids = [udiService.getKey(x.contentUdi), (x.settingsUdi ? udiService.getKey(x.settingsUdi) : null)];
+ guids.forEach(guid => {
+ if (guid) {
+ serverValidationManager.removePropertyError(guid, vm.umbProperty.property.culture, vm.umbProperty.property.segment, "", { matchType: "contains" });
+ }
+ })
});
modelObject.removeDataAndDestroyModel(block);
@@ -280,11 +299,11 @@
}
function deleteAllBlocks() {
- vm.layout.forEach(entry => {
- deleteBlock(entry.$block);
- });
+ while(vm.layout.length) {
+ deleteBlock(vm.layout[0].$block);
+ };
}
-
+
function activateBlock(blockObject) {
blockObject.active = true;
}
@@ -317,7 +336,7 @@
if (blockObject.config.settingsElementTypeKey) {
blockSettingsClone = Utilities.copy(blockObject.settings);
}
-
+
var blockEditorModel = {
$parentScope: $scope, // pass in a $parentScope, this maintains the scope inheritance in infinite editing
$parentForm: parentForm || vm.propertyForm, // pass in a $parentForm, this maintains the FormController hierarchy with the infinite editing view (if it contains a form)
@@ -325,7 +344,6 @@
openSettings: openSettings === true,
liveEditing: liveEditing,
title: blockObject.label,
- index: blockIndex,
view: "views/common/infiniteeditors/blockeditor/blockeditor.html",
size: blockObject.config.editorSize || "medium",
submit: function(blockEditorModel) {
@@ -366,11 +384,11 @@
vm.showCreateDialog = showCreateDialog;
function showCreateDialog(createIndex, $event) {
-
+
if (vm.blockTypePicker) {
return;
}
-
+
if (vm.availableBlockTypes.length === 0) {
return;
}
@@ -403,9 +421,9 @@
submit: function(blockPickerModel, mouseEvent) {
var added = false;
if (blockPickerModel && blockPickerModel.selectedItem) {
- added = addNewBlock(createIndex, blockPickerModel.selectedItem.blockConfigModel.contentTypeKey);
+ added = addNewBlock(createIndex, blockPickerModel.selectedItem.blockConfigModel.contentElementTypeKey);
}
-
+
if(!(mouseEvent.ctrlKey || mouseEvent.metaKey)) {
editorService.close();
if (added && vm.layout.length > createIndex) {
@@ -448,7 +466,7 @@
}
);
});
-
+
var arrayEntriesForPaste = clipboardService.retriveEntriesOfType("elementTypeArray", vm.availableContentTypesAliases);
arrayEntriesForPaste.forEach(function (entry) {
blockPickerModel.clipboardItems.push(
@@ -472,13 +490,13 @@
var requestCopyAllBlocks = function() {
var elementTypesToCopy = vm.layout.filter(entry => entry.$block.config.unsupported !== true).map(entry => entry.$block.content);
-
+
// list aliases
var aliases = elementTypesToCopy.map(content => content.contentTypeAlias);
// remove dublicates
aliases = aliases.filter((item, index) => aliases.indexOf(item) === index);
-
+
var contentNodeName = "";
if(vm.umbVariantContent) {
contentNodeName = vm.umbVariantContent.editor.content.name;
@@ -494,7 +512,7 @@
clipboardService.copy("elementType", block.content.contentTypeAlias, block.content, block.label);
}
function requestPasteFromClipboard(index, pasteEntry) {
-
+
if (pasteEntry === undefined) {
return false;
}
@@ -512,7 +530,7 @@
// set the BlockObject on our layout entry.
layoutEntry.$block = blockObject;
-
+
// insert layout entry at the decired location in layout.
vm.layout.splice(index, 0, layoutEntry);
@@ -565,7 +583,7 @@
copyBlock: copyBlock,
requestDeleteBlock: requestDeleteBlock,
deleteBlock: deleteBlock,
- openSettingsForBlock: openSettingsForBlock
+ openSettingsForBlock: openSettingsForBlock
}
vm.sortableOptions = {
@@ -594,15 +612,15 @@
var isMinRequirementGood = vm.validationLimit.min === null || vm.layout.length >= vm.validationLimit.min;
vm.propertyForm.minCount.$setValidity("minCount", isMinRequirementGood);
-
+
var isMaxRequirementGood = vm.validationLimit.max === null || vm.layout.length <= vm.validationLimit.max;
vm.propertyForm.maxCount.$setValidity("maxCount", isMaxRequirementGood);
-
+
}
}
unsubscribe.push($scope.$watch(() => vm.layout.length, onAmountOfBlocksChanged));
-
+
$scope.$on("$destroy", function () {
for (const subscription of unsubscribe) {
subscription();
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/dropdownFlexible/dropdownFlexible.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/dropdownFlexible/dropdownFlexible.controller.js
index afbb4feb20..4064df6a24 100644
--- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/dropdownFlexible/dropdownFlexible.controller.js
+++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/dropdownFlexible/dropdownFlexible.controller.js
@@ -15,14 +15,14 @@ angular.module("umbraco").controller("Umbraco.PropertyEditors.DropdownFlexibleCo
//ensure this is a bool, old data could store zeros/ones or string versions
$scope.model.config.multiple = Object.toBoolean($scope.model.config.multiple);
-
+
//ensure when form is saved that we don't store [] or [null] as string values in the database when no items are selected
$scope.$on("formSubmitting", function () {
- if ($scope.model.value.length === 0 || $scope.model.value[0] === null) {
+ if ($scope.model.value && ($scope.model.value.length === 0 || $scope.model.value[0] === null)) {
$scope.model.value = null;
}
});
-
+
function convertArrayToDictionaryArray(model){
//now we need to format the items in the dictionary because we always want to have an array
var newItems = [];
@@ -41,7 +41,7 @@ angular.module("umbraco").controller("Umbraco.PropertyEditors.DropdownFlexibleCo
var keys = _.keys($scope.model.config.items);
for (var i = 0; i < vals.length; i++) {
- var label = vals[i].value ? vals[i].value : vals[i];
+ var label = vals[i].value ? vals[i].value : vals[i];
newItems.push({ id: keys[i], sortOrder: vals[i].sortOrder, value: label });
}
@@ -65,7 +65,7 @@ angular.module("umbraco").controller("Umbraco.PropertyEditors.DropdownFlexibleCo
else {
throw "The items property must be either an array or a dictionary";
}
-
+
//sort the values
$scope.model.config.items.sort(function (a, b) { return (a.sortOrder > b.sortOrder) ? 1 : ((b.sortOrder > a.sortOrder) ? -1 : 0); });
@@ -80,7 +80,7 @@ angular.module("umbraco").controller("Umbraco.PropertyEditors.DropdownFlexibleCo
$scope.model.value = "";
}
}
-
+
// if we run in single mode we'll store the value in a local variable
// so we can pass an array as the model as our PropertyValueEditor expects that
$scope.model.singleDropdownValue = "";
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts.prevalues.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts.prevalues.html
index 15b66d856d..1240a61fbb 100644
--- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts.prevalues.html
+++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts.prevalues.html
@@ -1,50 +1,49 @@
+
+
+
diff --git a/src/Umbraco.Web.UI.NetCore/umbraco/config/lang/da.xml b/src/Umbraco.Web.UI.NetCore/umbraco/config/lang/da.xml
index ed526dccfd..fef9c1cf82 100644
--- a/src/Umbraco.Web.UI.NetCore/umbraco/config/lang/da.xml
+++ b/src/Umbraco.Web.UI.NetCore/umbraco/config/lang/da.xml
@@ -285,7 +285,7 @@
Tilføj en ny tekstboksFjern denne tekstboksIndholdsrod
- Inkluder ikke-udgivet indhold.
+ Medtag udkast: udgiv også ikke-publicerede sider.Denne værdi er skjult.Hvis du har brug for adgang til at se denne værdi, bedes du kontakte din web-administrator.Denne værdi er skjult.Hvilke sprog vil du gerne udgive? Alle sprog med indhold gemmes!
@@ -647,6 +647,7 @@
DesignOrdbogDimensioner
+ KassérNedHentRediger
@@ -668,6 +669,7 @@
IkonIdImporter
+ Inkludér undermapper i søgningSøg kun i denne mappeInfoIndre margen
@@ -1850,6 +1852,7 @@ Mange hilsner fra Umbraco robotten
IndstillingerAvanceretSkjuld indholds editoren
+ Du har lavet ændringer til dette indhold. Er du sikker på at du vil kassere dem?
diff --git a/src/Umbraco.Web.UI.NetCore/umbraco/config/lang/en.xml b/src/Umbraco.Web.UI.NetCore/umbraco/config/lang/en.xml
index df6bcb9e48..d72179fc75 100644
--- a/src/Umbraco.Web.UI.NetCore/umbraco/config/lang/en.xml
+++ b/src/Umbraco.Web.UI.NetCore/umbraco/config/lang/en.xml
@@ -295,7 +295,7 @@
Add another text boxRemove this text boxContent root
- Include unpublished content items.
+ Include drafts and unpublished content items.This value is hidden. If you need access to view this value please contact your website administrator.This value is hidden.What languages would you like to publish? All languages with content are saved!
@@ -675,6 +675,7 @@
DesignDictionaryDimensions
+ DiscardDownDownloadEdit
@@ -2467,6 +2468,7 @@ To manage your website, simply open the Umbraco back office and start adding con
SettingsAdvancedForce hide content editor
+ You have made changes to this content. Are you sure you want to discard them?What are Content Templates?
diff --git a/src/Umbraco.Web.UI.NetCore/umbraco/config/lang/en_us.xml b/src/Umbraco.Web.UI.NetCore/umbraco/config/lang/en_us.xml
index fa3a8da934..41efb12f51 100644
--- a/src/Umbraco.Web.UI.NetCore/umbraco/config/lang/en_us.xml
+++ b/src/Umbraco.Web.UI.NetCore/umbraco/config/lang/en_us.xml
@@ -300,7 +300,7 @@
Add another text boxRemove this text boxContent root
- Include unpublished content items.
+ Include drafts and unpublished content items.This value is hidden. If you need access to view this value please contact your website administrator.This value is hidden.What languages would you like to publish? All languages with content are saved!
@@ -682,6 +682,7 @@
DesignDictionaryDimensions
+ DiscardDownDownloadEdit
@@ -2487,6 +2488,7 @@ To manage your website, simply open the Umbraco back office and start adding con
SettingsAdvancedForce hide content editor
+ You have made changes to this content. Are you sure you want to discard them?What are Content Templates?
diff --git a/src/Umbraco.Web.UI/Views/Partials/BlockList/Default.cshtml b/src/Umbraco.Web.UI/Views/Partials/BlockList/Default.cshtml
index 19ae842759..c94a51f6d9 100644
--- a/src/Umbraco.Web.UI/Views/Partials/BlockList/Default.cshtml
+++ b/src/Umbraco.Web.UI/Views/Partials/BlockList/Default.cshtml
@@ -1,13 +1,13 @@
@inherits UmbracoViewPage
@using Umbraco.Core.Models.Blocks
@{
- if (Model?.Layout == null || !Model.Layout.Any()) { return; }
+ if (!Model.Any()) { return; }
}
- @foreach (var layout in Model.Layout)
+ @foreach (var block in Model)
{
- if (layout?.Udi == null) { continue; }
- var data = layout.Data;
- @Html.Partial("BlockList/Components/" + data.ContentType.Alias, layout)
+ if (block?.ContentUdi == null) { continue; }
+ var data = block.Content;
+ @Html.Partial("BlockList/Components/" + data.ContentType.Alias, block)
}
diff --git a/src/Umbraco.Web.Website/Umbraco.Web.Website.csproj b/src/Umbraco.Web.Website/Umbraco.Web.Website.csproj
index 24a3260a00..683fd2cc6d 100644
--- a/src/Umbraco.Web.Website/Umbraco.Web.Website.csproj
+++ b/src/Umbraco.Web.Website/Umbraco.Web.Website.csproj
@@ -3,6 +3,7 @@
netcoreapp3.1Library
+ latest
diff --git a/src/Umbraco.Web/BlockListTemplateExtensions.cs b/src/Umbraco.Web/BlockListTemplateExtensions.cs
index 1754eb4fc4..413584bc8e 100644
--- a/src/Umbraco.Web/BlockListTemplateExtensions.cs
+++ b/src/Umbraco.Web/BlockListTemplateExtensions.cs
@@ -14,7 +14,7 @@ namespace Umbraco.Web
public static MvcHtmlString GetBlockListHtml(this HtmlHelper html, BlockListModel model, string template = DefaultTemplate)
{
- if (model?.Layout == null || !model.Layout.Any()) return new MvcHtmlString(string.Empty);
+ if (model?.Count == 0) return new MvcHtmlString(string.Empty);
var view = DefaultFolder + template;
return html.Partial(view, model);