diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 5537a46ef8..295dc54371 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -123,7 +123,7 @@ You can get in touch with [the core contributors team](#the-core-contributors-te In order to build the Umbraco source code locally, first make sure you have the following installed. - * [Visual Studio 2017 v15.9.7+](https://visualstudio.microsoft.com/vs/) + * [Visual Studio 2019 v16.3+ (with .NET Core 3.0)](https://visualstudio.microsoft.com/vs/) * [Node.js v10+](https://nodejs.org/en/download/) * npm v6.4.1+ (installed with Node.js) * [Git command line](https://git-scm.com/download/) diff --git a/.github/ISSUE_TEMPLATE/3_BugNetCore.md b/.github/ISSUE_TEMPLATE/3_BugNetCore.md new file mode 100644 index 0000000000..989904d4d8 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/3_BugNetCore.md @@ -0,0 +1,65 @@ +--- +name: 🌟 .Net Core Bug Report +about: For bugs specifically for the upcoming .NET Core release of Umbraco, don't use this if you're working with Umbraco version 7 or 8 +labels: project/net-core +--- + +ℹ️ If this bug **also** appears on the current version 8 of Umbraco then please [report it as a regular bug](https://github.com/umbraco/Umbraco-CMS/issues/new?template=1_Bug.md), fixes in version 8 will be merged to the .NET Core version. + +A brief description of the issue goes here. + + + + +Reproduction +------------ + +If you're filing a bug, please describe how to reproduce it. Include as much +relevant information as possible, such as: + +### Bug summary + + + +### Specifics + + + +### Steps to reproduce + + + +### Expected result + + + +### Actual result + + diff --git a/.github/ISSUE_TEMPLATE/3_Support_question.md b/.github/ISSUE_TEMPLATE/4_Support_question.md similarity index 100% rename from .github/ISSUE_TEMPLATE/3_Support_question.md rename to .github/ISSUE_TEMPLATE/4_Support_question.md diff --git a/.github/ISSUE_TEMPLATE/4_Documentation_issue.md b/.github/ISSUE_TEMPLATE/5_Documentation_issue.md similarity index 100% rename from .github/ISSUE_TEMPLATE/4_Documentation_issue.md rename to .github/ISSUE_TEMPLATE/5_Documentation_issue.md diff --git a/.github/ISSUE_TEMPLATE/5_Security_issue.md b/.github/ISSUE_TEMPLATE/6_Security_issue.md similarity index 100% rename from .github/ISSUE_TEMPLATE/5_Security_issue.md rename to .github/ISSUE_TEMPLATE/6_Security_issue.md diff --git a/.gitignore b/.gitignore index e35117d8e6..8b8a0782af 100644 --- a/.gitignore +++ b/.gitignore @@ -174,6 +174,7 @@ cypress.env.json # eof /src/Umbraco.Web.UI.Client/TESTS-*.xml /src/ApiDocs/api/* +/src/Umbraco.Web.UI.Client/package-lock.json /src/Umbraco.Web.UI.NetCore/wwwroot/Media/* /src/Umbraco.Web.UI.NetCore/wwwroot/is-cache/* /src/Umbraco.Tests.Integration/App_Data/* diff --git a/build/NuSpecs/tools/Web.config.install.xdt b/build/NuSpecs/tools/Web.config.install.xdt index f118fa8c3a..e215bdbf29 100644 --- a/build/NuSpecs/tools/Web.config.install.xdt +++ b/build/NuSpecs/tools/Web.config.install.xdt @@ -1,139 +1,136 @@ - - - - - -
-
-
- - + + + + + + +
+
+
+ + - - - - - - + + + + + + - - - - - + + + + + + - - - - - - - + + + + + + + + - - - - - - + - - - > - - + + + + + + + + + + + + - + + + + + + + + + - - - - - - - - + + + + + - - - - - + + + + + + + + + + + + + + + - - - - - - - + + + + + + + + + + + - - - - - - - - + + + + - - - - - - - - - - - + + + + + + + - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + @@ -177,20 +174,21 @@ - + - - - - - - - - - - - - - + + + + + + + + + + + + + + diff --git a/build/build.ps1 b/build/build.ps1 index 98f975da4f..ee05710f47 100644 --- a/build/build.ps1 +++ b/build/build.ps1 @@ -125,7 +125,23 @@ $error.Clear() Write-Output "### gulp build for version $($this.Version.Release)" >> $log 2>&1 - npx gulp build --buildversion=$this.Version.Release >> $log 2>&1 + npm run build --buildversion=$this.Version.Release >> $log 2>&1 + + # We can ignore this warning, we need to update to node 12 at some point - https://github.com/jsdom/jsdom/issues/2939 + $indexes = [System.Collections.ArrayList]::new() + $index = 0; + $error | ForEach-Object { + # Find which of the errors is the ExperimentalWarning + if($_.ToString().Contains("ExperimentalWarning: The fs.promises API is experimental")) { + [void]$indexes.Add($index) + } + $index++ + } + $indexes | ForEach-Object { + # Loop through the list of indexes and remove the errors that we expect and feel confident we can ignore + $error.Remove($error[$_]) + } + if (-not $?) { throw "Failed to build" } # that one is expected to work } finally { Pop-Location diff --git a/src/Umbraco.Core/ContentExtensions.cs b/src/Umbraco.Core/ContentExtensions.cs index 3e70bcda53..51221086ec 100644 --- a/src/Umbraco.Core/ContentExtensions.cs +++ b/src/Umbraco.Core/ContentExtensions.cs @@ -7,7 +7,65 @@ namespace Umbraco.Core public static class ContentExtensions { - #region XML methods + internal static bool IsMoving(this IContentBase entity) + { + // Check if this entity is being moved as a descendant as part of a bulk moving operations. + // When this occurs, only Path + Level + UpdateDate are being changed. In this case we can bypass a lot of the below + // operations which will make this whole operation go much faster. When moving we don't need to create + // new versions, etc... because we cannot roll this operation back anyways. + var isMoving = entity.IsPropertyDirty(nameof(entity.Path)) + && entity.IsPropertyDirty(nameof(entity.Level)) + && entity.IsPropertyDirty(nameof(entity.UpdateDate)); + + return isMoving; + } + + + /// + /// Removes characters that are not valid XML characters from all entity properties + /// of type string. See: http://stackoverflow.com/a/961504/5018 + /// + /// + /// + /// If this is not done then the xml cache can get corrupt and it will throw YSODs upon reading it. + /// + /// + public static void SanitizeEntityPropertiesForXmlStorage(this IContentBase entity) + { + entity.Name = entity.Name.ToValidXmlString(); + foreach (var property in entity.Properties) + { + foreach (var propertyValue in property.Values) + { + if (propertyValue.EditedValue is string editString) + propertyValue.EditedValue = editString.ToValidXmlString(); + if (propertyValue.PublishedValue is string publishedString) + propertyValue.PublishedValue = publishedString.ToValidXmlString(); + } + } + } + + /// + /// Checks if the IContentBase has children + /// + /// + /// + /// + /// + /// This is a bit of a hack because we need to type check! + /// + internal static bool HasChildren(IContentBase content, ServiceContext services) + { + if (content is IContent) + { + return services.ContentService.HasChildren(content.Id); + } + if (content is IMedia) + { + return services.MediaService.HasChildren(content.Id); + } + return false; + } /// /// Creates the full xml representation for the object and all of it's descendants @@ -53,6 +111,5 @@ namespace Umbraco.Core { return serializer.Serialize(member); } - #endregion } } diff --git a/src/Umbraco.Core/Dashboards/DashboardCollectionBuilder.cs b/src/Umbraco.Core/Dashboards/DashboardCollectionBuilder.cs index d790b04d46..a903d15abb 100644 --- a/src/Umbraco.Core/Dashboards/DashboardCollectionBuilder.cs +++ b/src/Umbraco.Core/Dashboards/DashboardCollectionBuilder.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; using Umbraco.Core; using Umbraco.Core.Composing; @@ -9,8 +10,22 @@ namespace Umbraco.Web.Dashboards { public class DashboardCollectionBuilder : WeightedCollectionBuilderBase { + private Dictionary _customWeights = new Dictionary(); + protected override DashboardCollectionBuilder This => this; + /// + /// Changes the default weight of a dashboard + /// + /// The type of dashboard + /// The new dashboard weight + /// + public DashboardCollectionBuilder SetWeight(int weight) where T : IDashboard + { + _customWeights[typeof(T)] = weight; + return this; + } + protected override IEnumerable CreateItems(IFactory factory) { // get the manifest parser just-in-time - injecting it in the ctor would mean that @@ -32,6 +47,12 @@ namespace Umbraco.Web.Dashboards private int GetWeight(IDashboard dashboard) { + var typeOfDashboard = dashboard.GetType(); + if(_customWeights.ContainsKey(typeOfDashboard)) + { + return _customWeights[typeOfDashboard]; + } + switch (dashboard) { case ManifestDashboard manifestDashboardDefinition: diff --git a/src/Umbraco.Core/Models/Blocks/BlockListItem.cs b/src/Umbraco.Core/Models/Blocks/BlockListItem.cs index f4b5c489e7..620c3d9fe0 100644 --- a/src/Umbraco.Core/Models/Blocks/BlockListItem.cs +++ b/src/Umbraco.Core/Models/Blocks/BlockListItem.cs @@ -5,41 +5,126 @@ using Umbraco.Core.Models.PublishedContent; namespace Umbraco.Core.Models.Blocks { /// - /// Represents a layout item for the Block List editor + /// Represents a layout item for the Block List editor. /// + /// [DataContract(Name = "block", Namespace = "")] public class BlockListItem : IBlockReference { + /// + /// Initializes a new instance of the class. + /// + /// The content UDI. + /// The content. + /// The settings UDI. + /// The settings. + /// contentUdi + /// or + /// content public BlockListItem(Udi contentUdi, IPublishedElement content, Udi settingsUdi, IPublishedElement settings) { - ContentUdi = contentUdi ?? throw new ArgumentNullException(nameof(contentUdi)); + ContentUdi = contentUdi ?? throw new ArgumentNullException(nameof(contentUdi)); Content = content ?? throw new ArgumentNullException(nameof(content)); - Settings = settings; // can be null - SettingsUdi = settingsUdi; // can be null + SettingsUdi = settingsUdi; + Settings = settings; } /// - /// The Id of the content data item + /// Gets the content UDI. /// + /// + /// The content UDI. + /// [DataMember(Name = "contentUdi")] public Udi ContentUdi { get; } /// - /// The Id of the settings data item - /// - [DataMember(Name = "settingsUdi")] - public Udi SettingsUdi { get; } - - /// - /// The content data item referenced + /// Gets the content. /// + /// + /// The content. + /// [DataMember(Name = "content")] public IPublishedElement Content { get; } /// - /// The settings data item referenced + /// Gets the settings UDI. /// + /// + /// The settings UDI. + /// + [DataMember(Name = "settingsUdi")] + public Udi SettingsUdi { get; } + + /// + /// Gets the settings. + /// + /// + /// The settings. + /// [DataMember(Name = "settings")] public IPublishedElement Settings { get; } } + + /// + /// Represents a layout item with a generic content type for the Block List editor. + /// + /// The type of the content. + /// + public class BlockListItem : BlockListItem + where T : IPublishedElement + { + /// + /// Initializes a new instance of the class. + /// + /// The content UDI. + /// The content. + /// The settings UDI. + /// The settings. + public BlockListItem(Udi contentUdi, T content, Udi settingsUdi, IPublishedElement settings) + : base(contentUdi, content, settingsUdi, settings) + { + Content = content; + } + + /// + /// Gets the content. + /// + /// + /// The content. + /// + public new T Content { get; } + } + + /// + /// Represents a layout item with generic content and settings types for the Block List editor. + /// + /// The type of the content. + /// The type of the settings. + /// + public class BlockListItem : BlockListItem + where TContent : IPublishedElement + where TSettings : IPublishedElement + { + /// + /// Initializes a new instance of the class. + /// + /// The content udi. + /// The content. + /// The settings udi. + /// The settings. + public BlockListItem(Udi contentUdi, TContent content, Udi settingsUdi, TSettings settings) + : base(contentUdi, content, settingsUdi, settings) + { + Settings = settings; + } + + /// + /// Gets the settings. + /// + /// + /// The settings. + /// + public new TSettings Settings { get; } + } } diff --git a/src/Umbraco.Core/Models/Blocks/IBlockReference.cs b/src/Umbraco.Core/Models/Blocks/IBlockReference.cs index 8d8ddd47f0..7f5c835b3c 100644 --- a/src/Umbraco.Core/Models/Blocks/IBlockReference.cs +++ b/src/Umbraco.Core/Models/Blocks/IBlockReference.cs @@ -1,26 +1,37 @@ namespace Umbraco.Core.Models.Blocks { - /// - /// Represents a data item reference for a Block editor implementation - /// - /// - /// - /// see: https://github.com/umbraco/rfcs/blob/907f3758cf59a7b6781296a60d57d537b3b60b8c/cms/0011-block-data-structure.md#strongly-typed - /// - public interface IBlockReference : IBlockReference - { - TSettings Settings { get; } - } - - /// - /// Represents a data item reference for a Block Editor implementation + /// Represents a data item reference for a Block Editor implementation. /// /// - /// see: https://github.com/umbraco/rfcs/blob/907f3758cf59a7b6781296a60d57d537b3b60b8c/cms/0011-block-data-structure.md#strongly-typed + /// See: https://github.com/umbraco/rfcs/blob/907f3758cf59a7b6781296a60d57d537b3b60b8c/cms/0011-block-data-structure.md#strongly-typed /// public interface IBlockReference { + /// + /// Gets the content UDI. + /// + /// + /// The content UDI. + /// Udi ContentUdi { get; } } + + /// + /// Represents a data item reference with settings for a Block editor implementation. + /// + /// The type of the settings. + /// + /// See: https://github.com/umbraco/rfcs/blob/907f3758cf59a7b6781296a60d57d537b3b60b8c/cms/0011-block-data-structure.md#strongly-typed + /// + public interface IBlockReference : IBlockReference + { + /// + /// Gets the settings. + /// + /// + /// The settings. + /// + TSettings Settings { get; } + } } diff --git a/src/Umbraco.Core/Models/PropertyCollection.cs b/src/Umbraco.Core/Models/PropertyCollection.cs index dd62ba83cb..e206a3b38c 100644 --- a/src/Umbraco.Core/Models/PropertyCollection.cs +++ b/src/Umbraco.Core/Models/PropertyCollection.cs @@ -7,6 +7,7 @@ using System.Runtime.Serialization; namespace Umbraco.Core.Models { + /// /// Represents a collection of property values. /// diff --git a/src/Umbraco.Core/PropertyEditors/TextboxConfiguration.cs b/src/Umbraco.Core/PropertyEditors/TextboxConfiguration.cs index c9f15e2e2f..641cc8e42a 100644 --- a/src/Umbraco.Core/PropertyEditors/TextboxConfiguration.cs +++ b/src/Umbraco.Core/PropertyEditors/TextboxConfiguration.cs @@ -7,7 +7,7 @@ namespace Umbraco.Web.PropertyEditors /// public class TextboxConfiguration { - [ConfigurationField("maxChars", "Maximum allowed characters", "textstringlimited", Description = "If empty, 500 character limit")] + [ConfigurationField("maxChars", "Maximum allowed characters", "textstringlimited", Description = "If empty, 512 character limit")] public int? MaxChars { get; set; } } } diff --git a/src/Umbraco.Infrastructure/ContentExtensions.cs b/src/Umbraco.Infrastructure/ContentExtensions.cs index d8d39cc984..e71815f911 100644 --- a/src/Umbraco.Infrastructure/ContentExtensions.cs +++ b/src/Umbraco.Infrastructure/ContentExtensions.cs @@ -14,6 +14,14 @@ namespace Umbraco.Core { public static class ContentExtensions { + /// + /// Returns all properties based on the editorAlias + /// + /// + /// + /// + public static IEnumerable GetPropertiesByEditor(this IContentBase content, string editorAlias) + => content.Properties.Where(x => x.PropertyType.PropertyEditorAlias == editorAlias); internal static bool IsMoving(this IContentBase entity) { @@ -28,29 +36,6 @@ namespace Umbraco.Core return isMoving; } - /// - /// Removes characters that are not valid XML characters from all entity properties - /// of type string. See: http://stackoverflow.com/a/961504/5018 - /// - /// - /// - /// If this is not done then the xml cache can get corrupt and it will throw YSODs upon reading it. - /// - /// - public static void SanitizeEntityPropertiesForXmlStorage(this IContentBase entity) - { - entity.Name = entity.Name.ToValidXmlString(); - foreach (var property in entity.Properties) - { - foreach (var propertyValue in property.Values) - { - if (propertyValue.EditedValue is string editString) - propertyValue.EditedValue = editString.ToValidXmlString(); - if (propertyValue.PublishedValue is string publishedString) - propertyValue.PublishedValue = publishedString.ToValidXmlString(); - } - } - } #region IContent diff --git a/src/Umbraco.Infrastructure/Models/Blocks/BlockEditorDataConverter.cs b/src/Umbraco.Infrastructure/Models/Blocks/BlockEditorDataConverter.cs index 22e364c0f8..802e8c2ee3 100644 --- a/src/Umbraco.Infrastructure/Models/Blocks/BlockEditorDataConverter.cs +++ b/src/Umbraco.Infrastructure/Models/Blocks/BlockEditorDataConverter.cs @@ -18,10 +18,35 @@ namespace Umbraco.Core.Models.Blocks _propertyEditorAlias = propertyEditorAlias; } + public BlockEditorData ConvertFrom(JToken json) + { + var value = json.ToObject(); + return Convert(value); + } + + public bool TryDeserialize(string json, out BlockEditorData blockEditorData) + { + try + { + var value = JsonConvert.DeserializeObject(json); + blockEditorData = Convert(value); + return true; + } + catch (System.Exception) + { + blockEditorData = null; + return false; + } + } + public BlockEditorData Deserialize(string json) { var value = JsonConvert.DeserializeObject(json); + return Convert(value); + } + private BlockEditorData Convert(BlockValue value) + { if (value.Layout == null) return BlockEditorData.Empty; diff --git a/src/Umbraco.Infrastructure/Models/Blocks/BlockListLayoutItem.cs b/src/Umbraco.Infrastructure/Models/Blocks/BlockListLayoutItem.cs index 3453ff2a78..5de44e16c1 100644 --- a/src/Umbraco.Infrastructure/Models/Blocks/BlockListLayoutItem.cs +++ b/src/Umbraco.Infrastructure/Models/Blocks/BlockListLayoutItem.cs @@ -12,7 +12,7 @@ namespace Umbraco.Core.Models.Blocks [JsonConverter(typeof(UdiJsonConverter))] public Udi ContentUdi { get; set; } - [JsonProperty("settingsUdi")] + [JsonProperty("settingsUdi", NullValueHandling = NullValueHandling.Ignore)] [JsonConverter(typeof(UdiJsonConverter))] public Udi SettingsUdi { get; set; } } diff --git a/src/Umbraco.Infrastructure/Models/Blocks/BlockListModel.cs b/src/Umbraco.Infrastructure/Models/Blocks/BlockListModel.cs index 9a5a3af22a..9a3a26ab30 100644 --- a/src/Umbraco.Infrastructure/Models/Blocks/BlockListModel.cs +++ b/src/Umbraco.Infrastructure/Models/Blocks/BlockListModel.cs @@ -1,64 +1,63 @@ using System; -using System.Collections; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Linq; using System.Runtime.Serialization; -using Umbraco.Core.Models.PublishedContent; namespace Umbraco.Core.Models.Blocks { /// - /// The strongly typed model for the Block List editor + /// The strongly typed model for the Block List editor. /// + /// [DataContract(Name = "blockList", Namespace = "")] - public class BlockListModel : IReadOnlyList + public class BlockListModel : ReadOnlyCollection { - private readonly IReadOnlyList _layout = new List(); - + /// + /// Gets the empty . + /// + /// + /// The empty . + /// public static BlockListModel Empty { get; } = new BlockListModel(); + /// + /// Prevents a default instance of the class from being created. + /// private BlockListModel() - { - } - - public BlockListModel(IEnumerable layout) - { - _layout = layout.ToList(); - } - - public int Count => _layout.Count; + : this(new List()) + { } /// - /// Get the block by index + /// Initializes a new instance of the class. /// - /// - /// - public BlockListItem this[int index] => _layout[index]; + /// The list to wrap. + public BlockListModel(IList list) + : base(list) + { } /// - /// Get the block by content Guid + /// Gets the with the specified content key. /// - /// - /// - public BlockListItem this[Guid contentKey] => _layout.FirstOrDefault(x => x.Content.Key == contentKey); + /// + /// The . + /// + /// The content key. + /// + /// The with the specified content key. + /// + public BlockListItem this[Guid contentKey] => this.FirstOrDefault(x => x.Content.Key == contentKey); /// - /// Get the block by content element Udi + /// Gets the with the specified content 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(); - + /// + /// The . + /// + /// The content UDI. + /// + /// The with the specified content UDI. + /// + public BlockListItem this[Udi contentUdi] => contentUdi is GuidUdi guidUdi ? this.FirstOrDefault(x => x.Content.Key == guidUdi.Guid) : null; } } diff --git a/src/Umbraco.Infrastructure/Models/Blocks/ContentAndSettingsReference.cs b/src/Umbraco.Infrastructure/Models/Blocks/ContentAndSettingsReference.cs index 523a964c7b..f7222fe140 100644 --- a/src/Umbraco.Infrastructure/Models/Blocks/ContentAndSettingsReference.cs +++ b/src/Umbraco.Infrastructure/Models/Blocks/ContentAndSettingsReference.cs @@ -1,5 +1,5 @@ -using System.Collections.Generic; -using System; +using System; +using System.Collections.Generic; namespace Umbraco.Core.Models.Blocks { @@ -12,26 +12,16 @@ namespace Umbraco.Core.Models.Blocks } public Udi ContentUdi { get; } + public Udi SettingsUdi { get; } - public override bool Equals(object obj) - { - return obj is ContentAndSettingsReference reference && Equals(reference); - } + public override bool Equals(object obj) => obj is ContentAndSettingsReference reference && Equals(reference); - public bool Equals(ContentAndSettingsReference other) - { - return EqualityComparer.Default.Equals(ContentUdi, other.ContentUdi) && - EqualityComparer.Default.Equals(SettingsUdi, other.SettingsUdi); - } + public bool Equals(ContentAndSettingsReference other) => other != null + && EqualityComparer.Default.Equals(ContentUdi, other.ContentUdi) + && EqualityComparer.Default.Equals(SettingsUdi, other.SettingsUdi); - public override int GetHashCode() - { - var hashCode = 272556606; - hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(ContentUdi); - hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(SettingsUdi); - return hashCode; - } + public override int GetHashCode() => (ContentUdi, SettingsUdi).GetHashCode(); public static bool operator ==(ContentAndSettingsReference left, ContentAndSettingsReference right) { diff --git a/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/SpecialDbTypes.cs b/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/SpecialDbTypes.cs index 4b606bc0f5..6d211ebbd9 100644 --- a/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/SpecialDbTypes.cs +++ b/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/SpecialDbTypes.cs @@ -7,6 +7,7 @@ public enum SpecialDbTypes { NTEXT, - NCHAR + NCHAR, + NVARCHARMAX } } diff --git a/src/Umbraco.Infrastructure/Persistence/PocoDataDataReader.cs b/src/Umbraco.Infrastructure/Persistence/PocoDataDataReader.cs index 1d7d301b87..460a4d3d90 100644 --- a/src/Umbraco.Infrastructure/Persistence/PocoDataDataReader.cs +++ b/src/Umbraco.Infrastructure/Persistence/PocoDataDataReader.cs @@ -79,6 +79,9 @@ namespace Umbraco.Core.Persistence case SpecialDbTypes.NCHAR: sqlDbType = SqlDbType.NChar; break; + case SpecialDbTypes.NVARCHARMAX: + sqlDbType = SqlDbType.NVarChar; + break; default: throw new ArgumentOutOfRangeException(); } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeRepository.cs index 868e088d93..af0f58eb0e 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeRepository.cs @@ -132,7 +132,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement if (objectTypes.Any()) { - sql = sql.Where("umbracoNode.nodeObjectType IN (@objectTypes)", objectTypes); + sql = sql.Where("umbracoNode.nodeObjectType IN (@objectTypes)", new { objectTypes = objectTypes }); } return Database.Fetch(sql); diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs index 50ba0a698b..5bfbd86486 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs @@ -1203,7 +1203,7 @@ AND umbracoNode.id <> @id", { // first clear dependencies Database.Delete("WHERE propertyTypeId = @Id", new { Id = propertyTypeId }); - Database.Delete("WHERE propertytypeid = @Id", new { Id = propertyTypeId }); + Database.Delete("WHERE propertyTypeId = @Id", new { Id = propertyTypeId }); // then delete the property type Database.Delete("WHERE contentTypeId = @Id AND id = @PropertyTypeId", diff --git a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs index 6095630161..a9377d696b 100644 --- a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs +++ b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs @@ -198,7 +198,13 @@ namespace Umbraco.Core.Persistence.SqlSyntax return "NCHAR"; } else if (dbTypes == SpecialDbTypes.NTEXT) + { return "NTEXT"; + } + else if (dbTypes == SpecialDbTypes.NVARCHARMAX) + { + return "NVARCHAR(MAX)"; + } return "NVARCHAR"; } diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockListConfiguration.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockListConfiguration.cs index e461da40dc..1af3aa1303 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/BlockListConfiguration.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockListConfiguration.cs @@ -4,14 +4,11 @@ using Umbraco.Core.PropertyEditors; namespace Umbraco.Web.PropertyEditors { - /// /// The configuration object for the Block List editor /// public class BlockListConfiguration { - - [ConfigurationField("blocks", "Available Blocks", "views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.html", Description = "Define the available blocks.")] public BlockConfiguration[] Blocks { get; set; } @@ -61,7 +58,7 @@ namespace Umbraco.Web.PropertyEditors public int? Max { get; set; } } - [ConfigurationField("useLiveEditing", "Live editing mode", "boolean", Description = "Live editing in editor overlays for live updated custom views.")] + [ConfigurationField("useLiveEditing", "Live editing mode", "boolean", Description = "Live editing in editor overlays for live updated custom views or labels using custom expression.")] public bool UseLiveEditing { get; set; } [ConfigurationField("useInlineEditingAsDefault", "Inline editing mode", "boolean", Description = "Use the inline editor as the default block view.")] @@ -69,7 +66,5 @@ namespace Umbraco.Web.PropertyEditors [ConfigurationField("maxPropertyWidth", "Property editor width", "textstring", Description = "optional css overwrite, example: 800px or 100%")] public string MaxPropertyWidth { get; set; } - - } } diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockListConfigurationEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockListConfigurationEditor.cs index 328e545ded..050bcfbfd2 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/BlockListConfigurationEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockListConfigurationEditor.cs @@ -1,11 +1,4 @@ -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.Linq; -using System.Text.RegularExpressions; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using Umbraco.Core; -using Umbraco.Core.IO; +using Umbraco.Core.IO; using Umbraco.Core.PropertyEditors; namespace Umbraco.Web.PropertyEditors @@ -14,6 +7,8 @@ namespace Umbraco.Web.PropertyEditors { public BlockListConfigurationEditor(IIOHelper ioHelper) : base(ioHelper) { + } + } } diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ComplexPropertyEditorContentEventHandler.cs b/src/Umbraco.Infrastructure/PropertyEditors/ComplexPropertyEditorContentEventHandler.cs new file mode 100644 index 0000000000..d7fd184329 --- /dev/null +++ b/src/Umbraco.Infrastructure/PropertyEditors/ComplexPropertyEditorContentEventHandler.cs @@ -0,0 +1,98 @@ +using System; +using System.Collections.Generic; +using Umbraco.Core.Events; +using Umbraco.Core.Models; +using Umbraco.Core.Services; +using Umbraco.Core.Services.Implement; + +namespace Umbraco.Core.PropertyEditors +{ + /// + /// Utility class for dealing with Copying/Saving events for complex editors + /// + public class ComplexPropertyEditorContentEventHandler : IDisposable + { + private readonly string _editorAlias; + private readonly Func _formatPropertyValue; + private bool _disposedValue; + + public ComplexPropertyEditorContentEventHandler(string editorAlias, + Func formatPropertyValue) + { + _editorAlias = editorAlias; + _formatPropertyValue = formatPropertyValue; + ContentService.Copying += ContentService_Copying; + ContentService.Saving += ContentService_Saving; + } + + /// + /// Copying event handler + /// + /// + /// + private void ContentService_Copying(IContentService sender, CopyEventArgs e) + { + var props = e.Copy.GetPropertiesByEditor(_editorAlias); + UpdatePropertyValues(props, false); + } + + /// + /// Saving event handler + /// + /// + /// + private void ContentService_Saving(IContentService sender, ContentSavingEventArgs e) + { + foreach (var entity in e.SavedEntities) + { + var props = entity.GetPropertiesByEditor(_editorAlias); + UpdatePropertyValues(props, true); + } + } + + private void UpdatePropertyValues(IEnumerable props, bool onlyMissingKeys) + { + foreach (var prop in props) + { + // A Property may have one or more values due to cultures + var propVals = prop.Values; + foreach (var cultureVal in propVals) + { + // Remove keys from published value & any nested properties + var updatedPublishedVal = _formatPropertyValue(cultureVal.PublishedValue?.ToString(), onlyMissingKeys); + cultureVal.PublishedValue = updatedPublishedVal; + + // Remove keys from edited/draft value & any nested properties + var updatedEditedVal = _formatPropertyValue(cultureVal.EditedValue?.ToString(), onlyMissingKeys); + cultureVal.EditedValue = updatedEditedVal; + } + } + } + + /// + /// Unbinds from events + /// + /// + protected virtual void Dispose(bool disposing) + { + if (!_disposedValue) + { + if (disposing) + { + ContentService.Copying -= ContentService_Copying; + ContentService.Saving -= ContentService_Saving; + } + _disposedValue = true; + } + } + + /// + /// Unbinds from events + /// + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + } +} diff --git a/src/Umbraco.Infrastructure/PropertyEditors/NestedContentPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/NestedContentPropertyEditor.cs index 6ad465ecd5..01a03d36e3 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/NestedContentPropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/NestedContentPropertyEditor.cs @@ -95,7 +95,6 @@ namespace Umbraco.Web.PropertyEditors _contentTypes = new Lazy>(() => _contentTypeService.GetAll().ToDictionary(c => c.Alias) ); - } /// diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs index 0c90a41fbd..f46c118174 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs @@ -120,7 +120,9 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters settingsData = null; } - var layoutRef = new BlockListItem(contentGuidUdi, contentData, settingGuidUdi, settingsData); + var layoutType = typeof(BlockListItem<,>).MakeGenericType(contentData.GetType(), settingsData?.GetType() ?? typeof(IPublishedElement)); + var layoutRef = (BlockListItem)Activator.CreateInstance(layoutType, contentGuidUdi, contentData, settingGuidUdi, settingsData); + layout.Add(layoutRef); } diff --git a/src/Umbraco.Persistance.SqlCe/SqlCeSyntaxProvider.cs b/src/Umbraco.Persistance.SqlCe/SqlCeSyntaxProvider.cs index dbb48efa50..5bf0cf04be 100644 --- a/src/Umbraco.Persistance.SqlCe/SqlCeSyntaxProvider.cs +++ b/src/Umbraco.Persistance.SqlCe/SqlCeSyntaxProvider.cs @@ -237,5 +237,11 @@ where table_name=@0 and column_name=@1", tableName, columnName).FirstOrDefault() public override string DropIndex { get { return "DROP INDEX {1}.{0}"; } } + public override string GetSpecialDbType(SpecialDbTypes dbTypes) + { + if (dbTypes == SpecialDbTypes.NVARCHARMAX) // SqlCE does not have nvarchar(max) for now + return "NTEXT"; + return base.GetSpecialDbType(dbTypes); + } } } diff --git a/src/Umbraco.Tests.AcceptanceTest/README.md b/src/Umbraco.Tests.AcceptanceTest/README.md index e8699b0733..541efd40f8 100644 --- a/src/Umbraco.Tests.AcceptanceTest/README.md +++ b/src/Umbraco.Tests.AcceptanceTest/README.md @@ -1,18 +1,18 @@ # Umbraco Acceptance Tests -### Prerequisite +### Prerequisites - NodeJS 12+ - A running installed Umbraco on url: [https://localhost:44331](https://localhost:44331) (Default development port) - Install using a `SqlServer`/`LocalDb` as the tests execute too fast for `SqlCE` to handle. - User information in `cypress.env.json` (See [Getting started](#getting-started)) ### Getting started -The tests is located in the project/folder named `Umbraco.Tests.AcceptanceTests`. Ensur to run `npm install` in that folder, or let your IDE do that. +The tests are located in the project/folder as `Umbraco.Tests.AcceptanceTests`. Make sure you run `npm install` in that folder, or let your IDE do that. -Next, it is important you create a new file in the root of the project called `cypress.env.json`. -This file is already added to `.gitignore` and can contain values that is different for each developer machine. +Next, it is important that you create a new file in the root of the project called `cypress.env.json`. +This file is already added to `.gitignore` and can contain values that are different for each developer machine. -The file need the following content: +The file needs the following content: ``` { "username": "", @@ -24,8 +24,7 @@ Replace the `` and `` placeholder ### Executing tests - -There exists two npm scripts, that can be used to execute the test. +There are two npm scripts that can be used to execute the test: 1. `npm run test` - Executes the tests headless. diff --git a/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Content/content.ts b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Content/content.ts new file mode 100644 index 0000000000..23e97043b0 --- /dev/null +++ b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Content/content.ts @@ -0,0 +1,502 @@ +/// +import { DocumentTypeBuilder, ContentBuilder } from 'umbraco-cypress-testhelpers'; +context('Content', () => { + + beforeEach(() => { + cy.umbracoLogin(Cypress.env('username'), Cypress.env('password')); + }); + + it('Copy content', () => { + const rootDocTypeName = "Test document type"; + const childDocTypeName = "Child test document type"; + const nodeName = "1) Home"; + const childNodeName = "1) Child"; + const anotherNodeName = "2) Home"; + + const childDocType = new DocumentTypeBuilder() + .withName(childDocTypeName) + .build(); + + cy.deleteAllContent(); + cy.umbracoEnsureDocumentTypeNameNotExists(rootDocTypeName); + cy.umbracoEnsureDocumentTypeNameNotExists(childDocTypeName); + + cy.saveDocumentType(childDocType).then((generatedChildDocType) => { + const rootDocTypeAlias; + const createdChildDocType = generatedChildDocType; + + cy.get('li .umb-tree-root:contains("Content")').should("be.visible"); + + const rootDocType = new DocumentTypeBuilder() + .withName(rootDocTypeName) + .withAllowAsRoot(true) + .withAllowedContentTypes(createdChildDocType["id"]) + .build(); + + cy.saveDocumentType(rootDocType).then((generatedRootDocType) => { + rootDocTypeAlias = generatedRootDocType["alias"]; + + const rootContentNode = new ContentBuilder() + .withContentTypeAlias(rootDocTypeAlias) + .withAction("saveNew") + .addVariant() + .withName(nodeName) + .withSave(true) + .done() + .build(); + + cy.saveContent(rootContentNode).then((contentNode) => { + // Add an item under root node + const childContentNode = new ContentBuilder() + .withContentTypeAlias(createdChildDocType["alias"]) + .withAction("saveNew") + .withParent(contentNode["id"]) + .addVariant() + .withName(childNodeName) + .withSave(true) + .done() + .build(); + + cy.saveContent(childContentNode); + }); + + const anotherRootContentNode = new ContentBuilder() + .withContentTypeAlias(rootDocTypeAlias) + .withAction("saveNew") + .addVariant() + .withName(anotherNodeName) + .withSave(true) + .done() + .build(); + + cy.saveContent(anotherRootContentNode); + }); + }); + + // Refresh to update the tree + cy.get('li .umb-tree-root:contains("Content")').should("be.visible").rightclick(); + cy.umbracoContextMenuAction("action-refreshNode").click(); + + // Copy node + cy.umbracoTreeItem("content", [nodeName, childNodeName]).rightclick({ force: true }); + cy.umbracoContextMenuAction("action-copy").click(); + cy.get('.umb-pane [data-element="tree-item-' + anotherNodeName + '"]').click(); + cy.get('.umb-dialog-footer > .btn-primary').click(); + + // Assert + cy.get('.alert-success').should('exist'); + + // Clean up (content is automatically deleted when document types are gone) + cy.umbracoEnsureDocumentTypeNameNotExists(rootDocTypeName); + cy.umbracoEnsureDocumentTypeNameNotExists(childDocTypeName); + }); + + it('Move content', () => { + const rootDocTypeName = "Test document type"; + const childDocTypeName = "Child test document type"; + const nodeName = "1) Home"; + const childNodeName = "1) Child"; + const anotherNodeName = "2) Home"; + + const childDocType = new DocumentTypeBuilder() + .withName(childDocTypeName) + .build(); + + cy.deleteAllContent(); + cy.umbracoEnsureDocumentTypeNameNotExists(rootDocTypeName); + cy.umbracoEnsureDocumentTypeNameNotExists(childDocTypeName); + + cy.saveDocumentType(childDocType).then((generatedChildDocType) => { + const rootDocTypeAlias; + const createdChildDocType = generatedChildDocType; + + cy.get('li .umb-tree-root:contains("Content")').should("be.visible"); + + const rootDocType = new DocumentTypeBuilder() + .withName(rootDocTypeName) + .withAllowAsRoot(true) + .withAllowedContentTypes(createdChildDocType["id"]) + .build(); + + cy.saveDocumentType(rootDocType).then((generatedRootDocType) => { + rootDocTypeAlias = generatedRootDocType["alias"]; + + const rootContentNode = new ContentBuilder() + .withContentTypeAlias(rootDocTypeAlias) + .withAction("saveNew") + .addVariant() + .withName(nodeName) + .withSave(true) + .done() + .build(); + + cy.saveContent(rootContentNode).then((contentNode) => { + // Add an item under root node + const childContentNode = new ContentBuilder() + .withContentTypeAlias(createdChildDocType["alias"]) + .withAction("saveNew") + .withParent(contentNode["id"]) + .addVariant() + .withName(childNodeName) + .withSave(true) + .done() + .build(); + + cy.saveContent(childContentNode); + }); + + const anotherRootContentNode = new ContentBuilder() + .withContentTypeAlias(rootDocTypeAlias) + .withAction("saveNew") + .addVariant() + .withName(anotherNodeName) + .withSave(true) + .done() + .build(); + + cy.saveContent(anotherRootContentNode); + }); + }); + + // Refresh to update the tree + cy.get('li .umb-tree-root:contains("Content")').should("be.visible").rightclick(); + cy.umbracoContextMenuAction("action-refreshNode").click(); + + // Move node + cy.umbracoTreeItem("content", [nodeName, childNodeName]).rightclick({ force: true }); + cy.umbracoContextMenuAction("action-move").click(); + cy.get('.umb-pane [data-element="tree-item-' + anotherNodeName + '"]').click(); + cy.get('.umb-dialog-footer > .btn-primary').click(); + + // Assert + cy.get('.alert-success').should('exist'); + + // Clean up (content is automatically deleted when document types are gone) + cy.umbracoEnsureDocumentTypeNameNotExists(rootDocTypeName); + cy.umbracoEnsureDocumentTypeNameNotExists(childDocTypeName); + }); + + it('Sort content', () => { + const rootDocTypeName = "Test document type"; + const childDocTypeName = "Child test document type"; + const nodeName = "1) Home"; + const firstChildNodeName = "1) Child"; + const secondChildNodeName = "2) Child"; + + const childDocType = new DocumentTypeBuilder() + .withName(childDocTypeName) + .build(); + + cy.deleteAllContent(); + cy.umbracoEnsureDocumentTypeNameNotExists(rootDocTypeName); + cy.umbracoEnsureDocumentTypeNameNotExists(childDocTypeName); + + cy.saveDocumentType(childDocType).then((generatedChildDocType) => { + const createdChildDocType = generatedChildDocType; + + cy.get('li .umb-tree-root:contains("Content")').should("be.visible"); + + const rootDocType = new DocumentTypeBuilder() + .withName(rootDocTypeName) + .withAllowAsRoot(true) + .withAllowedContentTypes(createdChildDocType["id"]) + .build(); + + cy.saveDocumentType(rootDocType).then((generatedRootDocType) => { + const parentId; + + const rootContentNode = new ContentBuilder() + .withContentTypeAlias(generatedRootDocType["alias"]) + .withAction("saveNew") + .addVariant() + .withName(nodeName) + .withSave(true) + .done() + .build(); + + cy.saveContent(rootContentNode).then((contentNode) => { + parentId = contentNode["id"]; + + // Add an item under root node + const firstChildContentNode = new ContentBuilder() + .withContentTypeAlias(createdChildDocType["alias"]) + .withAction("saveNew") + .withParent(parentId) + .addVariant() + .withName(firstChildNodeName) + .withSave(true) + .done() + .build(); + + cy.saveContent(firstChildContentNode); + + // Add a second item under root node + const secondChildContentNode = new ContentBuilder() + .withContentTypeAlias(createdChildDocType["alias"]) + .withAction("saveNew") + .withParent(parentId) + .addVariant() + .withName(secondChildNodeName) + .withSave(true) + .done() + .build(); + + cy.saveContent(secondChildContentNode); + }); + }); + }); + + // Refresh to update the tree + cy.get('li .umb-tree-root:contains("Content")').should("be.visible").rightclick(); + cy.umbracoContextMenuAction("action-refreshNode").click(); + + // Sort nodes + cy.umbracoTreeItem("content", [nodeName]).rightclick({ force: true }); + cy.umbracoContextMenuAction("action-sort").click(); + + //Drag and drop + cy.get('.ui-sortable .ui-sortable-handle :nth-child(2)').eq(0).trigger('mousedown', { which: 1 }) + cy.get('.ui-sortable .ui-sortable-handle :nth-child(2)').eq(1).trigger("mousemove").trigger("mouseup") + + // Save and close dialog + cy.get('.umb-modalcolumn .btn-success').click(); + cy.get('.umb-modalcolumn .btn-link').click(); + + // Assert + cy.get('.umb-tree-item [node="child"]').eq(0).should('contain.text', secondChildNodeName); + cy.get('.umb-tree-item [node="child"]').eq(1).should('contain.text', firstChildNodeName); + + // Clean up (content is automatically deleted when document types are gone) + cy.umbracoEnsureDocumentTypeNameNotExists(rootDocTypeName); + cy.umbracoEnsureDocumentTypeNameNotExists(childDocTypeName); + }); + + it('Rollback content', () => { + const rootDocTypeName = "Test document type"; + const initialNodeName = "Home node"; + const nodeName = "Home"; + + const rootDocType = new DocumentTypeBuilder() + .withName(rootDocTypeName) + .withAllowAsRoot(true) + .build(); + + cy.deleteAllContent(); + cy.umbracoEnsureDocumentTypeNameNotExists(rootDocTypeName); + + cy.saveDocumentType(rootDocType).then((generatedRootDocType) => { + const rootContentNode = new ContentBuilder() + .withContentTypeAlias(generatedRootDocType["alias"]) + .addVariant() + .withName(initialNodeName) + .withSave(true) + .done() + .build(); + + cy.saveContent(rootContentNode) + }); + + // Refresh to update the tree + cy.get('li .umb-tree-root:contains("Content")').should("be.visible").rightclick(); + cy.umbracoContextMenuAction("action-refreshNode").click(); + + // Access node + cy.umbracoTreeItem("content", [initialNodeName]).click(); + + // Edit header + cy.get('#headerName').clear(); + cy.umbracoEditorHeaderName(nodeName); + + // Save and publish + cy.get('.btn-success').first().click(); + + // Rollback + cy.get('.umb-box-header :button').click(); + + cy.get('.umb-box-content > .ng-scope > .input-block-level') + .find('option[label*=' + new Date().getDate() + ']') + .then(elements => { + const option = elements[[elements.length - 1]].getAttribute('value'); + cy.get('.umb-box-content > .ng-scope > .input-block-level') + .select(option); + }); + + cy.get('.umb-editor-footer-content__right-side > [button-style="success"] > .umb-button > .btn-success').click(); + + cy.reload(); + + // Assert + cy.get('.history').find('.umb-badge').eq(0).should('contain.text', "Save"); + cy.get('.history').find('.umb-badge').eq(1).should('contain.text', "Rollback"); + cy.get('#headerName').should('have.value', initialNodeName); + + // Clean up (content is automatically deleted when document types are gone) + cy.umbracoEnsureDocumentTypeNameNotExists(rootDocTypeName); + }); + + it('View audit trail', () => { + const rootDocTypeName = "Test document type"; + const nodeName = "Home"; + const labelName = "Name"; + + const rootDocType = new DocumentTypeBuilder() + .withName(rootDocTypeName) + .withAllowAsRoot(true) + .addGroup() + .addTextBoxProperty() + .withLabel(labelName) + .done() + .done() + .build(); + + cy.deleteAllContent(); + cy.umbracoEnsureDocumentTypeNameNotExists(rootDocTypeName); + + cy.saveDocumentType(rootDocType).then((generatedRootDocType) => { + const rootContentNode = new ContentBuilder() + .withContentTypeAlias(generatedRootDocType["alias"]) + .addVariant() + .withName(nodeName) + .withSave(true) + .done() + .build(); + + cy.saveContent(rootContentNode) + }); + + // Refresh to update the tree + cy.get('li .umb-tree-root:contains("Content")').should("be.visible").rightclick(); + cy.umbracoContextMenuAction("action-refreshNode").click(); + + // Access node + cy.umbracoTreeItem("content", [nodeName]).click(); + + // Navigate to Info app + cy.get(':nth-child(2) > [ng-show="navItem.alias !== \'more\'"]').click(); + + // Assert + cy.get('.history').should('exist'); + + // Clean up (content is automatically deleted when document types are gone) + cy.umbracoEnsureDocumentTypeNameNotExists(rootDocTypeName); + }); + + it('Save draft', () => { + const rootDocTypeName = "Test document type"; + const nodeName = "Home"; + + const rootDocType = new DocumentTypeBuilder() + .withName(rootDocTypeName) + .withAllowAsRoot(true) + .build(); + + cy.deleteAllContent(); + cy.umbracoEnsureDocumentTypeNameNotExists(rootDocTypeName); + + cy.saveDocumentType(rootDocType).then((generatedRootDocType) => { + const rootContentNode = new ContentBuilder() + .withContentTypeAlias(generatedRootDocType["alias"]) + .withAction("saveNew") + .addVariant() + .withName(nodeName) + .withSave(true) + .done() + .build(); + + cy.saveContent(rootContentNode) + }); + + // Refresh to update the tree + cy.get('li .umb-tree-root:contains("Content")').should("be.visible").rightclick(); + cy.umbracoContextMenuAction("action-refreshNode").click(); + + // Access node + cy.umbracoTreeItem("content", [nodeName]).click(); + + // Assert + cy.get('[data-element="node-info-status"]').find('.umb-badge').should('contain.text', "Draft"); + + // Clean up (content is automatically deleted when document types are gone) + cy.umbracoEnsureDocumentTypeNameNotExists(rootDocTypeName); + }); + + it('Preview draft', () => { + const rootDocTypeName = "Test document type"; + const nodeName = "Home"; + + const rootDocType = new DocumentTypeBuilder() + .withName(rootDocTypeName) + .withAllowAsRoot(true) + .build(); + + cy.deleteAllContent(); + cy.umbracoEnsureDocumentTypeNameNotExists(rootDocTypeName); + + cy.saveDocumentType(rootDocType).then((generatedRootDocType) => { + const rootContentNode = new ContentBuilder() + .withContentTypeAlias(generatedRootDocType["alias"]) + .withAction("saveNew") + .addVariant() + .withName(nodeName) + .withSave(true) + .done() + .build(); + + cy.saveContent(rootContentNode) + }); + + // Refresh to update the tree + cy.get('li .umb-tree-root:contains("Content")').should("be.visible").rightclick(); + cy.umbracoContextMenuAction("action-refreshNode").click(); + + // Access node + cy.umbracoTreeItem("content", [nodeName]).click(); + + // Preview + cy.get('[alias="preview"]').should('be.visible').click(); + + // Assert + cy.umbracoSuccessNotification({ multiple: true }).should('be.visible'); + + // Clean up (content is automatically deleted when document types are gone) + cy.umbracoEnsureDocumentTypeNameNotExists(rootDocTypeName); + }); + + it('Publish draft', () => { + const rootDocTypeName = "Test document type"; + const nodeName = "Home"; + + const rootDocType = new DocumentTypeBuilder() + .withName(rootDocTypeName) + .withAllowAsRoot(true) + .build(); + + cy.deleteAllContent(); + cy.umbracoEnsureDocumentTypeNameNotExists(rootDocTypeName); + + cy.saveDocumentType(rootDocType).then((generatedRootDocType) => { + const rootContentNode = new ContentBuilder() + .withContentTypeAlias(generatedRootDocType["alias"]) + .addVariant() + .withName(nodeName) + .withSave(true) + .done() + .build(); + + cy.saveContent(rootContentNode) + }); + + // Refresh to update the tree + cy.get('li .umb-tree-root:contains("Content")').should("be.visible").rightclick(); + cy.umbracoContextMenuAction("action-refreshNode").click(); + + // Access node + cy.umbracoTreeItem("content", [nodeName]).click(); + + // Assert + cy.get('[data-element="node-info-status"]').find('.umb-badge').should('contain.text', "Published"); + + // Clean up (content is automatically deleted when document types are gone) + cy.umbracoEnsureDocumentTypeNameNotExists(rootDocTypeName); + }); +}); diff --git a/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Tour/backofficeTour.ts b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Tour/backofficeTour.ts deleted file mode 100644 index ed891a2eea..0000000000 --- a/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Tour/backofficeTour.ts +++ /dev/null @@ -1,49 +0,0 @@ -/// -context('Backoffice Tour', () => { - - beforeEach(() => { - cy.umbracoLogin(Cypress.env('username'), Cypress.env('password')); - }); - - it('Backoffice introduction tour should run', () => { - //arrange - cy.umbracoGlobalHelp().should("be.visible"); - - //act - cy.umbracoGlobalHelp().click() - //assert - cy.get('[data-element="help-tours"]').should("be.visible"); - //act - cy.get('[data-element="help-tours"]').click(); - //assert - cy.get('[data-element="tour-umbIntroIntroduction"] .umb-button').should("be.visible"); - //act - cy.get('[data-element="tour-umbIntroIntroduction"] .umb-button').click(); - //assert - cy.get('.umb-tour-step', { timeout: 60000 }).should('be.visible'); - cy.get('.umb-tour-step__footer').should('be.visible'); - cy.get('.umb-tour-step__counter').should('be.visible'); - - for(let i=1;i<7;i++){ - cy.get('.umb-tour-step__counter').contains(i + '/12'); - cy.get('.umb-tour-step__footer .umb-button').should('be.visible').click(); - } - cy.umbracoGlobalUser().click() - cy.get('.umb-tour-step__counter').contains('8/12'); - cy.get('.umb-tour-step__footer .umb-button').should('be.visible').click(); - cy.get('.umb-tour-step__counter').contains('9/12'); - cy.get('.umb-overlay-drawer__align-right .umb-button').should('be.visible').click(); - cy.get('.umb-tour-step__counter').contains('10/12'); - cy.umbracoGlobalHelp().click() - - for(let i=11;i<13;i++){ - cy.get('.umb-tour-step__counter').contains(i + '/12'); - cy.get('.umb-tour-step__footer .umb-button').should('be.visible').click(); - } - cy.get('.umb-tour-step__footer .umb-button').should('be.visible').click(); - - //assert - cy.umbracoGlobalHelp().should("be.visible"); - cy.get('[data-element="help-tours"] .umb-progress-circle').contains('17%'); - }); -}); diff --git a/src/Umbraco.Tests.AcceptanceTest/package.json b/src/Umbraco.Tests.AcceptanceTest/package.json index 867b7f5cf3..2017142d1e 100644 --- a/src/Umbraco.Tests.AcceptanceTest/package.json +++ b/src/Umbraco.Tests.AcceptanceTest/package.json @@ -5,9 +5,9 @@ }, "devDependencies": { "cross-env": "^7.0.2", - "cypress": "^4.12.1", + "cypress": "^5.1.0", "ncp": "^2.0.0", - "umbraco-cypress-testhelpers": "^1.0.0-beta-48" + "umbraco-cypress-testhelpers": "^1.0.0-beta-50" }, "dependencies": { "typescript": "^3.9.2" diff --git a/src/Umbraco.Tests/PropertyEditors/BlockEditorComponentTests.cs b/src/Umbraco.Tests/PropertyEditors/BlockEditorComponentTests.cs new file mode 100644 index 0000000000..bfd8b8c77b --- /dev/null +++ b/src/Umbraco.Tests/PropertyEditors/BlockEditorComponentTests.cs @@ -0,0 +1,261 @@ +using Newtonsoft.Json; +using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.Linq; +using Umbraco.Core; +using Umbraco.Web.Compose; + +namespace Umbraco.Tests.PropertyEditors +{ + [TestFixture] + public class BlockEditorComponentTests + { + private readonly JsonSerializerSettings _serializerSettings = new JsonSerializerSettings + { + Formatting = Formatting.None, + NullValueHandling = NullValueHandling.Ignore, + + }; + + private const string _contentGuid1 = "036ce82586a64dfba2d523a99ed80f58"; + private const string _contentGuid2 = "48288c21a38a40ef82deb3eda90a58f6"; + private const string _settingsGuid1 = "ffd35c4e2eea4900abfa5611b67b2492"; + private const string _subContentGuid1 = "4c44ce6b3a5c4f5f8f15e3dc24819a9e"; + private const string _subContentGuid2 = "a062c06d6b0b44ac892b35d90309c7f8"; + private const string _subSettingsGuid1 = "4d998d980ffa4eee8afdc23c4abd6d29"; + + [Test] + public void Cannot_Have_Null_Udi() + { + var component = new BlockEditorComponent(); + var json = GetBlockListJson(null, string.Empty); + Assert.Throws(() => component.ReplaceBlockListUdis(json)); + } + + [Test] + public void No_Nesting() + { + var guids = Enumerable.Range(0, 3).Select(x => Guid.NewGuid()).ToList(); + var guidCounter = 0; + Func guidFactory = () => guids[guidCounter++]; + + var json = GetBlockListJson(null); + + var expected = ReplaceGuids(json, guids, _contentGuid1, _contentGuid2, _settingsGuid1); + + var component = new BlockEditorComponent(); + var result = component.ReplaceBlockListUdis(json, guidFactory); + + var expectedJson = JsonConvert.SerializeObject(JsonConvert.DeserializeObject(expected, _serializerSettings), _serializerSettings); + var resultJson = JsonConvert.SerializeObject(JsonConvert.DeserializeObject(result, _serializerSettings), _serializerSettings); + Console.WriteLine(expectedJson); + Console.WriteLine(resultJson); + Assert.AreEqual(expectedJson, resultJson); + } + + [Test] + public void One_Level_Nesting_Escaped() + { + var guids = Enumerable.Range(0, 6).Select(x => Guid.NewGuid()).ToList(); + + var guidCounter = 0; + Func guidFactory = () => guids[guidCounter++]; + + var innerJson = GetBlockListJson(null, _subContentGuid1, _subContentGuid2, _subSettingsGuid1); + + // we need to ensure the escaped json is consistent with how it will be re-escaped after parsing + // and this is how to do that, the result will also include quotes around it. + var innerJsonEscaped = JsonConvert.ToString(innerJson); + + // get the json with the subFeatures as escaped + var json = GetBlockListJson(innerJsonEscaped); + + var component = new BlockEditorComponent(); + var result = component.ReplaceBlockListUdis(json, guidFactory); + + // the expected result is that the subFeatures data is no longer escaped + var expected = ReplaceGuids(GetBlockListJson(innerJson), guids, + _contentGuid1, _contentGuid2, _settingsGuid1, + _subContentGuid1, _subContentGuid2, _subSettingsGuid1); + + var expectedJson = JsonConvert.SerializeObject(JsonConvert.DeserializeObject(expected, _serializerSettings), _serializerSettings); + var resultJson = JsonConvert.SerializeObject(JsonConvert.DeserializeObject(result, _serializerSettings), _serializerSettings); + Console.WriteLine(expectedJson); + Console.WriteLine(resultJson); + Assert.AreEqual(expectedJson, resultJson); + } + + [Test] + public void One_Level_Nesting_Unescaped() + { + var guids = Enumerable.Range(0, 6).Select(x => Guid.NewGuid()).ToList(); + var guidCounter = 0; + Func guidFactory = () => guids[guidCounter++]; + + // nested blocks without property value escaping used in the conversion + var innerJson = GetBlockListJson(null, _subContentGuid1, _subContentGuid2, _subSettingsGuid1); + + // get the json with the subFeatures as unescaped + var json = GetBlockListJson(innerJson); + + var expected = ReplaceGuids(GetBlockListJson(innerJson), guids, + _contentGuid1, _contentGuid2, _settingsGuid1, + _subContentGuid1, _subContentGuid2, _subSettingsGuid1); + + var component = new BlockEditorComponent(); + var result = component.ReplaceBlockListUdis(json, guidFactory); + + var expectedJson = JsonConvert.SerializeObject(JsonConvert.DeserializeObject(expected, _serializerSettings), _serializerSettings); + var resultJson = JsonConvert.SerializeObject(JsonConvert.DeserializeObject(result, _serializerSettings), _serializerSettings); + Console.WriteLine(expectedJson); + Console.WriteLine(resultJson); + Assert.AreEqual(expectedJson, resultJson); + } + + [Test] + public void Nested_In_Complex_Editor_Escaped() + { + var guids = Enumerable.Range(0, 6).Select(x => Guid.NewGuid()).ToList(); + var guidCounter = 0; + Func guidFactory = () => guids[guidCounter++]; + + var innerJson = GetBlockListJson(null, _subContentGuid1, _subContentGuid2, _subSettingsGuid1); + + // we need to ensure the escaped json is consistent with how it will be re-escaped after parsing + // and this is how to do that, the result will also include quotes around it. + var innerJsonEscaped = JsonConvert.ToString(innerJson); + + // Complex editor such as the grid + var complexEditorJsonEscaped = GetGridJson(innerJsonEscaped); + + var json = GetBlockListJson(complexEditorJsonEscaped); + + var component = new BlockEditorComponent(); + var result = component.ReplaceBlockListUdis(json, guidFactory); + + // the expected result is that the subFeatures data is no longer escaped + var expected = ReplaceGuids(GetBlockListJson(GetGridJson(innerJson)), guids, + _contentGuid1, _contentGuid2, _settingsGuid1, + _subContentGuid1, _subContentGuid2, _subSettingsGuid1); + + var expectedJson = JsonConvert.SerializeObject(JsonConvert.DeserializeObject(expected, _serializerSettings), _serializerSettings); + var resultJson = JsonConvert.SerializeObject(JsonConvert.DeserializeObject(result, _serializerSettings), _serializerSettings); + Console.WriteLine(expectedJson); + Console.WriteLine(resultJson); + Assert.AreEqual(expectedJson, resultJson); + } + + private string GetBlockListJson(string subFeatures, + string contentGuid1 = _contentGuid1, + string contentGuid2 = _contentGuid2, + string settingsGuid1 = _settingsGuid1) + { + return @"{ + ""layout"": + { + ""Umbraco.BlockList"": [ + { + ""contentUdi"": """ + (contentGuid1.IsNullOrWhiteSpace() ? string.Empty : GuidUdi.Create(Constants.UdiEntityType.Element, Guid.Parse(contentGuid1)).ToString()) + @""" + }, + { + ""contentUdi"": ""umb://element/" + contentGuid2 + @""", + ""settingsUdi"": ""umb://element/" + settingsGuid1 + @""" + } + ] + }, + ""contentData"": [ + { + ""contentTypeKey"": ""d6ce4a86-91a2-45b3-a99c-8691fc1fb020"", + ""udi"": """ + (contentGuid1.IsNullOrWhiteSpace() ? string.Empty : GuidUdi.Create(Constants.UdiEntityType.Element, Guid.Parse(contentGuid1)).ToString()) + @""", + ""featureName"": ""Hello"", + ""featureDetails"": ""World"" + }, + { + ""contentTypeKey"": ""d6ce4a86-91a2-45b3-a99c-8691fc1fb020"", + ""udi"": ""umb://element/" + contentGuid2 + @""", + ""featureName"": ""Another"", + ""featureDetails"": ""Feature""" + (subFeatures == null ? string.Empty : (@", ""subFeatures"": " + subFeatures)) + @" + } + ], + ""settingsData"": [ + { + ""contentTypeKey"": ""d6ce4a86-91a2-45b3-a99c-8691fc1fb020"", + ""udi"": ""umb://element/" + settingsGuid1 + @""", + ""featureName"": ""Setting 1"", + ""featureDetails"": ""Setting 2"" + }, + ] +}"; + } + + private string GetGridJson(string subBlockList) + { + return @"{ + ""name"": ""1 column layout"", + ""sections"": [ + { + ""grid"": ""12"", + ""rows"": [ + { + ""name"": ""Article"", + ""id"": ""b4f6f651-0de3-ef46-e66a-464f4aaa9c57"", + ""areas"": [ + { + ""grid"": ""4"", + ""controls"": [ + { + ""value"": ""I am quote"", + ""editor"": { + ""alias"": ""quote"", + ""view"": ""textstring"" + }, + ""styles"": null, + ""config"": null + }], + ""styles"": null, + ""config"": null + }, + { + ""grid"": ""8"", + ""controls"": [ + { + ""value"": ""Header"", + ""editor"": { + ""alias"": ""headline"", + ""view"": ""textstring"" + }, + ""styles"": null, + ""config"": null + }, + { + ""value"": " + subBlockList + @", + ""editor"": { + ""alias"": ""madeUpNestedContent"", + ""view"": ""madeUpNestedContentInGrid"" + }, + ""styles"": null, + ""config"": null + }], + ""styles"": null, + ""config"": null + }], + ""styles"": null, + ""config"": null + }] + }] +}"; + } + + private string ReplaceGuids(string json, List newGuids, params string[] oldGuids) + { + for (var i = 0; i < oldGuids.Length; i++) + { + var old = oldGuids[i]; + json = json.Replace(old, newGuids[i].ToString("N")); + } + return json; + } + + } +} diff --git a/src/Umbraco.Tests/PropertyEditors/NestedContentPropertyComponentTests.cs b/src/Umbraco.Tests/PropertyEditors/NestedContentPropertyComponentTests.cs index 1b83c048d2..5b7e220123 100644 --- a/src/Umbraco.Tests/PropertyEditors/NestedContentPropertyComponentTests.cs +++ b/src/Umbraco.Tests/PropertyEditors/NestedContentPropertyComponentTests.cs @@ -9,6 +9,7 @@ using Umbraco.Web.Compose; namespace Umbraco.Tests.PropertyEditors { + [TestFixture] public class NestedContentPropertyComponentTests { diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index d2ace9d0c2..f1db4b619f 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -143,6 +143,7 @@ + diff --git a/src/Umbraco.Web.BackOffice/Controllers/DictionaryController.cs b/src/Umbraco.Web.BackOffice/Controllers/DictionaryController.cs index 26cccb9141..e43f124cc8 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/DictionaryController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/DictionaryController.cs @@ -141,7 +141,7 @@ namespace Umbraco.Web.BackOffice.Controllers /// /// Returns a not found response when dictionary item does not exist /// - [DetermineAmbiguousActionByPassingParameters] + [DetermineAmbiguousActionByPassingParameters] public ActionResult GetById(int id) { var dictionary = _localizationService.GetDictionaryItemById(id); diff --git a/src/Umbraco.Web.BackOffice/Controllers/ImagesController.cs b/src/Umbraco.Web.BackOffice/Controllers/ImagesController.cs index b264fd987b..70e0c71e55 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/ImagesController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/ImagesController.cs @@ -99,9 +99,6 @@ namespace Umbraco.Web.BackOffice.Controllers /// /// /// - /// - /// - /// /// /// /// If there is no media, image property or image file is found then this will return not found. @@ -112,7 +109,7 @@ namespace Umbraco.Web.BackOffice.Controllers decimal? focalPointLeft = null, decimal? focalPointTop = null, string animationProcessMode = "first", - ImageCropMode mode = ImageCropMode.Max, + ImageCropMode mode = ImageCropMode.Max, bool upscale = false, string cacheBusterValue = "", decimal? cropX1 = null, diff --git a/src/Umbraco.Web.BackOffice/Controllers/RelationTypeController.cs b/src/Umbraco.Web.BackOffice/Controllers/RelationTypeController.cs index 0991dca09f..54fc3352d3 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/RelationTypeController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/RelationTypeController.cs @@ -15,7 +15,6 @@ using Umbraco.Core.Mapping; using Umbraco.Web.BackOffice.Filters; using Umbraco.Web.Common.Attributes; using Umbraco.Web.Common.Exceptions; -using Umbraco.Web.Editors; namespace Umbraco.Web.BackOffice.Controllers { @@ -43,9 +42,8 @@ namespace Umbraco.Web.BackOffice.Controllers _shortStringHelper = shortStringHelper ?? throw new ArgumentNullException(nameof(shortStringHelper)); } - /// - /// Gets a relation type by ID. + /// Gets a relation type by id /// /// The relation type ID. /// Returns the . @@ -63,7 +61,6 @@ namespace Umbraco.Web.BackOffice.Controllers return display; } - /// /// Gets a relation type by guid /// diff --git a/src/Umbraco.Web.UI.Client/gulp/tasks/dependencies.js b/src/Umbraco.Web.UI.Client/gulp/tasks/dependencies.js index fb03cbd1b1..93191dbaf5 100644 --- a/src/Umbraco.Web.UI.Client/gulp/tasks/dependencies.js +++ b/src/Umbraco.Web.UI.Client/gulp/tasks/dependencies.js @@ -218,10 +218,10 @@ function dependencies() { { "name": "spectrum", "src": [ - "./node_modules/spectrum-colorpicker/spectrum.js", - "./node_modules/spectrum-colorpicker/spectrum.css" + "./node_modules/spectrum-colorpicker2/dist/spectrum.js", + "./node_modules/spectrum-colorpicker2/dist/spectrum.css" ], - "base": "./node_modules/spectrum-colorpicker" + "base": "./node_modules/spectrum-colorpicker2/dist" }, { "name": "tinymce", diff --git a/src/Umbraco.Web.UI.Client/package.json b/src/Umbraco.Web.UI.Client/package.json index 100774f385..e0afe1c2a7 100644 --- a/src/Umbraco.Web.UI.Client/package.json +++ b/src/Umbraco.Web.UI.Client/package.json @@ -38,10 +38,10 @@ "lazyload-js": "1.0.0", "moment": "2.22.2", "ng-file-upload": "12.2.13", - "nouislider": "14.6.0", + "nouislider": "14.6.1", "npm": "^6.14.7", "signalr": "2.4.0", - "spectrum-colorpicker": "1.8.0", + "spectrum-colorpicker2": "2.0.3", "tinymce": "4.9.11", "typeahead.js": "0.11.1", "underscore": "1.9.1", @@ -72,11 +72,11 @@ "gulp-wrap": "0.15.0", "gulp-wrap-js": "0.4.1", "jasmine-core": "3.5.0", + "jsdom": "16.4.0", "karma": "4.4.1", - "karma-chrome-launcher": "^3.1.0", + "karma-jsdom-launcher": "^8.0.2", "karma-jasmine": "2.0.1", "karma-junit-reporter": "2.0.1", - "karma-phantomjs-launcher": "1.0.4", "karma-spec-reporter": "0.0.32", "less": "3.10.3", "lodash": "4.17.19", 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 76687dc0d6..6e4a388276 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 @@ -228,6 +228,9 @@ Use this directive to construct a header inside the main editor window. // to make it work for language edit/create setAccessibilityForEditorState(); scope.loading = false; + } else if (scope.name) { + setAccessibilityForName(); + scope.loading = false; } else { scope.loading = false; } @@ -266,6 +269,15 @@ Use this directive to construct a header inside the main editor window. editorService.iconPicker(iconPicker); }; + function setAccessibilityForName() { + var setTitle = false; + if (scope.setpagetitle !== undefined) { + setTitle = scope.setpagetitle; + } + if (setTitle) { + setAccessibilityHeaderDirective(false, scope.editorfor, scope.nameLocked, scope.name, "", true); + } + } function setAccessibilityForEditorState() { var isNew = editorState.current.id === 0 || editorState.current.id === "0" || diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/events/onOutsideClick.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/events/onOutsideClick.directive.js index a657fcbc55..07c8dc88fe 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/events/onOutsideClick.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/events/onOutsideClick.directive.js @@ -44,7 +44,7 @@ } // please to not use angularHelper.safeApply here, it won't work - scope.$apply(attrs.onOutsideClick); + scope.$evalAsync(attrs.onOutsideClick); } diff --git a/src/Umbraco.Web.UI.Client/src/views/components/property/property-actions/umbpropertyactions.component.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/property/umbpropertyactions.component.js similarity index 51% rename from src/Umbraco.Web.UI.Client/src/views/components/property/property-actions/umbpropertyactions.component.js rename to src/Umbraco.Web.UI.Client/src/common/directives/components/property/umbpropertyactions.component.js index b0dc15d6cd..41dbd9f547 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/property/property-actions/umbpropertyactions.component.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/property/umbpropertyactions.component.js @@ -5,50 +5,77 @@ * A component to render the property action toggle */ - function umbPropertyActionsController(keyboardService) { + function umbPropertyActionsController(keyboardService, localizationService) { var vm = this; vm.isOpen = false; + vm.labels = { + openText: "Open Property Actions", + closeText: "Close Property Actions" + }; + + vm.open = open; + vm.close = close; + vm.toggle = toggle; + vm.executeAction = executeAction; + + vm.$onDestroy = onDestroy; + vm.$onInit = onInit; function initDropDown() { keyboardService.bind("esc", vm.close); } + function destroyDropDown() { keyboardService.unbind("esc"); } - vm.toggle = function() { + function toggle() { if (vm.isOpen === true) { vm.close(); } else { vm.open(); } } - vm.open = function() { + + function open() { vm.isOpen = true; initDropDown(); } - vm.close = function() { + + function close() { vm.isOpen = false; destroyDropDown(); } - vm.executeAction = function(action) { + function executeAction(action) { action.method(); vm.close(); } - vm.$onDestroy = function () { + function onDestroy() { if (vm.isOpen === true) { destroyDropDown(); } } - + + function onInit() { + + var labelKeys = [ + "propertyActions_tooltipForPropertyActionsMenu", + "propertyActions_tooltipForPropertyActionsMenuClose" + ] + + localizationService.localizeMany(labelKeys).then(values => { + vm.labels.openText = values[0]; + vm.labels.closeText = values[1]; + }); + } } var umbPropertyActionsComponent = { - templateUrl: 'views/components/property/property-actions/umb-property-actions.html', + templateUrl: 'views/components/property/umb-property-actions.html', bindings: { actions: "<" }, diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtreeitem.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtreeitem.directive.js index f2fc0d2dae..3ec1756e6c 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtreeitem.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtreeitem.directive.js @@ -190,7 +190,7 @@ angular.module("umbraco.directives") var evts = []; - //listen for section changes + // Listen for section changes evts.push(eventsService.on("appState.sectionState.changed", function(e, args) { if (args.key === "currentSection") { //when the section changes disable all delete animations @@ -198,6 +198,13 @@ angular.module("umbraco.directives") } })); + // Update tree icon if changed + evts.push(eventsService.on("editors.tree.icon.changed", function (e, args) { + if (args.icon !== scope.node.icon && args.id === scope.node.id) { + scope.node.icon = args.icon; + } + })); + /** Depending on if any menu is shown and if the menu is shown for the current node, toggle delete animations */ function toggleDeleteAnimations() { //if both are false then remove animations diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbdropdown.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbdropdown.directive.js index d6006114a6..cfba5be2ce 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbdropdown.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbdropdown.directive.js @@ -22,7 +22,7 @@ - {{ item.name }} + @@ -110,7 +110,7 @@ // Stop listening when scope is destroyed. scope.$on('$destroy', stopListening); - + } var directive = { diff --git a/src/Umbraco.Web.UI.Client/src/views/components/umb-mini-search/umbminisearch.component.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbminisearch.component.js similarity index 79% rename from src/Umbraco.Web.UI.Client/src/views/components/umb-mini-search/umbminisearch.component.js rename to src/Umbraco.Web.UI.Client/src/common/directives/components/umbminisearch.component.js index d7aee744e4..6c65cb6e23 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/umb-mini-search/umbminisearch.component.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbminisearch.component.js @@ -4,7 +4,7 @@ angular .module('umbraco') .component('umbMiniSearch', { - templateUrl: 'views/components/umb-mini-search/umb-mini-search.html', + templateUrl: 'views/components/umb-mini-search.html', controller: UmbMiniSearchController, controllerAs: 'vm', bindings: { @@ -18,6 +18,9 @@ function UmbMiniSearchController($scope) { var vm = this; + + vm.onKeyDown = onKeyDown; + vm.onChange = onChange; var searchDelay = _.debounce(function () { $scope.$apply(function () { @@ -27,23 +30,23 @@ }); }, 500); - vm.onKeyDown = function (ev) { + function onKeyDown(evt) { //13: enter - switch (ev.keyCode) { + switch (evt.keyCode) { case 13: if (vm.onSearch) { vm.onSearch(); } break; } - }; + } - vm.onChange = function () { + function onChange() { if (vm.onStartTyping) { vm.onStartTyping(); } searchDelay(); - }; + } } diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/util/umbkeyboardlist.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/util/umbkeyboardlist.directive.js index 20572fdf16..0b743d0f10 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/util/umbkeyboardlist.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/util/umbkeyboardlist.directive.js @@ -10,12 +10,12 @@
     
@@ -33,11 +33,11 @@ angular.module('umbraco.directives') return { restrict: 'A', link: function (scope, element, attr) { - + var listItems = []; var currentIndex = 0; var focusSet = false; - + $timeout(function(){ // get list of all links in the list listItems = element.find("li :tabbable"); @@ -82,7 +82,7 @@ angular.module('umbraco.directives') function arrowDown() { if (currentIndex < listItems.length - 1) { - // only bump the current index if the focus is already + // only bump the current index if the focus is already // set else we just want to focus the first element if (focusSet) { currentIndex++; @@ -112,4 +112,4 @@ angular.module('umbraco.directives') } }; - }]); \ No newline at end of file + }]); diff --git a/src/Umbraco.Web.UI.Client/src/common/filters/compareArrays.filter.js b/src/Umbraco.Web.UI.Client/src/common/filters/compareArrays.filter.js index 13f603260d..0cbf3077e6 100644 --- a/src/Umbraco.Web.UI.Client/src/common/filters/compareArrays.filter.js +++ b/src/Umbraco.Web.UI.Client/src/common/filters/compareArrays.filter.js @@ -2,13 +2,17 @@ angular.module("umbraco.filters") .filter('compareArrays', function() { return function inArray(array, compareArray, compareProperty) { + if (!compareArray || !compareArray.length) { + return [...array]; + } + var result = []; - angular.forEach(array, function(arrayItem){ + array.forEach(function(arrayItem){ var exists = false; - angular.forEach(compareArray, function(compareItem){ + compareArray.forEach(function(compareItem){ if( arrayItem[compareProperty] === compareItem[compareProperty]) { exists = true; } diff --git a/src/Umbraco.Web.UI.Client/src/common/services/angularhelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/angularhelper.service.js index fd620bac18..7e7f804656 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/angularhelper.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/angularhelper.service.js @@ -95,7 +95,7 @@ function angularHelper($q) { */ revalidateNgModel: function (scope, ngModel) { this.safeApply(scope, function() { - angular.forEach(ngModel.$parsers, function (parser) { + ngModel.$parsers.forEach(function (parser) { parser(ngModel.$viewValue); }); }); diff --git a/src/Umbraco.Web.UI.Client/src/common/services/blockeditor.service.js b/src/Umbraco.Web.UI.Client/src/common/services/blockeditor.service.js index ffb1971169..dfa0eae297 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/blockeditor.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/blockeditor.service.js @@ -4,39 +4,113 @@ * * @description * Added in Umbraco 8.7. Service for dealing with Block Editors. - * + * * Block Editor Service provides the basic features for a block editor. * The main feature is the ability to create a Model Object which takes care of your data for your Block Editor. - * - * + * + * * ##Samples * * ####Instantiate a Model Object for your property editor: - * + * *
  *     modelObject = blockEditorService.createModelObject(vm.model.value, vm.model.editor, vm.model.config.blocks, $scope);
  *     modelObject.load().then(onLoaded);
  * 
* - * + * * See {@link umbraco.services.blockEditorModelObject BlockEditorModelObject} for more samples. - * + * */ (function () { 'use strict'; + + /** + * When performing a runtime copy of Block Editors entries, we copy the ElementType Data Model and inner IDs are kept identical, to ensure new IDs are changed on paste we need to provide a resolver for the ClipboardService. + */ + angular.module('umbraco').run(['clipboardService', 'udiService', function (clipboardService, udiService) { + + function replaceUdi(obj, key, dataObject) { + var udi = obj[key]; + var newUdi = udiService.create("element"); + obj[key] = newUdi; + dataObject.forEach((data) => { + if (data.udi === udi) { + data.udi = newUdi; + } + }); + } + function replaceUdisOfObject(obj, propValue) { + for (var k in obj) { + if(k === "contentUdi") { + replaceUdi(obj, k, propValue.contentData); + } else if(k === "settingsUdi") { + replaceUdi(obj, k, propValue.settingsData); + } else { + // lets crawl through all properties of layout to make sure get captured all `contentUdi` and `settingsUdi` properties. + var propType = typeof obj[k]; + if(propType === "object" || propType === "array") { + replaceUdisOfObject(obj[k], propValue) + } + } + } + } + function replaceElementTypeBlockListUDIsResolver(obj, propClearingMethod) { + replaceRawBlockListUDIsResolver(obj.value, propClearingMethod); + } + + clipboardService.registerPastePropertyResolver(replaceElementTypeBlockListUDIsResolver, clipboardService.TYPES.ELEMENT_TYPE); + + + function replaceRawBlockListUDIsResolver(value, propClearingMethod) { + if (typeof value === "object") { + + // we got an object, and it has these three props then we are most likely dealing with a Block Editor. + if ((value.layout !== undefined && value.contentData !== undefined && value.settingsData !== undefined)) { + + replaceUdisOfObject(value.layout, value); + + // replace UDIs for inner properties of this Block Editors content data. + if(value.contentData.length > 0) { + value.contentData.forEach((item) => { + for (var k in item) { + propClearingMethod(item[k], clipboardService.TYPES.RAW); + } + }); + } + // replace UDIs for inner properties of this Block Editors settings data. + if(value.settingsData.length > 0) { + value.settingsData.forEach((item) => { + for (var k in item) { + propClearingMethod(item[k], clipboardService.TYPES.RAW); + } + }); + } + + } + } + } + + clipboardService.registerPastePropertyResolver(replaceRawBlockListUDIsResolver, clipboardService.TYPES.RAW); + + }]); + + + + function blockEditorService(blockEditorModelObject) { /** * @ngdocs function * @name createModelObject * @methodOf umbraco.services.blockEditorService - * + * * @description * Create a new Block Editor Model Object. * See {@link umbraco.services.blockEditorModelObject blockEditorModelObject} - * + * * @see umbraco.services.blockEditorModelObject * @param {object} propertyModelValue data object of the property editor, usually model.value. * @param {string} propertyEditorAlias alias of the property. diff --git a/src/Umbraco.Web.UI.Client/src/common/services/blockeditormodelobject.service.js b/src/Umbraco.Web.UI.Client/src/common/services/blockeditormodelobject.service.js index 86b5bdd0d0..868b8baba7 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/blockeditormodelobject.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/blockeditormodelobject.service.js @@ -13,8 +13,7 @@ (function () { 'use strict'; - - function blockEditorModelObjectFactory($interpolate, $q, udiService, contentResource, localizationService, umbRequestHelper) { + function blockEditorModelObjectFactory($interpolate, $q, udiService, contentResource, localizationService, umbRequestHelper, clipboardService) { /** * Simple mapping from property model content entry to editing model, @@ -231,7 +230,8 @@ var notSupportedProperties = [ "Umbraco.Tags", "Umbraco.UploadField", - "Umbraco.ImageCropper" + "Umbraco.ImageCropper", + "Umbraco.NestedContent" ]; @@ -524,12 +524,11 @@ } var blockConfiguration = this.getBlockConfiguration(dataModel.contentTypeKey); - var contentScaffold; + var contentScaffold = null; if (blockConfiguration === null) { - console.error("The block of " + contentUdi + " is not being initialized because its contentTypeKey('" + dataModel.contentTypeKey + "') is not allowed for this PropertyEditor"); - } - else { + console.warn("The block of " + contentUdi + " is not being initialized because its contentTypeKey('" + dataModel.contentTypeKey + "') is not allowed for this PropertyEditor"); + } else { contentScaffold = this.getScaffoldFromKey(blockConfiguration.contentElementTypeKey); if (contentScaffold === null) { console.error("The block of " + contentUdi + " is not begin initialized cause its Element Type was not loaded."); @@ -539,11 +538,9 @@ if (blockConfiguration === null || contentScaffold === null) { blockConfiguration = { - label: "Unsupported Block", + label: "Unsupported", unsupported: true }; - contentScaffold = {}; - } var blockObject = {}; @@ -568,10 +565,14 @@ , 10); // make basics from scaffold - blockObject.content = Utilities.copy(contentScaffold); - ensureUdiAndKey(blockObject.content, contentUdi); + if(contentScaffold !== null) {// We might not have contentScaffold + blockObject.content = Utilities.copy(contentScaffold); + ensureUdiAndKey(blockObject.content, contentUdi); - mapToElementModel(blockObject.content, dataModel); + mapToElementModel(blockObject.content, dataModel); + } else { + blockObject.content = null; + } blockObject.data = dataModel; blockObject.layout = layoutEntry; @@ -614,8 +615,7 @@ if (this.config.settingsElementTypeKey !== null) { mapElementValues(settings, this.settings); } - } - + }; blockObject.sync = function () { if (this.content !== null) { @@ -624,7 +624,7 @@ if (this.config.settingsElementTypeKey !== null) { mapToPropertyModel(this.settings, this.settingsData); } - } + }; // first time instant update of label. blockObject.label = getBlockLabel(blockObject); @@ -663,7 +663,6 @@ } return blockObject; - }, /** @@ -675,11 +674,8 @@ * @param {Object} blockObject The BlockObject to be removed and destroyed. */ removeDataAndDestroyModel: function (blockObject) { - var udi = blockObject.content.udi; - var settingsUdi = null; - if (blockObject.settings) { - settingsUdi = blockObject.settings.udi; - } + var udi = blockObject.layout.contentUdi; + var settingsUdi = blockObject.layout.settingsUdi || null; this.destroyBlockObject(blockObject); this.removeDataByUdi(udi); if (settingsUdi) { @@ -748,7 +744,7 @@ */ createFromElementType: function (elementTypeDataModel) { - elementTypeDataModel = Utilities.copy(elementTypeDataModel); + elementTypeDataModel = clipboardService.parseContentForPaste(elementTypeDataModel, clipboardService.TYPES.ELEMENT_TYPE); var contentElementTypeKey = elementTypeDataModel.contentTypeKey; diff --git a/src/Umbraco.Web.UI.Client/src/common/services/clipboard.service.js b/src/Umbraco.Web.UI.Client/src/common/services/clipboard.service.js index 0d2ca6623b..58ed07367e 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/clipboard.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/clipboard.service.js @@ -13,7 +13,28 @@ function clipboardService(notificationsService, eventsService, localStorageService, iconHelper) { - var clearPropertyResolvers = []; + const TYPES = {}; + TYPES.ELEMENT_TYPE = "elementType"; + TYPES.RAW = "raw"; + + var clearPropertyResolvers = {}; + var pastePropertyResolvers = {}; + var clipboardTypeResolvers = {}; + + clipboardTypeResolvers[TYPES.ELEMENT_TYPE] = function(data, propMethod) { + for (var t = 0; t < data.variants[0].tabs.length; t++) { + var tab = data.variants[0].tabs[t]; + for (var p = 0; p < tab.properties.length; p++) { + var prop = tab.properties[p]; + propMethod(prop, TYPES.ELEMENT_TYPE); + } + } + } + clipboardTypeResolvers[TYPES.RAW] = function(data, propMethod) { + for (var p = 0; p < data.length; p++) { + propMethod(data[p], TYPES.RAW); + } + } var STORAGE_KEY = "umbClipboardService"; @@ -57,28 +78,29 @@ function clipboardService(notificationsService, eventsService, localStorageServi } - function clearPropertyForStorage(prop) { + function resolvePropertyForStorage(prop, type) { - for (var i=0; i prepareEntryForStorage(data, firstLevelClearupMethod)); + var copiedDatas = datas.map(data => prepareEntryForStorage(type, data, firstLevelClearupMethod)); // remove previous copies of this entry: storage.entries = storage.entries.filter( diff --git a/src/Umbraco.Web.UI.Client/src/common/services/contenteditinghelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/contenteditinghelper.service.js index c27283d5ad..6d41ea087d 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 @@ -151,7 +151,7 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, editorSt // first check if tab is already added var foundInfoTab = false; - angular.forEach(tabs, function (tab) { + tabs.forEach(function (tab) { if (tab.id === infoTab.id && tab.alias === infoTab.alias) { foundInfoTab = true; } diff --git a/src/Umbraco.Web.UI.Client/src/common/services/contenttypehelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/contenttypehelper.service.js index 1be66cc68f..9cec15d519 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/contenttypehelper.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/contenttypehelper.service.js @@ -11,7 +11,7 @@ function contentTypeHelper(contentTypeResource, dataTypeResource, $filter, $inje var newArray = []; - angular.forEach(array, function (arrayItem) { + array.forEach(function (arrayItem) { if (Utilities.isObject(arrayItem)) { newArray.push(arrayItem.id); @@ -116,13 +116,12 @@ function contentTypeHelper(contentTypeResource, dataTypeResource, $filter, $inje throw new Error("Cannot add this composition, these properties already exist on the content type: " + overlappingAliases.join()); } - angular.forEach(compositeContentType.groups, function (compositionGroup) { - + compositeContentType.groups.forEach(function (compositionGroup) { // order composition groups based on sort order compositionGroup.properties = $filter('orderBy')(compositionGroup.properties, 'sortOrder'); // get data type details - angular.forEach(compositionGroup.properties, function (property) { + compositionGroup.properties.forEach(function (property) { dataTypeResource.getById(property.dataTypeId) .then(function (dataType) { property.dataTypeIcon = dataType.icon; @@ -134,7 +133,7 @@ function contentTypeHelper(contentTypeResource, dataTypeResource, $filter, $inje compositionGroup.inherited = true; // set inherited state on properties - angular.forEach(compositionGroup.properties, function (compositionProperty) { + compositionGroup.properties.forEach(function (compositionProperty) { compositionProperty.inherited = true; }); @@ -142,7 +141,7 @@ function contentTypeHelper(contentTypeResource, dataTypeResource, $filter, $inje compositionGroup.tabState = "inActive"; // if groups are named the same - merge the groups - angular.forEach(contentType.groups, function (contentTypeGroup) { + contentType.groups.forEach(function (contentTypeGroup) { if (contentTypeGroup.name === compositionGroup.name) { @@ -224,7 +223,7 @@ function contentTypeHelper(contentTypeResource, dataTypeResource, $filter, $inje var groups = []; - angular.forEach(contentType.groups, function (contentTypeGroup) { + contentType.groups.forEach(function (contentTypeGroup) { if (contentTypeGroup.tabState !== "init") { @@ -238,7 +237,7 @@ function contentTypeHelper(contentTypeResource, dataTypeResource, $filter, $inje var properties = []; // remove all properties from composite content type - angular.forEach(contentTypeGroup.properties, function (property) { + contentTypeGroup.properties.forEach(function (property) { if (property.contentTypeId !== compositeContentType.id) { properties.push(property); } @@ -283,7 +282,7 @@ function contentTypeHelper(contentTypeResource, dataTypeResource, $filter, $inje var sortOrder = 0; - angular.forEach(properties, function (property) { + properties.forEach(function (property) { if (!property.inherited && property.propertyState !== "init") { property.sortOrder = sortOrder; } diff --git a/src/Umbraco.Web.UI.Client/src/common/services/editor.service.js b/src/Umbraco.Web.UI.Client/src/common/services/editor.service.js index 0f4f04c6bf..381d09f62d 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/editor.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/editor.service.js @@ -761,6 +761,23 @@ When building a custom infinite editor view you can use the same components as a open(editor); } + /** + * @ngdoc method + * @name umbraco.services.editorService#userGroupEditor + * @methodOf umbraco.services.editorService + * + * @description + * Opens the user group picker in infinite editing, the submit callback returns the saved user group + * @param {Object} editor rendering options + * @param {Callback} editor.submit Submits the editor + * @param {Callback} editor.close Closes the editor + * @returns {Object} editor object + */ + function userGroupEditor(editor) { + editor.view = "views/users/group.html"; + open(editor); + } + /** * @ngdoc method * @name umbraco.services.editorService#templateEditor @@ -1028,6 +1045,7 @@ When building a custom infinite editor view you can use the same components as a nodePermissions: nodePermissions, insertCodeSnippet: insertCodeSnippet, userGroupPicker: userGroupPicker, + userGroupEditor: userGroupEditor, templateEditor: templateEditor, sectionPicker: sectionPicker, insertField: insertField, 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 d9c11770cc..bd6bbcc5b3 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) { @@ -46,7 +46,12 @@ function formHelper(angularHelper, serverValidationManager, notificationsService args.scope.$broadcast("formSubmitting", { scope: args.scope, action: args.action }); this.focusOnFirstError(currentForm); - args.scope.$broadcast("postFormSubmitting", { 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("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) { @@ -101,18 +106,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) { @@ -137,7 +156,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) { @@ -176,7 +195,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/common/services/listviewhelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/listviewhelper.service.js index 28156e70c3..ee1e2a2311 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/listviewhelper.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/listviewhelper.service.js @@ -419,7 +419,7 @@ if (isSelectedAll(items, selection)) { // unselect all items - angular.forEach(items, function (item) { + items.forEach(function (item) { item.selected = false; }); @@ -432,7 +432,7 @@ selection.length = 0; // select all items - angular.forEach(items, function (item) { + items.forEach(function (item) { var obj = { id: item.id }; diff --git a/src/Umbraco.Web.UI.Client/src/common/services/navigation.service.js b/src/Umbraco.Web.UI.Client/src/common/services/navigation.service.js index 0907538b24..c1d84aa16b 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/navigation.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/navigation.service.js @@ -127,6 +127,13 @@ function navigationService($routeParams, $location, $q, $injector, eventsService } } + function showBackdrop() { + var backDropOptions = { + 'element': $('#leftcolumn')[0] + }; + backdropService.open(backDropOptions); + } + var service = { /** @@ -427,13 +434,9 @@ function navigationService($routeParams, $location, $q, $injector, eventsService showMenu: function (args) { var self = this; - var backDropOptions = { - 'element': $('#leftcolumn')[0] - }; - return treeService.getMenu({ treeNode: args.node }) .then(function (data) { - backdropService.open(backDropOptions); + showBackdrop(); //check for a default //NOTE: event will be undefined when a call to hideDialog is made so it won't re-load the default again. // but perhaps there's a better way to deal with with an additional parameter in the args ? it works though. @@ -544,6 +547,7 @@ function navigationService($routeParams, $location, $q, $injector, eventsService } } else { + showBackdrop(); service.showDialog({ node: node, action: action, diff --git a/src/Umbraco.Web.UI.Client/src/common/services/search.service.js b/src/Umbraco.Web.UI.Client/src/common/services/search.service.js index 803cd857b7..8e9525af84 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/search.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/search.service.js @@ -12,7 +12,7 @@ * *
  *      searchService.searchMembers({term: 'bob'}).then(function(results){
- *          angular.forEach(results, function(result){
+ *          results.forEach(function(result){
  *                  //returns:
  *                  {name: "name", id: 1234, menuUrl: "url", editorPath: "url", metaData: {}, subtitle: "/path/etc" }
  *           })
diff --git a/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js b/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js
index 5d6b4646a3..6c0165ebfe 100644
--- a/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js
+++ b/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js
@@ -107,7 +107,7 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s
 
         //queue rules loading
         if (configuredStylesheets) {
-            angular.forEach(configuredStylesheets, function (val, key) {
+            configuredStylesheets.forEach(function (val, key) {
 
                 if (val.indexOf(Umbraco.Sys.ServerVariables.umbracoSettings.cssPath + "/") === 0) {
                     // current format (full path to stylesheet)
@@ -119,7 +119,7 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s
                 }
 
                 promises.push(stylesheetResource.getRulesByName(val).then(function (rules) {
-                    angular.forEach(rules, function (rule) {
+                    rules.forEach(function (rule) {
                         var r = {};
                         r.title = rule.name;
                         if (rule.selector[0] == ".") {
diff --git a/src/Umbraco.Web.UI.Client/src/installer/steps/database.html b/src/Umbraco.Web.UI.Client/src/installer/steps/database.html
index cfe0940aa2..ebffc4cf97 100644
--- a/src/Umbraco.Web.UI.Client/src/installer/steps/database.html
+++ b/src/Umbraco.Web.UI.Client/src/installer/steps/database.html
@@ -40,7 +40,7 @@
                         
                         
Enter server domain or IP
diff --git a/src/Umbraco.Web.UI.Client/src/less/belle.less b/src/Umbraco.Web.UI.Client/src/less/belle.less index d99573decb..ff834de837 100644 --- a/src/Umbraco.Web.UI.Client/src/less/belle.less +++ b/src/Umbraco.Web.UI.Client/src/less/belle.less @@ -77,7 +77,6 @@ @import "main.less"; @import "listview.less"; @import "gridview.less"; -@import "filter-toggle.less"; @import "forms/umb-validation-label.less"; @@ -173,6 +172,7 @@ @import "components/umb-textstring.less"; @import "components/umb-textarea.less"; @import "components/umb-dropdown.less"; +@import "components/umb-filter.less"; @import "components/umb-range-slider.less"; @import "components/umb-number.less"; @import "components/umb-tags-editor.less"; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/application/umb-language-picker.less b/src/Umbraco.Web.UI.Client/src/less/components/application/umb-language-picker.less index 4e3741905f..34a339b4c9 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/application/umb-language-picker.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/application/umb-language-picker.less @@ -24,6 +24,7 @@ .umb-language-picker__expand { font-size: 14px; + pointer-events: none; } .umb-language-picker__toggle:hover { diff --git a/src/Umbraco.Web.UI.Client/src/less/components/prevalues/multivalues.less b/src/Umbraco.Web.UI.Client/src/less/components/prevalues/multivalues.less index 7036d60a63..6cdc5b1c99 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/prevalues/multivalues.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/prevalues/multivalues.less @@ -6,7 +6,7 @@ width: 500px; } - p{ + p { margin: 7px 0; } } @@ -23,21 +23,18 @@ align-items: center; } -.umb-prevalues-multivalues__add { +.umb-prevalues-multivalues__add { display: flex; -} -.umb-prevalues-multivalues__add input { - width: 320px; -} + input { + display: flex; + width: 320px; + } -.umb-prevalues-multivalues__add input { - display: flex; -} - -.umb-prevalues-multivalues__add button { - margin: 0 6px 0 0; - margin-left: auto; + button { + margin: 0 6px 0 0; + margin-left: auto; + } } .umb-prevalues-multivalues__listitem { @@ -45,20 +42,26 @@ padding: 6px; margin: 10px 0px !important; background: @gray-10; - cursor: move; -} -.umb-prevalues-multivalues__listitem i { - display: flex; - align-items: center; - margin-right: 5px -} + &.ui-sortable-handle, + .ui-sortable-handle, + .handle + { + cursor: move; + } -.umb-prevalues-multivalues__listitem a { - cursor: pointer; - margin-left: auto; -} + i { + display: flex; + align-items: center; + margin-right: 5px + } -.umb-prevalues-multivalues__listitem input { - width: 295px; + a { + cursor: pointer; + margin-left: auto; + } + + input { + width: 295px; + } } diff --git a/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-tree.less b/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-tree.less index 59d81eb3ea..d4599ea03d 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-tree.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-tree.less @@ -96,7 +96,6 @@ body.touch .umb-tree { width: auto; height: auto; margin: 0 5px 0 auto; - padding: 7px 5px; overflow: visible; clip: auto; } @@ -185,12 +184,15 @@ body.touch .umb-tree { display: flex; flex: 0 0 auto; justify-content: flex-end; - padding: 7px 5px; text-align: center; margin: 0 5px 0 auto; cursor: pointer; border-radius: @baseBorderRadius; + .umb-button-ellipsis { + padding: 3px 5px; + } + i { height: 5px !important; width: 5px !important; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-checkmark.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-checkmark.less index f82e47bf88..6ab6169eec 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-checkmark.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-checkmark.less @@ -17,10 +17,21 @@ } } +.umb-checkmark__action { + &:hover, + &:focus { + .umb-checkmark { + border-color:@ui-action-discreet-border-hover; + color: @ui-selected-type-hover; + } + } +} + .umb-checkmark--checked { background: @ui-selected-border; border-color: @ui-selected-border; color: @white; + &:hover { background: @ui-selected-border-hover; border-color: @ui-selected-border-hover; @@ -28,6 +39,17 @@ } } +.umb-checkmark__action { + &:hover, + &:focus { + .umb-checkmark--checked { + background: @ui-selected-border-hover; + border-color: @ui-selected-border-hover; + color: @white; + } + } +} + .umb-checkmark--xs { width: 20px; height: 20px; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-filter.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-filter.less new file mode 100644 index 0000000000..7432555472 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-filter.less @@ -0,0 +1,13 @@ +.umb-filter { + position: relative; +} + +.umb-filter .umb-filter__toggle { + display: flex; +} + +.umb-filter .umb-filter__label { + margin-left: 5px; + margin-right: 3px; + max-width: 150px; +} diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-grid.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-grid.less index a8c428ba7a..0915f5f64f 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-grid.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-grid.less @@ -596,9 +596,10 @@ } .umb-grid .iconBox i { - font-size: 16px !important; - color: @gray-3 ; + color: @gray-3; display: block; + font-size: 16px; + line-height: inherit; } .umb-grid .help-text { @@ -609,8 +610,6 @@ clear: both; } - - // TINYMCE EDITOR // ------------------------- diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-insert-code-box.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-insert-code-box.less index f3b53f4def..006dae09dc 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-insert-code-box.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-insert-code-box.less @@ -8,20 +8,20 @@ padding: 15px 20px; margin-bottom: 10px; border-radius: 3px; - cursor: pointer; + text-align: left; } .umb-insert-code-box:hover, .umb-insert-code-box.-selected { background-color: @ui-option-hover; color: @ui-action-type-hover; - //border-color: @ui-action-border-hover; } .umb-insert-code-box__title { font-size: 15px; margin-bottom: 5px; font-weight: bold; + line-height: 1; } .umb-insert-code-box__description { diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-logviewer.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-logviewer.less index 56fa905310..32dcccb8da 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-logviewer.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-logviewer.less @@ -122,6 +122,11 @@ .table { table-layout: fixed; + table { + display: table; + width: 100%; + } + thead th:first-child, thead th:nth-child(3) { width: 20%; } @@ -130,9 +135,16 @@ width: 15%; } + tr td:nth-child(3) { word-break: break-word; } + + button { + white-space: normal; + word-break: break-word; + text-align-last: left; + } } .exception { diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-media-grid.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-media-grid.less index abd1bfd047..09fea977c7 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-media-grid.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-media-grid.less @@ -138,6 +138,7 @@ .umb-media-grid__item-overlay { display: flex; + align-items: center; width: 100%; opacity: 0; position: absolute; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-nested-content.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-nested-content.less index 384d9b4ac1..7962981592 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-nested-content.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-nested-content.less @@ -84,10 +84,8 @@ .umb-nested-content__heading { line-height: 20px; position: relative; - margin-top:1px; padding: 15px 5px; color:@ui-option-type; - border-radius: 3px 3px 0 0; &:hover { color:@ui-option-type-hover; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-property-actions.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-property-actions.less index f86b27af17..6b7d22d0b7 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-property-actions.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-property-actions.less @@ -1,9 +1,4 @@ -.umb-property-actions { - display: inline; -} - -.umb-property-actions__toggle, -.umb-property-actions__menu-open-toggle { +.umb-property-actions__toggle { position: relative; display: flex; flex: 0 0 auto; @@ -11,7 +6,6 @@ text-align: center; cursor: pointer; border-radius: 3px; - background-color: @ui-action-hover; i { @@ -32,27 +26,20 @@ } } } -.umb-property-actions__menu-open-toggle { - position: absolute; - z-index:1; - outline: none;// this is not acceccible by keyboard, since we use the .umb-property-actions__toggle for that. - top: -15px; - border-radius: 3px 3px 0 0; - - border-top-left-radius: 3px; - border-top-right-radius: 3px; - - border: 1px solid @dropdownBorder; - - border-bottom: 1px solid @gray-9; - - .box-shadow(0 5px 20px rgba(0,0,0,.3)); - - background-color: @white; +.umb-property-actions { + display: inline; + &.-open { + .umb-property-actions__toggle { + background-color: @white; + border-radius: 3px 3px 0 0; + border: 1px solid @dropdownBorder; + border-bottom: 1px solid @gray-9; + .box-shadow(0 5px 20px rgba(0,0,0,.3)); + } + } } - .umb-property .umb-property-actions { float: left; } @@ -66,32 +53,22 @@ .umb-property .umb-property-actions__toggle:focus { opacity: 1; } + // Revert-style-hack that ensures that we only show property-actions on properties that are directly begin hovered. .umb-property:hover .umb-property:not(:hover) .umb-property-actions__toggle { opacity: 0; } .umb-property-actions__menu { - position: absolute; z-index: 1000; - display: block; - float: left; min-width: 160px; list-style: none; .umb-contextmenu { - border-top-left-radius: 0; - margin-top:-2px; - - } - - .umb-contextmenu-item > button { - - z-index:2;// need to stay on top of menu-toggle-open shadow. - + margin-top: 0; } } diff --git a/src/Umbraco.Web.UI.Client/src/less/components/users/umb-user-picker-list.less b/src/Umbraco.Web.UI.Client/src/less/components/users/umb-user-picker-list.less index 2e0d79e803..4a343f780a 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/users/umb-user-picker-list.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/users/umb-user-picker-list.less @@ -8,6 +8,8 @@ margin-bottom: 5px; padding: 10px; align-items: center; + width: 100%; + text-align: left; } .umb-user-picker-list-item:active, @@ -39,4 +41,4 @@ .umb-user-picker-list-item__name { font-size: 15px; font-weight: bold; -} \ No newline at end of file +} diff --git a/src/Umbraco.Web.UI.Client/src/less/filter-toggle.less b/src/Umbraco.Web.UI.Client/src/less/filter-toggle.less deleted file mode 100644 index 82f9f3f553..0000000000 --- a/src/Umbraco.Web.UI.Client/src/less/filter-toggle.less +++ /dev/null @@ -1,20 +0,0 @@ -.filter-toggle{ - margin: 0; - padding: 0 8px 0 0; - position: relative; -} - -.filter-toggle__level{ - display: inline-block; - font-weight: 700; - margin: 0 5px; - max-width: 150px; -} - -.filter-toggle__icon{ - position: absolute; - top: 0; - bottom: 0; - right: 0; - margin: auto 0; -} diff --git a/src/Umbraco.Web.UI.Client/src/less/forms.less b/src/Umbraco.Web.UI.Client/src/less/forms.less index ed5a2c0622..76c5af8819 100644 --- a/src/Umbraco.Web.UI.Client/src/less/forms.less +++ b/src/Umbraco.Web.UI.Client/src/less/forms.less @@ -118,6 +118,10 @@ label.control-label, .control-label { width: 100%; } +.macro-select .form-search { + margin: 0 0 10px; +} + // GENERAL STYLES // -------------- diff --git a/src/Umbraco.Web.UI.Client/src/less/gridview.less b/src/Umbraco.Web.UI.Client/src/less/gridview.less index 11ba7b2795..80f13fbf1f 100644 --- a/src/Umbraco.Web.UI.Client/src/less/gridview.less +++ b/src/Umbraco.Web.UI.Client/src/less/gridview.less @@ -21,7 +21,7 @@ overflow: hidden; padding: 5px; border-radius:5px; - box-shadow: 3px 3px 12px 0px rgba(50, 50, 50, 0.45); + box-shadow: 3px 3px 12px 0 rgba(50, 50, 50, 0.45); } .usky-grid .ui-sortable-helper *{ @@ -150,7 +150,7 @@ .usky-grid .cell-tools-add { position: absolute; text-align: center; - bottom: 0px; + bottom: 0; left: 0; right: 0; margin: 0 45px 1px 0; @@ -160,14 +160,14 @@ } } -.usky-grid .usky-control:hover .cell-tools-add{ +.usky-grid .usky-control:hover .cell-tools-add{ opacity: 1; } -.usky-grid .cell-tools-remove { +.usky-grid .cell-tools-remove { display:inline-block; position: absolute; - top: 0px; + top: 0; right: 5px; text-align: right; z-index: 500; @@ -221,7 +221,7 @@ top: -22px; left: -1px; text-decoration: none; - padding: 0px 7px; + padding: 0 7px; display:none; font-size:0.8em; background-color: @white; @@ -234,7 +234,7 @@ .usky-grid .usky-row-inner > ins.item-label{ top: -20px; - left: 0px; + left: 0; } .usky-grid .usky-control-inner.selectedControl , .usky-grid .usky-row-inner.selectedRow{ @@ -408,7 +408,7 @@ .usky-grid ul li { display: inline-block; width: 120px; - margin: 8px 8px 0px 8px; + margin: 8px 8px 0 8px; } @@ -435,7 +435,7 @@ padding: -1px; position: absolute; margin: -1px -1px 0 -1px; - box-shadow: 2px 2px 10px 0px rgba(50, 50, 50, 0.14); + box-shadow: 2px 2px 10px 0 rgba(50, 50, 50, 0.14); z-index: 9999999; } @@ -558,7 +558,8 @@ margin: 20px; } - .usky-grid .uSky-templates-template a.tb:hover { + .usky-grid .uSky-templates-template button.tb:hover, + .usky-grid .uSky-templates-template button.tb:focus { border:5px solid @blueMid; } @@ -587,7 +588,9 @@ border-right: 1px dashed @gray-8 !important; } - .usky-grid a.uSky-templates-column:hover, .usky-grid a.uSky-templates-column.selected{ + .usky-grid button.uSky-templates-column:hover, + .usky-grid button.uSky-templates-column:focus, + .usky-grid button.uSky-templates-column.selected{ background-color: @blueMid; } @@ -628,13 +631,13 @@ transition: border 200ms linear; &.prevalues-rows { - margin: 0px 20px 20px 0px; + margin: 0 20px 20px 0; width: 80px; float:left; } &.prevalues-templates { - margin: 0px 20px 20px 0px; + margin: 0 20px 20px 0; float:left; } @@ -803,7 +806,7 @@ .usky-grid-configuration .uSky-templates .uSky-templates-template .tb{ max-height: 50px; border-width: 2px !important; - padding: 0px; + padding: 0; border-spacing:2px; overflow: hidden; } @@ -844,13 +847,13 @@ } .usky-grid-configuration .uSky-templates-rows .uSky-templates-row{ - margin: 0px 50px 20px 0px; + margin: 0 50px 20px 0; width: 60px; } .usky-grid-configuration .uSky-templates-rows .uSky-templates-row .tb{ border-width: 2px !important; - padding: 0px; + padding: 0; border-spacing:2px; } @@ -858,4 +861,6 @@ height: 10px !important; } -.usky-grid-configuration a.uSky-templates-column{height: 70px !important;} +.usky-grid-configuration button.uSky-templates-column { + height: 70px !important; +} diff --git a/src/Umbraco.Web.UI.Client/src/less/listview.less b/src/Umbraco.Web.UI.Client/src/less/listview.less index 9d0ed002bb..582da12804 100644 --- a/src/Umbraco.Web.UI.Client/src/less/listview.less +++ b/src/Umbraco.Web.UI.Client/src/less/listview.less @@ -259,20 +259,21 @@ margin-right: 15px; } +.list-view-layout__icon-wrapper { + margin-right: 10px; +} + .list-view-layout__icon { font-size: 18px; - margin-right: 10px; vertical-align: middle; border: 1px solid @gray-8; background: @white; - padding: 6px 8px; - display: block; -} - -.list-view-layout__icon:hover, -.list-view-layout__icon:focus, -.list-view-layout__icon:active { - text-decoration: none; + padding: 0; + display: flex; + align-items: center; + justify-content: center; + width: 30px; + height: 30px; } .list-view-layout__remove { @@ -281,20 +282,9 @@ } .list-view-add-layout { - width:100%; - background:0 0; - margin-top: 10px; - color: @ui-action-discreet-type; - border: 1px dashed @ui-action-discreet-border; - display: flex; - align-items: center; - justify-content: center; - padding: 5px 0; - box-sizing: border-box; + &:extend(.umb-node-preview-add); } .list-view-add-layout:hover { - text-decoration: none; - color: @ui-action-discreet-type-hover; - border-color: @ui-action-discreet-border-hover; + &:extend(.umb-node-preview-add:hover); } diff --git a/src/Umbraco.Web.UI.Client/src/less/panel.less b/src/Umbraco.Web.UI.Client/src/less/panel.less index 115bdaed70..81326826f6 100644 --- a/src/Umbraco.Web.UI.Client/src/less/panel.less +++ b/src/Umbraco.Web.UI.Client/src/less/panel.less @@ -413,6 +413,8 @@ input.umb-panel-header-name-input.name-is-empty { .umb-panel-header-name { font-size: 16px; font-weight: bold; + margin: 0; + line-height: 1.2; } diff --git a/src/Umbraco.Web.UI.Client/src/less/property-editors.less b/src/Umbraco.Web.UI.Client/src/less/property-editors.less index 664be1dafc..3912d11161 100644 --- a/src/Umbraco.Web.UI.Client/src/less/property-editors.less +++ b/src/Umbraco.Web.UI.Client/src/less/property-editors.less @@ -163,6 +163,16 @@ .sp-replacer { display: inline-flex; margin-right: 18px; + height: auto; + + .sp-preview { + margin: 5px; + height: auto; + } + + .sp-dd { + line-height: 2rem; + } } label { diff --git a/src/Umbraco.Web.UI.Client/src/less/utilities/_spacing.less b/src/Umbraco.Web.UI.Client/src/less/utilities/_spacing.less index 787e50f204..d2968696dc 100644 --- a/src/Umbraco.Web.UI.Client/src/less/utilities/_spacing.less +++ b/src/Umbraco.Web.UI.Client/src/less/utilities/_spacing.less @@ -35,10 +35,13 @@ 7 = 7th step in spacing scale */ -.m-center { - margin-left: auto; +.m-center, +.mx-auto { + margin-left: auto; margin-right: auto; } +.ml-auto { margin-left: auto; } +.mr-auto { margin-right: auto; } .mt0 { margin-top: @spacing-none; } .mt1 { margin-top: @spacing-extra-small; } diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockeditor/blockeditor.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockeditor/blockeditor.controller.js index a08a05b0f7..88cda027a8 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockeditor/blockeditor.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockeditor/blockeditor.controller.js @@ -7,8 +7,8 @@ angular.module("umbraco") vm.tabs = []; localizationService.localizeMany([ - vm.model.liveEditing ? "prompt_discardChanges" : "general_close", - vm.model.liveEditing ? "buttons_confirmActionConfirm" : "buttons_submitChanges" + vm.model.createFlow ? "general_cancel" : (vm.model.liveEditing ? "prompt_discardChanges" : "general_close"), + vm.model.createFlow ? "general_create" : (vm.model.liveEditing ? "buttons_confirmActionConfirm" : "buttons_submitChanges") ]).then(function (data) { vm.closeLabel = data[0]; vm.submitLabel = data[1]; @@ -68,16 +68,16 @@ angular.module("umbraco") // * It would have a 'commit' method to commit the removed errors - which we would call in the formHelper.submitForm when it's successful // * It would have a 'rollback' method to reset the removed errors - which we would call here - - if (vm.blockForm.$dirty === true) { - localizationService.localizeMany(["prompt_discardChanges", "blockEditor_blockHasChanges"]).then(function (localizations) { + if (vm.model.createFlow === true || vm.blockForm.$dirty === true) { + var labels = vm.model.createFlow === true ? ["blockEditor_confirmCancelBlockCreationHeadline", "blockEditor_confirmCancelBlockCreationMessage"] : ["prompt_discardChanges", "blockEditor_blockHasChanges"]; + localizationService.localizeMany(labels).then(function (localizations) { const confirm = { title: localizations[0], view: "default", content: localizations[1], submitButtonLabelKey: "general_discard", submitButtonStyle: "danger", - closeButtonLabelKey: "general_cancel", + closeButtonLabelKey: "prompt_stay", submit: function () { overlayService.close(); vm.model.close(vm.model); @@ -88,11 +88,10 @@ angular.module("umbraco") }; overlayService.open(confirm); }); - - return; + } else { + vm.model.close(vm.model); } - // TODO: check if content/settings has changed and ask user if they are sure. - vm.model.close(vm.model); + } } diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockeditor/blockeditor.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockeditor/blockeditor.html index 285e554c68..2367771804 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 @@ -37,6 +37,7 @@ diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockpicker/blockpicker.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockpicker/blockpicker.html index fb7e946ee7..6dea4debb6 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockpicker/blockpicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockpicker/blockpicker.html @@ -69,6 +69,7 @@ diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/embed/embed.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/embed/embed.controller.js index 515f54e3d7..c3d1312109 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/embed/embed.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/embed/embed.controller.js @@ -22,7 +22,7 @@ }; if ($scope.model.modify) { - angular.extend($scope.model.embed, $scope.model.modify); + Utilities.extend($scope.model.embed, $scope.model.modify); showPreview(); } @@ -34,7 +34,7 @@ vm.close = close; function onInit() { - if(!$scope.model.title) { + if (!$scope.model.title) { localizationService.localize("general_embed").then(function(value){ $scope.model.title = value; }); @@ -122,7 +122,6 @@ if ($scope.model.embed.url !== "") { showPreview(); } - } function toggleConstrain() { @@ -130,19 +129,18 @@ } function submit() { - if($scope.model && $scope.model.submit) { + if ($scope.model && $scope.model.submit) { $scope.model.submit($scope.model); } } function close() { - if($scope.model && $scope.model.close) { + if ($scope.model && $scope.model.close) { $scope.model.close(); } } onInit(); - } angular.module("umbraco").controller("Umbraco.Editors.EmbedController", EmbedController); diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/embed/embed.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/embed/embed.html index 5862ca7059..19cf9b2278 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/embed/embed.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/embed/embed.html @@ -16,7 +16,7 @@ - + - +
-
-
...
-
-
-
-
...
-
- -
-
-
-
...
-
- -
-
-
-
...
-
-
+ + + +
@@ -54,5 +54,5 @@ - + diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/itempicker/itempicker.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/itempicker/itempicker.html index 5b121c172f..c563394ab3 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/itempicker/itempicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/itempicker/itempicker.html @@ -25,14 +25,13 @@
diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/macroparameterpicker/macroparameterpicker.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/macroparameterpicker/macroparameterpicker.html index e92ce65bde..151a8167e8 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/macroparameterpicker/macroparameterpicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/macroparameterpicker/macroparameterpicker.html @@ -34,15 +34,18 @@
{{key}}
@@ -55,15 +58,18 @@
{{result.group}}
- + Sorry, we can not find what you are looking for. diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/macropicker/macropicker.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/macropicker/macropicker.html index fc1bec4ec1..c23aaa7cb9 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/macropicker/macropicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/macropicker/macropicker.html @@ -15,7 +15,7 @@
-
+