diff --git a/.github/CONTRIBUTION_GUIDELINES.md b/.github/CONTRIBUTION_GUIDELINES.md new file mode 100644 index 0000000000..7d2afb46bf --- /dev/null +++ b/.github/CONTRIBUTION_GUIDELINES.md @@ -0,0 +1,35 @@ +# Contributing to Umbraco CMS + +When you’re considering creating a pull request for Umbraco CMS, we will categorize them in two different sizes, small and large. + +The process for both sizes is very similar, as [explained in the contribution document](CONTRIBUTING.md#how-do-i-begin). + +## Small PRs +Bug fixes and small improvements - can be recognized by seeing a small number of changes and possibly a small number of new files. + +We’re usually able to handle small PRs pretty quickly. A community volunteer will do the initial review and flag it for Umbraco HQ as “community tested”. If everything looks good, it will be merged pretty quickly [as per the described process](REVIEW_PROCESS.md). + +### Up for grabs + +Umbraco HQ will regularly mark newly created issues on the issue tracker with the `Up for grabs` tag. This means that the proposed changes are wanted in Umbraco but the HQ does not have the time to make them at this time. We encourage anyone to pick them up and help out. + +If you do start working on something, make sure leave a small comment on the issue saying something like: "I'm working on this". That way other people stumbling upon the issue know they don't need to pick it up, someone already has. + +## Large PRs +New features and large refactorings - can be recognized by seeing a large number of changes, plenty of new files, updates to package manager files (NuGet’s packages.config, NPM’s packages.json, etc.). + +We would love to follow the same process for larger PRs but this is not always possible due to time limitations and priorities that need to be aligned. We don’t want to put up any barriers, but this document should set the correct expectations. + +Please make sure to describe your idea in an issue, it helps to put in mockup screenshots or videos. + +If the change makes sense for HQ to include in Umbraco CMS we will leave you some feedback on how we’d like to see it being implemented. + +If a larger pull request is encouraged by Umbraco HQ, the process will be similar to what is described in the [small PRs process](#small-prs) above, we strive to feedback within 14 days. Finalizing and merging the PR might take longer though as it will likely need to be picked up by the development team to make sure everything is in order. We’ll keep you posted on the progress. + +It is highly recommended that you speak to the HQ before making large, complex changes. + +### Pull request or package? + +If it doesn’t fit in CMS right now, we will likely encourage you to make it into a package instead. A package is a great way to check out popularity of a feature, learn how people use it, validate good usability and to fix bugs. + +Eventually, a package could "graduate" to be included in the CMS. diff --git a/.github/REVIEW_PROCESS.md b/.github/REVIEW_PROCESS.md new file mode 100644 index 0000000000..917d25b090 --- /dev/null +++ b/.github/REVIEW_PROCESS.md @@ -0,0 +1,25 @@ +# Review process + +You're an awesome person and have sent us your contribution in the form of a pull request! It's now time to relax for a bit and wait for our response. + +In order to set some expectations, here's what happens next. + +## Review process + +You will get an initial reply within 48 hours (workdays) to acknowledge that we’ve seen your PR and we’ll pick it up as soon as we can. + +You will get feedback within at most 14 days after opening the PR. You’ll most likely get feedback sooner though. Then there are a few possible outcomes: + +- Your proposed change is awesome! We merge it in and it will be included in the next minor release of Umbraco +- If the change is a high priority bug fix, we will cherry-pick it into the next patch release as well so that we can release it as soon as possible +- Your proposed change is awesome but needs a bit more work, we’ll give you feedback on the changes we’d like to see +- Your proposed change is awesome but.. not something we’re looking to include at this point. We’ll close your PR and the related issue (we’ll be nice about it!) + +## Are you still available? + +We understand you have other things to do and can't just drop everything to help us out. +So if we’re asking for your help to improve the PR we’ll wait for two weeks to give you a fair chance to make changes. We’ll ask for an update if we don’t hear back from you after that time. + +If we don’t hear back from you for 4 weeks, we’ll close the PR so that it doesn’t just hang around forever. You’re very welcome to re-open it once you have some more time to spend on it. + +There will be times that we really like your proposed changes and we’ll finish the final improvements we’d like to see ourselves. You still get the credits and your commits will live on in the git repository. \ No newline at end of file diff --git a/build/NuSpecs/tools/Readme.txt b/build/NuSpecs/tools/Readme.txt index e40b0dbc7e..b0583a2b4d 100644 --- a/build/NuSpecs/tools/Readme.txt +++ b/build/NuSpecs/tools/Readme.txt @@ -1,11 +1,11 @@ - 888 - 888 -888 888 88888b.d88b. 88888b. 888d888 8888b. .d8888b .d88b. -888 888 888 "888 "88b 888 "88b 888P" "88b d88P" d88""88b -888 888 888 888 888 888 888 888 .d888888 888 888 888 -Y88b 888 888 888 888 888 d88P 888 888 888 Y88b. Y88..88P - "Y88888 888 888 888 88888P" 888 "Y888888 "Y8888P "Y88P" + 888 + 888 +888 888 88888b.d88b. 88888b. 888d888 8888b. .d8888b .d88b. +888 888 888 "888 "88b 888 "88b 888P" "88b d88P" d88""88b +888 888 888 888 888 888 888 888 .d888888 888 888 888 +Y88 88Y 888 888 888 888 d88P 888 888 888 Y88b. Y88..88P + "Y888P" 888 888 888 88888P" 888 "Y888888 "Y8888P "Y88P" ------------------------------------------------------------------ diff --git a/build/NuSpecs/tools/ReadmeUpgrade.txt b/build/NuSpecs/tools/ReadmeUpgrade.txt index df364c64ed..2f52d03776 100644 --- a/build/NuSpecs/tools/ReadmeUpgrade.txt +++ b/build/NuSpecs/tools/ReadmeUpgrade.txt @@ -1,12 +1,13 @@ - _ _ __ __ ____ _____ _____ ____ - | | | | \/ | _ \| __ \ /\ / ____/ __ \ - | | | | \ / | |_) | |__) | / \ | | | | | | - | | | | |\/| | _ <| _ / / /\ \| | | | | | - | |__| | | | | |_) | | \ \ / ____ | |___| |__| | - \____/|_| |_|____/|_| \_/_/ \_\_____\____/ + 888 + 888 +888 888 88888b.d88b. 88888b. 888d888 8888b. .d8888b .d88b. +888 888 888 "888 "88b 888 "88b 888P" "88b d88P" d88""88b +888 888 888 888 888 888 888 888 .d888888 888 888 888 +Y88 88Y 888 888 888 888 d88P 888 888 888 Y88b. Y88..88P + "Y888P" 888 888 888 88888P" 888 "Y888888 "Y8888P "Y88P" ----------------------------------------------------- +------------------------------------------------------------------ Don't forget to build! diff --git a/build/NuSpecs/tools/Web.config.install.xdt b/build/NuSpecs/tools/Web.config.install.xdt index 14778e0f10..f0bfb01585 100644 --- a/build/NuSpecs/tools/Web.config.install.xdt +++ b/build/NuSpecs/tools/Web.config.install.xdt @@ -128,6 +128,7 @@ + diff --git a/src/Umbraco.Core/Composing/CompositionExtensions/FileSystems.cs b/src/Umbraco.Core/Composing/CompositionExtensions/FileSystems.cs index 078a505be9..8518d907b5 100644 --- a/src/Umbraco.Core/Composing/CompositionExtensions/FileSystems.cs +++ b/src/Umbraco.Core/Composing/CompositionExtensions/FileSystems.cs @@ -90,7 +90,7 @@ namespace Umbraco.Core.Composing.CompositionExtensions // register the IFileSystem supporting the IMediaFileSystem // THIS IS THE ONLY THING THAT NEEDS TO CHANGE, IN ORDER TO REPLACE THE UNDERLYING FILESYSTEM // and, SupportingFileSystem.For() returns the underlying filesystem - composition.SetMediaFileSystem(() => new PhysicalFileSystem("~/media")); + composition.SetMediaFileSystem(() => new PhysicalFileSystem(SystemDirectories.Media)); return composition; } diff --git a/src/Umbraco.Core/Composing/CompositionExtensions/Services.cs b/src/Umbraco.Core/Composing/CompositionExtensions/Services.cs index 0baefe104b..d252c58730 100644 --- a/src/Umbraco.Core/Composing/CompositionExtensions/Services.cs +++ b/src/Umbraco.Core/Composing/CompositionExtensions/Services.cs @@ -96,7 +96,7 @@ namespace Umbraco.Core.Composing.CompositionExtensions var pluginLangFolders = appPlugins.Exists == false ? Enumerable.Empty() : appPlugins.GetDirectories() - .SelectMany(x => x.GetDirectories("Lang")) + .SelectMany(x => x.GetDirectories("Lang", SearchOption.AllDirectories)) .SelectMany(x => x.GetFiles("*.xml", SearchOption.TopDirectoryOnly)) .Select(x => new LocalizedTextServiceSupplementaryFileSource(x, false)); diff --git a/src/Umbraco.Core/Composing/Lifetime.cs b/src/Umbraco.Core/Composing/Lifetime.cs index 1a2cc3119a..012555be5e 100644 --- a/src/Umbraco.Core/Composing/Lifetime.cs +++ b/src/Umbraco.Core/Composing/Lifetime.cs @@ -12,31 +12,62 @@ /// or MS.DI, PerDependency in Autofac. Transient, + // TODO: We need to fix this up, currently LightInject is the only one that behaves differently from all other containers. + // ... the simple fix would be to map this to PerScopeLifetime in LI but need to wait on a response here https://github.com/seesharper/LightInject/issues/494#issuecomment-518942625 + // + // we use it for controllers, httpContextBase and other request scoped objects: MembershpHelper, TagQuery, UmbracoTreeSearcher and ISearchableTree + // - so that they are automatically disposed at the end of the scope (ie request) + // - not sure they should not be simply 'scoped'? + /// /// One unique instance per request. /// - // TODO: review lifetimes for LightInject vs other containers - // currently, corresponds to 'Request' in LightInject which is 'Transient + disposed by Scope' - // but NOT (in LightInject) a per-web-request lifetime, more a TransientScoped - // - // we use it for controllers, httpContextBase and umbracoContext - // - so that they are automatically disposed at the end of the scope (ie request) - // - not sure they should not be simply 'scoped'? - // - // Castle has an extra PerWebRequest something, and others use scope - // what about Request before first request ie during application startup? - // see http://blog.ploeh.dk/2009/11/17/UsingCastleWindsor'sPerWebRequestlifestylewithASP.NETMVConIIS7/ - // Castle ends up requiring a special scope manager too - // see https://groups.google.com/forum/#!topic/castle-project-users/1E2W9LVIYR4 - // - // but maybe also - why are we requiring scoped services at startup? + /// + /// + /// Any instance created with this lifetime will be disposed at the end of a request. + /// + /// Corresponds to + /// + /// PerRequestLifeTime in LightInject - means transient but disposed at the end of the current web request. + /// see: https://github.com/seesharper/LightInject/issues/494#issuecomment-518493262 + /// + /// + /// Scoped in MS.DI - means one per web request. + /// see https://docs.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection?view=aspnetcore-2.2#service-lifetimes + /// + /// InstancePerRequest in Autofac - means one per web request. + /// see https://autofaccn.readthedocs.io/en/latest/lifetime/instance-scope.html#instance-per-request + /// But "Behind the scenes, though, it’s still just instance per matching lifetime scope." + /// + /// + /// LifestylePerWebRequest in Castle Windsor - means one per web request. + /// see https://github.com/castleproject/Windsor/blob/master/docs/mvc-tutorial-part-7-lifestyles.md#the-perwebrequest-lifestyle + /// + /// Request, /// - /// One unique instance per container scope. + /// One unique instance per scope. /// - /// Corresponds to Scope in LightInject, Scoped in MS.DI - /// or Castle Windsor, PerLifetimeScope in Autofac. + /// + /// + /// Any instance created with this lifetime will be disposed at the end of the current scope. + /// + /// Corresponds to + /// PerScopeLifetime in LightInject (when in a request, means one per web request) + /// + /// Scoped in MS.DI (when in a request, means one per web request) + /// see https://docs.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection?view=aspnetcore-2.2#service-lifetimes + /// + /// InstancePerLifetimeScope in Autofac (when in a request, means one per web request) + /// see https://autofaccn.readthedocs.io/en/latest/lifetime/instance-scope.html#instance-per-lifetime-scope + /// Also note that Autofac's InstancePerRequest is the same as this, see https://autofaccn.readthedocs.io/en/latest/lifetime/instance-scope.html#instance-per-request + /// it says "Behind the scenes, though, it’s still just instance per matching lifetime scope." + /// + /// + /// LifestyleScoped in Castle Windsor + /// + /// Scope, /// diff --git a/src/Umbraco.Core/Constants-PropertyEditors.cs b/src/Umbraco.Core/Constants-PropertyEditors.cs index b48286f197..b55dc0ca18 100644 --- a/src/Umbraco.Core/Constants-PropertyEditors.cs +++ b/src/Umbraco.Core/Constants-PropertyEditors.cs @@ -96,6 +96,11 @@ namespace Umbraco.Core /// public const string MediaPicker = "Umbraco.MediaPicker"; + /// + /// Multiple Media Picker. + /// + public const string MultipleMediaPicker = "Umbraco.MultipleMediaPicker"; + /// /// Member Picker. /// @@ -203,6 +208,24 @@ namespace Umbraco.Core /// Must be a valid value. public const string DataValueType = "umbracoDataValueType"; } + + /// + /// Defines Umbraco's built-in property editor groups. + /// + public static class Groups + { + public const string Common = "Common"; + + public const string Lists = "Lists"; + + public const string Media = "Media"; + + public const string People = "People"; + + public const string Pickers = "Pickers"; + + public const string RichContent = "Rich Content"; + } } } } diff --git a/src/Umbraco.Core/DateTimeExtensions.cs b/src/Umbraco.Core/DateTimeExtensions.cs index 66b788f9c8..f665aaa8ed 100644 --- a/src/Umbraco.Core/DateTimeExtensions.cs +++ b/src/Umbraco.Core/DateTimeExtensions.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Text; @@ -15,7 +16,7 @@ namespace Umbraco.Core /// public static string ToIsoString(this DateTime dt) { - return dt.ToString("yyyy-MM-dd HH:mm:ss"); + return dt.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture); } public static DateTime TruncateTo(this DateTime dt, DateTruncate truncateTo) diff --git a/src/Umbraco.Core/Exceptions/PanicException.cs b/src/Umbraco.Core/Exceptions/PanicException.cs new file mode 100644 index 0000000000..4d41cafc65 --- /dev/null +++ b/src/Umbraco.Core/Exceptions/PanicException.cs @@ -0,0 +1,28 @@ +using System; +using System.Runtime.Serialization; + +namespace Umbraco.Core.Exceptions +{ + /// + /// Internal exception that in theory should never ben thrown, it is only thrown in circumstances that should never happen + /// + [Serializable] + internal class PanicException : Exception + { + public PanicException() + { + } + + public PanicException(string message) : base(message) + { + } + + public PanicException(string message, Exception innerException) : base(message, innerException) + { + } + + protected PanicException(SerializationInfo info, StreamingContext context) : base(info, context) + { + } + } +} diff --git a/src/Umbraco.Core/Logging/Viewer/ILogViewer.cs b/src/Umbraco.Core/Logging/Viewer/ILogViewer.cs index b39a3f38df..dbdd7842ba 100644 --- a/src/Umbraco.Core/Logging/Viewer/ILogViewer.cs +++ b/src/Umbraco.Core/Logging/Viewer/ILogViewer.cs @@ -42,6 +42,12 @@ namespace Umbraco.Core.Logging.Viewer bool CheckCanOpenLogs(LogTimePeriod logTimePeriod); + /// + /// Gets the current Serilog minimum log level + /// + /// + string GetLogLevel(); + /// /// Returns the collection of logs /// diff --git a/src/Umbraco.Core/Logging/Viewer/LogViewerSourceBase.cs b/src/Umbraco.Core/Logging/Viewer/LogViewerSourceBase.cs index acb2f5dbf9..607c20e601 100644 --- a/src/Umbraco.Core/Logging/Viewer/LogViewerSourceBase.cs +++ b/src/Umbraco.Core/Logging/Viewer/LogViewerSourceBase.cs @@ -1,11 +1,14 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Xml; using Newtonsoft.Json; +using Serilog; using Serilog.Events; using Umbraco.Core.IO; using Umbraco.Core.Models; using Umbraco.Core.Persistence.DatabaseModelDefinitions; +using Formatting = Newtonsoft.Json.Formatting; namespace Umbraco.Core.Logging.Viewer { @@ -89,6 +92,16 @@ namespace Umbraco.Core.Logging.Viewer return errorCounter.Count; } + /// + /// Get the Serilog minimum-level value from the config file. + /// + /// + public string GetLogLevel() + { + var logLevel = Enum.GetValues(typeof(LogEventLevel)).Cast().Where(Log.Logger.IsEnabled)?.Min() ?? null; + return logLevel?.ToString() ?? ""; + } + public LogLevelCounts GetLogLevelCounts(LogTimePeriod logTimePeriod) { var counter = new CountingFilter(); diff --git a/src/Umbraco.Core/Mapping/UmbracoMapper.cs b/src/Umbraco.Core/Mapping/UmbracoMapper.cs index 8915ebcf74..90f56f3ec1 100644 --- a/src/Umbraco.Core/Mapping/UmbracoMapper.cs +++ b/src/Umbraco.Core/Mapping/UmbracoMapper.cs @@ -3,6 +3,7 @@ using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; +using Umbraco.Core.Exceptions; namespace Umbraco.Core.Mapping { @@ -259,7 +260,7 @@ namespace Umbraco.Core.Mapping if (typeof(TTarget).IsArray) { var elementType = typeof(TTarget).GetElementType(); - if (elementType == null) throw new Exception("panic"); + if (elementType == null) throw new PanicException("elementType == null which should never occur"); var targetArray = Array.CreateInstance(elementType, targetList.Count); targetList.CopyTo(targetArray, 0); target = targetArray; @@ -342,7 +343,12 @@ namespace Umbraco.Core.Mapping if (ctor == null) return null; - _ctors[sourceType] = sourceCtor; + if (_ctors.ContainsKey(sourceType)) + foreach (var c in sourceCtor) + _ctors[sourceType].Add(c.Key, c.Value); + else + _ctors[sourceType] = sourceCtor; + return ctor; } @@ -367,7 +373,12 @@ namespace Umbraco.Core.Mapping if (map == null) return null; - _maps[sourceType] = sourceMap; + if (_maps.ContainsKey(sourceType)) + foreach(var m in sourceMap) + _maps[sourceType].Add(m.Key, m.Value); + else + _maps[sourceType] = sourceMap; + return map; } @@ -382,7 +393,7 @@ namespace Umbraco.Core.Mapping { if (type.IsArray) return type.GetElementType(); if (type.IsGenericType) return type.GenericTypeArguments[0]; - throw new Exception("panic"); + throw new PanicException($"Could not get enumerable or array type from {type}"); } /// diff --git a/src/Umbraco.Core/Migrations/Expressions/Delete/KeysAndIndexes/DeleteKeysAndIndexesBuilder.cs b/src/Umbraco.Core/Migrations/Expressions/Delete/KeysAndIndexes/DeleteKeysAndIndexesBuilder.cs index 9b13457b76..df74bf7c87 100644 --- a/src/Umbraco.Core/Migrations/Expressions/Delete/KeysAndIndexes/DeleteKeysAndIndexesBuilder.cs +++ b/src/Umbraco.Core/Migrations/Expressions/Delete/KeysAndIndexes/DeleteKeysAndIndexesBuilder.cs @@ -1,5 +1,7 @@ -using System.Linq; +using System.Collections.Generic; +using System.Linq; using NPoco; +using Umbraco.Core; using Umbraco.Core.Migrations.Expressions.Common; using Umbraco.Core.Persistence.SqlSyntax; @@ -27,31 +29,57 @@ namespace Umbraco.Core.Migrations.Expressions.Delete.KeysAndIndexes { _context.BuildingExpression = false; + //get a list of all constraints - this will include all PK, FK and unique constraints + var tableConstraints = _context.SqlContext.SqlSyntax.GetConstraintsPerTable(_context.Database).DistinctBy(x => x.Item2).ToList(); + + //get a list of defined indexes - this will include all indexes, unique indexes and unique constraint indexes + var indexes = _context.SqlContext.SqlSyntax.GetDefinedIndexesDefinitions(_context.Database).DistinctBy(x => x.IndexName).ToList(); + + var uniqueConstraintNames = tableConstraints.Where(x => !x.Item2.InvariantStartsWith("PK_") && !x.Item2.InvariantStartsWith("FK_")).Select(x => x.Item2); + var indexNames = indexes.Select(x => x.IndexName).ToList(); + // drop keys if (DeleteLocal || DeleteForeign) { // table, constraint - var tableKeys = _context.SqlContext.SqlSyntax.GetConstraintsPerTable(_context.Database).DistinctBy(x => x.Item2).ToList(); + if (DeleteForeign) { - foreach (var key in tableKeys.Where(x => x.Item1 == TableName && x.Item2.StartsWith("FK_"))) + //In some cases not all FK's are prefixed with "FK" :/ mostly with old upgraded databases so we need to check if it's either: + // * starts with FK OR + // * doesn't start with PK_ and doesn't exist in the list of indexes + + foreach (var key in tableConstraints.Where(x => x.Item1 == TableName + && (x.Item2.InvariantStartsWith("FK_") || (!x.Item2.InvariantStartsWith("PK_") && !indexNames.InvariantContains(x.Item2))))) + { Delete.ForeignKey(key.Item2).OnTable(key.Item1).Do(); + } + } if (DeleteLocal) { - foreach (var key in tableKeys.Where(x => x.Item1 == TableName && x.Item2.StartsWith("PK_"))) + foreach (var key in tableConstraints.Where(x => x.Item1 == TableName && x.Item2.InvariantStartsWith("PK_"))) Delete.PrimaryKey(key.Item2).FromTable(key.Item1).Do(); - // note: we do *not* delete the DEFAULT constraints + // note: we do *not* delete the DEFAULT constraints and if we wanted to we'd have to deal with that in interesting ways + // since SQL server has a specific way to handle that, see SqlServerSyntaxProvider.GetDefaultConstraintsPerColumn } } // drop indexes if (DeleteLocal) - { - var indexes = _context.SqlContext.SqlSyntax.GetDefinedIndexesDefinitions(_context.Database).DistinctBy(x => x.IndexName).ToList(); + { foreach (var index in indexes.Where(x => x.TableName == TableName)) - Delete.Index(index.IndexName).OnTable(index.TableName).Do(); + { + //if this is a unique constraint we need to drop the constraint, else drop the index + //to figure this out, the index must be tagged as unique and it must exist in the tableConstraints + + if (index.IsUnique && uniqueConstraintNames.InvariantContains(index.IndexName)) + Delete.UniqueConstraint(index.IndexName).FromTable(index.TableName).Do(); + else + Delete.Index(index.IndexName).OnTable(index.TableName).Do(); + } + } } diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/MarkdownEditorPreValueMigrator.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/MarkdownEditorPreValueMigrator.cs new file mode 100644 index 0000000000..87a1fd6504 --- /dev/null +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/MarkdownEditorPreValueMigrator.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0.DataTypes +{ + class MarkdownEditorPreValueMigrator : DefaultPreValueMigrator //PreValueMigratorBase + { + public override bool CanMigrate(string editorAlias) + => editorAlias == Constants.PropertyEditors.Aliases.MarkdownEditor; + + protected override object GetPreValueValue(PreValueDto preValue) + { + if (preValue.Alias == "preview") + return preValue.Value == "1"; + + return base.GetPreValueValue(preValue); + } + } +} diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/PreValueMigratorComposer.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/PreValueMigratorComposer.cs index 8dfa464508..db9021d653 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/PreValueMigratorComposer.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/PreValueMigratorComposer.cs @@ -20,7 +20,8 @@ public class PreValueMigratorComposer : ICoreComposer .Append() .Append() .Append() - .Append(); + .Append() + .Append(); } } } diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/RenamingPreValueMigrator.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/RenamingPreValueMigrator.cs index c04e7c8fda..89a71fdaf4 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/RenamingPreValueMigrator.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/RenamingPreValueMigrator.cs @@ -1,5 +1,6 @@ using System; using System.Linq; +using Umbraco.Core.Exceptions; namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0.DataTypes { @@ -20,7 +21,7 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0.DataTypes case "Umbraco.NoEdit": return Constants.PropertyEditors.Aliases.Label; default: - throw new Exception("panic"); + throw new PanicException($"The alias {editorAlias} is not supported"); } } } diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/RefactorMacroColumns.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/RefactorMacroColumns.cs index bd08b53877..58ec0e30c2 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/RefactorMacroColumns.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/RefactorMacroColumns.cs @@ -15,21 +15,17 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 { //special trick to add the column without constraints and return the sql to add them later AddColumn("macroType", out var sqls1); - //now we need to update the new column with some values because this column doesn't allow NULL values - Update.Table(Constants.DatabaseSchema.Tables.Macro).Set(new { macroType = (int)MacroTypes.Unknown}).AllRows().Do(); - //now apply constraints (NOT NULL) to new table - foreach (var sql in sqls1) Execute.Sql(sql).Do(); - - //special trick to add the column without constraints and return the sql to add them later AddColumn("macroSource", out var sqls2); - //populate the new macroSource column with legacy data - Execute.Sql($"UPDATE {Constants.DatabaseSchema.Tables.Macro} SET macroSource = macroXSLT, macroType = {(int)MacroTypes.Unknown} WHERE macroXSLT IS NOT NULL").Do(); - Execute.Sql($"UPDATE {Constants.DatabaseSchema.Tables.Macro} SET macroSource = macroScriptAssembly, macroType = {(int)MacroTypes.Unknown} WHERE macroScriptAssembly IS NOT NULL").Do(); - Execute.Sql($"UPDATE {Constants.DatabaseSchema.Tables.Macro} SET macroSource = macroScriptType, macroType = {(int)MacroTypes.Unknown} WHERE macroScriptType IS NOT NULL").Do(); - Execute.Sql($"UPDATE {Constants.DatabaseSchema.Tables.Macro} SET macroSource = macroPython, macroType = {(int)MacroTypes.PartialView} WHERE macroPython IS NOT NULL").Do(); + //populate the new columns with legacy data + Execute.Sql($"UPDATE {Constants.DatabaseSchema.Tables.Macro} SET macroSource = '', macroType = {(int)MacroTypes.Unknown}").Do(); + Execute.Sql($"UPDATE {Constants.DatabaseSchema.Tables.Macro} SET macroSource = macroXSLT, macroType = {(int)MacroTypes.Unknown} WHERE macroXSLT != '' AND macroXSLT IS NOT NULL").Do(); + Execute.Sql($"UPDATE {Constants.DatabaseSchema.Tables.Macro} SET macroSource = macroScriptAssembly, macroType = {(int)MacroTypes.Unknown} WHERE macroScriptAssembly != '' AND macroScriptAssembly IS NOT NULL").Do(); + Execute.Sql($"UPDATE {Constants.DatabaseSchema.Tables.Macro} SET macroSource = macroScriptType, macroType = {(int)MacroTypes.Unknown} WHERE macroScriptType != '' AND macroScriptType IS NOT NULL").Do(); + Execute.Sql($"UPDATE {Constants.DatabaseSchema.Tables.Macro} SET macroSource = macroPython, macroType = {(int)MacroTypes.PartialView} WHERE macroPython != '' AND macroPython IS NOT NULL").Do(); //now apply constraints (NOT NULL) to new table + foreach (var sql in sqls1) Execute.Sql(sql).Do(); foreach (var sql in sqls2) Execute.Sql(sql).Do(); //now remove these old columns diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/SuperZero.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/SuperZero.cs index 64ac20d175..9026f15fc1 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/SuperZero.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/SuperZero.cs @@ -30,6 +30,7 @@ Database.Execute("set identity_insert umbracoUser off;"); Database.Execute("update umbracoUser2UserGroup set userId=-1 where userId=0;"); + Database.Execute("update umbracoUser2NodeNotify set userId=-1 where userId=0;"); Database.Execute("update umbracoNode set nodeUser=-1 where nodeUser=0;"); Database.Execute("update umbracoUserLogin set userId=-1 where userId=0;"); Database.Execute($"update {Constants.DatabaseSchema.Tables.ContentVersion} set userId=-1 where userId=0;"); diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_1_0/ConvertTinyMceAndGridMediaUrlsToLocalLink.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_1_0/ConvertTinyMceAndGridMediaUrlsToLocalLink.cs index 1b6597a660..b68aa23d78 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_1_0/ConvertTinyMceAndGridMediaUrlsToLocalLink.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_1_0/ConvertTinyMceAndGridMediaUrlsToLocalLink.cs @@ -50,10 +50,10 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_1_0 var obj = JsonConvert.DeserializeObject(value); var allControls = obj.SelectTokens("$.sections..rows..areas..controls"); - foreach (var control in allControls.SelectMany(c => c)) + foreach (var control in allControls.SelectMany(c => c).OfType()) { var controlValue = control["value"]; - if (controlValue.Type == JTokenType.String) + if (controlValue?.Type == JTokenType.String) { control["value"] = UpdateMediaUrls(mediaLinkPattern, controlValue.Value()); } diff --git a/src/Umbraco.Core/Models/ContentRepositoryExtensions.cs b/src/Umbraco.Core/Models/ContentRepositoryExtensions.cs index bf7228ca47..f9efc60142 100644 --- a/src/Umbraco.Core/Models/ContentRepositoryExtensions.cs +++ b/src/Umbraco.Core/Models/ContentRepositoryExtensions.cs @@ -222,7 +222,13 @@ namespace Umbraco.Core.Models return true; } - public static void UnpublishCulture(this IContent content, string culture = "*") + /// + /// Returns false if the culture is already unpublished + /// + /// + /// + /// + public static bool UnpublishCulture(this IContent content, string culture = "*") { culture = culture.NullOrWhiteSpaceAsNull(); @@ -230,16 +236,31 @@ namespace Umbraco.Core.Models if (!content.ContentType.SupportsPropertyVariation(culture, "*", true)) throw new NotSupportedException($"Culture \"{culture}\" is not supported by content type \"{content.ContentType.Alias}\" with variation \"{content.ContentType.Variations}\"."); - if (culture == "*") // all cultures + + var keepProcessing = true; + + if (culture == "*") + { + // all cultures content.ClearPublishInfos(); - else // one single culture - content.ClearPublishInfo(culture); + } + else + { + // one single culture + keepProcessing = content.ClearPublishInfo(culture); + } + - // property.PublishValues only publishes what is valid, variation-wise - foreach (var property in content.Properties) - property.UnpublishValues(culture); + if (keepProcessing) + { + // property.PublishValues only publishes what is valid, variation-wise + foreach (var property in content.Properties) + property.UnpublishValues(culture); - content.PublishedState = PublishedState.Publishing; + content.PublishedState = PublishedState.Publishing; + } + + return keepProcessing; } public static void ClearPublishInfos(this IContent content) @@ -247,15 +268,24 @@ namespace Umbraco.Core.Models content.PublishCultureInfos = null; } - public static void ClearPublishInfo(this IContent content, string culture) + /// + /// Returns false if the culture is already unpublished + /// + /// + /// + /// + public static bool ClearPublishInfo(this IContent content, string culture) { if (culture.IsNullOrWhiteSpace()) throw new ArgumentNullOrEmptyException(nameof(culture)); - content.PublishCultureInfos.Remove(culture); - - // set the culture to be dirty - it's been modified - content.TouchCulture(culture); + var removed = content.PublishCultureInfos.Remove(culture); + if (removed) + { + // set the culture to be dirty - it's been modified + content.TouchCulture(culture); + } + return removed; } /// diff --git a/src/Umbraco.Core/Models/IContentTypeBase.cs b/src/Umbraco.Core/Models/IContentTypeBase.cs index 5f1fe6ed49..ed87c5f320 100644 --- a/src/Umbraco.Core/Models/IContentTypeBase.cs +++ b/src/Umbraco.Core/Models/IContentTypeBase.cs @@ -101,7 +101,7 @@ namespace Umbraco.Core.Models PropertyGroupCollection PropertyGroups { get; set; } /// - /// Gets all local property types belonging to a group, across all local property groups. + /// Gets all local property types all local property groups or ungrouped. /// IEnumerable PropertyTypes { get; } diff --git a/src/Umbraco.Core/Models/PublishedContent/IPublishedContentTypeFactory.cs b/src/Umbraco.Core/Models/PublishedContent/IPublishedContentTypeFactory.cs index 816bfdbb01..89009ac7b8 100644 --- a/src/Umbraco.Core/Models/PublishedContent/IPublishedContentTypeFactory.cs +++ b/src/Umbraco.Core/Models/PublishedContent/IPublishedContentTypeFactory.cs @@ -30,6 +30,16 @@ /// Is used by constructor to create special property types. IPublishedPropertyType CreatePropertyType(IPublishedContentType contentType, string propertyTypeAlias, int dataTypeId, ContentVariation variations); + /// + /// Creates a core (non-user) published property type. + /// + /// The published content type owning the property. + /// The property type alias. + /// The datatype identifier. + /// The variations. + /// Is used by constructor to create special property types. + IPublishedPropertyType CreateCorePropertyType(IPublishedContentType contentType, string propertyTypeAlias, int dataTypeId, ContentVariation variations); + /// /// Gets a published datatype. /// diff --git a/src/Umbraco.Core/Models/PublishedContent/ModelType.cs b/src/Umbraco.Core/Models/PublishedContent/ModelType.cs index 540abda2c5..318ccc916e 100644 --- a/src/Umbraco.Core/Models/PublishedContent/ModelType.cs +++ b/src/Umbraco.Core/Models/PublishedContent/ModelType.cs @@ -75,7 +75,7 @@ namespace Umbraco.Core.Models.PublishedContent return type; var def = type.GetGenericTypeDefinition(); if (def == null) - throw new InvalidOperationException("panic"); + throw new PanicException($"The type {type} has not generic type definition"); var args = type.GetGenericArguments().Select(x => Map(x, modelTypes, true)).ToArray(); return def.MakeGenericType(args); @@ -114,7 +114,7 @@ namespace Umbraco.Core.Models.PublishedContent return type.FullName; var def = type.GetGenericTypeDefinition(); if (def == null) - throw new InvalidOperationException("panic"); + throw new PanicException($"The type {type} has not generic type definition"); var args = type.GetGenericArguments().Select(x => MapToName(x, map, true)).ToArray(); var defFullName = def.FullName.Substring(0, def.FullName.IndexOf('`')); diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedContentType.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedContentType.cs index 3b03cfc9ea..b11e991118 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedContentType.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedContentType.cs @@ -92,26 +92,26 @@ namespace Umbraco.Core.Models.PublishedContent { var aliases = new HashSet(propertyTypes.Select(x => x.Alias), StringComparer.OrdinalIgnoreCase); - foreach ((var alias, (var dataTypeId, var editorAlias)) in BuiltinMemberProperties) + foreach (var (alias, dataTypeId) in BuiltinMemberProperties) { if (aliases.Contains(alias)) continue; - propertyTypes.Add(factory.CreatePropertyType(this, alias, dataTypeId, ContentVariation.Nothing)); + propertyTypes.Add(factory.CreateCorePropertyType(this, alias, dataTypeId, ContentVariation.Nothing)); } } // TODO: this list somehow also exists in constants, see memberTypeRepository => remove duplicate! - private static readonly Dictionary BuiltinMemberProperties = new Dictionary + private static readonly Dictionary BuiltinMemberProperties = new Dictionary { - { "Email", (Constants.DataTypes.Textbox, Constants.PropertyEditors.Aliases.TextBox) }, - { "Username", (Constants.DataTypes.Textbox, Constants.PropertyEditors.Aliases.TextBox) }, - { "PasswordQuestion", (Constants.DataTypes.Textbox, Constants.PropertyEditors.Aliases.TextBox) }, - { "Comments", (Constants.DataTypes.Textbox, Constants.PropertyEditors.Aliases.TextBox) }, - { "IsApproved", (Constants.DataTypes.Boolean, Constants.PropertyEditors.Aliases.Boolean) }, - { "IsLockedOut", (Constants.DataTypes.Boolean, Constants.PropertyEditors.Aliases.Boolean) }, - { "LastLockoutDate", (Constants.DataTypes.DateTime, Constants.PropertyEditors.Aliases.DateTime) }, - { "CreateDate", (Constants.DataTypes.DateTime, Constants.PropertyEditors.Aliases.DateTime) }, - { "LastLoginDate", (Constants.DataTypes.DateTime, Constants.PropertyEditors.Aliases.DateTime) }, - { "LastPasswordChangeDate", (Constants.DataTypes.DateTime, Constants.PropertyEditors.Aliases.DateTime) }, + { "Email", Constants.DataTypes.Textbox }, + { "Username", Constants.DataTypes.Textbox }, + { "PasswordQuestion", Constants.DataTypes.Textbox }, + { "Comments", Constants.DataTypes.Textbox }, + { "IsApproved", Constants.DataTypes.Boolean }, + { "IsLockedOut", Constants.DataTypes.Boolean }, + { "LastLockoutDate", Constants.DataTypes.DateTime }, + { "CreateDate", Constants.DataTypes.DateTime }, + { "LastLoginDate", Constants.DataTypes.DateTime }, + { "LastPasswordChangeDate", Constants.DataTypes.DateTime }, }; #region Content type diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedContentTypeFactory.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedContentTypeFactory.cs index 17a15a2536..34094508c3 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedContentTypeFactory.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedContentTypeFactory.cs @@ -61,6 +61,12 @@ namespace Umbraco.Core.Models.PublishedContent return new PublishedPropertyType(contentType, propertyTypeAlias, dataTypeId, true, variations, _propertyValueConverters, _publishedModelFactory, this); } + /// + public IPublishedPropertyType CreateCorePropertyType(IPublishedContentType contentType, string propertyTypeAlias, int dataTypeId, ContentVariation variations = ContentVariation.Nothing) + { + return new PublishedPropertyType(contentType, propertyTypeAlias, dataTypeId, false, variations, _propertyValueConverters, _publishedModelFactory, this); + } + /// /// This method is for tests and is not intended to be used directly from application code. /// diff --git a/src/Umbraco.Core/Packaging/PackageDataInstallation.cs b/src/Umbraco.Core/Packaging/PackageDataInstallation.cs index c811f484bc..2f6b91edee 100644 --- a/src/Umbraco.Core/Packaging/PackageDataInstallation.cs +++ b/src/Umbraco.Core/Packaging/PackageDataInstallation.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; -using System.Text.RegularExpressions; using System.Web; using System.Xml.Linq; using System.Xml.XPath; @@ -575,12 +574,11 @@ namespace Umbraco.Core.Packaging contentType.Thumbnail = infoElement.Element("Thumbnail").Value; contentType.Description = infoElement.Element("Description").Value; - //NOTE AllowAtRoot is a new property in the package xml so we need to verify it exists before using it. + //NOTE AllowAtRoot, IsListView, IsElement and Variations are new properties in the package xml so we need to verify it exists before using it. var allowAtRoot = infoElement.Element("AllowAtRoot"); if (allowAtRoot != null) contentType.AllowedAsRoot = allowAtRoot.Value.InvariantEquals("true"); - //NOTE IsListView is a new property in the package xml so we need to verify it exists before using it. var isListView = infoElement.Element("IsListView"); if (isListView != null) contentType.IsContainer = isListView.Value.InvariantEquals("true"); @@ -589,6 +587,10 @@ namespace Umbraco.Core.Packaging if (isElement != null) contentType.IsElement = isElement.Value.InvariantEquals("true"); + var variationsElement = infoElement.Element("Variations"); + if (variationsElement != null) + contentType.Variations = (ContentVariation)Enum.Parse(typeof(ContentVariation), variationsElement.Value); + //Name of the master corresponds to the parent and we need to ensure that the Parent Id is set var masterElement = infoElement.Element("Master"); if (masterElement != null) @@ -614,7 +616,7 @@ namespace Umbraco.Core.Packaging var compositionContentType = importedContentTypes.ContainsKey(compositionAlias) ? importedContentTypes[compositionAlias] : _contentTypeService.Get(compositionAlias); - var added = contentType.AddContentType(compositionContentType); + contentType.AddContentType(compositionContentType); } } } @@ -748,9 +750,14 @@ namespace Umbraco.Core.Packaging { Name = property.Element("Name").Value, Description = (string)property.Element("Description"), - Mandatory = property.Element("Mandatory") != null ? property.Element("Mandatory").Value.ToLowerInvariant().Equals("true") : false, + Mandatory = property.Element("Mandatory") != null + ? property.Element("Mandatory").Value.ToLowerInvariant().Equals("true") + : false, ValidationRegExp = (string)property.Element("Validation"), - SortOrder = sortOrder + SortOrder = sortOrder, + Variations = property.Element("Variations") != null + ? (ContentVariation)Enum.Parse(typeof(ContentVariation), property.Element("Variations").Value) + : ContentVariation.Nothing }; var tab = (string)property.Element("Tab"); diff --git a/src/Umbraco.Core/Persistence/Factories/PropertyFactory.cs b/src/Umbraco.Core/Persistence/Factories/PropertyFactory.cs index f1473b5888..33dabe1b24 100644 --- a/src/Umbraco.Core/Persistence/Factories/PropertyFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/PropertyFactory.cs @@ -12,7 +12,7 @@ namespace Umbraco.Core.Persistence.Factories public static IEnumerable BuildEntities(PropertyType[] propertyTypes, IReadOnlyCollection dtos, int publishedVersionId, ILanguageRepository languageRepository) { var properties = new List(); - var xdtos = dtos.GroupBy(x => x.PropertyTypeId).ToDictionary(x => x.Key, x => (IEnumerable) x); + var xdtos = dtos.GroupBy(x => x.PropertyTypeId).ToDictionary(x => x.Key, x => (IEnumerable)x); foreach (var propertyType in propertyTypes) { @@ -104,10 +104,14 @@ namespace Umbraco.Core.Persistence.Factories /// The properties to map /// /// out parameter indicating that one or more properties have been edited - /// out parameter containing a collection of edited cultures when the contentVariation varies by culture + /// + /// Out parameter containing a collection of edited cultures when the contentVariation varies by culture. + /// The value of this will be used to populate the edited cultures in the umbracoDocumentCultureVariation table. + /// /// public static IEnumerable BuildDtos(ContentVariation contentVariation, int currentVersionId, int publishedVersionId, IEnumerable properties, - ILanguageRepository languageRepository, out bool edited, out HashSet editedCultures) + ILanguageRepository languageRepository, out bool edited, + out HashSet editedCultures) { var propertyDataDtos = new List(); edited = false; @@ -130,6 +134,9 @@ namespace Umbraco.Core.Persistence.Factories // publishing = deal with edit and published values foreach (var propertyValue in property.Values) { + var isInvariantValue = propertyValue.Culture == null; + var isCultureValue = propertyValue.Culture != null && propertyValue.Segment == null; + // deal with published value if (propertyValue.PublishedValue != null && publishedVersionId > 0) propertyDataDtos.Add(BuildDto(publishedVersionId, property, languageRepository.GetIdByIsoCode(propertyValue.Culture), propertyValue.Segment, propertyValue.PublishedValue)); @@ -138,26 +145,36 @@ namespace Umbraco.Core.Persistence.Factories if (propertyValue.EditedValue != null) propertyDataDtos.Add(BuildDto(currentVersionId, property, languageRepository.GetIdByIsoCode(propertyValue.Culture), propertyValue.Segment, propertyValue.EditedValue)); + // property.Values will contain ALL of it's values, both variant and invariant which will be populated if the + // administrator has previously changed the property type to be variant vs invariant. + // We need to check for this scenario here because otherwise the editedCultures and edited flags + // will end up incorrectly set in the umbracoDocumentCultureVariation table so here we need to + // only process edited cultures based on the current value type and how the property varies. + // The above logic will still persist the currently saved property value for each culture in case the admin + // decides to swap the property's variance again, in which case the edited flag will be recalculated. + + if (property.PropertyType.VariesByCulture() && isInvariantValue || !property.PropertyType.VariesByCulture() && isCultureValue) + continue; + // use explicit equals here, else object comparison fails at comparing eg strings var sameValues = propertyValue.PublishedValue == null ? propertyValue.EditedValue == null : propertyValue.PublishedValue.Equals(propertyValue.EditedValue); + edited |= !sameValues; - if (entityVariesByCulture // cultures can be edited, ie CultureNeutral is supported - && propertyValue.Culture != null && propertyValue.Segment == null // and value is CultureNeutral - && !sameValues) // and edited and published are different + if (entityVariesByCulture && !sameValues) { - editedCultures.Add(propertyValue.Culture); // report culture as edited - } + if (isCultureValue) + { + editedCultures.Add(propertyValue.Culture); // report culture as edited + } + else if (isInvariantValue) + { + // flag culture as edited if it contains an edited invariant property + if (defaultCulture == null) + defaultCulture = languageRepository.GetDefaultIsoCode(); - // flag culture as edited if it contains an edited invariant property - if (propertyValue.Culture == null //invariant property - && !sameValues // and edited and published are different - && entityVariesByCulture) //only when the entity is variant - { - if (defaultCulture == null) - defaultCulture = languageRepository.GetDefaultIsoCode(); - - editedCultures.Add(defaultCulture); + editedCultures.Add(defaultCulture); + } } } } @@ -167,7 +184,7 @@ namespace Umbraco.Core.Persistence.Factories { // not publishing = only deal with edit values if (propertyValue.EditedValue != null) - propertyDataDtos.Add(BuildDto(currentVersionId, property, languageRepository.GetIdByIsoCode(propertyValue.Culture), propertyValue.Segment, propertyValue.EditedValue)); + propertyDataDtos.Add(BuildDto(currentVersionId, property, languageRepository.GetIdByIsoCode(propertyValue.Culture), propertyValue.Segment, propertyValue.EditedValue)); } edited = true; } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/AuditRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/AuditRepository.cs index c25328b10c..0788594e3a 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/AuditRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/AuditRepository.cs @@ -174,7 +174,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement totalRecords = page.TotalItems; var items = page.Items.Select( - dto => new AuditItem(dto.Id, Enum.ParseOrNull(dto.Header) ?? AuditType.Custom, dto.UserId ?? Constants.Security.UnknownUserId, dto.EntityType, dto.Comment, dto.Parameters)).ToList(); + dto => new AuditItem(dto.NodeId, Enum.ParseOrNull(dto.Header) ?? AuditType.Custom, dto.UserId ?? Constants.Security.UnknownUserId, dto.EntityType, dto.Comment, dto.Parameters)).ToList(); // map the DateStamp for (var i = 0; i < items.Count; i++) diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs index aeb4c3774f..7ab73f3f2d 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs @@ -512,31 +512,16 @@ namespace Umbraco.Core.Persistence.Repositories.Implement foreach (var a in allPropertyDataDtos) a.PropertyTypeDto = indexedPropertyTypeDtos[a.PropertyTypeId]; - // prefetch configuration for tag properties - var tagEditors = new Dictionary(); - foreach (var propertyTypeDto in indexedPropertyTypeDtos.Values) - { - var editorAlias = propertyTypeDto.DataTypeDto.EditorAlias; - var editorAttribute = PropertyEditors[editorAlias].GetTagAttribute(); - if (editorAttribute == null) continue; - var tagConfigurationSource = propertyTypeDto.DataTypeDto.Configuration; - var tagConfiguration = string.IsNullOrWhiteSpace(tagConfigurationSource) - ? new TagConfiguration() - : JsonConvert.DeserializeObject(tagConfigurationSource); - if (tagConfiguration.Delimiter == default) tagConfiguration.Delimiter = editorAttribute.Delimiter; - tagEditors[editorAlias] = tagConfiguration; - } - // now we have // - the definitions // - all property data dtos - // - tag editors + // - tag editors (Actually ... no we don't since i removed that code, but we don't need them anyways it seems) // and we need to build the proper property collections - return GetPropertyCollections(temps, allPropertyDataDtos, tagEditors); + return GetPropertyCollections(temps, allPropertyDataDtos); } - private IDictionary GetPropertyCollections(List> temps, IEnumerable allPropertyDataDtos, Dictionary tagConfigurations) + private IDictionary GetPropertyCollections(List> temps, IEnumerable allPropertyDataDtos) where T : class, IContentBase { var result = new Dictionary(); diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeCommonRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeCommonRepository.cs index 6b751eb8ff..4393d365f8 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeCommonRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeCommonRepository.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using NPoco; using Umbraco.Core.Cache; +using Umbraco.Core.Exceptions; using Umbraco.Core.Models; using Umbraco.Core.Persistence.Dtos; using Umbraco.Core.Persistence.Factories; @@ -90,7 +91,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement contentType = ContentTypeFactory.BuildContentTypeEntity(contentTypeDto); else if (contentTypeDto.NodeDto.NodeObjectType == Constants.ObjectTypes.MemberType) contentType = ContentTypeFactory.BuildMemberTypeEntity(contentTypeDto); - else throw new Exception("panic"); + else throw new PanicException($"The node object type {contentTypeDto.NodeDto.NodeObjectType} is not supported"); contentTypes.Add(contentType.Id, contentType); // map allowed content types diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepository.cs index 9d77eb0990..359b967dab 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepository.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using NPoco; using Umbraco.Core.Cache; +using Umbraco.Core.Exceptions; using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Models.Entities; @@ -18,8 +19,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement /// internal class ContentTypeRepository : ContentTypeRepositoryBase, IContentTypeRepository { - public ContentTypeRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger, IContentTypeCommonRepository commonRepository) - : base(scopeAccessor, cache, logger, commonRepository) + public ContentTypeRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger, IContentTypeCommonRepository commonRepository, ILanguageRepository languageRepository) + : base(scopeAccessor, cache, logger, commonRepository, languageRepository) { } protected override bool SupportsPublishing => ContentType.SupportsPublishingConst; @@ -56,7 +57,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement { // the cache policy will always want everything // even GetMany(ids) gets everything and filters afterwards - if (ids.Any()) throw new Exception("panic"); + if (ids.Any()) throw new PanicException("There can be no ids specified"); return CommonRepository.GetAllTypes().OfType(); } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs index 22c9244d8f..f2efb03ba4 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs @@ -15,6 +15,7 @@ using Umbraco.Core.Persistence.Factories; using Umbraco.Core.Persistence.Querying; using Umbraco.Core.Scoping; using Umbraco.Core.Services; +using static Umbraco.Core.Persistence.NPocoSqlExtensions.Statics; namespace Umbraco.Core.Persistence.Repositories.Implement { @@ -26,14 +27,15 @@ namespace Umbraco.Core.Persistence.Repositories.Implement internal abstract class ContentTypeRepositoryBase : NPocoRepositoryBase, IReadRepository where TEntity : class, IContentTypeComposition { - protected ContentTypeRepositoryBase(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger, IContentTypeCommonRepository commonRepository) + protected ContentTypeRepositoryBase(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger, IContentTypeCommonRepository commonRepository, ILanguageRepository languageRepository) : base(scopeAccessor, cache, logger) { CommonRepository = commonRepository; + LanguageRepository = languageRepository; } protected IContentTypeCommonRepository CommonRepository { get; } - + protected ILanguageRepository LanguageRepository { get; } protected abstract bool SupportsPublishing { get; } public IEnumerable> Move(TEntity moving, EntityContainer container) @@ -98,6 +100,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement protected void PersistNewBaseContentType(IContentTypeComposition entity) { + ValidateVariations(entity); + var dto = ContentTypeFactory.BuildContentTypeDto(entity); //Cannot add a duplicate content type @@ -163,11 +167,11 @@ AND umbracoNode.nodeObjectType = @objectType", foreach (var allowedContentType in entity.AllowedContentTypes) { Database.Insert(new ContentTypeAllowedContentTypeDto - { - Id = entity.Id, - AllowedId = allowedContentType.Id.Value, - SortOrder = allowedContentType.SortOrder - }); + { + Id = entity.Id, + AllowedId = allowedContentType.Id.Value, + SortOrder = allowedContentType.SortOrder + }); } @@ -214,6 +218,8 @@ AND umbracoNode.nodeObjectType = @objectType", protected void PersistUpdatedBaseContentType(IContentTypeComposition entity) { + ValidateVariations(entity); + var dto = ContentTypeFactory.BuildContentTypeDto(entity); // ensure the alias is not used already @@ -370,7 +376,7 @@ AND umbracoNode.id <> @id", foreach (var propertyGroup in entity.PropertyGroups) { // insert or update group - var groupDto = PropertyGroupFactory.BuildGroupDto(propertyGroup,entity.Id); + var groupDto = PropertyGroupFactory.BuildGroupDto(propertyGroup, entity.Id); var groupId = propertyGroup.HasIdentity ? Database.Update(groupDto) : Convert.ToInt32(Database.Insert(groupDto)); @@ -388,7 +394,7 @@ AND umbracoNode.id <> @id", //check if the content type variation has been changed var contentTypeVariationDirty = entity.IsPropertyDirty("Variations"); - var oldContentTypeVariation = (ContentVariation) dtoPk.Variations; + var oldContentTypeVariation = (ContentVariation)dtoPk.Variations; var newContentTypeVariation = entity.Variations; var contentTypeVariationChanging = contentTypeVariationDirty && oldContentTypeVariation != newContentTypeVariation; if (contentTypeVariationChanging) @@ -449,7 +455,7 @@ AND umbracoNode.id <> @id", // via composition, with their original variations (ie not filtered by this // content type variations - we need this true value to make decisions. - foreach (var propertyType in ((ContentTypeCompositionBase) entity).RawComposedPropertyTypes) + foreach (var propertyType in ((ContentTypeCompositionBase)entity).RawComposedPropertyTypes) { if (propertyType.VariesBySegment() || newContentTypeVariation.VariesBySegment()) throw new NotSupportedException(); // TODO: support this @@ -518,6 +524,25 @@ AND umbracoNode.id <> @id", CommonRepository.ClearCache(); // always } + /// + /// Ensures that no property types are flagged for a variance that is not supported by the content type itself + /// + /// + private void ValidateVariations(IContentTypeComposition entity) + { + //if the entity does not vary at all, then the property cannot have a variance value greater than it + if (entity.Variations == ContentVariation.Nothing) + { + foreach (var prop in entity.PropertyTypes) + { + if (prop.IsPropertyDirty(nameof(prop.Variations)) && prop.Variations > entity.Variations) + throw new InvalidOperationException($"The property {prop.Alias} cannot have variations of {prop.Variations} with the content type variations of {entity.Variations}"); + } + + } + + } + private IEnumerable GetImpactedContentTypes(IContentTypeComposition contentType, IEnumerable all) { var impact = new List(); @@ -525,12 +550,12 @@ AND umbracoNode.id <> @id", var tree = new Dictionary>(); foreach (var x in all) - foreach (var y in x.ContentTypeComposition) - { - if (!tree.TryGetValue(y.Id, out var list)) - list = tree[y.Id] = new List(); - list.Add(x); - } + foreach (var y in x.ContentTypeComposition) + { + if (!tree.TryGetValue(y.Id, out var list)) + list = tree[y.Id] = new List(); + list.Add(x); + } var nset = new List(); do @@ -572,7 +597,7 @@ AND umbracoNode.id <> @id", // new property type, ignore if (!oldVariations.TryGetValue(propertyType.Id, out var oldVariationB)) continue; - var oldVariation = (ContentVariation) oldVariationB; // NPoco cannot fetch directly + var oldVariation = (ContentVariation)oldVariationB; // NPoco cannot fetch directly // only those property types that *actually* changed var newVariation = propertyType.Variations; @@ -636,7 +661,7 @@ AND umbracoNode.id <> @id", var impactedL = impacted.Select(x => x.Id).ToList(); //Group by the "To" variation so we can bulk update in the correct batches - foreach(var grouping in propertyTypeChanges.GroupBy(x => x.Value.ToVariation)) + foreach (var grouping in propertyTypeChanges.GroupBy(x => x.Value.ToVariation)) { var propertyTypeIds = grouping.Select(x => x.Key).ToList(); var toVariation = grouping.Key; @@ -646,10 +671,12 @@ AND umbracoNode.id <> @id", case ContentVariation.Culture: CopyPropertyData(null, defaultLanguageId, propertyTypeIds, impactedL); CopyTagData(null, defaultLanguageId, propertyTypeIds, impactedL); + RenormalizeDocumentEditedFlags(propertyTypeIds, impactedL); break; case ContentVariation.Nothing: CopyPropertyData(defaultLanguageId, null, propertyTypeIds, impactedL); CopyTagData(defaultLanguageId, null, propertyTypeIds, impactedL); + RenormalizeDocumentEditedFlags(propertyTypeIds, impactedL); break; case ContentVariation.CultureAndSegment: case ContentVariation.Segment: @@ -963,6 +990,205 @@ AND umbracoNode.id <> @id", Database.Execute(sqlDelete); } + + } + + /// + /// Re-normalizes the edited value in the umbracoDocumentCultureVariation and umbracoDocument table when variations are changed + /// + /// + /// + /// + /// If this is not done, then in some cases the "edited" value for a particular culture for a document will remain true when it should be false + /// if the property was changed to invariant. In order to do this we need to recalculate this value based on the values stored for each + /// property, culture and current/published version. + /// + private void RenormalizeDocumentEditedFlags(IReadOnlyCollection propertyTypeIds, IReadOnlyCollection contentTypeIds = null) + { + var defaultLang = LanguageRepository.GetDefaultId(); + + //This will build up a query to get the property values of both the current and the published version so that we can check + //based on the current variance of each item to see if it's 'edited' value should be true/false. + + var whereInArgsCount = propertyTypeIds.Count + (contentTypeIds?.Count ?? 0); + if (whereInArgsCount > 2000) + throw new NotSupportedException("Too many property/content types."); + + var propertySql = Sql() + .Select() + .AndSelect(x => x.NodeId, x => x.Current) + .AndSelect(x => x.Published) + .AndSelect(x => x.Variations) + .From() + .InnerJoin().On((left, right) => left.Id == right.VersionId) + .InnerJoin().On((left, right) => left.Id == right.PropertyTypeId); + + if (contentTypeIds != null) + { + propertySql.InnerJoin().On((c, cversion) => c.NodeId == cversion.NodeId); + } + + propertySql.LeftJoin().On((docversion, cversion) => cversion.Id == docversion.Id) + .Where((docversion, cversion) => cversion.Current || docversion.Published) + .WhereIn(x => x.PropertyTypeId, propertyTypeIds); + + if (contentTypeIds != null) + { + propertySql.WhereIn(x => x.ContentTypeId, contentTypeIds); + } + + propertySql + .OrderBy(x => x.NodeId) + .OrderBy(x => x.PropertyTypeId, x => x.LanguageId, x => x.VersionId); + + //keep track of this node/lang to mark or unmark a culture as edited + var editedLanguageVersions = new Dictionary<(int nodeId, int? langId), bool>(); + //keep track of which node to mark or unmark as edited + var editedDocument = new Dictionary(); + var nodeId = -1; + var propertyTypeId = -1; + + PropertyValueVersionDto pubRow = null; + + //This is a reader (Query), we are not fetching this all into memory so we cannot make any changes during this iteration, we are just collecting data. + //Published data will always come before Current data based on the version id sort. + //There will only be one published row (max) and one current row per property. + foreach (var row in Database.Query(propertySql)) + { + //make sure to reset on each node/property change + if (nodeId != row.NodeId || propertyTypeId != row.PropertyTypeId) + { + nodeId = row.NodeId; + propertyTypeId = row.PropertyTypeId; + pubRow = null; + } + + if (row.Published) + pubRow = row; + + if (row.Current) + { + var propVariations = (ContentVariation)row.Variations; + + //if this prop doesn't vary but the row has a lang assigned or vice versa, flag this as not edited + if (!propVariations.VariesByCulture() && row.LanguageId.HasValue + || propVariations.VariesByCulture() && !row.LanguageId.HasValue) + { + //Flag this as not edited for this node/lang if the key doesn't exist + if (!editedLanguageVersions.TryGetValue((row.NodeId, row.LanguageId), out _)) + editedLanguageVersions.Add((row.NodeId, row.LanguageId), false); + + //mark as false if the item doesn't exist, else coerce to true + editedDocument[row.NodeId] = editedDocument.TryGetValue(row.NodeId, out var edited) ? (edited |= false) : false; + } + else if (pubRow == null) + { + //this would mean that that this property is 'edited' since there is no published version + editedLanguageVersions[(row.NodeId, row.LanguageId)] = true; + editedDocument[row.NodeId] = true; + } + //compare the property values, if they differ from versions then flag the current version as edited + else if (IsPropertyValueChanged(pubRow, row)) + { + //Here we would check if the property is invariant, in which case the edited language should be indicated by the default lang + editedLanguageVersions[(row.NodeId, !propVariations.VariesByCulture() ? defaultLang : row.LanguageId)] = true; + editedDocument[row.NodeId] = true; + } + + //reset + pubRow = null; + } + } + + //lookup all matching rows in umbracoDocumentCultureVariation + var docCultureVariationsToUpdate = editedLanguageVersions.InGroupsOf(2000) + .SelectMany(_ => Database.Fetch( + Sql().Select().From() + .WhereIn(x => x.LanguageId, editedLanguageVersions.Keys.Select(x => x.langId).ToList()) + .WhereIn(x => x.NodeId, editedLanguageVersions.Keys.Select(x => x.nodeId)))) + //convert to dictionary with the same key type + .ToDictionary(x => (x.NodeId, (int?)x.LanguageId), x => x); + + var toUpdate = new List(); + foreach (var ev in editedLanguageVersions) + { + if (docCultureVariationsToUpdate.TryGetValue(ev.Key, out var docVariations)) + { + //check if it needs updating + if (docVariations.Edited != ev.Value) + { + docVariations.Edited = ev.Value; + toUpdate.Add(docVariations); + } + } + else if (ev.Key.langId.HasValue) + { + //This should never happen! If a property culture is flagged as edited then the culture must exist at the document level + throw new PanicException($"The existing DocumentCultureVariationDto was not found for node {ev.Key.nodeId} and language {ev.Key.langId}"); + } + } + + //Now bulk update the table DocumentCultureVariationDto, once for edited = true, another for edited = false + foreach (var editValue in toUpdate.GroupBy(x => x.Edited)) + { + Database.Execute(Sql().Update(u => u.Set(x => x.Edited, editValue.Key)) + .WhereIn(x => x.Id, editValue.Select(x => x.Id))); + } + + //Now bulk update the umbracoDocument table + foreach (var editValue in editedDocument.GroupBy(x => x.Value)) + { + Database.Execute(Sql().Update(u => u.Set(x => x.Edited, editValue.Key)) + .WhereIn(x => x.NodeId, editValue.Select(x => x.Key))); + } + } + + private static bool IsPropertyValueChanged(PropertyValueVersionDto pubRow, PropertyValueVersionDto row) + { + return !pubRow.TextValue.IsNullOrWhiteSpace() && pubRow.TextValue != row.TextValue + || !pubRow.VarcharValue.IsNullOrWhiteSpace() && pubRow.VarcharValue != row.VarcharValue + || pubRow.DateValue.HasValue && pubRow.DateValue != row.DateValue + || pubRow.DecimalValue.HasValue && pubRow.DecimalValue != row.DecimalValue + || pubRow.IntValue.HasValue && pubRow.IntValue != row.IntValue; + } + + private class NameCompareDto + { + public int NodeId { get; set; } + public int CurrentVersion { get; set; } + public int LanguageId { get; set; } + public string CurrentName { get; set; } + public string PublishedName { get; set; } + public int? PublishedVersion { get; set; } + public int Id { get; set; } // the Id of the DocumentCultureVariationDto + public bool Edited { get; set; } + } + + private class PropertyValueVersionDto + { + public int VersionId { get; set; } + public int PropertyTypeId { get; set; } + public int? LanguageId { get; set; } + public string Segment { get; set; } + public int? IntValue { get; set; } + + private decimal? _decimalValue; + [Column("decimalValue")] + public decimal? DecimalValue + { + get => _decimalValue; + set => _decimalValue = value?.Normalize(); + } + + public DateTime? DateValue { get; set; } + public string VarcharValue { get; set; } + public string TextValue { get; set; } + + public int NodeId { get; set; } + public bool Current { get; set; } + public bool Published { get; set; } + + public byte Variations { get; set; } } private void DeletePropertyType(int contentTypeId, int propertyTypeId) diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs index 30a2927cc8..344557d815 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs @@ -386,7 +386,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement Database.BulkInsertRecords(GetContentVariationDtos(entity, publishing)); // insert document variations - Database.BulkInsertRecords(GetDocumentVariationDtos(entity, publishing, editedCultures)); + Database.BulkInsertRecords(GetDocumentVariationDtos(entity, editedCultures)); } // refresh content @@ -511,7 +511,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement entity.VersionId = documentVersionDto.Id = contentVersionDto.Id; // get the new id documentVersionDto.Published = false; // non-published version - Database.Insert(documentVersionDto); + Database.Insert(documentVersionDto); } // replace the property data (rather than updating) @@ -571,7 +571,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement Database.BulkInsertRecords(GetContentVariationDtos(entity, publishing)); // insert document variations - Database.BulkInsertRecords(GetDocumentVariationDtos(entity, publishing, editedCultures)); + Database.BulkInsertRecords(GetDocumentVariationDtos(entity, editedCultures)); } // refresh content @@ -1297,25 +1297,28 @@ namespace Umbraco.Core.Persistence.Repositories.Implement }; } - private IEnumerable GetDocumentVariationDtos(IContent content, bool publishing, HashSet editedCultures) + private IEnumerable GetDocumentVariationDtos(IContent content, HashSet editedCultures) { var allCultures = content.AvailableCultures.Union(content.PublishedCultures); // union = distinct foreach (var culture in allCultures) - yield return new DocumentCultureVariationDto + { + var dto = new DocumentCultureVariationDto { NodeId = content.Id, LanguageId = LanguageRepository.GetIdByIsoCode(culture) ?? throw new InvalidOperationException("Not a valid culture."), Culture = culture, Name = content.GetCultureName(culture) ?? content.GetPublishName(culture), - - // note: can't use IsCultureEdited at that point - hasn't been updated yet - see PersistUpdatedItem - Available = content.IsCultureAvailable(culture), Published = content.IsCulturePublished(culture), + // note: can't use IsCultureEdited at that point - hasn't been updated yet - see PersistUpdatedItem Edited = content.IsCultureAvailable(culture) && (!content.IsCulturePublished(culture) || (editedCultures != null && editedCultures.Contains(culture))) }; + + yield return dto; + } + } private class ContentVariation diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs index 4a32e373c1..b6d39fe54f 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs @@ -404,7 +404,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement if (isMedia) { sql - .InnerJoin().On((left, right) => left.Id == right.Id); + .LeftJoin().On((left, right) => left.Id == right.Id); } //Any LeftJoin statements need to come last diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs index 8429532b01..8597bbf19f 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs @@ -250,7 +250,6 @@ namespace Umbraco.Core.Persistence.Repositories.Implement lock (_codeIdMap) { if (_codeIdMap.TryGetValue(isoCode, out var id)) return id; - if (isoCode.Contains('-') && _codeIdMap.TryGetValue(isoCode.Split('-').First(), out var invariantId)) return invariantId; } if (throwOnNotFound) throw new ArgumentException($"Code {isoCode} does not correspond to an existing language.", nameof(isoCode)); diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/MediaTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/MediaTypeRepository.cs index 1abc75cf3a..512011b52c 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/MediaTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/MediaTypeRepository.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using NPoco; using Umbraco.Core.Cache; +using Umbraco.Core.Exceptions; using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Models.Entities; @@ -17,8 +18,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement /// internal class MediaTypeRepository : ContentTypeRepositoryBase, IMediaTypeRepository { - public MediaTypeRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger, IContentTypeCommonRepository commonRepository) - : base(scopeAccessor, cache, logger, commonRepository) + public MediaTypeRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger, IContentTypeCommonRepository commonRepository, ILanguageRepository languageRepository) + : base(scopeAccessor, cache, logger, commonRepository, languageRepository) { } protected override bool SupportsPublishing => MediaType.SupportsPublishingConst; @@ -50,7 +51,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement { // the cache policy will always want everything // even GetMany(ids) gets everything and filters afterwards - if (ids.Any()) throw new Exception("panic"); + if (ids.Any()) throw new PanicException("There can be no ids specified"); return CommonRepository.GetAllTypes().OfType(); } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/MemberTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/MemberTypeRepository.cs index d96854743e..ee651819bf 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/MemberTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/MemberTypeRepository.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using NPoco; using Umbraco.Core.Cache; +using Umbraco.Core.Exceptions; using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Models.Entities; @@ -18,8 +19,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement /// internal class MemberTypeRepository : ContentTypeRepositoryBase, IMemberTypeRepository { - public MemberTypeRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger, IContentTypeCommonRepository commonRepository) - : base(scopeAccessor, cache, logger, commonRepository) + public MemberTypeRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger, IContentTypeCommonRepository commonRepository, ILanguageRepository languageRepository) + : base(scopeAccessor, cache, logger, commonRepository, languageRepository) { } protected override bool SupportsPublishing => MemberType.SupportsPublishingConst; @@ -57,7 +58,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement { // the cache policy will always want everything // even GetMany(ids) gets everything and filters afterwards - if (ids.Any()) throw new Exception("panic"); + if (ids.Any()) throw new PanicException("There can be no ids specified"); return CommonRepository.GetAllTypes().OfType(); } diff --git a/src/Umbraco.Core/PropertyEditors/ConfigurationFieldsExtensions.cs b/src/Umbraco.Core/PropertyEditors/ConfigurationFieldsExtensions.cs new file mode 100644 index 0000000000..25fba622d5 --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/ConfigurationFieldsExtensions.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; + +namespace Umbraco.Core.PropertyEditors +{ + public static partial class ConfigurationFieldsExtensions + { + /// + /// Adds a configuration field. + /// + /// The list of configuration fields. + /// The key (alias) of the field. + /// The name (label) of the field. + /// The description for the field. + /// The path to the editor view to be used for the field. + /// Optional configuration used for field's editor. + public static void Add( + this List fields, + string key, + string name, + string description, + string view, + IDictionary config = null) + { + fields.Add(new ConfigurationField + { + Key = key, + Name = name, + Description = description, + View = view, + Config = config, + }); + } + } +} diff --git a/src/Umbraco.Core/PropertyEditors/DataEditor.cs b/src/Umbraco.Core/PropertyEditors/DataEditor.cs index 43f4b68b99..dbb2fc467e 100644 --- a/src/Umbraco.Core/PropertyEditors/DataEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/DataEditor.cs @@ -30,7 +30,7 @@ namespace Umbraco.Core.PropertyEditors // defaults Type = type; Icon = Constants.Icons.PropertyEditor; - Group = "common"; + Group = Constants.PropertyEditors.Groups.Common; // assign properties based on the attribute, if it is found Attribute = GetType().GetCustomAttribute(false); diff --git a/src/Umbraco.Core/PropertyEditors/DataEditorAttribute.cs b/src/Umbraco.Core/PropertyEditors/DataEditorAttribute.cs index bf67522449..2639b627a9 100644 --- a/src/Umbraco.Core/PropertyEditors/DataEditorAttribute.cs +++ b/src/Umbraco.Core/PropertyEditors/DataEditorAttribute.cs @@ -126,7 +126,7 @@ namespace Umbraco.Core.PropertyEditors /// Gets or sets an optional group. /// /// The group can be used for example to group the editors by category. - public string Group { get; set; } = "common"; + public string Group { get; set; } = Constants.PropertyEditors.Groups.Common; /// /// Gets or sets a value indicating whether the value editor is deprecated. diff --git a/src/Umbraco.Core/PropertyEditors/LabelPropertyEditor.cs b/src/Umbraco.Core/PropertyEditors/LabelPropertyEditor.cs index d0c40b1e63..60b7d55c01 100644 --- a/src/Umbraco.Core/PropertyEditors/LabelPropertyEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/LabelPropertyEditor.cs @@ -5,7 +5,11 @@ namespace Umbraco.Core.PropertyEditors /// /// Represents a property editor for label properties. /// - [DataEditor(Constants.PropertyEditors.Aliases.Label, "Label", "readonlyvalue", Icon = "icon-readonly")] + [DataEditor( + Constants.PropertyEditors.Aliases.Label, + "Label", + "readonlyvalue", + Icon = "icon-readonly")] public class LabelPropertyEditor : DataEditor { /// diff --git a/src/Umbraco.Core/PropertyEditors/TagsPropertyEditorAttribute.cs b/src/Umbraco.Core/PropertyEditors/TagsPropertyEditorAttribute.cs index 2439c7c02e..6549f1a233 100644 --- a/src/Umbraco.Core/PropertyEditors/TagsPropertyEditorAttribute.cs +++ b/src/Umbraco.Core/PropertyEditors/TagsPropertyEditorAttribute.cs @@ -55,6 +55,7 @@ namespace Umbraco.Core.PropertyEditors /// /// Gets the type of the dynamic configuration provider. /// + //TODO: This is not used and should be implemented in a nicer way, see https://github.com/umbraco/Umbraco-CMS/issues/6017#issuecomment-516253562 public Type TagsConfigurationProviderType { get; } } } diff --git a/src/Umbraco.Core/Services/ContentTypeServiceExtensions.cs b/src/Umbraco.Core/Services/ContentTypeServiceExtensions.cs index f643039dca..37f1e5127f 100644 --- a/src/Umbraco.Core/Services/ContentTypeServiceExtensions.cs +++ b/src/Umbraco.Core/Services/ContentTypeServiceExtensions.cs @@ -7,6 +7,21 @@ namespace Umbraco.Core.Services { public static class ContentTypeServiceExtensions { + /// + /// Gets all of the element types (e.g. content types that have been marked as an element type). + /// + /// The content type service. + /// Returns all the element types. + public static IEnumerable GetAllElementTypes(this IContentTypeService contentTypeService) + { + if (contentTypeService == null) + { + return Enumerable.Empty(); + } + + return contentTypeService.GetAll().Where(x => x.IsElement); + } + /// /// Returns the available composite content types for a given content type /// @@ -22,12 +37,14 @@ namespace Umbraco.Core.Services /// This is required because in the case of creating/modifying a content type because new property types being added to it are not yet persisted so cannot /// be looked up via the db, they need to be passed in. /// + /// Wether the composite content types should be applicable for an element type /// internal static ContentTypeAvailableCompositionsResults GetAvailableCompositeContentTypes(this IContentTypeService ctService, IContentTypeComposition source, IContentTypeComposition[] allContentTypes, string[] filterContentTypes = null, - string[] filterPropertyTypes = null) + string[] filterPropertyTypes = null, + bool isElement = false) { filterContentTypes = filterContentTypes == null ? Array.Empty() @@ -46,7 +63,7 @@ namespace Umbraco.Core.Services .Select(c => c.Alias) .Union(filterPropertyTypes) .ToArray(); - + var sourceId = source?.Id ?? 0; // find out if any content type uses this content type @@ -64,8 +81,9 @@ namespace Umbraco.Core.Services x => x.Id)); // usable types are those that are top-level + // do not allow element types to be composed by non-element types as this will break the model generation in ModelsBuilder var usableContentTypes = allContentTypes - .Where(x => x.ContentTypeComposition.Any() == false).ToArray(); + .Where(x => x.ContentTypeComposition.Any() == false && (isElement == false || x.IsElement)).ToArray(); foreach (var x in usableContentTypes) list.Add(x); diff --git a/src/Umbraco.Core/Services/Implement/ContentService.cs b/src/Umbraco.Core/Services/Implement/ContentService.cs index e49dcf4a12..faf4744c76 100644 --- a/src/Umbraco.Core/Services/Implement/ContentService.cs +++ b/src/Umbraco.Core/Services/Implement/ContentService.cs @@ -757,11 +757,11 @@ namespace Umbraco.Core.Services.Implement { var publishedState = content.PublishedState; if (publishedState != PublishedState.Published && publishedState != PublishedState.Unpublished) - throw new InvalidOperationException("Cannot save (un)publishing content, use the dedicated SavePublished method."); + throw new InvalidOperationException($"Cannot save (un)publishing content with name: {content.Name} - and state: {content.PublishedState}, use the dedicated SavePublished method."); if (content.Name != null && content.Name.Length > 255) { - throw new InvalidOperationException("Name cannot be more than 255 characters in length."); + throw new InvalidOperationException($"Content with the name {content.Name} cannot be more than 255 characters in length."); } var evtMsgs = EventMessagesFactory.Get(); @@ -886,6 +886,8 @@ namespace Umbraco.Core.Services.Implement { scope.WriteLock(Constants.Locks.ContentTree); + var allLangs = _languageRepository.GetMany().ToList(); + var saveEventArgs = new ContentSavingEventArgs(content, evtMsgs); if (raiseEvents && scope.Events.DispatchCancelable(Saving, this, saveEventArgs, nameof(Saving))) return new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, content); @@ -894,13 +896,13 @@ namespace Umbraco.Core.Services.Implement // if culture is '*', then publish them all (including variants) //this will create the correct culture impact even if culture is * or null - var impact = CultureImpact.Create(culture, _languageRepository.IsDefault(culture), content); + var impact = CultureImpact.Create(culture, IsDefaultCulture(allLangs, culture), content); // publish the culture(s) // we don't care about the response here, this response will be rechecked below but we need to set the culture info values now. content.PublishCulture(impact); - var result = CommitDocumentChangesInternal(scope, content, saveEventArgs, userId, raiseEvents); + var result = CommitDocumentChangesInternal(scope, content, saveEventArgs, allLangs, userId, raiseEvents); scope.Complete(); return result; } @@ -921,6 +923,8 @@ namespace Umbraco.Core.Services.Implement { scope.WriteLock(Constants.Locks.ContentTree); + var allLangs = _languageRepository.GetMany().ToList(); + var evtMsgs = EventMessagesFactory.Get(); var saveEventArgs = new ContentSavingEventArgs(content, evtMsgs); if (raiseEvents && scope.Events.DispatchCancelable(Saving, this, saveEventArgs, nameof(Saving))) @@ -928,25 +932,23 @@ namespace Umbraco.Core.Services.Implement var varies = content.ContentType.VariesByCulture(); - if (cultures.Length == 0) + if (cultures.Length == 0 && !varies) { //no cultures specified and doesn't vary, so publish it, else nothing to publish - return !varies - ? SaveAndPublish(content, userId: userId, raiseEvents: raiseEvents) - : new PublishResult(PublishResultType.FailedPublishNothingToPublish, evtMsgs, content); + return SaveAndPublish(content, userId: userId, raiseEvents: raiseEvents); } if (cultures.Any(x => x == null || x == "*")) throw new InvalidOperationException("Only valid cultures are allowed to be used in this method, wildcards or nulls are not allowed"); - var impacts = cultures.Select(x => CultureImpact.Explicit(x, _languageRepository.IsDefault(x))); + var impacts = cultures.Select(x => CultureImpact.Explicit(x, IsDefaultCulture(allLangs, x))); // publish the culture(s) // we don't care about the response here, this response will be rechecked below but we need to set the culture info values now. foreach (var impact in impacts) content.PublishCulture(impact); - var result = CommitDocumentChangesInternal(scope, content, saveEventArgs, userId, raiseEvents); + var result = CommitDocumentChangesInternal(scope, content, saveEventArgs, allLangs, userId, raiseEvents); scope.Complete(); return result; } @@ -986,6 +988,8 @@ namespace Umbraco.Core.Services.Implement { scope.WriteLock(Constants.Locks.ContentTree); + var allLangs = _languageRepository.GetMany().ToList(); + var saveEventArgs = new ContentSavingEventArgs(content, evtMsgs); if (scope.Events.DispatchCancelable(Saving, this, saveEventArgs, nameof(Saving))) return new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, content); @@ -993,26 +997,39 @@ namespace Umbraco.Core.Services.Implement // all cultures = unpublish whole if (culture == "*" || (!content.ContentType.VariesByCulture() && culture == null)) { + // It's important to understand that when the document varies by culture but the "*" is used, + // we are just unpublishing the whole document but leaving all of the culture's as-is. This is expected + // because we don't want to actually unpublish every culture and then the document, we just want everything + // to be non-routable so that when it's re-published all variants were as they were. + content.PublishedState = PublishedState.Unpublishing; + var result = CommitDocumentChangesInternal(scope, content, saveEventArgs, allLangs, userId); + scope.Complete(); + return result; } else { - // If the culture we want to unpublish was already unpublished, nothing to do. - // To check for that we need to lookup the persisted content item - var persisted = content.HasIdentity ? GetById(content.Id) : null; + // Unpublish the culture, this will change the document state to Publishing! ... which is expected because this will + // essentially be re-publishing the document with the requested culture removed. + // The call to CommitDocumentChangesInternal will perform all the checks like if this is a mandatory culture or the last culture being unpublished + // and will then unpublish the document accordingly. + // If the result of this is false it means there was no culture to unpublish (i.e. it was already unpublished or it did not exist) + var removed = content.UnpublishCulture(culture); - if (persisted != null && !persisted.IsCulturePublished(culture)) + //save and publish any changes + var result = CommitDocumentChangesInternal(scope, content, saveEventArgs, allLangs, userId); + + scope.Complete(); + + // In one case the result will be PublishStatusType.FailedPublishNothingToPublish which means that no cultures + // were specified to be published which will be the case when removed is false. In that case + // we want to swap the result type to PublishResultType.SuccessUnpublishAlready (that was the expectation before). + if (result.Result == PublishResultType.FailedPublishNothingToPublish && !removed) return new PublishResult(PublishResultType.SuccessUnpublishAlready, evtMsgs, content); - // unpublish the culture - content.UnpublishCulture(culture); + return result; } - - var result = CommitDocumentChangesInternal(scope, content, saveEventArgs, userId); - scope.Complete(); - return result; } - } /// @@ -1047,15 +1064,35 @@ namespace Umbraco.Core.Services.Implement if (raiseEvents && scope.Events.DispatchCancelable(Saving, this, saveEventArgs, nameof(Saving))) return new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, content); - var result = CommitDocumentChangesInternal(scope, content, saveEventArgs, userId, raiseEvents); + var allLangs = _languageRepository.GetMany().ToList(); + + var result = CommitDocumentChangesInternal(scope, content, saveEventArgs, allLangs, userId, raiseEvents); scope.Complete(); return result; } } + /// + /// Handles a lot of business logic cases for how the document should be persisted + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// Business logic cases such: as unpublishing a mandatory culture, or unpublishing the last culture, checking for pending scheduled publishing, etc... is dealt with in this method. + /// There is quite a lot of cases to take into account along with logic that needs to deal with scheduled saving/publishing, branch saving/publishing, etc... + /// + /// private PublishResult CommitDocumentChangesInternal(IScope scope, IContent content, - ContentSavingEventArgs saveEventArgs, - int userId = Constants.Security.SuperUserId, bool raiseEvents = true, bool branchOne = false, bool branchRoot = false) + ContentSavingEventArgs saveEventArgs, IReadOnlyCollection allLangs, + int userId = Constants.Security.SuperUserId, + bool raiseEvents = true, bool branchOne = false, bool branchRoot = false) { if (scope == null) throw new ArgumentNullException(nameof(scope)); if (content == null) throw new ArgumentNullException(nameof(content)); @@ -1070,8 +1107,8 @@ namespace Umbraco.Core.Services.Implement if (content.PublishedState != PublishedState.Publishing && content.PublishedState != PublishedState.Unpublishing) content.PublishedState = PublishedState.Publishing; - // state here is either Publishing or Unpublishing - // (even though, Publishing to unpublish a culture may end up unpublishing everything) + // State here is either Publishing or Unpublishing + // Publishing to unpublish a culture may end up unpublishing everything so these flags can be flipped later var publishing = content.PublishedState == PublishedState.Publishing; var unpublishing = content.PublishedState == PublishedState.Unpublishing; @@ -1088,6 +1125,18 @@ namespace Umbraco.Core.Services.Implement var changeType = isNew ? TreeChangeTypes.RefreshNode : TreeChangeTypes.RefreshBranch; var previouslyPublished = content.HasIdentity && content.Published; + //inline method to persist the document with the documentRepository since this logic could be called a couple times below + void SaveDocument(IContent c) + { + // save, always + if (c.HasIdentity == false) + c.CreatorId = userId; + c.WriterId = userId; + + // saving does NOT change the published version, unless PublishedState is Publishing or Unpublishing + _documentRepository.Save(c); + } + if (publishing) { //determine cultures publishing/unpublishing which will be based on previous calls to content.PublishCulture and ClearPublishInfo @@ -1097,11 +1146,25 @@ namespace Umbraco.Core.Services.Implement : null; // ensure that the document can be published, and publish handling events, business rules, etc - publishResult = StrategyCanPublish(scope, content, /*checkPath:*/ (!branchOne || branchRoot), culturesPublishing, culturesUnpublishing, evtMsgs, saveEventArgs); + publishResult = StrategyCanPublish(scope, content, /*checkPath:*/ (!branchOne || branchRoot), culturesPublishing, culturesUnpublishing, evtMsgs, saveEventArgs, allLangs); if (publishResult.Success) { // note: StrategyPublish flips the PublishedState to Publishing! publishResult = StrategyPublish(content, culturesPublishing, culturesUnpublishing, evtMsgs); + + //check if a culture has been unpublished and if there are no cultures left, and then unpublish document as a whole + if (publishResult.Result == PublishResultType.SuccessUnpublishCulture && content.PublishCultureInfos.Count == 0) + { + // This is a special case! We are unpublishing the last culture and to persist that we need to re-publish without any cultures + // so the state needs to remain Publishing to do that. However, we then also need to unpublish the document and to do that + // the state needs to be Unpublishing and it cannot be both. This state is used within the documentRepository to know how to + // persist certain things. So before proceeding below, we need to save the Publishing state to publish no cultures, then we can + // mark the document for Unpublishing. + SaveDocument(content); + + //set the flag to unpublish and continue + unpublishing = content.Published; // if not published yet, nothing to do + } } else { @@ -1162,13 +1225,8 @@ namespace Umbraco.Core.Services.Implement } } - // save, always - if (content.HasIdentity == false) - content.CreatorId = userId; - content.WriterId = userId; - - // saving does NOT change the published version, unless PublishedState is Publishing or Unpublishing - _documentRepository.Save(content); + //Persist the document + SaveDocument(content); // raise the Saved event, always if (raiseEvents) @@ -1186,17 +1244,34 @@ namespace Umbraco.Core.Services.Implement if (culturesUnpublishing != null) { - //If we are here, it means we tried unpublishing a culture but it was mandatory so now everything is unpublished - var langs = string.Join(", ", _languageRepository.GetMany() + // This will mean that that we unpublished a mandatory culture or we unpublished the last culture. + + var langs = string.Join(", ", allLangs .Where(x => culturesUnpublishing.InvariantContains(x.IsoCode)) .Select(x => x.CultureName)); Audit(AuditType.UnpublishVariant, userId, content.Id, $"Unpublished languages: {langs}", langs); - //log that the whole content item has been unpublished due to mandatory culture unpublished - Audit(AuditType.Unpublish, userId, content.Id, "Unpublished (mandatory language unpublished)"); - } - else - Audit(AuditType.Unpublish, userId, content.Id); + if (publishResult == null) + throw new PanicException("publishResult == null - should not happen"); + + switch(publishResult.Result) + { + case PublishResultType.FailedPublishMandatoryCultureMissing: + //occurs when a mandatory culture was unpublished (which means we tried publishing the document without a mandatory culture) + + //log that the whole content item has been unpublished due to mandatory culture unpublished + Audit(AuditType.Unpublish, userId, content.Id, "Unpublished (mandatory language unpublished)"); + return new PublishResult(PublishResultType.SuccessUnpublishMandatoryCulture, evtMsgs, content); + case PublishResultType.SuccessUnpublishCulture: + //occurs when the last culture is unpublished + + Audit(AuditType.Unpublish, userId, content.Id, "Unpublished (last language unpublished)"); + return new PublishResult(PublishResultType.SuccessUnpublishLastCulture, evtMsgs, content); + } + + } + + Audit(AuditType.Unpublish, userId, content.Id); return new PublishResult(PublishResultType.SuccessUnpublish, evtMsgs, content); } @@ -1236,7 +1311,7 @@ namespace Umbraco.Core.Services.Implement case PublishResultType.SuccessPublishCulture: if (culturesPublishing != null) { - var langs = string.Join(", ", _languageRepository.GetMany() + var langs = string.Join(", ", allLangs .Where(x => culturesPublishing.InvariantContains(x.IsoCode)) .Select(x => x.CultureName)); Audit(AuditType.PublishVariant, userId, content.Id, $"Published languages: {langs}", langs); @@ -1245,7 +1320,7 @@ namespace Umbraco.Core.Services.Implement case PublishResultType.SuccessUnpublishCulture: if (culturesUnpublishing != null) { - var langs = string.Join(", ", _languageRepository.GetMany() + var langs = string.Join(", ", allLangs .Where(x => culturesUnpublishing.InvariantContains(x.IsoCode)) .Select(x => x.CultureName)); Audit(AuditType.UnpublishVariant, userId, content.Id, $"Unpublished languages: {langs}", langs); @@ -1259,14 +1334,14 @@ namespace Umbraco.Core.Services.Implement // should not happen if (branchOne && !branchRoot) - throw new Exception("panic"); + throw new PanicException("branchOne && !branchRoot - should not happen"); //if publishing didn't happen or if it has failed, we still need to log which cultures were saved if (!branchOne && (publishResult == null || !publishResult.Success)) { if (culturesChanging != null) { - var langs = string.Join(", ", _languageRepository.GetMany() + var langs = string.Join(", ", allLangs .Where(x => culturesChanging.InvariantContains(x.IsoCode)) .Select(x => x.CultureName)); Audit(AuditType.SaveVariant, userId, content.Id, $"Saved languages: {langs}", langs); @@ -1297,6 +1372,8 @@ namespace Umbraco.Core.Services.Implement { scope.WriteLock(Constants.Locks.ContentTree); + var allLangs = _languageRepository.GetMany().ToList(); + foreach (var d in _documentRepository.GetContentForRelease(date)) { PublishResult result; @@ -1325,7 +1402,7 @@ namespace Umbraco.Core.Services.Implement //publish the culture values and validate the property values, if validation fails, log the invalid properties so the develeper has an idea of what has failed Property[] invalidProperties = null; - var impact = CultureImpact.Explicit(culture, _languageRepository.IsDefault(culture)); + var impact = CultureImpact.Explicit(culture, IsDefaultCulture(allLangs, culture)); var tryPublish = d.PublishCulture(impact) && _propertyValidationService.Value.IsPropertyDataValid(d, out invalidProperties, impact); if (invalidProperties != null && invalidProperties.Length > 0) Logger.Warn("Scheduled publishing will fail for document {DocumentId} and culture {Culture} because of invalid properties {InvalidProperties}", @@ -1340,7 +1417,7 @@ namespace Umbraco.Core.Services.Implement else if (!publishing) result = new PublishResult(PublishResultType.FailedPublishContentInvalid, evtMsgs, d); else - result = CommitDocumentChangesInternal(scope, d, saveEventArgs, d.WriterId); + result = CommitDocumentChangesInternal(scope, d, saveEventArgs, allLangs, d.WriterId); if (result.Success == false) @@ -1390,7 +1467,7 @@ namespace Umbraco.Core.Services.Implement d.UnpublishCulture(c); } - result = CommitDocumentChangesInternal(scope, d, saveEventArgs, d.WriterId); + result = CommitDocumentChangesInternal(scope, d, saveEventArgs, allLangs, d.WriterId); if (result.Success == false) Logger.Error(null, "Failed to publish document id={DocumentId}, reason={Reason}.", d.Id, result.Result); yield return result; @@ -1416,7 +1493,7 @@ namespace Umbraco.Core.Services.Implement } // utility 'PublishCultures' func used by SaveAndPublishBranch - private bool SaveAndPublishBranch_PublishCultures(IContent content, HashSet culturesToPublish) + private bool SaveAndPublishBranch_PublishCultures(IContent content, HashSet culturesToPublish, IReadOnlyCollection allLangs) { //TODO: This does not support being able to return invalid property details to bubble up to the UI @@ -1426,7 +1503,7 @@ namespace Umbraco.Core.Services.Implement { return culturesToPublish.All(culture => { - var impact = CultureImpact.Create(culture, _languageRepository.IsDefault(culture), content); + var impact = CultureImpact.Create(culture, IsDefaultCulture(allLangs, culture), content); return content.PublishCulture(impact) && _propertyValidationService.Value.IsPropertyDataValid(content, out _, impact); }); } @@ -1535,7 +1612,7 @@ namespace Umbraco.Core.Services.Implement internal IEnumerable SaveAndPublishBranch(IContent document, bool force, Func> shouldPublish, - Func, bool> publishCultures, + Func, IReadOnlyCollection, bool> publishCultures, int userId = Constants.Security.SuperUserId) { if (shouldPublish == null) throw new ArgumentNullException(nameof(shouldPublish)); @@ -1549,6 +1626,8 @@ namespace Umbraco.Core.Services.Implement { scope.WriteLock(Constants.Locks.ContentTree); + var allLangs = _languageRepository.GetMany().ToList(); + if (!document.HasIdentity) throw new InvalidOperationException("Cannot not branch-publish a new document."); @@ -1557,7 +1636,7 @@ namespace Umbraco.Core.Services.Implement throw new InvalidOperationException("Cannot mix PublishCulture and SaveAndPublishBranch."); // deal with the branch root - if it fails, abort - var result = SaveAndPublishBranchItem(scope, document, shouldPublish, publishCultures, true, publishedDocuments, evtMsgs, userId); + var result = SaveAndPublishBranchItem(scope, document, shouldPublish, publishCultures, true, publishedDocuments, evtMsgs, userId, allLangs); if (result != null) { results.Add(result); @@ -1588,7 +1667,7 @@ namespace Umbraco.Core.Services.Implement } // no need to check path here, parent has to be published here - result = SaveAndPublishBranchItem(scope, d, shouldPublish, publishCultures, false, publishedDocuments, evtMsgs, userId); + result = SaveAndPublishBranchItem(scope, d, shouldPublish, publishCultures, false, publishedDocuments, evtMsgs, userId, allLangs); if (result != null) { results.Add(result); @@ -1620,10 +1699,10 @@ namespace Umbraco.Core.Services.Implement // publishValues: a function publishing values (using the appropriate PublishCulture calls) private PublishResult SaveAndPublishBranchItem(IScope scope, IContent document, Func> shouldPublish, - Func, bool> publishCultures, + Func, IReadOnlyCollection, bool> publishCultures, bool isRoot, ICollection publishedDocuments, - EventMessages evtMsgs, int userId) + EventMessages evtMsgs, int userId, IReadOnlyCollection allLangs) { var culturesToPublish = shouldPublish(document); if (culturesToPublish == null) // null = do not include @@ -1636,13 +1715,13 @@ namespace Umbraco.Core.Services.Implement return new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, document); // publish & check if values are valid - if (!publishCultures(document, culturesToPublish)) + if (!publishCultures(document, culturesToPublish, allLangs)) { //TODO: Based on this callback behavior there is no way to know which properties may have been invalid if this failed, see other results of FailedPublishContentInvalid return new PublishResult(PublishResultType.FailedPublishContentInvalid, evtMsgs, document); } - var result = CommitDocumentChangesInternal(scope, document, saveEventArgs, userId, branchOne: true, branchRoot: isRoot); + var result = CommitDocumentChangesInternal(scope, document, saveEventArgs, allLangs, userId, branchOne: true, branchRoot: isRoot); if (result.Success) publishedDocuments.Add(document); return result; @@ -2343,6 +2422,9 @@ namespace Umbraco.Core.Services.Implement _auditRepository.Save(new AuditItem(objectId, type, userId, ObjectTypes.GetName(UmbracoObjectTypes.Document), message, parameters)); } + private bool IsDefaultCulture(IReadOnlyCollection langs, string culture) => langs.Any(x => x.IsDefault && x.IsoCode.InvariantEquals(culture)); + private bool IsMandatoryCulture(IReadOnlyCollection langs, string culture) => langs.Any(x => x.IsMandatory && x.IsoCode.InvariantEquals(culture)); + #endregion #region Event Handlers @@ -2497,7 +2579,9 @@ namespace Umbraco.Core.Services.Implement /// /// /// - private PublishResult StrategyCanPublish(IScope scope, IContent content, bool checkPath, IReadOnlyList culturesPublishing, IReadOnlyCollection culturesUnpublishing, EventMessages evtMsgs, ContentSavingEventArgs savingEventArgs) + private PublishResult StrategyCanPublish(IScope scope, IContent content, bool checkPath, IReadOnlyList culturesPublishing, + IReadOnlyCollection culturesUnpublishing, EventMessages evtMsgs, ContentSavingEventArgs savingEventArgs, + IReadOnlyCollection allLangs) { // raise Publishing event if (scope.Events.DispatchCancelable(Publishing, this, savingEventArgs.ToContentPublishingEventArgs())) @@ -2510,7 +2594,7 @@ namespace Umbraco.Core.Services.Implement var impactsToPublish = culturesPublishing == null ? new[] {CultureImpact.Invariant} //if it's null it's invariant - : culturesPublishing.Select(x => CultureImpact.Explicit(x, _languageRepository.IsDefault(x))).ToArray(); + : culturesPublishing.Select(x => CultureImpact.Explicit(x, allLangs.Any(lang => lang.IsoCode.InvariantEquals(x) && lang.IsMandatory))).ToArray(); // publish the culture(s) if (!impactsToPublish.All(content.PublishCulture)) @@ -2531,11 +2615,17 @@ namespace Umbraco.Core.Services.Implement if (culturesPublishing == null) throw new InvalidOperationException("Internal error, variesByCulture but culturesPublishing is null."); - if (content.Published && culturesPublishing.Count == 0 && culturesUnpublishing.Count == 0) // no published cultures = cannot be published + if (content.Published && culturesPublishing.Count == 0 && culturesUnpublishing.Count == 0) + { + // no published cultures = cannot be published + // This will occur if for example, a culture that is already unpublished is sent to be unpublished again, or vice versa, in that case + // there will be nothing to publish/unpublish. return new PublishResult(PublishResultType.FailedPublishNothingToPublish, evtMsgs, content); + } + // missing mandatory culture = cannot be published - var mandatoryCultures = _languageRepository.GetMany().Where(x => x.IsMandatory).Select(x => x.IsoCode); + var mandatoryCultures = allLangs.Where(x => x.IsMandatory).Select(x => x.IsoCode); var mandatoryMissing = mandatoryCultures.Any(x => !content.PublishedCultures.Contains(x, StringComparer.OrdinalIgnoreCase)); if (mandatoryMissing) return new PublishResult(PublishResultType.FailedPublishMandatoryCultureMissing, evtMsgs, content); @@ -2676,6 +2766,7 @@ namespace Umbraco.Core.Services.Implement { var attempt = new PublishResult(PublishResultType.SuccessUnpublish, evtMsgs, content); + //TODO: What is this check?? we just created this attempt and of course it is Success?! if (attempt.Success == false) return attempt; diff --git a/src/Umbraco.Core/Services/Implement/ContentTypeServiceBaseOfTRepositoryTItemTService.cs b/src/Umbraco.Core/Services/Implement/ContentTypeServiceBaseOfTRepositoryTItemTService.cs index 1f1f0d9ac3..6ac8e1404a 100644 --- a/src/Umbraco.Core/Services/Implement/ContentTypeServiceBaseOfTRepositoryTItemTService.cs +++ b/src/Umbraco.Core/Services/Implement/ContentTypeServiceBaseOfTRepositoryTItemTService.cs @@ -390,6 +390,11 @@ namespace Umbraco.Core.Services.Implement if (string.IsNullOrWhiteSpace(item.Name)) throw new ArgumentException("Cannot save item with empty name."); + if (item.Name != null && item.Name.Length > 255) + { + throw new InvalidOperationException("Name cannot be more than 255 characters in length."); + } + scope.WriteLock(WriteLockIds); // validate the DAG transform, within the lock diff --git a/src/Umbraco.Core/Services/Implement/DataTypeService.cs b/src/Umbraco.Core/Services/Implement/DataTypeService.cs index 3552b2d8fc..dc998b18dd 100644 --- a/src/Umbraco.Core/Services/Implement/DataTypeService.cs +++ b/src/Umbraco.Core/Services/Implement/DataTypeService.cs @@ -349,6 +349,11 @@ namespace Umbraco.Core.Services.Implement throw new ArgumentException("Cannot save datatype with empty name."); } + if (dataType.Name != null && dataType.Name.Length > 255) + { + throw new InvalidOperationException("Name cannot be more than 255 characters in length."); + } + _dataTypeRepository.Save(dataType); saveEventArgs.CanCancel = false; diff --git a/src/Umbraco.Core/Services/Implement/EntityXmlSerializer.cs b/src/Umbraco.Core/Services/Implement/EntityXmlSerializer.cs index 863ecc0b1b..ace740a831 100644 --- a/src/Umbraco.Core/Services/Implement/EntityXmlSerializer.cs +++ b/src/Umbraco.Core/Services/Implement/EntityXmlSerializer.cs @@ -437,7 +437,8 @@ namespace Umbraco.Core.Services.Implement new XElement("Description", contentType.Description), new XElement("AllowAtRoot", contentType.AllowedAsRoot.ToString()), new XElement("IsListView", contentType.IsContainer.ToString()), - new XElement("IsElement", contentType.IsElement.ToString())); + new XElement("IsElement", contentType.IsElement.ToString()), + new XElement("Variations", contentType.Variations.ToString())); var masterContentType = contentType.ContentTypeComposition.FirstOrDefault(x => x.Id == contentType.ParentId); if(masterContentType != null) @@ -487,7 +488,8 @@ namespace Umbraco.Core.Services.Implement new XElement("SortOrder", propertyType.SortOrder), new XElement("Mandatory", propertyType.Mandatory.ToString()), propertyType.ValidationRegExp != null ? new XElement("Validation", propertyType.ValidationRegExp) : null, - propertyType.Description != null ? new XElement("Description", new XCData(propertyType.Description)) : null); + propertyType.Description != null ? new XElement("Description", new XCData(propertyType.Description)) : null, + new XElement("Variations", propertyType.Variations.ToString())); genericProperties.Add(genericProperty); } diff --git a/src/Umbraco.Core/Services/Implement/FileService.cs b/src/Umbraco.Core/Services/Implement/FileService.cs index 79d5b35775..26a24e9b98 100644 --- a/src/Umbraco.Core/Services/Implement/FileService.cs +++ b/src/Umbraco.Core/Services/Implement/FileService.cs @@ -358,6 +358,11 @@ namespace Umbraco.Core.Services.Implement { "ContentTypeAlias", contentTypeAlias }, }; + if (contentTypeAlias != null && contentTypeAlias.Length > 255) + { + throw new InvalidOperationException("Name cannot be more than 255 characters in length."); + } + // check that the template hasn't been created on disk before creating the content type // if it exists, set the new template content to the existing file content string content = GetViewContent(contentTypeAlias); @@ -365,7 +370,10 @@ namespace Umbraco.Core.Services.Implement { template.Content = content; } + + + using (var scope = ScopeProvider.CreateScope()) { var saveEventArgs = new SaveEventArgs(template, true, evtMsgs, additionalData); diff --git a/src/Umbraco.Core/Services/Implement/MediaService.cs b/src/Umbraco.Core/Services/Implement/MediaService.cs index ab075c4ade..e9fdedbf33 100644 --- a/src/Umbraco.Core/Services/Implement/MediaService.cs +++ b/src/Umbraco.Core/Services/Implement/MediaService.cs @@ -139,6 +139,10 @@ namespace Umbraco.Core.Services.Implement var parent = parentId > 0 ? GetById(parentId) : null; if (parentId > 0 && parent == null) throw new ArgumentException("No media with that id.", nameof(parentId)); + if (name != null && name.Length > 255) + { + throw new InvalidOperationException("Name cannot be more than 255 characters in length."); throw new InvalidOperationException("Name cannot be more than 255 characters in length."); + } var media = new Models.Media(name, parentId, mediaType); using (var scope = ScopeProvider.CreateScope()) @@ -168,6 +172,10 @@ namespace Umbraco.Core.Services.Implement var mediaType = GetMediaType(mediaTypeAlias); if (mediaType == null) throw new ArgumentException("No media type with that alias.", nameof(mediaTypeAlias)); + if (name != null && name.Length > 255) + { + throw new InvalidOperationException("Name cannot be more than 255 characters in length."); throw new InvalidOperationException("Name cannot be more than 255 characters in length."); + } var media = new Models.Media(name, -1, mediaType); using (var scope = ScopeProvider.CreateScope()) @@ -202,6 +210,10 @@ namespace Umbraco.Core.Services.Implement var mediaType = GetMediaType(mediaTypeAlias); if (mediaType == null) throw new ArgumentException("No media type with that alias.", nameof(mediaTypeAlias)); // causes rollback + if (name != null && name.Length > 255) + { + throw new InvalidOperationException("Name cannot be more than 255 characters in length."); throw new InvalidOperationException("Name cannot be more than 255 characters in length."); + } var media = new Models.Media(name, parent, mediaType); CreateMedia(scope, media, parent, userId, false); @@ -648,6 +660,11 @@ namespace Umbraco.Core.Services.Implement if (string.IsNullOrWhiteSpace(media.Name)) throw new ArgumentException("Media has no name.", nameof(media)); + if (media.Name != null && media.Name.Length > 255) + { + throw new InvalidOperationException("Name cannot be more than 255 characters in length."); throw new InvalidOperationException("Name cannot be more than 255 characters in length."); + } + scope.WriteLock(Constants.Locks.MediaTree); if (media.HasIdentity == false) media.CreatorId = userId; @@ -760,7 +777,7 @@ namespace Umbraco.Core.Services.Implement const int pageSize = 500; var page = 0; var total = long.MaxValue; - while(page * pageSize < total) + while (page * pageSize < total) { //get descendants - ordered from deepest to shallowest var descendants = GetPagedDescendants(media.Id, page, pageSize, out total, ordering: Ordering.By("Path", Direction.Descending)); @@ -945,7 +962,7 @@ namespace Umbraco.Core.Services.Implement // if media was trashed, and since we're not moving to the recycle bin, // indicate that the trashed status should be changed to false, else just // leave it unchanged - var trashed = media.Trashed ? false : (bool?) null; + var trashed = media.Trashed ? false : (bool?)null; PerformMoveLocked(media, parentId, parent, userId, moves, trashed); scope.Events.Dispatch(TreeChanged, this, new TreeChange(media, TreeChangeTypes.RefreshBranch).ToEventArgs()); @@ -1009,7 +1026,7 @@ namespace Umbraco.Core.Services.Implement private void PerformMoveMediaLocked(IMedia media, int userId, bool? trash) { - if (trash.HasValue) ((ContentBase) media).Trashed = trash.Value; + if (trash.HasValue) ((ContentBase)media).Trashed = trash.Value; _mediaRepository.Save(media); } diff --git a/src/Umbraco.Core/Services/PublishResultType.cs b/src/Umbraco.Core/Services/PublishResultType.cs index 1a2b52f9c9..66c1e38267 100644 --- a/src/Umbraco.Core/Services/PublishResultType.cs +++ b/src/Umbraco.Core/Services/PublishResultType.cs @@ -49,6 +49,11 @@ /// SuccessUnpublishMandatoryCulture = 6, + /// + /// The specified document culture was unpublished, and was the last published culture in the document, therefore the document itself was unpublished. + /// + SuccessUnpublishLastCulture = 8, + #endregion #region Success - Mixed @@ -113,9 +118,9 @@ FailedPublishContentInvalid = FailedPublish | 8, /// - /// The document could not be published because it has no publishing flags or values. + /// The document could not be published because it has no publishing flags or values or if its a variant document, no cultures were specified to be published. /// - FailedPublishNothingToPublish = FailedPublish | 9, // TODO: in ContentService.StrategyCanPublish - weird + FailedPublishNothingToPublish = FailedPublish | 9, /// /// The document could not be published because some mandatory cultures are missing. diff --git a/src/Umbraco.Core/Sync/ApplicationUrlHelper.cs b/src/Umbraco.Core/Sync/ApplicationUrlHelper.cs index 9cd4a58c65..2a558f85aa 100644 --- a/src/Umbraco.Core/Sync/ApplicationUrlHelper.cs +++ b/src/Umbraco.Core/Sync/ApplicationUrlHelper.cs @@ -97,8 +97,7 @@ namespace Umbraco.Core.Sync ? ":" + request.ServerVariables["SERVER_PORT"] : ""; - var useSsl = globalSettings.UseHttps || port == "443"; - var ssl = useSsl ? "s" : ""; // force, whatever the first request + var ssl = globalSettings.UseHttps ? "s" : ""; // force, whatever the first request var url = "http" + ssl + "://" + request.ServerVariables["SERVER_NAME"] + port + IOHelper.ResolveUrl(SystemDirectories.Umbraco); return url.TrimEnd('/'); diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 6adce1944f..a0d0fad6d9 100755 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -204,6 +204,7 @@ + @@ -234,6 +235,7 @@ + @@ -265,6 +267,7 @@ + diff --git a/src/Umbraco.Examine/ContentValueSetBuilder.cs b/src/Umbraco.Examine/ContentValueSetBuilder.cs index 44cef08813..9cbc311639 100644 --- a/src/Umbraco.Examine/ContentValueSetBuilder.cs +++ b/src/Umbraco.Examine/ContentValueSetBuilder.cs @@ -54,7 +54,7 @@ namespace Umbraco.Examine {"updateDate", new object[] {c.UpdateDate}}, //Always add invariant updateDate {"nodeName", (PublishedValuesOnly //Always add invariant nodeName ? c.PublishName?.Yield() - : c?.Name.Yield()) ?? Enumerable.Empty()}, + : c.Name?.Yield()) ?? Enumerable.Empty()}, {"urlName", urlValue?.Yield() ?? Enumerable.Empty()}, //Always add invariant urlName {"path", c.Path?.Yield() ?? Enumerable.Empty()}, {"nodeType", c.ContentType.Id.ToString().Yield() ?? Enumerable.Empty()}, diff --git a/src/Umbraco.Examine/ExamineExtensions.cs b/src/Umbraco.Examine/ExamineExtensions.cs index d97278f31c..d231a86f69 100644 --- a/src/Umbraco.Examine/ExamineExtensions.cs +++ b/src/Umbraco.Examine/ExamineExtensions.cs @@ -12,6 +12,7 @@ using Lucene.Net.Store; using Umbraco.Core; using Version = Lucene.Net.Util.Version; using Umbraco.Core.Logging; +using System.Threading; namespace Umbraco.Examine { @@ -28,6 +29,29 @@ namespace Umbraco.Examine /// internal static readonly Regex CultureIsoCodeFieldNameMatchExpression = new Regex("^([_\\w]+)_([a-z]{2}-[a-z0-9]{2,4})$", RegexOptions.Compiled); + private static bool _isConfigured = false; + private static object _configuredInit = null; + private static object _isConfiguredLocker = new object(); + + /// + /// Called on startup to configure each index. + /// + /// + /// Configures and unlocks all Lucene based indexes registered with the . + /// + internal static void ConfigureIndexes(this IExamineManager examineManager, IMainDom mainDom, ILogger logger) + { + LazyInitializer.EnsureInitialized( + ref _configuredInit, + ref _isConfigured, + ref _isConfiguredLocker, + () => + { + examineManager.ConfigureLuceneIndexes(logger, !mainDom.IsMainDom); + return null; + }); + } + //TODO: We need a public method here to just match a field name against CultureIsoCodeFieldNameMatchExpression /// diff --git a/src/Umbraco.Examine/IndexRebuilder.cs b/src/Umbraco.Examine/IndexRebuilder.cs index 43c309b9c5..786aecac71 100644 --- a/src/Umbraco.Examine/IndexRebuilder.cs +++ b/src/Umbraco.Examine/IndexRebuilder.cs @@ -5,7 +5,8 @@ using System.Threading.Tasks; using Examine; namespace Umbraco.Examine -{ +{ + /// /// Utility to rebuild all indexes ensuring minimal data queries /// diff --git a/src/Umbraco.Examine/LuceneIndexDiagnostics.cs b/src/Umbraco.Examine/LuceneIndexDiagnostics.cs new file mode 100644 index 0000000000..96363904b4 --- /dev/null +++ b/src/Umbraco.Examine/LuceneIndexDiagnostics.cs @@ -0,0 +1,83 @@ +using System.Collections.Generic; +using Examine.LuceneEngine.Providers; +using Umbraco.Core; +using Umbraco.Core.Logging; +using Lucene.Net.Store; +using Umbraco.Core.IO; +using System.Linq; + +namespace Umbraco.Examine +{ + public class LuceneIndexDiagnostics : IIndexDiagnostics + { + public LuceneIndexDiagnostics(LuceneIndex index, ILogger logger) + { + Index = index; + Logger = logger; + } + + public LuceneIndex Index { get; } + public ILogger Logger { get; } + + public int DocumentCount + { + get + { + try + { + return Index.GetIndexDocumentCount(); + } + catch (AlreadyClosedException) + { + Logger.Warn(typeof(UmbracoContentIndex), "Cannot get GetIndexDocumentCount, the writer is already closed"); + return 0; + } + } + } + + public int FieldCount + { + get + { + try + { + return Index.GetIndexFieldCount(); + } + catch (AlreadyClosedException) + { + Logger.Warn(typeof(UmbracoContentIndex), "Cannot get GetIndexFieldCount, the writer is already closed"); + return 0; + } + } + } + + public Attempt IsHealthy() + { + var isHealthy = Index.IsHealthy(out var indexError); + return isHealthy ? Attempt.Succeed() : Attempt.Fail(indexError.Message); + } + + public virtual IReadOnlyDictionary Metadata + { + get + { + var luceneDir = Index.GetLuceneDirectory(); + var d = new Dictionary + { + [nameof(UmbracoExamineIndex.CommitCount)] = Index.CommitCount, + [nameof(UmbracoExamineIndex.DefaultAnalyzer)] = Index.DefaultAnalyzer.GetType().Name, + ["LuceneDirectory"] = luceneDir.GetType().Name + }; + + if (luceneDir is FSDirectory fsDir) + { + d[nameof(UmbracoExamineIndex.LuceneIndexFolder)] = fsDir.Directory.ToString().ToLowerInvariant().TrimStart(IOHelper.MapPath(SystemDirectories.Root).ToLowerInvariant()).Replace("\\", "/").EnsureStartsWith('/'); + } + + return d; + } + } + + + } +} diff --git a/src/Umbraco.Examine/Umbraco.Examine.csproj b/src/Umbraco.Examine/Umbraco.Examine.csproj index 95690c17e4..e28a8e674e 100644 --- a/src/Umbraco.Examine/Umbraco.Examine.csproj +++ b/src/Umbraco.Examine/Umbraco.Examine.csproj @@ -72,6 +72,7 @@ + diff --git a/src/Umbraco.Examine/UmbracoExamineIndexDiagnostics.cs b/src/Umbraco.Examine/UmbracoExamineIndexDiagnostics.cs index fed5b9bae7..4a926deebe 100644 --- a/src/Umbraco.Examine/UmbracoExamineIndexDiagnostics.cs +++ b/src/Umbraco.Examine/UmbracoExamineIndexDiagnostics.cs @@ -7,73 +7,24 @@ using Umbraco.Core.Logging; namespace Umbraco.Examine { - public class UmbracoExamineIndexDiagnostics : IIndexDiagnostics + public class UmbracoExamineIndexDiagnostics : LuceneIndexDiagnostics { private readonly UmbracoExamineIndex _index; - private readonly ILogger _logger; public UmbracoExamineIndexDiagnostics(UmbracoExamineIndex index, ILogger logger) + : base(index, logger) { _index = index; - _logger = logger; } - public int DocumentCount + public override IReadOnlyDictionary Metadata { get { - try - { - return _index.GetIndexDocumentCount(); - } - catch (AlreadyClosedException) - { - _logger.Warn(typeof(UmbracoContentIndex), "Cannot get GetIndexDocumentCount, the writer is already closed"); - return 0; - } - } - } + var d = base.Metadata.ToDictionary(x => x.Key, x => x.Value); - public int FieldCount - { - get - { - try - { - return _index.GetIndexFieldCount(); - } - catch (AlreadyClosedException) - { - _logger.Warn(typeof(UmbracoContentIndex), "Cannot get GetIndexFieldCount, the writer is already closed"); - return 0; - } - } - } - - public Attempt IsHealthy() - { - var isHealthy = _index.IsHealthy(out var indexError); - return isHealthy ? Attempt.Succeed() : Attempt.Fail(indexError.Message); - } - - public virtual IReadOnlyDictionary Metadata - { - get - { - var d = new Dictionary - { - [nameof(UmbracoExamineIndex.CommitCount)] = _index.CommitCount, - [nameof(UmbracoExamineIndex.DefaultAnalyzer)] = _index.DefaultAnalyzer.GetType().Name, - ["LuceneDirectory"] = _index.GetLuceneDirectory().GetType().Name, - [nameof(UmbracoExamineIndex.EnableDefaultEventHandler)] = _index.EnableDefaultEventHandler, - [nameof(UmbracoExamineIndex.LuceneIndexFolder)] = - _index.LuceneIndexFolder == null - ? string.Empty - : _index.LuceneIndexFolder.ToString().ToLowerInvariant().TrimStart(IOHelper.MapPath(SystemDirectories.Root).ToLowerInvariant()).Replace("\\", "/").EnsureStartsWith('/'), - [nameof(UmbracoExamineIndex.PublishedValuesOnly)] = _index.PublishedValuesOnly, - //There's too much info here - //[nameof(UmbracoExamineIndexer.FieldDefinitionCollection)] = _index.FieldDefinitionCollection, - }; + d[nameof(UmbracoExamineIndex.EnableDefaultEventHandler)] = _index.EnableDefaultEventHandler; + d[nameof(UmbracoExamineIndex.PublishedValuesOnly)] = _index.PublishedValuesOnly; if (_index.ValueSetValidator is ValueSetValidator vsv) { diff --git a/src/Umbraco.Tests/Composing/TypeLoaderTests.cs b/src/Umbraco.Tests/Composing/TypeLoaderTests.cs index 7459ae848b..9cd4f39c17 100644 --- a/src/Umbraco.Tests/Composing/TypeLoaderTests.cs +++ b/src/Umbraco.Tests/Composing/TypeLoaderTests.cs @@ -268,7 +268,7 @@ AnotherContentFinder public void GetDataEditors() { var types = _typeLoader.GetDataEditors(); - Assert.AreEqual(38, types.Count()); + Assert.AreEqual(39, types.Count()); } /// diff --git a/src/Umbraco.Tests/Persistence/Repositories/ContentTypeRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/ContentTypeRepositoryTest.cs index 53f150f140..f953b9cce6 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/ContentTypeRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/ContentTypeRepositoryTest.cs @@ -29,10 +29,11 @@ namespace Umbraco.Tests.Persistence.Repositories private DocumentRepository CreateRepository(IScopeAccessor scopeAccessor, out ContentTypeRepository contentTypeRepository) { + var langRepository = new LanguageRepository(scopeAccessor, AppCaches.Disabled, Logger); var templateRepository = new TemplateRepository(scopeAccessor, AppCaches.Disabled, Logger, TestObjects.GetFileSystemsMock()); var tagRepository = new TagRepository(scopeAccessor, AppCaches.Disabled, Logger); var commonRepository = new ContentTypeCommonRepository(scopeAccessor, templateRepository, AppCaches.Disabled); - contentTypeRepository = new ContentTypeRepository(scopeAccessor, AppCaches.Disabled, Logger, commonRepository); + contentTypeRepository = new ContentTypeRepository(scopeAccessor, AppCaches.Disabled, Logger, commonRepository, langRepository); var languageRepository = new LanguageRepository(scopeAccessor, AppCaches.Disabled, Logger); var repository = new DocumentRepository(scopeAccessor, AppCaches.Disabled, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository); return repository; @@ -40,9 +41,10 @@ namespace Umbraco.Tests.Persistence.Repositories private ContentTypeRepository CreateRepository(IScopeAccessor scopeAccessor) { + var langRepository = new LanguageRepository(scopeAccessor, AppCaches.Disabled, Logger); var templateRepository = new TemplateRepository(scopeAccessor, AppCaches.Disabled, Logger, TestObjects.GetFileSystemsMock()); var commonRepository = new ContentTypeCommonRepository(scopeAccessor, templateRepository, AppCaches.Disabled); - var contentTypeRepository = new ContentTypeRepository(scopeAccessor, AppCaches.Disabled, Logger, commonRepository); + var contentTypeRepository = new ContentTypeRepository(scopeAccessor, AppCaches.Disabled, Logger, commonRepository, langRepository); return contentTypeRepository; } @@ -50,7 +52,8 @@ namespace Umbraco.Tests.Persistence.Repositories { var templateRepository = new TemplateRepository(scopeAccessor, AppCaches.Disabled, Logger, TestObjects.GetFileSystemsMock()); var commonRepository = new ContentTypeCommonRepository(scopeAccessor, templateRepository, AppCaches.Disabled); - var contentTypeRepository = new MediaTypeRepository(scopeAccessor, AppCaches.Disabled, Logger, commonRepository); + var langRepository = new LanguageRepository(scopeAccessor, AppCaches.Disabled, Logger); + var contentTypeRepository = new MediaTypeRepository(scopeAccessor, AppCaches.Disabled, Logger, commonRepository, langRepository); return contentTypeRepository; } diff --git a/src/Umbraco.Tests/Persistence/Repositories/DocumentRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/DocumentRepositoryTest.cs index fd797662c0..4d62ec8301 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/DocumentRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/DocumentRepositoryTest.cs @@ -67,8 +67,8 @@ namespace Umbraco.Tests.Persistence.Repositories templateRepository = new TemplateRepository(scopeAccessor, appCaches, Logger, TestObjects.GetFileSystemsMock()); var tagRepository = new TagRepository(scopeAccessor, appCaches, Logger); var commonRepository = new ContentTypeCommonRepository(scopeAccessor, templateRepository, appCaches); - contentTypeRepository = new ContentTypeRepository(scopeAccessor, appCaches, Logger, commonRepository); var languageRepository = new LanguageRepository(scopeAccessor, appCaches, Logger); + contentTypeRepository = new ContentTypeRepository(scopeAccessor, appCaches, Logger, commonRepository, languageRepository); var repository = new DocumentRepository(scopeAccessor, appCaches, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository); return repository; } diff --git a/src/Umbraco.Tests/Persistence/Repositories/DomainRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/DomainRepositoryTest.cs index f00b2fd046..628f8d75a7 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/DomainRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/DomainRepositoryTest.cs @@ -23,8 +23,8 @@ namespace Umbraco.Tests.Persistence.Repositories var templateRepository = new TemplateRepository(accessor, Core.Cache.AppCaches.Disabled, Logger, TestObjects.GetFileSystemsMock()); var tagRepository = new TagRepository(accessor, Core.Cache.AppCaches.Disabled, Logger); var commonRepository = new ContentTypeCommonRepository(accessor, templateRepository, AppCaches); - contentTypeRepository = new ContentTypeRepository(accessor, Core.Cache.AppCaches.Disabled, Logger, commonRepository); languageRepository = new LanguageRepository(accessor, Core.Cache.AppCaches.Disabled, Logger); + contentTypeRepository = new ContentTypeRepository(accessor, Core.Cache.AppCaches.Disabled, Logger, commonRepository, languageRepository); documentRepository = new DocumentRepository(accessor, Core.Cache.AppCaches.Disabled, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository); var domainRepository = new DomainRepository(accessor, Core.Cache.AppCaches.Disabled, Logger); return domainRepository; diff --git a/src/Umbraco.Tests/Persistence/Repositories/LanguageRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/LanguageRepositoryTest.cs index 39f6e3e114..03c1713268 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/LanguageRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/LanguageRepositoryTest.cs @@ -79,39 +79,6 @@ namespace Umbraco.Tests.Persistence.Repositories } } - [Test] - public void Can_Perform_Get_By_Invariant_Code_On_LanguageRepository() - { - var provider = TestObjects.GetScopeProvider(Logger); - using (var scope = provider.CreateScope()) - { - var repository = CreateRepository(provider); - - var es = new CultureInfo("es"); - var esSpecific = new CultureInfo("es-ES"); - - var language = (ILanguage)new Language(es.Name) - { - CultureName = es.DisplayName, - FallbackLanguageId = 1 - }; - repository.Save(language); - - language = repository.GetByIsoCode(es.Name); - var languageSpecific = repository.GetByIsoCode(esSpecific.Name); - - // Assert - Assert.That(language, Is.Not.Null); - Assert.That(language.HasIdentity, Is.True); - Assert.That(language.IsoCode, Is.EqualTo(es.Name)); - - Assert.That(languageSpecific, Is.Not.Null); - Assert.That(languageSpecific.HasIdentity, Is.True); - Assert.That(languageSpecific.Id, Is.EqualTo(language.Id)); - Assert.That(language.IsoCode, Is.EqualTo(language.IsoCode)); - } - } - [Test] public void Get_When_Id_Doesnt_Exist_Returns_Null() { diff --git a/src/Umbraco.Tests/Persistence/Repositories/MediaRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/MediaRepositoryTest.cs index 1d9cf6d022..e2123df9e3 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/MediaRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/MediaRepositoryTest.cs @@ -38,7 +38,8 @@ namespace Umbraco.Tests.Persistence.Repositories var templateRepository = new TemplateRepository(scopeAccessor, appCaches, Logger, TestObjects.GetFileSystemsMock()); var commonRepository = new ContentTypeCommonRepository(scopeAccessor, templateRepository, appCaches); - mediaTypeRepository = new MediaTypeRepository(scopeAccessor, appCaches, Logger, commonRepository); + var languageRepository = new LanguageRepository(scopeAccessor, appCaches, Logger); + mediaTypeRepository = new MediaTypeRepository(scopeAccessor, appCaches, Logger, commonRepository, languageRepository); var tagRepository = new TagRepository(scopeAccessor, appCaches, Logger); var repository = new MediaRepository(scopeAccessor, appCaches, Logger, mediaTypeRepository, tagRepository, Mock.Of()); return repository; diff --git a/src/Umbraco.Tests/Persistence/Repositories/MediaTypeRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/MediaTypeRepositoryTest.cs index f302d1d992..bb3286daed 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/MediaTypeRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/MediaTypeRepositoryTest.cs @@ -23,7 +23,8 @@ namespace Umbraco.Tests.Persistence.Repositories var cacheHelper = AppCaches.Disabled; var templateRepository = new TemplateRepository((IScopeAccessor)provider, cacheHelper, Logger, TestObjects.GetFileSystemsMock()); var commonRepository = new ContentTypeCommonRepository((IScopeAccessor)provider, templateRepository, AppCaches); - return new MediaTypeRepository((IScopeAccessor) provider, AppCaches.Disabled, Logger, commonRepository); + var languageRepository = new LanguageRepository((IScopeAccessor)provider, AppCaches, Logger); + return new MediaTypeRepository((IScopeAccessor) provider, AppCaches.Disabled, Logger, commonRepository, languageRepository); } private EntityContainerRepository CreateContainerRepository(IScopeProvider provider) diff --git a/src/Umbraco.Tests/Persistence/Repositories/MemberRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/MemberRepositoryTest.cs index a5f7f08f22..17b16ad7ab 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/MemberRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/MemberRepositoryTest.cs @@ -31,7 +31,8 @@ namespace Umbraco.Tests.Persistence.Repositories var accessor = (IScopeAccessor) provider; var templateRepository = Mock.Of(); var commonRepository = new ContentTypeCommonRepository(accessor, templateRepository, AppCaches); - memberTypeRepository = new MemberTypeRepository(accessor, AppCaches.Disabled, Logger, commonRepository); + var languageRepository = new LanguageRepository(accessor, AppCaches.Disabled, Logger); + memberTypeRepository = new MemberTypeRepository(accessor, AppCaches.Disabled, Logger, commonRepository, languageRepository); memberGroupRepository = new MemberGroupRepository(accessor, AppCaches.Disabled, Logger); var tagRepo = new TagRepository(accessor, AppCaches.Disabled, Logger); var repository = new MemberRepository(accessor, AppCaches.Disabled, Logger, memberTypeRepository, memberGroupRepository, tagRepo, Mock.Of()); diff --git a/src/Umbraco.Tests/Persistence/Repositories/MemberTypeRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/MemberTypeRepositoryTest.cs index 79e8e43804..4b9f3096ce 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/MemberTypeRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/MemberTypeRepositoryTest.cs @@ -24,7 +24,8 @@ namespace Umbraco.Tests.Persistence.Repositories { var templateRepository = Mock.Of(); var commonRepository = new ContentTypeCommonRepository((IScopeAccessor)provider, templateRepository, AppCaches); - return new MemberTypeRepository((IScopeAccessor) provider, AppCaches.Disabled, Mock.Of(), commonRepository); + var languageRepository = new LanguageRepository((IScopeAccessor)provider, AppCaches.Disabled, Mock.Of()); + return new MemberTypeRepository((IScopeAccessor) provider, AppCaches.Disabled, Mock.Of(), commonRepository, languageRepository); } [Test] diff --git a/src/Umbraco.Tests/Persistence/Repositories/PublicAccessRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/PublicAccessRepositoryTest.cs index 803eff25af..56041c24aa 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/PublicAccessRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/PublicAccessRepositoryTest.cs @@ -308,8 +308,8 @@ namespace Umbraco.Tests.Persistence.Repositories var templateRepository = new TemplateRepository(accessor, AppCaches, Logger, TestObjects.GetFileSystemsMock()); var tagRepository = new TagRepository(accessor, AppCaches, Logger); var commonRepository = new ContentTypeCommonRepository(accessor, templateRepository, AppCaches); - contentTypeRepository = new ContentTypeRepository(accessor, AppCaches, Logger, commonRepository); var languageRepository = new LanguageRepository(accessor, AppCaches, Logger); + contentTypeRepository = new ContentTypeRepository(accessor, AppCaches, Logger, commonRepository, languageRepository); var repository = new DocumentRepository(accessor, AppCaches, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository); return repository; } diff --git a/src/Umbraco.Tests/Persistence/Repositories/TagRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/TagRepositoryTest.cs index b6cc4dc50d..e3de2c2892 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/TagRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/TagRepositoryTest.cs @@ -956,8 +956,8 @@ namespace Umbraco.Tests.Persistence.Repositories var templateRepository = new TemplateRepository(accessor, AppCaches.Disabled, Logger, TestObjects.GetFileSystemsMock()); var tagRepository = new TagRepository(accessor, AppCaches.Disabled, Logger); var commonRepository = new ContentTypeCommonRepository(accessor, templateRepository, AppCaches.Disabled); - contentTypeRepository = new ContentTypeRepository(accessor, AppCaches.Disabled, Logger, commonRepository); var languageRepository = new LanguageRepository(accessor, AppCaches.Disabled, Logger); + contentTypeRepository = new ContentTypeRepository(accessor, AppCaches.Disabled, Logger, commonRepository, languageRepository); var repository = new DocumentRepository(accessor, AppCaches.Disabled, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository); return repository; } @@ -968,7 +968,8 @@ namespace Umbraco.Tests.Persistence.Repositories var templateRepository = new TemplateRepository(accessor, AppCaches.Disabled, Logger, TestObjects.GetFileSystemsMock()); var tagRepository = new TagRepository(accessor, AppCaches.Disabled, Logger); var commonRepository = new ContentTypeCommonRepository(accessor, templateRepository, AppCaches.Disabled); - mediaTypeRepository = new MediaTypeRepository(accessor, AppCaches.Disabled, Logger, commonRepository); + var languageRepository = new LanguageRepository(accessor, AppCaches.Disabled, Logger); + mediaTypeRepository = new MediaTypeRepository(accessor, AppCaches.Disabled, Logger, commonRepository, languageRepository); var repository = new MediaRepository(accessor, AppCaches.Disabled, Logger, mediaTypeRepository, tagRepository, Mock.Of()); return repository; } diff --git a/src/Umbraco.Tests/Persistence/Repositories/TemplateRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/TemplateRepositoryTest.cs index 13cbd463fb..b0f9a5335b 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/TemplateRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/TemplateRepositoryTest.cs @@ -239,8 +239,8 @@ namespace Umbraco.Tests.Persistence.Repositories var tagRepository = new TagRepository((IScopeAccessor) ScopeProvider, AppCaches.Disabled, Logger); var commonRepository = new ContentTypeCommonRepository(ScopeProvider, templateRepository, AppCaches); - var contentTypeRepository = new ContentTypeRepository((IScopeAccessor) ScopeProvider, AppCaches.Disabled, Logger, commonRepository); var languageRepository = new LanguageRepository((IScopeAccessor) ScopeProvider, AppCaches.Disabled, Logger); + var contentTypeRepository = new ContentTypeRepository((IScopeAccessor) ScopeProvider, AppCaches.Disabled, Logger, commonRepository, languageRepository); var contentRepo = new DocumentRepository((IScopeAccessor) ScopeProvider, AppCaches.Disabled, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository); var contentType = MockedContentTypes.CreateSimpleContentType("umbTextpage2", "Textpage"); diff --git a/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs index b550091591..3e5919d7f3 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs @@ -26,7 +26,8 @@ namespace Umbraco.Tests.Persistence.Repositories var accessor = (IScopeAccessor) provider; var templateRepository = new TemplateRepository(accessor, AppCaches.Disabled, Logger, TestObjects.GetFileSystemsMock()); var commonRepository = new ContentTypeCommonRepository(accessor, templateRepository, AppCaches); - mediaTypeRepository = new MediaTypeRepository(accessor, AppCaches, Mock.Of(), commonRepository); + var languageRepository = new LanguageRepository(accessor, AppCaches, Logger); + mediaTypeRepository = new MediaTypeRepository(accessor, AppCaches, Mock.Of(), commonRepository, languageRepository); var tagRepository = new TagRepository(accessor, AppCaches, Mock.Of()); var repository = new MediaRepository(accessor, AppCaches, Mock.Of(), mediaTypeRepository, tagRepository, Mock.Of()); return repository; @@ -44,8 +45,8 @@ namespace Umbraco.Tests.Persistence.Repositories templateRepository = new TemplateRepository(accessor, AppCaches, Logger, TestObjects.GetFileSystemsMock()); var tagRepository = new TagRepository(accessor, AppCaches, Logger); var commonRepository = new ContentTypeCommonRepository(accessor, templateRepository, AppCaches); - contentTypeRepository = new ContentTypeRepository(accessor, AppCaches, Logger, commonRepository); var languageRepository = new LanguageRepository(accessor, AppCaches, Logger); + contentTypeRepository = new ContentTypeRepository(accessor, AppCaches, Logger, commonRepository, languageRepository); var repository = new DocumentRepository(accessor, AppCaches, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository); return repository; } diff --git a/src/Umbraco.Tests/Routing/RenderRouteHandlerTests.cs b/src/Umbraco.Tests/Routing/RenderRouteHandlerTests.cs index 935417c510..135172460d 100644 --- a/src/Umbraco.Tests/Routing/RenderRouteHandlerTests.cs +++ b/src/Umbraco.Tests/Routing/RenderRouteHandlerTests.cs @@ -40,7 +40,7 @@ namespace Umbraco.Tests.Routing { base.SetUp(); - WebFinalComponent.CreateRoutes( + WebInitialComponent.CreateRoutes( new TestUmbracoContextAccessor(), TestObjects.GetGlobalSettings(), new SurfaceControllerTypeCollection(Enumerable.Empty()), diff --git a/src/Umbraco.Tests/Services/ContentServicePerformanceTest.cs b/src/Umbraco.Tests/Services/ContentServicePerformanceTest.cs index ed5e6073ac..ef80672baf 100644 --- a/src/Umbraco.Tests/Services/ContentServicePerformanceTest.cs +++ b/src/Umbraco.Tests/Services/ContentServicePerformanceTest.cs @@ -166,8 +166,8 @@ namespace Umbraco.Tests.Services var tRepository = new TemplateRepository((IScopeAccessor) provider, AppCaches.Disabled, Logger, TestObjects.GetFileSystemsMock()); var tagRepo = new TagRepository((IScopeAccessor) provider, AppCaches.Disabled, Logger); var commonRepository = new ContentTypeCommonRepository((IScopeAccessor)provider, tRepository, AppCaches); - var ctRepository = new ContentTypeRepository((IScopeAccessor) provider, AppCaches.Disabled, Logger, commonRepository); var languageRepository = new LanguageRepository((IScopeAccessor) provider, AppCaches.Disabled, Logger); + var ctRepository = new ContentTypeRepository((IScopeAccessor) provider, AppCaches.Disabled, Logger, commonRepository, languageRepository); var repository = new DocumentRepository((IScopeAccessor) provider, AppCaches.Disabled, Logger, ctRepository, tRepository, tagRepo, languageRepository); // Act @@ -200,8 +200,8 @@ namespace Umbraco.Tests.Services var tRepository = new TemplateRepository((IScopeAccessor) provider, AppCaches.Disabled, Logger, TestObjects.GetFileSystemsMock()); var tagRepo = new TagRepository((IScopeAccessor) provider, AppCaches.Disabled, Logger); var commonRepository = new ContentTypeCommonRepository((IScopeAccessor)provider, tRepository, AppCaches); - var ctRepository = new ContentTypeRepository((IScopeAccessor) provider, AppCaches.Disabled, Logger, commonRepository); var languageRepository = new LanguageRepository((IScopeAccessor) provider, AppCaches.Disabled, Logger); + var ctRepository = new ContentTypeRepository((IScopeAccessor) provider, AppCaches.Disabled, Logger, commonRepository, languageRepository); var repository = new DocumentRepository((IScopeAccessor) provider, AppCaches.Disabled, Logger, ctRepository, tRepository, tagRepo, languageRepository); // Act @@ -232,8 +232,8 @@ namespace Umbraco.Tests.Services var tRepository = new TemplateRepository((IScopeAccessor) provider, AppCaches.Disabled, Logger, TestObjects.GetFileSystemsMock()); var tagRepo = new TagRepository((IScopeAccessor) provider, AppCaches.Disabled, Logger); var commonRepository = new ContentTypeCommonRepository((IScopeAccessor) provider, tRepository, AppCaches); - var ctRepository = new ContentTypeRepository((IScopeAccessor) provider, AppCaches.Disabled, Logger, commonRepository); - var languageRepository = new LanguageRepository((IScopeAccessor) provider, AppCaches.Disabled, Logger); + var languageRepository = new LanguageRepository((IScopeAccessor)provider, AppCaches.Disabled, Logger); + var ctRepository = new ContentTypeRepository((IScopeAccessor) provider, AppCaches.Disabled, Logger, commonRepository, languageRepository); var repository = new DocumentRepository((IScopeAccessor) provider, AppCaches.Disabled, Logger, ctRepository, tRepository, tagRepo, languageRepository); // Act @@ -267,8 +267,8 @@ namespace Umbraco.Tests.Services var tRepository = new TemplateRepository((IScopeAccessor) provider, AppCaches.Disabled, Logger, TestObjects.GetFileSystemsMock()); var tagRepo = new TagRepository((IScopeAccessor) provider, AppCaches.Disabled, Logger); var commonRepository = new ContentTypeCommonRepository((IScopeAccessor)provider, tRepository, AppCaches); - var ctRepository = new ContentTypeRepository((IScopeAccessor) provider, AppCaches.Disabled, Logger, commonRepository); - var languageRepository = new LanguageRepository((IScopeAccessor) provider, AppCaches.Disabled, Logger); + var languageRepository = new LanguageRepository((IScopeAccessor)provider, AppCaches.Disabled, Logger); + var ctRepository = new ContentTypeRepository((IScopeAccessor) provider, AppCaches.Disabled, Logger, commonRepository, languageRepository); var repository = new DocumentRepository((IScopeAccessor) provider, AppCaches.Disabled, Logger, ctRepository, tRepository, tagRepo, languageRepository); // Act diff --git a/src/Umbraco.Tests/Services/ContentServiceTests.cs b/src/Umbraco.Tests/Services/ContentServiceTests.cs index 222f40aeed..7cfc650510 100644 --- a/src/Umbraco.Tests/Services/ContentServiceTests.cs +++ b/src/Umbraco.Tests/Services/ContentServiceTests.cs @@ -717,21 +717,8 @@ namespace Umbraco.Tests.Services [Test] public void Can_Unpublish_Content_Variation() { - // Arrange + var content = CreateEnglishAndFrenchDocument(out var langUk, out var langFr, out var contentType); - var langUk = new Language("en-GB") { IsDefault = true }; - var langFr = new Language("fr-FR"); - - ServiceContext.LocalizationService.Save(langFr); - ServiceContext.LocalizationService.Save(langUk); - - var contentType = MockedContentTypes.CreateBasicContentType(); - contentType.Variations = ContentVariation.Culture; - ServiceContext.ContentTypeService.Save(contentType); - - IContent content = new Content("content", Constants.System.Root, contentType); - content.SetCultureName("content-fr", langFr.IsoCode); - content.SetCultureName("content-en", langUk.IsoCode); content.PublishCulture(CultureImpact.Explicit(langFr.IsoCode, langFr.IsDefault)); content.PublishCulture(CultureImpact.Explicit(langUk.IsoCode, langUk.IsDefault)); Assert.IsTrue(content.IsCulturePublished(langFr.IsoCode)); @@ -761,6 +748,185 @@ namespace Umbraco.Tests.Services } + [Test] + public void Can_Publish_Culture_After_Last_Culture_Unpublished() + { + var content = CreateEnglishAndFrenchDocument(out var langUk, out var langFr, out var contentType); + + var published = ServiceContext.ContentService.SaveAndPublish(content, new[] { langFr.IsoCode, langUk.IsoCode }); + Assert.AreEqual(PublishedState.Published, content.PublishedState); + + //re-get + content = ServiceContext.ContentService.GetById(content.Id); + + var unpublished = ServiceContext.ContentService.Unpublish(content, langUk.IsoCode); //first culture + Assert.IsTrue(unpublished.Success); + Assert.AreEqual(PublishResultType.SuccessUnpublishCulture, unpublished.Result); + Assert.IsFalse(content.IsCulturePublished(langUk.IsoCode)); + Assert.IsTrue(content.IsCulturePublished(langFr.IsoCode)); + + content = ServiceContext.ContentService.GetById(content.Id); + + unpublished = ServiceContext.ContentService.Unpublish(content, langFr.IsoCode); //last culture + Assert.IsTrue(unpublished.Success); + Assert.AreEqual(PublishResultType.SuccessUnpublishLastCulture, unpublished.Result); + Assert.IsFalse(content.IsCulturePublished(langFr.IsoCode)); + Assert.IsFalse(content.IsCulturePublished(langUk.IsoCode)); + + content = ServiceContext.ContentService.GetById(content.Id); + + published = ServiceContext.ContentService.SaveAndPublish(content, langUk.IsoCode); + Assert.AreEqual(PublishedState.Published, content.PublishedState); + Assert.IsTrue(content.IsCulturePublished(langUk.IsoCode)); + Assert.IsFalse(content.IsCulturePublished(langFr.IsoCode)); + + content = ServiceContext.ContentService.GetById(content.Id); //reget + Assert.AreEqual(PublishedState.Published, content.PublishedState); + Assert.IsTrue(content.IsCulturePublished(langUk.IsoCode)); + Assert.IsFalse(content.IsCulturePublished(langFr.IsoCode)); + + } + + + + [Test] + public void Unpublish_All_Cultures_Has_Unpublished_State() + { + var content = CreateEnglishAndFrenchDocument(out var langUk, out var langFr, out var contentType); + + var published = ServiceContext.ContentService.SaveAndPublish(content, new[] { langFr.IsoCode, langUk.IsoCode }); + Assert.IsTrue(content.IsCulturePublished(langFr.IsoCode)); + Assert.IsTrue(content.IsCulturePublished(langUk.IsoCode)); + Assert.IsTrue(published.Success); + Assert.AreEqual(PublishedState.Published, content.PublishedState); + + //re-get + content = ServiceContext.ContentService.GetById(content.Id); + Assert.IsTrue(content.IsCulturePublished(langFr.IsoCode)); + Assert.IsTrue(content.IsCulturePublished(langUk.IsoCode)); + Assert.AreEqual(PublishedState.Published, content.PublishedState); + + var unpublished = ServiceContext.ContentService.Unpublish(content, langFr.IsoCode); //first culture + Assert.IsTrue(unpublished.Success); + Assert.AreEqual(PublishResultType.SuccessUnpublishCulture, unpublished.Result); + Assert.IsFalse(content.IsCulturePublished(langFr.IsoCode)); + Assert.IsTrue(content.IsCulturePublished(langUk.IsoCode)); + Assert.AreEqual(PublishedState.Published, content.PublishedState); //still published + + //re-get + content = ServiceContext.ContentService.GetById(content.Id); + Assert.IsFalse(content.IsCulturePublished(langFr.IsoCode)); + Assert.IsTrue(content.IsCulturePublished(langUk.IsoCode)); + + unpublished = ServiceContext.ContentService.Unpublish(content, langUk.IsoCode); //last culture + Assert.IsTrue(unpublished.Success); + Assert.AreEqual(PublishResultType.SuccessUnpublishLastCulture, unpublished.Result); + Assert.IsFalse(content.IsCulturePublished(langFr.IsoCode)); + Assert.IsFalse(content.IsCulturePublished(langUk.IsoCode)); + Assert.AreEqual(PublishedState.Unpublished, content.PublishedState); //the last culture was unpublished so the document should also reflect this + + //re-get + content = ServiceContext.ContentService.GetById(content.Id); + Assert.AreEqual(PublishedState.Unpublished, content.PublishedState); //just double checking + Assert.IsFalse(content.IsCulturePublished(langFr.IsoCode)); + Assert.IsFalse(content.IsCulturePublished(langUk.IsoCode)); + } + + [Test] + public void Unpublishing_Mandatory_Language_Unpublishes_Document() + { + var langUk = new Language("en-GB") { IsDefault = true, IsMandatory = true }; + var langFr = new Language("fr-FR"); + + ServiceContext.LocalizationService.Save(langFr); + ServiceContext.LocalizationService.Save(langUk); + + var contentType = MockedContentTypes.CreateBasicContentType(); + contentType.Variations = ContentVariation.Culture; + ServiceContext.ContentTypeService.Save(contentType); + + IContent content = new Content("content", Constants.System.Root, contentType); + content.SetCultureName("content-fr", langFr.IsoCode); + content.SetCultureName("content-en", langUk.IsoCode); + + var published = ServiceContext.ContentService.SaveAndPublish(content, new[] { langFr.IsoCode, langUk.IsoCode }); + Assert.IsTrue(content.IsCulturePublished(langFr.IsoCode)); + Assert.IsTrue(content.IsCulturePublished(langUk.IsoCode)); + Assert.IsTrue(published.Success); + Assert.AreEqual(PublishedState.Published, content.PublishedState); + + //re-get + content = ServiceContext.ContentService.GetById(content.Id); + + var unpublished = ServiceContext.ContentService.Unpublish(content, langUk.IsoCode); //unpublish mandatory lang + Assert.IsTrue(unpublished.Success); + Assert.AreEqual(PublishResultType.SuccessUnpublishMandatoryCulture, unpublished.Result); + Assert.IsFalse(content.IsCulturePublished(langUk.IsoCode)); + Assert.IsTrue(content.IsCulturePublished(langFr.IsoCode)); //remains published + Assert.AreEqual(PublishedState.Unpublished, content.PublishedState); + } + + [Test] + public void Unpublishing_Already_Unpublished_Culture() + { + var content = CreateEnglishAndFrenchDocument(out var langUk, out var langFr, out var contentType); + + var published = ServiceContext.ContentService.SaveAndPublish(content, new[] { langFr.IsoCode, langUk.IsoCode }); + Assert.IsTrue(content.IsCulturePublished(langFr.IsoCode)); + Assert.IsTrue(content.IsCulturePublished(langUk.IsoCode)); + Assert.IsTrue(published.Success); + Assert.AreEqual(PublishedState.Published, content.PublishedState); + + //re-get + content = ServiceContext.ContentService.GetById(content.Id); + + var unpublished = ServiceContext.ContentService.Unpublish(content, langUk.IsoCode); + Assert.IsTrue(unpublished.Success); + Assert.AreEqual(PublishResultType.SuccessUnpublishCulture, unpublished.Result); + Assert.IsFalse(content.IsCulturePublished(langUk.IsoCode)); + + content = ServiceContext.ContentService.GetById(content.Id); + + //Change some data since Unpublish should always Save + content.SetCultureName("content-en-updated", langUk.IsoCode); + + unpublished = ServiceContext.ContentService.Unpublish(content, langUk.IsoCode); //unpublish again + Assert.IsTrue(unpublished.Success); + Assert.AreEqual(PublishResultType.SuccessUnpublishAlready, unpublished.Result); + Assert.IsFalse(content.IsCulturePublished(langUk.IsoCode)); + + content = ServiceContext.ContentService.GetById(content.Id); + //ensure that even though the culture was already unpublished that the data was still persisted + Assert.AreEqual("content-en-updated", content.GetCultureName(langUk.IsoCode)); + } + + [Test] + public void Publishing_No_Cultures_Still_Saves() + { + var content = CreateEnglishAndFrenchDocument(out var langUk, out var langFr, out var contentType); + + var published = ServiceContext.ContentService.SaveAndPublish(content, new[] { langFr.IsoCode, langUk.IsoCode }); + Assert.IsTrue(content.IsCulturePublished(langFr.IsoCode)); + Assert.IsTrue(content.IsCulturePublished(langUk.IsoCode)); + Assert.IsTrue(published.Success); + Assert.AreEqual(PublishedState.Published, content.PublishedState); + + //re-get + content = ServiceContext.ContentService.GetById(content.Id); + + //Change some data since SaveAndPublish should always Save + content.SetCultureName("content-en-updated", langUk.IsoCode); + + var saved = ServiceContext.ContentService.SaveAndPublish(content, new string [] { }); //save without cultures + Assert.AreEqual(PublishResultType.FailedPublishNothingToPublish, saved.Result); + + //re-get + content = ServiceContext.ContentService.GetById(content.Id); + //ensure that even though nothing was published that the data was still persisted + Assert.AreEqual("content-en-updated", content.GetCultureName(langUk.IsoCode)); + } + + [Test] public void Pending_Invariant_Property_Changes_Affect_Default_Language_Edited_State() { @@ -811,17 +977,7 @@ namespace Umbraco.Tests.Services [Test] public void Can_Publish_Content_Variation_And_Detect_Changed_Cultures() { - // Arrange - - var langGB = new Language("en-GB") { IsDefault = true }; - var langFr = new Language("fr-FR"); - - ServiceContext.LocalizationService.Save(langFr); - ServiceContext.LocalizationService.Save(langGB); - - var contentType = MockedContentTypes.CreateBasicContentType(); - contentType.Variations = ContentVariation.Culture; - ServiceContext.ContentTypeService.Save(contentType); + CreateEnglishAndFrenchDocumentType(out var langUk, out var langFr, out var contentType); IContent content = new Content("content", Constants.System.Root, contentType); content.SetCultureName("content-fr", langFr.IsoCode); @@ -832,8 +988,8 @@ namespace Umbraco.Tests.Services //re-get content = ServiceContext.ContentService.GetById(content.Id); - content.SetCultureName("content-en", langGB.IsoCode); - published = ServiceContext.ContentService.SaveAndPublish(content, langGB.IsoCode); + content.SetCultureName("content-en", langUk.IsoCode); + published = ServiceContext.ContentService.SaveAndPublish(content, langUk.IsoCode); //audit log will only show that english was published lastLog = ServiceContext.AuditService.GetLogs(content.Id).Last(); Assert.AreEqual($"Published languages: English (United Kingdom)", lastLog.Comment); @@ -3008,10 +3164,33 @@ namespace Umbraco.Tests.Services var templateRepository = new TemplateRepository(accessor, AppCaches.Disabled, Logger, TestObjects.GetFileSystemsMock()); var tagRepository = new TagRepository(accessor, AppCaches.Disabled, Logger); var commonRepository = new ContentTypeCommonRepository(accessor, templateRepository, AppCaches); - contentTypeRepository = new ContentTypeRepository(accessor, AppCaches.Disabled, Logger, commonRepository); var languageRepository = new LanguageRepository(accessor, AppCaches.Disabled, Logger); + contentTypeRepository = new ContentTypeRepository(accessor, AppCaches.Disabled, Logger, commonRepository, languageRepository); var repository = new DocumentRepository(accessor, AppCaches.Disabled, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository); return repository; } + + private void CreateEnglishAndFrenchDocumentType(out Language langUk, out Language langFr, out ContentType contentType) + { + langUk = new Language("en-GB") { IsDefault = true }; + langFr = new Language("fr-FR"); + ServiceContext.LocalizationService.Save(langFr); + ServiceContext.LocalizationService.Save(langUk); + + contentType = MockedContentTypes.CreateBasicContentType(); + contentType.Variations = ContentVariation.Culture; + ServiceContext.ContentTypeService.Save(contentType); + } + + private IContent CreateEnglishAndFrenchDocument(out Language langUk, out Language langFr, out ContentType contentType) + { + CreateEnglishAndFrenchDocumentType(out langUk, out langFr, out contentType); + + IContent content = new Content("content", Constants.System.Root, contentType); + content.SetCultureName("content-fr", langFr.IsoCode); + content.SetCultureName("content-en", langUk.IsoCode); + + return content; + } } } diff --git a/src/Umbraco.Tests/Services/ContentTypeServiceTests.cs b/src/Umbraco.Tests/Services/ContentTypeServiceTests.cs index a0b5f01a1f..f2a4368ae4 100644 --- a/src/Umbraco.Tests/Services/ContentTypeServiceTests.cs +++ b/src/Umbraco.Tests/Services/ContentTypeServiceTests.cs @@ -53,360 +53,7 @@ namespace Umbraco.Tests.Services Assert.IsTrue(contentType.IsElement); } - [Test] - public void Change_Content_Type_Variation_Clears_Redirects() - { - //create content type with a property type that varies by culture - var contentType = MockedContentTypes.CreateBasicContentType(); - contentType.Variations = ContentVariation.Nothing; - var contentCollection = new PropertyTypeCollection(true); - contentCollection.Add(new PropertyType("test", ValueStorageType.Ntext) - { - Alias = "title", - Name = "Title", - Description = "", - Mandatory = false, - SortOrder = 1, - DataTypeId = -88, - Variations = ContentVariation.Nothing - }); - contentType.PropertyGroups.Add(new PropertyGroup(contentCollection) { Name = "Content", SortOrder = 1 }); - ServiceContext.ContentTypeService.Save(contentType); - var contentType2 = MockedContentTypes.CreateBasicContentType("test"); - ServiceContext.ContentTypeService.Save(contentType2); - - //create some content of this content type - IContent doc = MockedContent.CreateBasicContent(contentType); - doc.Name = "Hello1"; - ServiceContext.ContentService.Save(doc); - - IContent doc2 = MockedContent.CreateBasicContent(contentType2); - ServiceContext.ContentService.Save(doc2); - - ServiceContext.RedirectUrlService.Register("hello/world", doc.Key); - ServiceContext.RedirectUrlService.Register("hello2/world2", doc2.Key); - - Assert.AreEqual(1, ServiceContext.RedirectUrlService.GetContentRedirectUrls(doc.Key).Count()); - Assert.AreEqual(1, ServiceContext.RedirectUrlService.GetContentRedirectUrls(doc2.Key).Count()); - - //change variation - contentType.Variations = ContentVariation.Culture; - ServiceContext.ContentTypeService.Save(contentType); - - Assert.AreEqual(0, ServiceContext.RedirectUrlService.GetContentRedirectUrls(doc.Key).Count()); - Assert.AreEqual(1, ServiceContext.RedirectUrlService.GetContentRedirectUrls(doc2.Key).Count()); - - } - - [Test] - public void Change_Content_Type_From_Invariant_Variant() - { - //create content type with a property type that varies by culture - var contentType = MockedContentTypes.CreateBasicContentType(); - contentType.Variations = ContentVariation.Nothing; - var contentCollection = new PropertyTypeCollection(true); - contentCollection.Add(new PropertyType("test", ValueStorageType.Ntext) - { - Alias = "title", - Name = "Title", - Description = "", - Mandatory = false, - SortOrder = 1, - DataTypeId = -88, - Variations = ContentVariation.Nothing - }); - contentType.PropertyGroups.Add(new PropertyGroup(contentCollection) { Name = "Content", SortOrder = 1 }); - ServiceContext.ContentTypeService.Save(contentType); - - //create some content of this content type - IContent doc = MockedContent.CreateBasicContent(contentType); - doc.Name = "Hello1"; - doc.SetValue("title", "hello world"); - ServiceContext.ContentService.Save(doc); - - Assert.AreEqual("Hello1", doc.Name); - Assert.AreEqual("hello world", doc.GetValue("title")); - - //change the content type to be variant, we will also update the name here to detect the copy changes - doc.Name = "Hello2"; - ServiceContext.ContentService.Save(doc); - contentType.Variations = ContentVariation.Culture; - ServiceContext.ContentTypeService.Save(contentType); - doc = ServiceContext.ContentService.GetById(doc.Id); //re-get - - Assert.AreEqual("Hello2", doc.GetCultureName("en-US")); - Assert.AreEqual("hello world", doc.GetValue("title")); //We are not checking against en-US here because properties will remain invariant - - //change back property type to be invariant, we will also update the name here to detect the copy changes - doc.SetCultureName("Hello3", "en-US"); - ServiceContext.ContentService.Save(doc); - contentType.Variations = ContentVariation.Nothing; - ServiceContext.ContentTypeService.Save(contentType); - doc = ServiceContext.ContentService.GetById(doc.Id); //re-get - - Assert.AreEqual("Hello3", doc.Name); - Assert.AreEqual("hello world", doc.GetValue("title")); - } - - [Test] - public void Change_Content_Type_From_Variant_Invariant() - { - //create content type with a property type that varies by culture - var contentType = MockedContentTypes.CreateBasicContentType(); - contentType.Variations = ContentVariation.Culture; - var contentCollection = new PropertyTypeCollection(true); - contentCollection.Add(new PropertyType("test", ValueStorageType.Ntext) - { - Alias = "title", - Name = "Title", - Description = "", - Mandatory = false, - SortOrder = 1, - DataTypeId = -88, - Variations = ContentVariation.Culture - }); - contentType.PropertyGroups.Add(new PropertyGroup(contentCollection) { Name = "Content", SortOrder = 1 }); - ServiceContext.ContentTypeService.Save(contentType); - - //create some content of this content type - IContent doc = MockedContent.CreateBasicContent(contentType); - doc.SetCultureName("Hello1", "en-US"); - doc.SetValue("title", "hello world", "en-US"); - ServiceContext.ContentService.Save(doc); - - Assert.AreEqual("Hello1", doc.GetCultureName("en-US")); - Assert.AreEqual("hello world", doc.GetValue("title", "en-US")); - - //change the content type to be invariant, we will also update the name here to detect the copy changes - doc.SetCultureName("Hello2", "en-US"); - ServiceContext.ContentService.Save(doc); - contentType.Variations = ContentVariation.Nothing; - ServiceContext.ContentTypeService.Save(contentType); - doc = ServiceContext.ContentService.GetById(doc.Id); //re-get - - Assert.AreEqual("Hello2", doc.Name); - Assert.AreEqual("hello world", doc.GetValue("title")); - - //change back property type to be variant, we will also update the name here to detect the copy changes - doc.Name = "Hello3"; - ServiceContext.ContentService.Save(doc); - contentType.Variations = ContentVariation.Culture; - ServiceContext.ContentTypeService.Save(contentType); - doc = ServiceContext.ContentService.GetById(doc.Id); //re-get - - //at this stage all property types were switched to invariant so even though the variant value - //exists it will not be returned because the property type is invariant, - //so this check proves that null will be returned - Assert.IsNull(doc.GetValue("title", "en-US")); - - //we can now switch the property type to be variant and the value can be returned again - contentType.PropertyTypes.First().Variations = ContentVariation.Culture; - ServiceContext.ContentTypeService.Save(contentType); - doc = ServiceContext.ContentService.GetById(doc.Id); //re-get - - Assert.AreEqual("Hello3", doc.GetCultureName("en-US")); - Assert.AreEqual("hello world", doc.GetValue("title", "en-US")); - - } - - [Test] - public void Change_Property_Type_From_Invariant_Variant() - { - //create content type with a property type that varies by culture - var contentType = MockedContentTypes.CreateBasicContentType(); - contentType.Variations = ContentVariation.Nothing; - var contentCollection = new PropertyTypeCollection(true); - contentCollection.Add(new PropertyType("test", ValueStorageType.Ntext) - { - Alias = "title", - Name = "Title", - Description = "", - Mandatory = false, - SortOrder = 1, - DataTypeId = -88, - Variations = ContentVariation.Nothing - }); - contentType.PropertyGroups.Add(new PropertyGroup(contentCollection) { Name = "Content", SortOrder = 1 }); - ServiceContext.ContentTypeService.Save(contentType); - - //create some content of this content type - IContent doc = MockedContent.CreateBasicContent(contentType); - doc.Name = "Home"; - doc.SetValue("title", "hello world"); - ServiceContext.ContentService.Save(doc); - - Assert.AreEqual("hello world", doc.GetValue("title")); - - //change the property type to be variant - contentType.PropertyTypes.First().Variations = ContentVariation.Culture; - ServiceContext.ContentTypeService.Save(contentType); - doc = ServiceContext.ContentService.GetById(doc.Id); //re-get - - Assert.AreEqual("hello world", doc.GetValue("title", "en-US")); - - //change back property type to be invariant - contentType.PropertyTypes.First().Variations = ContentVariation.Nothing; - ServiceContext.ContentTypeService.Save(contentType); - doc = ServiceContext.ContentService.GetById(doc.Id); //re-get - - Assert.AreEqual("hello world", doc.GetValue("title")); - } - - [Test] - public void Change_Property_Type_From_Variant_Invariant() - { - //create content type with a property type that varies by culture - var contentType = MockedContentTypes.CreateBasicContentType(); - contentType.Variations = ContentVariation.Culture; - var contentCollection = new PropertyTypeCollection(true); - contentCollection.Add(new PropertyType("test", ValueStorageType.Ntext) - { - Alias = "title", - Name = "Title", - Description = "", - Mandatory = false, - SortOrder = 1, - DataTypeId = -88, - Variations = ContentVariation.Culture - }); - contentType.PropertyGroups.Add(new PropertyGroup(contentCollection) { Name = "Content", SortOrder = 1 }); - ServiceContext.ContentTypeService.Save(contentType); - - //create some content of this content type - IContent doc = MockedContent.CreateBasicContent(contentType); - doc.SetCultureName("Home", "en-US"); - doc.SetValue("title", "hello world", "en-US"); - ServiceContext.ContentService.Save(doc); - - Assert.AreEqual("hello world", doc.GetValue("title", "en-US")); - - //change the property type to be invariant - contentType.PropertyTypes.First().Variations = ContentVariation.Nothing; - ServiceContext.ContentTypeService.Save(contentType); - doc = ServiceContext.ContentService.GetById(doc.Id); //re-get - - Assert.AreEqual("hello world", doc.GetValue("title")); - - //change back property type to be variant - contentType.PropertyTypes.First().Variations = ContentVariation.Culture; - ServiceContext.ContentTypeService.Save(contentType); - doc = ServiceContext.ContentService.GetById(doc.Id); //re-get - - Assert.AreEqual("hello world", doc.GetValue("title", "en-US")); - } - - [Test] - public void Change_Property_Type_From_Variant_Invariant_On_A_Composition() - { - //create content type with a property type that varies by culture - var contentType = MockedContentTypes.CreateBasicContentType(); - contentType.Variations = ContentVariation.Culture; - var contentCollection = new PropertyTypeCollection(true); - contentCollection.Add(new PropertyType("test", ValueStorageType.Ntext) - { - Alias = "title", - Name = "Title", - Description = "", - Mandatory = false, - SortOrder = 1, - DataTypeId = -88, - Variations = ContentVariation.Culture - }); - contentType.PropertyGroups.Add(new PropertyGroup(contentCollection) { Name = "Content", SortOrder = 1 }); - ServiceContext.ContentTypeService.Save(contentType); - - //compose this from the other one - var contentType2 = MockedContentTypes.CreateBasicContentType("test"); - contentType2.Variations = ContentVariation.Culture; - contentType2.AddContentType(contentType); - ServiceContext.ContentTypeService.Save(contentType2); - - //create some content of this content type - IContent doc = MockedContent.CreateBasicContent(contentType); - doc.SetCultureName("Home", "en-US"); - doc.SetValue("title", "hello world", "en-US"); - ServiceContext.ContentService.Save(doc); - - IContent doc2 = MockedContent.CreateBasicContent(contentType2); - doc2.SetCultureName("Home", "en-US"); - doc2.SetValue("title", "hello world", "en-US"); - ServiceContext.ContentService.Save(doc2); - - //change the property type to be invariant - contentType.PropertyTypes.First().Variations = ContentVariation.Nothing; - ServiceContext.ContentTypeService.Save(contentType); - doc = ServiceContext.ContentService.GetById(doc.Id); //re-get - doc2 = ServiceContext.ContentService.GetById(doc2.Id); //re-get - - Assert.AreEqual("hello world", doc.GetValue("title")); - Assert.AreEqual("hello world", doc2.GetValue("title")); - - //change back property type to be variant - contentType.PropertyTypes.First().Variations = ContentVariation.Culture; - ServiceContext.ContentTypeService.Save(contentType); - doc = ServiceContext.ContentService.GetById(doc.Id); //re-get - doc2 = ServiceContext.ContentService.GetById(doc2.Id); //re-get - - Assert.AreEqual("hello world", doc.GetValue("title", "en-US")); - Assert.AreEqual("hello world", doc2.GetValue("title", "en-US")); - } - - [Test] - public void Change_Content_Type_From_Variant_Invariant_On_A_Composition() - { - //create content type with a property type that varies by culture - var contentType = MockedContentTypes.CreateBasicContentType(); - contentType.Variations = ContentVariation.Culture; - var contentCollection = new PropertyTypeCollection(true); - contentCollection.Add(new PropertyType("test", ValueStorageType.Ntext) - { - Alias = "title", - Name = "Title", - Description = "", - Mandatory = false, - SortOrder = 1, - DataTypeId = -88, - Variations = ContentVariation.Culture - }); - contentType.PropertyGroups.Add(new PropertyGroup(contentCollection) { Name = "Content", SortOrder = 1 }); - ServiceContext.ContentTypeService.Save(contentType); - - //compose this from the other one - var contentType2 = MockedContentTypes.CreateBasicContentType("test"); - contentType2.Variations = ContentVariation.Culture; - contentType2.AddContentType(contentType); - ServiceContext.ContentTypeService.Save(contentType2); - - //create some content of this content type - IContent doc = MockedContent.CreateBasicContent(contentType); - doc.SetCultureName("Home", "en-US"); - doc.SetValue("title", "hello world", "en-US"); - ServiceContext.ContentService.Save(doc); - - IContent doc2 = MockedContent.CreateBasicContent(contentType2); - doc2.SetCultureName("Home", "en-US"); - doc2.SetValue("title", "hello world", "en-US"); - ServiceContext.ContentService.Save(doc2); - - //change the content type to be invariant - contentType.Variations = ContentVariation.Nothing; - ServiceContext.ContentTypeService.Save(contentType); - doc = ServiceContext.ContentService.GetById(doc.Id); //re-get - doc2 = ServiceContext.ContentService.GetById(doc2.Id); //re-get - - Assert.AreEqual("hello world", doc.GetValue("title")); - Assert.AreEqual("hello world", doc2.GetValue("title")); - - //change back content type to be variant - contentType.Variations = ContentVariation.Culture; - ServiceContext.ContentTypeService.Save(contentType); - doc = ServiceContext.ContentService.GetById(doc.Id); //re-get - doc2 = ServiceContext.ContentService.GetById(doc2.Id); //re-get - - //this will be null because the doc type was changed back to variant but it's property types don't get changed back - Assert.IsNull(doc.GetValue("title", "en-US")); - Assert.IsNull(doc2.GetValue("title", "en-US")); - } + [Test] public void Deleting_Content_Type_With_Hierarchy_Of_Content_Items_Moves_Orphaned_Content_To_Recycle_Bin() diff --git a/src/Umbraco.Tests/Services/ContentTypeServiceVariantsTests.cs b/src/Umbraco.Tests/Services/ContentTypeServiceVariantsTests.cs index 18ea95cd98..956de186be 100644 --- a/src/Umbraco.Tests/Services/ContentTypeServiceVariantsTests.cs +++ b/src/Umbraco.Tests/Services/ContentTypeServiceVariantsTests.cs @@ -17,6 +17,7 @@ using Umbraco.Core.PropertyEditors; using Umbraco.Core.Services; using Umbraco.Core.Strings; using Umbraco.Core.Sync; +using Umbraco.Tests.TestHelpers.Entities; using Umbraco.Tests.Testing; using Umbraco.Web.PublishedCache; using Umbraco.Web.PublishedCache.NuCache; @@ -106,44 +107,346 @@ namespace Umbraco.Tests.Services } } + [Test] + public void Change_Content_Type_Variation_Clears_Redirects() + { + var contentType = MockedContentTypes.CreateBasicContentType(); + contentType.Variations = ContentVariation.Nothing; + var properties = CreatePropertyCollection(("title", ContentVariation.Nothing)); + contentType.PropertyGroups.Add(new PropertyGroup(properties) { Name = "Content" }); + ServiceContext.ContentTypeService.Save(contentType); + var contentType2 = MockedContentTypes.CreateBasicContentType("test"); + ServiceContext.ContentTypeService.Save(contentType2); + + //create some content of this content type + IContent doc = MockedContent.CreateBasicContent(contentType); + doc.Name = "Hello1"; + ServiceContext.ContentService.Save(doc); + + IContent doc2 = MockedContent.CreateBasicContent(contentType2); + ServiceContext.ContentService.Save(doc2); + + ServiceContext.RedirectUrlService.Register("hello/world", doc.Key); + ServiceContext.RedirectUrlService.Register("hello2/world2", doc2.Key); + + Assert.AreEqual(1, ServiceContext.RedirectUrlService.GetContentRedirectUrls(doc.Key).Count()); + Assert.AreEqual(1, ServiceContext.RedirectUrlService.GetContentRedirectUrls(doc2.Key).Count()); + + //change variation + contentType.Variations = ContentVariation.Culture; + ServiceContext.ContentTypeService.Save(contentType); + + Assert.AreEqual(0, ServiceContext.RedirectUrlService.GetContentRedirectUrls(doc.Key).Count()); + Assert.AreEqual(1, ServiceContext.RedirectUrlService.GetContentRedirectUrls(doc2.Key).Count()); + + } + + [Test] + public void Change_Content_Type_From_Invariant_Variant() + { + var contentType = MockedContentTypes.CreateBasicContentType(); + contentType.Variations = ContentVariation.Nothing; + var properties = CreatePropertyCollection(("title", ContentVariation.Nothing)); + contentType.PropertyGroups.Add(new PropertyGroup(properties) { Name = "Content" }); + ServiceContext.ContentTypeService.Save(contentType); + + //create some content of this content type + IContent doc = MockedContent.CreateBasicContent(contentType); + doc.Name = "Hello1"; + doc.SetValue("title", "hello world"); + ServiceContext.ContentService.Save(doc); + + doc = ServiceContext.ContentService.GetById(doc.Id); //re-get + + Assert.AreEqual("Hello1", doc.Name); + Assert.AreEqual("hello world", doc.GetValue("title")); + Assert.IsTrue(doc.Edited); + Assert.IsFalse (doc.IsCultureEdited("en-US")); + + //change the content type to be variant, we will also update the name here to detect the copy changes + doc.Name = "Hello2"; + ServiceContext.ContentService.Save(doc); + contentType.Variations = ContentVariation.Culture; + ServiceContext.ContentTypeService.Save(contentType); + doc = ServiceContext.ContentService.GetById(doc.Id); //re-get + + Assert.AreEqual("Hello2", doc.GetCultureName("en-US")); + Assert.AreEqual("hello world", doc.GetValue("title")); //We are not checking against en-US here because properties will remain invariant + Assert.IsTrue(doc.Edited); + Assert.IsTrue(doc.IsCultureEdited("en-US")); + + //change back property type to be invariant, we will also update the name here to detect the copy changes + doc.SetCultureName("Hello3", "en-US"); + ServiceContext.ContentService.Save(doc); + contentType.Variations = ContentVariation.Nothing; + ServiceContext.ContentTypeService.Save(contentType); + doc = ServiceContext.ContentService.GetById(doc.Id); //re-get + + Assert.AreEqual("Hello3", doc.Name); + Assert.AreEqual("hello world", doc.GetValue("title")); + Assert.IsTrue(doc.Edited); + Assert.IsFalse(doc.IsCultureEdited("en-US")); + } + + [Test] + public void Change_Content_Type_From_Variant_Invariant() + { + var contentType = MockedContentTypes.CreateBasicContentType(); + contentType.Variations = ContentVariation.Culture; + var properties = CreatePropertyCollection(("title", ContentVariation.Culture)); + contentType.PropertyGroups.Add(new PropertyGroup(properties) { Name = "Content" }); + ServiceContext.ContentTypeService.Save(contentType); + + //create some content of this content type + IContent doc = MockedContent.CreateBasicContent(contentType); + doc.SetCultureName("Hello1", "en-US"); + doc.SetValue("title", "hello world", "en-US"); + ServiceContext.ContentService.Save(doc); + + doc = ServiceContext.ContentService.GetById(doc.Id); //re-get + Assert.AreEqual("Hello1", doc.GetCultureName("en-US")); + Assert.AreEqual("hello world", doc.GetValue("title", "en-US")); + Assert.IsTrue(doc.Edited); + Assert.IsTrue(doc.IsCultureEdited("en-US")); + + //change the content type to be invariant, we will also update the name here to detect the copy changes + doc.SetCultureName("Hello2", "en-US"); + ServiceContext.ContentService.Save(doc); + contentType.Variations = ContentVariation.Nothing; + ServiceContext.ContentTypeService.Save(contentType); + doc = ServiceContext.ContentService.GetById(doc.Id); //re-get + + Assert.AreEqual("Hello2", doc.Name); + Assert.AreEqual("hello world", doc.GetValue("title")); + Assert.IsTrue(doc.Edited); + Assert.IsFalse(doc.IsCultureEdited("en-US")); + + //change back property type to be variant, we will also update the name here to detect the copy changes + doc.Name = "Hello3"; + ServiceContext.ContentService.Save(doc); + contentType.Variations = ContentVariation.Culture; + ServiceContext.ContentTypeService.Save(contentType); + doc = ServiceContext.ContentService.GetById(doc.Id); //re-get + + //at this stage all property types were switched to invariant so even though the variant value + //exists it will not be returned because the property type is invariant, + //so this check proves that null will be returned + Assert.IsNull(doc.GetValue("title", "en-US")); + Assert.IsTrue(doc.Edited); + Assert.IsTrue(doc.IsCultureEdited("en-US")); // this is true because the name change is copied to the default language + + //we can now switch the property type to be variant and the value can be returned again + contentType.PropertyTypes.First().Variations = ContentVariation.Culture; + ServiceContext.ContentTypeService.Save(contentType); + doc = ServiceContext.ContentService.GetById(doc.Id); //re-get + + Assert.AreEqual("Hello3", doc.GetCultureName("en-US")); + Assert.AreEqual("hello world", doc.GetValue("title", "en-US")); + Assert.IsTrue(doc.Edited); + Assert.IsTrue(doc.IsCultureEdited("en-US")); + + } + + + [Test] + public void Change_Property_Type_From_To_Variant_On_Invariant_Content_Type() + { + var contentType = MockedContentTypes.CreateBasicContentType(); + contentType.Variations = ContentVariation.Nothing; + var properties = CreatePropertyCollection(("title", ContentVariation.Nothing)); + contentType.PropertyGroups.Add(new PropertyGroup(properties) { Name = "Content" }); + ServiceContext.ContentTypeService.Save(contentType); + + //change the property type to be variant + contentType.PropertyTypes.First().Variations = ContentVariation.Culture; + + //Cannot change a property type to be variant if the content type itself is not variant + Assert.Throws(() => ServiceContext.ContentTypeService.Save(contentType)); + } + + [Test] + public void Change_Property_Type_From_Invariant_Variant() + { + var contentType = MockedContentTypes.CreateBasicContentType(); + contentType.Variations = ContentVariation.Culture; + var properties = CreatePropertyCollection(("title", ContentVariation.Nothing)); + contentType.PropertyGroups.Add(new PropertyGroup(properties) { Name = "Content" }); + ServiceContext.ContentTypeService.Save(contentType); + + //create some content of this content type + IContent doc = MockedContent.CreateBasicContent(contentType); + doc.SetCultureName("Home", "en-US"); + doc.SetValue("title", "hello world"); + ServiceContext.ContentService.Save(doc); + + doc = ServiceContext.ContentService.GetById(doc.Id); //re-get + Assert.AreEqual("hello world", doc.GetValue("title")); + Assert.IsTrue(doc.IsCultureEdited("en-US")); //invariant prop changes show up on default lang + Assert.IsTrue(doc.Edited); + + //change the property type to be variant + contentType.PropertyTypes.First().Variations = ContentVariation.Culture; + ServiceContext.ContentTypeService.Save(contentType); + doc = ServiceContext.ContentService.GetById(doc.Id); //re-get + + Assert.AreEqual("hello world", doc.GetValue("title", "en-US")); + Assert.IsTrue(doc.IsCultureEdited("en-US")); + Assert.IsTrue(doc.Edited); + + //change back property type to be invariant + contentType.PropertyTypes.First().Variations = ContentVariation.Nothing; + ServiceContext.ContentTypeService.Save(contentType); + doc = ServiceContext.ContentService.GetById(doc.Id); //re-get + + Assert.AreEqual("hello world", doc.GetValue("title")); + Assert.IsTrue(doc.IsCultureEdited("en-US")); //invariant prop changes show up on default lang + Assert.IsTrue(doc.Edited); + } + + [Test] + public void Change_Property_Type_From_Variant_Invariant() + { + //create content type with a property type that varies by culture + var contentType = MockedContentTypes.CreateBasicContentType(); + contentType.Variations = ContentVariation.Culture; + var properties = CreatePropertyCollection(("title", ContentVariation.Culture)); + contentType.PropertyGroups.Add(new PropertyGroup(properties) { Name = "Content" }); + ServiceContext.ContentTypeService.Save(contentType); + + //create some content of this content type + IContent doc = MockedContent.CreateBasicContent(contentType); + doc.SetCultureName("Home", "en-US"); + doc.SetValue("title", "hello world", "en-US"); + ServiceContext.ContentService.Save(doc); + + Assert.AreEqual("hello world", doc.GetValue("title", "en-US")); + + //change the property type to be invariant + contentType.PropertyTypes.First().Variations = ContentVariation.Nothing; + ServiceContext.ContentTypeService.Save(contentType); + doc = ServiceContext.ContentService.GetById(doc.Id); //re-get + + Assert.AreEqual("hello world", doc.GetValue("title")); + + //change back property type to be variant + contentType.PropertyTypes.First().Variations = ContentVariation.Culture; + ServiceContext.ContentTypeService.Save(contentType); + doc = ServiceContext.ContentService.GetById(doc.Id); //re-get + + Assert.AreEqual("hello world", doc.GetValue("title", "en-US")); + } + + [Test] + public void Change_Property_Type_From_Variant_Invariant_On_A_Composition() + { + //create content type with a property type that varies by culture + var contentType = MockedContentTypes.CreateBasicContentType(); + contentType.Variations = ContentVariation.Culture; + var properties = CreatePropertyCollection(("title", ContentVariation.Culture)); + contentType.PropertyGroups.Add(new PropertyGroup(properties) { Name = "Content" }); + ServiceContext.ContentTypeService.Save(contentType); + + //compose this from the other one + var contentType2 = MockedContentTypes.CreateBasicContentType("test"); + contentType2.Variations = ContentVariation.Culture; + contentType2.AddContentType(contentType); + ServiceContext.ContentTypeService.Save(contentType2); + + //create some content of this content type + IContent doc = MockedContent.CreateBasicContent(contentType); + doc.SetCultureName("Home", "en-US"); + doc.SetValue("title", "hello world", "en-US"); + ServiceContext.ContentService.Save(doc); + + IContent doc2 = MockedContent.CreateBasicContent(contentType2); + doc2.SetCultureName("Home", "en-US"); + doc2.SetValue("title", "hello world", "en-US"); + ServiceContext.ContentService.Save(doc2); + + //change the property type to be invariant + contentType.PropertyTypes.First().Variations = ContentVariation.Nothing; + ServiceContext.ContentTypeService.Save(contentType); + doc = ServiceContext.ContentService.GetById(doc.Id); //re-get + doc2 = ServiceContext.ContentService.GetById(doc2.Id); //re-get + + Assert.AreEqual("hello world", doc.GetValue("title")); + Assert.AreEqual("hello world", doc2.GetValue("title")); + + //change back property type to be variant + contentType.PropertyTypes.First().Variations = ContentVariation.Culture; + ServiceContext.ContentTypeService.Save(contentType); + doc = ServiceContext.ContentService.GetById(doc.Id); //re-get + doc2 = ServiceContext.ContentService.GetById(doc2.Id); //re-get + + Assert.AreEqual("hello world", doc.GetValue("title", "en-US")); + Assert.AreEqual("hello world", doc2.GetValue("title", "en-US")); + } + + [Test] + public void Change_Content_Type_From_Variant_Invariant_On_A_Composition() + { + //create content type with a property type that varies by culture + var contentType = MockedContentTypes.CreateBasicContentType(); + contentType.Variations = ContentVariation.Culture; + var properties = CreatePropertyCollection(("title", ContentVariation.Culture)); + contentType.PropertyGroups.Add(new PropertyGroup(properties) { Name = "Content" }); + ServiceContext.ContentTypeService.Save(contentType); + + //compose this from the other one + var contentType2 = MockedContentTypes.CreateBasicContentType("test"); + contentType2.Variations = ContentVariation.Culture; + contentType2.AddContentType(contentType); + ServiceContext.ContentTypeService.Save(contentType2); + + //create some content of this content type + IContent doc = MockedContent.CreateBasicContent(contentType); + doc.SetCultureName("Home", "en-US"); + doc.SetValue("title", "hello world", "en-US"); + ServiceContext.ContentService.Save(doc); + + IContent doc2 = MockedContent.CreateBasicContent(contentType2); + doc2.SetCultureName("Home", "en-US"); + doc2.SetValue("title", "hello world", "en-US"); + ServiceContext.ContentService.Save(doc2); + + //change the content type to be invariant + contentType.Variations = ContentVariation.Nothing; + ServiceContext.ContentTypeService.Save(contentType); + doc = ServiceContext.ContentService.GetById(doc.Id); //re-get + doc2 = ServiceContext.ContentService.GetById(doc2.Id); //re-get + + Assert.AreEqual("hello world", doc.GetValue("title")); + Assert.AreEqual("hello world", doc2.GetValue("title")); + + //change back content type to be variant + contentType.Variations = ContentVariation.Culture; + ServiceContext.ContentTypeService.Save(contentType); + doc = ServiceContext.ContentService.GetById(doc.Id); //re-get + doc2 = ServiceContext.ContentService.GetById(doc2.Id); //re-get + + //this will be null because the doc type was changed back to variant but it's property types don't get changed back + Assert.IsNull(doc.GetValue("title", "en-US")); + Assert.IsNull(doc2.GetValue("title", "en-US")); + } + [Test] public void Change_Variations_SimpleContentType_VariantToInvariantAndBack() { // one simple content type, variant, with both variant and invariant properties // can change it to invariant and back - var languageEn = new Language("en") { IsDefault = true }; - ServiceContext.LocalizationService.Save(languageEn); - var languageFr = new Language("fr"); - ServiceContext.LocalizationService.Save(languageFr); + CreateFrenchAndEnglishLangs(); - var contentType = new ContentType(-1) - { - Alias = "contentType", - Name = "contentType", - Variations = ContentVariation.Culture - }; + var contentType = CreateContentType(ContentVariation.Culture); - var properties = new PropertyTypeCollection(true) - { - new PropertyType("value1", ValueStorageType.Ntext) - { - Alias = "value1", - DataTypeId = -88, - Variations = ContentVariation.Culture - }, - new PropertyType("value2", ValueStorageType.Ntext) - { - Alias = "value2", - DataTypeId = -88, - Variations = ContentVariation.Nothing - } - }; + var properties = CreatePropertyCollection( + ("value1", ContentVariation.Culture), + ("value2", ContentVariation.Nothing)); contentType.PropertyGroups.Add(new PropertyGroup(properties) { Name = "Content" }); ServiceContext.ContentTypeService.Save(contentType); - var document = (IContent) new Content("document", -1, contentType); + var document = (IContent)new Content("document", -1, contentType); document.SetCultureName("doc1en", "en"); document.SetCultureName("doc1fr", "fr"); document.SetValue("value1", "v1en", "en"); @@ -226,28 +529,11 @@ namespace Umbraco.Tests.Services var languageFr = new Language("fr"); ServiceContext.LocalizationService.Save(languageFr); - var contentType = new ContentType(-1) - { - Alias = "contentType", - Name = "contentType", - Variations = ContentVariation.Nothing - }; + var contentType = CreateContentType(ContentVariation.Nothing); - var properties = new PropertyTypeCollection(true) - { - new PropertyType("value1", ValueStorageType.Ntext) - { - Alias = "value1", - DataTypeId = -88, - Variations = ContentVariation.Nothing - }, - new PropertyType("value2", ValueStorageType.Ntext) - { - Alias = "value2", - DataTypeId = -88, - Variations = ContentVariation.Nothing - } - }; + var properties = CreatePropertyCollection( + ("value1", ContentVariation.Nothing), + ("value2", ContentVariation.Nothing)); contentType.PropertyGroups.Add(new PropertyGroup(properties) { Name = "Content" }); ServiceContext.ContentTypeService.Save(contentType); @@ -326,33 +612,13 @@ namespace Umbraco.Tests.Services // one simple content type, variant, with both variant and invariant properties // can change an invariant property to variant and back - var languageEn = new Language("en") { IsDefault = true }; - ServiceContext.LocalizationService.Save(languageEn); - var languageFr = new Language("fr"); - ServiceContext.LocalizationService.Save(languageFr); + CreateFrenchAndEnglishLangs(); - var contentType = new ContentType(-1) - { - Alias = "contentType", - Name = "contentType", - Variations = ContentVariation.Culture - }; + var contentType = CreateContentType(ContentVariation.Culture); - var properties = new PropertyTypeCollection(true) - { - new PropertyType("value1", ValueStorageType.Ntext) - { - Alias = "value1", - DataTypeId = -88, - Variations = ContentVariation.Culture - }, - new PropertyType("value2", ValueStorageType.Ntext) - { - Alias = "value2", - DataTypeId = -88, - Variations = ContentVariation.Nothing - } - }; + var properties = CreatePropertyCollection( + ("value1", ContentVariation.Culture), + ("value2", ContentVariation.Nothing)); contentType.PropertyGroups.Add(new PropertyGroup(properties) { Name = "Content" }); ServiceContext.ContentTypeService.Save(contentType); @@ -429,6 +695,185 @@ namespace Umbraco.Tests.Services "{'properties':{'value1':[{'culture':'en','seg':'','val':'v1en'},{'culture':'fr','seg':'','val':'v1fr'}],'value2':[{'culture':'en','seg':'','val':'v2'}]},'cultureData':"); } + [Test] + public void Change_Property_Variations_From_Variant_To_Invariant_And_Ensure_Edited_Values_Are_Renormalized() + { + // one simple content type, variant, with both variant and invariant properties + // can change an invariant property to variant and back + + CreateFrenchAndEnglishLangs(); + + var contentType = CreateContentType(ContentVariation.Culture); + + var properties = CreatePropertyCollection(("value1", ContentVariation.Culture)); + + contentType.PropertyGroups.Add(new PropertyGroup(properties) { Name = "Content" }); + ServiceContext.ContentTypeService.Save(contentType); + + var document = (IContent)new Content("document", -1, contentType); + document.SetCultureName("doc1en", "en"); + document.SetCultureName("doc1fr", "fr"); + document.SetValue("value1", "v1en-init", "en"); + document.SetValue("value1", "v1fr-init", "fr"); + ServiceContext.ContentService.SaveAndPublish(document); //all values are published which means the document is not 'edited' + + document = ServiceContext.ContentService.GetById(document.Id); + Assert.IsFalse(document.IsCultureEdited("en")); + Assert.IsFalse(document.IsCultureEdited("fr")); + Assert.IsFalse(document.Edited); + + document.SetValue("value1", "v1en", "en"); //change the property culture value, so now this culture will be edited + document.SetValue("value1", "v1fr", "fr"); //change the property culture value, so now this culture will be edited + ServiceContext.ContentService.Save(document); + + document = ServiceContext.ContentService.GetById(document.Id); + Assert.AreEqual("doc1en", document.Name); + Assert.AreEqual("doc1en", document.GetCultureName("en")); + Assert.AreEqual("doc1fr", document.GetCultureName("fr")); + Assert.AreEqual("v1en", document.GetValue("value1", "en")); + Assert.AreEqual("v1en-init", document.GetValue("value1", "en", published: true)); + Assert.AreEqual("v1fr", document.GetValue("value1", "fr")); + Assert.AreEqual("v1fr-init", document.GetValue("value1", "fr", published: true)); + Assert.IsTrue(document.IsCultureEdited("en")); //This will be true because the edited value isn't the same as the published value + Assert.IsTrue(document.IsCultureEdited("fr")); //This will be true because the edited value isn't the same as the published value + Assert.IsTrue(document.Edited); + + // switch property type to Invariant + contentType.PropertyTypes.First(x => x.Alias == "value1").Variations = ContentVariation.Nothing; + ServiceContext.ContentTypeService.Save(contentType); //This is going to have to re-normalize the "Edited" flag + + document = ServiceContext.ContentService.GetById(document.Id); + Assert.IsTrue(document.IsCultureEdited("en")); //This will remain true because there is now a pending change for the invariant property data which is flagged under the default lang + Assert.IsFalse(document.IsCultureEdited("fr")); //This will be false because nothing has changed for this culture and the property no longer reflects variant changes + Assert.IsTrue(document.Edited); + + //update the invariant value and publish + document.SetValue("value1", "v1inv"); + ServiceContext.ContentService.SaveAndPublish(document); + + document = ServiceContext.ContentService.GetById(document.Id); + Assert.AreEqual("doc1en", document.Name); + Assert.AreEqual("doc1en", document.GetCultureName("en")); + Assert.AreEqual("doc1fr", document.GetCultureName("fr")); + Assert.IsNull(document.GetValue("value1", "en")); //The values are there but the business logic returns null + Assert.IsNull(document.GetValue("value1", "fr")); //The values are there but the business logic returns null + Assert.IsNull(document.GetValue("value1", "en", published: true)); //The values are there but the business logic returns null + Assert.IsNull(document.GetValue("value1", "fr", published: true)); //The values are there but the business logic returns null + Assert.AreEqual("v1inv", document.GetValue("value1")); + Assert.AreEqual("v1inv", document.GetValue("value1", published: true)); + Assert.IsFalse(document.IsCultureEdited("en")); //This returns false, everything is published + Assert.IsFalse(document.IsCultureEdited("fr")); //This will be false because nothing has changed for this culture and the property no longer reflects variant changes + Assert.IsFalse(document.Edited); + + // switch property back to Culture + contentType.PropertyTypes.First(x => x.Alias == "value1").Variations = ContentVariation.Culture; + ServiceContext.ContentTypeService.Save(contentType); + + document = ServiceContext.ContentService.GetById(document.Id); + Assert.AreEqual("v1inv", document.GetValue("value1", "en")); //The invariant property value gets copied over to the default language + Assert.AreEqual("v1inv", document.GetValue("value1", "en", published: true)); + Assert.AreEqual("v1fr", document.GetValue("value1", "fr")); //values are still retained + Assert.AreEqual("v1fr-init", document.GetValue("value1", "fr", published: true)); //values are still retained + Assert.IsFalse(document.IsCultureEdited("en")); //The invariant published AND edited values are copied over to the default language + Assert.IsTrue(document.IsCultureEdited("fr")); //The previously existing french values are there and there is no published value + Assert.IsTrue(document.Edited); //Will be flagged edited again because the french culture had pending changes + + // publish again + document.SetValue("value1", "v1en2", "en"); //update the value now that it's variant again + document.SetValue("value1", "v1fr2", "fr"); //update the value now that it's variant again + ServiceContext.ContentService.SaveAndPublish(document); + + document = ServiceContext.ContentService.GetById(document.Id); + Assert.AreEqual("doc1en", document.Name); + Assert.AreEqual("doc1en", document.GetCultureName("en")); + Assert.AreEqual("doc1fr", document.GetCultureName("fr")); + Assert.AreEqual("v1en2", document.GetValue("value1", "en")); + Assert.AreEqual("v1fr2", document.GetValue("value1", "fr")); + Assert.IsNull(document.GetValue("value1")); //The value is there but the business logic returns null + Assert.IsFalse(document.IsCultureEdited("en")); //This returns false, the variant property value has been published + Assert.IsFalse(document.IsCultureEdited("fr")); //This returns false, the variant property value has been published + Assert.IsFalse(document.Edited); + } + + [Test] + public void Change_Property_Variations_From_Invariant_To_Variant_And_Ensure_Edited_Values_Are_Renormalized() + { + // one simple content type, variant, with both variant and invariant properties + // can change an invariant property to variant and back + + CreateFrenchAndEnglishLangs(); + + var contentType = CreateContentType(ContentVariation.Culture); + + var properties = CreatePropertyCollection(("value1", ContentVariation.Nothing)); + + contentType.PropertyGroups.Add(new PropertyGroup(properties) { Name = "Content" }); + ServiceContext.ContentTypeService.Save(contentType); + + var document = (IContent)new Content("document", -1, contentType); + document.SetCultureName("doc1en", "en"); + document.SetCultureName("doc1fr", "fr"); + document.SetValue("value1", "v1en-init"); + ServiceContext.ContentService.SaveAndPublish(document); //all values are published which means the document is not 'edited' + + document = ServiceContext.ContentService.GetById(document.Id); + Assert.IsFalse(document.IsCultureEdited("en")); + Assert.IsFalse(document.IsCultureEdited("fr")); + Assert.IsFalse(document.Edited); + + document.SetValue("value1", "v1en"); //change the property value, so now the invariant (default) culture will be edited + ServiceContext.ContentService.Save(document); + + document = ServiceContext.ContentService.GetById(document.Id); + Assert.AreEqual("doc1en", document.Name); + Assert.AreEqual("doc1en", document.GetCultureName("en")); + Assert.AreEqual("doc1fr", document.GetCultureName("fr")); + Assert.AreEqual("v1en", document.GetValue("value1")); + Assert.AreEqual("v1en-init", document.GetValue("value1", published: true)); + Assert.IsTrue(document.IsCultureEdited("en")); //This is true because the invariant property reflects changes on the default lang + Assert.IsFalse(document.IsCultureEdited("fr")); + Assert.IsTrue(document.Edited); + + // switch property type to Culture + contentType.PropertyTypes.First(x => x.Alias == "value1").Variations = ContentVariation.Culture; + ServiceContext.ContentTypeService.Save(contentType); //This is going to have to re-normalize the "Edited" flag + + document = ServiceContext.ContentService.GetById(document.Id); + Assert.IsTrue(document.IsCultureEdited("en")); //Remains true + Assert.IsFalse(document.IsCultureEdited("fr")); //False because no french property has ever been edited + Assert.IsTrue(document.Edited); + + //update the culture value and publish + document.SetValue("value1", "v1en2", "en"); + ServiceContext.ContentService.SaveAndPublish(document); + + document = ServiceContext.ContentService.GetById(document.Id); + Assert.AreEqual("doc1en", document.Name); + Assert.AreEqual("doc1en", document.GetCultureName("en")); + Assert.AreEqual("doc1fr", document.GetCultureName("fr")); + Assert.IsNull(document.GetValue("value1")); //The values are there but the business logic returns null + Assert.IsNull(document.GetValue("value1", published: true)); //The values are there but the business logic returns null + Assert.AreEqual("v1en2", document.GetValue("value1", "en")); + Assert.AreEqual("v1en2", document.GetValue("value1", "en", published: true)); + Assert.IsFalse(document.IsCultureEdited("en")); //This returns false, everything is published + Assert.IsFalse(document.IsCultureEdited("fr")); //False because no french property has ever been edited + Assert.IsFalse(document.Edited); + + // switch property back to Invariant + contentType.PropertyTypes.First(x => x.Alias == "value1").Variations = ContentVariation.Nothing; + ServiceContext.ContentTypeService.Save(contentType); + + document = ServiceContext.ContentService.GetById(document.Id); + Assert.AreEqual("v1en2", document.GetValue("value1")); //The variant property value gets copied over to the invariant + Assert.AreEqual("v1en2", document.GetValue("value1", published: true)); + Assert.IsNull(document.GetValue("value1", "fr")); //The values are there but the business logic returns null + Assert.IsNull(document.GetValue("value1", "fr", published: true)); //The values are there but the business logic returns null + Assert.IsFalse(document.IsCultureEdited("en")); //The variant published AND edited values are copied over to the invariant + Assert.IsFalse(document.IsCultureEdited("fr")); + Assert.IsFalse(document.Edited); + + } + [Test] public void Change_Variations_ComposedContentType_1() { @@ -437,59 +882,22 @@ namespace Umbraco.Tests.Services // can change the composing content type to invariant and back // can change the composed content type to invariant and back - var languageEn = new Language("en") { IsDefault = true }; - ServiceContext.LocalizationService.Save(languageEn); - var languageFr = new Language("fr"); - ServiceContext.LocalizationService.Save(languageFr); + CreateFrenchAndEnglishLangs(); - var composing = new ContentType(-1) - { - Alias = "composing", - Name = "composing", - Variations = ContentVariation.Culture - }; + var composing = CreateContentType(ContentVariation.Culture, "composing"); - var properties1 = new PropertyTypeCollection(true) - { - new PropertyType("value11", ValueStorageType.Ntext) - { - Alias = "value11", - DataTypeId = -88, - Variations = ContentVariation.Culture - }, - new PropertyType("value12", ValueStorageType.Ntext) - { - Alias = "value12", - DataTypeId = -88, - Variations = ContentVariation.Nothing - } - }; + var properties1 = CreatePropertyCollection( + ("value11", ContentVariation.Culture), + ("value12", ContentVariation.Nothing)); composing.PropertyGroups.Add(new PropertyGroup(properties1) { Name = "Content" }); ServiceContext.ContentTypeService.Save(composing); - var composed = new ContentType(-1) - { - Alias = "composed", - Name = "composed", - Variations = ContentVariation.Culture - }; + var composed = CreateContentType(ContentVariation.Culture, "composed"); - var properties2 = new PropertyTypeCollection(true) - { - new PropertyType("value21", ValueStorageType.Ntext) - { - Alias = "value21", - DataTypeId = -88, - Variations = ContentVariation.Culture - }, - new PropertyType("value22", ValueStorageType.Ntext) - { - Alias = "value22", - DataTypeId = -88, - Variations = ContentVariation.Nothing - } - }; + var properties2 = CreatePropertyCollection( + ("value21", ContentVariation.Culture), + ("value22", ContentVariation.Nothing)); composed.PropertyGroups.Add(new PropertyGroup(properties2) { Name = "Content" }); composed.AddContentType(composing); @@ -569,86 +977,32 @@ namespace Umbraco.Tests.Services // can change the composing content type to invariant and back // can change the variant composed content type to invariant and back - var languageEn = new Language("en") { IsDefault = true }; - ServiceContext.LocalizationService.Save(languageEn); - var languageFr = new Language("fr"); - ServiceContext.LocalizationService.Save(languageFr); + CreateFrenchAndEnglishLangs(); - var composing = new ContentType(-1) - { - Alias = "composing", - Name = "composing", - Variations = ContentVariation.Culture - }; + var composing = CreateContentType(ContentVariation.Culture, "composing"); - var properties1 = new PropertyTypeCollection(true) - { - new PropertyType("value11", ValueStorageType.Ntext) - { - Alias = "value11", - DataTypeId = -88, - Variations = ContentVariation.Culture - }, - new PropertyType("value12", ValueStorageType.Ntext) - { - Alias = "value12", - DataTypeId = -88, - Variations = ContentVariation.Nothing - } - }; + var properties1 = CreatePropertyCollection( + ("value11", ContentVariation.Culture), + ("value12", ContentVariation.Nothing)); composing.PropertyGroups.Add(new PropertyGroup(properties1) { Name = "Content" }); ServiceContext.ContentTypeService.Save(composing); - var composed1 = new ContentType(-1) - { - Alias = "composed1", - Name = "composed1", - Variations = ContentVariation.Culture - }; + var composed1 = CreateContentType(ContentVariation.Culture, "composed1"); - var properties2 = new PropertyTypeCollection(true) - { - new PropertyType("value21", ValueStorageType.Ntext) - { - Alias = "value21", - DataTypeId = -88, - Variations = ContentVariation.Culture - }, - new PropertyType("value22", ValueStorageType.Ntext) - { - Alias = "value22", - DataTypeId = -88, - Variations = ContentVariation.Nothing - } - }; + var properties2 = CreatePropertyCollection( + ("value21", ContentVariation.Culture), + ("value22", ContentVariation.Nothing)); composed1.PropertyGroups.Add(new PropertyGroup(properties2) { Name = "Content" }); composed1.AddContentType(composing); ServiceContext.ContentTypeService.Save(composed1); - var composed2 = new ContentType(-1) - { - Alias = "composed2", - Name = "composed2", - Variations = ContentVariation.Nothing - }; + var composed2 = CreateContentType(ContentVariation.Nothing, "composed2"); - var properties3 = new PropertyTypeCollection(true) - { - new PropertyType("value31", ValueStorageType.Ntext) - { - Alias = "value31", - DataTypeId = -88, - Variations = ContentVariation.Nothing - }, - new PropertyType("value32", ValueStorageType.Ntext) - { - Alias = "value32", - DataTypeId = -88, - Variations = ContentVariation.Nothing - } - }; + var properties3 = CreatePropertyCollection( + ("value31", ContentVariation.Nothing), + ("value32", ContentVariation.Nothing)); composed2.PropertyGroups.Add(new PropertyGroup(properties3) { Name = "Content" }); composed2.AddContentType(composing); @@ -754,5 +1108,35 @@ namespace Umbraco.Tests.Services AssertJsonStartsWith(document2.Id, "{'properties':{'value11':[{'culture':'','seg':'','val':'v11'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value31':[{'culture':'','seg':'','val':'v31'}],'value32':[{'culture':'','seg':'','val':'v32'}]},'cultureData':"); } + + private void CreateFrenchAndEnglishLangs() + { + var languageEn = new Language("en") { IsDefault = true }; + ServiceContext.LocalizationService.Save(languageEn); + var languageFr = new Language("fr"); + ServiceContext.LocalizationService.Save(languageFr); + } + + private IContentType CreateContentType(ContentVariation variance, string alias = "contentType") => new ContentType(-1) + { + Alias = alias, + Name = alias, + Variations = variance + }; + + private PropertyTypeCollection CreatePropertyCollection(params (string alias, ContentVariation variance)[] props) + { + var propertyCollection = new PropertyTypeCollection(true); + + foreach (var (alias, variance) in props) + propertyCollection.Add(new PropertyType(alias, ValueStorageType.Ntext) + { + Alias = alias, + DataTypeId = -88, + Variations = variance + }); + + return propertyCollection; + } } } diff --git a/src/Umbraco.Tests/Services/MemberServiceTests.cs b/src/Umbraco.Tests/Services/MemberServiceTests.cs index cb55a891a7..340ada2519 100644 --- a/src/Umbraco.Tests/Services/MemberServiceTests.cs +++ b/src/Umbraco.Tests/Services/MemberServiceTests.cs @@ -11,15 +11,20 @@ using Umbraco.Core.Events; using Umbraco.Core.Models; using Umbraco.Core.Models.Entities; using Umbraco.Core.Models.Membership; +using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.DatabaseModelDefinitions; using Umbraco.Core.Persistence.Dtos; using Umbraco.Core.Persistence.Querying; +using Umbraco.Core.PropertyEditors; using Umbraco.Core.Services; using Umbraco.Core.Services.Implement; using Umbraco.Tests.LegacyXmlPublishedCache; using Umbraco.Tests.TestHelpers.Entities; using Umbraco.Tests.Testing; +using Umbraco.Tests.Testing.Objects.Accessors; +using Umbraco.Web; +using Umbraco.Web.PublishedCache.NuCache; using Umbraco.Web.Security.Providers; namespace Umbraco.Tests.Services @@ -43,6 +48,61 @@ namespace Umbraco.Tests.Services ((MemberService)ServiceContext.MemberService).MembershipProvider = provider; } + [Test] + public void Can_Create_Member_With_Properties() + { + var memberType = ServiceContext.MemberTypeService.Get("member"); + IMember member = new Member("xname", "xemail", "xusername", "xrawpassword", memberType, true); + ServiceContext.MemberService.Save(member); + + member = ServiceContext.MemberService.GetById(member.Id); + Assert.AreEqual("xemail", member.Email); + + var dataTypeService = ServiceContext.DataTypeService; + var contentTypeFactory = new PublishedContentTypeFactory(new NoopPublishedModelFactory(), new PropertyValueConverterCollection(Enumerable.Empty()), dataTypeService); + var pmemberType = new PublishedContentType(memberType, contentTypeFactory); + + var publishedSnapshotAccessor = new TestPublishedSnapshotAccessor(); + var variationContextAccessor = new TestVariationContextAccessor(); + var pmember = PublishedMember.Create(member, pmemberType, false, publishedSnapshotAccessor, variationContextAccessor); + + // contains the umbracoMember... properties created when installing, on the member type + // contains the other properties, that PublishedContentType adds (BuiltinMemberProperties) + // + // TODO: see TODO in PublishedContentType, this list contains duplicates + + var aliases = new[] + { + "umbracoMemberPasswordRetrievalQuestion", + "umbracoMemberPasswordRetrievalAnswer", + "umbracoMemberComments", + "umbracoMemberFailedPasswordAttempts", + "umbracoMemberApproved", + "umbracoMemberLockedOut", + "umbracoMemberLastLockoutDate", + "umbracoMemberLastLogin", + "umbracoMemberLastPasswordChangeDate", + "Email", + "Username", + "PasswordQuestion", + "Comments", + "IsApproved", + "IsLockedOut", + "LastLockoutDate", + "CreateDate", + "LastLoginDate", + "LastPasswordChangeDate" + }; + + var properties = pmember.Properties.ToList(); + + for (var i = 0; i < aliases.Length; i++) + Assert.AreEqual(properties[i].Alias, aliases[i]); + + var email = properties[aliases.IndexOf("Email")]; + Assert.AreEqual("xemail", email.GetSourceValue()); + } + [Test] public void Can_Set_Password_On_New_Member() { diff --git a/src/Umbraco.Tests/TestHelpers/ControllerTesting/TestRunner.cs b/src/Umbraco.Tests/TestHelpers/ControllerTesting/TestRunner.cs index 8c598281dd..9f9f933d72 100644 --- a/src/Umbraco.Tests/TestHelpers/ControllerTesting/TestRunner.cs +++ b/src/Umbraco.Tests/TestHelpers/ControllerTesting/TestRunner.cs @@ -25,19 +25,23 @@ namespace Umbraco.Tests.TestHelpers.ControllerTesting public async Task> Execute(string controllerName, string actionName, HttpMethod method, HttpContent content = null, MediaTypeWithQualityHeaderValue mediaTypeHeader = null, - bool assertOkResponse = true) + bool assertOkResponse = true, object routeDefaults = null, string url = null) { if (mediaTypeHeader == null) { mediaTypeHeader = new MediaTypeWithQualityHeaderValue("application/json"); } + if (routeDefaults == null) + { + routeDefaults = new { controller = controllerName, action = actionName, id = RouteParameter.Optional }; + } var startup = new TestStartup( configuration => { configuration.Routes.MapHttpRoute("Default", routeTemplate: "{controller}/{action}/{id}", - defaults: new { controller = controllerName, action = actionName, id = RouteParameter.Optional }); + defaults: routeDefaults); }, _controllerFactory); @@ -45,7 +49,7 @@ namespace Umbraco.Tests.TestHelpers.ControllerTesting { var request = new HttpRequestMessage { - RequestUri = new Uri("https://testserver/"), + RequestUri = new Uri("https://testserver/" + (url ?? "")), Method = method }; diff --git a/src/Umbraco.Tests/Testing/TestOptionAttributeBase.cs b/src/Umbraco.Tests/Testing/TestOptionAttributeBase.cs index db7dd25152..9f72a022f3 100644 --- a/src/Umbraco.Tests/Testing/TestOptionAttributeBase.cs +++ b/src/Umbraco.Tests/Testing/TestOptionAttributeBase.cs @@ -2,6 +2,7 @@ using System.Linq; using System.Reflection; using NUnit.Framework; +using Umbraco.Core.Exceptions; namespace Umbraco.Tests.Testing { @@ -29,7 +30,7 @@ namespace Umbraco.Tests.Testing var methodName = test.MethodName; var type = Type.GetType(typeName, true); if (type == null) - throw new Exception("panic"); // makes no sense + throw new PanicException($"Could not resolve the type from type name {typeName}"); // makes no sense var methodInfo = type.GetMethod(methodName); // what about overloads? var options = GetTestOptions(methodInfo); return options; @@ -53,7 +54,7 @@ namespace Umbraco.Tests.Testing { if (other == null) throw new ArgumentNullException(nameof(other)); if (!(Merge((TestOptionAttributeBase) other) is TOptions merged)) - throw new Exception("panic"); + throw new PanicException("Could not merge test options"); return merged; } diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index 717006b702..f788168ddc 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -243,6 +243,7 @@ + diff --git a/src/Umbraco.Tests/Web/Controllers/AuthenticationControllerTests.cs b/src/Umbraco.Tests/Web/Controllers/AuthenticationControllerTests.cs new file mode 100644 index 0000000000..3d264663b5 --- /dev/null +++ b/src/Umbraco.Tests/Web/Controllers/AuthenticationControllerTests.cs @@ -0,0 +1,139 @@ +using System; +using System.Collections.Concurrent; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Reflection; +using System.Security.Cryptography; +using System.Threading; +using System.Web; +using System.Web.Hosting; +using System.Web.Http; +using Moq; +using Newtonsoft.Json; +using NUnit.Framework; +using Umbraco.Core; +using Umbraco.Core.Cache; +using Umbraco.Core.Composing; +using Umbraco.Core.Configuration; +using Umbraco.Core.IO; +using Umbraco.Core.Logging; +using Umbraco.Core.Persistence; +using Umbraco.Core.Persistence.Mappers; +using Umbraco.Core.Persistence.Querying; +using Umbraco.Core.Persistence.SqlSyntax; +using Umbraco.Core.Services; +using Umbraco.Tests.TestHelpers; +using Umbraco.Tests.TestHelpers.ControllerTesting; +using Umbraco.Tests.Testing; +using Umbraco.Web; +using Umbraco.Web.Editors; +using Umbraco.Web.Features; +using Umbraco.Web.Models.ContentEditing; +using IUser = Umbraco.Core.Models.Membership.IUser; + +namespace Umbraco.Tests.Web.Controllers +{ + [TestFixture] + [UmbracoTest(Database = UmbracoTestOptions.Database.None)] + public class AuthenticationControllerTests : TestWithDatabaseBase + { + protected override void ComposeApplication(bool withApplication) + { + base.ComposeApplication(withApplication); + //if (!withApplication) return; + + // replace the true IUserService implementation with a mock + // so that each test can configure the service to their liking + Composition.RegisterUnique(f => Mock.Of()); + + // kill the true IEntityService too + Composition.RegisterUnique(f => Mock.Of()); + + Composition.RegisterUnique(); + } + + + [Test] + public async System.Threading.Tasks.Task GetCurrentUser_Fips() + { + ApiController CtrlFactory(HttpRequestMessage message, IUmbracoContextAccessor umbracoContextAccessor, UmbracoHelper helper) + { + //setup some mocks + var userServiceMock = Mock.Get(Current.Services.UserService); + userServiceMock.Setup(service => service.GetUserById(It.IsAny())) + .Returns(() => null); + + if (Thread.GetDomain().GetData(".appPath") != null) + { + HttpContext.Current = new HttpContext(new SimpleWorkerRequest("", "", new StringWriter())); + } + else + { + var baseDir = IOHelper.MapPath("", false).TrimEnd(IOHelper.DirSepChar); + HttpContext.Current = new HttpContext(new SimpleWorkerRequest("/", baseDir, "", "", new StringWriter())); + } + IOHelper.ForceNotHosted = true; + var usersController = new AuthenticationController( + Factory.GetInstance(), + umbracoContextAccessor, + Factory.GetInstance(), + Factory.GetInstance(), + Factory.GetInstance(), + Factory.GetInstance(), + Factory.GetInstance(), + helper); + return usersController; + } + + Mock.Get(Current.SqlContext) + .Setup(x => x.Query()) + .Returns(new Query(Current.SqlContext)); + + var syntax = new SqlCeSyntaxProvider(); + + Mock.Get(Current.SqlContext) + .Setup(x => x.SqlSyntax) + .Returns(syntax); + + var mappers = new MapperCollection(new[] + { + new UserMapper(new Lazy(() => Current.SqlContext), new ConcurrentDictionary>()) + }); + + Mock.Get(Current.SqlContext) + .Setup(x => x.Mappers) + .Returns(mappers); + + // Testing what happens if the system were configured to only use FIPS-compliant algorithms + var typ = typeof(CryptoConfig); + var flds = typ.GetFields(BindingFlags.Static | BindingFlags.NonPublic); + var haveFld = flds.FirstOrDefault(f => f.Name == "s_haveFipsAlgorithmPolicy"); + var isFld = flds.FirstOrDefault(f => f.Name == "s_fipsAlgorithmPolicy"); + var originalFipsValue = CryptoConfig.AllowOnlyFipsAlgorithms; + + try + { + if (!originalFipsValue) + { + haveFld.SetValue(null, true); + isFld.SetValue(null, true); + } + + var runner = new TestRunner(CtrlFactory); + var response = await runner.Execute("Authentication", "GetCurrentUser", HttpMethod.Get); + + var obj = JsonConvert.DeserializeObject(response.Item2); + Assert.AreEqual(-1, obj.UserId); + } + finally + { + if (!originalFipsValue) + { + haveFld.SetValue(null, false); + isFld.SetValue(null, false); + } + } + } + } +} diff --git a/src/Umbraco.Tests/Web/Controllers/UsersControllerTests.cs b/src/Umbraco.Tests/Web/Controllers/UsersControllerTests.cs index a4c3078b8f..85dd303432 100644 --- a/src/Umbraco.Tests/Web/Controllers/UsersControllerTests.cs +++ b/src/Umbraco.Tests/Web/Controllers/UsersControllerTests.cs @@ -4,6 +4,8 @@ using System.Collections.Generic; using System.Linq; using System.Net.Http; using System.Net.Http.Formatting; +using System.Reflection; +using System.Security.Cryptography; using System.Web.Http; using Moq; using Newtonsoft.Json; @@ -155,7 +157,7 @@ namespace Umbraco.Tests.Web.Controllers var runner = new TestRunner(CtrlFactory); var response = await runner.Execute("Users", "GetPagedUsers", HttpMethod.Get); - var obj = JsonConvert.DeserializeObject>(response.Item2); + var obj = JsonConvert.DeserializeObject>(response.Item2); Assert.AreEqual(0, obj.TotalItems); } @@ -190,9 +192,100 @@ namespace Umbraco.Tests.Web.Controllers var runner = new TestRunner(CtrlFactory); var response = await runner.Execute("Users", "GetPagedUsers", HttpMethod.Get); - var obj = JsonConvert.DeserializeObject>(response.Item2); + var obj = JsonConvert.DeserializeObject>(response.Item2); Assert.AreEqual(10, obj.TotalItems); Assert.AreEqual(10, obj.Items.Count()); } + + [Test] + public async System.Threading.Tasks.Task GetPagedUsers_Fips() + { + await RunFipsTest("GetPagedUsers", mock => + { + var users = MockedUser.CreateMulipleUsers(10); + long outVal = 10; + mock.Setup(service => service.GetAll( + It.IsAny(), It.IsAny(), out outVal, It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>())) + .Returns(() => users); + }, response => + { + var obj = JsonConvert.DeserializeObject>(response.Item2); + Assert.AreEqual(10, obj.TotalItems); + Assert.AreEqual(10, obj.Items.Count()); + }); + } + + [Test] + public async System.Threading.Tasks.Task GetById_Fips() + { + const int mockUserId = 1234; + var user = MockedUser.CreateUser(); + + await RunFipsTest("GetById", mock => + { + mock.Setup(service => service.GetUserById(1234)) + .Returns((int i) => i == mockUserId ? user : null); + }, response => + { + var obj = JsonConvert.DeserializeObject(response.Item2); + Assert.AreEqual(user.Username, obj.Username); + Assert.AreEqual(user.Email, obj.Email); + }, new { controller = "Users", action = "GetById" }, $"Users/GetById/{mockUserId}"); + } + + + private async System.Threading.Tasks.Task RunFipsTest(string action, Action> userServiceSetup, + Action> verification, + object routeDefaults = null, string url = null) + { + ApiController CtrlFactory(HttpRequestMessage message, IUmbracoContextAccessor umbracoContextAccessor, UmbracoHelper helper) + { + //setup some mocks + var userServiceMock = Mock.Get(Current.Services.UserService); + userServiceSetup(userServiceMock); + + var usersController = new UsersController( + Factory.GetInstance(), + umbracoContextAccessor, + Factory.GetInstance(), + Factory.GetInstance(), + Factory.GetInstance(), + Factory.GetInstance(), + Factory.GetInstance(), + helper); + return usersController; + } + + // Testing what happens if the system were configured to only use FIPS-compliant algorithms + var typ = typeof(CryptoConfig); + var flds = typ.GetFields(BindingFlags.Static | BindingFlags.NonPublic); + var haveFld = flds.FirstOrDefault(f => f.Name == "s_haveFipsAlgorithmPolicy"); + var isFld = flds.FirstOrDefault(f => f.Name == "s_fipsAlgorithmPolicy"); + var originalFipsValue = CryptoConfig.AllowOnlyFipsAlgorithms; + + try + { + if (!originalFipsValue) + { + haveFld.SetValue(null, true); + isFld.SetValue(null, true); + } + + MockForGetPagedUsers(); + + var runner = new TestRunner(CtrlFactory); + var response = await runner.Execute("Users", action, HttpMethod.Get, routeDefaults: routeDefaults, url: url); + verification(response); + } + finally + { + if (!originalFipsValue) + { + haveFld.SetValue(null, false); + isFld.SetValue(null, false); + } + } + } } } diff --git a/src/Umbraco.Web.UI.Client/gulp/config.js b/src/Umbraco.Web.UI.Client/gulp/config.js index 4dae0dac08..705074a18f 100755 --- a/src/Umbraco.Web.UI.Client/gulp/config.js +++ b/src/Umbraco.Web.UI.Client/gulp/config.js @@ -16,13 +16,31 @@ module.exports = { //processed in the js task js: { preview: { files: ["./src/preview/**/*.js"], out: "umbraco.preview.js" }, - installer: { files: ["./src/installer/**/*.js"], out: "umbraco.installer.js" }, - controllers: { files: ["./src/{views,controllers}/**/*.controller.js"], out: "umbraco.controllers.js" }, - directives: { files: ["./src/common/directives/**/*.js"], out: "umbraco.directives.js" }, + installer: { files: ["./src/installer/**/*.js"], out: "umbraco.installer.js" }, filters: { files: ["./src/common/filters/**/*.js"], out: "umbraco.filters.js" }, resources: { files: ["./src/common/resources/**/*.js"], out: "umbraco.resources.js" }, services: { files: ["./src/common/services/**/*.js"], out: "umbraco.services.js" }, - security: { files: ["./src/common/interceptors/**/*.js"], out: "umbraco.interceptors.js" } + security: { files: ["./src/common/interceptors/**/*.js"], out: "umbraco.interceptors.js" }, + + //the controllers for views + controllers: { + files: [ + "./src/views/**/*.controller.js", + "./src/*.controller.js" + ], out: "umbraco.controllers.js" + }, + + //directives/components + // - any JS file found in common / directives or common/ components + // - any JS file found inside views that has the suffix .directive.js or .component.js + directives: { + files: [ + "./src/common/directives/_module.js", + "./src/{common/directives,common/components}/**/*.js", + "./src/views/**/*.{directive,component}.js" + ], + out: "umbraco.directives.js" + }, }, //selectors for copying all views into the build @@ -34,7 +52,7 @@ module.exports = { //globs for file-watching globs:{ - views: "./src/views/**/*.html", + views: ["./src/views/**/*.html", "./src/common/directives/**/*.html", "./src/common/components/**/*.html" ], less: "./src/less/**/*.less", js: "./src/*.js", lib: "./lib/**/*", diff --git a/src/Umbraco.Web.UI.Client/gulp/tasks/dev.js b/src/Umbraco.Web.UI.Client/gulp/tasks/dev.js index bca4da8c43..8b07f5156e 100644 --- a/src/Umbraco.Web.UI.Client/gulp/tasks/dev.js +++ b/src/Umbraco.Web.UI.Client/gulp/tasks/dev.js @@ -5,6 +5,9 @@ var gulp = require('gulp'); var runSequence = require('run-sequence'); // Dev - build the files ready for development and start watchers -gulp.task('dev', function(cb) { +gulp.task('dev', function (cb) { + + global.isProd = false; + runSequence(["dependencies", "js", "less", "views"], "watch", cb); }); diff --git a/src/Umbraco.Web.UI.Client/gulp/tasks/fastdev.js b/src/Umbraco.Web.UI.Client/gulp/tasks/fastdev.js deleted file mode 100644 index 888ed38fec..0000000000 --- a/src/Umbraco.Web.UI.Client/gulp/tasks/fastdev.js +++ /dev/null @@ -1,13 +0,0 @@ -'use strict'; - -var config = require('../config'); -var gulp = require('gulp'); -var runSequence = require('run-sequence'); - -// Dev - build the files ready for development and start watchers -gulp.task('fastdev', function(cb) { - - global.isProd = false; - - runSequence(["dependencies", "js", "less", "views"], "watch", cb); -}); \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/gulp/util/processJs.js b/src/Umbraco.Web.UI.Client/gulp/util/processJs.js index 976708e62f..9d69840a38 100644 --- a/src/Umbraco.Web.UI.Client/gulp/util/processJs.js +++ b/src/Umbraco.Web.UI.Client/gulp/util/processJs.js @@ -9,26 +9,28 @@ var concat = require('gulp-concat'); var wrap = require("gulp-wrap-js"); var embedTemplates = require('gulp-angular-embed-templates'); -module.exports = function(files, out) { - +module.exports = function (files, out) { + var task = gulp.src(files); - - if (global.isProd === true) { - // check for js errors - task = task.pipe(eslint()); - // outputs the lint results to the console - task = task.pipe(eslint.format()); - } - + + // check for js errors + task = task.pipe(eslint()); + // outputs the lint results to the console + task = task.pipe(eslint.format()); + // sort files in stream by path or any custom sort comparator task = task.pipe(babel()) - .pipe(sort()) - .pipe(embedTemplates({ basePath: "./src/" })) - .pipe(concat(out)) + .pipe(sort()); + + if (global.isProd === true) { + //in production, embed the templates + task = task.pipe(embedTemplates({ basePath: "./src/", minimize: { loose: true } })) + } + task = task.pipe(concat(out)) .pipe(wrap('(function(){\n%= body %\n})();')) .pipe(gulp.dest(config.root + config.targets.js)); - - + + return task; - + }; diff --git a/src/Umbraco.Web.UI.Client/gulp/util/processLess.js b/src/Umbraco.Web.UI.Client/gulp/util/processLess.js index 26f69865d9..b866815f34 100644 --- a/src/Umbraco.Web.UI.Client/gulp/util/processLess.js +++ b/src/Umbraco.Web.UI.Client/gulp/util/processLess.js @@ -17,17 +17,12 @@ module.exports = function(files, out) { ]; var task = gulp.src(files) - .pipe(less()); - - - if (global.isProd === true) { - task = task.pipe(cleanCss()); - } - - task = task.pipe(postcss(processors)) + .pipe(less()) + .pipe(cleanCss()) + .pipe(postcss(processors)) .pipe(rename(out)) .pipe(gulp.dest(config.root + config.targets.css)); return task; -}; \ No newline at end of file +}; diff --git a/src/Umbraco.Web.UI.Client/lib/bootstrap/less/dropdowns.less b/src/Umbraco.Web.UI.Client/lib/bootstrap/less/dropdowns.less index 7707e04feb..94f229a191 100644 --- a/src/Umbraco.Web.UI.Client/lib/bootstrap/less/dropdowns.less +++ b/src/Umbraco.Web.UI.Client/lib/bootstrap/less/dropdowns.less @@ -80,6 +80,8 @@ // ----------- .dropdown-menu > li > a:hover, .dropdown-menu > li > a:focus, +.dropdown-menu > li > button:hover, +.dropdown-menu > li > button:focus, .dropdown-submenu:hover > a, .dropdown-submenu:focus > a { text-decoration: none; diff --git a/src/Umbraco.Web.UI.Client/lib/bootstrap/less/sprites.less b/src/Umbraco.Web.UI.Client/lib/bootstrap/less/sprites.less index d73e23f5ea..dd60f10cee 100644 --- a/src/Umbraco.Web.UI.Client/lib/bootstrap/less/sprites.less +++ b/src/Umbraco.Web.UI.Client/lib/bootstrap/less/sprites.less @@ -39,6 +39,10 @@ .dropdown-menu > li > a:focus > [class^="icon-"], .dropdown-menu > li > a:hover > [class*=" icon-"], .dropdown-menu > li > a:focus > [class*=" icon-"], +.dropdown-menu > li > button:hover > [class^="icon-"], +.dropdown-menu > li > button:focus > [class^="icon-"], +.dropdown-menu > li > button:hover > [class*=" icon-"], +.dropdown-menu > li > button:focus > [class*=" icon-"], .dropdown-menu > .active > a > [class^="icon-"], .dropdown-menu > .active > a > [class*=" icon-"], .dropdown-submenu:hover > a > [class^="icon-"], diff --git a/src/Umbraco.Web.UI.Client/lib/bootstrap/less/thumbnails.less b/src/Umbraco.Web.UI.Client/lib/bootstrap/less/thumbnails.less index d86f4ed7a9..6ec4bd4439 100644 --- a/src/Umbraco.Web.UI.Client/lib/bootstrap/less/thumbnails.less +++ b/src/Umbraco.Web.UI.Client/lib/bootstrap/less/thumbnails.less @@ -41,8 +41,8 @@ a.thumbnail:hover, a.thumbnail:focus, a div.thumbnail:hover, a div.thumbnail:focus { - border-color: @turquoise; - .box-shadow(0 1px 4px rgba(0,105,214,.25)); + border-color: @pinkLight; + .box-shadow(0 1px 4px rgba(245, 193, 188, .25)); } // Images and captions diff --git a/src/Umbraco.Web.UI.Client/lib/markdown/markdown.editor.js b/src/Umbraco.Web.UI.Client/lib/markdown/markdown.editor.js index e1617d8365..a75a7f1f3c 100644 --- a/src/Umbraco.Web.UI.Client/lib/markdown/markdown.editor.js +++ b/src/Umbraco.Web.UI.Client/lib/markdown/markdown.editor.js @@ -1239,6 +1239,11 @@ // Perform the button's action. function doClick(button) { + // don't do anything if the editor input or button bar isn't the currently active element + if (document.activeElement !== panels.input && !panels.buttonBar.contains(document.activeElement)) { + return; + } + inputBox.focus(); if (button.textOp) { diff --git a/src/Umbraco.Web.UI.Client/package-lock.json b/src/Umbraco.Web.UI.Client/package-lock.json index 3e8f053bc6..b14c415dfb 100644 --- a/src/Umbraco.Web.UI.Client/package-lock.json +++ b/src/Umbraco.Web.UI.Client/package-lock.json @@ -1104,6 +1104,7 @@ "resolved": "https://registry.npmjs.org/archive-type/-/archive-type-3.2.0.tgz", "integrity": "sha1-nNnABpV+vpX62tW9YJiUKoE3N/Y=", "dev": true, + "optional": true, "requires": { "file-type": "^3.1.0" }, @@ -1112,7 +1113,8 @@ "version": "3.9.0", "resolved": "http://registry.npmjs.org/file-type/-/file-type-3.9.0.tgz", "integrity": "sha1-JXoHg4TR24CHvESdEH1SpSZyuek=", - "dev": true + "dev": true, + "optional": true } } }, @@ -1538,6 +1540,7 @@ "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.2.tgz", "integrity": "sha512-e8tQYnZodmebYDWGH7KMRvtzKXaJHx3BbilrgZCfvyLUYdKpK1t5PSPmpkny/SgiTSCnjfLW7v5rlONXVFkQEA==", "dev": true, + "optional": true, "requires": { "readable-stream": "^2.3.5", "safe-buffer": "^5.1.1" @@ -1547,13 +1550,15 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true + "dev": true, + "optional": true }, "readable-stream": { "version": "2.3.6", - "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "dev": true, + "optional": true, "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -1566,9 +1571,10 @@ }, "string_decoder": { "version": "1.1.1", - "resolved": "http://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "dev": true, + "optional": true, "requires": { "safe-buffer": "~5.1.0" } @@ -1740,7 +1746,7 @@ "buffer-alloc": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz", - "integrity": "sha1-iQ3ZDZI6hz4I4Q5f1RpX5bfM4Ow=", + "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==", "dev": true, "requires": { "buffer-alloc-unsafe": "^1.1.0", @@ -1750,14 +1756,15 @@ "buffer-alloc-unsafe": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz", - "integrity": "sha1-vX3CauKXLQ7aJTvgYdupkjScGfA=", + "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==", "dev": true }, "buffer-crc32": { "version": "0.2.13", "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=", - "dev": true + "dev": true, + "optional": true }, "buffer-fill": { "version": "1.0.0", @@ -1776,6 +1783,7 @@ "resolved": "https://registry.npmjs.org/buffer-to-vinyl/-/buffer-to-vinyl-1.1.0.tgz", "integrity": "sha1-APFfruOreh3aLN5tkSG//dB7ImI=", "dev": true, + "optional": true, "requires": { "file-type": "^3.1.0", "readable-stream": "^2.0.2", @@ -1787,19 +1795,22 @@ "version": "3.9.0", "resolved": "http://registry.npmjs.org/file-type/-/file-type-3.9.0.tgz", "integrity": "sha1-JXoHg4TR24CHvESdEH1SpSZyuek=", - "dev": true + "dev": true, + "optional": true }, "isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true + "dev": true, + "optional": true }, "readable-stream": { "version": "2.3.6", "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "dev": true, + "optional": true, "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -1815,6 +1826,7 @@ "resolved": "http://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "dev": true, + "optional": true, "requires": { "safe-buffer": "~5.1.0" } @@ -1823,13 +1835,15 @@ "version": "2.0.3", "resolved": "http://registry.npmjs.org/uuid/-/uuid-2.0.3.tgz", "integrity": "sha1-Z+LoY3lyFVMN/zGOW/nc6/1Hsho=", - "dev": true + "dev": true, + "optional": true }, "vinyl": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-1.2.0.tgz", "integrity": "sha1-XIgDbPVl5d8FVYv8kR+GVt8hiIQ=", "dev": true, + "optional": true, "requires": { "clone": "^1.0.0", "clone-stats": "^0.0.1", @@ -1950,7 +1964,8 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/capture-stack-trace/-/capture-stack-trace-1.0.1.tgz", "integrity": "sha512-mYQLZnx5Qt1JgB1WEiMCf2647plpGeQ2NMR/5L0HNZzGQo4fuSPnK+wjfPnKZV0aiJDgzmWqqkV/g7JD+DW0qw==", - "dev": true + "dev": true, + "optional": true }, "caseless": { "version": "0.12.0", @@ -1963,6 +1978,7 @@ "resolved": "https://registry.npmjs.org/caw/-/caw-1.2.0.tgz", "integrity": "sha1-/7Im/n78VHKI3GLuPpcHPCEtEDQ=", "dev": true, + "optional": true, "requires": { "get-proxy": "^1.0.1", "is-obj": "^1.0.0", @@ -1974,7 +1990,8 @@ "version": "0.4.3", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.4.3.tgz", "integrity": "sha1-Y3PbdpCf5XDgjXNYM2Xtgop07us=", - "dev": true + "dev": true, + "optional": true } } }, @@ -2249,7 +2266,8 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/co/-/co-3.1.0.tgz", "integrity": "sha1-TqVOpaCJOBUxheFSEMaNkJK8G3g=", - "dev": true + "dev": true, + "optional": true }, "coa": { "version": "2.0.1", @@ -2373,6 +2391,7 @@ "resolved": "https://registry.npmjs.org/commander/-/commander-2.8.1.tgz", "integrity": "sha1-Br42f+v9oMMwqh4qBy09yXYkJdQ=", "dev": true, + "optional": true, "requires": { "graceful-readlink": ">= 1.0.0" } @@ -2585,6 +2604,7 @@ "resolved": "https://registry.npmjs.org/create-error-class/-/create-error-class-3.0.2.tgz", "integrity": "sha1-Br56vvlHo/FKMP1hBnHUAbyot7Y=", "dev": true, + "optional": true, "requires": { "capture-stack-trace": "^1.0.0" } @@ -2839,6 +2859,7 @@ "resolved": "https://registry.npmjs.org/decompress/-/decompress-3.0.0.tgz", "integrity": "sha1-rx3VDQbjv8QyRh033hGzjA2ZG+0=", "dev": true, + "optional": true, "requires": { "buffer-to-vinyl": "^1.0.0", "concat-stream": "^1.4.6", @@ -2856,6 +2877,7 @@ "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz", "integrity": "sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8=", "dev": true, + "optional": true, "requires": { "arr-flatten": "^1.0.1" } @@ -2864,13 +2886,15 @@ "version": "0.2.1", "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz", "integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=", - "dev": true + "dev": true, + "optional": true }, "braces": { "version": "1.8.5", "resolved": "https://registry.npmjs.org/braces/-/braces-1.8.5.tgz", "integrity": "sha1-uneWLhLf+WnWt2cR6RS3N4V79qc=", "dev": true, + "optional": true, "requires": { "expand-range": "^1.8.1", "preserve": "^0.2.0", @@ -2882,6 +2906,7 @@ "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-0.1.5.tgz", "integrity": "sha1-3wcoTjQqgHzXM6xa9yQR5YHRF3s=", "dev": true, + "optional": true, "requires": { "is-posix-bracket": "^0.1.0" } @@ -2891,6 +2916,7 @@ "resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz", "integrity": "sha1-Lhj/PS9JqydlzskCPwEdqo2DSaE=", "dev": true, + "optional": true, "requires": { "is-extglob": "^1.0.0" } @@ -2900,6 +2926,7 @@ "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz", "integrity": "sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E=", "dev": true, + "optional": true, "requires": { "inflight": "^1.0.4", "inherits": "2", @@ -2913,6 +2940,7 @@ "resolved": "https://registry.npmjs.org/glob-stream/-/glob-stream-5.3.5.tgz", "integrity": "sha1-pVZlqajM3EGRWofHAeMtTgFvrSI=", "dev": true, + "optional": true, "requires": { "extend": "^3.0.0", "glob": "^5.0.3", @@ -2928,13 +2956,15 @@ "version": "0.0.1", "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", - "dev": true + "dev": true, + "optional": true }, "readable-stream": { "version": "1.0.34", "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", "dev": true, + "optional": true, "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.1", @@ -2946,13 +2976,15 @@ "version": "0.10.31", "resolved": "http://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", - "dev": true + "dev": true, + "optional": true }, "through2": { "version": "0.6.5", "resolved": "http://registry.npmjs.org/through2/-/through2-0.6.5.tgz", "integrity": "sha1-QaucZ7KdVyCQcUEOHXp6lozTrUg=", "dev": true, + "optional": true, "requires": { "readable-stream": ">=1.0.33-1 <1.1.0-0", "xtend": ">=4.0.0 <4.1.0-0" @@ -2964,19 +2996,22 @@ "version": "4.1.15", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.15.tgz", "integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==", - "dev": true + "dev": true, + "optional": true }, "is-extglob": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", - "dev": true + "dev": true, + "optional": true }, "is-glob": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", "dev": true, + "optional": true, "requires": { "is-extglob": "^1.0.0" } @@ -2985,13 +3020,15 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true + "dev": true, + "optional": true }, "kind-of": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", "dev": true, + "optional": true, "requires": { "is-buffer": "^1.1.5" } @@ -3001,6 +3038,7 @@ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz", "integrity": "sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU=", "dev": true, + "optional": true, "requires": { "arr-diff": "^2.0.0", "array-unique": "^0.2.1", @@ -3021,13 +3059,15 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", - "dev": true + "dev": true, + "optional": true }, "ordered-read-streams": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/ordered-read-streams/-/ordered-read-streams-0.3.0.tgz", "integrity": "sha1-cTfmmzKYuzQiR6G77jiByA4v14s=", "dev": true, + "optional": true, "requires": { "is-stream": "^1.0.1", "readable-stream": "^2.0.1" @@ -3038,6 +3078,7 @@ "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "dev": true, + "optional": true, "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -3053,6 +3094,7 @@ "resolved": "http://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "dev": true, + "optional": true, "requires": { "safe-buffer": "~5.1.0" } @@ -3062,6 +3104,7 @@ "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", "dev": true, + "optional": true, "requires": { "is-utf8": "^0.2.0" } @@ -3071,6 +3114,7 @@ "resolved": "https://registry.npmjs.org/strip-bom-stream/-/strip-bom-stream-1.0.0.tgz", "integrity": "sha1-5xRDmFd9Uaa+0PoZlPoF9D/ZiO4=", "dev": true, + "optional": true, "requires": { "first-chunk-stream": "^1.0.0", "strip-bom": "^2.0.0" @@ -3081,6 +3125,7 @@ "resolved": "https://registry.npmjs.org/unique-stream/-/unique-stream-2.3.1.tgz", "integrity": "sha512-2nY4TnBE70yoxHkDli7DMazpWiP7xMdCYqU2nBRO0UB+ZpEkGsSija7MvmvnZFUeC+mrgiUfcHSr3LmRFIg4+A==", "dev": true, + "optional": true, "requires": { "json-stable-stringify-without-jsonify": "^1.0.1", "through2-filter": "^3.0.0" @@ -3091,6 +3136,7 @@ "resolved": "https://registry.npmjs.org/through2-filter/-/through2-filter-3.0.0.tgz", "integrity": "sha512-jaRjI2WxN3W1V8/FMZ9HKIBXixtiqs3SQSX4/YGIiP3gL6djW48VoZq9tDqeCWs3MT8YY5wb/zli8VW8snY1CA==", "dev": true, + "optional": true, "requires": { "through2": "~2.0.0", "xtend": "~4.0.0" @@ -3103,6 +3149,7 @@ "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-1.2.0.tgz", "integrity": "sha1-XIgDbPVl5d8FVYv8kR+GVt8hiIQ=", "dev": true, + "optional": true, "requires": { "clone": "^1.0.0", "clone-stats": "^0.0.1", @@ -3114,6 +3161,7 @@ "resolved": "https://registry.npmjs.org/vinyl-fs/-/vinyl-fs-2.4.4.tgz", "integrity": "sha1-vm/zJwy1Xf19MGNkDegfJddTIjk=", "dev": true, + "optional": true, "requires": { "duplexify": "^3.2.0", "glob-stream": "^5.3.2", @@ -3141,6 +3189,7 @@ "resolved": "https://registry.npmjs.org/decompress-tar/-/decompress-tar-3.1.0.tgz", "integrity": "sha1-IXx4n5uURQ76rcXF5TeXj8MzxGY=", "dev": true, + "optional": true, "requires": { "is-tar": "^1.0.0", "object-assign": "^2.0.0", @@ -3154,19 +3203,22 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/clone/-/clone-0.2.0.tgz", "integrity": "sha1-xhJqkK1Pctv1rNskPMN3JP6T/B8=", - "dev": true + "dev": true, + "optional": true }, "object-assign": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-2.1.1.tgz", "integrity": "sha1-Q8NuXVaf+OSBbE76i+AtJpZ8GKo=", - "dev": true + "dev": true, + "optional": true }, "readable-stream": { "version": "1.0.34", "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", "dev": true, + "optional": true, "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.1", @@ -3179,6 +3231,7 @@ "resolved": "http://registry.npmjs.org/through2/-/through2-0.6.5.tgz", "integrity": "sha1-QaucZ7KdVyCQcUEOHXp6lozTrUg=", "dev": true, + "optional": true, "requires": { "readable-stream": ">=1.0.33-1 <1.1.0-0", "xtend": ">=4.0.0 <4.1.0-0" @@ -3189,6 +3242,7 @@ "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-0.4.6.tgz", "integrity": "sha1-LzVsh6VQolVGHza76ypbqL94SEc=", "dev": true, + "optional": true, "requires": { "clone": "^0.2.0", "clone-stats": "^0.0.1" @@ -3201,6 +3255,7 @@ "resolved": "https://registry.npmjs.org/decompress-tarbz2/-/decompress-tarbz2-3.1.0.tgz", "integrity": "sha1-iyOTVoE1X58YnYclag+L3ZbZZm0=", "dev": true, + "optional": true, "requires": { "is-bzip2": "^1.0.0", "object-assign": "^2.0.0", @@ -3215,19 +3270,22 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/clone/-/clone-0.2.0.tgz", "integrity": "sha1-xhJqkK1Pctv1rNskPMN3JP6T/B8=", - "dev": true + "dev": true, + "optional": true }, "object-assign": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-2.1.1.tgz", "integrity": "sha1-Q8NuXVaf+OSBbE76i+AtJpZ8GKo=", - "dev": true + "dev": true, + "optional": true }, "readable-stream": { "version": "1.0.34", "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", "dev": true, + "optional": true, "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.1", @@ -3240,6 +3298,7 @@ "resolved": "http://registry.npmjs.org/through2/-/through2-0.6.5.tgz", "integrity": "sha1-QaucZ7KdVyCQcUEOHXp6lozTrUg=", "dev": true, + "optional": true, "requires": { "readable-stream": ">=1.0.33-1 <1.1.0-0", "xtend": ">=4.0.0 <4.1.0-0" @@ -3250,6 +3309,7 @@ "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-0.4.6.tgz", "integrity": "sha1-LzVsh6VQolVGHza76ypbqL94SEc=", "dev": true, + "optional": true, "requires": { "clone": "^0.2.0", "clone-stats": "^0.0.1" @@ -3262,6 +3322,7 @@ "resolved": "https://registry.npmjs.org/decompress-targz/-/decompress-targz-3.1.0.tgz", "integrity": "sha1-ssE9+YFmJomRtxXWRH9kLpaW9aA=", "dev": true, + "optional": true, "requires": { "is-gzip": "^1.0.0", "object-assign": "^2.0.0", @@ -3275,19 +3336,22 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/clone/-/clone-0.2.0.tgz", "integrity": "sha1-xhJqkK1Pctv1rNskPMN3JP6T/B8=", - "dev": true + "dev": true, + "optional": true }, "object-assign": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-2.1.1.tgz", "integrity": "sha1-Q8NuXVaf+OSBbE76i+AtJpZ8GKo=", - "dev": true + "dev": true, + "optional": true }, "readable-stream": { "version": "1.0.34", "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", "dev": true, + "optional": true, "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.1", @@ -3300,6 +3364,7 @@ "resolved": "http://registry.npmjs.org/through2/-/through2-0.6.5.tgz", "integrity": "sha1-QaucZ7KdVyCQcUEOHXp6lozTrUg=", "dev": true, + "optional": true, "requires": { "readable-stream": ">=1.0.33-1 <1.1.0-0", "xtend": ">=4.0.0 <4.1.0-0" @@ -3310,6 +3375,7 @@ "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-0.4.6.tgz", "integrity": "sha1-LzVsh6VQolVGHza76ypbqL94SEc=", "dev": true, + "optional": true, "requires": { "clone": "^0.2.0", "clone-stats": "^0.0.1" @@ -3322,6 +3388,7 @@ "resolved": "https://registry.npmjs.org/decompress-unzip/-/decompress-unzip-3.4.0.tgz", "integrity": "sha1-YUdbQVIGa74/7hL51inRX+ZHjus=", "dev": true, + "optional": true, "requires": { "is-zip": "^1.0.0", "read-all-stream": "^3.0.0", @@ -3337,6 +3404,7 @@ "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-1.2.0.tgz", "integrity": "sha1-XIgDbPVl5d8FVYv8kR+GVt8hiIQ=", "dev": true, + "optional": true, "requires": { "clone": "^1.0.0", "clone-stats": "^0.0.1", @@ -3349,7 +3417,8 @@ "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "dev": true + "dev": true, + "optional": true }, "deep-is": { "version": "0.1.3", @@ -3568,6 +3637,7 @@ "resolved": "https://registry.npmjs.org/download/-/download-4.4.3.tgz", "integrity": "sha1-qlX9rTktldS2jowr4D4MKqIbqaw=", "dev": true, + "optional": true, "requires": { "caw": "^1.0.1", "concat-stream": "^1.4.7", @@ -3591,6 +3661,7 @@ "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz", "integrity": "sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8=", "dev": true, + "optional": true, "requires": { "arr-flatten": "^1.0.1" } @@ -3599,13 +3670,15 @@ "version": "0.2.1", "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz", "integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=", - "dev": true + "dev": true, + "optional": true }, "braces": { "version": "1.8.5", "resolved": "https://registry.npmjs.org/braces/-/braces-1.8.5.tgz", "integrity": "sha1-uneWLhLf+WnWt2cR6RS3N4V79qc=", "dev": true, + "optional": true, "requires": { "expand-range": "^1.8.1", "preserve": "^0.2.0", @@ -3617,6 +3690,7 @@ "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-0.1.5.tgz", "integrity": "sha1-3wcoTjQqgHzXM6xa9yQR5YHRF3s=", "dev": true, + "optional": true, "requires": { "is-posix-bracket": "^0.1.0" } @@ -3626,6 +3700,7 @@ "resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz", "integrity": "sha1-Lhj/PS9JqydlzskCPwEdqo2DSaE=", "dev": true, + "optional": true, "requires": { "is-extglob": "^1.0.0" } @@ -3635,6 +3710,7 @@ "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz", "integrity": "sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E=", "dev": true, + "optional": true, "requires": { "inflight": "^1.0.4", "inherits": "2", @@ -3648,6 +3724,7 @@ "resolved": "https://registry.npmjs.org/glob-stream/-/glob-stream-5.3.5.tgz", "integrity": "sha1-pVZlqajM3EGRWofHAeMtTgFvrSI=", "dev": true, + "optional": true, "requires": { "extend": "^3.0.0", "glob": "^5.0.3", @@ -3663,13 +3740,15 @@ "version": "0.0.1", "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", - "dev": true + "dev": true, + "optional": true }, "readable-stream": { "version": "1.0.34", "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", "dev": true, + "optional": true, "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.1", @@ -3681,13 +3760,15 @@ "version": "0.10.31", "resolved": "http://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", - "dev": true + "dev": true, + "optional": true }, "through2": { "version": "0.6.5", "resolved": "http://registry.npmjs.org/through2/-/through2-0.6.5.tgz", "integrity": "sha1-QaucZ7KdVyCQcUEOHXp6lozTrUg=", "dev": true, + "optional": true, "requires": { "readable-stream": ">=1.0.33-1 <1.1.0-0", "xtend": ">=4.0.0 <4.1.0-0" @@ -3699,19 +3780,22 @@ "version": "4.1.15", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.15.tgz", "integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==", - "dev": true + "dev": true, + "optional": true }, "is-extglob": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", - "dev": true + "dev": true, + "optional": true }, "is-glob": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", "dev": true, + "optional": true, "requires": { "is-extglob": "^1.0.0" } @@ -3720,13 +3804,15 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true + "dev": true, + "optional": true }, "kind-of": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", "dev": true, + "optional": true, "requires": { "is-buffer": "^1.1.5" } @@ -3736,6 +3822,7 @@ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz", "integrity": "sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU=", "dev": true, + "optional": true, "requires": { "arr-diff": "^2.0.0", "array-unique": "^0.2.1", @@ -3756,13 +3843,15 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", - "dev": true + "dev": true, + "optional": true }, "ordered-read-streams": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/ordered-read-streams/-/ordered-read-streams-0.3.0.tgz", "integrity": "sha1-cTfmmzKYuzQiR6G77jiByA4v14s=", "dev": true, + "optional": true, "requires": { "is-stream": "^1.0.1", "readable-stream": "^2.0.1" @@ -3773,6 +3862,7 @@ "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "dev": true, + "optional": true, "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -3788,6 +3878,7 @@ "resolved": "http://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "dev": true, + "optional": true, "requires": { "safe-buffer": "~5.1.0" } @@ -3797,6 +3888,7 @@ "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", "dev": true, + "optional": true, "requires": { "is-utf8": "^0.2.0" } @@ -3806,6 +3898,7 @@ "resolved": "https://registry.npmjs.org/strip-bom-stream/-/strip-bom-stream-1.0.0.tgz", "integrity": "sha1-5xRDmFd9Uaa+0PoZlPoF9D/ZiO4=", "dev": true, + "optional": true, "requires": { "first-chunk-stream": "^1.0.0", "strip-bom": "^2.0.0" @@ -3816,6 +3909,7 @@ "resolved": "https://registry.npmjs.org/unique-stream/-/unique-stream-2.3.1.tgz", "integrity": "sha512-2nY4TnBE70yoxHkDli7DMazpWiP7xMdCYqU2nBRO0UB+ZpEkGsSija7MvmvnZFUeC+mrgiUfcHSr3LmRFIg4+A==", "dev": true, + "optional": true, "requires": { "json-stable-stringify-without-jsonify": "^1.0.1", "through2-filter": "^3.0.0" @@ -3826,6 +3920,7 @@ "resolved": "https://registry.npmjs.org/through2-filter/-/through2-filter-3.0.0.tgz", "integrity": "sha512-jaRjI2WxN3W1V8/FMZ9HKIBXixtiqs3SQSX4/YGIiP3gL6djW48VoZq9tDqeCWs3MT8YY5wb/zli8VW8snY1CA==", "dev": true, + "optional": true, "requires": { "through2": "~2.0.0", "xtend": "~4.0.0" @@ -3838,6 +3933,7 @@ "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-1.2.0.tgz", "integrity": "sha1-XIgDbPVl5d8FVYv8kR+GVt8hiIQ=", "dev": true, + "optional": true, "requires": { "clone": "^1.0.0", "clone-stats": "^0.0.1", @@ -3849,6 +3945,7 @@ "resolved": "https://registry.npmjs.org/vinyl-fs/-/vinyl-fs-2.4.4.tgz", "integrity": "sha1-vm/zJwy1Xf19MGNkDegfJddTIjk=", "dev": true, + "optional": true, "requires": { "duplexify": "^3.2.0", "glob-stream": "^5.3.2", @@ -3891,6 +3988,7 @@ "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.6.1.tgz", "integrity": "sha512-vM58DwdnKmty+FSPzT14K9JXb90H+j5emaR4KYbr2KTIz00WHGbWOe5ghQTx233ZCLZtrGDALzKwcjEtSt35mA==", "dev": true, + "optional": true, "requires": { "end-of-stream": "^1.0.0", "inherits": "^2.0.1", @@ -3903,6 +4001,7 @@ "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", "dev": true, + "optional": true, "requires": { "once": "^1.4.0" } @@ -3911,13 +4010,15 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true + "dev": true, + "optional": true }, "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", "dev": true, + "optional": true, "requires": { "wrappy": "1" } @@ -3927,6 +4028,7 @@ "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "dev": true, + "optional": true, "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -3942,6 +4044,7 @@ "resolved": "http://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "dev": true, + "optional": true, "requires": { "safe-buffer": "~5.1.0" } @@ -3953,6 +4056,7 @@ "resolved": "https://registry.npmjs.org/each-async/-/each-async-1.1.1.tgz", "integrity": "sha1-3uUim98KtrogEqOV4bhpq/iBNHM=", "dev": true, + "optional": true, "requires": { "onetime": "^1.0.0", "set-immediate-shim": "^1.0.0" @@ -3962,7 +4066,8 @@ "version": "1.1.0", "resolved": "http://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz", "integrity": "sha1-ofeDj4MUxRbwXs78vEzP4EtO14k=", - "dev": true + "dev": true, + "optional": true } } }, @@ -4061,7 +4166,7 @@ }, "engine.io-client": { "version": "3.2.1", - "resolved": "http://registry.npmjs.org/engine.io-client/-/engine.io-client-3.2.1.tgz", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.2.1.tgz", "integrity": "sha512-y5AbkytWeM4jQr7m/koQLc5AxpRKC1hEVUb/s1FUAWEJq5AzJJ4NLvzuKPuxtDi5Mq755WuDvZ6Iv2rXj4PTzw==", "dev": true, "requires": { @@ -4482,7 +4587,7 @@ "exec-buffer": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/exec-buffer/-/exec-buffer-3.2.0.tgz", - "integrity": "sha1-sWhtvZBMfPmC5lLB9aebHlVzCCs=", + "integrity": "sha512-wsiD+2Tp6BWHoVv3B+5Dcx6E7u5zky+hUwOHjuH2hKSLR3dvRmX8fk8UD8uqQixHs4Wk6eDmiegVrMPjKj7wpA==", "dev": true, "optional": true, "requires": { @@ -4945,6 +5050,7 @@ "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", "integrity": "sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4=", "dev": true, + "optional": true, "requires": { "pend": "~1.2.0" } @@ -4992,13 +5098,15 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-1.0.0.tgz", "integrity": "sha1-5hz4BfDeHJhFZ9A4bcXfUO5a9+Q=", - "dev": true + "dev": true, + "optional": true }, "filenamify": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/filenamify/-/filenamify-1.2.1.tgz", "integrity": "sha1-qfL/0RxQO+0wABUCknI3jx8TZaU=", "dev": true, + "optional": true, "requires": { "filename-reserved-regex": "^1.0.0", "strip-outer": "^1.0.0", @@ -5244,8 +5352,9 @@ "fs-constants": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha1-a+Dem+mYzhavivwkSXue6bfM2a0=", - "dev": true + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true, + "optional": true }, "fs-extra": { "version": "1.0.0", @@ -5869,6 +5978,7 @@ "resolved": "https://registry.npmjs.org/get-proxy/-/get-proxy-1.1.0.tgz", "integrity": "sha1-iUhUSRvFkbDxR9euVw9cZ4tyVus=", "dev": true, + "optional": true, "requires": { "rc": "^1.1.2" } @@ -6175,6 +6285,7 @@ "resolved": "http://registry.npmjs.org/got/-/got-5.7.1.tgz", "integrity": "sha1-X4FjWmHkplifGAVp6k44FoClHzU=", "dev": true, + "optional": true, "requires": { "create-error-class": "^3.0.1", "duplexer2": "^0.1.4", @@ -6198,6 +6309,7 @@ "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", "integrity": "sha1-ixLauHjA1p4+eJEFFmKjL8a93ME=", "dev": true, + "optional": true, "requires": { "readable-stream": "^2.0.2" } @@ -6206,19 +6318,22 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true + "dev": true, + "optional": true }, "object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", - "dev": true + "dev": true, + "optional": true }, "parse-json": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", "dev": true, + "optional": true, "requires": { "error-ex": "^1.2.0" } @@ -6228,6 +6343,7 @@ "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "dev": true, + "optional": true, "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -6243,6 +6359,7 @@ "resolved": "http://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "dev": true, + "optional": true, "requires": { "safe-buffer": "~5.1.0" } @@ -6262,7 +6379,8 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz", "integrity": "sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=", - "dev": true + "dev": true, + "optional": true }, "growly": { "version": "1.3.0", @@ -6579,6 +6697,7 @@ "resolved": "https://registry.npmjs.org/gulp-decompress/-/gulp-decompress-1.2.0.tgz", "integrity": "sha1-jutlpeAV+O2FMsr+KEVJYGJvDcc=", "dev": true, + "optional": true, "requires": { "archive-type": "^3.0.0", "decompress": "^3.0.0", @@ -6590,13 +6709,15 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true + "dev": true, + "optional": true }, "readable-stream": { "version": "2.3.6", "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "dev": true, + "optional": true, "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -6612,6 +6733,7 @@ "resolved": "http://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "dev": true, + "optional": true, "requires": { "safe-buffer": "~5.1.0" } @@ -7229,6 +7351,7 @@ "resolved": "https://registry.npmjs.org/gulp-sourcemaps/-/gulp-sourcemaps-1.6.0.tgz", "integrity": "sha1-uG/zSdgBzrVuHZ59x7vLS33uYAw=", "dev": true, + "optional": true, "requires": { "convert-source-map": "^1.1.1", "graceful-fs": "^4.1.2", @@ -7241,13 +7364,15 @@ "version": "4.1.15", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.15.tgz", "integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==", - "dev": true + "dev": true, + "optional": true }, "strip-bom": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", "dev": true, + "optional": true, "requires": { "is-utf8": "^0.2.0" } @@ -7257,6 +7382,7 @@ "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-1.2.0.tgz", "integrity": "sha1-XIgDbPVl5d8FVYv8kR+GVt8hiIQ=", "dev": true, + "optional": true, "requires": { "clone": "^1.0.0", "clone-stats": "^0.0.1", @@ -8139,7 +8265,8 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-bzip2/-/is-bzip2-1.0.0.tgz", "integrity": "sha1-XuWOqlounIDiFAe+3yOuWsCRs/w=", - "dev": true + "dev": true, + "optional": true }, "is-callable": { "version": "1.1.4", @@ -8274,7 +8401,8 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-gzip/-/is-gzip-1.0.0.tgz", "integrity": "sha1-bKiwe5nHeZgCWQDlVc7Y7YCHmoM=", - "dev": true + "dev": true, + "optional": true }, "is-jpg": { "version": "1.0.1", @@ -8287,7 +8415,8 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-natural-number/-/is-natural-number-2.1.1.tgz", "integrity": "sha1-fUxXKDd+84bD4ZSpkRv1fG3DNec=", - "dev": true + "dev": true, + "optional": true }, "is-number": { "version": "3.0.0", @@ -8353,7 +8482,8 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-redirect/-/is-redirect-1.0.0.tgz", "integrity": "sha1-HQPd7VO9jbDzDCbk+V02/HyH3CQ=", - "dev": true + "dev": true, + "optional": true }, "is-regex": { "version": "1.0.4", @@ -8383,7 +8513,8 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.1.0.tgz", "integrity": "sha1-EaBgVotnM5REAz0BJaYaINVk+zQ=", - "dev": true + "dev": true, + "optional": true }, "is-stream": { "version": "1.1.0", @@ -8413,7 +8544,8 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-tar/-/is-tar-1.0.0.tgz", "integrity": "sha1-L2suF5LB9bs2UZrKqdZcDSb+hT0=", - "dev": true + "dev": true, + "optional": true }, "is-typedarray": { "version": "1.0.0", @@ -8434,7 +8566,8 @@ "version": "1.2.4", "resolved": "https://registry.npmjs.org/is-url/-/is-url-1.2.4.tgz", "integrity": "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==", - "dev": true + "dev": true, + "optional": true }, "is-utf8": { "version": "0.2.1", @@ -8446,7 +8579,8 @@ "version": "0.3.0", "resolved": "https://registry.npmjs.org/is-valid-glob/-/is-valid-glob-0.3.0.tgz", "integrity": "sha1-1LVcafUYhvm2XHDWwmItN+KfSP4=", - "dev": true + "dev": true, + "optional": true }, "is-windows": { "version": "1.0.2", @@ -8464,7 +8598,8 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-zip/-/is-zip-1.0.0.tgz", "integrity": "sha1-R7Co/004p2QxzP2ZqOFaTIa6IyU=", - "dev": true + "dev": true, + "optional": true }, "isarray": { "version": "0.0.1", @@ -8804,6 +8939,7 @@ "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.0.tgz", "integrity": "sha1-9plf4PggOS9hOWvolGJAe7dxaOQ=", "dev": true, + "optional": true, "requires": { "readable-stream": "^2.0.5" }, @@ -8812,13 +8948,15 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true + "dev": true, + "optional": true }, "readable-stream": { "version": "2.3.6", "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "dev": true, + "optional": true, "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -8834,6 +8972,7 @@ "resolved": "http://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "dev": true, + "optional": true, "requires": { "safe-buffer": "~5.1.0" } @@ -9139,7 +9278,8 @@ "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=", - "dev": true + "dev": true, + "optional": true }, "lodash.isobject": { "version": "2.4.1", @@ -9335,8 +9475,9 @@ "lowercase-keys": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", - "integrity": "sha1-b54wtHCE2XGnyCD/FabFFnt0wm8=", - "dev": true + "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==", + "dev": true, + "optional": true }, "lpad-align": { "version": "1.1.2", @@ -9369,7 +9510,7 @@ "make-dir": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", - "integrity": "sha1-ecEDO4BRW9bSTsmTPoYMp17ifww=", + "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", "dev": true, "requires": { "pify": "^3.0.0" @@ -9772,7 +9913,8 @@ "version": "1.0.0", "resolved": "http://registry.npmjs.org/node-status-codes/-/node-status-codes-1.0.0.tgz", "integrity": "sha1-WuVUHQJGRdMqWPzdyc7s6nrjrC8=", - "dev": true + "dev": true, + "optional": true }, "node.extend": { "version": "1.1.8", @@ -12735,7 +12877,7 @@ "dependencies": { "minimist": { "version": "0.0.10", - "resolved": "http://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz", "integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=", "dev": true }, @@ -12822,7 +12964,8 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", - "dev": true + "dev": true, + "optional": true }, "p-pipe": { "version": "1.2.0", @@ -13533,7 +13676,8 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz", "integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=", - "dev": true + "dev": true, + "optional": true }, "preserve": { "version": "0.2.0", @@ -13665,6 +13809,7 @@ "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", "dev": true, + "optional": true, "requires": { "deep-extend": "^0.6.0", "ini": "~1.3.0", @@ -13677,6 +13822,7 @@ "resolved": "https://registry.npmjs.org/read-all-stream/-/read-all-stream-3.1.0.tgz", "integrity": "sha1-NcPhd/IHjveJ7kv6+kNzB06u9Po=", "dev": true, + "optional": true, "requires": { "pinkie-promise": "^2.0.0", "readable-stream": "^2.0.0" @@ -13686,13 +13832,15 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true + "dev": true, + "optional": true }, "readable-stream": { "version": "2.3.6", "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "dev": true, + "optional": true, "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -13708,6 +13856,7 @@ "resolved": "http://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "dev": true, + "optional": true, "requires": { "safe-buffer": "~5.1.0" } @@ -14324,6 +14473,7 @@ "resolved": "https://registry.npmjs.org/seek-bzip/-/seek-bzip-1.0.5.tgz", "integrity": "sha1-z+kXyz0nS8/6x5J1ivUxc+sfq9w=", "dev": true, + "optional": true, "requires": { "commander": "~2.8.1" } @@ -14469,7 +14619,8 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz", "integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=", - "dev": true + "dev": true, + "optional": true }, "set-value": { "version": "2.0.0", @@ -14775,7 +14926,7 @@ }, "socket.io-parser": { "version": "3.2.0", - "resolved": "http://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.2.0.tgz", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.2.0.tgz", "integrity": "sha512-FYiBx7rc/KORMJlgsXysflWx/RIvtqZbyGLlHZvjfmPTPeuD/I8MaW7cfFrj5tRltICJdgwflhfZ3NVVbVLFQA==", "dev": true, "requires": { @@ -14920,7 +15071,7 @@ }, "chalk": { "version": "1.1.3", - "resolved": "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", "dev": true, "optional": true, @@ -14974,7 +15125,8 @@ "version": "0.2.2", "resolved": "https://registry.npmjs.org/stat-mode/-/stat-mode-0.2.2.tgz", "integrity": "sha1-5sgLYjEj19gM8TLOU480YokHJQI=", - "dev": true + "dev": true, + "optional": true }, "static-extend": { "version": "0.1.2", @@ -15018,6 +15170,7 @@ "resolved": "https://registry.npmjs.org/stream-combiner2/-/stream-combiner2-1.1.1.tgz", "integrity": "sha1-+02KFCDqNidk4hrUeAOXvry0HL4=", "dev": true, + "optional": true, "requires": { "duplexer2": "~0.1.0", "readable-stream": "^2.0.2" @@ -15028,6 +15181,7 @@ "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", "integrity": "sha1-ixLauHjA1p4+eJEFFmKjL8a93ME=", "dev": true, + "optional": true, "requires": { "readable-stream": "^2.0.2" } @@ -15036,13 +15190,15 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true + "dev": true, + "optional": true }, "readable-stream": { "version": "2.3.6", "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "dev": true, + "optional": true, "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -15058,6 +15214,7 @@ "resolved": "http://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "dev": true, + "optional": true, "requires": { "safe-buffer": "~5.1.0" } @@ -15074,7 +15231,8 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.0.tgz", "integrity": "sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI=", - "dev": true + "dev": true, + "optional": true }, "streamroller": { "version": "0.7.0", @@ -15252,6 +15410,7 @@ "resolved": "http://registry.npmjs.org/strip-dirs/-/strip-dirs-1.1.1.tgz", "integrity": "sha1-lgu9EoeETzl1pFWKoQOoJV4kVqA=", "dev": true, + "optional": true, "requires": { "chalk": "^1.0.0", "get-stdin": "^4.0.1", @@ -15265,13 +15424,15 @@ "version": "2.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", - "dev": true + "dev": true, + "optional": true }, "chalk": { "version": "1.1.3", "resolved": "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", "dev": true, + "optional": true, "requires": { "ansi-styles": "^2.2.1", "escape-string-regexp": "^1.0.2", @@ -15285,6 +15446,7 @@ "resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-0.1.7.tgz", "integrity": "sha1-hHSREZ/MtftDYhfMc39/qtUPYD8=", "dev": true, + "optional": true, "requires": { "is-relative": "^0.1.0" } @@ -15293,13 +15455,15 @@ "version": "0.1.3", "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-0.1.3.tgz", "integrity": "sha1-kF/uiuhvRbPsYUvDwVyGnfCHboI=", - "dev": true + "dev": true, + "optional": true }, "supports-color": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", - "dev": true + "dev": true, + "optional": true } } }, @@ -15328,8 +15492,9 @@ "strip-outer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/strip-outer/-/strip-outer-1.0.1.tgz", - "integrity": "sha1-sv0qv2YEudHmATBXGV34Nrip1jE=", + "integrity": "sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg==", "dev": true, + "optional": true, "requires": { "escape-string-regexp": "^1.0.2" } @@ -15363,6 +15528,7 @@ "resolved": "https://registry.npmjs.org/sum-up/-/sum-up-1.0.3.tgz", "integrity": "sha1-HGYfZnBX9jvLeHWqFDi8FiUlFW4=", "dev": true, + "optional": true, "requires": { "chalk": "^1.0.0" }, @@ -15371,13 +15537,15 @@ "version": "2.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", - "dev": true + "dev": true, + "optional": true }, "chalk": { "version": "1.1.3", "resolved": "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", "dev": true, + "optional": true, "requires": { "ansi-styles": "^2.2.1", "escape-string-regexp": "^1.0.2", @@ -15390,7 +15558,8 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", - "dev": true + "dev": true, + "optional": true } } }, @@ -15452,6 +15621,7 @@ "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.6.2.tgz", "integrity": "sha512-rzS0heiNf8Xn7/mpdSVVSMAWAoy9bfb1WOTYC78Z0UQKeKa/CWS8FOq0lKGNa8DWKAn9gxjCvMLYc5PGXYlK2A==", "dev": true, + "optional": true, "requires": { "bl": "^1.0.0", "buffer-alloc": "^1.2.0", @@ -15467,6 +15637,7 @@ "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", "integrity": "sha1-7SljTRm6ukY7bOa4CjchPqtx7EM=", "dev": true, + "optional": true, "requires": { "once": "^1.4.0" } @@ -15475,22 +15646,25 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true + "dev": true, + "optional": true }, "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", "dev": true, + "optional": true, "requires": { "wrappy": "1" } }, "readable-stream": { "version": "2.3.6", - "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "dev": true, + "optional": true, "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -15506,6 +15680,7 @@ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "dev": true, + "optional": true, "requires": { "safe-buffer": "~5.1.0" } @@ -15610,6 +15785,7 @@ "resolved": "https://registry.npmjs.org/through2-filter/-/through2-filter-2.0.0.tgz", "integrity": "sha1-YLxVoNrLdghdsfna6Zq0P4PWIuw=", "dev": true, + "optional": true, "requires": { "through2": "~2.0.0", "xtend": "~4.0.0" @@ -15634,7 +15810,8 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/timed-out/-/timed-out-3.1.3.tgz", "integrity": "sha1-lYYL/MXHbCd/j4Mm/Q9bLiDrohc=", - "dev": true + "dev": true, + "optional": true }, "timers-ext": { "version": "0.1.7", @@ -15707,6 +15884,7 @@ "resolved": "https://registry.npmjs.org/to-absolute-glob/-/to-absolute-glob-0.1.1.tgz", "integrity": "sha1-HN+kcqnvUMI57maZm2YsoOs5k38=", "dev": true, + "optional": true, "requires": { "extend-shallow": "^2.0.1" }, @@ -15716,6 +15894,7 @@ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", "dev": true, + "optional": true, "requires": { "is-extendable": "^0.1.0" } @@ -15731,8 +15910,9 @@ "to-buffer": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.1.1.tgz", - "integrity": "sha1-STvUj2LXxD/N7TE6A9ytsuEhOoA=", - "dev": true + "integrity": "sha512-lx9B5iv7msuFYE3dytT+KE5tap+rNYw+K4jVkb9R/asAb+pbBSM17jtunHplhBe6RRJdZx3Pn2Jph24O32mOVg==", + "dev": true, + "optional": true }, "to-fast-properties": { "version": "2.0.0", @@ -15811,6 +15991,7 @@ "resolved": "https://registry.npmjs.org/trim-repeated/-/trim-repeated-1.0.0.tgz", "integrity": "sha1-42RqLqTokTEr9+rObPsFOAvAHCE=", "dev": true, + "optional": true, "requires": { "escape-string-regexp": "^1.0.2" } @@ -16059,7 +16240,8 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/unzip-response/-/unzip-response-1.0.2.tgz", "integrity": "sha1-uYTwh3/AqJwsdzzB73tbIytbBv4=", - "dev": true + "dev": true, + "optional": true }, "upath": { "version": "1.1.0", @@ -16087,6 +16269,7 @@ "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-1.0.0.tgz", "integrity": "sha1-evjzA2Rem9eaJy56FKxovAYJ2nM=", "dev": true, + "optional": true, "requires": { "prepend-http": "^1.0.1" } @@ -16172,7 +16355,8 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/vali-date/-/vali-date-1.0.0.tgz", "integrity": "sha1-G5BKWWCfsyjvB4E4Qgk09rhnCaY=", - "dev": true + "dev": true, + "optional": true }, "validate-npm-package-license": { "version": "3.0.4", @@ -16217,6 +16401,7 @@ "resolved": "https://registry.npmjs.org/vinyl-assign/-/vinyl-assign-1.2.1.tgz", "integrity": "sha1-TRmIkbVRWRHXcajNnFSApGoHSkU=", "dev": true, + "optional": true, "requires": { "object-assign": "^4.0.1", "readable-stream": "^2.0.0" @@ -16226,19 +16411,22 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true + "dev": true, + "optional": true }, "object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", - "dev": true + "dev": true, + "optional": true }, "readable-stream": { "version": "2.3.6", "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "dev": true, + "optional": true, "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -16254,6 +16442,7 @@ "resolved": "http://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "dev": true, + "optional": true, "requires": { "safe-buffer": "~5.1.0" } @@ -16393,6 +16582,7 @@ "resolved": "https://registry.npmjs.org/ware/-/ware-1.3.0.tgz", "integrity": "sha1-0bFPOdLiy0q4xAmPdW/ksWTkc9Q=", "dev": true, + "optional": true, "requires": { "wrap-fn": "^0.1.0" } @@ -16483,6 +16673,7 @@ "resolved": "https://registry.npmjs.org/wrap-fn/-/wrap-fn-0.1.5.tgz", "integrity": "sha1-8htuQQFv9KfjFyDbxjoJAWvfmEU=", "dev": true, + "optional": true, "requires": { "co": "3.1.0" } @@ -16586,6 +16777,7 @@ "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", "integrity": "sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=", "dev": true, + "optional": true, "requires": { "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" diff --git a/src/Umbraco.Web.UI.Client/src/assets/img/checkered-background-20.png b/src/Umbraco.Web.UI.Client/src/assets/img/checkered-background-20.png deleted file mode 100644 index eb0ca08a6f..0000000000 Binary files a/src/Umbraco.Web.UI.Client/src/assets/img/checkered-background-20.png and /dev/null differ diff --git a/src/Umbraco.Web.UI.Client/src/assets/img/checkered-background.png b/src/Umbraco.Web.UI.Client/src/assets/img/checkered-background.png deleted file mode 100644 index c6c66da3d9..0000000000 Binary files a/src/Umbraco.Web.UI.Client/src/assets/img/checkered-background.png and /dev/null differ diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umblogin.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umblogin.directive.js index 7fb75c45e6..19ebe448e0 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umblogin.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umblogin.directive.js @@ -13,7 +13,7 @@ } }); - function UmbLoginController($scope, $location, currentUserResource, formHelper, mediaHelper, umbRequestHelper, Upload, localizationService, userService, externalLoginInfo, resetPasswordCodeInfo, $timeout, authResource, $q) { + function UmbLoginController($scope, $location, currentUserResource, formHelper, mediaHelper, umbRequestHelper, Upload, localizationService, userService, externalLoginInfo, resetPasswordCodeInfo, $timeout, authResource, $q, $route) { const vm = this; @@ -60,7 +60,6 @@ vm.loginSubmit = loginSubmit; vm.requestPasswordResetSubmit = requestPasswordResetSubmit; vm.setPasswordSubmit = setPasswordSubmit; - vm.labels = {}; localizationService.localizeMany([ vm.usernameIsEmail ? "general_email" : "general_username", @@ -76,6 +75,8 @@ // Check if it is a new user const inviteVal = $location.search().invite; + + vm.baseTitle = $scope.$root.locationTitle; //1 = enter password, 2 = password set, 3 = invalid token if (inviteVal && (inviteVal === "1" || inviteVal === "2")) { @@ -122,6 +123,7 @@ vm.showLogin(); } + SetTitle(); } function togglePassword() { @@ -173,6 +175,7 @@ vm.errorMsg = ""; resetInputValidation(); vm.view = "login"; + SetTitle(); } function showRequestPasswordReset() { @@ -180,12 +183,14 @@ resetInputValidation(); vm.view = "request-password-reset"; vm.showEmailResetConfirmation = false; + SetTitle(); } function showSetPassword() { vm.errorMsg = ""; resetInputValidation(); vm.view = "set-password"; + SetTitle(); } function loginSubmit() { @@ -413,6 +418,7 @@ } vm.twoFactor.view = viewPath; vm.view = "2fa-login"; + SetTitle(); } function resetInputValidation() { @@ -433,7 +439,28 @@ } + function SetTitle() { + var title = null; + switch (vm.view.toLowerCase()) { + case "login": + title = "Login"; + break; + case "password-reset-code-expired": + case "request-password-reset": + title = "Password Reset"; + break; + case "set-password": + title = "Change Password"; + break; + case "2fa-login": + title = "Two Factor Authentication"; + break; + } + if (title != null) { + $scope.$root.locationTitle = title + " - " + vm.baseTitle; + } + } } diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/buttons/umbbuttongroup.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/buttons/umbbuttongroup.directive.js index 74f0870f1b..301e542ec3 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/buttons/umbbuttongroup.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/buttons/umbbuttongroup.directive.js @@ -127,7 +127,9 @@ Use this directive to render a button with a dropdown of alternative actions. float: "@?", buttonStyle: "@?", size: "@?", - icon: "@?" + icon: "@?", + label: "@?", + labelKey: "@?" }, link: link }; diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js index 58a22ac74e..806d43f92b 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js @@ -21,6 +21,12 @@ $scope.page.menu.currentSection = appState.getSectionState("currentSection"); $scope.page.listViewPath = null; $scope.page.isNew = $scope.isNew ? true : false; + + if (infiniteMode) { + $scope.page.allowInfinitePublishAndClose = $scope.infiniteModel.allowPublishAndClose; + $scope.page.allowInfiniteSaveAndClose = $scope.infiniteModel.allowSaveAndClose; + } + $scope.page.buttonGroupState = "init"; $scope.page.hideActionsMenu = infiniteMode ? true : false; $scope.page.hideChangeVariant = false; @@ -56,11 +62,13 @@ function init() { var content = $scope.content; - if (content.id && content.isChildOfListView && content.trashed === false) { - $scope.page.listViewPath = ($routeParams.page) ? - "/content/content/edit/" + content.parentId + "?page=" + $routeParams.page : - "/content/content/edit/" + content.parentId; + $scope.page.listViewPath = "/content/content/edit/" + content.parentId + + "?list=" + $routeParams.list + + "&page=" + $routeParams.page + + "&filter=" + $routeParams.filter + + "&orderBy=" + $routeParams.orderBy + + "&orderDirection=" + $routeParams.orderDirection; } // we need to check wether an app is present in the current data, if not we will present the default app. @@ -369,12 +377,17 @@ saveMethod: args.saveMethod, scope: $scope, content: $scope.content, + create: $scope.page.isNew, action: args.action, showNotifications: args.showNotifications, softRedirect: true }).then(function (data) { //success init(); + + //needs to be manually set for infinite editing mode + $scope.page.isNew = false; + syncTreeNode($scope.content, data.path); eventsService.emit("content.saved", { content: $scope.content, action: args.action }); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbcontentnodeinfo.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbcontentnodeinfo.directive.js index e2f5f71781..a1df6c2914 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbcontentnodeinfo.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbcontentnodeinfo.directive.js @@ -1,7 +1,7 @@ (function () { 'use strict'; - function ContentNodeInfoDirective($timeout, logResource, eventsService, userService, localizationService, dateHelper, editorService, redirectUrlsResource, overlayService) { + function ContentNodeInfoDirective($timeout, logResource, eventsService, userService, localizationService, dateHelper, editorService, redirectUrlsResource, overlayService, entityResource) { function link(scope) { @@ -16,8 +16,12 @@ scope.disableTemplates = Umbraco.Sys.ServerVariables.features.disabledFeatures.disableTemplates; scope.allowChangeDocumentType = false; scope.allowChangeTemplate = false; + scope.allTemplates = []; function onInit() { + entityResource.getAll("Template").then(function (templates) { + scope.allTemplates = templates; + }); // set currentVariant scope.currentVariant = _.find(scope.node.variants, (v) => v.active); @@ -158,8 +162,12 @@ } scope.openTemplate = function () { + var template = _.findWhere(scope.allTemplates, {alias: scope.node.template}) + if (!template) { + return; + } var templateEditor = { - id: scope.node.templateId, + id: template.id, submit: function (model) { editorService.close(); }, diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorcontentheader.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorcontentheader.directive.js index 75fa0469bb..8668d62d4f 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorcontentheader.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorcontentheader.directive.js @@ -1,7 +1,7 @@ (function () { 'use strict'; - function EditorContentHeader(serverValidationManager) { + function EditorContentHeader(serverValidationManager, localizationService, editorState) { function link(scope, el, attr, ctrl) { @@ -13,7 +13,28 @@ if (!scope.serverValidationAliasField) { scope.serverValidationAliasField = "Alias"; } - + + scope.isNew = scope.content.state == "NotCreated"; + + localizationService.localizeMany([ + scope.isNew ? "placeholders_a11yCreateItem" : "placeholders_a11yEdit", + "placeholders_a11yName", + scope.isNew?"general_new":"general_edit"] + ).then(function (data) { + scope.a11yMessage = data[0]; + scope.a11yName = data[1]; + var title = data[2] +":"; + if (!scope.isNew) { + scope.a11yMessage += " " + scope.content.name; + title += scope.content.name; + } else { + var name = editorState.current.contentTypeName; + scope.a11yMessage += " " + name; + scope.a11yName = name + " " + scope.a11yName; + title += name; + } + scope.$root.locationTitle = title + " - " + scope.$root.locationTitle ; + }); scope.vm = {}; scope.vm.dropdownOpen = false; scope.vm.currentVariant = ""; diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditors.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditors.directive.js index 4104a663d3..9bdef41225 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditors.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditors.directive.js @@ -16,9 +16,7 @@ editor.moveRight = true; editor.level = 0; editor.styleIndex = 0; - - editor.infinityMode = true; - + // push the new editor to the dom scope.editors.push(editor); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/events/events.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/events/events.directive.js index 15e74bbd90..53aa7475c4 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/events/events.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/events/events.directive.js @@ -233,4 +233,41 @@ angular.module('umbraco.directives') }); } }; + }) + + // A slightly modified version of https://github.com/myplanet/angular-deep-blur/blob/master/angular-deep-blur.js - Kudos to Ufuk Kayserilioglu (paracycle) + .directive('deepBlur', function ($timeout) { + return { + + restrict: 'A', + + controller: function ($scope, $element, $attrs) { + var leaveExpr = $attrs.deepBlur, + dom = $element[0]; + + function containsDom(parent, dom) { + while (dom) { + if (dom === parent) { + return true; + } + dom = dom.parentNode; + } + return false; + } + + function onBlur(e) { + var targetElement = e.relatedTarget; + + if (!containsDom(dom, targetElement)) { + $timeout(function () { + $scope.$apply(leaveExpr); + }, 10); + } + } + + dom.addEventListener('blur', onBlur, true); + } + }; }); + + diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/hexbackgroundcolor.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/hexbackgroundcolor.directive.js index eb64439e0b..6780ad8e58 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/hexbackgroundcolor.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/hexbackgroundcolor.directive.js @@ -11,6 +11,11 @@ function hexBgColor() { restrict: "A", link: function (scope, element, attr, formCtrl) { + function setBackgroundColor(color) { + // note: can't use element.css(), it doesn't support hexa background colors + angular.element(element)[0].style.backgroundColor = "#" + color; + } + // Only add inline hex background color if defined and not "true". if (attr.hexBgInline === undefined || (attr.hexBgInline !== undefined && attr.hexBgInline === "true")) { @@ -26,18 +31,21 @@ function hexBgColor() { // Get the orig color before changing it. origColor = element.css("border-color"); } - // Validate it - test with and without the leading hash. - if (/^([0-9a-f]{3}|[0-9a-f]{6})$/i.test(newVal)) { - element.css("background-color", "#" + newVal); + // Is it a regular hex value - (#)AABBCC ? + var match = newVal.match(/^#?([0-9a-f]{3}|[0-9a-f]{6})$/i); + if (match && match.length) { + setBackgroundColor(match[1]); return; } - if (/^#([0-9a-f]{3}|[0-9a-f]{6})$/i.test(newVal)) { - element.css("background-color", newVal); + // Is it a hexa value - (#)AABBCCDD ? + match = newVal.match(/^#?([0-9a-f]{4}|[0-9a-f]{8})$/i); + if (match && match.length) { + setBackgroundColor(match[1]); return; } } - element.css("background-color", origColor); + setBackgroundColor(origColor); }); } } diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbcheckbox.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbcheckbox.directive.js index 47381a15c0..f8643ad191 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbcheckbox.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbcheckbox.directive.js @@ -22,14 +22,14 @@ @param {boolean} model Set to true or false to set the checkbox to checked or unchecked. -@param {string} input-id Set the id of the checkbox. +@param {string} inputId Set the id of the checkbox. @param {string} value Set the value of the checkbox. @param {string} name Set the name of the checkbox. @param {string} text Set the text for the checkbox label. -@param {string} server-validation-field Set the val-server-field of the checkbox. +@param {string} serverValidationField Set the val-server-field of the checkbox. @param {boolean} disabled Set the checkbox to be disabled. @param {boolean} required Set the checkbox to be required. -@param {string} on-change Callback when the value of the checkbox changed by interaction. +@param {callback} onChange Callback when the value of the checkbox change by interaction. **/ @@ -40,8 +40,8 @@ function UmbCheckboxController($timeout) { var vm = this; - - vm.callOnChange = function() { + + if (vm.onChange) { $timeout(function() { vm.onChange({model:vm.model, value:vm.value}); }, 0); @@ -49,7 +49,6 @@ } - var component = { templateUrl: 'views/components/forms/umb-checkbox.html', controller: UmbCheckboxController, @@ -63,7 +62,7 @@ serverValidationField: "@", disabled: "<", required: "<", - onChange: "&" + onChange: "&?" } }; diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbradiobutton.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbradiobutton.directive.js index 351ba2fee2..933107527b 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbradiobutton.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbradiobutton.directive.js @@ -27,31 +27,40 @@ @param {string} text Set the text for the radiobutton label. @param {boolean} disabled Set the radiobutton to be disabled. @param {boolean} required Set the radiobutton to be required. +@param {callback} onChange Callback when the value of the radiobutton change by interaction. **/ (function () { 'use strict'; - function RadiobuttonDirective() { - var directive = { - restrict: 'E', - replace: true, - templateUrl: 'views/components/forms/umb-radiobutton.html', - scope: { - model: "=", - value: "@", - name: "@", - text: "@", - disabled: "=", - required: "=" - } - }; + function UmbRadiobuttonController($timeout) { - return directive; + var vm = this; + if (vm.onChange) { + $timeout(function () { + vm.onChange({ model: vm.model, value: vm.value }); + }, 0); + } + } - angular.module('umbraco.directives').directive('umbRadiobutton', RadiobuttonDirective); + var component = { + templateUrl: 'views/components/forms/umb-radiobutton.html', + controller: UmbRadiobuttonController, + controllerAs: 'vm', + bindings: { + model: "=", + value: "@", + name: "@", + text: "@", + disabled: "=", + required: "=", + onChange: "&?" + } + }; + + angular.module('umbraco.directives').component('umbRadiobutton', component); })(); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/grid/grid.rte.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/grid/grid.rte.directive.js index cd1f011018..af82199d55 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/grid/grid.rte.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/grid/grid.rte.directive.js @@ -14,6 +14,8 @@ angular.module("umbraco.directives") // TODO: A lot of the code below should be shared between the grid rte and the normal rte + scope.isLoading = true; + var promises = []; //To id the html textarea we need to use the datetime ticks because we can have multiple rte's per a single property alias @@ -91,6 +93,10 @@ angular.module("umbraco.directives") //custom initialization for this editor within the grid editor.on('init', function (e) { + // Used this init event - as opposed to property init_instance_callback + // to turn off the loader + scope.isLoading = false; + //force overflow to hidden to prevent no needed scroll editor.getBody().style.overflow = "hidden"; diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/media/umbmedianodeinfo.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/media/umbmedianodeinfo.directive.js index 577ffe0176..4993b013c7 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/media/umbmedianodeinfo.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/media/umbmedianodeinfo.directive.js @@ -1,7 +1,7 @@ (function () { 'use strict'; - function MediaNodeInfoDirective($timeout, $location, eventsService, userService, dateHelper, editorService) { + function MediaNodeInfoDirective($timeout, $location, eventsService, userService, dateHelper, editorService, mediaHelper) { function link(scope, element, attrs, ctrl) { @@ -28,6 +28,9 @@ // make sure dates are formatted to the user's locale formatDatesToLocal(); + + // set media file extension initially + setMediaExtension(); } function formatDatesToLocal() { @@ -49,6 +52,10 @@ } } + function setMediaExtension() { + scope.node.extension = mediaHelper.getFileExtension(scope.nodeUrl); + } + scope.openMediaType = function (mediaType) { var editor = { id: mediaType.id, @@ -62,6 +69,16 @@ editorService.mediaTypeEditor(editor); }; + scope.openSVG = function () { + var popup = window.open('', '_blank'); + var html = '' + + ''; + + popup.document.open(); + popup.document.write(html); + popup.document.close(); + } + // watch for content updates - reload content when node is saved, published etc. scope.$watch('node.updateDate', function(newValue, oldValue){ if(!newValue) { return; } @@ -72,6 +89,9 @@ // Update the create and update dates formatDatesToLocal(); + + //Update the media file format + setMediaExtension(); }); //ensure to unregister from all events! diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbflatpickr.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbdatetimepicker.directive.js similarity index 93% rename from src/Umbraco.Web.UI.Client/src/common/directives/components/umbflatpickr.directive.js rename to src/Umbraco.Web.UI.Client/src/common/directives/components/umbdatetimepicker.directive.js index 79b29b407b..899b8f3c23 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbflatpickr.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbdatetimepicker.directive.js @@ -1,13 +1,13 @@ /** @ngdoc directive -@name umbraco.directives.directive:umbFlatpickr +@name umbraco.directives.directive:umbDateTimePicker @restrict E @scope @description Added in Umbraco version 8.0 This directive is a wrapper of the flatpickr library. Use it to render a date time picker. -For extra details about options and events take a look here: https://flatpickr.js.org/ +For extra details about options and events take a look here: https://flatpickr.js.org Use this directive to render a date time picker @@ -15,11 +15,11 @@ Use this directive to render a date time picker
 	
- - +
@@ -70,12 +70,12 @@ Use this directive to render a date time picker (function() { 'use strict'; - var umbFlatpickr = { + var umbDateTimePicker = { template: '' + '' + '
' + '
', - controller: umbFlatpickrCtrl, + controller: umbDateTimePickerCtrl, transclude: true, bindings: { ngModel: '<', @@ -92,9 +92,9 @@ Use this directive to render a date time picker } }; - function umbFlatpickrCtrl($element, $timeout, $scope, assetsService, userService) { + function umbDateTimePickerCtrl($element, $timeout, $scope, assetsService, userService) { + var ctrl = this; - var loaded = false; var userLocale = null; ctrl.$onInit = function() { @@ -102,14 +102,14 @@ Use this directive to render a date time picker // load css file for the date picker assetsService.loadCss('lib/flatpickr/flatpickr.css', $scope).then(function () { userService.getCurrentUser().then(function (user) { + // init date picker userLocale = user.locale; if (userLocale.indexOf('-') > -1) { userLocale = userLocale.split('-')[0]; } - loaded = true; - grabElementAndRunFlatpickr(); + grabElementAndRunFlatpickr(); }); }); @@ -234,7 +234,8 @@ Use this directive to render a date time picker } } - - angular.module('umbraco.directives').component('umbFlatpickr', umbFlatpickr); - + + // umbFlatpickr (umb-flatpickr) is deprecated, but we keep it for backwards compatibility + angular.module('umbraco.directives').component('umbFlatpickr', umbDateTimePicker); + angular.module('umbraco.directives').component('umbDateTimePicker', umbDateTimePicker); })(); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbgroupsbuilder.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbgroupsbuilder.directive.js index a51afd200c..ba479429f1 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbgroupsbuilder.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbgroupsbuilder.directive.js @@ -17,6 +17,7 @@ scope.sortableOptionsGroup = {}; scope.sortableOptionsProperty = {}; scope.sortingButtonKey = "general_reorder"; + scope.compositionsButtonState = "init"; function activate() { @@ -47,6 +48,7 @@ function setSortingOptions() { scope.sortableOptionsGroup = { + axis: 'y', distance: 10, tolerance: "pointer", opacity: 0.7, @@ -65,6 +67,7 @@ }; scope.sortableOptionsProperty = { + axis: 'y', distance: 10, tolerance: "pointer", connectWith: ".umb-group-builder__properties", @@ -254,7 +257,7 @@ view: "views/common/infiniteeditors/compositions/compositions.html", size: "small", submit: function() { - + // make sure that all tabs has an init property if (scope.model.groups.length !== 0) { angular.forEach(scope.model.groups, function(group) { @@ -335,9 +338,10 @@ })), function(f) { return f !== null && f !== undefined; }); + scope.compositionsButtonState = "busy"; $q.all([ //get available composite types - availableContentTypeResource(scope.model.id, [], propAliasesExisting).then(function (result) { + availableContentTypeResource(scope.model.id, [], propAliasesExisting, scope.model.isElement).then(function (result) { setupAvailableContentTypesModel(result); }), //get where used document types @@ -354,6 +358,7 @@ ]).then(function() { //resolves when both other promises are done, now show it editorService.open(scope.compositionsDialogModel); + scope.compositionsButtonState = "init"; }); }; @@ -379,6 +384,8 @@ // activate group scope.activateGroup(group); + // push new init tab to the scope + addInitGroup(scope.model.groups); }; scope.activateGroup = function(selectedGroup) { @@ -397,7 +404,6 @@ scope.removeGroup = function(groupIndex) { scope.model.groups.splice(groupIndex, 1); - addInitGroup(scope.model.groups); }; scope.updateGroupTitle = function(group) { @@ -526,10 +532,8 @@ // set focus on init property var numberOfProperties = group.properties.length; group.properties[numberOfProperties - 1].focus = true; - - // push new init tab to the scope - addInitGroup(scope.model.groups); + notifyChanged(); }, close: function() { if(_.isEqual(oldPropertyModel, propertyModel) === false) { @@ -575,19 +579,13 @@ // remove property tab.properties.splice(propertyIndex, 1); - // if the last property in group is an placeholder - remove add new tab placeholder - if(tab.properties.length === 1 && tab.properties[0].propertyState === "init") { - - angular.forEach(scope.model.groups, function(group, index, groups){ - if(group.tabState === 'init') { - groups.splice(index, 1); - } - }); - - } - + notifyChanged(); }; + function notifyChanged() { + eventsService.emit("editors.groupsBuilder.changed"); + } + function addInitProperty(group) { var addInitPropertyBool = true; diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umblayoutselector.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umblayoutselector.directive.js index 58a5e1be0e..7453353018 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umblayoutselector.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umblayoutselector.directive.js @@ -24,6 +24,7 @@ vm.showLayoutSelector = true; vm.pickLayout = pickLayout; vm.toggleLayoutDropdown = toggleLayoutDropdown; + vm.leaveLayoutDropdown = leaveLayoutDropdown; vm.closeLayoutDropdown = closeLayoutDropdown; function onInit() { @@ -38,6 +39,10 @@ vm.layoutDropDownIsOpen = !vm.layoutDropDownIsOpen; } + function leaveLayoutDropdown() { + vm.layoutDropDownIsOpen = false; + } + function pickLayout(selectedLayout) { if (vm.onLayoutSelect) { vm.onLayoutSelect({ layout: selectedLayout }); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbloader.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbloader.directive.js new file mode 100644 index 0000000000..e70f7b3cac --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbloader.directive.js @@ -0,0 +1,75 @@ +/** +@ngdoc directive +@name umbraco.directives.directive:umbLoader +@restrict E + +@description +Use this directive to generate a loading indicator. + +

Markup example

+
+    
+ + + + +
+

{{content}}

+
+ +
+
+ +

Controller example

+
+    (function () {
+        "use strict";
+
+        function Controller(myService) {
+
+            var vm = this;
+
+            vm.content = "";
+            vm.loading = true;
+
+            myService.getContent().then(function(content){
+                vm.content = content;
+                vm.loading = false;
+            });
+
+        }
+
+        angular.module("umbraco").controller("My.Controller", Controller);
+    })();
+
+ +@param {string=} position The loader position ("top", "bottom"). + +**/ + +(function() { + 'use strict'; + + function UmbLoaderDirective() { + + function link(scope, el, attr, ctrl) { + + } + + var directive = { + restrict: 'E', + replace: true, + templateUrl: 'views/components/umb-loader.html', + scope: { + position: "@?" + }, + link: link + }; + + return directive; + } + + angular.module('umbraco.directives').directive('umbLoader', UmbLoaderDirective); + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbloadindicator.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbloadindicator.directive.js index 0671770796..c45b8f3f47 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbloadindicator.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbloadindicator.directive.js @@ -39,7 +39,7 @@ Use this directive to generate a loading indicator. }); } -½ + angular.module("umbraco").controller("My.Controller", Controller); })(); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbstickybar.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbstickybar.directive.js index 07e45ff0f7..de6555a05a 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbstickybar.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbstickybar.directive.js @@ -25,92 +25,64 @@ Use this directive make an element sticky and follow the page when scrolling. `u function StickyBarDirective() { - /** - On initial load, the intersector fires if the grid editor is in the viewport - This flag is used to suppress the setClass behaviour on the initial load - **/ - var initial = true; + let headerObserver; /** Toggle `umb-sticky-bar--active` class on the sticky-bar element **/ - function setClass(addClass, current) { - if (!initial) { - current.classList.toggle('umb-sticky-bar--active', addClass); - } else { - initial = false; - } - } + const setClass = (addClass, current) => current.classList.toggle('umb-sticky-bar--active', addClass); /** Inserts two elements in the umbStickyBar parent element These are used by the IntersectionObserve to calculate scroll position **/ - function addSentinels(current) { - ['-top', '-bottom'].forEach(s => { - const sentinel = document.createElement('div'); - sentinel.classList.add('umb-sticky-sentinel', s); - current.parentElement.appendChild(sentinel); - }); - } - - /** - Calls into setClass when the footer sentinel enters/exits the bottom of the container - Container is the parent element of the umbStickyBar element - **/ - function observeFooter(current, container) { - const observer = new IntersectionObserver((records, observer) => { - let [target, rootBounds, intersected] = [records[0].boundingClientRect, records[0].rootBounds, records[0].intersectionRatio === 1]; - - if (target.bottom > rootBounds.top && intersected) { - setClass(true, current); - } - if (target.top < rootBounds.top && target.bottom < rootBounds.bottom) { - setClass(false, current); - } - }, { - threshold: [1], - root: container - }); - - observer.observe(current.parentElement.querySelector('.umb-sticky-sentinel.-bottom')); - } + const addSentinel = current => { + const sentinel = document.createElement('div'); + sentinel.classList.add('umb-sticky-sentinel', '-top'); + current.parentElement.prepend(sentinel); + }; /** Calls into setClass when the header sentinel enters/exits the top of the container Container is the parent element of the umbStickyBar element **/ - function observeHeader(current, container) { - const observer = new IntersectionObserver((records, observer) => { + const observeHeader = (current, container) => { + headerObserver = new IntersectionObserver((records, observer) => { let [target, rootBounds] = [records[0].boundingClientRect, records[0].rootBounds]; - if (target.bottom < rootBounds.top) { - setClass(true, current); - } + if (rootBounds && target) { + if (target.bottom < rootBounds.top) { + setClass(true, current); + } - if (target.bottom >= rootBounds.top && target.bottom < rootBounds.bottom) { - setClass(false, current); + if (target.bottom >= rootBounds.top && target.bottom < rootBounds.bottom) { + setClass(false, current); + } } }, { threshold: [0], root: container }); - observer.observe(current.parentElement.querySelector('.umb-sticky-sentinel.-top')); - } + headerObserver.observe(current.parentElement.querySelector('.umb-sticky-sentinel.-top')); + }; function link(scope, el, attr, ctrl) { let current = el[0]; - let container = current.closest('[data-element="editor-container"]'); + let container = current.closest('.umb-editor-container') || current.closest('.umb-dashboard'); - addSentinels(current); + if (container) { + addSentinel(current); + observeHeader(current, container); + } - observeHeader(current, container); - observeFooter(current, container); + scope.$on('$destroy', () => { + headerObserver.disconnect(); + }); } - var directive = { + const directive = { restrict: 'A', link: link }; diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/upload/umbpropertyfileupload.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/upload/umbpropertyfileupload.directive.js index e5bdd3ca78..6360b429b9 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/upload/umbpropertyfileupload.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/upload/umbpropertyfileupload.directive.js @@ -176,6 +176,11 @@ } function getThumbnail(file) { + + if (file.extension === 'svg') { + return file.fileName; + } + if (!file.isImage) { return null; } diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/users/changepassword.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/users/changepassword.directive.js index f8dbefa5d7..97eb2bf708 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/users/changepassword.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/users/changepassword.directive.js @@ -1,167 +1,172 @@ (function () { - 'use strict'; + 'use strict'; - function ChangePasswordController($scope) { + function ChangePasswordController($scope) { - function resetModel(isNew) { - //the model config will contain an object, if it does not we'll create defaults - //NOTE: We will not support doing the password regex on the client side because the regex on the server side - //based on the membership provider cannot always be ported to js from .net directly. - /* - { - hasPassword: true/false, - requiresQuestionAnswer: true/false, - enableReset: true/false, - enablePasswordRetrieval: true/false, - minPasswordLength: 10 - } - */ + var vm = this; - $scope.showReset = false; + vm.$onInit = onInit; + vm.$onDestroy = onDestroy; + vm.doChange = doChange; + vm.cancelChange = cancelChange; + vm.showOldPass = showOldPass; + vm.showCancelBtn = showCancelBtn; - //set defaults if they are not available - if ($scope.config.disableToggle === undefined) { - $scope.config.disableToggle = false; - } - if ($scope.config.hasPassword === undefined) { - $scope.config.hasPassword = false; - } - if ($scope.config.enablePasswordRetrieval === undefined) { - $scope.config.enablePasswordRetrieval = true; - } - if ($scope.config.requiresQuestionAnswer === undefined) { - $scope.config.requiresQuestionAnswer = false; - } - //don't enable reset if it is new - that doesn't make sense - if (isNew === "true") { - $scope.config.enableReset = false; - } - else if ($scope.config.enableReset === undefined) { - $scope.config.enableReset = true; - } - - if ($scope.config.minPasswordLength === undefined) { - $scope.config.minPasswordLength = 0; - } - - //set the model defaults - if (!angular.isObject($scope.passwordValues)) { - //if it's not an object then just create a new one - $scope.passwordValues = { - newPassword: null, - oldPassword: null, - reset: null, - answer: null - }; - } - else { - //just reset the values + var unsubscribe = []; + + function resetModel(isNew) { + //the model config will contain an object, if it does not we'll create defaults + //NOTE: We will not support doing the password regex on the client side because the regex on the server side + //based on the membership provider cannot always be ported to js from .net directly. + /* + { + hasPassword: true/false, + requiresQuestionAnswer: true/false, + enableReset: true/false, + enablePasswordRetrieval: true/false, + minPasswordLength: 10 + } + */ + + vm.showReset = false; + + //set defaults if they are not available + if (vm.config.disableToggle === undefined) { + vm.config.disableToggle = false; + } + if (vm.config.hasPassword === undefined) { + vm.config.hasPassword = false; + } + if (vm.config.enablePasswordRetrieval === undefined) { + vm.config.enablePasswordRetrieval = true; + } + if (vm.config.requiresQuestionAnswer === undefined) { + vm.config.requiresQuestionAnswer = false; + } + //don't enable reset if it is new - that doesn't make sense + if (isNew === "true") { + vm.config.enableReset = false; + } + else if (vm.config.enableReset === undefined) { + vm.config.enableReset = true; + } + + if (vm.config.minPasswordLength === undefined) { + vm.config.minPasswordLength = 0; + } + + //set the model defaults + if (!angular.isObject(vm.passwordValues)) { + //if it's not an object then just create a new one + vm.passwordValues = { + newPassword: null, + oldPassword: null, + reset: null, + answer: null + }; + } + else { + //just reset the values + + if (!isNew) { + //if it is new, then leave the generated pass displayed + vm.passwordValues.newPassword = null; + vm.passwordValues.oldPassword = null; + } + vm.passwordValues.reset = null; + vm.passwordValues.answer = null; + } + + //the value to compare to match passwords + if (!isNew) { + vm.passwordValues.confirm = ""; + } + else if (vm.passwordValues.newPassword && vm.passwordValues.newPassword.length > 0) { + //if it is new and a new password has been set, then set the confirm password too + vm.passwordValues.confirm = vm.passwordValues.newPassword; + } - if (!isNew) { - //if it is new, then leave the generated pass displayed - $scope.passwordValues.newPassword = null; - $scope.passwordValues.oldPassword = null; } - $scope.passwordValues.reset = null; - $scope.passwordValues.answer = null; - } - //the value to compare to match passwords - if (!isNew) { - $scope.passwordValues.confirm = ""; - } - else if ($scope.passwordValues.newPassword && $scope.passwordValues.newPassword.length > 0) { - //if it is new and a new password has been set, then set the confirm password too - $scope.passwordValues.confirm = $scope.passwordValues.newPassword; - } + //when the scope is destroyed we need to unsubscribe + function onDestroy() { + for (var u in unsubscribe) { + unsubscribe[u](); + } + } + + function onInit() { + //listen for the saved event, when that occurs we'll + //change to changing = false; + unsubscribe.push($scope.$on("formSubmitted", function () { + if (vm.config.disableToggle === false) { + vm.changing = false; + } + })); + + unsubscribe.push($scope.$on("formSubmitting", function () { + //if there was a previously generated password displaying, clear it + if (vm.changing && vm.passwordValues) { + vm.passwordValues.generatedPassword = null; + } + else if (!vm.changing) { + //we are not changing, so the model needs to be null + vm.passwordValues = null; + } + })); + + resetModel(vm.isNew); + + //if there is no password saved for this entity , it must be new so we do not allow toggling of the change password, it is always there + //with validators turned on. + vm.changing = vm.config.disableToggle === true || !vm.config.hasPassword; + + //we're not currently changing so set the model to null + if (!vm.changing) { + vm.passwordValues = null; + } + } + + function doChange() { + resetModel(); + vm.changing = true; + //if there was a previously generated password displaying, clear it + vm.passwordValues.generatedPassword = null; + vm.passwordValues.confirm = null; + }; + + function cancelChange() { + vm.changing = false; + //set model to null + vm.passwordValues = null; + }; + + function showOldPass() { + return vm.config.hasPassword && + !vm.config.allowManuallyChangingPassword && + !vm.config.enablePasswordRetrieval && !vm.showReset; + }; + + // TODO: I don't think we need this or the cancel button, this can be up to the editor rendering this component + function showCancelBtn() { + return vm.config.disableToggle !== true && vm.config.hasPassword; + }; } - resetModel($scope.isNew); - - //if there is no password saved for this entity , it must be new so we do not allow toggling of the change password, it is always there - //with validators turned on. - $scope.changing = $scope.config.disableToggle === true || !$scope.config.hasPassword; - - //we're not currently changing so set the model to null - if (!$scope.changing) { - $scope.passwordValues = null; - } - - $scope.doChange = function () { - resetModel(); - $scope.changing = true; - //if there was a previously generated password displaying, clear it - $scope.passwordValues.generatedPassword = null; - $scope.passwordValues.confirm = null; + var component = { + templateUrl: 'views/components/users/change-password.html', + controller: ChangePasswordController, + controllerAs: 'vm', + bindings: { + isNew: "<", + passwordValues: "=", //TODO: Do we need bi-directional vals? + config: "=" //TODO: Do we need bi-directional vals? + //TODO: Do we need callbacks? + } }; - $scope.cancelChange = function () { - $scope.changing = false; - //set model to null - $scope.passwordValues = null; - }; - - var unsubscribe = []; - - //listen for the saved event, when that occurs we'll - //change to changing = false; - unsubscribe.push($scope.$on("formSubmitted", function () { - if ($scope.config.disableToggle === false) { - $scope.changing = false; - } - })); - unsubscribe.push($scope.$on("formSubmitting", function () { - //if there was a previously generated password displaying, clear it - if ($scope.changing && $scope.passwordValues) { - $scope.passwordValues.generatedPassword = null; - } - else if (!$scope.changing) { - //we are not changing, so the model needs to be null - $scope.passwordValues = null; - } - })); - - //when the scope is destroyed we need to unsubscribe - $scope.$on('$destroy', function () { - for (var u in unsubscribe) { - unsubscribe[u](); - } - }); - - $scope.showOldPass = function () { - return $scope.config.hasPassword && - !$scope.config.allowManuallyChangingPassword && - !$scope.config.enablePasswordRetrieval && !$scope.showReset; - }; - - // TODO: I don't think we need this or the cancel button, this can be up to the editor rendering this directive - $scope.showCancelBtn = function () { - return $scope.config.disableToggle !== true && $scope.config.hasPassword; - }; - - } - - function ChangePasswordDirective() { - - var directive = { - restrict: 'E', - replace: true, - templateUrl: 'views/components/users/change-password.html', - controller: 'Umbraco.Editors.Users.ChangePasswordDirectiveController', - scope: { - isNew: "=?", - passwordValues: "=", - config: "=" - } - }; - - return directive; - - } - - angular.module('umbraco.directives').controller('Umbraco.Editors.Users.ChangePasswordDirectiveController', ChangePasswordController); - angular.module('umbraco.directives').directive('changePassword', ChangePasswordDirective); + angular.module('umbraco.directives').component('changePassword', component); })(); diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/contenttype.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/contenttype.resource.js index 4e4c8d2eb5..64accc18c1 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/contenttype.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/contenttype.resource.js @@ -16,7 +16,7 @@ function contentTypeResource($q, $http, umbRequestHelper, umbDataFormatter, loca 'Failed to retrieve count'); }, - getAvailableCompositeContentTypes: function (contentTypeId, filterContentTypes, filterPropertyTypes) { + getAvailableCompositeContentTypes: function (contentTypeId, filterContentTypes, filterPropertyTypes, isElement) { if (!filterContentTypes) { filterContentTypes = []; } @@ -27,7 +27,8 @@ function contentTypeResource($q, $http, umbRequestHelper, umbDataFormatter, loca var query = { contentTypeId: contentTypeId, filterContentTypes: filterContentTypes, - filterPropertyTypes: filterPropertyTypes + filterPropertyTypes: filterPropertyTypes, + isElement: isElement }; return umbRequestHelper.resourcePromise( diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/entity.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/entity.resource.js index a910629f4a..9cf1181cfa 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/entity.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/entity.resource.js @@ -104,21 +104,26 @@ function entityResource($q, $http, umbRequestHelper) { * * @param {Int} id Id of node to return the public url to * @param {string} type Object type name + * @param {string} culture Culture * @returns {Promise} resourcePromise object containing the url. * */ - getUrl: function (id, type) { + getUrl: function (id, type, culture) { if (id === -1 || id === "-1") { return ""; } + if (!culture) { + culture = ""; + } + return umbRequestHelper.resourcePromise( $http.get( umbRequestHelper.getApiUrl( "entityApiBaseUrl", "GetUrl", - [{ id: id }, {type: type }])), + [{ id: id }, {type: type }, {culture: culture }])), 'Failed to retrieve url for id:' + id); }, @@ -133,7 +138,7 @@ function entityResource($q, $http, umbRequestHelper) { * ##usage *
          * //get media by id
-         * entityResource.getEntityById(0, "Media")
+         * entityResource.getById(0, "Media")
          *    .then(function(ent) {
          *        var myDoc = ent;
          *        alert('its here!');
@@ -204,7 +209,7 @@ function entityResource($q, $http, umbRequestHelper) {
          * ##usage
          * 
          * //Get templates for ids
-         * entityResource.getEntitiesByIds( [1234,2526,28262], "Template")
+         * entityResource.getByIds( [1234,2526,28262], "Template")
          *    .then(function(templateArray) {
          *        var myDoc = contentArray;
          *        alert('they are here!');
diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/logviewer.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/logviewer.resource.js
index b52021ed38..d2d008640d 100644
--- a/src/Umbraco.Web.UI.Client/src/common/resources/logviewer.resource.js
+++ b/src/Umbraco.Web.UI.Client/src/common/resources/logviewer.resource.js
@@ -7,64 +7,44 @@
  **/
 function logViewerResource($q, $http, umbRequestHelper) {
 
+    /**
+     * verb => 'get', 'post',
+     * method => API method to call
+     * params => additional data to send
+     * error => error message when things go wrong...
+     */
+    const request = (verb, method, params, error) =>
+        umbRequestHelper.resourcePromise(
+            (verb === 'GET' ?
+            $http.get(umbRequestHelper.getApiUrl("logViewerApiBaseUrl", method) + (params ? params : '')) : 
+            $http.post(umbRequestHelper.getApiUrl("logViewerApiBaseUrl", method), params)),
+        error); 
+
     //the factory object returned
     return {
 
-        getNumberOfErrors: function (startDate, endDate) {
-            return umbRequestHelper.resourcePromise(
-                $http.get(
-                    umbRequestHelper.getApiUrl(
-                        "logViewerApiBaseUrl",
-                        "GetNumberOfErrors")+ '?startDate='+startDate+ '&endDate='+  endDate ),
-                'Failed to retrieve number of errors in logs');
-        },
+        getNumberOfErrors: (startDate, endDate) => 
+            request('GET', 'GetNumberOfErrors', '?startDate=' + startDate + '&endDate=' + endDate, 'Failed to retrieve number of errors in logs'),    
 
-        getLogLevelCounts: function (startDate, endDate) {
-            return umbRequestHelper.resourcePromise(
-                $http.get(
-                    umbRequestHelper.getApiUrl(
-                        "logViewerApiBaseUrl",
-                        "GetLogLevelCounts")+ '?startDate='+startDate+ '&endDate='+  endDate ),
-                'Failed to retrieve log level counts');
-        },
+        getLogLevel: () =>
+            request('GET', 'GetLogLevel', null, 'Failed to retrieve log level'),        
 
-        getMessageTemplates: function (startDate, endDate) {
-            return umbRequestHelper.resourcePromise(
-                $http.get(
-                    umbRequestHelper.getApiUrl(
-                        "logViewerApiBaseUrl",
-                        "GetMessageTemplates")+ '?startDate='+startDate+ '&endDate='+  endDate ),
-                'Failed to retrieve log templates');
-        },
+        getLogLevelCounts: (startDate, endDate) =>
+            request('GET', 'GetLogLevelCounts', '?startDate=' + startDate + '&endDate=' + endDate, 'Failed to retrieve log level counts'),  
 
-        getSavedSearches: function () {
-            return umbRequestHelper.resourcePromise(
-                $http.get(
-                    umbRequestHelper.getApiUrl(
-                        "logViewerApiBaseUrl",
-                        "GetSavedSearches")),
-                'Failed to retrieve saved searches');
-        },
+        getMessageTemplates: (startDate, endDate) => 
+            request('GET', 'GetMessageTemplates', '?startDate=' + startDate + '&endDate=' + endDate, 'Failed to retrieve log templates'), 
 
-        postSavedSearch: function (name, query) {
-            return umbRequestHelper.resourcePromise(
-                $http.post(
-                    umbRequestHelper.getApiUrl(
-                        "logViewerApiBaseUrl",
-                        "PostSavedSearch"), { 'name': name, 'query': query }),
-                'Failed to add new saved search');
-        },
+        getSavedSearches: () =>
+            request('GET', 'GetSavedSearches', null, 'Failed to retrieve saved searches'),      
 
-        deleteSavedSearch: function (name, query) {
-            return umbRequestHelper.resourcePromise(
-                $http.post(
-                    umbRequestHelper.getApiUrl(
-                        "logViewerApiBaseUrl",
-                        "DeleteSavedSearch"), { 'name': name, 'query': query }),
-                'Failed to delete saved search');
-        },
+        postSavedSearch: (name, query) =>
+            request('POST', 'PostSavedSearch', { 'name': name, 'query': query }, 'Failed to add new saved search'),
 
-        getLogs: function (options) {
+        deleteSavedSearch: (name, query) =>
+            request('POST', 'DeleteSavedSearch', { 'name': name, 'query': query }, 'Failed to delete saved search'),
+
+        getLogs: options => {
 
             var defaults = {
                 pageSize: 100,
@@ -83,7 +63,6 @@ function logViewerResource($q, $http, umbRequestHelper) {
             //now copy back to the options we will use
             options = defaults;
 
-
             return umbRequestHelper.resourcePromise(
                 $http.get(
                     umbRequestHelper.getApiUrl(
@@ -93,15 +72,8 @@ function logViewerResource($q, $http, umbRequestHelper) {
                 'Failed to retrieve common log messages');
         },
 
-        canViewLogs: function (startDate, endDate) {
-            return umbRequestHelper.resourcePromise(
-                $http.get(
-                    umbRequestHelper.getApiUrl(
-                        "logViewerApiBaseUrl",
-                        "GetCanViewLogs") + '?startDate='+startDate+ '&endDate='+  endDate ),
-                'Failed to retrieve state if logs can be viewed');
-        }
-
+        canViewLogs: (startDate, endDate) => 
+            request('GET', 'GetCanViewLogs', '?startDate=' + startDate + '&endDate=' + endDate, 'Failed to retrieve state if logs can be viewed')    
     };
 }
 
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 732f682082..200e61ee96 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
@@ -1,4 +1,4 @@
-
+
 /**
 * @ngdoc service
 * @name umbraco.services.contentEditingHelper
@@ -53,6 +53,12 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, editorSt
             }
             if (args.showNotifications === undefined) {
                 args.showNotifications = true;
+            }
+			// needed for infinite editing to create new items
+			if (args.create === undefined) {
+                if ($routeParams.create) {
+                    args.create = true;
+                }
             }
             if (args.softRedirect === undefined) {
                 //when true, the url will change but it won't actually re-route
@@ -61,6 +67,7 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, editorSt
                 args.softRedirect = false; 
             }
 
+
             var self = this;
 
             //we will use the default one for content if not specified
@@ -68,7 +75,7 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, editorSt
 
             if (formHelper.submitForm({ scope: args.scope, action: args.action })) {
 
-                return args.saveMethod(args.content, $routeParams.create, fileManager.getFiles(), args.showNotifications)
+                return args.saveMethod(args.content, args.create, fileManager.getFiles(), args.showNotifications)
                     .then(function (data) {
 
                         formHelper.resetForm({ scope: args.scope });
@@ -484,7 +491,7 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, editorSt
             var savedVariants = [];
             if (origContent.variants) {
                 isContent = true;
-                //it's contnet so assign the variants as they exist
+                //it's content so assign the variants as they exist
                 origVariants = origContent.variants;
                 savedVariants = savedContent.variants;
             }
@@ -510,7 +517,7 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, editorSt
 
                 //special case for content, don't sync this variant if it wasn't tagged
                 //for saving in the first place
-                if (!origVariant.save) {
+                if (isContent && !origVariant.save) {
                     continue;
                 }
 
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 72957e1c72..ad565412d9 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
@@ -261,7 +261,7 @@ When building a custom infinite editor view you can use the same components as a
             */
             unbindKeyboardShortcuts();
 
-            // set flag so we know when the editor is open in "infinie mode"
+            // set flag so we know when the editor is open in "infinite mode"
             editor.infiniteMode = true;
 
             editors.push(editor);
@@ -331,13 +331,17 @@ When building a custom infinite editor view you can use the same components as a
          * @methodOf umbraco.services.editorService
          *
          * @description
-         * Opens a media editor in infinite editing, the submit callback returns the updated content item
+         * Opens a content editor in infinite editing, the submit callback returns the updated content item
          * @param {Object} editor rendering options
          * @param {String} editor.id The id of the content item
          * @param {Boolean} editor.create Create new content item
          * @param {Function} editor.submit Callback function when the publish and close button is clicked. Returns the editor model object
          * @param {Function} editor.close Callback function when the close button is clicked.
-         *
+         * @param {String} editor.parentId If editor.create is true, provide parentId for the creation of the content item
+         * @param {String} editor.documentTypeAlias If editor.create is true, provide document type alias for the creation of the content item
+         * @param {Boolean} editor.allowSaveAndClose If editor is being used in infinite editing allows the editor to close when the save action is performed
+         * @param {Boolean} editor.allowPublishAndClose If editor is being used in infinite editing allows the editor to close when the publish action is performed
+         * 
          * @returns {Object} editor object
          */
         function contentEditor(editor) {
diff --git a/src/Umbraco.Web.UI.Client/src/common/services/mediahelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/mediahelper.service.js
index 9350af1c47..c8acdec353 100644
--- a/src/Umbraco.Web.UI.Client/src/common/services/mediahelper.service.js
+++ b/src/Umbraco.Web.UI.Client/src/common/services/mediahelper.service.js
@@ -378,7 +378,7 @@ function mediaHelper(umbRequestHelper, $log) {
         getFileExtension: function (filePath) {
 
             if (!filePath) {
-                return false;
+                return null;
             }
 
             var lowered = filePath.toLowerCase();
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 e61bd38bc0..5578c7a609 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
@@ -233,17 +233,22 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s
 
                     //this would be for a theme other than inlite
                     toolbar: args.toolbar.join(" "),
+
                     //these are for the inlite theme to work
                     insert_toolbar: toolbars.insertToolbar,
                     selection_toolbar: toolbars.selectionToolbar,
 
                     body_class: 'umb-rte',
+
                     //see http://archive.tinymce.com/wiki.php/Configuration:cache_suffix
                     cache_suffix: "?umb__rnd=" + Umbraco.Sys.ServerVariables.application.cacheBuster,
 
                     //this is used to style the inline macro bits, sorry hard coding this form now since we don't have a standalone
                     //stylesheet to load in for this with only these styles (the color is @pinkLight)
-                    content_style: ".mce-content-body .umb-macro-holder { border: 3px dotted #f5c1bc; padding: 7px; display: block; margin: 3px; } .umb-rte .mce-content-body .umb-macro-holder.loading {background: url(assets/img/loader.gif) right no-repeat; background-size: 18px; background-position-x: 99%;}"
+                    content_style: ".mce-content-body .umb-macro-holder { border: 3px dotted #f5c1bc; padding: 7px; display: block; margin: 3px; } .umb-rte .mce-content-body .umb-macro-holder.loading {background: url(assets/img/loader.gif) right no-repeat; background-size: 18px; background-position-x: 99%;}",
+
+                    // This allows images to be pasted in & stored as Base64 until they get uploaded to server
+                    paste_data_images: true
                 };
 
                 if (tinyMceConfig.customConfig) {
diff --git a/src/Umbraco.Web.UI.Client/src/installer/installer.service.js b/src/Umbraco.Web.UI.Client/src/installer/installer.service.js
index 50b9f2f3c0..f6f162f04f 100644
--- a/src/Umbraco.Web.UI.Client/src/installer/installer.service.js
+++ b/src/Umbraco.Web.UI.Client/src/installer/installer.service.js
@@ -31,7 +31,7 @@ angular.module("umbraco.install").factory('installerService', function ($rootSco
 					"At least 4 people have the Umbraco logo tattooed on them",
 					"'Umbraco' is the Danish name for an allen key",
 					"Umbraco has been around since 2005, that's a looong time in IT",
-					"More than 600 people from all over the world meet each year in Denmark in May for our annual conference CodeGarden", 
+					"More than 700 people from all over the world meet each year in Denmark in May for our annual conference CodeGarden", 
 					"While you are installing Umbraco someone else on the other side of the planet is probably doing it too",
 					"You can extend Umbraco without modifying the source code using either JavaScript or C#",
 					"Umbraco has been installed in more than 198 countries"
diff --git a/src/Umbraco.Web.UI.Client/src/less/belle.less b/src/Umbraco.Web.UI.Client/src/less/belle.less
index bd1cdd5b4f..b0b7e80762 100644
--- a/src/Umbraco.Web.UI.Client/src/less/belle.less
+++ b/src/Umbraco.Web.UI.Client/src/less/belle.less
@@ -112,7 +112,7 @@
 @import "components/umb-editor-navigation-item.less";
 @import "components/umb-editor-sub-views.less";
 @import "components/editor/subheader/umb-editor-sub-header.less";
-@import "components/umb-flatpickr.less";
+@import "components/umb-date-time-picker.less";
 @import "components/umb-grid-selector.less";
 @import "components/umb-child-selector.less";
 @import "components/umb-group-builder.less";
@@ -124,6 +124,7 @@
 @import "components/umb-form-check.less";
 @import "components/umb-locked-field.less";
 @import "components/umb-tabs.less";
+@import "components/umb-loader.less";
 @import "components/umb-load-indicator.less";
 @import "components/umb-breadcrumbs.less";
 @import "components/umb-media-grid.less";
diff --git a/src/Umbraco.Web.UI.Client/src/less/canvas-designer.less b/src/Umbraco.Web.UI.Client/src/less/canvas-designer.less
index 7440a5723a..4db2e434d2 100644
--- a/src/Umbraco.Web.UI.Client/src/less/canvas-designer.less
+++ b/src/Umbraco.Web.UI.Client/src/less/canvas-designer.less
@@ -163,7 +163,8 @@ a, a:hover{
     background-clip: padding-box;
 }
 
-.dropdown-menu > li > a {
+.dropdown-menu > li > a,
+.dropdown-menu > li > button {
     display: block;
     padding: 3px 20px;
     clear: both;
@@ -174,7 +175,12 @@ a, a:hover{
     cursor:pointer;
 }
 
-.dropdown-menu > li > a:hover, .dropdown-menu > li > a:focus, .dropdown-submenu:hover > a, .dropdown-submenu:focus > a {
+.dropdown-menu > li > a:hover, 
+.dropdown-menu > li > a:focus, 
+.dropdown-menu > li > button:hover, 
+.dropdown-menu > li > button:focus, 
+.dropdown-submenu:hover > a, 
+.dropdown-submenu:focus > a {
     color: #000000;
     background: #e4e0dd;
 }
diff --git a/src/Umbraco.Web.UI.Client/src/less/components/application/umb-drawer.less b/src/Umbraco.Web.UI.Client/src/less/components/application/umb-drawer.less
index 8c3b059a94..064ad67438 100644
--- a/src/Umbraco.Web.UI.Client/src/less/components/application/umb-drawer.less
+++ b/src/Umbraco.Web.UI.Client/src/less/components/application/umb-drawer.less
@@ -194,7 +194,6 @@
 .umb-help-list-item__title {
     font-size: 14px;
     display: block;
-    margin-left: 26px;
 }
 
 .umb-help-list-item__description {
diff --git a/src/Umbraco.Web.UI.Client/src/less/components/application/umb-tour.less b/src/Umbraco.Web.UI.Client/src/less/components/application/umb-tour.less
index 315cd91dbd..33a723a3f7 100644
--- a/src/Umbraco.Web.UI.Client/src/less/components/application/umb-tour.less
+++ b/src/Umbraco.Web.UI.Client/src/less/components/application/umb-tour.less
@@ -1,8 +1,12 @@
-.umb-tour__loader {
-    background: @white;
-    z-index: @zindexTourModal;
+.umb-loader-wrapper.umb-tour__loader {
+    margin: 0;
     position: fixed;
-    height: 5px;
+    z-index: @zindexTourModal;
+
+    .umb-loader {
+        background-color: @white;
+        height: 5px;
+    }
 }
 
 .umb-tour__pulse {
diff --git a/src/Umbraco.Web.UI.Client/src/less/components/buttons/umb-button-group.less b/src/Umbraco.Web.UI.Client/src/less/components/buttons/umb-button-group.less
index e40282cb58..0465881387 100644
--- a/src/Umbraco.Web.UI.Client/src/less/components/buttons/umb-button-group.less
+++ b/src/Umbraco.Web.UI.Client/src/less/components/buttons/umb-button-group.less
@@ -9,6 +9,7 @@
    left: auto;
 }
 
+.umb-button-group__sub-buttons>li>button,
 .umb-button-group__sub-buttons>li>a {
     display: flex;
 }
@@ -20,7 +21,7 @@
     }
 
     .umb-button-group__toggle {
-        border-radius: 0px @baseBorderRadius @baseBorderRadius 0;
+        border-radius: 0 @baseBorderRadius @baseBorderRadius 0;
         border-left: 1px solid rgba(0,0,0,0.09);
         margin-left: -2px;
         padding-left: 10px;
diff --git a/src/Umbraco.Web.UI.Client/src/less/components/editor.less b/src/Umbraco.Web.UI.Client/src/less/components/editor.less
index 52dd7ea678..85fcc249f9 100644
--- a/src/Umbraco.Web.UI.Client/src/less/components/editor.less
+++ b/src/Umbraco.Web.UI.Client/src/less/components/editor.less
@@ -74,8 +74,11 @@
 }
 
 .umb-editor-header__back {
-	color: @gray-6;
-    margin-bottom: 1px;
+    background: transparent;
+    border: 0;
+    color: @gray-6;
+    margin: 0 0 1px 0;
+    padding: 0;
 	transition: color 0.1s ease-in-out;
 }
 
@@ -373,14 +376,3 @@ a.umb-variant-switcher__toggle {
 	margin-right: auto;
 	padding-right: 10px;
 }
-
-/* Confirm */
-.umb-editor-confirm {
-	background-color: @white;
-	padding: 20px;
-	position: absolute;
-	left: 0;
-	bottom: 0;
-	z-index: 10;
-	box-shadow: 0 -3px 12px 0px rgba(0,0,0,0.16);
-}
diff --git a/src/Umbraco.Web.UI.Client/src/less/components/editor/subheader/umb-editor-sub-header.less b/src/Umbraco.Web.UI.Client/src/less/components/editor/subheader/umb-editor-sub-header.less
index 6cf3598638..649aa89055 100644
--- a/src/Umbraco.Web.UI.Client/src/less/components/editor/subheader/umb-editor-sub-header.less
+++ b/src/Umbraco.Web.UI.Client/src/less/components/editor/subheader/umb-editor-sub-header.less
@@ -1,14 +1,11 @@
 .umb-editor-sub-header {
    padding: 10px 0;
-   margin-bottom: 10px;
    background: @brownGrayLight;
    border-left: 5px solid @brownGrayLight;
    border-right: 5px solid @brownGrayLight;
-   margin-left: -5px;
-   margin-right: -5px;
    display: flex;
    justify-content: space-between;
-   margin-top: -10px;
+   margin: -10px -5px 10px;
    position: relative;
    top: 0;
    box-sizing: border-box;
@@ -34,30 +31,25 @@
 
 [umb-sticky-bar] {
     transition: box-shadow 240ms;
-    margin-top: 0; 
-    margin-bottom: 0;
     position:sticky;
     z-index: 99;
     
     &.umb-sticky-bar--active {
         box-shadow: 0 6px 3px -3px rgba(0,0,0,.16);
     }
+    
+    .umb-dashboard__content & {
+        top:-20px; // umb-dashboard__content has 20px padding - offset here prevents sticky position from firing when page loads
+    }
 }
 
 .umb-sticky-sentinel {
-    position: absolute;
-    left: 0;
-    width: 100%;
     pointer-events: none;
+    z-index: 5050;    
     
     &.-top {
-        top:0px;
         height:1px;
-    }
-    
-    &.-bottom {        
-        bottom:50px;
-        height:10px;
+        transform:translateY(-10px);
     }
 }
 
diff --git a/src/Umbraco.Web.UI.Client/src/less/components/editor/umb-editor.less b/src/Umbraco.Web.UI.Client/src/less/components/editor/umb-editor.less
index a9d879ab7f..3665f7d8fd 100644
--- a/src/Umbraco.Web.UI.Client/src/less/components/editor/umb-editor.less
+++ b/src/Umbraco.Web.UI.Client/src/less/components/editor/umb-editor.less
@@ -17,7 +17,7 @@
     z-index: @zIndexEditor;
 }
 
-.umb-editor--infinityMode {
+.umb-editor--infiniteMode {
     transform: none;
     will-change: transform;
     transition: transform 400ms ease-in-out;
diff --git a/src/Umbraco.Web.UI.Client/src/less/components/notifications/umb-notifications.less b/src/Umbraco.Web.UI.Client/src/less/components/notifications/umb-notifications.less
index 7f04fef9a9..5d78bed2a3 100644
--- a/src/Umbraco.Web.UI.Client/src/less/components/notifications/umb-notifications.less
+++ b/src/Umbraco.Web.UI.Client/src/less/components/notifications/umb-notifications.less
@@ -1,5 +1,5 @@
 .umb-notifications {
-    z-index: 1000;
+   z-index: 1100;
     position: absolute;
     bottom: @editorFooterHeight;
     left: 0;
diff --git a/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-actions.less b/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-actions.less
index d6e792de73..0c231830de 100644
--- a/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-actions.less
+++ b/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-actions.less
@@ -94,6 +94,7 @@
         font-size: 14px;
         color: @black;
         margin-left: 10px;
+        text-align: left;
     }
 
     small {
diff --git a/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-tree-item.less b/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-tree-item.less
index 8f0b55f9ed..8945d15ec6 100644
--- a/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-tree-item.less
+++ b/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-tree-item.less
@@ -39,17 +39,10 @@
     
     // Loading Animation
     // ------------------------
-    .l {
+    .umb-tree-item__loader {
         width: 100%;
-        height: 2px;
-        overflow: hidden;
         position: absolute;
-        left: 0;
-        bottom: -1px;
-
-        div { 
-            .umb-loader;
-        }
+        margin: 0;
     }
 
     .umb-tree-item__label {
@@ -57,19 +50,20 @@
         white-space: nowrap;
         overflow: hidden;
         text-overflow: ellipsis;
-        flex: 1 0 auto;
+        flex: 1 1 auto;
     }
 }
 
 // active is equivilant to selected, its the item that is begin affected by the actions performed in the right-click-dialog.
 .umb-tree-item.active > .umb-tree-item__inner {
+    border-color: @ui-selected-border;
+    box-shadow: 0 0 2px 0 fade(@ui-selected-border, 80%);
     color: @ui-selected-type;
+
     a {
         color: @ui-selected-type;
     }
-    
-    border-color: @ui-selected-border;
-    box-shadow: 0 0 2px 0 fade(@ui-selected-border, 80%);
+
     &::before {
         content: "";
         position: absolute;
@@ -79,8 +73,10 @@
         bottom: 0;
         border: 2px solid fade(white, 80%);
     }
+
     &:hover {
         color: @ui-selected-type-hover;
+
         a {
             color: @ui-selected-type-hover;
         }
@@ -88,7 +84,6 @@
 }
 
 .umb-tree-item.current > .umb-tree-item__inner {
-
     background: @ui-active;
     color:@ui-active-type;
     
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 5c54232200..93850656f0 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
@@ -33,12 +33,6 @@
             display: none;
         }
     }
-    
-    //loader defaults
-    .umb-loader {
-        height: 10px;
-        margin: 10px 10px 10px 10px;
-    }
 
     .search-subtitle {
         color: @gray-7;
@@ -72,8 +66,7 @@ body.touch .umb-tree {
     overflow: hidden;
     display: flex;
     flex-wrap: nowrap;
-    align-items: center;
-    
+    align-items: center;  
     border:2px solid transparent;
     
     color: @ui-option-type;
@@ -183,7 +176,6 @@ body.touch .umb-tree {
     &:hover {
         background: @btnBackgroundHighlight;
     }
-
     // NOTE - We're having to repeat ourselves here due to an .sr-only class appearing in umbraco/lib/font-awesome/css/font-awesome.min.css
     &.sr-only--hoverable:hover,
     &.sr-only--focusable:focus {
diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-box.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-box.less
index c0e91e28c2..1fe59ab9dd 100644
--- a/src/Umbraco.Web.UI.Client/src/less/components/umb-box.less
+++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-box.less
@@ -1,12 +1,14 @@
+@boxUnit: 10px;
+
 .umb-box {
     background: @white;
     border-radius: 3px;
-    margin-bottom: 20px;
+    margin-bottom: @boxUnit * 2;
     box-shadow: 0 1px 1px 0 rgba(0,0,0,.16);
 }
 
 .umb-box-header {
-    padding: 10px 20px;
+    padding: @boxUnit @boxUnit * 2;
     border-bottom: 1px solid @gray-9;
     display: flex;
     align-items: center;
@@ -27,5 +29,19 @@
 }
 
 .umb-box-content {
-    padding: 20px;
+    padding: @boxUnit * 2;
 }
+
+// allow side-by-side boxes
+.umb-box-row {
+    margin-left:-@boxUnit;
+    margin-right:-@boxUnit;
+    display:flex;
+    justify-content: space-around;
+    
+    .umb-box {
+        margin-left:@boxUnit;
+        margin-right:@boxUnit;
+        flex:1;
+    }
+}
\ No newline at end of file
diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-content-grid.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-content-grid.less
index 84cfe04263..f27e1e4ec8 100644
--- a/src/Umbraco.Web.UI.Client/src/less/components/umb-content-grid.less
+++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-content-grid.less
@@ -105,6 +105,7 @@
 .umb-content-grid__details-value {
    display: inline-block;
    word-break: break-word;
+   margin-left: 3px;
 }
 
 .umb-content-grid__checkmark {
diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-flatpickr.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-date-time-picker.less
similarity index 90%
rename from src/Umbraco.Web.UI.Client/src/less/components/umb-flatpickr.less
rename to src/Umbraco.Web.UI.Client/src/less/components/umb-date-time-picker.less
index 8cdcc8b877..2df0cc5fd8 100644
--- a/src/Umbraco.Web.UI.Client/src/less/components/umb-flatpickr.less
+++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-date-time-picker.less
@@ -1,4 +1,4 @@
-.flatpickr-calendar.flatpickr-calendar {
+.flatpickr-calendar {
     border-radius: @baseBorderRadius;
     box-shadow: 0 5px 10px 0 rgba(0,0,0,0.16);
 }
diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-editor-navigation-item.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-editor-navigation-item.less
index e6b3fdbfa9..8a8032ee16 100644
--- a/src/Umbraco.Web.UI.Client/src/less/components/umb-editor-navigation-item.less
+++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-editor-navigation-item.less
@@ -2,12 +2,16 @@
     position: relative;
     display: block;
 }
+
+.umb-sub-views-nav-item__action,
 .umb-sub-views-nav-item > a {
+  background: transparent;
   text-align: center;
   cursor: pointer;
   display: block;
   padding: 4px 10px 0 10px;
   min-width: 70px;
+  border: 0 none;
   border-right: 1px solid @gray-9;
   box-sizing: border-box;
   display: flex;
@@ -19,6 +23,7 @@
   
   color: @ui-active-type;
   
+  &:focus,
   &:hover {
       color: @ui-active-type-hover !important;
   }
@@ -37,18 +42,23 @@
   }
 }
 
+.umb-sub-views-nav-item__action:focus,
+.umb-sub-views-nav-item__action:active,
 .umb-sub-views-nav-item > a:active {
   .box-shadow(~"inset 0 2px 4px rgba(0,0,0,.15), 0 1px 2px rgba(0,0,0,.05)");
 }
+.umb-sub-views-nav-item__action:focus,
 .umb-sub-views-nav-item > a:focus {
   outline: none;
 }
 
+.umb-sub-views-nav-item__action:hover,
+.umb-sub-views-nav-item__action:focus,
 .umb-sub-views-nav-item > a:hover,
 .umb-sub-views-nav-item > a:focus {
   text-decoration: none;
 }
-
+.umb-sub-views-nav-item__action.is-active,
 .umb-sub-views-nav-item > a.is-active {
     
     color: @ui-light-active-type;
@@ -59,6 +69,7 @@
     }
 }
 
+.umb-sub-views-nav-item__action.-has-error,
 .show-validation .umb-sub-views-nav-item > a.-has-error {
   color: @red;
   &::after {
@@ -114,11 +125,19 @@
     // center align horizontal
     left: 50%;
     transform: translateX(-50%);
-    
-    visibility:hidden;
     opacity: 0;
-    transition: visibility 0s 500ms, opacity 250ms 250ms;
+    transition: opacity 250ms 250ms;
 }
+
+// Currently Edge 18 does unfortunately not support :focus-within so for now we will use the "old" behavior - Support is coming with the upcoming release of Edge 76
+// See https://caniuse.com/#search=focus-within
+@supports (-ms-ime-align:auto) { 
+    .umb-sub-views-nav-item__anchor_dropdown {
+        visibility: hidden;
+        transition: visibility 0 500ms, opacity 250ms 250ms;
+    }
+}
+
 .umb-sub-views-nav-item__anchor_dropdown li a {
     border-left: 4px solid transparent;
 }
@@ -126,12 +145,20 @@
     border-left-color: @ui-selected-border;
 }
 
+.umb-sub-views-nav-item:focus-within .umb-sub-views-nav-item__anchor_dropdown,
 .umb-sub-views-nav-item:hover .umb-sub-views-nav-item__anchor_dropdown {
     visibility:visible;
     opacity: 1;
-    transition: visibility 0s 0s, opacity 20ms 0s;
+    transition: opacity 20ms 0;
 }
 
+// Currently Edge 18 does unfortunately not support :focus-within so for now we will use the "old" behavior - Support is coming with the upcoming release of Edge 76
+// See https://caniuse.com/#search=focus-within
+@supports (-ms-ime-align:auto) { 
+    .umb-sub-views-nav-item:hover .umb-sub-views-nav-item__anchor_dropdown {
+        transition: visibility 0 0, opacity 20ms 0;
+    }
+}
 
 
 // --------------------------------
diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-file-dropzone.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-file-dropzone.less
index 5071091fcc..b5d8c3cced 100644
--- a/src/Umbraco.Web.UI.Client/src/less/components/umb-file-dropzone.less
+++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-file-dropzone.less
@@ -14,15 +14,18 @@
     margin: 0 0 20px 0;
     position: relative;
     transition: height 0.8s;
+
     .illustration {
       width: 300px;
     }
+
     &.is-small {
       height: 100px;
       .illustration {
         width: 200px;
       }
     }
+    
     &.drag-over {
       border: 1px dashed @gray-1;
     }
@@ -35,15 +38,19 @@
     top: 50%;
     left: 50%;
     transform: translate(-50%,-50%);
+    display: flex;
+    flex-direction: column;
   }
 
 
   // file select link
   .file-select {
+    background: transparent;
+    border: 0;
+    padding: 0;
     font-size: 15px;
     color: @ui-action-discreet-type;
     cursor: pointer;
-
     margin-top: 10px;
 
     &:hover {
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 2eafe9b3d7..ba0b32bb0d 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
@@ -408,7 +408,19 @@
     margin-bottom: 10px;
 }
 
+.umb-grid .umb-editor-preview {
+    position: relative;
 
+    .umb-editor-preview-overlay {
+        cursor: pointer;
+        position: absolute;
+        top: 0;
+        left: 0;
+        right: 0;
+        bottom: 0;
+        z-index: 1;
+    }
+}
 
 // Active states
 // -------------------------
diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-layout-selector.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-layout-selector.less
index cf407b667f..cdc6cfcb63 100644
--- a/src/Umbraco.Web.UI.Client/src/less/components/umb-layout-selector.less
+++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-layout-selector.less
@@ -4,6 +4,7 @@
 }
 
 .umb-layout-selector__active-layout {
+   background: transparent;
    box-sizing: border-box;
    border: 1px solid @inputBorder;
    cursor: pointer;
@@ -33,6 +34,7 @@
 }
 
 .umb-layout-selector__dropdown-item {
+   background: transparent;
    padding: 5px;
    margin: 3px 5px;
    display: flex;
diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-loader.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-loader.less
new file mode 100644
index 0000000000..260710ce72
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-loader.less
@@ -0,0 +1,42 @@
+// Loading Animation
+// ------------------------
+
+.umb-loader {
+    background-color: @blue;
+    margin-top: 0;
+    margin-left: -100%;
+    animation-name: bounce_loadingProgressG;
+    animation-duration: 1s;
+    animation-iteration-count: infinite;
+    animation-timing-function: linear;
+    width: 100%;
+    height: 2px;
+}
+
+@keyframes bounce_loadingProgressG {
+    0% {
+        margin-left: -100%;
+    }
+
+    100% {
+        margin-left: 100%;
+    }
+}
+
+.umb-loader-wrapper {
+    position: absolute;
+    right: 0;
+    left: 0;
+    margin: 10px 0;
+    overflow: hidden;
+}
+
+.umb-loader-wrapper.-top {
+    top: 0;
+    bottom: auto;
+}
+
+.umb-loader-wrapper.-bottom {
+    top: auto;
+    bottom: 0;
+}
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 68973b4c7c..8c8e4b101c 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
@@ -21,15 +21,9 @@
 
     margin: 10px;
     position: relative;
-    //overflow: hidden;
-    
-    user-select: none;
-    
+    user-select: none; 
     cursor: pointer;
-    
     box-shadow: 0 1px 1px 0 rgba(0,0,0,.2);
-    //border: 2px solid transparent;
-    
     transition: box-shadow 150ms ease-in-out;
 }
 
@@ -69,16 +63,34 @@
     }
 }
 
-.umb-media-grid__item-file-icon > span {
-    color: @white;
-    background: @gray-4;
-    padding: 1px 3px;
-    font-size: 10px;
-    line-height: 130%;
-    display: block;
-    margin-top: -30px;
-    margin-left: -10px;
-    position: relative;
+.umb-media-grid__item-file-icon {
+    display: flex;
+    flex-direction: column;
+    align-items: flex-start;
+    transform: translate(-50%,-50%);
+    position: absolute;
+    top: 45%;
+    left: 50%;
+
+    .umb-media-grid__item-icon {
+        display: flex;
+        flex-direction: column;
+        align-items: flex-start;
+        color: @gray-4;
+        font-size: 50px !important;
+    }
+
+    > span {
+        color: @white;
+        background: @ui-active;
+        padding: 1px 3px;
+        font-size: 10px;
+        line-height: 130%;
+        display: block;
+        margin-top: -15px;
+        margin-left: 5px;
+        position: relative;
+    }
 }
 
 .umb-media-grid__item:hover {
@@ -86,34 +98,27 @@
 }
 
 .umb-media-grid__item-image {
-   //max-width: 100% !important;
-   //height: auto;
    position: relative;
-   
    object-fit: contain;
    height: 100%;
 }
 
 .umb-media-grid__item-image-placeholder {
     width: 100%;
-    //max-width: 100%;
-    //height: auto;
     position: relative;
-    
     object-fit: contain;
     height: 100%;
 }
 
 .umb-media-grid__image-background {
     content: "";
-    background: url("../img/checkered-background.png");
-    background-repeat: repeat;
     opacity: 0.5;
     top: 0;
     left: 0;
     bottom: 0;
     right: 0;
     position: absolute;
+    .checkeredBackground();
 }
 
 .umb-media-grid__item-overlay {
@@ -139,19 +144,6 @@
    }
 }
 
-/*
-.umb-media-grid__item.-file .umb-media-grid__item-overlay {
-    opacity: 1;
-    color: @gray-4;
-    background: @white;
-}
-
-.umb-media-grid__item.-file:hover .umb-media-grid__item-overlay,
-.umb-media-grid__item.-file.-selected .umb-media-grid__item-overlay {
-    color: @white;
-    background: @blueExtraDark;
-}
-*/
 .umb-media-grid__info {
     margin-right: 5px;
 }
@@ -173,33 +165,7 @@
    text-overflow: ellipsis;
 }
 
-.umb-media-grid__item-icon {
-   color: @gray-4;
-   position: absolute;
-   top: 45%;
-   left: 50%;
-   font-size: 40px !important;
-   transform: translate(-50%,-50%);
-}
-/*
-.umb-media-grid__checkmark {
-    position: absolute;
-    z-index: 2;
-    top: 10px;
-    left: 10px;
-    width: 26px;
-    height: 26px;
-    border: 2px solid @white;
-    background: @green;
-    border-radius: 50px;
-    box-sizing: border-box;
-    display: flex;
-    justify-content: center;
-    align-items: center;
-    color: @white;
-    transition: background 100ms;
-}
-*/
+
 .umb-media-grid__edit {
     position: absolute;
     opacity: 0;
diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-packages.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-packages.less
index f704dd48e2..16457787a3 100644
--- a/src/Umbraco.Web.UI.Client/src/less/components/umb-packages.less
+++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-packages.less
@@ -264,12 +264,16 @@
     flex-flow: row wrap;
 }
 
-a.umb-package-details__back-link {
+.umb-package-details__back-action {
     font-weight: bold;
     color: @black;
+    padding: 0;
+    border: 0;
+    background-color: transparent;
 }
 
-.umb-package-details__back-link:hover {
+.umb-package-details__back-action:focus,
+.umb-package-details__back-action:hover {
     color: @gray-4;
     text-decoration: none;
 }
diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-table.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-table.less
index f387b6540b..27b64f85fb 100644
--- a/src/Umbraco.Web.UI.Client/src/less/components/umb-table.less
+++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-table.less
@@ -295,7 +295,11 @@ input.umb-table__input {
 .umb-table__row-expand {
     font-size: 12px;
     text-decoration: none;
-    color: @black;
+    color: @gray-4;
+
+    &:hover {
+        color: @black;
+    }
 }
 
 .umb-table__row-expand--hidden {
diff --git a/src/Umbraco.Web.UI.Client/src/less/dashboards/healthcheck.less b/src/Umbraco.Web.UI.Client/src/less/dashboards/healthcheck.less
index 449cc4066b..b3af476406 100644
--- a/src/Umbraco.Web.UI.Client/src/less/dashboards/healthcheck.less
+++ b/src/Umbraco.Web.UI.Client/src/less/dashboards/healthcheck.less
@@ -22,15 +22,18 @@
 .umb-healthcheck-group {
 	display: flex;
 	flex-wrap: wrap;
-	flex-direction: column;
-	background: @white;
+    flex-direction: column;
+    align-items: center;
+    background: @white;
+    border: 0;
 	border-radius: 3px;
 	padding: 20px;
 	box-sizing: border-box;
 	text-align: center;
 	box-shadow: 0 1px 1px 0 rgba(0,0,0,0.16);
 	height: 100%;
-	box-sizing: border-box;
+    box-sizing: border-box;
+    width: 100%;
 }
 
 .umb-healthcheck-group:hover {
diff --git a/src/Umbraco.Web.UI.Client/src/less/installer.less b/src/Umbraco.Web.UI.Client/src/less/installer.less
index 798252c394..8090b971fc 100644
--- a/src/Umbraco.Web.UI.Client/src/less/installer.less
+++ b/src/Umbraco.Web.UI.Client/src/less/installer.less
@@ -13,6 +13,9 @@
 @import "../../lib/bootstrap/less/thumbnails.less";
 @import "../../lib/bootstrap/less/media.less";
 
+// Umbraco Components
+@import "components/umb-loader.less";
+
 
 [ng\:cloak], [ng-cloak], [data-ng-cloak], [x-ng-cloak], .ng-cloak, .x-ng-cloak {
   display: none !important;
@@ -165,45 +168,17 @@ input.ng-dirty.ng-invalid{border-color: #b94a48; color: #b94a48;}
     opacity:1;
 }
 
+.umb-installer-loader {
+    margin: 0;
+    width: 0;
+    z-index: 777;
 
-.umb-loader{
-    background-color: @white;
-    margin-top:0;
-    margin-left:-100%;
-    animation-name:bounce_loadingProgressG;
-    animation-duration:1s;
-    animation-iteration-count:infinite;
-    animation-timing-function:linear;
-    width:100%;
-    height: 5px;
-}
-
-@keyframes bounce_loadingProgressG{
-    0%{
-        margin-left:-100%;
-    }
-    100%{
-        margin-left:100%;
+    .umb-loader {
+        background-color: @white;
+        height: 5px;
     }
 }
 
-//loader defaults
-.umb-loader-container, .umb-loader-done{
-	height: 3px;
-  position: absolute;
-  bottom: 0;
-  left: 0;
-  overflow: hidden;
-  width: 0%;
-  z-index: 777;
-}
-
-.umb-loader-done{
-   right: 0%;
-   background: @white;
-}
-
-
 .permissions-report {
     overflow:auto;
     height:320px;
diff --git a/src/Umbraco.Web.UI.Client/src/less/main.less b/src/Umbraco.Web.UI.Client/src/less/main.less
index 9f9bbce310..920fcdb1eb 100644
--- a/src/Umbraco.Web.UI.Client/src/less/main.less
+++ b/src/Umbraco.Web.UI.Client/src/less/main.less
@@ -486,42 +486,6 @@ table thead a {
     color:@green;
 }
 
-// Loading Animation
-// ------------------------
-
-.umb-loader{
-    background-color: @blue;
-    margin-top:0;
-    margin-left:-100%;
-    animation-name:bounce_loadingProgressG;
-    animation-duration:1s;
-    animation-iteration-count:infinite;
-    animation-timing-function:linear;
-    width:100%;
-    height:2px;
-}
-
-@keyframes bounce_loadingProgressG{
-    0%{
-        margin-left:-100%;
-    }
-    100%{
-        margin-left:100%;
-    }
-}
-
-.umb-loader-wrapper {
-    position: absolute;
-    right: 0;
-    left: 0;
-    margin: 10px 0;
-    overflow: hidden;
-}
-
-.umb-loader-wrapper.-bottom {
-	bottom: 0;
-}
-
 // Helpers
 
 .strong {
@@ -592,13 +556,3 @@ input[type=checkbox]:checked + .input-label--small {
   background-color: @green-l3;
   text-decoration: none;
 }
-
-.visuallyhidden{
-    position: absolute !important;
-	clip: rect(1px, 1px, 1px, 1px);
-	padding:0 !important;
-	border:0 !important;
-	height: 1px !important;
-	width: 1px !important;
-	overflow: hidden;
-}
diff --git a/src/Umbraco.Web.UI.Client/src/less/mixins.less b/src/Umbraco.Web.UI.Client/src/less/mixins.less
index ce35097658..60132edab5 100644
--- a/src/Umbraco.Web.UI.Client/src/less/mixins.less
+++ b/src/Umbraco.Web.UI.Client/src/less/mixins.less
@@ -397,11 +397,19 @@
   }
 }
 
-
+.checkeredBackground(@backgroundColor: #eee, @fillColor: #000, @fillOpacity: 0.25) {
+    background-image: url('data:image/svg+xml;charset=utf-8,\
+                        \
+                            \
+                            \
+                        ');
+    background-color: @backgroundColor;
+    background-size: 10px 10px;
+    background-repeat: repeat;
+}
 
 // COMPONENT MIXINS
 // --------------------------------------------------
-
 // Limit width of specific property editors
 .umb-property-editor--limit-width {
     max-width: 800px;
diff --git a/src/Umbraco.Web.UI.Client/src/less/navs.less b/src/Umbraco.Web.UI.Client/src/less/navs.less
index a2710fab6c..5b97464e31 100644
--- a/src/Umbraco.Web.UI.Client/src/less/navs.less
+++ b/src/Umbraco.Web.UI.Client/src/less/navs.less
@@ -237,7 +237,27 @@
     color: @ui-option-type;
 }
 
-.dropdown-menu > li > a:hover, .dropdown-menu > li > a:focus, .dropdown-submenu:hover > a, .dropdown-submenu:focus > a {
+.dropdown-menu > li > button {
+  background: transparent;
+  border: 0;
+  padding: 8px 20px;
+  color: @ui-option-type;
+  display: block;
+  clear: both;
+  font-weight: normal;
+  line-height: 20px;
+  white-space: nowrap;
+  cursor:pointer;
+  width: 100%;
+  text-align: left;
+}
+
+.dropdown-menu > li > a:hover, 
+.dropdown-menu > li > a:focus, 
+.dropdown-menu > li > button:hover, 
+.dropdown-menu > li > button:focus, 
+.dropdown-submenu:hover > a, 
+.dropdown-submenu:focus > a {
     color: @ui-option-type-hover;
     background: @ui-option-hover;
 }
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 1c91be2c8f..3328d9cf02 100644
--- a/src/Umbraco.Web.UI.Client/src/less/property-editors.less
+++ b/src/Umbraco.Web.UI.Client/src/less/property-editors.less
@@ -1,11 +1,8 @@
-@checkered-background: url(../img/checkered-background.png);
-
 //
 // Container styles
 // --------------------------------------------------
-.umb-property-editor {
+.umb-property-editor { 
     width: 100%;
-    position:relative;
 }
 
 .umb-property-editor-tiny {
@@ -366,7 +363,6 @@
     padding: 5px;
     margin: 5px;
     background: @white;
-    //border: 1px solid @gray-10;
     max-width: 100%;
 }
 .umb-mediapicker {
@@ -377,14 +373,9 @@
     
 }
 
-
-
 .umb-mediapicker .umb-sortable-thumbnails li {
     flex-direction: column;
 }
-/*.umb-mediapicker .umb-sortable-thumbnails li.add-wrapper {
-    padding: 0px;
-}*/
 
 .umb-sortable-thumbnails li:hover a {
     display: flex;
@@ -393,11 +384,11 @@
 }
 
 .umb-sortable-thumbnails li img {
-    max-width:100%;
-    max-height:100%;
-    margin:auto;
-    display:block;
-    background-image: @checkered-background;
+    max-width: 100%;
+    max-height: 100%;
+    margin: auto;
+    display: block;
+    .checkeredBackground();
 }
 
 .umb-sortable-thumbnails li .trashed {
@@ -411,31 +402,40 @@
 
 .umb-sortable-thumbnails .umb-icon-holder {
     text-align: center;
-}
+    display: flex;
+    flex-direction: column;
+    align-items: center;
 
-.umb-sortable-thumbnails .umb-icon-holder .icon {
-    font-size: 40px;
-    line-height: 50px;
-    color: @gray-3;
-    display: block;
-}
+    .file-icon {
+        display: flex;
+        flex-direction: column;
+        align-items: flex-start;
 
-.umb-sortable-thumbnails .umb-icon-holder .file-icon > span {
-    color: @white;
-    background: @gray-4;
-    padding: 1px 3px;
-    font-size: 10px;
-    line-height: 130%;
-    display: block;
-    margin-top: -30px;
-    width: 2em;
-}
+        > .icon {
+            font-size: 50px;
+            line-height: 50px;
+            color: @gray-4;
+            display: block;
+            text-align: center;
+        }
 
-.umb-sortable-thumbnails .umb-icon-holder .file-icon + small {
-    display: block;
-    margin-top: 1em;
-}
+        > span {
+            color: @white;
+            background: @ui-active;
+            padding: 1px 3px;
+            font-size: 10px;
+            line-height: 130%;
+            display: block;
+            margin-top: -25px;
+            min-width: 2em;
+        }
 
+        & + small {
+            display: block;
+            margin-top: 1em;
+        }
+    }
+}
 
 .umb-sortable-thumbnails .umb-sortable-thumbnails__wrapper {
     width: 124px;
@@ -487,8 +487,6 @@
     align-items: center;
     margin-left: 5px;
     text-decoration: none;
-    
-    //border-color: @inputBorder;
     .box-shadow(0 1px 2px rgba(0,0,0,0.25));
 }
 
@@ -628,7 +626,13 @@
 
         .viewport {
             max-width: 600px;
-            background: @checkered-background;
+            .checkeredBackground();
+
+            img {
+                display: block;
+                margin-left: auto;
+                margin-right: auto;
+            }
 
             &:hover {
                 cursor: pointer;
@@ -648,7 +652,7 @@
         }
 
         .viewport img {
-            background: @checkered-background;
+            .checkeredBackground();
         }
     }
 
@@ -767,13 +771,13 @@
 }
 
 .umb-fileupload ul {
-  list-style: none;
-  vertical-align: middle;
-  margin-bottom: 0;
+    list-style: none;
+    vertical-align: middle;
+    margin-bottom: 0;
 
-  img {
-    background: @checkered-background;
-  }
+    img {
+        .checkeredBackground();
+    }
 }
 
 .umb-fileupload label {
@@ -792,28 +796,38 @@
   padding-top: 27px;
 }
 
-.umb-fileupload .file-icon {
-  display: inline-block;
-  position: relative;
-  padding: 5px 0;
-
-  > .icon {
-    font-size: 70px;
-    line-height: 110%;
-    color: @gray-4;
+.umb-fileupload .umb-icon-holder {
     text-align: center;
-  }
+    display: flex;
+    flex-direction: column;
+    align-items: center;
 
-  > span {
-    color: @white;
-    background: @gray-4;
-    padding: 1px 3px;
-    font-size: 12px;
-    line-height: 130%;
-    position: absolute;
-    top: 45px;
-    left: 10px;
-  }
+    .file-icon {
+        display: flex;
+        flex-direction: column;
+        align-items: flex-start;
+        position: relative;
+        padding: 15px 0;
+
+        > .icon {
+            display: block;
+            font-size: 70px;
+            line-height: 110%;
+            color: @gray-4;
+            text-align: center;
+        }
+
+        > span {
+            color: @white;
+            background: @ui-active;
+            padding: 1px 3px;
+            font-size: 12px;
+            line-height: 130%;
+            display: block;
+            margin-top: -35px;
+            min-width: 2em;
+        }
+    }
 }
 
 .umb-fileupload input {
diff --git a/src/Umbraco.Web.UI.Client/src/less/sections.less b/src/Umbraco.Web.UI.Client/src/less/sections.less
index ef6c5f5046..5551ba6376 100644
--- a/src/Umbraco.Web.UI.Client/src/less/sections.less
+++ b/src/Umbraco.Web.UI.Client/src/less/sections.less
@@ -2,129 +2,135 @@
 // -------------------------
 
 ul.sections {
-	margin: 0;
-	display: flex;
-	margin-left: -20px;
-}
-
-ul.sections>li {
-	display: flex;
-	justify-content: center;
-	align-items: center;
-	position: relative;
-}
-
-ul.sections>li>a {
-	color: @white;
-	height: @appHeaderHeight;
+    margin: 0;
     display: flex;
-	align-items: center;
-	justify-content: center;
-	position: relative;
-	padding: 0 10px;
-	text-decoration: none;
-	outline: none;
-	cursor: pointer;
-}
+    margin-left: -20px;
 
-ul.sections>li>a .section__name {
-    border-radius: 3px;
-    margin-top:1px;
-    padding: 3px 10px 4px 10px;
-	opacity: 0.8;
-	transition: opacity .1s linear, box-shadow .1s;
-}
+    > li {
+        display: flex;
+        justify-content: center;
+        align-items: center;
+        position: relative;
 
-ul.sections>li>a::after {
-	content: "";
-    left: 10px;
-    right: 10px;
-	height: 4px;
-	bottom: 0;
-    transform: translateY(4px);
-	background-color: @pinkLight;
-    position: absolute;
-	border-radius: 3px 3px 0 0;
-	opacity: 0;
-	padding: 0 2px;
-	transition: transform 240ms ease-in-out;
-}
+        > a {
+            color: @white;
+            height: @appHeaderHeight;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            position: relative;
+            padding: 0 10px;
+            text-decoration: none;
+            outline: none;
+            cursor: pointer;
 
-ul.sections>li.current>a {
-    color:@pinkLight;
-}
-ul.sections>li.current>a::after {
-	opacity: 1;
-    transform: translateY(0px);
-}
-ul.sections > li.current > a .section__name,
-ul.sections > li > a:hover .section__name {
-    opacity: 1;
-    -webkit-font-smoothing: subpixel-antialiased;
-}
+            &::after {
+                content: "";
+                left: 10px;
+                right: 10px;
+                height: 4px;
+                bottom: 0;
+                transform: translateY(4px);
+                background-color: @ui-active;
+                position: absolute;
+                border-radius: 3px 3px 0 0;
+                opacity: 0;
+                padding: 0 2px;
+                transition: transform 240ms ease-in-out;
+            }
 
-ul.sections > li > a:focus .section__name {
-    .tabbing-active & {
-        
-        border: 1px solid;
-        border-color: @gray-9;
+            &:focus .section__name {
+                .tabbing-active & {
+                    border: 1px solid;
+                    border-color: @gray-9;
+                }
+            }
+        }
+
+        .section__name {
+            border-radius: 3px;
+            margin-top: 1px;
+            padding: 3px 10px 4px 10px;
+            opacity: 0.8;
+            transition: opacity .1s linear, box-shadow .1s;
+        }
+
+        &.current a {
+            color: @ui-active;
+
+            &::after {
+                opacity: 1;
+                transform: translateY(0px);
+            }
+        }
+
+        &.expand {
+            i {
+                height: 5px;
+                width: 5px;
+                border-radius: 50%;
+                background: @white;
+                display: inline-block;
+                margin: 0 5px 0 0;
+                opacity: 0.6;
+                transition: opacity .1s linear;
+            }
+            
+            &:hover i {
+                opacity:1;
+            }
+        }
+
+        &.current .section__name,
+        a:hover .section__name {
+            opacity: 1;
+            -webkit-font-smoothing: subpixel-antialiased;
+        }
     }
 }
 
-
-
 /* Sections tray */
 
-ul.sections>li.expand i {
-	height: 5px;
-    width: 5px;
-    border-radius: 50%;
-    background: #fff;
-    display: inline-block;
-    margin: 0 5px 0 0;
-    opacity: 0.6;
-}
-
 ul.sections-tray {
-	position: absolute;
-	top: @appHeaderHeight;
-	left: 0;
-	margin: 0;
+    position: absolute;
+    top: @appHeaderHeight;
+    left: 0;
+    margin: 0;
     list-style: none;
-	background: @purple;
-	z-index: 10000;
-	border-radius: 0 0 3px 3px;
-}
+    background: @blueExtraDark;
+    z-index: 10000;
+    border-radius: 0 0 3px 3px;
 
-ul.sections-tray>li>a {
-	padding: 8px 24px;
-	color: @white;
-	text-decoration: none;
-	display: block;
-	position: relative;
-}
+    li {
 
-ul.sections-tray>li>a::after {
-	content: "";
-	width: 4px;
-	height: 100%;
-	background-color: @ui-active;
-	position: absolute;
-	border-radius: 0 3px 3px 0;
-	opacity: 0;
-	transition: all .2s linear;
-	top: 0;
-	left: 0;
-}
+        &.current a {
+            color: @ui-active;
+            opacity: 1;
 
-ul.sections-tray>li.current>a::after {
-	opacity: 1;
-}
+            &::after {
+                opacity: 1;
+            }
+        }
 
-ul.sections-tray>li>a .section__name {
-	opacity: 0.6;
-}
+        a {
+            padding: 8px 24px;
+            color: @white;
+            text-decoration: none;
+            display: block;
+            position: relative;
 
-ul.sections-tray>li>a:hover	.section__name {
-	opacity: 1;
+            &::after {
+                content: "";
+                width: 4px;
+                height: 100%;
+                background-color: @ui-active;
+                position: absolute;
+                border-radius: 0 3px 3px 0;
+                opacity: 0;
+                transition: all .2s linear;
+                top: 0;
+                left: 0;
+            }
+        }
+    }
 }
diff --git a/src/Umbraco.Web.UI.Client/src/less/utilities/_flexbox.less b/src/Umbraco.Web.UI.Client/src/less/utilities/_flexbox.less
index c0815fa8ac..a3427074cd 100644
--- a/src/Umbraco.Web.UI.Client/src/less/utilities/_flexbox.less
+++ b/src/Umbraco.Web.UI.Client/src/less/utilities/_flexbox.less
@@ -1,7 +1,5 @@
 /*
-
     Flexbox
-
 */
 
 .flex { display: flex; }
diff --git a/src/Umbraco.Web.UI.Client/src/less/variables.less b/src/Umbraco.Web.UI.Client/src/less/variables.less
index a1dc0ba187..166640829b 100644
--- a/src/Umbraco.Web.UI.Client/src/less/variables.less
+++ b/src/Umbraco.Web.UI.Client/src/less/variables.less
@@ -517,7 +517,7 @@
 @heroUnitLeadColor:               inherit;
 
 
-// alerts
+// Alerts
 // -------------------------
 @warningText:             @white;
 @warningBackground:       @yellow-d2;
diff --git a/src/Umbraco.Web.UI.Client/src/controllers/main.controller.js b/src/Umbraco.Web.UI.Client/src/main.controller.js
similarity index 96%
rename from src/Umbraco.Web.UI.Client/src/controllers/main.controller.js
rename to src/Umbraco.Web.UI.Client/src/main.controller.js
index 654bbb1d03..93870f8a56 100644
--- a/src/Umbraco.Web.UI.Client/src/controllers/main.controller.js
+++ b/src/Umbraco.Web.UI.Client/src/main.controller.js
@@ -10,7 +10,7 @@
  */
 function MainController($scope, $location, appState, treeService, notificationsService, 
     userService, historyService, updateChecker, navigationService, eventsService, 
-    tmhDynamicLocale, localStorageService, editorService, overlayService) {
+    tmhDynamicLocale, localStorageService, editorService, overlayService, assetsService, tinyMceAssets) {
 
     //the null is important because we do an explicit bool check on this in the view
     $scope.authenticated = null;
@@ -21,7 +21,13 @@ function MainController($scope, $location, appState, treeService, notificationsS
     $scope.search = {};
     $scope.login = {};
     $scope.tabbingActive = false;
-    
+
+    // Load TinyMCE assets ahead of time in the background for the user
+    // To help with first load of the RTE
+    tinyMceAssets.forEach(function (tinyJsAsset) {
+        assetsService.loadJs(tinyJsAsset, $scope);
+    });
+
     // There are a number of ways to detect when a focus state should be shown when using the tab key and this seems to be the simplest solution. 
     // For more information about this approach, see https://hackernoon.com/removing-that-ugly-focus-ring-and-keeping-it-too-6c8727fefcd2
     function handleFirstTab(evt) {
diff --git a/src/Umbraco.Web.UI.Client/src/controllers/navigation.controller.js b/src/Umbraco.Web.UI.Client/src/navigation.controller.js
similarity index 100%
rename from src/Umbraco.Web.UI.Client/src/controllers/navigation.controller.js
rename to src/Umbraco.Web.UI.Client/src/navigation.controller.js
diff --git a/src/Umbraco.Web.UI.Client/src/views/common/drawers/help/help.html b/src/Umbraco.Web.UI.Client/src/views/common/drawers/help/help.html
index 56db1fd88a..a0413ce1a6 100644
--- a/src/Umbraco.Web.UI.Client/src/views/common/drawers/help/help.html
+++ b/src/Umbraco.Web.UI.Client/src/views/common/drawers/help/help.html
@@ -38,8 +38,8 @@
                                         {{ tour.name }}
                                     
                                     
- - + +
diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/compositions/compositions.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/compositions/compositions.controller.js index 75bf414099..c9d2e43a98 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/compositions/compositions.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/compositions/compositions.controller.js @@ -1,7 +1,7 @@ (function () { "use strict"; - function CompositionsController($scope, $location, $filter) { + function CompositionsController($scope, $location, $filter, overlayService) { var vm = this; var oldModel = null; @@ -56,19 +56,35 @@ if ($scope.model && $scope.model.submit) { // check if any compositions has been removed - vm.compositionRemoved = false; + var compositionRemoved = false; for (var i = 0; oldModel.compositeContentTypes.length > i; i++) { var oldComposition = oldModel.compositeContentTypes[i]; if (_.contains($scope.model.compositeContentTypes, oldComposition) === false) { - vm.compositionRemoved = true; + compositionRemoved = true; } } /* submit the form if there havne't been removed any composition or the confirm checkbox has been checked */ - if (!vm.compositionRemoved || vm.allowSubmit) { - $scope.model.submit($scope.model); + if (compositionRemoved) { + vm.allowSubmit = false; + const dialog = { + view: "views/common/infiniteeditors/compositions/overlays/confirmremove.html", + submitButtonLabelKey: "general_ok", + closeButtonLabelKey: "general_cancel", + submit: function (model) { + $scope.model.submit($scope.model); + overlayService.close(); + }, + close: function () { + overlayService.close(); + } + }; + overlayService.open(dialog); + return; } + + $scope.model.submit($scope.model); } } diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/compositions/compositions.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/compositions/compositions.html index bf74431d96..bb0804e2bf 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/compositions/compositions.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/compositions/compositions.html @@ -92,16 +92,6 @@ - -
-
Warning
-

Removing a composition will delete all the associated property data. Once you save the document type there's no way back, are you sure?

- -
- diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/compositions/overlays/confirmremove.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/compositions/overlays/confirmremove.html new file mode 100644 index 0000000000..dd5e244ba5 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/compositions/overlays/confirmremove.html @@ -0,0 +1,6 @@ +
+ +
Warning
+

Removing a composition will delete all the associated property data. Once you save the document type there's no way back, are you sure?

+ +
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 06a5f028ef..f34dc949ab 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 @@ -7,6 +7,7 @@ var origWidth = 500; var origHeight = 300; + vm.loading = false; vm.trustedPreview = null; $scope.model.embed = { @@ -17,9 +18,16 @@ preview: "", success: false, info: "", - supportsDimensions: "" + supportsDimensions: false }; + if ($scope.model.original) { + angular.extend($scope.model.embed, $scope.model.original); + + showPreview(); + } + + vm.toggleConstrain = toggleConstrain; vm.showPreview = showPreview; vm.changeSize = changeSize; vm.submit = submit; @@ -37,10 +45,10 @@ if ($scope.model.embed.url) { $scope.model.embed.show = true; - $scope.model.embed.preview = "
"; $scope.model.embed.info = ""; $scope.model.embed.success = false; + vm.loading = true; $http({ method: 'GET', @@ -54,29 +62,41 @@ $scope.model.embed.preview = ""; - switch (response.data.OEmbedStatus) { case 0: //not supported + $scope.model.embed.preview = ""; $scope.model.embed.info = "Not supported"; + $scope.model.embed.success = false; + $scope.model.embed.supportsDimensions = false; + vm.trustedPreview = null; break; case 1: //error + $scope.model.embed.preview = ""; $scope.model.embed.info = "Could not embed media - please ensure the URL is valid"; + $scope.model.embed.success = false; + $scope.model.embed.supportsDimensions = false; + vm.trustedPreview = null; break; case 2: + $scope.model.embed.success = true; + $scope.model.embed.supportsDimensions = response.data.SupportsDimensions; $scope.model.embed.preview = response.data.Markup; vm.trustedPreview = $sce.trustAsHtml(response.data.Markup); - $scope.model.embed.supportsDimensions = response.data.SupportsDimensions; - $scope.model.embed.success = true; break; } + + vm.loading = false; + }, function() { + $scope.model.embed.success = false; $scope.model.embed.supportsDimensions = false; $scope.model.embed.preview = ""; $scope.model.embed.info = "Could not embed media - please ensure the URL is valid"; - }); + vm.loading = false; + }); } else { $scope.model.embed.supportsDimensions = false; $scope.model.embed.preview = ""; @@ -105,6 +125,10 @@ } + function toggleConstrain() { + $scope.model.embed.constrain = !$scope.model.embed.constrain; + } + function submit() { if($scope.model && $scope.model.submit) { $scope.model.submit($scope.model); 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 e48ec84b25..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 @@ - + - - -

-
-
- -
- - + + + +
+ + +

+
+ +
+ + + - - - + + + - - - + + + +
+
@@ -62,6 +68,7 @@ button-style="success" label-key="general_submit" state="vm.saveButtonState" + disabled="!model.embed.url.length || !model.embed.preview.length" action="vm.submit(model)"> diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/linkpicker/linkpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/linkpicker/linkpicker.controller.js index 5da14fc6d3..b5043293e5 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/linkpicker/linkpicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/linkpicker/linkpicker.controller.js @@ -93,7 +93,7 @@ angular.module("umbraco").controller("Umbraco.Editors.LinkPickerController", }); } - } else if ($scope.model.target.url.length) { + } else if ($scope.model.target.url && $scope.model.target.url.length) { // a url but no id/udi indicates an external link - trim the url to remove the anchor/qs // only do the substring if there's a # or a ? var indexOfAnchor = $scope.model.target.url.search(/(#|\?)/); diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/mediapicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/mediapicker.controller.js index 5efc25fa10..47182bfbee 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/mediapicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/mediapicker.controller.js @@ -268,7 +268,7 @@ angular.module("umbraco") // also make sure the node is not trashed if (nodePath.indexOf($scope.startNodeId.toString()) !== -1 && node.trashed === false) { - $scope.gotoFolder({ id: $scope.lastOpenedNode, name: "Media", icon: "icon-folder" }); + $scope.gotoFolder({ id: $scope.lastOpenedNode, name: "Media", icon: "icon-folder", path: node.path }); return true; } else { $scope.gotoFolder({ id: $scope.startNodeId, name: "Media", icon: "icon-folder" }); diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/querybuilder/querybuilder.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/querybuilder/querybuilder.html index 8376f50713..779ca739d2 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/querybuilder/querybuilder.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/querybuilder/querybuilder.html @@ -109,10 +109,10 @@ - - + diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/sectionpicker/sectionpicker.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/sectionpicker/sectionpicker.html index 2e88bf709c..33906dcd75 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/sectionpicker/sectionpicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/sectionpicker/sectionpicker.html @@ -19,10 +19,10 @@
    -
  • -
  • + diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/user/user.html b/src/Umbraco.Web.UI.Client/src/views/common/overlays/user/user.html index 4af8c83983..531feef892 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/user/user.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/user/user.html @@ -26,8 +26,7 @@ action="togglePasswordFields()" button-style="action" label="Change password" - label-key="general_changePassword" - button-style="success"> + label-key="general_changePassword"> -
    + - + + Link your {{login.caption}} account +
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/application/umb-login.html b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-login.html index e7513617b7..7a7d7c4dba 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/application/umb-login.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-login.html @@ -18,11 +18,11 @@

-
- - + + Required The confirmed password doesn't match the new password! @@ -138,7 +138,7 @@ id="{{login.authType}}" name="provider" value="{{login.authType}}" title="Log in using your {{login.caption}} account"> - Sign in with {{login.caption}} + Sign in with {{login.caption}}
@@ -152,13 +152,13 @@
- - + +
- - + +
-

+

An email will be sent to the address specified with a link to reset your password

- - + +
@@ -198,9 +198,11 @@
-
+ +

+
@@ -220,13 +222,13 @@
- - + +
- - + +
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/buttons/umb-button-group.html b/src/Umbraco.Web.UI.Client/src/views/components/buttons/umb-button-group.html index 6ae256f4ee..de082fb48e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/buttons/umb-button-group.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/buttons/umb-button-group.html @@ -1,5 +1,5 @@
- + - - - + ng-click="toggleDropdown()" + aria-haspopup="true" + aria-expanded="{{dropdown.isOpen}}"> + + + {{label}} + + + - - {{subButton.labelKey}} - ... - + diff --git a/src/Umbraco.Web.UI.Client/src/views/components/content/edit.html b/src/Umbraco.Web.UI.Client/src/views/components/content/edit.html index cd2c94c458..32e91b935a 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/content/edit.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/content/edit.html @@ -68,7 +68,9 @@ sub-buttons="subButtons" state="page.buttonGroupState" direction="up" - float="right"> + float="right" + label-key="buttons_morePublishingOptions" + label="More publishing options">
-
{{ group.label }}
+
{{ group.label }}
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-content-header.html b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-content-header.html index 37f363f50d..6856daf42b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-content-header.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-content-header.html @@ -3,32 +3,39 @@
- +
+
+

+ {{a11yMessage}} +

+
+ - + diff --git a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-header.html b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-header.html index cc2ef0f2fa..aca27da7be 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-header.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-header.html @@ -3,9 +3,10 @@
- +
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-navigation-item.html b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-navigation-item.html index 68a3da435b..dda8fa70f4 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-navigation-item.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-navigation-item.html @@ -1,19 +1,19 @@ - - + ng-class="{'is-active': vm.item.active, '-has-error': vm.item.hasError}" + class="umb-sub-views-nav-item__action"> + {{ vm.item.name }}
{{vm.item.badge.count}}
-
+ diff --git a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editors.html b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editors.html index 99f9e96765..84d0712327 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editors.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editors.html @@ -5,7 +5,7 @@ ng-class="{'umb-editor--small': model.size === 'small', 'umb-editor--animating': model.animating, '--notInFront': model.inFront !== true, - 'umb-editor--infinityMode': model.infinityMode, + 'umb-editor--infiniteMode': model.infiniteMode, 'moveRight': model.moveRight, 'umb-editor--n0': model.styleIndex === 0, 'umb-editor--n1': model.styleIndex === 1, diff --git a/src/Umbraco.Web.UI.Client/src/views/components/forms/umb-checkbox.html b/src/Umbraco.Web.UI.Client/src/views/components/forms/umb-checkbox.html index d40263c6b6..2f90905c7d 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/forms/umb-checkbox.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/forms/umb-checkbox.html @@ -1,4 +1,4 @@ -
- Akk {{itemLabel}}s are added + All {{itemLabel}}s are added
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/umb-groups-builder.html b/src/Umbraco.Web.UI.Client/src/views/components/umb-groups-builder.html index 026b0637c7..e8e47f6ae6 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/umb-groups-builder.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/umb-groups-builder.html @@ -14,7 +14,8 @@ icon="icon-merge" action="openCompositionsDialog()" size="xs" - add-ellipsis="true"> + add-ellipsis="true" + state="compositionsButtonState"> -
- -
+
+ on-outside-click="vm.closeLayoutDropdown()" + deep-blur="vm.leaveLayoutDropdown()"> -
- + + {{layout.name}}
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/umb-loader.html b/src/Umbraco.Web.UI.Client/src/views/components/umb-loader.html new file mode 100644 index 0000000000..07aeb7fdfa --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/components/umb-loader.html @@ -0,0 +1,3 @@ +
+
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/umb-media-grid.html b/src/Umbraco.Web.UI.Client/src/views/components/umb-media-grid.html index a91bc8c876..4cc4c105fc 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/umb-media-grid.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/umb-media-grid.html @@ -10,19 +10,19 @@
-
+
{{item.name}} - {{item.name}} + {{item.name}} - {{item.name}} + {{item.name}} - + .{{item.extension}} diff --git a/src/Umbraco.Web.UI.Client/src/views/components/umb-mini-list-view.html b/src/Umbraco.Web.UI.Client/src/views/components/umb-mini-list-view.html index 55d7bd0744..f11df6042e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/umb-mini-list-view.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/umb-mini-list-view.html @@ -60,7 +60,7 @@
-
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/upload/umb-file-dropzone.html b/src/Umbraco.Web.UI.Client/src/views/components/upload/umb-file-dropzone.html index a379a05829..1a0086e836 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/upload/umb-file-dropzone.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/upload/umb-file-dropzone.html @@ -17,13 +17,17 @@
- +

+ Drag and drop your file(s) into the area +

-
- - or click here to choose files -
+ - or click here to choose files +
@@ -104,7 +108,7 @@ ng-if="mediatypepickerOverlay.show" model="mediatypepickerOverlay" view="mediatypepickerOverlay.view" - position="right"> + position="right">
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/upload/umb-property-file-upload.html b/src/Umbraco.Web.UI.Client/src/views/components/upload/umb-property-file-upload.html index 6c73122464..5f728f47e4 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/upload/umb-property-file-upload.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/upload/umb-property-file-upload.html @@ -15,7 +15,7 @@
-
+
{{file.fileName}} @@ -26,21 +26,21 @@
-
+
- + - .{{file.extension}} + .{{file.extension}}
{{file.fileName}}
- + - .{{file.extension}} + .{{file.extension}}
{{file.fileName}}
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/users/change-password.html b/src/Umbraco.Web.UI.Client/src/views/components/users/change-password.html index 7b6c7ca0e1..ef364264ab 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/users/change-password.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/users/change-password.html @@ -1,68 +1,68 @@
-
+
Password has been reset to:
- {{passwordValues.generatedPassword}} + {{vm.passwordValues.generatedPassword}}
-
+
- - - + + - - {{passwordForm.resetPassword.errorMsg}} + ng-change="vm.showReset = !vm.showReset" /> + + {{changePasswordForm.resetPassword.errorMsg}} - - + + - - + - + Required - {{passwordForm.oldPassword.errorMsg}} + {{changePasswordForm.oldPassword.errorMsg}} - - + - + Required - Minimum {{config.minPasswordLength}} characters - {{passwordForm.password.errorMsg}} + Minimum {{vm.config.minPasswordLength}} characters + {{changePasswordForm.password.errorMsg}} - - + - + The confirmed password doesn't match the new password! - + Cancel diff --git a/src/Umbraco.Web.UI.Client/src/views/content/content.create.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/content.create.controller.js index d04c707b25..1413965a19 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/content.create.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/content.create.controller.js @@ -51,7 +51,10 @@ function contentCreateController($scope, .search("create", "true") /* when we create a new node we want to make sure it uses the same language as what is selected in the tree */ - .search("cculture", mainCulture); + .search("cculture", mainCulture) + /* when we create a new node we must make sure that any previously + used blueprint is reset */ + .search("blueprintId", null); close(); } @@ -94,6 +97,11 @@ function contentCreateController($scope, navigationService.hideDialog(showMenu); }; + $scope.editContentType = function() { + $location.path("/settings/documenttypes/edit/" + $scope.contentTypeId).search("view", "permissions"); + close(); + } + $scope.createBlank = createBlank; $scope.createOrSelectBlueprintIfAny = createOrSelectBlueprintIfAny; $scope.createFromBlueprint = createFromBlueprint; diff --git a/src/Umbraco.Web.UI.Client/src/views/content/content.delete.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/content.delete.controller.js index 56ce67a0dc..855afc2bd2 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/content.delete.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/content.delete.controller.js @@ -58,7 +58,7 @@ function ContentDeleteController($scope, $timeout, contentResource, treeService, $location.path(location); } - navigationService.hideMenu(); + $scope.success = true; }, function(err) { toggleDeleting(false); @@ -74,7 +74,11 @@ function ContentDeleteController($scope, $timeout, contentResource, treeService, $scope.cancel = function() { toggleDeleting(false); - navigationService.hideDialog(); + $scope.close(); + }; + + $scope.close = function () { + navigationService.hideDialog(); }; } diff --git a/src/Umbraco.Web.UI.Client/src/views/content/content.edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/content.edit.controller.js index 5cbf25ba7d..ad79bf01d1 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/content.edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/content.edit.controller.js @@ -13,6 +13,9 @@ function ContentEditController($scope, $routeParams, contentResource) { function scaffoldEmpty() { return contentResource.getScaffold($routeParams.id, $routeParams.doctype); } + function scaffoldInfiniteEmpty() { + return contentResource.getScaffold($scope.model.parentId, $scope.model.documentTypeAlias); + } function scaffoldBlueprint() { return contentResource.getBlueprintScaffold($routeParams.id, $routeParams.blueprintId); } @@ -20,11 +23,11 @@ function ContentEditController($scope, $routeParams, contentResource) { $scope.contentId = infiniteMode ? $scope.model.id : $routeParams.id; $scope.saveMethod = contentResource.save; $scope.getMethod = contentResource.getById; - $scope.getScaffoldMethod = $routeParams.blueprintId ? scaffoldBlueprint : scaffoldEmpty; + $scope.getScaffoldMethod = $routeParams.blueprintId ? scaffoldBlueprint : infiniteMode ? scaffoldInfiniteEmpty : scaffoldEmpty; $scope.page = $routeParams.page; $scope.isNew = infiniteMode ? $scope.model.create : $routeParams.create; //load the default culture selected in the main tree if any - $scope.culture = $routeParams.cculture ? $routeParams.cculture : ($routeParams.mculture === "true"); + $scope.culture = $routeParams.cculture ? $routeParams.cculture : $routeParams.mculture; //Bind to $routeUpdate which will execute anytime a location changes but the route is not triggered. //This is so we can listen to changes on the cculture parameter since that will not cause a route change diff --git a/src/Umbraco.Web.UI.Client/src/views/content/copy.html b/src/Umbraco.Web.UI.Client/src/views/content/copy.html index 0ebe577ed8..111dacd7cb 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/copy.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/copy.html @@ -24,9 +24,7 @@ to in the tree structure below

-
-
-
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/content/create.html b/src/Umbraco.Web.UI.Client/src/views/content/create.html index 059b5eaf25..cbedabba79 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/create.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/create.html @@ -10,7 +10,7 @@

-
@@ -42,7 +42,7 @@
    -
  • +
- +
+

+ Are you sure you want to delete {{currentNode.name}}? +

+ +
+ This will delete the node and all its languages. If you only want to delete one language go and unpublish it instead. +
+ + +
diff --git a/src/Umbraco.Web.UI.Client/src/views/content/emptyrecyclebin.html b/src/Umbraco.Web.UI.Client/src/views/content/emptyrecyclebin.html index 524bb06354..9ac7ef10fc 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/emptyrecyclebin.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/emptyrecyclebin.html @@ -1,9 +1,8 @@
-
-
-
+ +

When items are deleted from the recycle bin, they will be gone forever. diff --git a/src/Umbraco.Web.UI.Client/src/views/content/move.html b/src/Umbraco.Web.UI.Client/src/views/content/move.html index fb72c38974..3c073ba404 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/move.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/move.html @@ -24,9 +24,7 @@ to in the tree structure below

-
-
-
+
@@ -76,9 +74,9 @@
-
diff --git a/src/Umbraco.Web.UI.Client/src/views/contentblueprints/create.html b/src/Umbraco.Web.UI.Client/src/views/contentblueprints/create.html index 24cfe5d0d1..6146c007b1 100644 --- a/src/Umbraco.Web.UI.Client/src/views/contentblueprints/create.html +++ b/src/Umbraco.Web.UI.Client/src/views/contentblueprints/create.html @@ -11,7 +11,7 @@ diff --git a/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/examinemanagement.html b/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/examinemanagement.html index 6a7e65f7ae..6b7e3f4519 100644 --- a/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/examinemanagement.html +++ b/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/examinemanagement.html @@ -89,7 +89,7 @@ - ← Back to overview + @@ -198,7 +198,7 @@ - Back to overview + diff --git a/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/healthcheck.html b/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/healthcheck.html index 1f81bfc6e2..67d8562f7e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/healthcheck.html +++ b/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/healthcheck.html @@ -1,19 +1,19 @@
- +

Health Check

- +

The health checker evaluates various areas of your site for best practice settings, configuration, potential problems, etc. You can easily fix problems by pressing a button. You can add your own health checks, have a look at the documentation for more information about custom health checks.

@@ -23,40 +23,40 @@
-
-
+
+
@@ -79,7 +79,7 @@
{{ vm.selectedGroup.name }}
diff --git a/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/nucache.html b/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/nucache.html index 689bcf8fa2..eb012c3d98 100644 --- a/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/nucache.html +++ b/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/nucache.html @@ -4,13 +4,13 @@
-

+

(wait) {{vm.status}}

- +

Memory Cache

@@ -24,7 +24,7 @@

- +

Database Cache

@@ -37,7 +37,7 @@

- +

Internals

@@ -48,7 +48,7 @@

- +
diff --git a/src/Umbraco.Web.UI.Client/src/views/datatypes/create.html b/src/Umbraco.Web.UI.Client/src/views/datatypes/create.html index c359e7bfd1..55039831ac 100644 --- a/src/Umbraco.Web.UI.Client/src/views/datatypes/create.html +++ b/src/Umbraco.Web.UI.Client/src/views/datatypes/create.html @@ -6,20 +6,20 @@
diff --git a/src/Umbraco.Web.UI.Client/src/views/datatypes/move.html b/src/Umbraco.Web.UI.Client/src/views/datatypes/move.html index 2723dd305d..7e7e70d650 100644 --- a/src/Umbraco.Web.UI.Client/src/views/datatypes/move.html +++ b/src/Umbraco.Web.UI.Client/src/views/datatypes/move.html @@ -7,9 +7,7 @@ Select the folder to move {{source.name}} to in the tree structure below

-
-
-
+
@@ -45,9 +43,9 @@
/// Int id of the entity to fetch URL for /// The type of entity such as Document, Media, Member + /// The culture to fetch the URL for /// The URL or path to the item /// /// We are not restricting this with security because there is no sensitive data /// - public HttpResponseMessage GetUrl(int id, UmbracoEntityTypes type) + public HttpResponseMessage GetUrl(int id, UmbracoEntityTypes type, string culture = null) { + culture = culture ?? ClientCulture(); + var returnUrl = string.Empty; if (type == UmbracoEntityTypes.Document) { - var foundUrl = UmbracoContext.Url(id); + var foundUrl = UmbracoContext.Url(id, culture); if (string.IsNullOrEmpty(foundUrl) == false && foundUrl != "#") { returnUrl = foundUrl; diff --git a/src/Umbraco.Web/Editors/LogViewerController.cs b/src/Umbraco.Web/Editors/LogViewerController.cs index d9fcfd108a..d48de8a00a 100644 --- a/src/Umbraco.Web/Editors/LogViewerController.cs +++ b/src/Umbraco.Web/Editors/LogViewerController.cs @@ -130,5 +130,11 @@ namespace Umbraco.Web.Editors { return _logViewer.DeleteSavedSearch(item.Name, item.Query); } + + [HttpGet] + public string GetLogLevel() + { + return _logViewer.GetLogLevel(); + } } } diff --git a/src/Umbraco.Web/Editors/MacrosController.cs b/src/Umbraco.Web/Editors/MacrosController.cs index d7d50236d5..429e8b6190 100644 --- a/src/Umbraco.Web/Editors/MacrosController.cs +++ b/src/Umbraco.Web/Editors/MacrosController.cs @@ -62,6 +62,11 @@ namespace Umbraco.Web.Editors return this.ReturnErrorResponse("Macro with this alias already exists"); } + if (name == null || name.Length > 255) + { + return this.ReturnErrorResponse("Name cannnot be more than 255 characters in length."); + } + try { var macro = new Macro @@ -149,6 +154,11 @@ namespace Umbraco.Web.Editors return this.ReturnErrorResponse($"No macro data found in request"); } + if (macroDisplay.Name == null || macroDisplay.Name.Length > 255) + { + return this.ReturnErrorResponse("Name cannnot be more than 255 characters in length."); + } + var macro = _macroService.GetById(int.Parse(macroDisplay.Id.ToString())); if (macro == null) diff --git a/src/Umbraco.Web/Editors/MediaTypeController.cs b/src/Umbraco.Web/Editors/MediaTypeController.cs index c55f07d559..43569c77e2 100644 --- a/src/Umbraco.Web/Editors/MediaTypeController.cs +++ b/src/Umbraco.Web/Editors/MediaTypeController.cs @@ -19,6 +19,7 @@ using Umbraco.Core.Dictionary; using Umbraco.Core.Logging; using Umbraco.Core.Persistence; using Umbraco.Web.Composing; +using IMediaType = Umbraco.Core.Models.IMediaType; namespace Umbraco.Web.Editors { @@ -109,7 +110,7 @@ namespace Umbraco.Web.Editors [HttpPost] public HttpResponseMessage GetAvailableCompositeMediaTypes(GetAvailableCompositionsFilter filter) { - var result = PerformGetAvailableCompositeContentTypes(filter.ContentTypeId, UmbracoObjectTypes.MediaType, filter.FilterContentTypes, filter.FilterPropertyTypes) + var result = PerformGetAvailableCompositeContentTypes(filter.ContentTypeId, UmbracoObjectTypes.MediaType, filter.FilterContentTypes, filter.FilterPropertyTypes, filter.IsElement) .Select(x => new { contentType = x.Item1, @@ -135,12 +136,18 @@ namespace Umbraco.Web.Editors } public MediaTypeDisplay GetEmpty(int parentId) { - var ct = new MediaType(parentId) + IMediaType mt; + if (parentId != Constants.System.Root) { - Icon = Constants.Icons.MediaImage - }; + var parent = Services.MediaTypeService.Get(parentId); + mt = parent != null ? new MediaType(parent, string.Empty) : new MediaType(parentId); + } + else + mt = new MediaType(parentId); - var dto = Mapper.Map(ct); + mt.Icon = Constants.Icons.MediaImage; + + var dto = Mapper.Map(mt); return dto; } diff --git a/src/Umbraco.Web/Editors/MemberTypeController.cs b/src/Umbraco.Web/Editors/MemberTypeController.cs index f406988ae5..fffead9155 100644 --- a/src/Umbraco.Web/Editors/MemberTypeController.cs +++ b/src/Umbraco.Web/Editors/MemberTypeController.cs @@ -89,7 +89,7 @@ namespace Umbraco.Web.Editors [FromUri]string[] filterContentTypes, [FromUri]string[] filterPropertyTypes) { - var result = PerformGetAvailableCompositeContentTypes(contentTypeId, UmbracoObjectTypes.MemberType, filterContentTypes, filterPropertyTypes) + var result = PerformGetAvailableCompositeContentTypes(contentTypeId, UmbracoObjectTypes.MemberType, filterContentTypes, filterPropertyTypes, false) .Select(x => new { contentType = x.Item1, diff --git a/src/Umbraco.Web/HtmlHelperBackOfficeExtensions.cs b/src/Umbraco.Web/HtmlHelperBackOfficeExtensions.cs index 26a1d97f67..1d518fa1d3 100644 --- a/src/Umbraco.Web/HtmlHelperBackOfficeExtensions.cs +++ b/src/Umbraco.Web/HtmlHelperBackOfficeExtensions.cs @@ -13,6 +13,7 @@ using Umbraco.Web.Models; namespace Umbraco.Web { using Core.Configuration; + using Umbraco.Web.JavaScript; /// /// HtmlHelper extensions for the back office @@ -118,6 +119,21 @@ namespace Umbraco.Web sb.AppendLine(JsonConvert.SerializeObject(resetCodeModel)); sb.AppendLine(@"});"); + return html.Raw(sb.ToString()); + } + + public static IHtmlString AngularValueTinyMceAssets(this HtmlHelper html) + { + var ctx = new HttpContextWrapper(HttpContext.Current); + var files = JsInitialization.OptimizeTinyMceScriptFiles(ctx); + + var sb = new StringBuilder(); + + sb.AppendLine(@"app.value(""tinyMceAssets"","); + sb.AppendLine(JsonConvert.SerializeObject(files)); + sb.AppendLine(@");"); + + return html.Raw(sb.ToString()); } } diff --git a/src/Umbraco.Web/HtmlStringUtilities.cs b/src/Umbraco.Web/HtmlStringUtilities.cs index 6cf5092bb0..4606a58a3a 100644 --- a/src/Umbraco.Web/HtmlStringUtilities.cs +++ b/src/Umbraco.Web/HtmlStringUtilities.cs @@ -19,10 +19,26 @@ namespace Umbraco.Web /// Replaces text line breaks with HTML line breaks /// /// The text. - /// The text with text line breaks replaced with HTML line breaks (
)
+ /// The text with text line breaks replaced with HTML line breaks (<br />). + [Obsolete("This method doesn't HTML encode the text. Use ReplaceLineBreaks instead.")] public HtmlString ReplaceLineBreaksForHtml(string text) { - return new HtmlString(text.Replace("\r\n", @"
").Replace("\n", @"
").Replace("\r", @"
")); + return new HtmlString(text.Replace("\r\n", @"
").Replace("\n", @"
").Replace("\r", @"
")); + } + + /// + /// HTML encodes the text and replaces text line breaks with HTML line breaks. + /// + /// The text. + /// The HTML encoded text with text line breaks replaced with HTML line breaks (<br />). + public IHtmlString ReplaceLineBreaks(string text) + { + var value = HttpUtility.HtmlEncode(text)? + .Replace("\r\n", "
") + .Replace("\r", "
") + .Replace("\n", "
"); + + return new HtmlString(value); } public HtmlString StripHtmlTags(string html, params string[] tags) diff --git a/src/Umbraco.Web/JavaScript/JsInitialization.cs b/src/Umbraco.Web/JavaScript/JsInitialization.cs index 638b1bf5b4..17e4f7b094 100644 --- a/src/Umbraco.Web/JavaScript/JsInitialization.cs +++ b/src/Umbraco.Web/JavaScript/JsInitialization.cs @@ -131,6 +131,17 @@ namespace Umbraco.Web.JavaScript return resources.Where(x => x.Type == JTokenType.String).Select(x => x.ToString()); } + internal static IEnumerable GetTinyMceInitialization() + { + var resources = JsonConvert.DeserializeObject(Resources.TinyMceInitialize); + return resources.Where(x => x.Type == JTokenType.String).Select(x => x.ToString()); + } + + internal static IEnumerable OptimizeTinyMceScriptFiles(HttpContextBase httpContext) + { + return OptimizeScriptFiles(httpContext, GetTinyMceInitialization()); + } + /// /// Parses the JsResources.Main and replaces the replacement tokens accordingly. /// diff --git a/src/Umbraco.Web/JavaScript/Resources.Designer.cs b/src/Umbraco.Web/JavaScript/Resources.Designer.cs index ca571b1186..f11839f6ca 100644 --- a/src/Umbraco.Web/JavaScript/Resources.Designer.cs +++ b/src/Umbraco.Web/JavaScript/Resources.Designer.cs @@ -146,5 +146,17 @@ namespace Umbraco.Web.JavaScript { return ResourceManager.GetString("ServerVariables", resourceCulture); } } + + /// + /// Looks up a localized string similar to [ + /// '../lib/tinymce/tinymce.min.js', + ///] + ///. + /// + internal static string TinyMceInitialize { + get { + return ResourceManager.GetString("TinyMceInitialize", resourceCulture); + } + } } } diff --git a/src/Umbraco.Web/JavaScript/Resources.resx b/src/Umbraco.Web/JavaScript/Resources.resx index 95526f51e7..1adcaeeb4a 100644 --- a/src/Umbraco.Web/JavaScript/Resources.resx +++ b/src/Umbraco.Web/JavaScript/Resources.resx @@ -130,4 +130,7 @@ servervariables.js;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8 - + + TinyMceInitialize.js;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8 + + \ No newline at end of file diff --git a/src/Umbraco.Web/JavaScript/TinyMceInitialize.js b/src/Umbraco.Web/JavaScript/TinyMceInitialize.js new file mode 100644 index 0000000000..6cb9cf6277 --- /dev/null +++ b/src/Umbraco.Web/JavaScript/TinyMceInitialize.js @@ -0,0 +1,17 @@ +[ + 'lib/tinymce/tinymce.min.js', + + 'lib/tinymce/plugins/paste/plugin.min.js', + 'lib/tinymce/plugins/anchor/plugin.min.js', + 'lib/tinymce/plugins/charmap/plugin.min.js', + 'lib/tinymce/plugins/table/plugin.min.js', + 'lib/tinymce/plugins/lists/plugin.min.js', + 'lib/tinymce/plugins/advlist/plugin.min.js', + 'lib/tinymce/plugins/hr/plugin.min.js', + 'lib/tinymce/plugins/autolink/plugin.min.js', + 'lib/tinymce/plugins/directionality/plugin.min.js', + 'lib/tinymce/plugins/tabfocus/plugin.min.js', + 'lib/tinymce/plugins/searchreplace/plugin.min.js', + 'lib/tinymce/plugins/fullscreen/plugin.min.js', + 'lib/tinymce/plugins/noneditable/plugin.min.js' +] diff --git a/src/Umbraco.Web/Models/ContentEditing/GetAvailableCompositionsFilter.cs b/src/Umbraco.Web/Models/ContentEditing/GetAvailableCompositionsFilter.cs index 38442f6da5..b1528db697 100644 --- a/src/Umbraco.Web/Models/ContentEditing/GetAvailableCompositionsFilter.cs +++ b/src/Umbraco.Web/Models/ContentEditing/GetAvailableCompositionsFilter.cs @@ -16,5 +16,10 @@ /// along with any content types that have matching property types that are included in the filtered content types ///
public string[] FilterContentTypes { get; set; } + + /// + /// Wether the content type is currently marked as an element type + /// + public bool IsElement { get; set; } } } diff --git a/src/Umbraco.Web/Models/Mapping/ContentTypeMapDefinition.cs b/src/Umbraco.Web/Models/Mapping/ContentTypeMapDefinition.cs index fc029eabe4..528d5f6de5 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentTypeMapDefinition.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentTypeMapDefinition.cs @@ -8,6 +8,7 @@ using Umbraco.Core.Models; using Umbraco.Core.PropertyEditors; using Umbraco.Web.Models.ContentEditing; using Umbraco.Core.Services; +using Umbraco.Core.Exceptions; namespace Umbraco.Web.Models.Mapping { @@ -577,7 +578,7 @@ namespace Umbraco.Web.Models.Mapping udiType = Constants.UdiEntityType.DocumentType; break; default: - throw new Exception("panic"); + throw new PanicException($"Source is of type {source.GetType()} which isn't supported here"); } return Udi.Create(udiType, source.Key); diff --git a/src/Umbraco.Web/Models/Mapping/EntityMapDefinition.cs b/src/Umbraco.Web/Models/Mapping/EntityMapDefinition.cs index 38ec557fdb..eb474e4cbe 100644 --- a/src/Umbraco.Web/Models/Mapping/EntityMapDefinition.cs +++ b/src/Umbraco.Web/Models/Mapping/EntityMapDefinition.cs @@ -46,10 +46,19 @@ namespace Umbraco.Web.Models.Mapping target.Icon = Constants.Icons.Member; if (source is IContentEntitySlim contentSlim) + { source.AdditionalData["ContentTypeAlias"] = contentSlim.ContentTypeAlias; + } + + if (source is IDocumentEntitySlim documentSlim) + { + source.AdditionalData["IsPublished"] = documentSlim.Published; + } if (source is IMediaEntitySlim mediaSlim) + { source.AdditionalData["MediaPath"] = mediaSlim.MediaPath; + } // NOTE: we're mapping the objects in AdditionalData by object reference here. // it works fine for now, but it's something to keep in mind in the future diff --git a/src/Umbraco.Web/Models/Mapping/UserMapDefinition.cs b/src/Umbraco.Web/Models/Mapping/UserMapDefinition.cs index 3860d5d525..4acda1e552 100644 --- a/src/Umbraco.Web/Models/Mapping/UserMapDefinition.cs +++ b/src/Umbraco.Web/Models/Mapping/UserMapDefinition.cs @@ -301,7 +301,7 @@ namespace Umbraco.Web.Models.Mapping target.Avatars = source.GetUserAvatarUrls(_appCaches.RuntimeCache); target.Culture = source.GetUserCulture(_textService, _globalSettings).ToString(); target.Email = source.Email; - target.EmailHash = source.Email.ToLowerInvariant().Trim().ToMd5(); + target.EmailHash = source.Email.ToLowerInvariant().Trim().GenerateHash(); target.Id = source.Id; target.Key = source.Key; target.LastLoginDate = source.LastLoginDate == default ? null : (DateTime?) source.LastLoginDate; diff --git a/src/Umbraco.Web/PropertyEditors/CheckBoxListPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/CheckBoxListPropertyEditor.cs index e0f7184dc7..d81b63f2ad 100644 --- a/src/Umbraco.Web/PropertyEditors/CheckBoxListPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/CheckBoxListPropertyEditor.cs @@ -8,7 +8,12 @@ namespace Umbraco.Web.PropertyEditors /// /// A property editor to allow multiple checkbox selection of pre-defined items. /// - [DataEditor(Constants.PropertyEditors.Aliases.CheckBoxList, "Checkbox list", "checkboxlist", Icon="icon-bulleted-list", Group="lists")] + [DataEditor( + Constants.PropertyEditors.Aliases.CheckBoxList, + "Checkbox list", + "checkboxlist", + Icon = "icon-bulleted-list", + Group = Constants.PropertyEditors.Groups.Lists)] public class CheckBoxListPropertyEditor : DataEditor { private readonly ILocalizedTextService _textService; diff --git a/src/Umbraco.Web/PropertyEditors/ColorPickerPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/ColorPickerPropertyEditor.cs index 5da9d2888d..be6bde248f 100644 --- a/src/Umbraco.Web/PropertyEditors/ColorPickerPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/ColorPickerPropertyEditor.cs @@ -4,7 +4,12 @@ using Umbraco.Core.PropertyEditors; namespace Umbraco.Web.PropertyEditors { - [DataEditor(Constants.PropertyEditors.Aliases.ColorPicker, "Color Picker", "colorpicker", Icon="icon-colorpicker", Group="Pickers")] + [DataEditor( + Constants.PropertyEditors.Aliases.ColorPicker, + "Color Picker", + "colorpicker", + Icon = "icon-colorpicker", + Group = Constants.PropertyEditors.Groups.Pickers)] public class ColorPickerPropertyEditor : DataEditor { public ColorPickerPropertyEditor(ILogger logger) diff --git a/src/Umbraco.Web/PropertyEditors/ContentPickerPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/ContentPickerPropertyEditor.cs index 391257ca58..c6de91f560 100644 --- a/src/Umbraco.Web/PropertyEditors/ContentPickerPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/ContentPickerPropertyEditor.cs @@ -8,7 +8,13 @@ namespace Umbraco.Web.PropertyEditors /// /// Content property editor that stores UDI /// - [DataEditor(Constants.PropertyEditors.Aliases.ContentPicker, EditorType.PropertyValue | EditorType.MacroParameter, "Content Picker", "contentpicker", ValueType = ValueTypes.String, Group = "Pickers")] + [DataEditor( + Constants.PropertyEditors.Aliases.ContentPicker, + EditorType.PropertyValue | EditorType.MacroParameter, + "Content Picker", + "contentpicker", + ValueType = ValueTypes.String, + Group = Constants.PropertyEditors.Groups.Pickers)] public class ContentPickerPropertyEditor : DataEditor { public ContentPickerPropertyEditor(ILogger logger) diff --git a/src/Umbraco.Web/PropertyEditors/DateTimePropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/DateTimePropertyEditor.cs index fa3734e8aa..31fbc9e3f1 100644 --- a/src/Umbraco.Web/PropertyEditors/DateTimePropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/DateTimePropertyEditor.cs @@ -7,7 +7,12 @@ namespace Umbraco.Web.PropertyEditors /// /// Represents a date and time property editor. /// - [DataEditor(Constants.PropertyEditors.Aliases.DateTime, "Date/Time", "datepicker", ValueType = ValueTypes.DateTime, Icon="icon-time")] + [DataEditor( + Constants.PropertyEditors.Aliases.DateTime, + "Date/Time", + "datepicker", + ValueType = ValueTypes.DateTime, + Icon = "icon-time")] public class DateTimePropertyEditor : DataEditor { /// diff --git a/src/Umbraco.Web/PropertyEditors/DecimalPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/DecimalPropertyEditor.cs index 15b33f28b7..880cf8f204 100644 --- a/src/Umbraco.Web/PropertyEditors/DecimalPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/DecimalPropertyEditor.cs @@ -8,7 +8,12 @@ namespace Umbraco.Web.PropertyEditors /// /// Represents a decimal property and parameter editor. /// - [DataEditor(Constants.PropertyEditors.Aliases.Decimal, EditorType.PropertyValue | EditorType.MacroParameter, "Decimal", "decimal", ValueType = ValueTypes.Decimal)] + [DataEditor( + Constants.PropertyEditors.Aliases.Decimal, + EditorType.PropertyValue | EditorType.MacroParameter, + "Decimal", + "decimal", + ValueType = ValueTypes.Decimal)] public class DecimalPropertyEditor : DataEditor { /// diff --git a/src/Umbraco.Web/PropertyEditors/DropDownFlexiblePropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/DropDownFlexiblePropertyEditor.cs index eac692fcdd..98a19f39ad 100644 --- a/src/Umbraco.Web/PropertyEditors/DropDownFlexiblePropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/DropDownFlexiblePropertyEditor.cs @@ -5,7 +5,12 @@ using Umbraco.Core.Services; namespace Umbraco.Web.PropertyEditors { - [DataEditor(Constants.PropertyEditors.Aliases.DropDownListFlexible, "Dropdown", "dropdownFlexible", Group = "lists", Icon = "icon-indent")] + [DataEditor( + Constants.PropertyEditors.Aliases.DropDownListFlexible, + "Dropdown", + "dropdownFlexible", + Group = Constants.PropertyEditors.Groups.Lists, + Icon = "icon-indent")] public class DropDownFlexiblePropertyEditor : DataEditor { private readonly ILocalizedTextService _textService; diff --git a/src/Umbraco.Web/PropertyEditors/EmailAddressPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/EmailAddressPropertyEditor.cs index d55d3327e1..cd3a830c72 100644 --- a/src/Umbraco.Web/PropertyEditors/EmailAddressPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/EmailAddressPropertyEditor.cs @@ -5,7 +5,12 @@ using Umbraco.Core.PropertyEditors.Validators; namespace Umbraco.Web.PropertyEditors { - [DataEditor(Constants.PropertyEditors.Aliases.EmailAddress, EditorType.PropertyValue | EditorType.MacroParameter, "Email address", "email", Icon="icon-message")] + [DataEditor( + Constants.PropertyEditors.Aliases.EmailAddress, + EditorType.PropertyValue | EditorType.MacroParameter, + "Email address", + "email", + Icon = "icon-message")] public class EmailAddressPropertyEditor : DataEditor { /// diff --git a/src/Umbraco.Web/PropertyEditors/FileUploadPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/FileUploadPropertyEditor.cs index 6d87bfe495..702408788a 100644 --- a/src/Umbraco.Web/PropertyEditors/FileUploadPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/FileUploadPropertyEditor.cs @@ -12,7 +12,12 @@ using Umbraco.Web.Media; namespace Umbraco.Web.PropertyEditors { - [DataEditor(Constants.PropertyEditors.Aliases.UploadField, "File upload", "fileupload", Icon = "icon-download-alt", Group = "media")] + [DataEditor( + Constants.PropertyEditors.Aliases.UploadField, + "File upload", + "fileupload", + Group = Constants.PropertyEditors.Groups.Media, + Icon = "icon-download-alt")] public class FileUploadPropertyEditor : DataEditor { private readonly IMediaFileSystem _mediaFileSystem; diff --git a/src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs index 99972a27cf..aa7a1f7355 100644 --- a/src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs @@ -12,7 +12,14 @@ namespace Umbraco.Web.PropertyEditors /// /// Represents a grid property and parameter editor. /// - [DataEditor(Constants.PropertyEditors.Aliases.Grid, "Grid layout", "grid", HideLabel = true, ValueType = ValueTypes.Json, Group="rich content", Icon="icon-layout")] + [DataEditor( + Constants.PropertyEditors.Aliases.Grid, + "Grid layout", + "grid", + HideLabel = true, + ValueType = ValueTypes.Json, + Icon = "icon-layout", + Group = Constants.PropertyEditors.Groups.RichContent)] public class GridPropertyEditor : DataEditor { public GridPropertyEditor(ILogger logger) diff --git a/src/Umbraco.Web/PropertyEditors/GridPropertyIndexValueFactory.cs b/src/Umbraco.Web/PropertyEditors/GridPropertyIndexValueFactory.cs index 085d66f3c6..0cf36122d7 100644 --- a/src/Umbraco.Web/PropertyEditors/GridPropertyIndexValueFactory.cs +++ b/src/Umbraco.Web/PropertyEditors/GridPropertyIndexValueFactory.cs @@ -41,9 +41,7 @@ namespace Umbraco.Web.PropertyEditors { var controlVal = control.Value; - // TODO: If it's not a string, then it's a json formatted value - - // we cannot really index this in a smart way since it could be 'anything' - if (controlVal.Type == JTokenType.String) + if (controlVal?.Type == JTokenType.String) { var str = controlVal.Value(); str = XmlHelper.CouldItBeXml(str) ? str.StripHtml() : str; @@ -53,14 +51,22 @@ namespace Umbraco.Web.PropertyEditors //add the row name as an individual field result.Add(new KeyValuePair>($"{property.Alias}.{rowName}", new[] { str })); } + else if (controlVal is JContainer jc) + { + foreach (var s in jc.Descendants().Where(t => t.Type == JTokenType.String)) + { + sb.Append(s.Value()); + sb.Append(" "); + } + } } } + //First save the raw value to a raw field + result.Add(new KeyValuePair>($"{UmbracoExamineIndex.RawFieldPrefix}{property.Alias}", new[] { rawVal })); + if (sb.Length > 0) { - //First save the raw value to a raw field - result.Add(new KeyValuePair>($"{UmbracoExamineIndex.RawFieldPrefix}{property.Alias}", new[] { rawVal })); - //index the property with the combined/cleaned value result.Add(new KeyValuePair>(property.Alias, new[] { sb.ToString() })); } diff --git a/src/Umbraco.Web/PropertyEditors/ImageCropperPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/ImageCropperPropertyEditor.cs index 86f3412593..9a8fb7c40b 100644 --- a/src/Umbraco.Web/PropertyEditors/ImageCropperPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/ImageCropperPropertyEditor.cs @@ -19,7 +19,14 @@ namespace Umbraco.Web.PropertyEditors /// /// Represents an image cropper property editor. /// - [DataEditor(Constants.PropertyEditors.Aliases.ImageCropper, "Image Cropper", "imagecropper", ValueType = ValueTypes.Json, HideLabel = false, Group="media", Icon="icon-crop")] + [DataEditor( + Constants.PropertyEditors.Aliases.ImageCropper, + "Image Cropper", + "imagecropper", + ValueType = ValueTypes.Json, + HideLabel = false, + Group = Constants.PropertyEditors.Groups.Media, + Icon = "icon-crop")] public class ImageCropperPropertyEditor : DataEditor { private readonly IMediaFileSystem _mediaFileSystem; diff --git a/src/Umbraco.Web/PropertyEditors/IntegerPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/IntegerPropertyEditor.cs index 5d6adc018c..7717ced4a5 100644 --- a/src/Umbraco.Web/PropertyEditors/IntegerPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/IntegerPropertyEditor.cs @@ -8,7 +8,12 @@ namespace Umbraco.Web.PropertyEditors /// /// Represents an integer property and parameter editor. /// - [DataEditor(Constants.PropertyEditors.Aliases.Integer, EditorType.PropertyValue | EditorType.MacroParameter, "Numeric", "integer", ValueType = ValueTypes.Integer)] + [DataEditor( + Constants.PropertyEditors.Aliases.Integer, + EditorType.PropertyValue | EditorType.MacroParameter, + "Numeric", + "integer", + ValueType = ValueTypes.Integer)] public class IntegerPropertyEditor : DataEditor { public IntegerPropertyEditor(ILogger logger) diff --git a/src/Umbraco.Web/PropertyEditors/ListViewPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/ListViewPropertyEditor.cs index f170608545..53f9cb94ef 100644 --- a/src/Umbraco.Web/PropertyEditors/ListViewPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/ListViewPropertyEditor.cs @@ -8,7 +8,13 @@ namespace Umbraco.Web.PropertyEditors /// /// Represents a list-view editor. /// - [DataEditor(Constants.PropertyEditors.Aliases.ListView, "List view", "listview", HideLabel = true, Group = "lists", Icon = Constants.Icons.ListView)] + [DataEditor( + Constants.PropertyEditors.Aliases.ListView, + "List view", + "listview", + HideLabel = true, + Group = Constants.PropertyEditors.Groups.Lists, + Icon = Constants.Icons.ListView)] public class ListViewPropertyEditor : DataEditor { /// diff --git a/src/Umbraco.Web/PropertyEditors/MacroContainerPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/MacroContainerPropertyEditor.cs index 99a9f44487..ff2c08bf62 100644 --- a/src/Umbraco.Web/PropertyEditors/MacroContainerPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/MacroContainerPropertyEditor.cs @@ -5,7 +5,14 @@ using Umbraco.Core.PropertyEditors; namespace Umbraco.Web.PropertyEditors { // TODO: MacroContainerPropertyEditor is deprecated, but what's the alternative? - [DataEditor(Constants.PropertyEditors.Aliases.MacroContainer, "(Obsolete) Macro Picker", "macrocontainer", ValueType = ValueTypes.Text, Group = "rich content", Icon = Constants.Icons.Macro, IsDeprecated = true)] + [DataEditor( + Constants.PropertyEditors.Aliases.MacroContainer, + "(Obsolete) Macro Picker", + "macrocontainer", + ValueType = ValueTypes.Text, + Group = Constants.PropertyEditors.Groups.RichContent, + Icon = Constants.Icons.Macro, + IsDeprecated = true)] public class MacroContainerPropertyEditor : DataEditor { public MacroContainerPropertyEditor(ILogger logger) diff --git a/src/Umbraco.Web/PropertyEditors/MarkdownPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/MarkdownPropertyEditor.cs index 99b68a507b..2d66da5461 100644 --- a/src/Umbraco.Web/PropertyEditors/MarkdownPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/MarkdownPropertyEditor.cs @@ -7,7 +7,13 @@ namespace Umbraco.Web.PropertyEditors /// /// Represents a markdown editor. /// - [DataEditor(Constants.PropertyEditors.Aliases.MarkdownEditor, "Markdown editor", "markdowneditor", ValueType = ValueTypes.Text, Icon="icon-code", Group="rich content")] + [DataEditor( + Constants.PropertyEditors.Aliases.MarkdownEditor, + "Markdown editor", + "markdowneditor", + ValueType = ValueTypes.Text, + Group = Constants.PropertyEditors.Groups.RichContent, + Icon = "icon-code")] public class MarkdownPropertyEditor : DataEditor { /// diff --git a/src/Umbraco.Web/PropertyEditors/MediaPickerPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/MediaPickerPropertyEditor.cs index 52e616ffbd..dd755ee0ba 100644 --- a/src/Umbraco.Web/PropertyEditors/MediaPickerPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/MediaPickerPropertyEditor.cs @@ -7,8 +7,14 @@ namespace Umbraco.Web.PropertyEditors /// /// Represents a media picker property editor. /// - [DataEditor(Constants.PropertyEditors.Aliases.MediaPicker, EditorType.PropertyValue | EditorType.MacroParameter, - "Media Picker", "mediapicker", ValueType = ValueTypes.Text, Group = "media", Icon = Constants.Icons.MediaImage)] + [DataEditor( + Constants.PropertyEditors.Aliases.MediaPicker, + EditorType.PropertyValue | EditorType.MacroParameter, + "Media Picker", + "mediapicker", + ValueType = ValueTypes.Text, + Group = Constants.PropertyEditors.Groups.Media, + Icon = Constants.Icons.MediaImage)] public class MediaPickerPropertyEditor : DataEditor { /// diff --git a/src/Umbraco.Web/PropertyEditors/MemberGroupPickerPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/MemberGroupPickerPropertyEditor.cs index 5d89024692..b8e67c863c 100644 --- a/src/Umbraco.Web/PropertyEditors/MemberGroupPickerPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/MemberGroupPickerPropertyEditor.cs @@ -4,7 +4,13 @@ using Umbraco.Core.PropertyEditors; namespace Umbraco.Web.PropertyEditors { - [DataEditor(Constants.PropertyEditors.Aliases.MemberGroupPicker, "Member Group Picker", "membergrouppicker", ValueType = ValueTypes.Text, Group = "People", Icon = Constants.Icons.MemberGroup)] + [DataEditor( + Constants.PropertyEditors.Aliases.MemberGroupPicker, + "Member Group Picker", + "membergrouppicker", + ValueType = ValueTypes.Text, + Group = Constants.PropertyEditors.Groups.People, + Icon = Constants.Icons.MemberGroup)] public class MemberGroupPickerPropertyEditor : DataEditor { public MemberGroupPickerPropertyEditor(ILogger logger) diff --git a/src/Umbraco.Web/PropertyEditors/MemberPickerPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/MemberPickerPropertyEditor.cs index 858582ab72..69020ba350 100644 --- a/src/Umbraco.Web/PropertyEditors/MemberPickerPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/MemberPickerPropertyEditor.cs @@ -4,7 +4,13 @@ using Umbraco.Core.PropertyEditors; namespace Umbraco.Web.PropertyEditors { - [DataEditor(Constants.PropertyEditors.Aliases.MemberPicker, "Member Picker", "memberpicker", ValueType = ValueTypes.String, Group = "People", Icon = Constants.Icons.Member)] + [DataEditor( + Constants.PropertyEditors.Aliases.MemberPicker, + "Member Picker", + "memberpicker", + ValueType = ValueTypes.String, + Group = Constants.PropertyEditors.Groups.People, + Icon = Constants.Icons.Member)] public class MemberPickerPropertyEditor : DataEditor { public MemberPickerPropertyEditor(ILogger logger) diff --git a/src/Umbraco.Web/PropertyEditors/MultiNodeTreePickerPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/MultiNodeTreePickerPropertyEditor.cs index ad75e16717..742acbeca2 100644 --- a/src/Umbraco.Web/PropertyEditors/MultiNodeTreePickerPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/MultiNodeTreePickerPropertyEditor.cs @@ -4,7 +4,13 @@ using Umbraco.Core.PropertyEditors; namespace Umbraco.Web.PropertyEditors { - [DataEditor(Constants.PropertyEditors.Aliases.MultiNodeTreePicker, "Multinode Treepicker", "contentpicker", ValueType = ValueTypes.Text, Group = "pickers", Icon = "icon-page-add")] + [DataEditor( + Constants.PropertyEditors.Aliases.MultiNodeTreePicker, + "Multinode Treepicker", + "contentpicker", + ValueType = ValueTypes.Text, + Group = Constants.PropertyEditors.Groups.Pickers, + Icon = "icon-page-add")] public class MultiNodeTreePickerPropertyEditor : DataEditor { public MultiNodeTreePickerPropertyEditor(ILogger logger) diff --git a/src/Umbraco.Web/PropertyEditors/MultiUrlPickerPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/MultiUrlPickerPropertyEditor.cs index ea5d7aac8e..95ac809576 100644 --- a/src/Umbraco.Web/PropertyEditors/MultiUrlPickerPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/MultiUrlPickerPropertyEditor.cs @@ -7,7 +7,14 @@ using Umbraco.Web.PublishedCache; namespace Umbraco.Web.PropertyEditors { - [DataEditor(Constants.PropertyEditors.Aliases.MultiUrlPicker, EditorType.PropertyValue, "Multi Url Picker", "multiurlpicker", ValueType = ValueTypes.Json, Group = "pickers", Icon = "icon-link")] + [DataEditor( + Constants.PropertyEditors.Aliases.MultiUrlPicker, + EditorType.PropertyValue, + "Multi Url Picker", + "multiurlpicker", + ValueType = ValueTypes.Json, + Group = Constants.PropertyEditors.Groups.Pickers, + Icon = "icon-link")] public class MultiUrlPickerPropertyEditor : DataEditor { private readonly IEntityService _entityService; diff --git a/src/Umbraco.Web/PropertyEditors/MultipleTextStringPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/MultipleTextStringPropertyEditor.cs index 141bdea7b6..387fc670b8 100644 --- a/src/Umbraco.Web/PropertyEditors/MultipleTextStringPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/MultipleTextStringPropertyEditor.cs @@ -2,6 +2,7 @@ using System.Linq; using Newtonsoft.Json.Linq; using Umbraco.Core; +using Umbraco.Core.Exceptions; using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Models.Editors; @@ -13,7 +14,13 @@ namespace Umbraco.Web.PropertyEditors /// /// Represents a multiple text string property editor. /// - [DataEditor(Constants.PropertyEditors.Aliases.MultipleTextstring, "Repeatable textstrings", "multipletextbox", ValueType = ValueTypes.Text, Icon="icon-ordered-list", Group="lists")] + [DataEditor( + Constants.PropertyEditors.Aliases.MultipleTextstring, + "Repeatable textstrings", + "multipletextbox", + ValueType = ValueTypes.Text, + Group = Constants.PropertyEditors.Groups.Lists, + Icon = "icon-ordered-list")] public class MultipleTextStringPropertyEditor : DataEditor { /// @@ -56,7 +63,7 @@ namespace Umbraco.Web.PropertyEditors } if (!(editorValue.DataTypeConfiguration is MultipleTextStringConfiguration config)) - throw new Exception("panic"); + throw new PanicException($"editorValue.DataTypeConfiguration is {editorValue.DataTypeConfiguration.GetType()} but must be {typeof(MultipleTextStringConfiguration)}"); var max = config.Maximum; //The legacy property editor saved this data as new line delimited! strange but we have to maintain that. diff --git a/src/Umbraco.Web/PropertyEditors/NestedContentController.cs b/src/Umbraco.Web/PropertyEditors/NestedContentController.cs index 8d144c5904..590a286c5d 100644 --- a/src/Umbraco.Web/PropertyEditors/NestedContentController.cs +++ b/src/Umbraco.Web/PropertyEditors/NestedContentController.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using Umbraco.Core.Services; using Umbraco.Web.Editors; using Umbraco.Web.Mvc; @@ -11,8 +12,8 @@ namespace Umbraco.Web.PropertyEditors [System.Web.Http.HttpGet] public IEnumerable GetContentTypes() { - return Services.ContentTypeService.GetAll() - .Where(x => x.IsElement) + return Services.ContentTypeService + .GetAllElementTypes() .OrderBy(x => x.SortOrder) .Select(x => new { diff --git a/src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs index 6fcb7ea642..fb17e97519 100644 --- a/src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs @@ -18,7 +18,14 @@ namespace Umbraco.Web.PropertyEditors /// /// Represents a nested content property editor. /// - [DataEditor(Constants.PropertyEditors.Aliases.NestedContent, "Nested Content", "nestedcontent", ValueType = "JSON", Group = "lists", Icon = "icon-thumbnail-list", CanCopy = true)] + [DataEditor( + Constants.PropertyEditors.Aliases.NestedContent, + "Nested Content", + "nestedcontent", + ValueType = ValueTypes.Json, + Group = Constants.PropertyEditors.Groups.Lists, + Icon = "icon-thumbnail-list", + CanCopy = true)] public class NestedContentPropertyEditor : DataEditor { private readonly Lazy _propertyEditors; diff --git a/src/Umbraco.Web/PropertyEditors/ParameterEditors/ContentTypeParameterEditor.cs b/src/Umbraco.Web/PropertyEditors/ParameterEditors/ContentTypeParameterEditor.cs index cdab78d119..149243f122 100644 --- a/src/Umbraco.Web/PropertyEditors/ParameterEditors/ContentTypeParameterEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/ParameterEditors/ContentTypeParameterEditor.cs @@ -6,7 +6,11 @@ namespace Umbraco.Web.PropertyEditors.ParameterEditors /// /// Represents a content type parameter editor. /// - [DataEditor("contentType", EditorType.MacroParameter, "Content Type Picker", "entitypicker")] + [DataEditor( + "contentType", + EditorType.MacroParameter, + "Content Type Picker", + "entitypicker")] public class ContentTypeParameterEditor : DataEditor { /// diff --git a/src/Umbraco.Web/PropertyEditors/ParameterEditors/MultipleContentPickerParameterEditor.cs b/src/Umbraco.Web/PropertyEditors/ParameterEditors/MultipleContentPickerParameterEditor.cs index 3d0d0eb923..0ca657ac39 100644 --- a/src/Umbraco.Web/PropertyEditors/ParameterEditors/MultipleContentPickerParameterEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/ParameterEditors/MultipleContentPickerParameterEditor.cs @@ -7,7 +7,11 @@ namespace Umbraco.Web.PropertyEditors.ParameterEditors /// /// Represents a parameter editor of some sort. /// - [DataEditor(Constants.PropertyEditors.Aliases.MultiNodeTreePicker, EditorType.MacroParameter, "Multiple Content Picker", "contentpicker")] + [DataEditor( + Constants.PropertyEditors.Aliases.MultiNodeTreePicker, + EditorType.MacroParameter, + "Multiple Content Picker", + "contentpicker")] public class MultipleContentPickerParameterEditor : DataEditor { /// diff --git a/src/Umbraco.Web/PropertyEditors/ParameterEditors/MultipleContentTypeParameterEditor.cs b/src/Umbraco.Web/PropertyEditors/ParameterEditors/MultipleContentTypeParameterEditor.cs index 74bcafe30f..c8f19f8acd 100644 --- a/src/Umbraco.Web/PropertyEditors/ParameterEditors/MultipleContentTypeParameterEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/ParameterEditors/MultipleContentTypeParameterEditor.cs @@ -3,7 +3,11 @@ using Umbraco.Core.PropertyEditors; namespace Umbraco.Web.PropertyEditors.ParameterEditors { - [DataEditor("contentTypeMultiple", EditorType.MacroParameter, "Multiple Content Type Picker", "entitypicker")] + [DataEditor( + "contentTypeMultiple", + EditorType.MacroParameter, + "Multiple Content Type Picker", + "entitypicker")] public class MultipleContentTypeParameterEditor : DataEditor { public MultipleContentTypeParameterEditor(ILogger logger) diff --git a/src/Umbraco.Web/PropertyEditors/ParameterEditors/MultipleMediaPickerParameterEditor.cs b/src/Umbraco.Web/PropertyEditors/ParameterEditors/MultipleMediaPickerParameterEditor.cs new file mode 100644 index 0000000000..1208a5eecc --- /dev/null +++ b/src/Umbraco.Web/PropertyEditors/ParameterEditors/MultipleMediaPickerParameterEditor.cs @@ -0,0 +1,27 @@ +using Umbraco.Core; +using Umbraco.Core.Logging; +using Umbraco.Core.PropertyEditors; + +namespace Umbraco.Web.PropertyEditors.ParameterEditors +{ + /// + /// Represents a multiple media picker macro parameter editor. + /// + [DataEditor( + Constants.PropertyEditors.Aliases.MultipleMediaPicker, + EditorType.MacroParameter, + "Multiple Media Picker", + "mediapicker", + ValueType = ValueTypes.Text)] + public class MultipleMediaPickerParameterEditor : DataEditor + { + /// + /// Initializes a new instance of the class. + /// + public MultipleMediaPickerParameterEditor(ILogger logger) + : base(logger) + { + DefaultConfiguration.Add("multiPicker", "1"); + } + } +} diff --git a/src/Umbraco.Web/PropertyEditors/ParameterEditors/MultiplePropertyGroupParameterEditor.cs b/src/Umbraco.Web/PropertyEditors/ParameterEditors/MultiplePropertyGroupParameterEditor.cs index 87ddfb0b54..2a7079d578 100644 --- a/src/Umbraco.Web/PropertyEditors/ParameterEditors/MultiplePropertyGroupParameterEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/ParameterEditors/MultiplePropertyGroupParameterEditor.cs @@ -3,7 +3,11 @@ using Umbraco.Core.PropertyEditors; namespace Umbraco.Web.PropertyEditors.ParameterEditors { - [DataEditor("tabPickerMultiple", EditorType.MacroParameter, "Multiple Tab Picker", "entitypicker")] + [DataEditor( + "tabPickerMultiple", + EditorType.MacroParameter, + "Multiple Tab Picker", + "entitypicker")] public class MultiplePropertyGroupParameterEditor : DataEditor { public MultiplePropertyGroupParameterEditor(ILogger logger) diff --git a/src/Umbraco.Web/PropertyEditors/ParameterEditors/MultiplePropertyTypeParameterEditor.cs b/src/Umbraco.Web/PropertyEditors/ParameterEditors/MultiplePropertyTypeParameterEditor.cs index 124325dbd7..fe1432a655 100644 --- a/src/Umbraco.Web/PropertyEditors/ParameterEditors/MultiplePropertyTypeParameterEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/ParameterEditors/MultiplePropertyTypeParameterEditor.cs @@ -3,7 +3,11 @@ using Umbraco.Core.PropertyEditors; namespace Umbraco.Web.PropertyEditors.ParameterEditors { - [DataEditor("propertyTypePickerMultiple", EditorType.MacroParameter, "Multiple Property Type Picker", "entitypicker")] + [DataEditor( + "propertyTypePickerMultiple", + EditorType.MacroParameter, + "Multiple Property Type Picker", + "entitypicker")] public class MultiplePropertyTypeParameterEditor : DataEditor { public MultiplePropertyTypeParameterEditor(ILogger logger) diff --git a/src/Umbraco.Web/PropertyEditors/ParameterEditors/PropertyGroupParameterEditor.cs b/src/Umbraco.Web/PropertyEditors/ParameterEditors/PropertyGroupParameterEditor.cs index 76fa34341d..65ce595852 100644 --- a/src/Umbraco.Web/PropertyEditors/ParameterEditors/PropertyGroupParameterEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/ParameterEditors/PropertyGroupParameterEditor.cs @@ -3,7 +3,11 @@ using Umbraco.Core.PropertyEditors; namespace Umbraco.Web.PropertyEditors.ParameterEditors { - [DataEditor("tabPicker", EditorType.MacroParameter, "Tab Picker", "entitypicker")] + [DataEditor( + "tabPicker", + EditorType.MacroParameter, + "Tab Picker", + "entitypicker")] public class PropertyGroupParameterEditor : DataEditor { public PropertyGroupParameterEditor(ILogger logger) diff --git a/src/Umbraco.Web/PropertyEditors/ParameterEditors/PropertyTypeParameterEditor.cs b/src/Umbraco.Web/PropertyEditors/ParameterEditors/PropertyTypeParameterEditor.cs index b84749da27..4ea7c0ebdc 100644 --- a/src/Umbraco.Web/PropertyEditors/ParameterEditors/PropertyTypeParameterEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/ParameterEditors/PropertyTypeParameterEditor.cs @@ -3,7 +3,11 @@ using Umbraco.Core.PropertyEditors; namespace Umbraco.Web.PropertyEditors.ParameterEditors { - [DataEditor("propertyTypePicker", EditorType.MacroParameter, "Property Type Picker", "entitypicker")] + [DataEditor( + "propertyTypePicker", + EditorType.MacroParameter, + "Property Type Picker", + "entitypicker")] public class PropertyTypeParameterEditor : DataEditor { public PropertyTypeParameterEditor(ILogger logger) diff --git a/src/Umbraco.Web/PropertyEditors/RadioButtonsPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/RadioButtonsPropertyEditor.cs index 601728189c..f729decd2c 100644 --- a/src/Umbraco.Web/PropertyEditors/RadioButtonsPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/RadioButtonsPropertyEditor.cs @@ -8,7 +8,13 @@ namespace Umbraco.Web.PropertyEditors /// /// A property editor to allow the individual selection of pre-defined items. /// - [DataEditor(Constants.PropertyEditors.Aliases.RadioButtonList, "Radio button list", "radiobuttons", ValueType = ValueTypes.String, Group="lists", Icon="icon-target")] + [DataEditor( + Constants.PropertyEditors.Aliases.RadioButtonList, + "Radio button list", + "radiobuttons", + ValueType = ValueTypes.String, + Group = Constants.PropertyEditors.Groups.Lists, + Icon = "icon-target")] public class RadioButtonsPropertyEditor : DataEditor { private readonly ILocalizedTextService _textService; diff --git a/src/Umbraco.Web/PropertyEditors/RichTextPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/RichTextPropertyEditor.cs index 297f9b7fb8..3ced379604 100644 --- a/src/Umbraco.Web/PropertyEditors/RichTextPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/RichTextPropertyEditor.cs @@ -15,7 +15,14 @@ namespace Umbraco.Web.PropertyEditors /// /// Represents a rich text property editor. /// - [DataEditor(Constants.PropertyEditors.Aliases.TinyMce, "Rich Text Editor", "rte", ValueType = ValueTypes.Text, HideLabel = false, Group="Rich Content", Icon="icon-browser-window")] + [DataEditor( + Constants.PropertyEditors.Aliases.TinyMce, + "Rich Text Editor", + "rte", + ValueType = ValueTypes.Text, + HideLabel = false, + Group = Constants.PropertyEditors.Groups.RichContent, + Icon = "icon-browser-window")] public class RichTextPropertyEditor : DataEditor { /// diff --git a/src/Umbraco.Web/PropertyEditors/SliderPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/SliderPropertyEditor.cs index c4a966cf49..8a98f15081 100644 --- a/src/Umbraco.Web/PropertyEditors/SliderPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/SliderPropertyEditor.cs @@ -7,7 +7,11 @@ namespace Umbraco.Web.PropertyEditors /// /// Represents a slider editor. /// - [DataEditor(Constants.PropertyEditors.Aliases.Slider, "Slider", "slider", Icon = "icon-navigation-horizontal")] + [DataEditor( + Constants.PropertyEditors.Aliases.Slider, + "Slider", + "slider", + Icon = "icon-navigation-horizontal")] public class SliderPropertyEditor : DataEditor { /// diff --git a/src/Umbraco.Web/PropertyEditors/TagsPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/TagsPropertyEditor.cs index 578b6fcd00..fd7e8694a3 100644 --- a/src/Umbraco.Web/PropertyEditors/TagsPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/TagsPropertyEditor.cs @@ -15,7 +15,11 @@ namespace Umbraco.Web.PropertyEditors /// Represents a tags property editor. /// [TagsPropertyEditor] - [DataEditor(Constants.PropertyEditors.Aliases.Tags, "Tags", "tags", Icon="icon-tags")] + [DataEditor( + Constants.PropertyEditors.Aliases.Tags, + "Tags", + "tags", + Icon = "icon-tags")] public class TagsPropertyEditor : DataEditor { private readonly ManifestValueValidatorCollection _validators; diff --git a/src/Umbraco.Web/PropertyEditors/TextAreaPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/TextAreaPropertyEditor.cs index f9ba99fd1e..c7bc2efbda 100644 --- a/src/Umbraco.Web/PropertyEditors/TextAreaPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/TextAreaPropertyEditor.cs @@ -7,7 +7,13 @@ namespace Umbraco.Web.PropertyEditors /// /// Represents a textarea property and parameter editor. /// - [DataEditor(Constants.PropertyEditors.Aliases.TextArea, EditorType.PropertyValue | EditorType.MacroParameter, "Textarea", "textarea", ValueType = ValueTypes.Text, Icon="icon-application-window-alt")] + [DataEditor( + Constants.PropertyEditors.Aliases.TextArea, + EditorType.PropertyValue | EditorType.MacroParameter, + "Textarea", + "textarea", + ValueType = ValueTypes.Text, + Icon = "icon-application-window-alt")] public class TextAreaPropertyEditor : DataEditor { /// diff --git a/src/Umbraco.Web/PropertyEditors/TextboxPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/TextboxPropertyEditor.cs index 4862f6c9aa..18f8155198 100644 --- a/src/Umbraco.Web/PropertyEditors/TextboxPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/TextboxPropertyEditor.cs @@ -7,7 +7,12 @@ namespace Umbraco.Web.PropertyEditors /// /// Represents a textbox property and parameter editor. /// - [DataEditor(Constants.PropertyEditors.Aliases.TextBox, EditorType.PropertyValue | EditorType.MacroParameter, "Textbox", "textbox", Group = "Common")] + [DataEditor( + Constants.PropertyEditors.Aliases.TextBox, + EditorType.PropertyValue | EditorType.MacroParameter, + "Textbox", + "textbox", + Group = Constants.PropertyEditors.Groups.Common)] public class TextboxPropertyEditor : DataEditor { /// diff --git a/src/Umbraco.Web/PropertyEditors/TrueFalsePropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/TrueFalsePropertyEditor.cs index ccd3bee744..fb2d2b29fe 100644 --- a/src/Umbraco.Web/PropertyEditors/TrueFalsePropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/TrueFalsePropertyEditor.cs @@ -7,7 +7,14 @@ namespace Umbraco.Web.PropertyEditors /// /// Represents a checkbox property and parameter editor. /// - [DataEditor(Constants.PropertyEditors.Aliases.Boolean, EditorType.PropertyValue | EditorType.MacroParameter, "Checkbox", "boolean", ValueType = ValueTypes.Integer, Group = "Common", Icon="icon-checkbox")] + [DataEditor( + Constants.PropertyEditors.Aliases.Boolean, + EditorType.PropertyValue | EditorType.MacroParameter, + "Checkbox", + "boolean", + ValueType = ValueTypes.Integer, + Group = Constants.PropertyEditors.Groups.Common, + Icon = "icon-checkbox")] public class TrueFalsePropertyEditor : DataEditor { /// diff --git a/src/Umbraco.Web/PropertyEditors/UserPickerPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/UserPickerPropertyEditor.cs index 1d3ab05e96..daf574719a 100644 --- a/src/Umbraco.Web/PropertyEditors/UserPickerPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/UserPickerPropertyEditor.cs @@ -6,7 +6,13 @@ using Umbraco.Core.PropertyEditors; namespace Umbraco.Web.PropertyEditors { - [DataEditor(Constants.PropertyEditors.Aliases.UserPicker, "User picker", "entitypicker", ValueType = ValueTypes.Integer, Group = "People", Icon = Constants.Icons.User)] + [DataEditor( + Constants.PropertyEditors.Aliases.UserPicker, + "User picker", + "entitypicker", + ValueType = ValueTypes.Integer, + Group = Constants.PropertyEditors.Groups.People, + Icon = Constants.Icons.User)] public class UserPickerPropertyEditor : DataEditor { public UserPickerPropertyEditor(ILogger logger) diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/MediaPickerValueConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/MediaPickerValueConverter.cs index d2272a25b5..a56630d7c5 100644 --- a/src/Umbraco.Web/PropertyEditors/ValueConverters/MediaPickerValueConverter.cs +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/MediaPickerValueConverter.cs @@ -37,14 +37,8 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters public override Type GetPropertyValueType(IPublishedPropertyType propertyType) { var isMultiple = IsMultipleDataType(propertyType.DataType); - var isOnlyImages = IsOnlyImagesDataType(propertyType.DataType); - return isMultiple - ? isOnlyImages - ? typeof(IEnumerable<>).MakeGenericType(ModelType.For(ImageTypeAlias)) - : typeof(IEnumerable) - : isOnlyImages - ? ModelType.For(ImageTypeAlias) + ? typeof(IEnumerable) : typeof(IPublishedContent); } @@ -57,12 +51,6 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters return config.Multiple; } - private bool IsOnlyImagesDataType(PublishedDataType dataType) - { - var config = ConfigurationEditor.ConfigurationAs(dataType.Configuration); - return config.OnlyImages; - } - public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object source, bool preview) { @@ -79,12 +67,9 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters PropertyCacheLevel cacheLevel, object source, bool preview) { var isMultiple = IsMultipleDataType(propertyType.DataType); - var isOnlyImages = IsOnlyImagesDataType(propertyType.DataType); var udis = (Udi[]) source; - var mediaItems = isOnlyImages - ? _publishedModelFactory.CreateModelList(ImageTypeAlias) - : new List(); + var mediaItems = new List(); if (source == null) return isMultiple ? mediaItems : null; diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/RteMacroRenderingValueConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/RteMacroRenderingValueConverter.cs index cb6ce6dd6d..d5e1f841ea 100644 --- a/src/Umbraco.Web/PropertyEditors/ValueConverters/RteMacroRenderingValueConverter.cs +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/RteMacroRenderingValueConverter.cs @@ -11,6 +11,7 @@ using Umbraco.Core.Cache; using Umbraco.Core.Services; using Umbraco.Web.Composing; using Umbraco.Web.Macros; +using System.Web; namespace Umbraco.Web.PropertyEditors.ValueConverters { @@ -63,7 +64,14 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters } } - public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object source, bool preview) + public override object ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) + { + var converted = Convert(inter, preview); + + return new HtmlString(converted == null ? string.Empty : converted); + } + + private string Convert(object source, bool preview) { if (source == null) { diff --git a/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs b/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs index 2d501fa3b5..1bd58c3878 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs @@ -6,6 +6,7 @@ using System.Threading; using System.Threading.Tasks; using CSharpTest.Net.Collections; using Umbraco.Core; +using Umbraco.Core.Exceptions; using Umbraco.Core.Logging; using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.Scoping; @@ -711,9 +712,9 @@ namespace Umbraco.Web.PublishedCache.NuCache var id = content.FirstChildContentId; while (id > 0) { - var link = GetLinkedNode(id, "child"); - ClearBranchLocked(link.Value); - id = link.Value.NextSiblingContentId; + var child = GetLinkedNode(id, "child").Value; + ClearBranchLocked(child); + id = child.NextSiblingContentId; } } @@ -1044,7 +1045,7 @@ namespace Umbraco.Web.PublishedCache.NuCache if (_genObj == null) _genObjs.Enqueue(_genObj = new GenObj(snapGen)); else if (_genObj.Gen != snapGen) - throw new Exception("panic"); + throw new PanicException($"The generation {_genObj.Gen} does not equal the snapshot generation {snapGen}"); } else { diff --git a/src/Umbraco.Web/PublishedCache/NuCache/SnapDictionary.cs b/src/Umbraco.Web/PublishedCache/NuCache/SnapDictionary.cs index c5b1df1206..9671949ff0 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/SnapDictionary.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/SnapDictionary.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Umbraco.Core.Exceptions; using Umbraco.Core.Scoping; using Umbraco.Web.PublishedCache.NuCache.Snap; @@ -371,7 +372,7 @@ namespace Umbraco.Web.PublishedCache.NuCache // if we have one already, ensure it's consistent else if (_genObj.Gen != snapGen) - throw new Exception("panic"); + throw new PanicException($"The generation {_genObj.Gen} does not equal the snapshot generation {snapGen}"); } else { diff --git a/src/Umbraco.Web/PublishedContentExtensions.cs b/src/Umbraco.Web/PublishedContentExtensions.cs index d7283a1e90..d15d5817f6 100644 --- a/src/Umbraco.Web/PublishedContentExtensions.cs +++ b/src/Umbraco.Web/PublishedContentExtensions.cs @@ -1067,7 +1067,7 @@ namespace Umbraco.Web /// /// Note that in V7 this method also return the content node self. /// - public static IEnumerable Siblings(this IPublishedContent content, string culture = null) + public static IEnumerable Siblings(this IPublishedContent content, string culture = null) where T : class, IPublishedContent { return SiblingsAndSelf(content, culture).Where(x => x.Id != content.Id); diff --git a/src/Umbraco.Web/Runtime/WebFinalComponent.cs b/src/Umbraco.Web/Runtime/WebFinalComponent.cs index 6177d9b868..2ba78b2080 100644 --- a/src/Umbraco.Web/Runtime/WebFinalComponent.cs +++ b/src/Umbraco.Web/Runtime/WebFinalComponent.cs @@ -1,39 +1,18 @@ using System; -using System.Linq; -using System.Web.Helpers; using System.Web.Http; -using System.Web.Mvc; -using System.Web.Routing; -using Umbraco.Core; using Umbraco.Core.Composing; -using Umbraco.Core.Configuration; -using Umbraco.Web.Install; -using Umbraco.Web.Mvc; -using Umbraco.Web.Security; -using Umbraco.Web.WebApi; namespace Umbraco.Web.Runtime { public class WebFinalComponent : IComponent { - private readonly IUmbracoContextAccessor _umbracoContextAccessor; - private readonly SurfaceControllerTypeCollection _surfaceControllerTypes; - private readonly UmbracoApiControllerTypeCollection _apiControllerTypes; - private readonly IGlobalSettings _globalSettings; - public WebFinalComponent(IUmbracoContextAccessor umbracoContextAccessor, SurfaceControllerTypeCollection surfaceControllerTypes, UmbracoApiControllerTypeCollection apiControllerTypes, IGlobalSettings globalSettings) + public WebFinalComponent() { - _umbracoContextAccessor = umbracoContextAccessor; - _surfaceControllerTypes = surfaceControllerTypes; - _apiControllerTypes = apiControllerTypes; - _globalSettings = globalSettings; } public void Initialize() { - // set routes - CreateRoutes(_umbracoContextAccessor, _globalSettings, _surfaceControllerTypes, _apiControllerTypes); - // ensure WebAPI is initialized, after everything GlobalConfiguration.Configuration.EnsureInitialized(); } @@ -41,92 +20,5 @@ namespace Umbraco.Web.Runtime public void Terminate() { } - // internal for tests - internal static void CreateRoutes( - IUmbracoContextAccessor umbracoContextAccessor, - IGlobalSettings globalSettings, - SurfaceControllerTypeCollection surfaceControllerTypes, - UmbracoApiControllerTypeCollection apiControllerTypes) - { - var umbracoPath = globalSettings.GetUmbracoMvcArea(); - - // create the front-end route - var defaultRoute = RouteTable.Routes.MapRoute( - "Umbraco_default", - umbracoPath + "/RenderMvc/{action}/{id}", - new { controller = "RenderMvc", action = "Index", id = UrlParameter.Optional } - ); - defaultRoute.RouteHandler = new RenderRouteHandler(umbracoContextAccessor, ControllerBuilder.Current.GetControllerFactory()); - - // register install routes - RouteTable.Routes.RegisterArea(); - - // register all back office routes - RouteTable.Routes.RegisterArea(new BackOfficeArea(globalSettings)); - - // plugin controllers must come first because the next route will catch many things - RoutePluginControllers(globalSettings, surfaceControllerTypes, apiControllerTypes); - } - - private static void RoutePluginControllers( - IGlobalSettings globalSettings, - SurfaceControllerTypeCollection surfaceControllerTypes, - UmbracoApiControllerTypeCollection apiControllerTypes) - { - var umbracoPath = globalSettings.GetUmbracoMvcArea(); - - // need to find the plugin controllers and route them - var pluginControllers = surfaceControllerTypes.Concat(apiControllerTypes).ToArray(); - - // local controllers do not contain the attribute - var localControllers = pluginControllers.Where(x => PluginController.GetMetadata(x).AreaName.IsNullOrWhiteSpace()); - foreach (var s in localControllers) - { - if (TypeHelper.IsTypeAssignableFrom(s)) - RouteLocalSurfaceController(s, umbracoPath); - else if (TypeHelper.IsTypeAssignableFrom(s)) - RouteLocalApiController(s, umbracoPath); - } - - // get the plugin controllers that are unique to each area (group by) - var pluginSurfaceControlleres = pluginControllers.Where(x => PluginController.GetMetadata(x).AreaName.IsNullOrWhiteSpace() == false); - var groupedAreas = pluginSurfaceControlleres.GroupBy(controller => PluginController.GetMetadata(controller).AreaName); - // loop through each area defined amongst the controllers - foreach (var g in groupedAreas) - { - // create & register an area for the controllers (this will throw an exception if all controllers are not in the same area) - var pluginControllerArea = new PluginControllerArea(globalSettings, g.Select(PluginController.GetMetadata)); - RouteTable.Routes.RegisterArea(pluginControllerArea); - } - } - - private static void RouteLocalApiController(Type controller, string umbracoPath) - { - var meta = PluginController.GetMetadata(controller); - var url = umbracoPath + (meta.IsBackOffice ? "/BackOffice" : "") + "/Api/" + meta.ControllerName + "/{action}/{id}"; - var route = RouteTable.Routes.MapHttpRoute( - $"umbraco-api-{meta.ControllerName}", - url, // url to match - new { controller = meta.ControllerName, id = UrlParameter.Optional }, - new[] { meta.ControllerNamespace }); - if (route.DataTokens == null) // web api routes don't set the data tokens object - route.DataTokens = new RouteValueDictionary(); - route.DataTokens.Add(Core.Constants.Web.UmbracoDataToken, "api"); //ensure the umbraco token is set - } - - private static void RouteLocalSurfaceController(Type controller, string umbracoPath) - { - var meta = PluginController.GetMetadata(controller); - var url = umbracoPath + "/Surface/" + meta.ControllerName + "/{action}/{id}"; - var route = RouteTable.Routes.MapRoute( - $"umbraco-surface-{meta.ControllerName}", - url, // url to match - new { controller = meta.ControllerName, action = "Index", id = UrlParameter.Optional }, - new[] { meta.ControllerNamespace }); // look in this namespace to create the controller - route.DataTokens.Add(Core.Constants.Web.UmbracoDataToken, "surface"); // ensure the umbraco token is set - route.DataTokens.Add("UseNamespaceFallback", false); // don't look anywhere else except this namespace! - // make it use our custom/special SurfaceMvcHandler - route.RouteHandler = new SurfaceRouteHandler(); - } } } diff --git a/src/Umbraco.Web/Runtime/WebInitialComponent.cs b/src/Umbraco.Web/Runtime/WebInitialComponent.cs index e1e0f4d80a..ac813d7196 100644 --- a/src/Umbraco.Web/Runtime/WebInitialComponent.cs +++ b/src/Umbraco.Web/Runtime/WebInitialComponent.cs @@ -8,12 +8,14 @@ using System.Web.Configuration; using System.Web.Http; using System.Web.Http.Dispatcher; using System.Web.Mvc; +using System.Web.Routing; using ClientDependency.Core.CompositeFiles.Providers; using ClientDependency.Core.Config; using Umbraco.Core; using Umbraco.Core.Composing; using Umbraco.Core.Configuration; using Umbraco.Core.IO; +using Umbraco.Web.Install; using Umbraco.Web.JavaScript; using Umbraco.Web.Mvc; using Umbraco.Web.WebApi; @@ -24,10 +26,16 @@ namespace Umbraco.Web.Runtime { public sealed class WebInitialComponent : IComponent { + private readonly IUmbracoContextAccessor _umbracoContextAccessor; + private readonly SurfaceControllerTypeCollection _surfaceControllerTypes; + private readonly UmbracoApiControllerTypeCollection _apiControllerTypes; private readonly IGlobalSettings _globalSettings; - public WebInitialComponent(IGlobalSettings globalSettings) + public WebInitialComponent(IUmbracoContextAccessor umbracoContextAccessor, SurfaceControllerTypeCollection surfaceControllerTypes, UmbracoApiControllerTypeCollection apiControllerTypes, IGlobalSettings globalSettings) { + _umbracoContextAccessor = umbracoContextAccessor; + _surfaceControllerTypes = surfaceControllerTypes; + _apiControllerTypes = apiControllerTypes; _globalSettings = globalSettings; } @@ -51,6 +59,9 @@ namespace Umbraco.Web.Runtime // add global filters ConfigureGlobalFilters(); + + // set routes + CreateRoutes(_umbracoContextAccessor, _globalSettings, _surfaceControllerTypes, _apiControllerTypes); } public void Terminate() @@ -131,5 +142,94 @@ namespace Umbraco.Web.Runtime ClientDependencySettings.Instance.MvcRendererCollection.Add(renderer); } + + // internal for tests + internal static void CreateRoutes( + IUmbracoContextAccessor umbracoContextAccessor, + IGlobalSettings globalSettings, + SurfaceControllerTypeCollection surfaceControllerTypes, + UmbracoApiControllerTypeCollection apiControllerTypes) + { + var umbracoPath = globalSettings.GetUmbracoMvcArea(); + + // create the front-end route + var defaultRoute = RouteTable.Routes.MapRoute( + "Umbraco_default", + umbracoPath + "/RenderMvc/{action}/{id}", + new { controller = "RenderMvc", action = "Index", id = UrlParameter.Optional } + ); + defaultRoute.RouteHandler = new RenderRouteHandler(umbracoContextAccessor, ControllerBuilder.Current.GetControllerFactory()); + + // register install routes + RouteTable.Routes.RegisterArea(); + + // register all back office routes + RouteTable.Routes.RegisterArea(new BackOfficeArea(globalSettings)); + + // plugin controllers must come first because the next route will catch many things + RoutePluginControllers(globalSettings, surfaceControllerTypes, apiControllerTypes); + } + + private static void RoutePluginControllers( + IGlobalSettings globalSettings, + SurfaceControllerTypeCollection surfaceControllerTypes, + UmbracoApiControllerTypeCollection apiControllerTypes) + { + var umbracoPath = globalSettings.GetUmbracoMvcArea(); + + // need to find the plugin controllers and route them + var pluginControllers = surfaceControllerTypes.Concat(apiControllerTypes).ToArray(); + + // local controllers do not contain the attribute + var localControllers = pluginControllers.Where(x => PluginController.GetMetadata(x).AreaName.IsNullOrWhiteSpace()); + foreach (var s in localControllers) + { + if (TypeHelper.IsTypeAssignableFrom(s)) + RouteLocalSurfaceController(s, umbracoPath); + else if (TypeHelper.IsTypeAssignableFrom(s)) + RouteLocalApiController(s, umbracoPath); + } + + // get the plugin controllers that are unique to each area (group by) + var pluginSurfaceControlleres = pluginControllers.Where(x => PluginController.GetMetadata(x).AreaName.IsNullOrWhiteSpace() == false); + var groupedAreas = pluginSurfaceControlleres.GroupBy(controller => PluginController.GetMetadata(controller).AreaName); + // loop through each area defined amongst the controllers + foreach (var g in groupedAreas) + { + // create & register an area for the controllers (this will throw an exception if all controllers are not in the same area) + var pluginControllerArea = new PluginControllerArea(globalSettings, g.Select(PluginController.GetMetadata)); + RouteTable.Routes.RegisterArea(pluginControllerArea); + } + } + + private static void RouteLocalApiController(Type controller, string umbracoPath) + { + var meta = PluginController.GetMetadata(controller); + var url = umbracoPath + (meta.IsBackOffice ? "/BackOffice" : "") + "/Api/" + meta.ControllerName + "/{action}/{id}"; + var route = RouteTable.Routes.MapHttpRoute( + $"umbraco-api-{meta.ControllerName}", + url, // url to match + new { controller = meta.ControllerName, id = UrlParameter.Optional }, + new[] { meta.ControllerNamespace }); + if (route.DataTokens == null) // web api routes don't set the data tokens object + route.DataTokens = new RouteValueDictionary(); + route.DataTokens.Add(Core.Constants.Web.UmbracoDataToken, "api"); //ensure the umbraco token is set + } + + private static void RouteLocalSurfaceController(Type controller, string umbracoPath) + { + var meta = PluginController.GetMetadata(controller); + var url = umbracoPath + "/Surface/" + meta.ControllerName + "/{action}/{id}"; + var route = RouteTable.Routes.MapRoute( + $"umbraco-surface-{meta.ControllerName}", + url, // url to match + new { controller = meta.ControllerName, action = "Index", id = UrlParameter.Optional }, + new[] { meta.ControllerNamespace }); // look in this namespace to create the controller + route.DataTokens.Add(Core.Constants.Web.UmbracoDataToken, "surface"); // ensure the umbraco token is set + route.DataTokens.Add("UseNamespaceFallback", false); // don't look anywhere else except this namespace! + // make it use our custom/special SurfaceMvcHandler + route.RouteHandler = new SurfaceRouteHandler(); + } + } } diff --git a/src/Umbraco.Web/Search/BackgroundIndexRebuilder.cs b/src/Umbraco.Web/Search/BackgroundIndexRebuilder.cs new file mode 100644 index 0000000000..0ae5ceade9 --- /dev/null +++ b/src/Umbraco.Web/Search/BackgroundIndexRebuilder.cs @@ -0,0 +1,123 @@ +using System; +using System.Threading; +using Umbraco.Core.Logging; +using Umbraco.Examine; +using System.Threading.Tasks; +using Umbraco.Core; +using Umbraco.Web.Scheduling; + +namespace Umbraco.Web.Search +{ + /// + /// Utility to rebuild all indexes on a background thread + /// + public sealed class BackgroundIndexRebuilder + { + private static readonly object RebuildLocker = new object(); + private readonly IndexRebuilder _indexRebuilder; + private readonly IMainDom _mainDom; + private readonly IProfilingLogger _logger; + private static BackgroundTaskRunner _rebuildOnStartupRunner; + + public BackgroundIndexRebuilder(IMainDom mainDom, IProfilingLogger logger, IndexRebuilder indexRebuilder) + { + _mainDom = mainDom; + _logger = logger; + _indexRebuilder = indexRebuilder; + } + + /// + /// Called to rebuild empty indexes on startup + /// + /// + /// + /// + /// + public void RebuildIndexes(bool onlyEmptyIndexes, int waitMilliseconds = 0) + { + // TODO: need a way to disable rebuilding on startup + + lock (RebuildLocker) + { + if (_rebuildOnStartupRunner != null && _rebuildOnStartupRunner.IsRunning) + { + _logger.Warn("Call was made to RebuildIndexes but the task runner for rebuilding is already running"); + return; + } + + _logger.Info("Starting initialize async background thread."); + //do the rebuild on a managed background thread + var task = new RebuildOnStartupTask(_mainDom, _indexRebuilder, _logger, onlyEmptyIndexes, waitMilliseconds); + + _rebuildOnStartupRunner = new BackgroundTaskRunner( + "RebuildIndexesOnStartup", + _logger); + + _rebuildOnStartupRunner.TryAdd(task); + } + } + + /// + /// Background task used to rebuild empty indexes on startup + /// + private class RebuildOnStartupTask : IBackgroundTask + { + private readonly IMainDom _mainDom; + + private readonly IndexRebuilder _indexRebuilder; + private readonly ILogger _logger; + private readonly bool _onlyEmptyIndexes; + private readonly int _waitMilliseconds; + + public RebuildOnStartupTask(IMainDom mainDom, + IndexRebuilder indexRebuilder, ILogger logger, bool onlyEmptyIndexes, int waitMilliseconds = 0) + { + _mainDom = mainDom; + _indexRebuilder = indexRebuilder ?? throw new ArgumentNullException(nameof(indexRebuilder)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _onlyEmptyIndexes = onlyEmptyIndexes; + _waitMilliseconds = waitMilliseconds; + } + + public bool IsAsync => false; + + public void Dispose() + { + } + + public void Run() + { + try + { + // rebuilds indexes + RebuildIndexes(); + } + catch (Exception ex) + { + _logger.Error(ex, "Failed to rebuild empty indexes."); + } + } + + public Task RunAsync(CancellationToken token) + { + throw new NotImplementedException(); + } + + /// + /// Used to rebuild indexes on startup or cold boot + /// + private void RebuildIndexes() + { + //do not attempt to do this if this has been disabled since we are not the main dom. + //this can be called during a cold boot + if (!_mainDom.IsMainDom) return; + + if (_waitMilliseconds > 0) + Thread.Sleep(_waitMilliseconds); + + _indexRebuilder.ExamineManager.ConfigureIndexes(_mainDom, _logger); + _indexRebuilder.RebuildIndexes(_onlyEmptyIndexes); + } + } + } +} diff --git a/src/Umbraco.Web/Search/ExamineComponent.cs b/src/Umbraco.Web/Search/ExamineComponent.cs index ed248a9e24..f34b1f862b 100644 --- a/src/Umbraco.Web/Search/ExamineComponent.cs +++ b/src/Umbraco.Web/Search/ExamineComponent.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; -using System.Threading; using Examine; using Umbraco.Core; using Umbraco.Core.Cache; @@ -15,42 +14,36 @@ using Umbraco.Core.Sync; using Umbraco.Web.Cache; using Umbraco.Examine; using Umbraco.Core.Persistence.DatabaseModelDefinitions; -using Umbraco.Web.Scheduling; -using System.Threading.Tasks; using Examine.LuceneEngine.Directories; using Umbraco.Core.Composing; +using System.ComponentModel; namespace Umbraco.Web.Search { - public sealed class ExamineComponent : IComponent + public sealed class ExamineComponent : Umbraco.Core.Composing.IComponent { private readonly IExamineManager _examineManager; private readonly IContentValueSetBuilder _contentValueSetBuilder; private readonly IPublishedContentValueSetBuilder _publishedContentValueSetBuilder; private readonly IValueSetBuilder _mediaValueSetBuilder; private readonly IValueSetBuilder _memberValueSetBuilder; - private static bool _disableExamineIndexing = false; - private static bool _isConfigured = false; - private static object _configuredInit = null; private static object _isConfiguredLocker = new object(); private readonly IScopeProvider _scopeProvider; - private readonly ServiceContext _services; - private static BackgroundTaskRunner _rebuildOnStartupRunner; - private static readonly object RebuildLocker = new object(); + private readonly ServiceContext _services; private readonly IMainDom _mainDom; private readonly IProfilingLogger _logger; private readonly IUmbracoIndexesCreator _indexCreator; - private readonly IndexRebuilder _indexRebuilder; + // the default enlist priority is 100 // enlist with a lower priority to ensure that anything "default" runs after us // but greater that SafeXmlReaderWriter priority which is 60 private const int EnlistPriority = 80; - + public ExamineComponent(IMainDom mainDom, IExamineManager examineManager, IProfilingLogger profilingLogger, IScopeProvider scopeProvider, IUmbracoIndexesCreator indexCreator, - IndexRebuilder indexRebuilder, ServiceContext services, + ServiceContext services, IContentValueSetBuilder contentValueSetBuilder, IPublishedContentValueSetBuilder publishedContentValueSetBuilder, IValueSetBuilder mediaValueSetBuilder, @@ -67,7 +60,6 @@ namespace Umbraco.Web.Search _mainDom = mainDom; _logger = profilingLogger; _indexCreator = indexCreator; - _indexRebuilder = indexRebuilder; } public void Initialize() @@ -96,7 +88,6 @@ namespace Umbraco.Web.Search //if we could not register the shutdown examine ourselves, it means we are not maindom! in this case all of examine should be disabled! Suspendable.ExamineEvents.SuspendIndexers(_logger); - _disableExamineIndexing = true; return; //exit, do not continue } @@ -117,70 +108,17 @@ namespace Umbraco.Web.Search // bind to distributed cache events - this ensures that this logic occurs on ALL servers // that are taking part in a load balanced environment. ContentCacheRefresher.CacheUpdated += ContentCacheRefresherUpdated; - ContentTypeCacheRefresher.CacheUpdated += ContentTypeCacheRefresherUpdated; ; + ContentTypeCacheRefresher.CacheUpdated += ContentTypeCacheRefresherUpdated; MediaCacheRefresher.CacheUpdated += MediaCacheRefresherUpdated; MemberCacheRefresher.CacheUpdated += MemberCacheRefresherUpdated; - - ConfigureIndexes(_logger, _examineManager); - - // TODO: Instead of waiting 5000 ms, we could add an event handler on to fulfilling the first request, then start? - RebuildIndexes(_indexRebuilder, _logger, true, 5000); } public void Terminate() { } - /// - /// Called to rebuild empty indexes on startup - /// - /// - /// - /// - /// - public static void RebuildIndexes(IndexRebuilder indexRebuilder, ILogger logger, bool onlyEmptyIndexes, int waitMilliseconds = 0) - { - // TODO: need a way to disable rebuilding on startup - - lock(RebuildLocker) - { - if (_rebuildOnStartupRunner != null && _rebuildOnStartupRunner.IsRunning) - { - logger.Warn("Call was made to RebuildIndexes but the task runner for rebuilding is already running"); - return; - } - - logger.Info("Starting initialize async background thread."); - //do the rebuild on a managed background thread - var task = new RebuildOnStartupTask(indexRebuilder, logger, onlyEmptyIndexes, waitMilliseconds); - - _rebuildOnStartupRunner = new BackgroundTaskRunner( - "RebuildIndexesOnStartup", - logger); - - _rebuildOnStartupRunner.TryAdd(task); - } - } - - /// - /// Called on startup to configure each index. - /// - /// - /// Indexing rebuilding can occur on a normal boot if the indexes are empty or on a cold boot by the database server messenger. Before - /// either of these happens, we need to configure the indexes. - /// Configuring also ensure the indexes are not locked. - /// - private static void ConfigureIndexes(ILogger logger, IExamineManager examineManager) - { - LazyInitializer.EnsureInitialized( - ref _configuredInit, - ref _isConfigured, - ref _isConfiguredLocker, - () => - { - examineManager.ConfigureLuceneIndexes(logger, _disableExamineIndexing); - return null; - }); - } + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("This method should not be used and will be removed in future versions, rebuilding indexes can be done with the IndexRebuilder or the BackgroundIndexRebuilder")] + public static void RebuildIndexes(IndexRebuilder indexRebuilder, ILogger logger, bool onlyEmptyIndexes, int waitMilliseconds = 0) => Current.Factory.GetInstance().RebuildIndexes(onlyEmptyIndexes, waitMilliseconds); #region Cache refresher updated event handlers @@ -746,63 +684,6 @@ namespace Umbraco.Web.Search } #endregion - /// - /// Background task used to rebuild empty indexes on startup - /// - private class RebuildOnStartupTask : IBackgroundTask - { - private readonly IndexRebuilder _indexRebuilder; - private readonly ILogger _logger; - private readonly bool _onlyEmptyIndexes; - private readonly int _waitMilliseconds; - - public RebuildOnStartupTask(IndexRebuilder indexRebuilder, ILogger logger, bool onlyEmptyIndexes, int waitMilliseconds = 0) - { - _indexRebuilder = indexRebuilder ?? throw new ArgumentNullException(nameof(indexRebuilder)); - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _onlyEmptyIndexes = onlyEmptyIndexes; - _waitMilliseconds = waitMilliseconds; - } - - public bool IsAsync => false; - - public void Dispose() - { - } - - public void Run() - { - try - { - // rebuilds indexes - RebuildIndexes(); - } - catch (Exception ex) - { - _logger.Error(ex, "Failed to rebuild empty indexes."); - } - } - - public Task RunAsync(CancellationToken token) - { - throw new NotImplementedException(); - } - - /// - /// Used to rebuild indexes on startup or cold boot - /// - private void RebuildIndexes() - { - //do not attempt to do this if this has been disabled since we are not the main dom. - //this can be called during a cold boot - if (_disableExamineIndexing) return; - - if (_waitMilliseconds > 0) - Thread.Sleep(_waitMilliseconds); - - ConfigureIndexes(_logger, _indexRebuilder.ExamineManager); - _indexRebuilder.RebuildIndexes(_onlyEmptyIndexes); - } - } + } } diff --git a/src/Umbraco.Web/Search/ExamineComposer.cs b/src/Umbraco.Web/Search/ExamineComposer.cs index 6e74f0e89d..0ade432d70 100644 --- a/src/Umbraco.Web/Search/ExamineComposer.cs +++ b/src/Umbraco.Web/Search/ExamineComposer.cs @@ -10,6 +10,7 @@ using Umbraco.Examine; namespace Umbraco.Web.Search { + /// /// Configures and installs Examine. /// @@ -43,6 +44,7 @@ namespace Umbraco.Web.Search false)); composition.RegisterUnique, MediaValueSetBuilder>(); composition.RegisterUnique, MemberValueSetBuilder>(); + composition.RegisterUnique(); //We want to manage Examine's AppDomain shutdown sequence ourselves so first we'll disable Examine's default behavior //and then we'll use MainDom to control Examine's shutdown - this MUST be done in Compose ie before ExamineManager diff --git a/src/Umbraco.Web/Search/ExamineFinalComponent.cs b/src/Umbraco.Web/Search/ExamineFinalComponent.cs new file mode 100644 index 0000000000..95000b2b46 --- /dev/null +++ b/src/Umbraco.Web/Search/ExamineFinalComponent.cs @@ -0,0 +1,42 @@ +using Examine; +using Umbraco.Core.Logging; +using Umbraco.Examine; +using Umbraco.Core.Composing; +using Umbraco.Core; + +namespace Umbraco.Web.Search +{ + + /// + /// Executes after all other examine components have executed + /// + public sealed class ExamineFinalComponent : IComponent + { + private readonly IProfilingLogger _logger; + private readonly IExamineManager _examineManager; + BackgroundIndexRebuilder _indexRebuilder; + private readonly IMainDom _mainDom; + + public ExamineFinalComponent(IProfilingLogger logger, IExamineManager examineManager, BackgroundIndexRebuilder indexRebuilder, IMainDom mainDom) + { + _logger = logger; + _examineManager = examineManager; + _indexRebuilder = indexRebuilder; + _mainDom = mainDom; + } + + public void Initialize() + { + if (!_mainDom.IsMainDom) return; + + _examineManager.ConfigureIndexes(_mainDom, _logger); + + // TODO: Instead of waiting 5000 ms, we could add an event handler on to fulfilling the first request, then start? + _indexRebuilder.RebuildIndexes(true, 5000); + } + + public void Terminate() + { + } + } +} diff --git a/src/Umbraco.Web/Search/ExamineFinalComposer.cs b/src/Umbraco.Web/Search/ExamineFinalComposer.cs new file mode 100644 index 0000000000..5b6334f1f6 --- /dev/null +++ b/src/Umbraco.Web/Search/ExamineFinalComposer.cs @@ -0,0 +1,13 @@ +using Umbraco.Core; +using Umbraco.Core.Composing; + +namespace Umbraco.Web.Search +{ + // examine's final composer composes after all user composers + // and *also* after ICoreComposer (in case IUserComposer is disabled) + [ComposeAfter(typeof(IUserComposer))] + [ComposeAfter(typeof(ICoreComposer))] + [RuntimeLevel(MinLevel = RuntimeLevel.Run)] + public class ExamineFinalComposer : ComponentComposer + { } +} diff --git a/src/Umbraco.Web/Search/ExamineUserComponent.cs b/src/Umbraco.Web/Search/ExamineUserComponent.cs new file mode 100644 index 0000000000..35bc3f59ea --- /dev/null +++ b/src/Umbraco.Web/Search/ExamineUserComponent.cs @@ -0,0 +1,37 @@ +using Umbraco.Core; +using Umbraco.Core.Composing; + +namespace Umbraco.Web.Search +{ + /// + /// An abstract class for custom index authors to inherit from + /// + public abstract class ExamineUserComponent : IComponent + { + private readonly IMainDom _mainDom; + + public ExamineUserComponent(IMainDom mainDom) + { + _mainDom = mainDom; + } + + /// + /// Initialize the component, eagerly exits if ExamineComponent.ExamineEnabled == false + /// + public void Initialize() + { + if (!_mainDom.IsMainDom) return; + + InitializeComponent(); + } + + /// + /// Abstract method which executes to initialize this component if ExamineComponent.ExamineEnabled == true + /// + protected abstract void InitializeComponent(); + + public virtual void Terminate() + { + } + } +} diff --git a/src/Umbraco.Web/Search/GenericIndexDiagnostics.cs b/src/Umbraco.Web/Search/GenericIndexDiagnostics.cs index 560fb19f09..cb25e1242a 100644 --- a/src/Umbraco.Web/Search/GenericIndexDiagnostics.cs +++ b/src/Umbraco.Web/Search/GenericIndexDiagnostics.cs @@ -8,6 +8,7 @@ using Umbraco.Examine; namespace Umbraco.Web.Search { + /// /// Used to return diagnostic data for any index /// diff --git a/src/Umbraco.Web/Search/UmbracoTreeSearcher.cs b/src/Umbraco.Web/Search/UmbracoTreeSearcher.cs index 729b2c0a9d..c3c3dc75ce 100644 --- a/src/Umbraco.Web/Search/UmbracoTreeSearcher.cs +++ b/src/Umbraco.Web/Search/UmbracoTreeSearcher.cs @@ -384,19 +384,7 @@ namespace Umbraco.Web.Search /// /// private IEnumerable MediaFromSearchResults(IEnumerable results) - { - //add additional data - foreach (var result in results) - { - var m = _mapper.Map(result); - //if no icon could be mapped, it will be set to document, so change it to picture - if (m.Icon == Constants.Icons.DefaultIcon) - { - m.Icon = Constants.Icons.MediaImage; - } - yield return m; - } - } + => _mapper.Map>(results); /// /// Returns a collection of entities for content based on search results diff --git a/src/Umbraco.Web/Suspendable.cs b/src/Umbraco.Web/Suspendable.cs index 86c83120ea..cfe4d28e8b 100644 --- a/src/Umbraco.Web/Suspendable.cs +++ b/src/Umbraco.Web/Suspendable.cs @@ -50,6 +50,8 @@ namespace Umbraco.Web } } + //This is really needed at all since the only place this is used is in ExamineComponent and that already maintains a flag of whether it suspsended or not + // AHH... but Deploy probably uses this? public static class ExamineEvents { private static bool _tried, _suspended; diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 730a321a16..222d58d4dc 100755 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -224,6 +224,7 @@ + @@ -233,6 +234,10 @@ + + + + @@ -1201,6 +1206,7 @@ + diff --git a/src/umbraco.sln b/src/umbraco.sln index 39b757f88c..0522b56c0d 100644 --- a/src/umbraco.sln +++ b/src/umbraco.sln @@ -123,7 +123,6 @@ Global {31785BC3-256C-4613-B2F5-A1B0BDDED8C1}.Release|Any CPU.ActiveCfg = Release|Any CPU {31785BC3-256C-4613-B2F5-A1B0BDDED8C1}.Release|Any CPU.Build.0 = Release|Any CPU {5D3B8245-ADA6-453F-A008-50ED04BFE770}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {5D3B8245-ADA6-453F-A008-50ED04BFE770}.Debug|Any CPU.Build.0 = Debug|Any CPU {5D3B8245-ADA6-453F-A008-50ED04BFE770}.Release|Any CPU.ActiveCfg = Release|Any CPU {5D3B8245-ADA6-453F-A008-50ED04BFE770}.Release|Any CPU.Build.0 = Release|Any CPU {07FBC26B-2927-4A22-8D96-D644C667FECC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU @@ -131,7 +130,6 @@ Global {07FBC26B-2927-4A22-8D96-D644C667FECC}.Release|Any CPU.ActiveCfg = Release|Any CPU {07FBC26B-2927-4A22-8D96-D644C667FECC}.Release|Any CPU.Build.0 = Release|Any CPU {3A33ADC9-C6C0-4DB1-A613-A9AF0210DF3D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {3A33ADC9-C6C0-4DB1-A613-A9AF0210DF3D}.Debug|Any CPU.Build.0 = Debug|Any CPU {3A33ADC9-C6C0-4DB1-A613-A9AF0210DF3D}.Release|Any CPU.ActiveCfg = Release|Any CPU {3A33ADC9-C6C0-4DB1-A613-A9AF0210DF3D}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection