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.1 Library - 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.1 Library - 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: "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 @@
- + +
diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/itempicker/itempicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/overlays/itempicker/itempicker.controller.js index 17184ae9a3..c06a9400e7 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/itempicker/itempicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/itempicker/itempicker.controller.js @@ -26,7 +26,9 @@ function ItemPickerOverlay($scope, localizationService) { $scope.showTooltip = function(item, $event) { if (!item.tooltip) { - $scope.mouseLeave(); + if($scope.mouseLeave) { + $scope.mouseLeave(); + } return; } $scope.tooltip = { diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/user/user.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/overlays/user/user.controller.js index 0ed5239449..812e53a0a6 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/user/user.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/user/user.controller.js @@ -153,7 +153,7 @@ angular.module("umbraco") }, 2000); }, function (err) { - + formHelper.resetForm({ scope: $scope, hasErrors: true }); formHelper.handleError(err); $scope.changePasswordButtonState = "error"; diff --git a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-header.html b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-header.html index 7929f92371..8df6cbbc8f 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-header.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-header.html @@ -48,8 +48,8 @@ umb-auto-focus focus-on-filled="true" val-server-field="Name" - required - aria-required="true" + ng-required="nameRequired != null ? nameRequired : true" + aria-required="{{nameRequired != null ? nameRequired : true}}" aria-invalid="{{contentForm.headerNameForm.headerName.$invalid ? true : false}}" autocomplete="off" maxlength="255"/> diff --git a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-navigation-item.html b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-navigation-item.html index 484e0175c5..9e5669f443 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-navigation-item.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-navigation-item.html @@ -9,6 +9,7 @@ {{ vm.item.name }}
{{vm.item.badge.count}}
+
!
-
diff --git a/src/Umbraco.Web.UI.Client/src/views/content/protect.html b/src/Umbraco.Web.UI.Client/src/views/content/protect.html index 6377fd7c65..6fb9f78e67 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/protect.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/protect.html @@ -19,7 +19,13 @@

- + + +
- + + +
- + + on-confirm="vm.performDelete" + on-cancel="vm.cancel" + confirm-button-style="danger" + confirm-label-key="general_delete"> diff --git a/src/Umbraco.Web.UI.Client/src/views/dictionary/dictionary.create.controller.js b/src/Umbraco.Web.UI.Client/src/views/dictionary/dictionary.create.controller.js index 82c4e6e7b5..2be4e522c3 100644 --- a/src/Umbraco.Web.UI.Client/src/views/dictionary/dictionary.create.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/dictionary/dictionary.create.controller.js @@ -28,13 +28,14 @@ function DictionaryCreateController($scope, $location, dictionaryResource, navig navigationService.syncTree({ tree: "dictionary", path: currPath + "," + data, forceReload: true, activate: true }); // reset form state - formHelper.resetForm({ scope: $scope }); + formHelper.resetForm({ scope: $scope, formCtrl: this.createDictionaryForm }); // navigate to edit view var currentSection = appState.getSectionState("currentSection"); $location.path("/" + currentSection + "/dictionary/edit/" + data); }, function (err) { + formHelper.resetForm({ scope: $scope, formCtrl: this.createDictionaryForm, hasErrors: true }); if (err.data && err.data.message) { notificationsService.error(err.data.message); navigationService.hideMenu(); diff --git a/src/Umbraco.Web.UI.Client/src/views/dictionary/dictionary.edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/dictionary/dictionary.edit.controller.js index ea1ca00b21..7619c7abfc 100644 --- a/src/Umbraco.Web.UI.Client/src/views/dictionary/dictionary.edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/dictionary/dictionary.edit.controller.js @@ -86,7 +86,7 @@ function DictionaryEditController($scope, $routeParams, $location, dictionaryRes dictionaryResource.save(vm.content, vm.nameDirty) .then(function (data) { - formHelper.resetForm({ scope: $scope, notifications: data.notifications }); + formHelper.resetForm({ scope: $scope }); bindDictionary(data); @@ -94,6 +94,8 @@ function DictionaryEditController($scope, $routeParams, $location, dictionaryRes }, function (err) { + formHelper.resetForm({ scope: $scope, hasErrors: true }); + contentEditingHelper.handleSaveError({ err: err }); diff --git a/src/Umbraco.Web.UI.Client/src/views/documentTypes/create.controller.js b/src/Umbraco.Web.UI.Client/src/views/documentTypes/create.controller.js index 732aa898a7..2348b43852 100644 --- a/src/Umbraco.Web.UI.Client/src/views/documentTypes/create.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/documentTypes/create.controller.js @@ -47,12 +47,13 @@ function DocumentTypesCreateController($scope, $location, navigationService, con activate: true }); - formHelper.resetForm({ scope: $scope }); + formHelper.resetForm({ scope: $scope, formCtrl: this.createFolderForm }); var section = appState.getSectionState("currentSection"); }, function (err) { + formHelper.resetForm({ scope: $scope, formCtrl: this.createFolderForm, hasErrors: true }); $scope.error = err; }); @@ -83,9 +84,7 @@ function DocumentTypesCreateController($scope, $location, navigationService, con $location.search('create', null); $location.search('notemplate', null); - formHelper.resetForm({ - scope: $scope - }); + formHelper.resetForm({ scope: $scope, formCtrl: this.createDoctypeCollectionForm }); var section = appState.getSectionState("currentSection"); @@ -94,6 +93,7 @@ function DocumentTypesCreateController($scope, $location, navigationService, con }, function (err) { + formHelper.resetForm({ scope: $scope, formCtrl: this.createDoctypeCollectionForm, hasErrors: true }); $scope.error = err; //show any notifications diff --git a/src/Umbraco.Web.UI.Client/src/views/documentTypes/create.html b/src/Umbraco.Web.UI.Client/src/views/documentTypes/create.html index 5aba520b9b..1357965197 100644 --- a/src/Umbraco.Web.UI.Client/src/views/documentTypes/create.html +++ b/src/Umbraco.Web.UI.Client/src/views/documentTypes/create.html @@ -86,7 +86,8 @@ + text="Create template for the Parent Document Type"> + @@ -96,7 +97,8 @@ + text="Create template for the Item Document Type"> +
diff --git a/src/Umbraco.Web.UI.Client/src/views/languages/edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/languages/edit.controller.js index 96441e6101..a030e0d77f 100644 --- a/src/Umbraco.Web.UI.Client/src/views/languages/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/languages/edit.controller.js @@ -161,7 +161,7 @@ }, function (err) { vm.page.saveButtonState = "error"; - + formHelper.resetForm({ scope: $scope, hasErrors: true }); formHelper.handleError(err); }); diff --git a/src/Umbraco.Web.UI.Client/src/views/logViewer/search.html b/src/Umbraco.Web.UI.Client/src/views/logViewer/search.html index b4f910fd7b..6f828ac49f 100644 --- a/src/Umbraco.Web.UI.Client/src/views/logViewer/search.html +++ b/src/Umbraco.Web.UI.Client/src/views/logViewer/search.html @@ -39,7 +39,11 @@
- + + diff --git a/src/Umbraco.Web.UI.Client/src/views/macros/macros.create.controller.js b/src/Umbraco.Web.UI.Client/src/views/macros/macros.create.controller.js index e2559741a2..e8c5c550d0 100644 --- a/src/Umbraco.Web.UI.Client/src/views/macros/macros.create.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/macros/macros.create.controller.js @@ -25,7 +25,7 @@ function MacrosCreateController($scope, $location, macroResource, navigationServ navigationService.syncTree({ tree: "macros", path: currPath + "," + data, forceReload: true, activate: true }); // reset form state - formHelper.resetForm({ scope: $scope }); + formHelper.resetForm({ scope: $scope, formCtrl: this.createMacroForm }); // navigate to edit view var currentSection = appState.getSectionState("currentSection"); @@ -33,6 +33,7 @@ function MacrosCreateController($scope, $location, macroResource, navigationServ }, function (err) { + formHelper.resetForm({ scope: $scope, formCtrl: this.createMacroForm, hasErrors: true }); if (err.data && err.data.message) { notificationsService.error(err.data.message); navigationService.hideMenu(); diff --git a/src/Umbraco.Web.UI.Client/src/views/macros/macros.edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/macros/macros.edit.controller.js index 3261739d36..127f566995 100644 --- a/src/Umbraco.Web.UI.Client/src/views/macros/macros.edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/macros/macros.edit.controller.js @@ -33,10 +33,11 @@ function MacrosEditController($scope, $q, $routeParams, macroResource, editorSta vm.page.saveButtonState = "busy"; macroResource.saveMacro(vm.macro).then(function (data) { - formHelper.resetForm({ scope: $scope, notifications: data.notifications }); + formHelper.resetForm({ scope: $scope }); bindMacro(data); vm.page.saveButtonState = "success"; }, function (error) { + formHelper.resetForm({ scope: $scope, hasErrors: true }); contentEditingHelper.handleSaveError({ err: error }); diff --git a/src/Umbraco.Web.UI.Client/src/views/macros/views/settings.html b/src/Umbraco.Web.UI.Client/src/views/macros/views/settings.html index eada93f497..9825a6f919 100644 --- a/src/Umbraco.Web.UI.Client/src/views/macros/views/settings.html +++ b/src/Umbraco.Web.UI.Client/src/views/macros/views/settings.html @@ -38,10 +38,10 @@ - + - + diff --git a/src/Umbraco.Web.UI.Client/src/views/media/media.edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/media/media.edit.controller.js index a7ce67fc0b..c5d35ad241 100644 --- a/src/Umbraco.Web.UI.Client/src/views/media/media.edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/media/media.edit.controller.js @@ -206,6 +206,7 @@ function mediaEditController($scope, $routeParams, $q, appState, mediaResource, }, function(err) { + formHelper.resetForm({ scope: $scope, hasErrors: true }); contentEditingHelper.handleSaveError({ err: err, rebindCallback: contentEditingHelper.reBindChangedProperties($scope.content, err.data) diff --git a/src/Umbraco.Web.UI.Client/src/views/mediaTypes/create.controller.js b/src/Umbraco.Web.UI.Client/src/views/mediaTypes/create.controller.js index 8ff9106def..3b7e163840 100644 --- a/src/Umbraco.Web.UI.Client/src/views/mediaTypes/create.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/mediaTypes/create.controller.js @@ -30,11 +30,12 @@ function MediaTypesCreateController($scope, $location, navigationService, mediaT var currPath = node.path ? node.path : "-1"; navigationService.syncTree({ tree: "mediatypes", path: currPath + "," + folderId, forceReload: true, activate: true }); - formHelper.resetForm({ scope: $scope }); + formHelper.resetForm({ scope: $scope, formCtrl: this.createFolderForm }); var section = appState.getSectionState("currentSection"); - }, function(err) { + }, function (err) { + formHelper.resetForm({ scope: $scope, formCtrl: this.createFolderForm, hasErrors: true }); $scope.error = err; }); }; diff --git a/src/Umbraco.Web.UI.Client/src/views/member/member.edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/member/member.edit.controller.js index c0967df232..b70ae2cbf1 100644 --- a/src/Umbraco.Web.UI.Client/src/views/member/member.edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/member/member.edit.controller.js @@ -201,7 +201,7 @@ function MemberEditController($scope, $routeParams, $location, appState, memberR navigationService.syncTree({ tree: "member", path: path.split(","), forceReload: true }); }, function (err) { - + formHelper.resetForm({ scope: $scope, hasErrors: true }); contentEditingHelper.handleSaveError({ err: err, rebindCallback: contentEditingHelper.reBindChangedProperties($scope.content, err.data) diff --git a/src/Umbraco.Web.UI.Client/src/views/memberGroups/edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/memberGroups/edit.controller.js index 00c6dfbba8..4602a5aa25 100644 --- a/src/Umbraco.Web.UI.Client/src/views/memberGroups/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/memberGroups/edit.controller.js @@ -88,6 +88,7 @@ function MemberGroupsEditController($scope, $routeParams, appState, navigationSe }, function (err) { + formHelper.resetForm({ scope: $scope, hasErrors: true }); contentEditingHelper.handleSaveError({ err: err }); diff --git a/src/Umbraco.Web.UI.Client/src/views/memberTypes/create.controller.js b/src/Umbraco.Web.UI.Client/src/views/memberTypes/create.controller.js index adf6cbc8a6..d875442827 100644 --- a/src/Umbraco.Web.UI.Client/src/views/memberTypes/create.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/memberTypes/create.controller.js @@ -31,10 +31,10 @@ function MemberTypesCreateController($scope, $location, navigationService, membe var currPath = node.path ? node.path : "-1"; navigationService.syncTree({ tree: "membertypes", path: currPath + "," + folderId, forceReload: true, activate: true }); - formHelper.resetForm({ scope: $scope }); + formHelper.resetForm({ scope: $scope, formCtrl: this.createFolderForm }); }, function(err) { - + formHelper.resetForm({ scope: $scope, formCtrl: this.createFolderForm, hasErrors: true }); // TODO: Handle errors }); }; diff --git a/src/Umbraco.Web.UI.Client/src/views/packages/edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/packages/edit.controller.js index 63750ff0f2..de8ad6d1c4 100644 --- a/src/Umbraco.Web.UI.Client/src/views/packages/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/packages/edit.controller.js @@ -194,7 +194,7 @@ vm.package = updatedPackage; vm.buttonState = "success"; - formHelper.resetForm({ scope: $scope }); + formHelper.resetForm({ scope: $scope, formCtrl: editPackageForm }); if (create) { //if we are creating, then redirect to the correct url and reload @@ -204,6 +204,7 @@ } }, function (err) { + formHelper.resetForm({ scope: $scope, formCtrl: editPackageForm, hasErrors: true }); formHelper.handleError(err); vm.buttonState = "error"; }); diff --git a/src/Umbraco.Web.UI.Client/src/views/packages/edit.html b/src/Umbraco.Web.UI.Client/src/views/packages/edit.html index 9f91e1d0f7..45e2a4e376 100644 --- a/src/Umbraco.Web.UI.Client/src/views/packages/edit.html +++ b/src/Umbraco.Web.UI.Client/src/views/packages/edit.html @@ -122,73 +122,81 @@ on-remove="vm.removeContentItem()"> - + + + + -
- + +
- + +
- + +
- + +
- + +
- + +
- + +
diff --git a/src/Umbraco.Web.UI.Client/src/views/partialViewMacros/create.controller.js b/src/Umbraco.Web.UI.Client/src/views/partialViewMacros/create.controller.js index 52b58d094d..a843f420c8 100644 --- a/src/Umbraco.Web.UI.Client/src/views/partialViewMacros/create.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/partialViewMacros/create.controller.js @@ -46,12 +46,13 @@ activate: true }); - formHelper.resetForm({ scope: $scope }); + formHelper.resetForm({ scope: $scope, formCtrl: form }); var section = appState.getSectionState("currentSection"); }, function (err) { + formHelper.resetForm({ scope: $scope, formCtrl: form, hasErrors: true }); vm.createFolderError = err; }); diff --git a/src/Umbraco.Web.UI.Client/src/views/partialViews/create.controller.js b/src/Umbraco.Web.UI.Client/src/views/partialViews/create.controller.js index fbde1d5a07..97aa059806 100644 --- a/src/Umbraco.Web.UI.Client/src/views/partialViews/create.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/partialViews/create.controller.js @@ -56,12 +56,13 @@ activate: true }); - formHelper.resetForm({ scope: $scope }); + formHelper.resetForm({ scope: $scope, formCtrl: form }); var section = appState.getSectionState("currentSection"); }, function(err) { + formHelper.resetForm({ scope: $scope, formCtrl: form, hasErrors: true }); vm.createFolderError = err; }); } diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/inlineblock/inlineblock.editor.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/inlineblock/inlineblock.editor.controller.js index c2c86ca824..06c1424838 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/inlineblock/inlineblock.editor.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/inlineblock/inlineblock.editor.controller.js @@ -12,8 +12,8 @@ // boardcast the formSubmitting event to trigger syncronization or none-live property-editors $scope.$broadcast("formSubmitting", { scope: $scope }); // Some property editors need to performe an action after all property editors have reacted to the formSubmitting. - $scope.$broadcast("postFormSubmitting", { scope: $scope }); - + $scope.$broadcast("formSubmittingFinalPhase", { scope: $scope }); + block.active = false; } else { $scope.api.activateBlock(block); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/inlineblock/inlineblock.editor.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/inlineblock/inlineblock.editor.html index 360eeed8c0..e5a46cbdeb 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/inlineblock/inlineblock.editor.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/inlineblock/inlineblock.editor.html @@ -6,7 +6,7 @@ ng-focus="block.focus"> - {{block.label}} + {{block.label}}
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/inlineblock/inlineblock.editor.less b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/inlineblock/inlineblock.editor.less index 2be20946b8..08306deeba 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/inlineblock/inlineblock.editor.less +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/inlineblock/inlineblock.editor.less @@ -22,6 +22,7 @@ background-color: white; .caret { + vertical-align: middle; transform: rotate(-90deg); transition: transform 80ms ease-out; } @@ -32,7 +33,8 @@ vertical-align: middle; } - span { + span.name { + position: relative; display: inline-block; vertical-align: middle; } @@ -54,10 +56,55 @@ } } - &.--error { - border-color: @formErrorBorder !important; + + ng-form.ng-invalid-val-server-match-content > .umb-block-list__block > .umb-block-list__block--content > div > & { + > button { + color: @formErrorText; + span.caret { + border-top-color: @formErrorText; + } + } + } + ng-form.ng-invalid-val-server-match-content > .umb-block-list__block:not(.--active) > .umb-block-list__block--content > div > & { + > button { + span.name { + &::after { + content: "!"; + text-align: center; + position: absolute; + top: -6px; + right: -15px; + min-width: 10px; + color: @white; + background-color: @ui-active-type; + border: 2px solid @white; + border-radius: 50%; + font-size: 10px; + font-weight: bold; + padding: 2px; + line-height: 10px; + background-color: @formErrorText; + font-weight: 900; + + animation-duration: 1.4s; + animation-iteration-count: infinite; + animation-name: blockelement-inlineblock-editor--badge-bounce; + animation-timing-function: ease; + @keyframes blockelement-inlineblock-editor--badge-bounce { + 0% { transform: translateY(0); } + 20% { transform: translateY(-4px); } + 40% { transform: translateY(0); } + 55% { transform: translateY(-2px); } + 70% { transform: translateY(0); } + 100% { transform: translateY(0); } + } + } + } + } } } + + .blockelement-inlineblock-editor__inner { border-top: 1px solid @gray-8; background-color: @gray-12; diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/labelblock/labelblock.editor.less b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/labelblock/labelblock.editor.less index 51fb7242ef..f589249f97 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/labelblock/labelblock.editor.less +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/labelblock/labelblock.editor.less @@ -24,6 +24,7 @@ } span { + position: relative; display: inline-block; vertical-align: middle; } @@ -39,7 +40,42 @@ background-color: @ui-active; } - &.--error { - border-color: @formErrorBorder !important; + ng-form.ng-invalid-val-server-match-content > .umb-block-list__block > .umb-block-list__block--content > div > & { + color: @formErrorText; + } + ng-form.ng-invalid-val-server-match-content > .umb-block-list__block:not(.--active) > .umb-block-list__block--content > div > & { + span { + &::after { + content: "!"; + text-align: center; + position: absolute; + top: -6px; + right: -15px; + min-width: 10px; + color: @white; + background-color: @ui-active-type; + border: 2px solid @white; + border-radius: 50%; + font-size: 10px; + font-weight: bold; + padding: 2px; + line-height: 10px; + background-color: @formErrorText; + font-weight: 900; + + animation-duration: 1.4s; + animation-iteration-count: infinite; + animation-name: blockelement-inlineblock-editor--badge-bounce; + animation-timing-function: ease; + @keyframes blockelement-inlineblock-editor--badge-bounce { + 0% { transform: translateY(0); } + 20% { transform: translateY(-4px); } + 40% { transform: translateY(0); } + 55% { transform: translateY(-2px); } + 70% { transform: translateY(0); } + 100% { transform: translateY(0); } + } + } + } } } 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 dd9fc7f83d..ba0d4415f5 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 @@ -51,7 +51,7 @@ vm.requestRemoveBlockByIndex = function (index) { localizationService.localizeMany(["general_delete", "blockEditor_confirmDeleteBlockMessage", "blockEditor_confirmDeleteBlockNotice"]).then(function (data) { - var contentElementType = vm.getElementTypeByKey($scope.model.value[index].contentTypeKey); + var contentElementType = vm.getElementTypeByKey($scope.model.value[index].contentElementTypeKey); overlayService.confirmDelete({ title: data[0], content: localizationService.tokenReplace(data[1], [contentElementType.name]), @@ -70,19 +70,19 @@ vm.removeBlockByIndex = function (index) { $scope.model.value.splice(index, 1); }; - + vm.sortableOptions = { "ui-floating": true, items: "umb-block-card", cursor: "grabbing", placeholder: 'umb-block-card --sortable-placeholder' }; - + vm.getAvailableElementTypes = function () { return vm.elementTypes.filter(function (type) { return !$scope.model.value.find(function (entry) { - return type.key === entry.contentTypeKey; + return type.key === entry.contentElementTypeKey; }); }); }; @@ -99,14 +99,14 @@ //we have to add the 'alias' property to the objects, to meet the data requirements of itempicker. var selectedItems = Utilities.copy($scope.model.value).forEach((obj) => { - obj.alias = vm.getElementTypeByKey(obj.contentTypeKey).alias; + obj.alias = vm.getElementTypeByKey(obj.contentElementTypeKey).alias; return obj; }); var availableItems = vm.getAvailableElementTypes() localizationService.localizeMany(["blockEditor_headlineCreateBlock", "blockEditor_labelcreateNewElementType"]).then(function(localized) { - + var elemTypeSelectorOverlay = { view: "itempicker", title: localized[0], @@ -133,7 +133,7 @@ }; overlayService.open(elemTypeSelectorOverlay); - + }); }; @@ -158,7 +158,7 @@ vm.addBlockFromElementTypeKey = function(key) { var entry = { - "contentTypeKey": key, + "contentElementTypeKey": key, "settingsElementTypeKey": null, "labelTemplate": "", "view": null, @@ -178,7 +178,7 @@ vm.openBlockOverlay = function (block) { - localizationService.localize("blockEditor_blockConfigurationOverlayTitle", [vm.getElementTypeByKey(block.contentTypeKey).name]).then(function (data) { + localizationService.localize("blockEditor_blockConfigurationOverlayTitle", [vm.getElementTypeByKey(block.contentElementTypeKey).name]).then(function (data) { var clonedBlockData = Utilities.copy(block); vm.openBlock = block; @@ -209,7 +209,7 @@ $scope.$on('$destroy', function () { unsubscribe.forEach(u => { u(); }); }); - + onInit(); } diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.html index 41dea86131..2bd4be1505 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.html @@ -11,7 +11,7 @@ diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.overlay.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.overlay.controller.js index 0f58b84ee9..b1937e718d 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.overlay.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.overlay.controller.js @@ -23,7 +23,7 @@ return elementTypeResource.getAll().then(function (elementTypes) { vm.elementTypes = elementTypes; - vm.contentPreview = vm.getElementTypeByKey(vm.block.contentTypeKey); + vm.contentPreview = vm.getElementTypeByKey(vm.block.contentElementTypeKey); vm.settingsPreview = vm.getElementTypeByKey(vm.block.settingsElementTypeKey); }); } @@ -100,6 +100,7 @@ }; vm.applySettingsToBlock = function(block, key) { block.settingsElementTypeKey = key; + vm.settingsPreview = vm.getElementTypeByKey(vm.block.settingsElementTypeKey); }; vm.requestRemoveSettingsForBlock = function(block) { @@ -167,7 +168,7 @@ } }; editorService.treePicker(filePicker); - + }); } vm.requestRemoveViewForBlock = function(block) { @@ -190,10 +191,10 @@ }; - + vm.addStylesheetForBlock = function(block) { localizationService.localize("blockEditor_headlineAddCustomStylesheet").then(function(localizedTitle) { - + const filePicker = { title: localizedTitle, section: "settings", diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.overlay.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.overlay.html index 9675677c11..af458f6a13 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.overlay.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.overlay.html @@ -106,10 +106,10 @@
-
+
-
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 b6f3ace44c..0c7d58d245 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 @@ -39,6 +39,9 @@ } } } +ng-form.ng-invalid-val-server-match-settings > .umb-block-list__block > .umb-block-list__block--actions { + opacity: 1; +} .umb-block-list__block--actions { position: absolute; z-index:999999999;// We always want to be on top of custom view, but we need to make sure we still are behind relevant Umbraco CMS UI. ToDo: Needs further testing. @@ -58,15 +61,92 @@ &:hover { color: @ui-action-discreet-type-hover; } + > .__error-badge { + position: absolute; + top: -2px; + right: -2px; + min-width: 8px; + color: @white; + background-color: @ui-active-type; + border: 2px solid @white; + border-radius: 50%; + font-size: 8px; + font-weight: bold; + padding: 2px; + line-height: 8px; + background-color: @red; + display: none; + font-weight: 900; + } + &.--error > .__error-badge { + display: block; + + animation-duration: 1.4s; + animation-iteration-count: infinite; + animation-name: umb-block-list__action--badge-bounce; + animation-timing-function: ease; + @keyframes umb-block-list__action--badge-bounce { + 0% { transform: translateY(0); } + 20% { transform: translateY(-4px); } + 40% { transform: translateY(0); } + 55% { transform: translateY(-2px); } + 70% { transform: translateY(0); } + 100% { transform: translateY(0); } + } + + } } } .umb-block-list__block--content { - position: relative; - width: 100%; - min-height: @umb-block-list__item_minimum_height; - background-color: @white; - border-radius: @baseBorderRadius; + + > div { + position: relative; + width: 100%; + min-height: @umb-block-list__item_minimum_height; + background-color: @white; + border-radius: @baseBorderRadius; + 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 { 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 55f849e4d0..c2657985cf 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 @@ -3,6 +3,7 @@ Settings +
!
+ + +
+ +
-
-
-
- -
- - {{ layout.name }} - (system layout) -
- -
- -
- -
- -
- - - +
+ + {{ layout.name }} + (system layout)
-
-
+
+ +
- +
+ +
+ + + +
+
-
+
+ + + +
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/orderDirection.prevalues.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/orderDirection.prevalues.html index 7ec0895936..b6028ad5bb 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/orderDirection.prevalues.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/orderDirection.prevalues.html @@ -2,10 +2,20 @@
  • - + +
  • - + +
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/radiobuttons/radiobuttons.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/radiobuttons/radiobuttons.html index 1c3aa898db..82952193e7 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/radiobuttons/radiobuttons.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/radiobuttons/radiobuttons.html @@ -3,7 +3,12 @@
  • - + +
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.prevalues.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.prevalues.html index ee2929e3ea..d70f469e6f 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.prevalues.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.prevalues.html @@ -2,22 +2,22 @@
- + +
- + +
diff --git a/src/Umbraco.Web.UI.Client/src/views/relationTypes/create.controller.js b/src/Umbraco.Web.UI.Client/src/views/relationTypes/create.controller.js index 71f59a9a1f..b2fbee4b36 100644 --- a/src/Umbraco.Web.UI.Client/src/views/relationTypes/create.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/relationTypes/create.controller.js @@ -36,11 +36,12 @@ function RelationTypeCreateController($scope, $location, relationTypeResource, n var currentPath = node.path ? node.path : "-1"; navigationService.syncTree({ tree: "relationTypes", path: currentPath + "," + data, forceReload: true, activate: true }); - formHelper.resetForm({ scope: $scope }); + formHelper.resetForm({ scope: $scope, formCtrl: this.createRelationTypeForm }); var currentSection = appState.getSectionState("currentSection"); $location.path("/" + currentSection + "/relationTypes/edit/" + data); }, function (err) { + formHelper.resetForm({ scope: $scope, formCtrl: this.createRelationTypeForm, hasErrors: true }); if (err.data && err.data.message) { notificationsService.error(err.data.message); navigationService.hideMenu(); diff --git a/src/Umbraco.Web.UI.Client/src/views/relationTypes/create.html b/src/Umbraco.Web.UI.Client/src/views/relationTypes/create.html index beb1962b4e..4d8d94e1aa 100644 --- a/src/Umbraco.Web.UI.Client/src/views/relationTypes/create.html +++ b/src/Umbraco.Web.UI.Client/src/views/relationTypes/create.html @@ -14,13 +14,19 @@
  • diff --git a/src/Umbraco.Web.UI.Client/src/views/relationTypes/edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/relationTypes/edit.controller.js index 74b3e31b87..a39b48c278 100644 --- a/src/Umbraco.Web.UI.Client/src/views/relationTypes/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/relationTypes/edit.controller.js @@ -116,12 +116,13 @@ function RelationTypeEditController($scope, $routeParams, relationTypeResource, vm.page.saveButtonState = "busy"; relationTypeResource.save(vm.relationType).then(function (data) { - formHelper.resetForm({ scope: $scope, notifications: data.notifications }); + formHelper.resetForm({ scope: $scope }); bindRelationType(data); vm.page.saveButtonState = "success"; }, function (error) { + formHelper.resetForm({ scope: $scope, hasErrors: true }); contentEditingHelper.handleSaveError({ err: error }); diff --git a/src/Umbraco.Web.UI.Client/src/views/relationTypes/views/relationType.html b/src/Umbraco.Web.UI.Client/src/views/relationTypes/views/relationType.html index 043ced9a5f..100d8d9d09 100644 --- a/src/Umbraco.Web.UI.Client/src/views/relationTypes/views/relationType.html +++ b/src/Umbraco.Web.UI.Client/src/views/relationTypes/views/relationType.html @@ -11,10 +11,18 @@
    • - + +
    • - + +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/scripts/create.controller.js b/src/Umbraco.Web.UI.Client/src/views/scripts/create.controller.js index 096ae72e61..14d7e54a66 100644 --- a/src/Umbraco.Web.UI.Client/src/views/scripts/create.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/scripts/create.controller.js @@ -27,7 +27,7 @@ function createFolder(form) { - if (formHelper.submitForm({scope: $scope, formCtrl: form })) { + if (formHelper.submitForm({ scope: $scope, formCtrl: form })) { codefileResource.createContainer("scripts", node.id, vm.folderName).then(function (saved) { @@ -40,14 +40,15 @@ activate: true }); - formHelper.resetForm({ scope: $scope }); + formHelper.resetForm({ scope: $scope, formCtrl: form }); var section = appState.getSectionState("currentSection"); - }, function(err) { + }, function (err) { + + formHelper.resetForm({ scope: $scope, formCtrl: form, hasErrors: true }); + vm.createFolderError = err; - vm.createFolderError = err; - }); } diff --git a/src/Umbraco.Web.UI.Client/src/views/stylesheets/create.controller.js b/src/Umbraco.Web.UI.Client/src/views/stylesheets/create.controller.js index 6033a6bdc7..af80878c35 100644 --- a/src/Umbraco.Web.UI.Client/src/views/stylesheets/create.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/stylesheets/create.controller.js @@ -22,14 +22,14 @@ $location.path("/settings/stylesheets/edit/" + node.id).search("create", "true").search("rtestyle", "true"); navigationService.hideMenu(); } - + function showCreateFolder() { vm.creatingFolder = true; } function createFolder(form) { - if (formHelper.submitForm({scope: $scope, formCtrl: form })) { + if (formHelper.submitForm({ scope: $scope, formCtrl: form })) { codefileResource.createContainer("stylesheets", node.id, vm.folderName).then(function (saved) { @@ -42,12 +42,13 @@ activate: true }); - formHelper.resetForm({ scope: $scope }); + formHelper.resetForm({ scope: $scope, formCtrl: form }); - }, function(err) { + }, function (err) { + + formHelper.resetForm({ scope: $scope, formCtrl: form, hasErrors: true }); + vm.createFolderError = err; - vm.createFolderError = err; - }); } @@ -57,7 +58,7 @@ const showMenu = true; navigationService.hideDialog(showMenu); } - + } angular.module("umbraco").controller("Umbraco.Editors.StyleSheets.CreateController", StyleSheetsCreateController); diff --git a/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js b/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js index d12029313c..2845c8df68 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js @@ -154,7 +154,9 @@ extendedSave(saved).then(function (result) { //if all is good, then reset the form formHelper.resetForm({ scope: $scope }); - }, Utilities.noop); + }, function () { + formHelper.resetForm({ scope: $scope, hasErrors: true }); + }); vm.user = _.omit(saved, "navigation"); //restore @@ -165,7 +167,7 @@ vm.page.saveButtonState = "success"; }, function (err) { - + formHelper.resetForm({ scope: $scope, hasErrors: true }); contentEditingHelper.handleSaveError({ err: err, showNotifications: true diff --git a/src/Umbraco.Web.UI.Client/test/unit/common/services/block-editor-service.spec.js b/src/Umbraco.Web.UI.Client/test/unit/common/services/block-editor-service.spec.js index 71404b5e85..4ce6f4e6b5 100644 --- a/src/Umbraco.Web.UI.Client/test/unit/common/services/block-editor-service.spec.js +++ b/src/Umbraco.Web.UI.Client/test/unit/common/services/block-editor-service.spec.js @@ -11,7 +11,7 @@ beforeEach(module('umbraco.resources')); beforeEach(module('umbraco.mocks')); beforeEach(module('umbraco')); - + beforeEach(inject(function ($injector, mocksUtils, _$rootScope_, _$q_, _$timeout_) { mocksUtils.disableAuth(); @@ -41,7 +41,7 @@ })); - var blockConfigurationMock = { contentTypeKey: "7C5B74D1-E2F9-45A3-AE4B-FC7A829BF8AB", label: "Test label", settingsElementTypeKey: null, view: "testview.html" }; + var blockConfigurationMock = { contentElementTypeKey: "7C5B74D1-E2F9-45A3-AE4B-FC7A829BF8AB", label: "Test label", settingsElementTypeKey: null, view: "testview.html" }; var propertyModelMock = { layout: { @@ -60,7 +60,7 @@ ] }; - var blockWithSettingsConfigurationMock = { contentTypeKey: "7C5B74D1-E2F9-45A3-AE4B-FC7A829BF8AB", label: "Test label", settingsElementTypeKey: "7C5B74D1-E2F9-45A3-AE4B-FC7A829BF8AB", view: "testview.html" }; + var blockWithSettingsConfigurationMock = { contentElementTypeKey: "7C5B74D1-E2F9-45A3-AE4B-FC7A829BF8AB", label: "Test label", settingsElementTypeKey: "7C5B74D1-E2F9-45A3-AE4B-FC7A829BF8AB", view: "testview.html" }; var propertyModelWithSettingsMock = { layout: { "Umbraco.TestBlockEditor": [ @@ -105,17 +105,17 @@ it('getBlockConfiguration provide the requested block configurtion', function () { var modelObject = blockEditorService.createModelObject({}, "Umbraco.TestBlockEditor", [blockConfigurationMock], $scope, $scope); - expect(modelObject.getBlockConfiguration(blockConfigurationMock.contentTypeKey).label).toBe(blockConfigurationMock.label); + expect(modelObject.getBlockConfiguration(blockConfigurationMock.contentElementTypeKey).label).toBe(blockConfigurationMock.label); }); it('load provides data for itemPicker', function (done) { var modelObject = blockEditorService.createModelObject({}, "Umbraco.TestBlockEditor", [blockConfigurationMock], $scope, $scope); - modelObject.load().then(() => { + modelObject.load().then(() => { try { var itemPickerOptions = modelObject.getAvailableBlocksForBlockPicker(); expect(itemPickerOptions.length).toBe(1); - expect(itemPickerOptions[0].blockConfigModel.contentTypeKey).toBe(blockConfigurationMock.contentTypeKey); + expect(itemPickerOptions[0].blockConfigModel.contentElementTypeKey).toBe(blockConfigurationMock.contentElementTypeKey); done(); } catch (e) { done.fail(e); @@ -139,7 +139,7 @@ expect(layout).not.toBeUndefined(); expect(layout.length).toBe(1); expect(layout[0]).toBe(propertyModelMock.layout["Umbraco.TestBlockEditor"][0]); - expect(layout[0].udi).toBe(propertyModelMock.layout["Umbraco.TestBlockEditor"][0].udi); + expect(layout[0].contentUdi).toBe(propertyModelMock.layout["Umbraco.TestBlockEditor"][0].contentUdi); done(); } catch (e) { @@ -241,10 +241,10 @@ done(); }); - + } catch (e) { done.fail(e); - } + } }); $rootScope.$digest(); @@ -362,7 +362,7 @@ done(); }); - + }); $rootScope.$digest(); @@ -406,7 +406,7 @@ done(); }); - + } catch (e) { done.fail(e); } diff --git a/src/Umbraco.Web.UI.NetCore/Umbraco/UmbracoBackOffice/Default.cshtml b/src/Umbraco.Web.UI.NetCore/Umbraco/UmbracoBackOffice/Default.cshtml new file mode 100644 index 0000000000..d399817502 --- /dev/null +++ b/src/Umbraco.Web.UI.NetCore/Umbraco/UmbracoBackOffice/Default.cshtml @@ -0,0 +1,128 @@ +@using Umbraco.Core +@using Umbraco.Web.WebAssets +@using Umbraco.Web.Common.Security +@using Umbraco.Core.WebAssets +@using Umbraco.Core.Configuration +@using Umbraco.Core.Hosting +@using Umbraco.Extensions +@using Umbraco.Core.Logging +@using Umbraco.Web.BackOffice.Controllers +@inject BackOfficeSignInManager signInManager +@inject BackOfficeServerVariables backOfficeServerVariables +@inject IUmbracoVersion umbracoVersion +@inject IHostingEnvironment hostingEnvironment +@inject IGlobalSettings globalSettings +@inject IRuntimeMinifier runtimeMinifier +@inject IProfilerHtml profilerHtml + +@{ + var isDebug = false; + var qryDebug = Context.Request.Query["umbDebug"].TryConvertTo(); + isDebug = qryDebug.Success && qryDebug.Result; + var backOfficePath = globalSettings.GetBackOfficePath(hostingEnvironment); +} + + + + + + + + + + + + + + Umbraco + + @Html.Raw(await runtimeMinifier.RenderCssHereAsync(BackOfficeWebAssets.UmbracoInitCssBundleName)) + + + + + + +
    + + + + + + + + + + + +
    + + + + + + + + + + + + + @await Html.BareMinimumServerVariablesScriptAsync(backOfficeServerVariables) + + + + + + + @if (isDebug) + { + @Html.Raw(profilerHtml.Render()) + } + + + diff --git a/src/Umbraco.Web.UI.NetCore/umbraco/UmbracoBackOffice/Default.cshtml b/src/Umbraco.Web.UI.NetCore/umbraco/UmbracoBackOffice/Default.cshtml index 92555f6454..d399817502 100644 --- a/src/Umbraco.Web.UI.NetCore/umbraco/UmbracoBackOffice/Default.cshtml +++ b/src/Umbraco.Web.UI.NetCore/umbraco/UmbracoBackOffice/Default.cshtml @@ -69,14 +69,14 @@
- - -
+ + + 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 tekstboks Fjern denne tekstboks Indholdsrod - 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 @@ Design Ordbog Dimensioner + Kassér Ned Hent Rediger @@ -668,6 +669,7 @@ Ikon Id Importer + Inkludér undermapper i søgning Søg kun i denne mappe Info Indre margen @@ -1850,6 +1852,7 @@ Mange hilsner fra Umbraco robotten Indstillinger Avanceret Skjuld 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 box Remove this text box Content 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 @@ Design Dictionary Dimensions + Discard Down Download Edit @@ -2467,6 +2468,7 @@ To manage your website, simply open the Umbraco back office and start adding con Settings Advanced Force 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 box Remove this text box Content 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 @@ Design Dictionary Dimensions + Discard Down Download Edit @@ -2487,6 +2488,7 @@ To manage your website, simply open the Umbraco back office and start adding con Settings Advanced Force 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.1 Library + 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);