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/UmbracoCms.Web.nuspec b/build/NuSpecs/UmbracoCms.Web.nuspec index 1aeead52a5..765c45e860 100644 --- a/build/NuSpecs/UmbracoCms.Web.nuspec +++ b/build/NuSpecs/UmbracoCms.Web.nuspec @@ -43,18 +43,18 @@ - + - + - + 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.Configuration/Models/ModelsBuilderConfig.cs b/src/Umbraco.Configuration/Models/ModelsBuilderConfig.cs index d111dbba70..4d35e3fa95 100644 --- a/src/Umbraco.Configuration/Models/ModelsBuilderConfig.cs +++ b/src/Umbraco.Configuration/Models/ModelsBuilderConfig.cs @@ -20,7 +20,7 @@ namespace Umbraco.Configuration.Models _configuration = configuration; } - public string DefaultModelsDirectory => "~/App_Data/Models"; + public string DefaultModelsDirectory => "~/Umbraco/Models"; /// /// Gets a value indicating whether the whole models experience is enabled. @@ -65,7 +65,7 @@ namespace Umbraco.Configuration.Models /// /// Default is ~/App_Data/Models but that can be changed. public string ModelsDirectory => - _configuration.GetValue(Prefix+"ModelsDirectory", "~/App_Data/Models"); + _configuration.GetValue(Prefix+"ModelsDirectory", "~/Umbraco/Models"); /// /// Gets a value indicating whether to accept an unsafe value for ModelsDirectory. diff --git a/src/Umbraco.Core/Composing/DefaultUmbracoAssemblyProvider.cs b/src/Umbraco.Core/Composing/DefaultUmbracoAssemblyProvider.cs index c5d87abf91..3e0ea9c971 100644 --- a/src/Umbraco.Core/Composing/DefaultUmbracoAssemblyProvider.cs +++ b/src/Umbraco.Core/Composing/DefaultUmbracoAssemblyProvider.cs @@ -19,7 +19,7 @@ namespace Umbraco.Core.Composing "Umbraco.Core", "Umbraco.Infrastructure", "Umbraco.PublishedCache.NuCache", - // "Umbraco.ModelsBuilder.Embedded", TODO reintroduce when ModelsBuilder is migrated + "Umbraco.ModelsBuilder.Embedded", "Umbraco.Examine.Lucene", "Umbraco.Web.Common", "Umbraco.Web.BackOffice", diff --git a/src/Umbraco.Core/Configuration/IConfigManipulator.cs b/src/Umbraco.Core/Configuration/IConfigManipulator.cs index 1d9230be44..bc89b7bd1d 100644 --- a/src/Umbraco.Core/Configuration/IConfigManipulator.cs +++ b/src/Umbraco.Core/Configuration/IConfigManipulator.cs @@ -7,5 +7,6 @@ namespace Umbraco.Core.Configuration void RemoveConnectionString(); void SaveConnectionString(string connectionString, string providerName); void SaveConfigValue(string itemPath, object value); + void SaveDisableRedirectUrlTracking(bool disable); } } diff --git a/src/Umbraco.Core/Configuration/ModelsBuilderConfigExtensions.cs b/src/Umbraco.Core/Configuration/ModelsBuilderConfigExtensions.cs index 4117d904b9..edf068e16f 100644 --- a/src/Umbraco.Core/Configuration/ModelsBuilderConfigExtensions.cs +++ b/src/Umbraco.Core/Configuration/ModelsBuilderConfigExtensions.cs @@ -1,5 +1,6 @@ using System.Configuration; using System.IO; +using Umbraco.Core.Hosting; using Umbraco.Core.Configuration.Models; using Umbraco.Core.IO; @@ -9,12 +10,12 @@ namespace Umbraco.Core.Configuration { private static string _modelsDirectoryAbsolute = null; - public static string ModelsDirectoryAbsolute(this ModelsBuilderConfig modelsBuilderConfig, IIOHelper ioHelper) + public static string ModelsDirectoryAbsolute(this ModelsBuilderConfig modelsBuilderConfig, IHostingEnvironment hostingEnvironment) { if (_modelsDirectoryAbsolute is null) { var modelsDirectory = modelsBuilderConfig.ModelsDirectory; - var root = ioHelper.MapPath("~/"); + var root = hostingEnvironment.MapPathContentRoot("~/"); _modelsDirectoryAbsolute = GetModelsDirectory(root, modelsDirectory, modelsBuilderConfig.AcceptUnsafeModelsDirectory); diff --git a/src/Umbraco.Core/Configuration/XmlConfigManipulator.cs b/src/Umbraco.Core/Configuration/XmlConfigManipulator.cs index 80d795bb48..dd7e4baa94 100644 --- a/src/Umbraco.Core/Configuration/XmlConfigManipulator.cs +++ b/src/Umbraco.Core/Configuration/XmlConfigManipulator.cs @@ -2,8 +2,8 @@ using System.Configuration; using System.IO; using System.Linq; +using System.Xml; using System.Xml.Linq; -using Umbraco.Composing; using Umbraco.Core.IO; using Umbraco.Core.Logging; @@ -111,6 +111,26 @@ namespace Umbraco.Core.Configuration throw new NotImplementedException(); } + public void SaveDisableRedirectUrlTracking(bool disable) + { + var fileName = _ioHelper.MapPath("~/config/umbracoSettings.config"); + + var umbracoConfig = new XmlDocument { PreserveWhitespace = true }; + umbracoConfig.Load(fileName); + + var action = disable ? "disable" : "enable"; + + if (File.Exists(fileName) == false) + throw new Exception($"Couldn't {action} URL Tracker, the umbracoSettings.config file does not exist."); + + if (!(umbracoConfig.SelectSingleNode("//web.routing") is XmlElement webRoutingElement)) + throw new Exception($"Couldn't {action} URL Tracker, the web.routing element was not found in umbracoSettings.config."); + + // note: this adds the attribute if it does not exist + webRoutingElement.SetAttribute("disableRedirectUrlTracking", disable.ToString().ToLowerInvariant()); + umbracoConfig.Save(fileName); + } + private static void AddOrUpdateAttribute(XElement element, string name, string value) { var attribute = element.Attribute(name); 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/IO/ViewHelper.cs b/src/Umbraco.Core/IO/ViewHelper.cs index 77b2d6cc99..569d8cdfc9 100644 --- a/src/Umbraco.Core/IO/ViewHelper.cs +++ b/src/Umbraco.Core/IO/ViewHelper.cs @@ -69,6 +69,7 @@ namespace Umbraco.Core.IO // either // @inherits Umbraco.Web.Mvc.UmbracoViewPage // @inherits Umbraco.Web.Mvc.UmbracoViewPage + content.AppendLine("@using Umbraco.Web.PublishedModels;"); content.Append("@inherits Umbraco.Web.Common.AspNetCore.UmbracoViewPage"); if (modelClassName.IsNullOrWhiteSpace() == false) { 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/Configuration/JsonConfigManipulator.cs b/src/Umbraco.Infrastructure/Configuration/JsonConfigManipulator.cs index 17b0fc3505..a3a43d2b93 100644 --- a/src/Umbraco.Infrastructure/Configuration/JsonConfigManipulator.cs +++ b/src/Umbraco.Infrastructure/Configuration/JsonConfigManipulator.cs @@ -66,6 +66,40 @@ namespace Umbraco.Core.Configuration } + public void SaveDisableRedirectUrlTracking(bool disable) + { + var provider = GetJsonConfigurationProvider(); + + var json = GetJson(provider); + + var item = GetDisableRedirectUrlItem(disable.ToString().ToLowerInvariant()); + + json.Merge(item, new JsonMergeSettings()); + + SaveJson(provider, json); + } + + private JToken GetDisableRedirectUrlItem(string value) + { + JTokenWriter writer = new JTokenWriter(); + + writer.WriteStartObject(); + writer.WritePropertyName("Umbraco"); + writer.WriteStartObject(); + writer.WritePropertyName("CMS"); + writer.WriteStartObject(); + writer.WritePropertyName("WebRouting"); + writer.WriteStartObject(); + writer.WritePropertyName("DisableRedirectUrlTracking"); + writer.WriteValue(value); + writer.WriteEndObject(); + writer.WriteEndObject(); + writer.WriteEndObject(); + writer.WriteEndObject(); + + return writer.Token; + } + private JToken GetConnectionItem(string connectionString, string providerName) { JTokenWriter writer = new JTokenWriter(); @@ -136,7 +170,7 @@ namespace Umbraco.Core.Configuration { foreach (var provider in configurationRoot.Providers) { - if(provider is JsonConfigurationProvider jsonConfigurationProvider) + if (provider is JsonConfigurationProvider jsonConfigurationProvider) { if (requiredKey is null || provider.TryGet(requiredKey, out _)) { 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/Models/PublishedContent/PublishedModelFactory.cs b/src/Umbraco.Infrastructure/Models/PublishedContent/PublishedModelFactory.cs index 8be56850d1..e065ac17b7 100644 --- a/src/Umbraco.Infrastructure/Models/PublishedContent/PublishedModelFactory.cs +++ b/src/Umbraco.Infrastructure/Models/PublishedContent/PublishedModelFactory.cs @@ -12,11 +12,12 @@ namespace Umbraco.Core.Models.PublishedContent { private readonly Dictionary _modelInfos; private readonly Dictionary _modelTypeMap; + private readonly IPublishedValueFallback _publishedValueFallback; private class ModelInfo { public Type ParameterType { get; set; } - public Func Ctor { get; set; } + public Func Ctor { get; set; } public Type ModelType { get; set; } public Func ListCtor { get; set; } } @@ -35,7 +36,8 @@ namespace Umbraco.Core.Models.PublishedContent /// PublishedContentModelFactoryResolver.Current.SetFactory(factory); /// /// - public PublishedModelFactory(IEnumerable types) + public PublishedModelFactory(IEnumerable types, + IPublishedValueFallback publishedValueFallback) { var modelInfos = new Dictionary(StringComparer.InvariantCultureIgnoreCase); var modelTypeMap = new Dictionary(StringComparer.InvariantCultureIgnoreCase); @@ -52,7 +54,7 @@ namespace Umbraco.Core.Models.PublishedContent foreach (var ctor in type.GetConstructors()) { var parms = ctor.GetParameters(); - if (parms.Length == 1 && typeof(IPublishedElement).IsAssignableFrom(parms[0].ParameterType)) + if (parms.Length == 2 && typeof(IPublishedElement).IsAssignableFrom(parms[0].ParameterType) && typeof(IPublishedValueFallback).IsAssignableFrom(parms[1].ParameterType)) { if (constructor != null) throw new InvalidOperationException($"Type {type.FullName} has more than one public constructor with one argument of type, or implementing, IPublishedElement."); @@ -71,13 +73,14 @@ namespace Umbraco.Core.Models.PublishedContent throw new InvalidOperationException($"Both types '{type.AssemblyQualifiedName}' and '{modelInfo.ModelType.AssemblyQualifiedName}' want to be a model type for content type with alias \"{typeName}\"."); // have to use an unsafe ctor because we don't know the types, really - var modelCtor = ReflectionUtilities.EmitConstructorUnsafe>(constructor); + var modelCtor = ReflectionUtilities.EmitConstructorUnsafe>(constructor); modelInfos[typeName] = new ModelInfo { ParameterType = parameterType, ModelType = type, Ctor = modelCtor }; modelTypeMap[typeName] = type; } _modelInfos = modelInfos.Count > 0 ? modelInfos : null; _modelTypeMap = modelTypeMap; + _publishedValueFallback = publishedValueFallback; } /// @@ -95,7 +98,7 @@ namespace Umbraco.Core.Models.PublishedContent throw new InvalidOperationException($"Model {modelInfo.ModelType} expects argument of type {modelInfo.ParameterType.FullName}, but got {element.GetType().FullName}."); // can cast, because we checked when creating the ctor - return (IPublishedElement) modelInfo.Ctor(element); + return (IPublishedElement) modelInfo.Ctor(element, _publishedValueFallback); } /// 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.Infrastructure/Routing/RedirectTrackingComponent.cs b/src/Umbraco.Infrastructure/Routing/RedirectTrackingComponent.cs index 0273b3d14a..107e224486 100644 --- a/src/Umbraco.Infrastructure/Routing/RedirectTrackingComponent.cs +++ b/src/Umbraco.Infrastructure/Routing/RedirectTrackingComponent.cs @@ -41,9 +41,6 @@ namespace Umbraco.Web.Routing public void Initialize() { - // don't let the event handlers kick in if Redirect Tracking is turned off in the config - if (_webRoutingSettings.DisableRedirectUrlTracking) return; - ContentService.Publishing += ContentService_Publishing; ContentService.Published += ContentService_Published; ContentService.Moving += ContentService_Moving; @@ -67,6 +64,9 @@ namespace Umbraco.Web.Routing private void ContentService_Publishing(IContentService sender, PublishEventArgs args) { + // don't let the event handlers kick in if Redirect Tracking is turned off in the config + if (_webRoutingSettings.DisableRedirectUrlTracking) return; + var oldRoutes = GetOldRoutes(args.EventState); foreach (var entity in args.PublishedEntities) { @@ -76,12 +76,18 @@ namespace Umbraco.Web.Routing private void ContentService_Published(IContentService sender, ContentPublishedEventArgs args) { + // don't let the event handlers kick in if Redirect Tracking is turned off in the config + if (_webRoutingSettings.DisableRedirectUrlTracking) return; + var oldRoutes = GetOldRoutes(args.EventState); CreateRedirects(oldRoutes); } private void ContentService_Moving(IContentService sender, MoveEventArgs args) { + // don't let the event handlers kick in if Redirect Tracking is turned off in the config + if (_webRoutingSettings.DisableRedirectUrlTracking) return; + var oldRoutes = GetOldRoutes(args.EventState); foreach (var info in args.MoveInfoCollection) { @@ -91,6 +97,9 @@ namespace Umbraco.Web.Routing private void ContentService_Moved(IContentService sender, MoveEventArgs args) { + // don't let the event handlers kick in if Redirect Tracking is turned off in the config + if (_webRoutingSettings.DisableRedirectUrlTracking) return; + var oldRoutes = GetOldRoutes(args.EventState); CreateRedirects(oldRoutes); } diff --git a/src/Umbraco.ModelsBuilder.Embedded/BackOffice/ModelsBuilderDashboardController.cs b/src/Umbraco.ModelsBuilder.Embedded/BackOffice/ModelsBuilderDashboardController.cs index 6179e7c756..1339c79052 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/BackOffice/ModelsBuilderDashboardController.cs +++ b/src/Umbraco.ModelsBuilder.Embedded/BackOffice/ModelsBuilderDashboardController.cs @@ -1,16 +1,14 @@ using System; -using System.Net; -using System.Net.Http; using System.Runtime.Serialization; -using System.Web.Hosting; +using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; using Umbraco.Configuration; -using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.Models; using Umbraco.Core.Exceptions; +using Umbraco.Core.Hosting; using Umbraco.ModelsBuilder.Embedded.Building; -using Umbraco.Web.Editors; -using Umbraco.Web.WebApi.Filters; +using Umbraco.Web.BackOffice.Controllers; +using Umbraco.Web.BackOffice.Filters; namespace Umbraco.ModelsBuilder.Embedded.BackOffice { @@ -30,8 +28,9 @@ namespace Umbraco.ModelsBuilder.Embedded.BackOffice private readonly OutOfDateModelsStatus _outOfDateModels; private readonly ModelsGenerationError _mbErrors; private readonly DashboardReport _dashboardReport; + private readonly IHostingEnvironment _hostingEnvironment; - public ModelsBuilderDashboardController(IOptions config, ModelsGenerator modelsGenerator, OutOfDateModelsStatus outOfDateModels, ModelsGenerationError mbErrors) + public ModelsBuilderDashboardController(IOptions config, ModelsGenerator modelsGenerator, OutOfDateModelsStatus outOfDateModels, ModelsGenerationError mbErrors, IHostingEnvironment hostingEnvironment) { //_umbracoServices = umbracoServices; _config = config.Value; @@ -39,13 +38,14 @@ namespace Umbraco.ModelsBuilder.Embedded.BackOffice _outOfDateModels = outOfDateModels; _mbErrors = mbErrors; _dashboardReport = new DashboardReport(config, outOfDateModels, mbErrors); + _hostingEnvironment = hostingEnvironment; } // invoked by the dashboard // requires that the user is logged into the backoffice and has access to the settings section // beware! the name of the method appears in modelsbuilder.controller.js - [System.Web.Http.HttpPost] // use the http one, not mvc, with api controllers! - public HttpResponseMessage BuildModels() + [HttpPost] // use the http one, not mvc, with api controllers! + public IActionResult BuildModels() { try { @@ -54,10 +54,10 @@ namespace Umbraco.ModelsBuilder.Embedded.BackOffice if (!config.ModelsMode.SupportsExplicitGeneration()) { var result2 = new BuildResult { Success = false, Message = "Models generation is not enabled." }; - return Request.CreateResponse(HttpStatusCode.OK, result2, Configuration.Formatters.JsonFormatter); + return Ok(result2); } - var bin = HostingEnvironment.MapPath("~/bin"); + var bin = _hostingEnvironment.MapPathContentRoot("~/bin"); if (bin == null) throw new PanicException("bin is null."); @@ -70,13 +70,13 @@ namespace Umbraco.ModelsBuilder.Embedded.BackOffice _mbErrors.Report("Failed to build models.", e); } - return Request.CreateResponse(HttpStatusCode.OK, GetDashboardResult(), Configuration.Formatters.JsonFormatter); + return Ok(GetDashboardResult()); } // invoked by the back-office // requires that the user is logged into the backoffice and has access to the settings section - [System.Web.Http.HttpGet] // use the http one, not mvc, with api controllers! - public HttpResponseMessage GetModelsOutOfDateStatus() + [HttpGet] // use the http one, not mvc, with api controllers! + public ActionResult GetModelsOutOfDateStatus() { var status = _outOfDateModels.IsEnabled ? _outOfDateModels.IsOutOfDate @@ -84,16 +84,16 @@ namespace Umbraco.ModelsBuilder.Embedded.BackOffice : new OutOfDateStatus { Status = OutOfDateType.Current } : new OutOfDateStatus { Status = OutOfDateType.Unknown }; - return Request.CreateResponse(HttpStatusCode.OK, status, Configuration.Formatters.JsonFormatter); + return status; } // invoked by the back-office // requires that the user is logged into the backoffice and has access to the settings section // beware! the name of the method appears in modelsbuilder.controller.js - [System.Web.Http.HttpGet] // use the http one, not mvc, with api controllers! - public HttpResponseMessage GetDashboard() + [HttpGet] // use the http one, not mvc, with api controllers! + public ActionResult GetDashboard() { - return Request.CreateResponse(HttpStatusCode.OK, GetDashboardResult(), Configuration.Formatters.JsonFormatter); + return GetDashboardResult(); } private Dashboard GetDashboardResult() @@ -109,7 +109,7 @@ namespace Umbraco.ModelsBuilder.Embedded.BackOffice } [DataContract] - internal class BuildResult + public class BuildResult { [DataMember(Name = "success")] public bool Success; @@ -118,7 +118,7 @@ namespace Umbraco.ModelsBuilder.Embedded.BackOffice } [DataContract] - internal class Dashboard + public class Dashboard { [DataMember(Name = "enable")] public bool Enable; @@ -132,7 +132,7 @@ namespace Umbraco.ModelsBuilder.Embedded.BackOffice public string LastError; } - internal enum OutOfDateType + public enum OutOfDateType { OutOfDate, Current, @@ -140,7 +140,7 @@ namespace Umbraco.ModelsBuilder.Embedded.BackOffice } [DataContract] - internal class OutOfDateStatus + public class OutOfDateStatus { [DataMember(Name = "status")] public OutOfDateType Status { get; set; } diff --git a/src/Umbraco.ModelsBuilder.Embedded/Building/Builder.cs b/src/Umbraco.ModelsBuilder.Embedded/Building/Builder.cs index dc093d3c2d..4c90234fab 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/Building/Builder.cs +++ b/src/Umbraco.ModelsBuilder.Embedded/Building/Builder.cs @@ -17,11 +17,8 @@ namespace Umbraco.ModelsBuilder.Embedded.Building /// /// Provides a base class for all builders. /// - internal abstract class Builder + public abstract class Builder { - - - private readonly IList _typeModels; protected Dictionary ModelsMap { get; } = new Dictionary(); @@ -30,13 +27,11 @@ namespace Umbraco.ModelsBuilder.Embedded.Building protected readonly IList TypesUsing = new List { "System", - "System.Collections.Generic", "System.Linq.Expressions", - "System.Web", - "Umbraco.Core.Models", "Umbraco.Core.Models.PublishedContent", - "Umbraco.Web", - "Umbraco.ModelsBuilder.Embedded" + "Umbraco.Web.PublishedCache", + "Umbraco.ModelsBuilder.Embedded", + "Umbraco.Core" }; /// @@ -63,7 +58,7 @@ namespace Umbraco.ModelsBuilder.Embedded.Building /// Gets the list of all models. /// /// Includes those that are ignored. - internal IList TypeModels => _typeModels; + public IList TypeModels => _typeModels; /// /// Initializes a new instance of the class with a list of models to generate, @@ -200,7 +195,7 @@ namespace Umbraco.ModelsBuilder.Embedded.Building return true; } - internal string ModelsNamespaceForTests; + public string ModelsNamespaceForTests; public string GetModelsNamespace() { diff --git a/src/Umbraco.ModelsBuilder.Embedded/Building/ModelsGenerator.cs b/src/Umbraco.ModelsBuilder.Embedded/Building/ModelsGenerator.cs index c1e2f02031..0348c287cd 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/Building/ModelsGenerator.cs +++ b/src/Umbraco.ModelsBuilder.Embedded/Building/ModelsGenerator.cs @@ -4,6 +4,7 @@ using Microsoft.Extensions.Options; using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.Models; using Umbraco.Core.IO; +using Umbraco.Core.Hosting; namespace Umbraco.ModelsBuilder.Embedded.Building { @@ -12,19 +13,19 @@ namespace Umbraco.ModelsBuilder.Embedded.Building private readonly UmbracoServices _umbracoService; private readonly ModelsBuilderConfig _config; private readonly OutOfDateModelsStatus _outOfDateModels; - private readonly IIOHelper _ioHelper; + private readonly IHostingEnvironment _hostingEnvironment; - public ModelsGenerator(UmbracoServices umbracoService, IOptions config, OutOfDateModelsStatus outOfDateModels, IIOHelper ioHelper) + public ModelsGenerator(UmbracoServices umbracoService, IOptions config, OutOfDateModelsStatus outOfDateModels, IHostingEnvironment hostingEnvironment) { _umbracoService = umbracoService; _config = config.Value; _outOfDateModels = outOfDateModels; - _ioHelper = ioHelper; + _hostingEnvironment = hostingEnvironment; } internal void GenerateModels() { - var modelsDirectory = _config.ModelsDirectoryAbsolute(_ioHelper); + var modelsDirectory = _config.ModelsDirectoryAbsolute(_hostingEnvironment); if (!Directory.Exists(modelsDirectory)) Directory.CreateDirectory(modelsDirectory); diff --git a/src/Umbraco.ModelsBuilder.Embedded/Building/TextBuilder.cs b/src/Umbraco.ModelsBuilder.Embedded/Building/TextBuilder.cs index 9a2ad6f600..607aa129b1 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/Building/TextBuilder.cs +++ b/src/Umbraco.ModelsBuilder.Embedded/Building/TextBuilder.cs @@ -23,7 +23,7 @@ namespace Umbraco.ModelsBuilder.Embedded.Building { } // internal for unit tests only - internal TextBuilder() + public TextBuilder() { } /// @@ -185,16 +185,17 @@ namespace Umbraco.ModelsBuilder.Embedded.Building sb.AppendFormat("\t\tpublic new const PublishedItemType ModelItemType = PublishedItemType.{0};\n", itemType); WriteGeneratedCodeAttribute(sb, "\t\t"); - sb.Append("\t\tpublic new static IPublishedContentType GetModelContentType()\n"); - sb.Append("\t\t\t=> PublishedModelUtility.GetModelContentType(ModelItemType, ModelTypeAlias);\n"); + sb.Append("\t\tpublic new static IPublishedContentType GetModelContentType(IPublishedSnapshotAccessor publishedSnapshotAccessor)\n"); + sb.Append("\t\t\t=> PublishedModelUtility.GetModelContentType(publishedSnapshotAccessor, ModelItemType, ModelTypeAlias);\n"); WriteGeneratedCodeAttribute(sb, "\t\t"); - sb.AppendFormat("\t\tpublic static IPublishedPropertyType GetModelPropertyType(Expression> selector)\n", + sb.AppendFormat("\t\tpublic static IPublishedPropertyType GetModelPropertyType(IPublishedSnapshotAccessor publishedSnapshotAccessor, Expression> selector)\n", type.ClrName); - sb.Append("\t\t\t=> PublishedModelUtility.GetModelPropertyType(GetModelContentType(), selector);\n"); + sb.Append("\t\t\t=> PublishedModelUtility.GetModelPropertyType(GetModelContentType(publishedSnapshotAccessor), selector);\n"); sb.Append("#pragma warning restore 0109\n\n"); + sb.Append("\t\tprivate IPublishedValueFallback _publishedValueFallback;"); // write the ctor - sb.AppendFormat("\t\t// ctor\n\t\tpublic {0}(IPublished{1} content)\n\t\t\t: base(content)\n\t\t{{ }}\n\n", + sb.AppendFormat("\n\n\t\t// ctor\n\t\tpublic {0}(IPublished{1} content, IPublishedValueFallback publishedValueFallback)\n\t\t\t: base(content)\n\t\t{{\n\t\t\t_publishedValueFallback = publishedValueFallback; \n\t\t}}\n\n", type.ClrName, type.IsElement ? "Element" : "Content"); // write the properties @@ -325,7 +326,7 @@ namespace Umbraco.ModelsBuilder.Embedded.Building WriteClrType(sb, property.ClrTypeName); sb.Append(">"); } - sb.AppendFormat("(\"{0}\");\n", + sb.AppendFormat("(_publishedValueFallback, \"{0}\");\n", property.Alias); } @@ -417,7 +418,7 @@ namespace Umbraco.ModelsBuilder.Embedded.Building } // internal for unit tests - internal void WriteClrType(StringBuilder sb, Type type) + public void WriteClrType(StringBuilder sb, Type type) { var s = type.ToString(); diff --git a/src/Umbraco.ModelsBuilder.Embedded/Building/TypeModelHasher.cs b/src/Umbraco.ModelsBuilder.Embedded/Building/TypeModelHasher.cs index 2f14bec875..c5b053ca07 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/Building/TypeModelHasher.cs +++ b/src/Umbraco.ModelsBuilder.Embedded/Building/TypeModelHasher.cs @@ -1,5 +1,9 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; +using System.Security.Cryptography; +using System.Text; +using Umbraco.Core; namespace Umbraco.ModelsBuilder.Embedded.Building { @@ -7,38 +11,38 @@ namespace Umbraco.ModelsBuilder.Embedded.Building { public static string Hash(IEnumerable typeModels) { - var hash = new HashCombiner(); + var builder = new StringBuilder(); // see Umbraco.ModelsBuilder.Umbraco.Application for what's important to hash // ie what comes from Umbraco (not computed by ModelsBuilder) and makes a difference foreach (var typeModel in typeModels.OrderBy(x => x.Alias)) { - hash.Add("--- CONTENT TYPE MODEL ---"); - hash.Add(typeModel.Id); - hash.Add(typeModel.Alias); - hash.Add(typeModel.ClrName); - hash.Add(typeModel.ParentId); - hash.Add(typeModel.Name); - hash.Add(typeModel.Description); - hash.Add(typeModel.ItemType.ToString()); - hash.Add("MIXINS:" + string.Join(",", typeModel.MixinTypes.OrderBy(x => x.Id).Select(x => x.Id))); + builder.AppendLine("--- CONTENT TYPE MODEL ---"); + builder.AppendLine(typeModel.Id.ToString()); + builder.AppendLine(typeModel.Alias); + builder.AppendLine(typeModel.ClrName); + builder.AppendLine(typeModel.ParentId.ToString()); + builder.AppendLine(typeModel.Name); + builder.AppendLine(typeModel.Description); + builder.AppendLine(typeModel.ItemType.ToString()); + builder.AppendLine("MIXINS:" + string.Join(",", typeModel.MixinTypes.OrderBy(x => x.Id).Select(x => x.Id))); foreach (var prop in typeModel.Properties.OrderBy(x => x.Alias)) { - hash.Add("--- PROPERTY ---"); - hash.Add(prop.Alias); - hash.Add(prop.ClrName); - hash.Add(prop.Name); - hash.Add(prop.Description); - hash.Add(prop.ModelClrType.ToString()); // see ModelType tests, want ToString() not FullName + builder.AppendLine("--- PROPERTY ---"); + builder.AppendLine(prop.Alias); + builder.AppendLine(prop.ClrName); + builder.AppendLine(prop.Name); + builder.AppendLine(prop.Description); + builder.AppendLine(prop.ModelClrType.ToString()); // see ModelType tests, want ToString() not FullName } } // Include the MB version in the hash so that if the MB version changes, models are rebuilt - hash.Add(ApiVersion.Current.Version.ToString()); + builder.AppendLine(ApiVersion.Current.Version.ToString()); - return hash.GetCombinedHashCode(); + return builder.ToString().GenerateHash(); } } } diff --git a/src/Umbraco.ModelsBuilder.Embedded/Compose/DisabledModelsBuilderComponent.cs b/src/Umbraco.ModelsBuilder.Embedded/Compose/DisabledModelsBuilderComponent.cs index c599785711..9d63f65f64 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/Compose/DisabledModelsBuilderComponent.cs +++ b/src/Umbraco.ModelsBuilder.Embedded/Compose/DisabledModelsBuilderComponent.cs @@ -1,5 +1,4 @@ -using Umbraco.Core; -using Umbraco.Core.Composing; +using Umbraco.Core.Composing; using Umbraco.ModelsBuilder.Embedded.BackOffice; using Umbraco.Web.Features; diff --git a/src/Umbraco.ModelsBuilder.Embedded/Compose/ModelsBuilderComponent.cs b/src/Umbraco.ModelsBuilder.Embedded/Compose/ModelsBuilderComponent.cs index 30754d3a7f..ae2cbf6dde 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/Compose/ModelsBuilderComponent.cs +++ b/src/Umbraco.ModelsBuilder.Embedded/Compose/ModelsBuilderComponent.cs @@ -1,9 +1,7 @@ using System; using System.Collections.Generic; using System.Reflection; -using System.Web; -using System.Web.Mvc; -using System.Web.Routing; +using Microsoft.AspNetCore.Routing; using Umbraco.Configuration; using Umbraco.Core.Configuration; using Umbraco.Core.Composing; @@ -11,9 +9,11 @@ using Umbraco.Core.IO; using Umbraco.Core.Services; using Umbraco.Core.Services.Implement; using Umbraco.Core.Strings; +using Umbraco.Extensions; using Umbraco.ModelsBuilder.Embedded.BackOffice; -using Umbraco.Web; -using Umbraco.Web.Mvc; +using Umbraco.Net; +using Umbraco.Web.Common.Lifetime; +using Umbraco.Web.Common.ModelBinders; using Umbraco.Web.WebAssets; namespace Umbraco.ModelsBuilder.Embedded.Compose @@ -24,14 +24,22 @@ namespace Umbraco.ModelsBuilder.Embedded.Compose private readonly IShortStringHelper _shortStringHelper; private readonly LiveModelsProvider _liveModelsProvider; private readonly OutOfDateModelsStatus _outOfDateModels; + private readonly LinkGenerator _linkGenerator; + private readonly IUmbracoApplicationLifetime _umbracoApplicationLifetime; + private readonly IUmbracoRequestLifetime _umbracoRequestLifetime; - public ModelsBuilderComponent(IModelsBuilderConfig config, IShortStringHelper shortStringHelper, LiveModelsProvider liveModelsProvider, OutOfDateModelsStatus outOfDateModels) + public ModelsBuilderComponent(IModelsBuilderConfig config, IShortStringHelper shortStringHelper, + LiveModelsProvider liveModelsProvider, OutOfDateModelsStatus outOfDateModels, LinkGenerator linkGenerator, + IUmbracoRequestLifetime umbracoRequestLifetime, IUmbracoApplicationLifetime umbracoApplicationLifetime) { _config = config; _shortStringHelper = shortStringHelper; _liveModelsProvider = liveModelsProvider; _outOfDateModels = outOfDateModels; _shortStringHelper = shortStringHelper; + _linkGenerator = linkGenerator; + _umbracoRequestLifetime = umbracoRequestLifetime; + _umbracoApplicationLifetime = umbracoApplicationLifetime; } public void Initialize() @@ -39,6 +47,7 @@ namespace Umbraco.ModelsBuilder.Embedded.Compose // always setup the dashboard // note: UmbracoApiController instances are automatically registered InstallServerVars(); + _umbracoApplicationLifetime.ApplicationInit += InitializeApplication; ContentModelBinder.ModelBindingException += ContentModelBinder_ModelBindingException; @@ -59,6 +68,11 @@ namespace Umbraco.ModelsBuilder.Embedded.Compose FileService.SavingTemplate -= FileService_SavingTemplate; } + private void InitializeApplication(object sender, EventArgs args) + { + _umbracoRequestLifetime.RequestEnd += (sender, context) => _liveModelsProvider.AppEndRequest(context); + } + private void InstallServerVars() { // register our url - for the backoffice api @@ -80,11 +94,8 @@ namespace Umbraco.ModelsBuilder.Embedded.Compose if (!(serverVars["umbracoPlugins"] is Dictionary umbracoPlugins)) throw new ArgumentException("Invalid umbracoPlugins"); - if (HttpContext.Current == null) throw new InvalidOperationException("HttpContext is null"); - var urlHelper = new UrlHelper(new RequestContext(new HttpContextWrapper(HttpContext.Current), new RouteData())); - - umbracoUrls["modelsBuilderBaseUrl"] = urlHelper.GetUmbracoApiServiceBaseUrl(controller => controller.BuildModels()); - umbracoPlugins["modelsBuilder"] = GetModelsBuilderSettings(); + umbracoUrls["modelsBuilderBaseUrl"] = _linkGenerator.GetUmbracoApiServiceBaseUrl(controller => controller.BuildModels()); + umbracoPlugins["modelsBuilder"] = GetModelsBuilderSettings(); } private Dictionary GetModelsBuilderSettings() diff --git a/src/Umbraco.ModelsBuilder.Embedded/Compose/ModelsBuilderComposer.cs b/src/Umbraco.ModelsBuilder.Embedded/Compose/ModelsBuilderComposer.cs index 952fcd453c..951e9b6d41 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/Compose/ModelsBuilderComposer.cs +++ b/src/Umbraco.ModelsBuilder.Embedded/Compose/ModelsBuilderComposer.cs @@ -24,58 +24,29 @@ namespace Umbraco.ModelsBuilder.Embedded.Compose public void Compose(Composition composition) { - var isLegacyModelsBuilderInstalled = IsLegacyModelsBuilderInstalled(); - - if (isLegacyModelsBuilderInstalled) - { - ComposeForLegacyModelsBuilder(composition); - return; - } - composition.Components().Append(); composition.Register(Lifetime.Singleton); composition.RegisterUnique(); composition.RegisterUnique(); composition.RegisterUnique(); composition.RegisterUnique(); - + if (_config.ModelsMode == ModelsMode.PureLive) ComposeForLiveModels(composition); else if (_config.EnableFactory) ComposeForDefaultModelsFactory(composition); } - private static bool IsLegacyModelsBuilderInstalled() - { - Assembly legacyMbAssembly = null; - try - { - legacyMbAssembly = Assembly.Load("Umbraco.ModelsBuilder"); - } - catch (System.Exception) - { - //swallow exception, DLL must not be there - } - - return legacyMbAssembly != null; - } - - private void ComposeForLegacyModelsBuilder(Composition composition) - { - composition.Logger.Info("ModelsBuilder.Embedded is disabled, the external ModelsBuilder was detected."); - composition.Components().Append(); - composition.Dashboards().Remove(); - } - private void ComposeForDefaultModelsFactory(Composition composition) { composition.RegisterUnique(factory => { var typeLoader = factory.GetInstance(); + var publishedValueFallback = factory.GetInstance(); var types = typeLoader .GetTypes() // element models .Concat(typeLoader.GetTypes()); // content models - return new PublishedModelFactory(types); + return new PublishedModelFactory(types, publishedValueFallback); }); } diff --git a/src/Umbraco.ModelsBuilder.Embedded/Compose/ModelsBuilderInitializer.cs b/src/Umbraco.ModelsBuilder.Embedded/Compose/ModelsBuilderInitializer.cs deleted file mode 100644 index a86669b135..0000000000 --- a/src/Umbraco.ModelsBuilder.Embedded/Compose/ModelsBuilderInitializer.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System.Web; -using System.Web.Compilation; -using Umbraco.ModelsBuilder.Embedded.Compose; - -[assembly: PreApplicationStartMethod(typeof(ModelsBuilderInitializer), "Initialize")] - -namespace Umbraco.ModelsBuilder.Embedded.Compose -{ - public static class ModelsBuilderInitializer - { - public static void Initialize() - { - // for some reason, netstandard is missing from BuildManager.ReferencedAssemblies and yet, is part of - // the references that CSharpCompiler receives - in some cases eg when building views - but not when - // using BuildManager to build the PureLive models - where is it coming from? cannot figure it out - - // so... cheating here - - // this is equivalent to adding - // - // to web.config system.web/compilation/assemblies - - var netStandard = ReferencedAssemblies.GetNetStandardAssembly(); - if (netStandard != null) - BuildManager.AddReferencedAssembly(netStandard); - } - } -} diff --git a/src/Umbraco.ModelsBuilder.Embedded/HashCombiner.cs b/src/Umbraco.ModelsBuilder.Embedded/HashCombiner.cs deleted file mode 100644 index 1c1fca6f73..0000000000 --- a/src/Umbraco.ModelsBuilder.Embedded/HashCombiner.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System; -using System.Globalization; - -namespace Umbraco.ModelsBuilder.Embedded -{ - // because, of course, it's internal in Umbraco - // see also System.Web.Util.HashCodeCombiner - internal class HashCombiner - { - private long _combinedHash = 5381L; - - public void Add(int i) - { - _combinedHash = (_combinedHash << 5) + _combinedHash ^ i; - } - - public void Add(object o) - { - Add(o.GetHashCode()); - } - - public void Add(DateTime d) - { - Add(d.GetHashCode()); - } - - public void Add(string s) - { - if (s == null) return; - Add(StringComparer.InvariantCulture.GetHashCode(s)); - } - - public string GetCombinedHashCode() - { - return _combinedHash.ToString("x", CultureInfo.InvariantCulture); - } - } -} diff --git a/src/Umbraco.ModelsBuilder.Embedded/ImplementPropertyTypeAttribute.cs b/src/Umbraco.ModelsBuilder.Embedded/ImplementPropertyTypeAttribute.cs index b7b2695a08..6f52a7faa9 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/ImplementPropertyTypeAttribute.cs +++ b/src/Umbraco.ModelsBuilder.Embedded/ImplementPropertyTypeAttribute.cs @@ -6,7 +6,7 @@ namespace Umbraco.ModelsBuilder.Embedded /// Indicates that a property implements a given property alias. /// /// And therefore it should not be generated. - [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = false)] + [AttributeUsage(AttributeTargets.Property , AllowMultiple = false, Inherited = false)] public class ImplementPropertyTypeAttribute : Attribute { public ImplementPropertyTypeAttribute(string alias) diff --git a/src/Umbraco.ModelsBuilder.Embedded/LiveModelsProvider.cs b/src/Umbraco.ModelsBuilder.Embedded/LiveModelsProvider.cs index ef3b215968..48a79ba953 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/LiveModelsProvider.cs +++ b/src/Umbraco.ModelsBuilder.Embedded/LiveModelsProvider.cs @@ -1,7 +1,10 @@ using System; using System.Threading; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Extensions; using Umbraco.Core.Configuration; using Umbraco.Configuration; +using Umbraco.Core; using Umbraco.Core.Hosting; using Umbraco.Core.Logging; using Umbraco.ModelsBuilder.Embedded.Building; @@ -71,7 +74,7 @@ namespace Umbraco.ModelsBuilder.Embedded Interlocked.Exchange(ref _req, 1); } - public void GenerateModelsIfRequested(object sender, EventArgs args) + public void GenerateModelsIfRequested() { //if (HttpContext.Current.Items[this] == null) return; if (Interlocked.Exchange(ref _req, 0) == 0) return; @@ -110,6 +113,15 @@ namespace Umbraco.ModelsBuilder.Embedded _modelGenerator.GenerateModels(); } + public void AppEndRequest(HttpContext context) + { + var requestUri = new Uri(context.Request.GetEncodedUrl(), UriKind.RelativeOrAbsolute); + if (requestUri.IsClientSideRequest()) + return; + + if (!IsEnabled) return; + GenerateModelsIfRequested(); + } } } diff --git a/src/Umbraco.ModelsBuilder.Embedded/LiveModelsProviderModule.cs b/src/Umbraco.ModelsBuilder.Embedded/LiveModelsProviderModule.cs deleted file mode 100644 index a04723a05e..0000000000 --- a/src/Umbraco.ModelsBuilder.Embedded/LiveModelsProviderModule.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System; -using System.Web; -using Umbraco.Core; -using Umbraco.Web.Composing; -using Umbraco.ModelsBuilder.Embedded; - -// will install only if configuration says it needs to be installed -[assembly: PreApplicationStartMethod(typeof(LiveModelsProviderModule), "Install")] - -namespace Umbraco.ModelsBuilder.Embedded -{ - // have to do this because it's the only way to subscribe to EndRequest, - // module is installed by assembly attribute at the top of this file - public class LiveModelsProviderModule : IHttpModule - { - private static LiveModelsProvider _liveModelsProvider; - - public void Init(HttpApplication app) - { - app.EndRequest += App_EndRequest; - } - - private void App_EndRequest(object sender, EventArgs e) - { - if (((HttpApplication)sender).Request.Url.IsClientSideRequest()) - return; - - // here we're using "Current." since we're in a module, it is possible in a round about way to inject into a module but for now we'll just use Current - if (_liveModelsProvider == null) - _liveModelsProvider = Current.Factory.TryGetInstance(); // will be null in upgrade mode or if embedded MB is disabled - - if (_liveModelsProvider?.IsEnabled ?? false) - _liveModelsProvider.GenerateModelsIfRequested(sender, e); - } - - public void Dispose() - { - // nothing - } - - public static void Install() - { - // always - don't read config in PreApplicationStartMethod - HttpApplication.RegisterModule(typeof(LiveModelsProviderModule)); - } - } -} diff --git a/src/Umbraco.ModelsBuilder.Embedded/ModelsBuilderDashboard.cs b/src/Umbraco.ModelsBuilder.Embedded/ModelsBuilderDashboard.cs index b8b1945f32..867b22d14b 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/ModelsBuilderDashboard.cs +++ b/src/Umbraco.ModelsBuilder.Embedded/ModelsBuilderDashboard.cs @@ -15,5 +15,4 @@ namespace Umbraco.ModelsBuilder.Embedded public IAccessRule[] AccessRules => Array.Empty(); } - } diff --git a/src/Umbraco.ModelsBuilder.Embedded/ModelsGenerationError.cs b/src/Umbraco.ModelsBuilder.Embedded/ModelsGenerationError.cs index 554e8eda29..a5911bc9c6 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/ModelsGenerationError.cs +++ b/src/Umbraco.ModelsBuilder.Embedded/ModelsGenerationError.cs @@ -3,20 +3,20 @@ using System.IO; using System.Text; using Microsoft.Extensions.Options; using Umbraco.Core.Configuration; +using Umbraco.Core.Hosting; using Umbraco.Core.Configuration.Models; -using Umbraco.Core.IO; namespace Umbraco.ModelsBuilder.Embedded { public sealed class ModelsGenerationError { private readonly ModelsBuilderConfig _config; - private readonly IIOHelper _ioHelper; + private readonly IHostingEnvironment _hostingEnvironment; - public ModelsGenerationError(IOptions config, IIOHelper ioHelper) + public ModelsGenerationError(IOptions config, IHostingEnvironment hostingEnvironment) { _config = config.Value; - _ioHelper = ioHelper; + _hostingEnvironment = hostingEnvironment; } public void Clear() @@ -61,7 +61,7 @@ namespace Umbraco.ModelsBuilder.Embedded private string GetErrFile() { - var modelsDirectory = _config.ModelsDirectoryAbsolute(_ioHelper); + var modelsDirectory = _config.ModelsDirectoryAbsolute(_hostingEnvironment); if (!Directory.Exists(modelsDirectory)) return null; diff --git a/src/Umbraco.ModelsBuilder.Embedded/OutOfDateModelsStatus.cs b/src/Umbraco.ModelsBuilder.Embedded/OutOfDateModelsStatus.cs index 4fb23ad5b3..781dc40a02 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/OutOfDateModelsStatus.cs +++ b/src/Umbraco.ModelsBuilder.Embedded/OutOfDateModelsStatus.cs @@ -1,7 +1,7 @@ using System.IO; using Umbraco.Core.Configuration; +using Umbraco.Core.Hosting; using Umbraco.Core.Configuration.Models; -using Umbraco.Core.IO; using Umbraco.Web.Cache; namespace Umbraco.ModelsBuilder.Embedded @@ -9,12 +9,12 @@ namespace Umbraco.ModelsBuilder.Embedded public sealed class OutOfDateModelsStatus { private readonly ModelsBuilderConfig _config; - private readonly IIOHelper _ioHelper; + private readonly IHostingEnvironment _hostingEnvironment; - public OutOfDateModelsStatus(ModelsBuilderConfig config, IIOHelper ioHelper) + public OutOfDateModelsStatus(ModelsBuilderConfig config, IHostingEnvironment hostingEnvironment) { _config = config; - _ioHelper = ioHelper; + _hostingEnvironment = hostingEnvironment; } internal void Install() @@ -29,7 +29,7 @@ namespace Umbraco.ModelsBuilder.Embedded private string GetFlagPath() { - var modelsDirectory = _config.ModelsDirectoryAbsolute(_ioHelper); + var modelsDirectory = _config.ModelsDirectoryAbsolute(_hostingEnvironment); if (!Directory.Exists(modelsDirectory)) Directory.CreateDirectory(modelsDirectory); return Path.Combine(modelsDirectory, "ood.flag"); diff --git a/src/Umbraco.ModelsBuilder.Embedded/Properties/AssemblyInfo.cs b/src/Umbraco.ModelsBuilder.Embedded/Properties/AssemblyInfo.cs deleted file mode 100644 index 5fa17d3c77..0000000000 --- a/src/Umbraco.ModelsBuilder.Embedded/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -[assembly: AssemblyTitle("Umbraco.ModelsBuilder.Embedded")] -[assembly: AssemblyDescription("Umbraco ModelsBuilder")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyProduct("Umbraco CMS")] - -[assembly: ComVisible(false)] -[assembly: Guid("52ac0ba8-a60e-4e36-897b-e8b97a54ed1c")] - -[assembly: InternalsVisibleTo("Umbraco.Tests")] diff --git a/src/Umbraco.ModelsBuilder.Embedded/PublishedElementExtensions.cs b/src/Umbraco.ModelsBuilder.Embedded/PublishedElementExtensions.cs index 8a0a688942..0611d466dc 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/PublishedElementExtensions.cs +++ b/src/Umbraco.ModelsBuilder.Embedded/PublishedElementExtensions.cs @@ -2,12 +2,11 @@ using System.Linq.Expressions; using System.Reflection; using Umbraco.Core.Models.PublishedContent; -using Umbraco.ModelsBuilder; using Umbraco.ModelsBuilder.Embedded; // same namespace as original Umbraco.Web PublishedElementExtensions // ReSharper disable once CheckNamespace -namespace Umbraco.Web +namespace Umbraco.Core { /// /// Provides extension methods to models. @@ -17,11 +16,11 @@ namespace Umbraco.Web /// /// Gets the value of a property. /// - public static TValue ValueFor(this TModel model, Expression> property, string culture = null, string segment = null, Fallback fallback = default, TValue defaultValue = default) + public static TValue ValueFor(this TModel model, IPublishedValueFallback publishedValueFallback, Expression> property, string culture = null, string segment = null, Fallback fallback = default, TValue defaultValue = default) where TModel : IPublishedElement { var alias = GetAlias(model, property); - return model.Value(alias, culture, segment, fallback, defaultValue); + return model.Value(publishedValueFallback, alias, culture, segment, fallback, defaultValue); } // fixme that one should be public so ppl can use it diff --git a/src/Umbraco.ModelsBuilder.Embedded/PublishedModelUtility.cs b/src/Umbraco.ModelsBuilder.Embedded/PublishedModelUtility.cs index 8a6ed83ce9..fd1d5128a0 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/PublishedModelUtility.cs +++ b/src/Umbraco.ModelsBuilder.Embedded/PublishedModelUtility.cs @@ -2,7 +2,7 @@ using System.Linq; using System.Linq.Expressions; using Umbraco.Core.Models.PublishedContent; -using Umbraco.Web.Composing; +using Umbraco.Web.PublishedCache; namespace Umbraco.ModelsBuilder.Embedded { @@ -29,10 +29,10 @@ namespace Umbraco.ModelsBuilder.Embedded // var contentType = PublishedContentType.Get(itemType, alias); // // etc... //} - - public static IPublishedContentType GetModelContentType(PublishedItemType itemType, string alias) + + public static IPublishedContentType GetModelContentType(IPublishedSnapshotAccessor publishedSnapshotAccessor, PublishedItemType itemType, string alias) { - var facade = Current.UmbracoContext.PublishedSnapshot; // fixme inject! + var facade = publishedSnapshotAccessor.PublishedSnapshot; switch (itemType) { case PublishedItemType.Content: diff --git a/src/Umbraco.ModelsBuilder.Embedded/PureLiveModelFactory.cs b/src/Umbraco.ModelsBuilder.Embedded/PureLiveModelFactory.cs index c355f651da..596cc9ed26 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/PureLiveModelFactory.cs +++ b/src/Umbraco.ModelsBuilder.Embedded/PureLiveModelFactory.cs @@ -8,13 +8,9 @@ using System.Reflection.Emit; using System.Text; using System.Text.RegularExpressions; using System.Threading; -using System.Web; -using System.Web.Compilation; -using System.Web.WebPages.Razor; using Umbraco.Core.Configuration; using Umbraco.Core; using Umbraco.Core.Hosting; -using Umbraco.Core.IO; using Umbraco.Core.Logging; using Umbraco.Core.Models.PublishedContent; using Umbraco.ModelsBuilder.Embedded.Building; @@ -26,7 +22,6 @@ namespace Umbraco.ModelsBuilder.Embedded { internal class PureLiveModelFactory : ILivePublishedModelFactory, IRegisteredObject { - private Assembly _modelsAssembly; private Infos _infos = new Infos { ModelInfos = null, ModelTypeMap = new Dictionary() }; private readonly ReaderWriterLockSlim _locker = new ReaderWriterLockSlim(); private volatile bool _hasModels; // volatile 'cos reading outside lock @@ -35,18 +30,19 @@ namespace Umbraco.ModelsBuilder.Embedded private readonly FileSystemWatcher _watcher; private int _ver, _skipver; private readonly int _debugLevel; - private BuildManager _theBuildManager; + private RoslynCompiler _roslynCompiler; + private UmbracoAssemblyLoadContext _currentAssemblyLoadContext; private readonly Lazy _umbracoServices; // fixme: this is because of circular refs :( private UmbracoServices UmbracoServices => _umbracoServices.Value; private static readonly Regex AssemblyVersionRegex = new Regex("AssemblyVersion\\(\"[0-9]+.[0-9]+.[0-9]+.[0-9]+\"\\)", RegexOptions.Compiled); - private const string ProjVirt = "~/App_Data/Models/all.generated.cs"; - private static readonly string[] OurFiles = { "models.hash", "models.generated.cs", "all.generated.cs", "all.dll.path", "models.err" }; + private static readonly string[] OurFiles = { "models.hash", "models.generated.cs", "all.generated.cs", "all.dll.path", "models.err", "Compiled" }; private readonly ModelsBuilderConfig _config; + private readonly IHostingEnvironment _hostingEnvironment; private readonly IApplicationShutdownRegistry _hostingLifetime; - private readonly IIOHelper _ioHelper; private readonly ModelsGenerationError _errors; + private readonly IPublishedValueFallback _publishedValueFallback; public PureLiveModelFactory( Lazy umbracoServices, @@ -54,22 +50,20 @@ namespace Umbraco.ModelsBuilder.Embedded IOptions config, IHostingEnvironment hostingEnvironment, IApplicationShutdownRegistry hostingLifetime, - IIOHelper ioHelper) + IPublishedValueFallback publishedValueFallback) { _umbracoServices = umbracoServices; _logger = logger; _config = config.Value; + _hostingEnvironment = hostingEnvironment; _hostingLifetime = hostingLifetime; - _ioHelper = ioHelper; - _errors = new ModelsGenerationError(config, ioHelper); + _publishedValueFallback = publishedValueFallback; + _errors = new ModelsGenerationError(config, _hostingEnvironment); _ver = 1; // zero is for when we had no version _skipver = -1; // nothing to skip - - RazorBuildProvider.CodeGenerationStarted += RazorBuildProvider_CodeGenerationStarted; - if (!hostingEnvironment.IsHosted) return; - var modelsDirectory = _config.ModelsDirectoryAbsolute(_ioHelper); + var modelsDirectory = _config.ModelsDirectoryAbsolute(_hostingEnvironment); if (!Directory.Exists(modelsDirectory)) Directory.CreateDirectory(modelsDirectory); @@ -115,7 +109,7 @@ namespace Umbraco.ModelsBuilder.Embedded infos.TryGetValue(contentTypeAlias, out var info); // create model - return info == null ? element : info.Ctor(element); + return info == null ? element : info.Ctor(element, _publishedValueFallback); } // this runs only once the factory is ready @@ -160,7 +154,6 @@ namespace Umbraco.ModelsBuilder.Embedded #endregion #region Compilation - // deadlock note // // when RazorBuildProvider_CodeGenerationStarted runs, the thread has Monitor.Enter-ed the BuildManager @@ -190,32 +183,33 @@ namespace Umbraco.ModelsBuilder.Embedded // // well, that's what we are doing in this class' TheBuildManager property, using reflection. - private void RazorBuildProvider_CodeGenerationStarted(object sender, EventArgs e) - { - try - { - _locker.EnterReadLock(); + // private void RazorBuildProvider_CodeGenerationStarted(object sender, EventArgs e) + // { + // try + // { + // _locker.EnterReadLock(); + // + // // just be safe - can happen if the first view is not an Umbraco view, + // // or if something went wrong and we don't have an assembly at all + // if (_modelsAssembly == null) return; + // + // if (_debugLevel > 0) + // _logger.Debug("RazorBuildProvider.CodeGenerationStarted"); + // if (!(sender is RazorBuildProvider provider)) return; + // + // // add the assembly, and add a dependency to a text file that will change on each + // // compilation as in some environments (could not figure which/why) the BuildManager + // // would not re-compile the views when the models assembly is rebuilt. + // provider.AssemblyBuilder.AddAssemblyReference(_modelsAssembly); + // provider.AddVirtualPathDependency(ProjVirt); + // } + // finally + // { + // if (_locker.IsReadLockHeld) + // _locker.ExitReadLock(); + // } + // } - // just be safe - can happen if the first view is not an Umbraco view, - // or if something went wrong and we don't have an assembly at all - if (_modelsAssembly == null) return; - - if (_debugLevel > 0) - _logger.Debug("RazorBuildProvider.CodeGenerationStarted"); - if (!(sender is RazorBuildProvider provider)) return; - - // add the assembly, and add a dependency to a text file that will change on each - // compilation as in some environments (could not figure which/why) the BuildManager - // would not re-compile the views when the models assembly is rebuilt. - provider.AssemblyBuilder.AddAssemblyReference(_modelsAssembly); - provider.AddVirtualPathDependency(ProjVirt); - } - finally - { - if (_locker.IsReadLockHeld) - _locker.ExitReadLock(); - } - } // tells the factory that it should build a new generation of models private void ResetModels() @@ -229,14 +223,12 @@ namespace Umbraco.ModelsBuilder.Embedded _hasModels = false; _pendingRebuild = true; - var modelsDirectory = _config.ModelsDirectoryAbsolute(_ioHelper); + var modelsDirectory = _config.ModelsDirectoryAbsolute(_hostingEnvironment); if (!Directory.Exists(modelsDirectory)) Directory.CreateDirectory(modelsDirectory); // clear stuff var modelsHashFile = Path.Combine(modelsDirectory, "models.hash"); - //var modelsSrcFile = Path.Combine(modelsDirectory, "models.generated.cs"); - //var projFile = Path.Combine(modelsDirectory, "all.generated.cs"); var dllPathFile = Path.Combine(modelsDirectory, "all.dll.path"); if (File.Exists(dllPathFile)) File.Delete(dllPathFile); @@ -249,17 +241,14 @@ namespace Umbraco.ModelsBuilder.Embedded } } - // gets "the" build manager - private BuildManager TheBuildManager + // gets the RoslynCompiler + private RoslynCompiler RoslynCompiler { get { - if (_theBuildManager != null) return _theBuildManager; - var prop = typeof(BuildManager).GetProperty("TheBuildManager", BindingFlags.NonPublic | BindingFlags.Static); - if (prop == null) - throw new InvalidOperationException("Could not get BuildManager.TheBuildManager property."); - _theBuildManager = (BuildManager)prop.GetValue(null); - return _theBuildManager; + if (_roslynCompiler != null) return _roslynCompiler; + _roslynCompiler = new RoslynCompiler(System.Runtime.Loader.AssemblyLoadContext.All.SelectMany(x => x.Assemblies)); + return _roslynCompiler; } } @@ -282,12 +271,12 @@ namespace Umbraco.ModelsBuilder.Embedded _locker.ExitReadLock(); } - var buildManagerLocked = false; + var roslynLocked = false; try { // always take the BuildManager lock *before* taking the _locker lock // to avoid possible deadlock situations (see notes above) - Monitor.Enter(TheBuildManager, ref buildManagerLocked); + Monitor.Enter(RoslynCompiler, ref roslynLocked); _locker.EnterUpgradeableReadLock(); @@ -310,9 +299,6 @@ namespace Umbraco.ModelsBuilder.Embedded // this is for U4-8043 which is an obvious issue but I cannot replicate //_modelsAssembly = _modelsAssembly ?? assembly; - // the one below is the normal one - _modelsAssembly = assembly; - var types = assembly.ExportedTypes.Where(x => x.Inherits() || x.Inherits()); _infos = RegisterModels(types); _errors.Clear(); @@ -327,7 +313,6 @@ namespace Umbraco.ModelsBuilder.Embedded } finally { - _modelsAssembly = null; _infos = new Infos { ModelInfos = null, ModelTypeMap = new Dictionary() }; } } @@ -344,14 +329,36 @@ namespace Umbraco.ModelsBuilder.Embedded _locker.ExitWriteLock(); if (_locker.IsUpgradeableReadLockHeld) _locker.ExitUpgradeableReadLock(); - if (buildManagerLocked) - Monitor.Exit(TheBuildManager); + if (roslynLocked) + Monitor.Exit(RoslynCompiler); + } + } + + private Assembly ReloadAssembly(string pathToAssembly) + { + // If there's a current AssemblyLoadContext, unload it before creating a new one. + if(!(_currentAssemblyLoadContext is null)) + { + _currentAssemblyLoadContext.Unload(); + GC.Collect(); + GC.WaitForPendingFinalizers(); + } + + // We must create a new assembly load context + // as long as theres a reference to the assembly load context we can't delete the assembly it loaded + _currentAssemblyLoadContext = new UmbracoAssemblyLoadContext(); + + // Use filestream to load in the new assembly, otherwise it'll be locked + // See https://www.strathweb.com/2019/01/collectible-assemblies-in-net-core-3-0/ for more info + using (var fs = new FileStream(pathToAssembly, FileMode.Open, FileAccess.Read)) + { + return _currentAssemblyLoadContext.LoadFromStream(fs); } } private Assembly GetModelsAssembly(bool forceRebuild) { - var modelsDirectory = _config.ModelsDirectoryAbsolute(_ioHelper); + var modelsDirectory = _config.ModelsDirectoryAbsolute(_hostingEnvironment); if (!Directory.Exists(modelsDirectory)) Directory.CreateDirectory(modelsDirectory); @@ -394,23 +401,16 @@ namespace Umbraco.ModelsBuilder.Embedded // ensure that the .dll file does not have a corresponding .dll.delete file // as that would mean the the .dll file is going to be deleted and should not // be re-used - that should not happen in theory, but better be safe - // - // ensure that the .dll file is in the current codegen directory - when IIS - // or Express does a full restart, it can switch to an entirely new codegen - // directory, and then we end up referencing a dll which is *not* in that - // directory, and BuildManager fails to instantiate views ("the view found - // at ... was not created"). - // if (File.Exists(dllPathFile)) { var dllPath = File.ReadAllText(dllPathFile); - var codegen = HttpRuntime.CodegenDir; _logger.Debug($"Cached models dll at {dllPath}."); - if (File.Exists(dllPath) && !File.Exists(dllPath + ".delete") && dllPath.StartsWith(codegen)) + if (File.Exists(dllPath) && !File.Exists(dllPath + ".delete")) { - assembly = Assembly.LoadFile(dllPath); + assembly = ReloadAssembly(dllPath); + var attr = assembly.GetCustomAttribute(); if (attr != null && attr.PureLive && attr.SourceHash == currentHash) { @@ -430,8 +430,6 @@ namespace Umbraco.ModelsBuilder.Embedded _logger.Debug("Cached models dll does not exist."); else if (File.Exists(dllPath + ".delete")) _logger.Debug("Cached models dll is marked for deletion."); - else if (!dllPath.StartsWith(codegen)) - _logger.Debug("Cached models dll is in a different codegen directory."); else _logger.Debug("Cached models dll cannot be loaded (why?)."); } @@ -446,16 +444,15 @@ namespace Umbraco.ModelsBuilder.Embedded File.WriteAllText(projFile, text); } - // generate a marker file that will be a dependency - // see note in RazorBuildProvider_CodeGenerationStarted - // NO: using all.generated.cs as a dependency - //File.WriteAllText(Path.Combine(modelsDirectory, "models.dep"), "VER:" + _ver); - _ver++; try { - assembly = BuildManager.GetCompiledAssembly(ProjVirt); + var assemblyPath = GetOutputAssemblyPath(currentHash); + RoslynCompiler.CompileToFile(_hostingEnvironment.MapPathContentRoot(projFile), assemblyPath); + assembly = ReloadAssembly(assemblyPath); File.WriteAllText(dllPathFile, assembly.Location); + File.WriteAllText(modelsHashFile, currentHash); + TryDeleteUnusedAssemblies(dllPathFile); } catch { @@ -492,9 +489,12 @@ namespace Umbraco.ModelsBuilder.Embedded // compile and register try { - assembly = BuildManager.GetCompiledAssembly(ProjVirt); - File.WriteAllText(dllPathFile, assembly.Location); + var assemblyPath = GetOutputAssemblyPath(currentHash); + RoslynCompiler.CompileToFile(_hostingEnvironment.MapPathContentRoot(projFile), assemblyPath); + assembly = ReloadAssembly(assemblyPath); + File.WriteAllText(dllPathFile, assemblyPath); File.WriteAllText(modelsHashFile, currentHash); + TryDeleteUnusedAssemblies(dllPathFile); } catch { @@ -506,6 +506,37 @@ namespace Umbraco.ModelsBuilder.Embedded return assembly; } + private void TryDeleteUnusedAssemblies(string dllPathFile) + { + if (File.Exists(dllPathFile)) + { + var dllPath = File.ReadAllText(dllPathFile); + var dirInfo = new DirectoryInfo(dllPath).Parent; + var files = dirInfo.GetFiles().Where(f => f.FullName != dllPath); + foreach(var file in files) + { + try + { + File.Delete(file.FullName); + } + catch(UnauthorizedAccessException) + { + // The file is in use, we'll try again next time... + // This shouldn't happen anymore. + } + } + + } + } + + private string GetOutputAssemblyPath(string currentHash) + { + var dirInfo = new DirectoryInfo(Path.Combine(_config.ModelsDirectoryAbsolute(_hostingEnvironment), "Compiled")); + if (!dirInfo.Exists) + Directory.CreateDirectory(dirInfo.FullName); + return Path.Combine(dirInfo.FullName, $"generated.cs{currentHash}.dll"); + } + private void ClearOnFailingToCompile(string dllPathFile, string modelsHashFile, string projFile) { _logger.Debug("Failed to compile."); @@ -525,7 +556,7 @@ namespace Umbraco.ModelsBuilder.Embedded private static Infos RegisterModels(IEnumerable types) { - var ctorArgTypes = new[] { typeof(IPublishedElement) }; + var ctorArgTypes = new[] { typeof(IPublishedElement), typeof(IPublishedValueFallback) }; var modelInfos = new Dictionary(StringComparer.InvariantCultureIgnoreCase); var map = new Dictionary(); @@ -537,7 +568,7 @@ namespace Umbraco.ModelsBuilder.Embedded foreach (var ctor in type.GetConstructors()) { var parms = ctor.GetParameters(); - if (parms.Length == 1 && typeof(IPublishedElement).IsAssignableFrom(parms[0].ParameterType)) + if (parms.Length == 2 && typeof(IPublishedElement).IsAssignableFrom(parms[0].ParameterType) && typeof(IPublishedValueFallback).IsAssignableFrom(parms[1].ParameterType)) { if (constructor != null) throw new InvalidOperationException($"Type {type.FullName} has more than one public constructor with one argument of type, or implementing, IPropertySet."); @@ -562,9 +593,10 @@ namespace Umbraco.ModelsBuilder.Embedded var meth = new DynamicMethod(string.Empty, typeof(IPublishedElement), ctorArgTypes, type.Module, true); var gen = meth.GetILGenerator(); gen.Emit(OpCodes.Ldarg_0); + gen.Emit(OpCodes.Ldarg_1); gen.Emit(OpCodes.Newobj, constructor); gen.Emit(OpCodes.Ret); - var func = (Func)meth.CreateDelegate(typeof(Func)); + var func = (Func)meth.CreateDelegate(typeof(Func)); modelInfos[typeName] = new ModelInfo { ParameterType = parameterType, Ctor = func, ModelType = type }; map[typeName] = type; @@ -575,7 +607,7 @@ namespace Umbraco.ModelsBuilder.Embedded private string GenerateModelsCode(IList typeModels) { - var modelsDirectory = _config.ModelsDirectoryAbsolute(_ioHelper); + var modelsDirectory = _config.ModelsDirectoryAbsolute(_hostingEnvironment); if (!Directory.Exists(modelsDirectory)) Directory.CreateDirectory(modelsDirectory); @@ -653,7 +685,7 @@ namespace Umbraco.ModelsBuilder.Embedded internal class ModelInfo { public Type ParameterType { get; set; } - public Func Ctor { get; set; } + public Func Ctor { get; set; } public Type ModelType { get; set; } public Func ListCtor { get; set; } } diff --git a/src/Umbraco.ModelsBuilder.Embedded/ReferencedAssemblies.cs b/src/Umbraco.ModelsBuilder.Embedded/ReferencedAssemblies.cs deleted file mode 100644 index 8886afa1c8..0000000000 --- a/src/Umbraco.ModelsBuilder.Embedded/ReferencedAssemblies.cs +++ /dev/null @@ -1,159 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using System.Web.Compilation; -using System.Web.Hosting; -using Umbraco.Core; - -namespace Umbraco.ModelsBuilder.Embedded -{ - internal static class ReferencedAssemblies - { - private static readonly Lazy> LazyLocations; - - static ReferencedAssemblies() - { - LazyLocations = new Lazy>(() => HostingEnvironment.IsHosted - ? GetAllReferencedAssembliesLocationFromBuildManager() - : GetAllReferencedAssembliesFromDomain()); - } - - /// - /// Gets the assembly locations of all the referenced assemblies, that - /// are not dynamic, and have a non-null nor empty location. - /// - public static IEnumerable Locations => LazyLocations.Value; - - public static Assembly GetNetStandardAssembly(List assemblies) - { - if (assemblies == null) - assemblies = BuildManager.GetReferencedAssemblies().Cast().ToList(); - - // for some reason, netstandard is also missing from BuildManager.ReferencedAssemblies and yet, is part of - // the references that CSharpCompiler (above) receives - where is it coming from? cannot figure it out - try - { - // so, resorting to an ugly trick - // we should have System.Reflection.Metadata around, and it should reference netstandard - var someAssembly = assemblies.First(x => x.FullName.StartsWith("System.Reflection.Metadata,")); - var netStandardAssemblyName = someAssembly.GetReferencedAssemblies().First(x => x.FullName.StartsWith("netstandard,")); - var netStandard = Assembly.Load(netStandardAssemblyName.FullName); - return netStandard; - } - catch { /* never mind */ } - - return null; - } - - public static Assembly GetNetStandardAssembly() - { - // in PreApplicationStartMethod we cannot get BuildManager.Referenced assemblies, do it differently - try - { - var someAssembly = Assembly.Load("System.Reflection.Metadata"); - var netStandardAssemblyName = someAssembly.GetReferencedAssemblies().First(x => x.FullName.StartsWith("netstandard,")); - var netStandard = Assembly.Load(netStandardAssemblyName.FullName); - return netStandard; - } - catch { /* never mind */ } - - return null; - } - - // hosted, get referenced assemblies from the BuildManager and filter - private static IEnumerable GetAllReferencedAssembliesLocationFromBuildManager() - { - var assemblies = BuildManager.GetReferencedAssemblies().Cast().ToList(); - - assemblies.Add(typeof(ReferencedAssemblies).Assembly); // always include ourselves - - // see https://github.com/aspnet/RoslynCodeDomProvider/blob/master/src/Microsoft.CodeDom.Providers.DotNetCompilerPlatform/CSharpCompiler.cs: - // mentions "Bug 913691: Explicitly add System.Runtime as a reference." - // and explicitly adds System.Runtime to references before invoking csc.exe - // so, doing the same here - try - { - var systemRuntime = Assembly.Load("System.Runtime, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"); - assemblies.Add(systemRuntime); - } - catch { /* never mind */ } - - // for some reason, netstandard is also missing from BuildManager.ReferencedAssemblies and yet, is part of - // the references that CSharpCompiler (above) receives - where is it coming from? cannot figure it out - var netStandard = GetNetStandardAssembly(assemblies); - if (netStandard != null) assemblies.Add(netStandard); - - return assemblies - .Where(x => !x.IsDynamic && !x.Location.IsNullOrWhiteSpace()) - .Select(x => x.Location) - .Distinct() - .ToList(); - } - - // non-hosted, do our best - private static IEnumerable GetAllReferencedAssembliesFromDomain() - { - //TODO: This method has bugs since I've been stuck in an infinite loop with it, though this shouldn't - // execute while in the web application anyways. - - var assemblies = new List(); - var tmp1 = new List(); - var failed = new List(); - foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies() - .Where(x => x.IsDynamic == false) - .Where(x => !string.IsNullOrWhiteSpace(x.Location))) // though... IsDynamic should be enough? - { - assemblies.Add(assembly); - tmp1.Add(assembly); - } - - // fixme - AssemblyUtility questions - // - should we also load everything that's in the same directory? - // - do we want to load in the current app domain? - // - if this runs within Umbraco then we have already loaded them all? - - while (tmp1.Count > 0) - { - var tmp2 = tmp1 - .SelectMany(x => x.GetReferencedAssemblies()) - .Distinct() - .Where(x => assemblies.All(xx => x.FullName != xx.FullName)) // we don't have it already - .Where(x => failed.All(xx => x.FullName != xx.FullName)) // it hasn't failed already - .ToArray(); - tmp1.Clear(); - foreach (var assemblyName in tmp2) - { - try - { - var assembly = AppDomain.CurrentDomain.Load(assemblyName); - assemblies.Add(assembly); - tmp1.Add(assembly); - } - catch - { - failed.Add(assemblyName); - } - } - } - return assemblies.Select(x => x.Location).Distinct(); - } - - - // ---- - - - private static Assembly TryLoad(AssemblyName name) - { - try - { - return AppDomain.CurrentDomain.Load(name); - } - catch (Exception) - { - //Console.WriteLine(name); - return null; - } - } - } -} diff --git a/src/Umbraco.ModelsBuilder.Embedded/RoslynCompiler.cs b/src/Umbraco.ModelsBuilder.Embedded/RoslynCompiler.cs new file mode 100644 index 0000000000..7f1443b156 --- /dev/null +++ b/src/Umbraco.ModelsBuilder.Embedded/RoslynCompiler.cs @@ -0,0 +1,69 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Text; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; + +namespace Umbraco.ModelsBuilder.Embedded +{ + public class RoslynCompiler + { + private OutputKind _outputKind; + private CSharpParseOptions _parseOptions; + private List _refs; + + /// + /// Roslyn compiler which can be used to compile a c# file to a Dll assembly + /// + /// Referenced assemblies used in the source file + public RoslynCompiler(IEnumerable referenceAssemblies) + { + _outputKind = OutputKind.DynamicallyLinkedLibrary; + _parseOptions = CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.Latest); // What languageversion should we default to? + + // The references should be the same every time GetCompiledAssembly is called + // Making it kind of a waste to convert the Assembly types into MetadataReference + // every time GetCompiledAssembly is called, so that's why I do it in the ctor + _refs = new List(); + foreach(var assembly in referenceAssemblies.Where(x => !x.IsDynamic && !string.IsNullOrWhiteSpace(x.Location)).Distinct()) + { + _refs.Add(MetadataReference.CreateFromFile(assembly.Location)); + }; + + // Might have to do this another way, see + // see https://github.com/aspnet/RoslynCodeDomProvider/blob/master/src/Microsoft.CodeDom.Providers.DotNetCompilerPlatform/CSharpCompiler.cs: + // mentions "Bug 913691: Explicitly add System.Runtime as a reference." + // and explicitly adds System.Runtime to references + _refs.Add(MetadataReference.CreateFromFile(typeof(System.Runtime.AssemblyTargetedPatchBandAttribute).Assembly.Location)); + _refs.Add(MetadataReference.CreateFromFile(typeof(System.CodeDom.Compiler.GeneratedCodeAttribute).Assembly.Location)); + } + + /// + /// Compile a source file to a dll + /// + /// Path to the source file containing the code to be compiled. + /// The path where the output assembly will be saved. + public void CompileToFile(string pathToSourceFile, string savePath) + { + var sourceCode = File.ReadAllText(pathToSourceFile); + + var sourceText = SourceText.From(sourceCode); + + var syntaxTree = SyntaxFactory.ParseSyntaxTree(sourceText, _parseOptions); + + var compilation = CSharpCompilation.Create("ModelsGeneratedAssembly", + new[] { syntaxTree }, + references: _refs, + options: new CSharpCompilationOptions(_outputKind, + optimizationLevel: OptimizationLevel.Release, + // Not entirely certain that assemblyIdentityComparer is nececary? + assemblyIdentityComparer: DesktopAssemblyIdentityComparer.Default)); + + compilation.Emit(savePath); + + } + } +} diff --git a/src/Umbraco.ModelsBuilder.Embedded/Umbraco.ModelsBuilder.Embedded.csproj b/src/Umbraco.ModelsBuilder.Embedded/Umbraco.ModelsBuilder.Embedded.csproj index 608770b124..f2b264dcb0 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/Umbraco.ModelsBuilder.Embedded.csproj +++ b/src/Umbraco.ModelsBuilder.Embedded/Umbraco.ModelsBuilder.Embedded.csproj @@ -1,118 +1,37 @@ - - - - - Debug - AnyCPU - {52AC0BA8-A60E-4E36-897B-E8B97A54ED1C} - Library - Properties - Umbraco.ModelsBuilder.Embedded - Umbraco.ModelsBuilder.Embedded - v4.7.2 - 512 - true - 8 - - - true - portable - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - portable - true - bin\Release\ - TRACE - prompt - 4 - bin\Release\Umbraco.ModelsBuilder.Embedded.xml - - - - - - - - - - - - - - - - Properties\SolutionInfo.cs - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 2.10.0 - - - 1.0.0 - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - 4.7.0 - - - - - {29aa69d9-b597-4395-8d42-43b1263c240a} - Umbraco.Core - - - {3ae7bf57-966b-45a5-910a-954d7c554441} - Umbraco.Infrastructure - - - {651e1350-91b6-44b7-bd60-7207006d7003} - Umbraco.Web - - - - - 5.2.7 - - - - \ No newline at end of file + + + + netcoreapp3.1 + Library + latest + + + + bin\Release\Umbraco.ModelsBuilder.Embedded.xml + + + + + + + + + + + + + + + <_Parameter1>Umbraco.Tests + + + <_Parameter1>Umbraco.Tests.UnitTests + + + <_Parameter1>Umbraco.Tests.Benchmarks + + + <_Parameter1>Umbraco.Tests.Integration + + + diff --git a/src/Umbraco.ModelsBuilder.Embedded/UmbracoAssemblyLoadContext.cs b/src/Umbraco.ModelsBuilder.Embedded/UmbracoAssemblyLoadContext.cs new file mode 100644 index 0000000000..b7feeaaaeb --- /dev/null +++ b/src/Umbraco.ModelsBuilder.Embedded/UmbracoAssemblyLoadContext.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Runtime.Loader; +using System.Text; + +namespace Umbraco.ModelsBuilder.Embedded +{ + class UmbracoAssemblyLoadContext : AssemblyLoadContext + { + + /// + /// Collectible AssemblyLoadContext used to load in the compiled generated models. + /// Must be a collectible assembly in order to be able to be unloaded. + /// + public UmbracoAssemblyLoadContext() : base(isCollectible: true) + { + + } + + protected override Assembly Load(AssemblyName assemblyName) + { + return null; + } + } +} 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.Integration/Persistence/Repositories/TemplateRepositoryTest.cs b/src/Umbraco.Tests.Integration/Persistence/Repositories/TemplateRepositoryTest.cs index 2839aabe7e..4cb0217fe4 100644 --- a/src/Umbraco.Tests.Integration/Persistence/Repositories/TemplateRepositoryTest.cs +++ b/src/Umbraco.Tests.Integration/Persistence/Repositories/TemplateRepositoryTest.cs @@ -99,7 +99,7 @@ namespace Umbraco.Tests.Integration.Persistence.Repositories Assert.That(repository.Get("test"), Is.Not.Null); Assert.That(_fileSystems.MvcViewsFileSystem.FileExists("test.cshtml"), Is.True); Assert.AreEqual( - @"@inherits Umbraco.Web.Common.AspNetCore.UmbracoViewPage @{ Layout = null;}".StripWhitespace(), + @"@usingUmbraco.Web.PublishedModels;@inheritsUmbraco.Web.Common.AspNetCore.UmbracoViewPage@{Layout=null;}".StripWhitespace(), template.Content.StripWhitespace()); } } @@ -127,7 +127,7 @@ namespace Umbraco.Tests.Integration.Persistence.Repositories Assert.That(repository.Get("test2"), Is.Not.Null); Assert.That(_fileSystems.MvcViewsFileSystem.FileExists("test2.cshtml"), Is.True); Assert.AreEqual( - "@inherits Umbraco.Web.Common.AspNetCore.UmbracoViewPage @{ Layout = \"test.cshtml\";}".StripWhitespace(), + "@usingUmbraco.Web.PublishedModels;@inherits Umbraco.Web.Common.AspNetCore.UmbracoViewPage @{ Layout = \"test.cshtml\";}".StripWhitespace(), template2.Content.StripWhitespace()); } } diff --git a/src/Umbraco.Tests/ModelsBuilder/BuilderTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Tests.Common/ModelsBuilder/BuilderTests.cs similarity index 92% rename from src/Umbraco.Tests/ModelsBuilder/BuilderTests.cs rename to src/Umbraco.Tests.UnitTests/Umbraco.Tests.Common/ModelsBuilder/BuilderTests.cs index 91bf7406f3..f185a56868 100644 --- a/src/Umbraco.Tests/ModelsBuilder/BuilderTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Tests.Common/ModelsBuilder/BuilderTests.cs @@ -63,13 +63,11 @@ namespace Umbraco.Tests.ModelsBuilder //------------------------------------------------------------------------------ using System; -using System.Collections.Generic; using System.Linq.Expressions; -using System.Web; -using Umbraco.Core.Models; using Umbraco.Core.Models.PublishedContent; -using Umbraco.Web; +using Umbraco.Web.PublishedCache; using Umbraco.ModelsBuilder.Embedded; +using Umbraco.Core; namespace Umbraco.Web.PublishedModels { @@ -83,23 +81,27 @@ namespace Umbraco.Web.PublishedModels [global::System.CodeDom.Compiler.GeneratedCodeAttribute(""Umbraco.ModelsBuilder.Embedded"", """ + version + @""")] public new const PublishedItemType ModelItemType = PublishedItemType.Content; [global::System.CodeDom.Compiler.GeneratedCodeAttribute(""Umbraco.ModelsBuilder.Embedded"", """ + version + @""")] - public new static IPublishedContentType GetModelContentType() - => PublishedModelUtility.GetModelContentType(ModelItemType, ModelTypeAlias); + public new static IPublishedContentType GetModelContentType(IPublishedSnapshotAccessor publishedSnapshotAccessor) + => PublishedModelUtility.GetModelContentType(publishedSnapshotAccessor, ModelItemType, ModelTypeAlias); [global::System.CodeDom.Compiler.GeneratedCodeAttribute(""Umbraco.ModelsBuilder.Embedded"", """ + version + @""")] - public static IPublishedPropertyType GetModelPropertyType(Expression> selector) - => PublishedModelUtility.GetModelPropertyType(GetModelContentType(), selector); + public static IPublishedPropertyType GetModelPropertyType(IPublishedSnapshotAccessor publishedSnapshotAccessor, Expression> selector) + => PublishedModelUtility.GetModelPropertyType(GetModelContentType(publishedSnapshotAccessor), selector); #pragma warning restore 0109 + private IPublishedValueFallback _publishedValueFallback; + // ctor - public Type1(IPublishedContent content) + public Type1(IPublishedContent content, IPublishedValueFallback publishedValueFallback) : base(content) - { } + { + _publishedValueFallback = publishedValueFallback; + } // properties [global::System.CodeDom.Compiler.GeneratedCodeAttribute(""Umbraco.ModelsBuilder.Embedded"", """ + version + @""")] [ImplementPropertyType(""prop1"")] - public string Prop1 => this.Value(""prop1""); + public string Prop1 => this.Value(_publishedValueFallback, ""prop1""); } } "; @@ -179,13 +181,11 @@ namespace Umbraco.Web.PublishedModels //------------------------------------------------------------------------------ using System; -using System.Collections.Generic; using System.Linq.Expressions; -using System.Web; -using Umbraco.Core.Models; using Umbraco.Core.Models.PublishedContent; -using Umbraco.Web; +using Umbraco.Web.PublishedCache; using Umbraco.ModelsBuilder.Embedded; +using Umbraco.Core; namespace Umbraco.Web.PublishedModels { @@ -199,23 +199,27 @@ namespace Umbraco.Web.PublishedModels [global::System.CodeDom.Compiler.GeneratedCodeAttribute(""Umbraco.ModelsBuilder.Embedded"", """ + version + @""")] public new const PublishedItemType ModelItemType = PublishedItemType.Content; [global::System.CodeDom.Compiler.GeneratedCodeAttribute(""Umbraco.ModelsBuilder.Embedded"", """ + version + @""")] - public new static IPublishedContentType GetModelContentType() - => PublishedModelUtility.GetModelContentType(ModelItemType, ModelTypeAlias); + public new static IPublishedContentType GetModelContentType(IPublishedSnapshotAccessor publishedSnapshotAccessor) + => PublishedModelUtility.GetModelContentType(publishedSnapshotAccessor, ModelItemType, ModelTypeAlias); [global::System.CodeDom.Compiler.GeneratedCodeAttribute(""Umbraco.ModelsBuilder.Embedded"", """ + version + @""")] - public static IPublishedPropertyType GetModelPropertyType(Expression> selector) - => PublishedModelUtility.GetModelPropertyType(GetModelContentType(), selector); + public static IPublishedPropertyType GetModelPropertyType(IPublishedSnapshotAccessor publishedSnapshotAccessor, Expression> selector) + => PublishedModelUtility.GetModelPropertyType(GetModelContentType(publishedSnapshotAccessor), selector); #pragma warning restore 0109 + private IPublishedValueFallback _publishedValueFallback; + // ctor - public Type1(IPublishedContent content) + public Type1(IPublishedContent content, IPublishedValueFallback publishedValueFallback) : base(content) - { } + { + _publishedValueFallback = publishedValueFallback; + } // properties [global::System.CodeDom.Compiler.GeneratedCodeAttribute(""Umbraco.ModelsBuilder.Embedded"", """ + version + @""")] [ImplementPropertyType(""foo"")] - public global::System.Collections.Generic.IEnumerable Foo => this.Value>(""foo""); + public global::System.Collections.Generic.IEnumerable Foo => this.Value>(_publishedValueFallback, ""foo""); } } "; diff --git a/src/Umbraco.Tests/ModelsBuilder/ConfigTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Tests.Common/ModelsBuilder/ConfigTests.cs similarity index 98% rename from src/Umbraco.Tests/ModelsBuilder/ConfigTests.cs rename to src/Umbraco.Tests.UnitTests/Umbraco.Tests.Common/ModelsBuilder/ConfigTests.cs index 5ed96365fd..2e15381d18 100644 --- a/src/Umbraco.Tests/ModelsBuilder/ConfigTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Tests.Common/ModelsBuilder/ConfigTests.cs @@ -1,6 +1,5 @@ using System.Configuration; using NUnit.Framework; -using Umbraco.Configuration; using Umbraco.Configuration.Legacy; using Umbraco.Core; using Umbraco.Core.Configuration; diff --git a/src/Umbraco.Tests/ModelsBuilder/StringExtensions.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Tests.Common/ModelsBuilder/StringExtensions.cs similarity index 100% rename from src/Umbraco.Tests/ModelsBuilder/StringExtensions.cs rename to src/Umbraco.Tests.UnitTests/Umbraco.Tests.Common/ModelsBuilder/StringExtensions.cs diff --git a/src/Umbraco.Tests/ModelsBuilder/UmbracoApplicationTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Tests.Common/ModelsBuilder/UmbracoApplicationTests.cs similarity index 100% rename from src/Umbraco.Tests/ModelsBuilder/UmbracoApplicationTests.cs rename to src/Umbraco.Tests.UnitTests/Umbraco.Tests.Common/ModelsBuilder/UmbracoApplicationTests.cs diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj b/src/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj index 2cbf1549dd..6569a49a27 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj @@ -7,6 +7,7 @@ + diff --git a/src/Umbraco.Tests/PropertyEditors/BlockEditorComponentTests.cs b/src/Umbraco.Tests/PropertyEditors/BlockEditorComponentTests.cs new file mode 100644 index 0000000000..bfd8b8c77b --- /dev/null +++ b/src/Umbraco.Tests/PropertyEditors/BlockEditorComponentTests.cs @@ -0,0 +1,261 @@ +using Newtonsoft.Json; +using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.Linq; +using Umbraco.Core; +using Umbraco.Web.Compose; + +namespace Umbraco.Tests.PropertyEditors +{ + [TestFixture] + public class BlockEditorComponentTests + { + private readonly JsonSerializerSettings _serializerSettings = new JsonSerializerSettings + { + Formatting = Formatting.None, + NullValueHandling = NullValueHandling.Ignore, + + }; + + private const string _contentGuid1 = "036ce82586a64dfba2d523a99ed80f58"; + private const string _contentGuid2 = "48288c21a38a40ef82deb3eda90a58f6"; + private const string _settingsGuid1 = "ffd35c4e2eea4900abfa5611b67b2492"; + private const string _subContentGuid1 = "4c44ce6b3a5c4f5f8f15e3dc24819a9e"; + private const string _subContentGuid2 = "a062c06d6b0b44ac892b35d90309c7f8"; + private const string _subSettingsGuid1 = "4d998d980ffa4eee8afdc23c4abd6d29"; + + [Test] + public void Cannot_Have_Null_Udi() + { + var component = new BlockEditorComponent(); + var json = GetBlockListJson(null, string.Empty); + Assert.Throws(() => component.ReplaceBlockListUdis(json)); + } + + [Test] + public void No_Nesting() + { + var guids = Enumerable.Range(0, 3).Select(x => Guid.NewGuid()).ToList(); + var guidCounter = 0; + Func guidFactory = () => guids[guidCounter++]; + + var json = GetBlockListJson(null); + + var expected = ReplaceGuids(json, guids, _contentGuid1, _contentGuid2, _settingsGuid1); + + var component = new BlockEditorComponent(); + var result = component.ReplaceBlockListUdis(json, guidFactory); + + var expectedJson = JsonConvert.SerializeObject(JsonConvert.DeserializeObject(expected, _serializerSettings), _serializerSettings); + var resultJson = JsonConvert.SerializeObject(JsonConvert.DeserializeObject(result, _serializerSettings), _serializerSettings); + Console.WriteLine(expectedJson); + Console.WriteLine(resultJson); + Assert.AreEqual(expectedJson, resultJson); + } + + [Test] + public void One_Level_Nesting_Escaped() + { + var guids = Enumerable.Range(0, 6).Select(x => Guid.NewGuid()).ToList(); + + var guidCounter = 0; + Func guidFactory = () => guids[guidCounter++]; + + var innerJson = GetBlockListJson(null, _subContentGuid1, _subContentGuid2, _subSettingsGuid1); + + // we need to ensure the escaped json is consistent with how it will be re-escaped after parsing + // and this is how to do that, the result will also include quotes around it. + var innerJsonEscaped = JsonConvert.ToString(innerJson); + + // get the json with the subFeatures as escaped + var json = GetBlockListJson(innerJsonEscaped); + + var component = new BlockEditorComponent(); + var result = component.ReplaceBlockListUdis(json, guidFactory); + + // the expected result is that the subFeatures data is no longer escaped + var expected = ReplaceGuids(GetBlockListJson(innerJson), guids, + _contentGuid1, _contentGuid2, _settingsGuid1, + _subContentGuid1, _subContentGuid2, _subSettingsGuid1); + + var expectedJson = JsonConvert.SerializeObject(JsonConvert.DeserializeObject(expected, _serializerSettings), _serializerSettings); + var resultJson = JsonConvert.SerializeObject(JsonConvert.DeserializeObject(result, _serializerSettings), _serializerSettings); + Console.WriteLine(expectedJson); + Console.WriteLine(resultJson); + Assert.AreEqual(expectedJson, resultJson); + } + + [Test] + public void One_Level_Nesting_Unescaped() + { + var guids = Enumerable.Range(0, 6).Select(x => Guid.NewGuid()).ToList(); + var guidCounter = 0; + Func guidFactory = () => guids[guidCounter++]; + + // nested blocks without property value escaping used in the conversion + var innerJson = GetBlockListJson(null, _subContentGuid1, _subContentGuid2, _subSettingsGuid1); + + // get the json with the subFeatures as unescaped + var json = GetBlockListJson(innerJson); + + var expected = ReplaceGuids(GetBlockListJson(innerJson), guids, + _contentGuid1, _contentGuid2, _settingsGuid1, + _subContentGuid1, _subContentGuid2, _subSettingsGuid1); + + var component = new BlockEditorComponent(); + var result = component.ReplaceBlockListUdis(json, guidFactory); + + var expectedJson = JsonConvert.SerializeObject(JsonConvert.DeserializeObject(expected, _serializerSettings), _serializerSettings); + var resultJson = JsonConvert.SerializeObject(JsonConvert.DeserializeObject(result, _serializerSettings), _serializerSettings); + Console.WriteLine(expectedJson); + Console.WriteLine(resultJson); + Assert.AreEqual(expectedJson, resultJson); + } + + [Test] + public void Nested_In_Complex_Editor_Escaped() + { + var guids = Enumerable.Range(0, 6).Select(x => Guid.NewGuid()).ToList(); + var guidCounter = 0; + Func guidFactory = () => guids[guidCounter++]; + + var innerJson = GetBlockListJson(null, _subContentGuid1, _subContentGuid2, _subSettingsGuid1); + + // we need to ensure the escaped json is consistent with how it will be re-escaped after parsing + // and this is how to do that, the result will also include quotes around it. + var innerJsonEscaped = JsonConvert.ToString(innerJson); + + // Complex editor such as the grid + var complexEditorJsonEscaped = GetGridJson(innerJsonEscaped); + + var json = GetBlockListJson(complexEditorJsonEscaped); + + var component = new BlockEditorComponent(); + var result = component.ReplaceBlockListUdis(json, guidFactory); + + // the expected result is that the subFeatures data is no longer escaped + var expected = ReplaceGuids(GetBlockListJson(GetGridJson(innerJson)), guids, + _contentGuid1, _contentGuid2, _settingsGuid1, + _subContentGuid1, _subContentGuid2, _subSettingsGuid1); + + var expectedJson = JsonConvert.SerializeObject(JsonConvert.DeserializeObject(expected, _serializerSettings), _serializerSettings); + var resultJson = JsonConvert.SerializeObject(JsonConvert.DeserializeObject(result, _serializerSettings), _serializerSettings); + Console.WriteLine(expectedJson); + Console.WriteLine(resultJson); + Assert.AreEqual(expectedJson, resultJson); + } + + private string GetBlockListJson(string subFeatures, + string contentGuid1 = _contentGuid1, + string contentGuid2 = _contentGuid2, + string settingsGuid1 = _settingsGuid1) + { + return @"{ + ""layout"": + { + ""Umbraco.BlockList"": [ + { + ""contentUdi"": """ + (contentGuid1.IsNullOrWhiteSpace() ? string.Empty : GuidUdi.Create(Constants.UdiEntityType.Element, Guid.Parse(contentGuid1)).ToString()) + @""" + }, + { + ""contentUdi"": ""umb://element/" + contentGuid2 + @""", + ""settingsUdi"": ""umb://element/" + settingsGuid1 + @""" + } + ] + }, + ""contentData"": [ + { + ""contentTypeKey"": ""d6ce4a86-91a2-45b3-a99c-8691fc1fb020"", + ""udi"": """ + (contentGuid1.IsNullOrWhiteSpace() ? string.Empty : GuidUdi.Create(Constants.UdiEntityType.Element, Guid.Parse(contentGuid1)).ToString()) + @""", + ""featureName"": ""Hello"", + ""featureDetails"": ""World"" + }, + { + ""contentTypeKey"": ""d6ce4a86-91a2-45b3-a99c-8691fc1fb020"", + ""udi"": ""umb://element/" + contentGuid2 + @""", + ""featureName"": ""Another"", + ""featureDetails"": ""Feature""" + (subFeatures == null ? string.Empty : (@", ""subFeatures"": " + subFeatures)) + @" + } + ], + ""settingsData"": [ + { + ""contentTypeKey"": ""d6ce4a86-91a2-45b3-a99c-8691fc1fb020"", + ""udi"": ""umb://element/" + settingsGuid1 + @""", + ""featureName"": ""Setting 1"", + ""featureDetails"": ""Setting 2"" + }, + ] +}"; + } + + private string GetGridJson(string subBlockList) + { + return @"{ + ""name"": ""1 column layout"", + ""sections"": [ + { + ""grid"": ""12"", + ""rows"": [ + { + ""name"": ""Article"", + ""id"": ""b4f6f651-0de3-ef46-e66a-464f4aaa9c57"", + ""areas"": [ + { + ""grid"": ""4"", + ""controls"": [ + { + ""value"": ""I am quote"", + ""editor"": { + ""alias"": ""quote"", + ""view"": ""textstring"" + }, + ""styles"": null, + ""config"": null + }], + ""styles"": null, + ""config"": null + }, + { + ""grid"": ""8"", + ""controls"": [ + { + ""value"": ""Header"", + ""editor"": { + ""alias"": ""headline"", + ""view"": ""textstring"" + }, + ""styles"": null, + ""config"": null + }, + { + ""value"": " + subBlockList + @", + ""editor"": { + ""alias"": ""madeUpNestedContent"", + ""view"": ""madeUpNestedContentInGrid"" + }, + ""styles"": null, + ""config"": null + }], + ""styles"": null, + ""config"": null + }], + ""styles"": null, + ""config"": null + }] + }] +}"; + } + + private string ReplaceGuids(string json, List newGuids, params string[] oldGuids) + { + for (var i = 0; i < oldGuids.Length; i++) + { + var old = oldGuids[i]; + json = json.Replace(old, newGuids[i].ToString("N")); + } + return json; + } + + } +} diff --git a/src/Umbraco.Tests/PropertyEditors/NestedContentPropertyComponentTests.cs b/src/Umbraco.Tests/PropertyEditors/NestedContentPropertyComponentTests.cs index 1b83c048d2..5b7e220123 100644 --- a/src/Umbraco.Tests/PropertyEditors/NestedContentPropertyComponentTests.cs +++ b/src/Umbraco.Tests/PropertyEditors/NestedContentPropertyComponentTests.cs @@ -9,6 +9,7 @@ using Umbraco.Web.Compose; namespace Umbraco.Tests.PropertyEditors { + [TestFixture] public class NestedContentPropertyComponentTests { diff --git a/src/Umbraco.Tests/Published/ConvertersTests.cs b/src/Umbraco.Tests/Published/ConvertersTests.cs index 2ae4c238cc..5f1fd7c3fd 100644 --- a/src/Umbraco.Tests/Published/ConvertersTests.cs +++ b/src/Umbraco.Tests/Published/ConvertersTests.cs @@ -185,14 +185,14 @@ namespace Umbraco.Tests.Published var composition = new Composition(register, TestHelper.GetMockedTypeLoader(), Mock.Of(), ComponentTests.MockRuntimeState(RuntimeLevel.Run), TestHelper.IOHelper, AppCaches.NoCache); composition.WithCollectionBuilder() - .Append() + .Append() .Append(); IPublishedModelFactory factory = new PublishedModelFactory(new[] { typeof (PublishedSnapshotTestObjects.TestElementModel1), typeof (PublishedSnapshotTestObjects.TestElementModel2), typeof (PublishedSnapshotTestObjects.TestContentModel1), typeof (PublishedSnapshotTestObjects.TestContentModel2), - }); + }, Mock.Of()); register.Register(f => factory); var registerFactory = composition.CreateFactory(); diff --git a/src/Umbraco.Tests/Published/PublishedSnapshotTestObjects.cs b/src/Umbraco.Tests/Published/PublishedSnapshotTestObjects.cs index fcb462e5c5..0ec8c9cc4e 100644 --- a/src/Umbraco.Tests/Published/PublishedSnapshotTestObjects.cs +++ b/src/Umbraco.Tests/Published/PublishedSnapshotTestObjects.cs @@ -11,7 +11,7 @@ namespace Umbraco.Tests.Published [PublishedModel("element1")] public class TestElementModel1 : PublishedElementModel { - public TestElementModel1(IPublishedElement content) + public TestElementModel1(IPublishedElement content, IPublishedValueFallback fallback) : base(content) { } @@ -21,7 +21,7 @@ namespace Umbraco.Tests.Published [PublishedModel("element2")] public class TestElementModel2 : PublishedElementModel { - public TestElementModel2(IPublishedElement content) + public TestElementModel2(IPublishedElement content, IPublishedValueFallback fallback) : base(content) { } @@ -31,7 +31,7 @@ namespace Umbraco.Tests.Published [PublishedModel("content1")] public class TestContentModel1 : PublishedContentModel { - public TestContentModel1(IPublishedContent content) + public TestContentModel1(IPublishedContent content, IPublishedValueFallback fallback) : base(content) { } @@ -41,7 +41,7 @@ namespace Umbraco.Tests.Published [PublishedModel("content2")] public class TestContentModel2 : PublishedContentModel { - public TestContentModel2(IPublishedContent content) + public TestContentModel2(IPublishedContent content, IPublishedValueFallback fallback) : base(content) { } diff --git a/src/Umbraco.Tests/PublishedContent/PublishedContentSnapshotTestBase.cs b/src/Umbraco.Tests/PublishedContent/PublishedContentSnapshotTestBase.cs index 5af889bcc0..3d9b4af526 100644 --- a/src/Umbraco.Tests/PublishedContent/PublishedContentSnapshotTestBase.cs +++ b/src/Umbraco.Tests/PublishedContent/PublishedContentSnapshotTestBase.cs @@ -43,7 +43,7 @@ namespace Umbraco.Tests.PublishedContent { base.Compose(); - Composition.RegisterUnique(f => new PublishedModelFactory(f.GetInstance().GetTypes())); + Composition.RegisterUnique(f => new PublishedModelFactory(f.GetInstance().GetTypes(), f.GetInstance())); } protected override TypeLoader CreateTypeLoader(IIOHelper ioHelper, ITypeFinder typeFinder, IAppPolicyCache runtimeCache,IProfilingLogger logger, IHostingEnvironment hostingEnvironment) diff --git a/src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs b/src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs index 81b9318c57..53461d4b8f 100644 --- a/src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs +++ b/src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs @@ -42,7 +42,7 @@ namespace Umbraco.Tests.PublishedContent _publishedSnapshotAccessorMock = new Mock(); Composition.RegisterUnique(_publishedSnapshotAccessorMock.Object); - Composition.RegisterUnique(f => new PublishedModelFactory(f.GetInstance().GetTypes())); + Composition.RegisterUnique(f => new PublishedModelFactory(f.GetInstance().GetTypes(), f.GetInstance())); Composition.RegisterUnique(); Composition.RegisterUnique(); @@ -241,7 +241,7 @@ namespace Umbraco.Tests.PublishedContent [PublishedModel("Home")] internal class Home : PublishedContentModel { - public Home(IPublishedContent content) + public Home(IPublishedContent content, IPublishedValueFallback fallback) : base(content) {} } @@ -249,7 +249,7 @@ namespace Umbraco.Tests.PublishedContent [PublishedModel("anything")] internal class Anything : PublishedContentModel { - public Anything(IPublishedContent content) + public Anything(IPublishedContent content, IPublishedValueFallback fallback) : base(content) { } } diff --git a/src/Umbraco.Tests/PublishedContent/SolidPublishedSnapshot.cs b/src/Umbraco.Tests/PublishedContent/SolidPublishedSnapshot.cs index afe4596f54..5ce63874bc 100644 --- a/src/Umbraco.Tests/PublishedContent/SolidPublishedSnapshot.cs +++ b/src/Umbraco.Tests/PublishedContent/SolidPublishedSnapshot.cs @@ -360,7 +360,7 @@ namespace Umbraco.Tests.PublishedContent { #region Plumbing - public ContentType2(IPublishedContent content) + public ContentType2(IPublishedContent content, IPublishedValueFallback fallback) : base(content) { } @@ -374,8 +374,8 @@ namespace Umbraco.Tests.PublishedContent { #region Plumbing - public ContentType2Sub(IPublishedContent content) - : base(content) + public ContentType2Sub(IPublishedContent content, IPublishedValueFallback fallback) + : base(content, fallback) { } #endregion @@ -383,7 +383,7 @@ namespace Umbraco.Tests.PublishedContent internal class PublishedContentStrong1 : PublishedContentModel { - public PublishedContentStrong1(IPublishedContent content) + public PublishedContentStrong1(IPublishedContent content, IPublishedValueFallback fallback) : base(content) { } @@ -392,8 +392,8 @@ namespace Umbraco.Tests.PublishedContent internal class PublishedContentStrong1Sub : PublishedContentStrong1 { - public PublishedContentStrong1Sub(IPublishedContent content) - : base(content) + public PublishedContentStrong1Sub(IPublishedContent content, IPublishedValueFallback fallback) + : base(content, fallback) { } public int AnotherValue => this.Value("anotherValue"); @@ -401,7 +401,7 @@ namespace Umbraco.Tests.PublishedContent internal class PublishedContentStrong2 : PublishedContentModel { - public PublishedContentStrong2(IPublishedContent content) + public PublishedContentStrong2(IPublishedContent content, IPublishedValueFallback fallback) : base(content) { } diff --git a/src/Umbraco.Tests/Templates/ViewHelperTests.cs b/src/Umbraco.Tests/Templates/ViewHelperTests.cs index ca304d5cda..9f6da6c34a 100644 --- a/src/Umbraco.Tests/Templates/ViewHelperTests.cs +++ b/src/Umbraco.Tests/Templates/ViewHelperTests.cs @@ -10,7 +10,8 @@ namespace Umbraco.Tests.Templates public void NoOptions() { var view = ViewHelper.GetDefaultFileContent(); - Assert.AreEqual(FixView(@"@inherits Umbraco.Web.Common.AspNetCore.UmbracoViewPage + Assert.AreEqual(FixView(@"@using Umbraco.Web.PublishedModels; +@inherits Umbraco.Web.Common.AspNetCore.UmbracoViewPage @{ Layout = null; }"), FixView(view)); @@ -20,7 +21,8 @@ namespace Umbraco.Tests.Templates public void Layout() { var view = ViewHelper.GetDefaultFileContent(layoutPageAlias: "Dharznoik"); - Assert.AreEqual(FixView(@"@inherits Umbraco.Web.Common.AspNetCore.UmbracoViewPage + Assert.AreEqual(FixView(@"@using Umbraco.Web.PublishedModels; +@inherits Umbraco.Web.Common.AspNetCore.UmbracoViewPage @{ Layout = ""Dharznoik.cshtml""; }"), FixView(view)); @@ -30,7 +32,8 @@ namespace Umbraco.Tests.Templates public void ClassName() { var view = ViewHelper.GetDefaultFileContent(modelClassName: "ClassName"); - Assert.AreEqual(FixView(@"@inherits Umbraco.Web.Common.AspNetCore.UmbracoViewPage + Assert.AreEqual(FixView(@"@using Umbraco.Web.PublishedModels; +@inherits Umbraco.Web.Common.AspNetCore.UmbracoViewPage @{ Layout = null; }"), FixView(view)); @@ -40,7 +43,8 @@ namespace Umbraco.Tests.Templates public void Namespace() { var view = ViewHelper.GetDefaultFileContent(modelNamespace: "Models"); - Assert.AreEqual(FixView(@"@inherits Umbraco.Web.Common.AspNetCore.UmbracoViewPage + Assert.AreEqual(FixView(@"@using Umbraco.Web.PublishedModels; +@inherits Umbraco.Web.Common.AspNetCore.UmbracoViewPage @{ Layout = null; }"), FixView(view)); @@ -50,7 +54,8 @@ namespace Umbraco.Tests.Templates public void ClassNameAndNamespace() { var view = ViewHelper.GetDefaultFileContent(modelClassName: "ClassName", modelNamespace: "My.Models"); - Assert.AreEqual(FixView(@"@inherits Umbraco.Web.Common.AspNetCore.UmbracoViewPage + Assert.AreEqual(FixView(@"@using Umbraco.Web.PublishedModels; +@inherits Umbraco.Web.Common.AspNetCore.UmbracoViewPage @using ContentModels = My.Models; @{ Layout = null; @@ -61,7 +66,8 @@ namespace Umbraco.Tests.Templates public void ClassNameAndNamespaceAndAlias() { var view = ViewHelper.GetDefaultFileContent(modelClassName: "ClassName", modelNamespace: "My.Models", modelNamespaceAlias: "MyModels"); - Assert.AreEqual(FixView(@"@inherits Umbraco.Web.Common.AspNetCore.UmbracoViewPage + Assert.AreEqual(FixView(@"@using Umbraco.Web.PublishedModels; +@inherits Umbraco.Web.Common.AspNetCore.UmbracoViewPage @using MyModels = My.Models; @{ Layout = null; @@ -72,7 +78,8 @@ namespace Umbraco.Tests.Templates public void Combined() { var view = ViewHelper.GetDefaultFileContent(layoutPageAlias: "Dharznoik", modelClassName: "ClassName", modelNamespace: "My.Models", modelNamespaceAlias: "MyModels"); - Assert.AreEqual(FixView(@"@inherits Umbraco.Web.Common.AspNetCore.UmbracoViewPage + Assert.AreEqual(FixView(@"@using Umbraco.Web.PublishedModels; +@inherits Umbraco.Web.Common.AspNetCore.UmbracoViewPage @using MyModels = My.Models; @{ Layout = ""Dharznoik.cshtml""; diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index 2538e6918f..c210cb2ffa 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -130,10 +130,6 @@ - - - - @@ -149,6 +145,7 @@ + @@ -476,10 +473,6 @@ {3ae7bf57-966b-45a5-910a-954d7c554441} Umbraco.Infrastructure - - {52ac0ba8-a60e-4e36-897b-e8b97a54ed1c} - Umbraco.ModelsBuilder.Embedded - {33085570-9bf2-4065-a9b0-a29d920d13ba} Umbraco.Persistance.SqlCe diff --git a/src/Umbraco.Web.BackOffice/Controllers/DictionaryController.cs b/src/Umbraco.Web.BackOffice/Controllers/DictionaryController.cs index 68c22726e0..2882c28f4f 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/DictionaryController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/DictionaryController.cs @@ -143,7 +143,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 f9d1463e26..6f1b7fb037 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/ImagesController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/ImagesController.cs @@ -103,9 +103,6 @@ namespace Umbraco.Web.BackOffice.Controllers /// /// /// - /// - /// - /// /// /// /// If there is no media, image property or image file is found then this will return not found. @@ -116,7 +113,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/RedirectUrlManagementController.cs b/src/Umbraco.Web.BackOffice/Controllers/RedirectUrlManagementController.cs index 9e8358c4b1..7a4b18e807 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/RedirectUrlManagementController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/RedirectUrlManagementController.cs @@ -12,6 +12,7 @@ using Umbraco.Core.Mapping; using Umbraco.Core.Services; using Umbraco.Web.Common.Attributes; using Umbraco.Web.Security; +using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.Models; using Microsoft.Extensions.Options; @@ -26,13 +27,15 @@ namespace Umbraco.Web.BackOffice.Controllers private readonly IRedirectUrlService _redirectUrlService; private readonly UmbracoMapper _umbracoMapper; private readonly IHostingEnvironment _hostingEnvironment; + private readonly IConfigManipulator _configManipulator; public RedirectUrlManagementController(ILogger logger, IOptions webRoutingSettings, IWebSecurity webSecurity, IRedirectUrlService redirectUrlService, UmbracoMapper umbracoMapper, - IHostingEnvironment hostingEnvironment) + IHostingEnvironment hostingEnvironment, + IConfigManipulator configManipulator) { _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _webRoutingSettings = webRoutingSettings.Value ?? throw new ArgumentNullException(nameof(webRoutingSettings)); @@ -40,6 +43,7 @@ namespace Umbraco.Web.BackOffice.Controllers _redirectUrlService = redirectUrlService ?? throw new ArgumentNullException(nameof(redirectUrlService)); _umbracoMapper = umbracoMapper ?? throw new ArgumentNullException(nameof(umbracoMapper)); _hostingEnvironment = hostingEnvironment ?? throw new ArgumentNullException(nameof(hostingEnvironment)); + _configManipulator = configManipulator ?? throw new ArgumentNullException(nameof(configManipulator)); } /// @@ -113,23 +117,10 @@ namespace Umbraco.Web.BackOffice.Controllers _logger.Debug(errorMessage); throw new SecurityException(errorMessage); } - var configFilePath =_hostingEnvironment.MapPathContentRoot("~/config/umbracoSettings.config"); var action = disable ? "disable" : "enable"; - if (System.IO.File.Exists(configFilePath) == false) - return BadRequest($"Couldn't {action} URL Tracker, the umbracoSettings.config file does not exist."); - - var umbracoConfig = new XmlDocument { PreserveWhitespace = true }; - umbracoConfig.Load(configFilePath); - - var webRoutingElement = umbracoConfig.SelectSingleNode("//web.routing") as XmlElement; - if (webRoutingElement == null) - return BadRequest($"Couldn't {action} URL Tracker, the web.routing element was not found in umbracoSettings.config."); - - // note: this adds the attribute if it does not exist - webRoutingElement.SetAttribute("disableRedirectUrlTracking", disable.ToString().ToLowerInvariant()); - umbracoConfig.Save(configFilePath); + _configManipulator.SaveDisableRedirectUrlTracking(disable); return Ok($"URL tracker is now {action}d."); } 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.BackOffice/Trees/ContentTreeControllerBase.cs b/src/Umbraco.Web.BackOffice/Trees/ContentTreeControllerBase.cs index e458e8754b..8e05cb874e 100644 --- a/src/Umbraco.Web.BackOffice/Trees/ContentTreeControllerBase.cs +++ b/src/Umbraco.Web.BackOffice/Trees/ContentTreeControllerBase.cs @@ -64,7 +64,7 @@ namespace Umbraco.Web.Trees /// /// /// - public ActionResult GetTreeNode(string id, [ModelBinder(typeof(HttpQueryStringModelBinder))]FormCollection queryStrings) + public ActionResult GetTreeNode([FromRoute] string id, [ModelBinder(typeof(HttpQueryStringModelBinder))]FormCollection queryStrings) { int asInt; Guid asGuid = Guid.Empty; 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 @@
-
+