diff --git a/.github/V8_GETTING_STARTED.md b/.github/V8_GETTING_STARTED.md index 62b376b0e7..87ddbb166a 100644 --- a/.github/V8_GETTING_STARTED.md +++ b/.github/V8_GETTING_STARTED.md @@ -33,5 +33,5 @@ We recommend running the site with the Visual Studio since you'll be able to rem We are keeping track of [known issues and limitations here](http://issues.umbraco.org/issue/U4-11279). These line items will eventually be turned into actual tasks to be worked on. Feel free to help us keep this list updated if you find issues and even help fix some of these items. If there is a particular item you'd like to help fix please mention this on the task and we'll create a sub task for the item to continue discussion there. -There's [a list of tasks for v8 that haven't been completed](https://issues.umbraco.org/issues?q=&project=U4&tagValue=&release=8.0.0&issueType=&resolvedState=open&search=search). If you are interested in helping out with any of these please mention this on the task. This list will be constantly updated as we begin to document and design some of the other tasks that still need to get done. +There's [a list of tasks for v8 that haven't been completed](hhttps://github.com/umbraco/Umbraco-CMS/labels/release%2F8.0.0). If you are interested in helping out with any of these please mention this on the task. This list will be constantly updated as we begin to document and design some of the other tasks that still need to get done. diff --git a/build/NuSpecs/UmbracoCms.Core.nuspec b/build/NuSpecs/UmbracoCms.Core.nuspec index daa0018668..dd565aa1d4 100644 --- a/build/NuSpecs/UmbracoCms.Core.nuspec +++ b/build/NuSpecs/UmbracoCms.Core.nuspec @@ -36,7 +36,7 @@ - + diff --git a/build/NuSpecs/tools/Dashboard.config.install.xdt b/build/NuSpecs/tools/Dashboard.config.install.xdt index 036beeba29..a81af8c365 100644 --- a/build/NuSpecs/tools/Dashboard.config.install.xdt +++ b/build/NuSpecs/tools/Dashboard.config.install.xdt @@ -3,7 +3,7 @@
- + views/dashboard/settings/settingsdashboardintro.html @@ -14,7 +14,7 @@ forms - + views/dashboard/forms/formsdashboardintro.html @@ -28,7 +28,7 @@
- + views/dashboard/developer/developerdashboardvideos.html @@ -47,7 +47,7 @@ - + views/dashboard/media/mediafolderbrowser.html @@ -56,7 +56,7 @@
- + views/dashboard/members/membersdashboardvideos.html @@ -92,4 +92,4 @@
- \ No newline at end of file + diff --git a/src/Umbraco.Core/Composing/CompositionRoots/ConfigurationCompositionRoot.cs b/src/Umbraco.Core/Composing/CompositionRoots/ConfigurationCompositionRoot.cs index 82912163b6..80de6d1c35 100644 --- a/src/Umbraco.Core/Composing/CompositionRoots/ConfigurationCompositionRoot.cs +++ b/src/Umbraco.Core/Composing/CompositionRoots/ConfigurationCompositionRoot.cs @@ -16,6 +16,7 @@ namespace Umbraco.Core.Composing.CompositionRoots container.Register(factory => factory.GetInstance().Templates); container.Register(factory => factory.GetInstance().RequestHandler); container.Register(factory => UmbracoConfig.For.GlobalSettings()); + container.Register(factory => UmbracoConfig.For.DashboardSettings()); // fixme - other sections we need to add? } diff --git a/src/Umbraco.Core/Composing/TypeFinder.cs b/src/Umbraco.Core/Composing/TypeFinder.cs index a42b84e0c5..9604f599cf 100644 --- a/src/Umbraco.Core/Composing/TypeFinder.cs +++ b/src/Umbraco.Core/Composing/TypeFinder.cs @@ -210,53 +210,55 @@ namespace Umbraco.Core.Composing /// NOTE the comma vs period... comma delimits the name in an Assembly FullName property so if it ends with comma then its an exact name match /// NOTE this means that "foo." will NOT exclude "foo.dll" but only "foo.*.dll" /// - internal static readonly string[] KnownAssemblyExclusionFilter = new[] - { - "mscorlib,", - "System.", - "Antlr3.", - "Autofac.", - "Autofac,", - "Castle.", - "ClientDependency.", - "DataAnnotationsExtensions.", - "DataAnnotationsExtensions,", - "Dynamic,", - "HtmlDiff,", - "Iesi.Collections,", - "Microsoft.", - "Newtonsoft.", - "NHibernate.", - "NHibernate,", - "NuGet.", - "RouteDebugger,", - "SqlCE4Umbraco,", - "umbraco.datalayer,", - "umbraco.interfaces,", - //"umbraco.providers,", - //"Umbraco.Web.UI,", - "umbraco.webservices", - "Lucene.", - "Examine,", - "Examine.", - "ServiceStack.", - "MySql.", - "HtmlAgilityPack.", - "TidyNet.", - "ICSharpCode.", - "CookComputing.", - "AutoMapper,", - "AutoMapper.", - "AzureDirectory,", - "itextsharp,", - "UrlRewritingNet.", - "HtmlAgilityPack,", - "MiniProfiler,", - "Moq,", - "nunit.framework,", - "TidyNet,", - "WebDriver," - }; + internal static readonly string[] KnownAssemblyExclusionFilter = { + "Antlr3.", + "AutoMapper,", + "AutoMapper.", + "Autofac,", // DI + "Autofac.", + "AzureDirectory,", + "Castle.", // DI, tests + "ClientDependency.", + "CookComputing.", + "CSharpTest.", // BTree for NuCache + "DataAnnotationsExtensions,", + "DataAnnotationsExtensions.", + "Dynamic,", + "Examine,", + "Examine.", + "HtmlAgilityPack,", + "HtmlAgilityPack.", + "HtmlDiff,", + "ICSharpCode.", + "Iesi.Collections,", // used by NHibernate + "LightInject.", // DI + "LightInject,", + "Lucene.", + "Markdown,", + "Microsoft.", + "MiniProfiler,", + "Moq,", + "MySql.", + "NHibernate,", + "NHibernate.", + "Newtonsoft.", + "NPoco,", + "NuGet.", + "RouteDebugger,", + "Semver.", + "Serilog.", + "Serilog,", + "ServiceStack.", + "SqlCE4Umbraco,", + "Superpower,", // used by Serilog + "System.", + "TidyNet,", + "TidyNet.", + "WebDriver,", + "itextsharp,", + "mscorlib,", + "nunit.framework,", + }; /// /// Finds any classes derived from the type T that contain the attribute TAttribute diff --git a/src/Umbraco.Core/Composing/TypeLoader.cs b/src/Umbraco.Core/Composing/TypeLoader.cs index 304638e017..f79c288e91 100644 --- a/src/Umbraco.Core/Composing/TypeLoader.cs +++ b/src/Umbraco.Core/Composing/TypeLoader.cs @@ -520,6 +520,8 @@ namespace Umbraco.Core.Composing // if not caching, or not IDiscoverable, directly get types if (cache == false || typeof(IDiscoverable).IsAssignableFrom(typeof(T)) == false) { + _logger.Logger.Debug("Running a full, non-cached, scan for type {TypeName} (slow).", typeof(T).FullName); + return GetTypesInternal( typeof (T), null, () => TypeFinder.FindClassesOfType(specificAssemblies ?? AssembliesToScan), @@ -559,6 +561,8 @@ namespace Umbraco.Core.Composing // if not caching, or not IDiscoverable, directly get types if (cache == false || typeof(IDiscoverable).IsAssignableFrom(typeof(T)) == false) { + _logger.Logger.Debug("Running a full, non-cached, scan for type {TypeName} / attribute {AttributeName} (slow).", typeof(T).FullName, typeof(TAttribute).FullName); + return GetTypesInternal( typeof (T), typeof (TAttribute), () => TypeFinder.FindClassesOfTypeWithAttribute(specificAssemblies ?? AssembliesToScan), @@ -595,6 +599,11 @@ namespace Umbraco.Core.Composing // do not cache anything from specific assemblies cache &= specificAssemblies == null; + if (cache == false) + { + _logger.Logger.Debug("Running a full, non-cached, scan for types / attribute {AttributeName} (slow).", typeof(TAttribute).FullName); + } + return GetTypesInternal( typeof (object), typeof (TAttribute), () => TypeFinder.FindClassesWithAttribute(specificAssemblies ?? AssembliesToScan), diff --git a/src/Umbraco.Core/Configuration/Dashboard/AccessElement.cs b/src/Umbraco.Core/Configuration/Dashboard/AccessElement.cs index 1642f23fc5..01538c8e0b 100644 --- a/src/Umbraco.Core/Configuration/Dashboard/AccessElement.cs +++ b/src/Umbraco.Core/Configuration/Dashboard/AccessElement.cs @@ -7,26 +7,22 @@ namespace Umbraco.Core.Configuration.Dashboard internal class AccessElement : RawXmlConfigurationElement, IAccess { public AccessElement() - { - - } + { } public AccessElement(XElement rawXml) - :base(rawXml) - { - } + : base(rawXml) + { } - public IEnumerable Rules + public IEnumerable Rules { get { - var result = new List(); - if (RawXml != null) - { - result.AddRange(RawXml.Elements("deny").Select(x => new AccessItem {Action = AccessType.Deny, Value = x.Value })); - result.AddRange(RawXml.Elements("grant").Select(x => new AccessItem { Action = AccessType.Grant, Value = x.Value })); - result.AddRange(RawXml.Elements("grantBySection").Select(x => new AccessItem { Action = AccessType.GrantBySection, Value = x.Value })); - } + var result = new List(); + if (RawXml == null) return result; + + result.AddRange(RawXml.Elements("deny").Select(x => new AccessRule {Type = AccessRuleType.Deny, Value = x.Value })); + result.AddRange(RawXml.Elements("grant").Select(x => new AccessRule { Type = AccessRuleType.Grant, Value = x.Value })); + result.AddRange(RawXml.Elements("grantBySection").Select(x => new AccessRule { Type = AccessRuleType.GrantBySection, Value = x.Value })); return result; } } diff --git a/src/Umbraco.Core/Configuration/Dashboard/AccessItem.cs b/src/Umbraco.Core/Configuration/Dashboard/AccessItem.cs deleted file mode 100644 index 37cf491536..0000000000 --- a/src/Umbraco.Core/Configuration/Dashboard/AccessItem.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace Umbraco.Core.Configuration.Dashboard -{ - internal class AccessItem : IAccessItem - { - /// - /// This can be grant, deny or grantBySection - /// - public AccessType Action { get; set; } - - /// - /// The value of the action - /// - public string Value { get; set; } - } -} diff --git a/src/Umbraco.Core/Configuration/Dashboard/AccessRule.cs b/src/Umbraco.Core/Configuration/Dashboard/AccessRule.cs new file mode 100644 index 0000000000..fe6840ff64 --- /dev/null +++ b/src/Umbraco.Core/Configuration/Dashboard/AccessRule.cs @@ -0,0 +1,14 @@ +namespace Umbraco.Core.Configuration.Dashboard +{ + /// + /// Implements . + /// + internal class AccessRule : IAccessRule + { + /// + public AccessRuleType Type { get; set; } + + /// + public string Value { get; set; } + } +} diff --git a/src/Umbraco.Core/Configuration/Dashboard/AccessRuleType.cs b/src/Umbraco.Core/Configuration/Dashboard/AccessRuleType.cs new file mode 100644 index 0000000000..cb9ce983fe --- /dev/null +++ b/src/Umbraco.Core/Configuration/Dashboard/AccessRuleType.cs @@ -0,0 +1,28 @@ +namespace Umbraco.Core.Configuration.Dashboard +{ + /// + /// Defines dashboard access rules type. + /// + public enum AccessRuleType + { + /// + /// Unknown (default value). + /// + Unknown = 0, + + /// + /// Grant access to the dashboard if user belongs to the specified user group. + /// + Grant, + + /// + /// Deny access to the dashboard if user belongs to the specified user group. + /// + Deny, + + /// + /// Grant access to the dashboard if user has access to the specified section. + /// + GrantBySection + } +} diff --git a/src/Umbraco.Core/Configuration/Dashboard/AccessType.cs b/src/Umbraco.Core/Configuration/Dashboard/AccessType.cs deleted file mode 100644 index d72cac15d0..0000000000 --- a/src/Umbraco.Core/Configuration/Dashboard/AccessType.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Umbraco.Core.Configuration.Dashboard -{ - public enum AccessType - { - Grant, - Deny, - GrantBySection - } -} diff --git a/src/Umbraco.Core/Configuration/Dashboard/ControlElement.cs b/src/Umbraco.Core/Configuration/Dashboard/ControlElement.cs index 0434eea47e..20dac7460e 100644 --- a/src/Umbraco.Core/Configuration/Dashboard/ControlElement.cs +++ b/src/Umbraco.Core/Configuration/Dashboard/ControlElement.cs @@ -1,5 +1,4 @@ -using System; -using System.Configuration; +using System.Configuration; using System.Linq; using System.Xml.Linq; @@ -8,33 +7,12 @@ namespace Umbraco.Core.Configuration.Dashboard internal class ControlElement : RawXmlConfigurationElement, IDashboardControl { - public bool ShowOnce - { - get - { - return RawXml.Attribute("showOnce") == null - ? false - : bool.Parse(RawXml.Attribute("showOnce").Value); - } - } - - public bool AddPanel - { - get - { - return RawXml.Attribute("addPanel") == null - ? true - : bool.Parse(RawXml.Attribute("addPanel").Value); - } - } - public string PanelCaption { get { - return RawXml.Attribute("panelCaption") == null - ? "" - : RawXml.Attribute("panelCaption").Value; + var panelCaption = RawXml.Attribute("panelCaption"); + return panelCaption == null ? "" : panelCaption.Value; } } @@ -43,11 +21,7 @@ namespace Umbraco.Core.Configuration.Dashboard get { var access = RawXml.Element("access"); - if (access == null) - { - return new AccessElement(); - } - return new AccessElement(access); + return access == null ? new AccessElement() : new AccessElement(access); } } @@ -65,10 +39,6 @@ namespace Umbraco.Core.Configuration.Dashboard } } - - IAccess IDashboardControl.AccessRights - { - get { return Access; } - } + IAccess IDashboardControl.AccessRights => Access; } } diff --git a/src/Umbraco.Core/Configuration/Dashboard/IAccess.cs b/src/Umbraco.Core/Configuration/Dashboard/IAccess.cs index b7d8540a79..8ac1b18cca 100644 --- a/src/Umbraco.Core/Configuration/Dashboard/IAccess.cs +++ b/src/Umbraco.Core/Configuration/Dashboard/IAccess.cs @@ -4,6 +4,6 @@ namespace Umbraco.Core.Configuration.Dashboard { public interface IAccess { - IEnumerable Rules { get; } + IEnumerable Rules { get; } } } diff --git a/src/Umbraco.Core/Configuration/Dashboard/IAccessItem.cs b/src/Umbraco.Core/Configuration/Dashboard/IAccessItem.cs deleted file mode 100644 index 8b18d50bb3..0000000000 --- a/src/Umbraco.Core/Configuration/Dashboard/IAccessItem.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace Umbraco.Core.Configuration.Dashboard -{ - public interface IAccessItem - { - /// - /// This can be grant, deny or grantBySection - /// - AccessType Action { get; set; } - - /// - /// The value of the action - /// - string Value { get; set; } - } -} diff --git a/src/Umbraco.Core/Configuration/Dashboard/IAccessRule.cs b/src/Umbraco.Core/Configuration/Dashboard/IAccessRule.cs new file mode 100644 index 0000000000..8b51b1b73a --- /dev/null +++ b/src/Umbraco.Core/Configuration/Dashboard/IAccessRule.cs @@ -0,0 +1,18 @@ +namespace Umbraco.Core.Configuration.Dashboard +{ + /// + /// Represents an access rule. + /// + public interface IAccessRule + { + /// + /// Gets or sets the rule type. + /// + AccessRuleType Type { get; set; } + + /// + /// Gets or sets the value for the rule. + /// + string Value { get; set; } + } +} diff --git a/src/Umbraco.Core/Configuration/Dashboard/IDashboardControl.cs b/src/Umbraco.Core/Configuration/Dashboard/IDashboardControl.cs index 7dab542258..cdf05af1ec 100644 --- a/src/Umbraco.Core/Configuration/Dashboard/IDashboardControl.cs +++ b/src/Umbraco.Core/Configuration/Dashboard/IDashboardControl.cs @@ -2,10 +2,6 @@ { public interface IDashboardControl { - bool ShowOnce { get; } - - bool AddPanel { get; } - string PanelCaption { get; } string ControlPath { get; } diff --git a/src/Umbraco.Core/Manifest/DashboardAccessRuleConverter.cs b/src/Umbraco.Core/Manifest/DashboardAccessRuleConverter.cs new file mode 100644 index 0000000000..c627728a32 --- /dev/null +++ b/src/Umbraco.Core/Manifest/DashboardAccessRuleConverter.cs @@ -0,0 +1,45 @@ +using System; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Umbraco.Core.Configuration.Dashboard; +using Umbraco.Core.Serialization; + +namespace Umbraco.Core.Manifest +{ + /// + /// Implements a json read converter for . + /// + internal class DashboardAccessRuleConverter : JsonReadConverter + { + /// + protected override IAccessRule Create(Type objectType, string path, JObject jObject) + { + return new AccessRule(); + } + + /// + protected override void Deserialize(JObject jobject, IAccessRule target, JsonSerializer serializer) + { + // see Create above, target is either DataEditor (parameter) or ConfiguredDataEditor (property) + + if (!(target is AccessRule accessRule)) + throw new Exception("panic."); + + GetRule(accessRule, jobject, "grant", AccessRuleType.Grant); + GetRule(accessRule, jobject, "deny", AccessRuleType.Deny); + GetRule(accessRule, jobject, "grantBySection", AccessRuleType.GrantBySection); + + if (accessRule.Type == AccessRuleType.Unknown) throw new InvalidOperationException("Rule is not defined."); + } + + private void GetRule(AccessRule rule, JObject jobject, string name, AccessRuleType type) + { + var token = jobject[name]; + if (token == null) return; + if (rule.Type != AccessRuleType.Unknown) throw new InvalidOperationException("Multiple definition of a rule."); + if (token.Type != JTokenType.String) throw new InvalidOperationException("Rule value is not a string."); + rule.Type = type; + rule.Value = token.Value(); + } + } +} diff --git a/src/Umbraco.Core/Manifest/ManifestDashboardDefinition.cs b/src/Umbraco.Core/Manifest/ManifestDashboardDefinition.cs new file mode 100644 index 0000000000..83f047b264 --- /dev/null +++ b/src/Umbraco.Core/Manifest/ManifestDashboardDefinition.cs @@ -0,0 +1,36 @@ +using System; +using System.ComponentModel; +using Newtonsoft.Json; +using Umbraco.Core.Configuration.Dashboard; +using Umbraco.Core.IO; + +namespace Umbraco.Core.Manifest +{ + public class ManifestDashboardDefinition + { + private string _view; + + [JsonProperty("name", Required = Required.Always)] + public string Name { get; set; } + + [JsonProperty("alias", Required = Required.Always)] + public string Alias { get; set; } + + [JsonProperty("weight", DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate)] + [DefaultValue(100)] + public int Weight { get; set; } + + [JsonProperty("view", Required = Required.Always)] + public string View + { + get => _view; + set => _view = IOHelper.ResolveVirtualUrl(value); + } + + [JsonProperty("sections")] + public string[] Sections { get; set; } = Array.Empty(); + + [JsonProperty("access")] + public IAccessRule[] AccessRules { get; set; } = Array.Empty(); + } +} diff --git a/src/Umbraco.Core/Manifest/ManifestParser.cs b/src/Umbraco.Core/Manifest/ManifestParser.cs index 125dee5c05..fe021fae5b 100644 --- a/src/Umbraco.Core/Manifest/ManifestParser.cs +++ b/src/Umbraco.Core/Manifest/ManifestParser.cs @@ -1,177 +1,180 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; -using Newtonsoft.Json; -using Umbraco.Core.Cache; -using Umbraco.Core.Exceptions; -using Umbraco.Core.IO; -using Umbraco.Core.Logging; -using Umbraco.Core.Models.ContentEditing; -using Umbraco.Core.PropertyEditors; - -namespace Umbraco.Core.Manifest -{ - /// - /// Parses the Main.js file and replaces all tokens accordingly. - /// - public class ManifestParser - { - private static readonly string Utf8Preamble = Encoding.UTF8.GetString(Encoding.UTF8.GetPreamble()); - - private readonly IRuntimeCacheProvider _cache; - private readonly ILogger _logger; - private readonly ManifestValueValidatorCollection _validators; - - private string _path; - - /// - /// Initializes a new instance of the class. - /// - public ManifestParser(IRuntimeCacheProvider cache, ManifestValueValidatorCollection validators, ILogger logger) - : this(cache, validators, "~/App_Plugins", logger) - { } - - /// - /// Initializes a new instance of the class. - /// - private ManifestParser(IRuntimeCacheProvider cache, ManifestValueValidatorCollection validators, string path, ILogger logger) - { - _cache = cache ?? throw new ArgumentNullException(nameof(cache)); - _validators = validators ?? throw new ArgumentNullException(nameof(validators)); - if (string.IsNullOrWhiteSpace(path)) throw new ArgumentNullOrEmptyException(nameof(path)); - Path = path; - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - } - - public string Path - { - get => _path; - set => _path = value.StartsWith("~/") ? IOHelper.MapPath(value) : value; - } - - /// - /// Gets all manifests, merged into a single manifest object. - /// - /// - public PackageManifest Manifest - => _cache.GetCacheItem("Umbraco.Core.Manifest.ManifestParser::Manifests", () => - { - var manifests = GetManifests(); - return MergeManifests(manifests); - }, new TimeSpan(0, 4, 0)); - - /// - /// Gets all manifests. - /// - private IEnumerable GetManifests() - { - var manifests = new List(); - - foreach (var path in GetManifestFiles()) - { - try - { - var text = File.ReadAllText(path); - text = TrimPreamble(text); - if (string.IsNullOrWhiteSpace(text)) - continue; - var manifest = ParseManifest(text); - manifests.Add(manifest); - } - catch (Exception e) - { - _logger.Error(e, "Failed to parse manifest at '{Path}', ignoring.", path); - } - } - - return manifests; - } - - /// - /// Merges all manifests into one. - /// - private static PackageManifest MergeManifests(IEnumerable manifests) - { - var scripts = new HashSet(); - var stylesheets = new HashSet(); - var propertyEditors = new List(); - var parameterEditors = new List(); - var gridEditors = new List(); - var contentApps = new List(); - - foreach (var manifest in manifests) - { - if (manifest.Scripts != null) foreach (var script in manifest.Scripts) scripts.Add(script); - if (manifest.Stylesheets != null) foreach (var stylesheet in manifest.Stylesheets) stylesheets.Add(stylesheet); - if (manifest.PropertyEditors != null) propertyEditors.AddRange(manifest.PropertyEditors); - if (manifest.ParameterEditors != null) parameterEditors.AddRange(manifest.ParameterEditors); - if (manifest.GridEditors != null) gridEditors.AddRange(manifest.GridEditors); - if (manifest.ContentApps != null) contentApps.AddRange(manifest.ContentApps); - } - - return new PackageManifest - { - Scripts = scripts.ToArray(), - Stylesheets = stylesheets.ToArray(), - PropertyEditors = propertyEditors.ToArray(), - ParameterEditors = parameterEditors.ToArray(), - GridEditors = gridEditors.ToArray(), - ContentApps = contentApps.ToArray() - }; - } - - // gets all manifest files (recursively) - private IEnumerable GetManifestFiles() - { - if (Directory.Exists(_path) == false) - return new string[0]; - return Directory.GetFiles(_path, "package.manifest", SearchOption.AllDirectories); - } - - - private static string TrimPreamble(string text) - { - // strangely StartsWith(preamble) would always return true - if (text.Substring(0, 1) == Utf8Preamble) - text = text.Remove(0, Utf8Preamble.Length); - - return text; - } - - /// - /// Parses a manifest. - /// - internal PackageManifest ParseManifest(string text) - { - if (string.IsNullOrWhiteSpace(text)) - throw new ArgumentNullOrEmptyException(nameof(text)); - - var manifest = JsonConvert.DeserializeObject(text, - new DataEditorConverter(_logger), - new ValueValidatorConverter(_validators), - new ContentAppDefinitionConverter()); - - // scripts and stylesheets are raw string, must process here - for (var i = 0; i < manifest.Scripts.Length; i++) - manifest.Scripts[i] = IOHelper.ResolveVirtualUrl(manifest.Scripts[i]); - for (var i = 0; i < manifest.Stylesheets.Length; i++) - manifest.Stylesheets[i] = IOHelper.ResolveVirtualUrl(manifest.Stylesheets[i]); - - // add property editors that are also parameter editors, to the parameter editors list - // (the manifest format is kinda legacy) - var ppEditors = manifest.PropertyEditors.Where(x => (x.Type & EditorType.MacroParameter) > 0).ToList(); - if (ppEditors.Count > 0) - manifest.ParameterEditors = manifest.ParameterEditors.Union(ppEditors).ToArray(); - - return manifest; - } - - // purely for tests - internal IEnumerable ParseGridEditors(string text) - { - return JsonConvert.DeserializeObject>(text); - } - } -} +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using Newtonsoft.Json; +using Umbraco.Core.Cache; +using Umbraco.Core.Exceptions; +using Umbraco.Core.IO; +using Umbraco.Core.Logging; +using Umbraco.Core.Models.ContentEditing; +using Umbraco.Core.PropertyEditors; + +namespace Umbraco.Core.Manifest +{ + /// + /// Parses the Main.js file and replaces all tokens accordingly. + /// + public class ManifestParser + { + private static readonly string Utf8Preamble = Encoding.UTF8.GetString(Encoding.UTF8.GetPreamble()); + + private readonly IRuntimeCacheProvider _cache; + private readonly ILogger _logger; + private readonly ManifestValueValidatorCollection _validators; + + private string _path; + + /// + /// Initializes a new instance of the class. + /// + public ManifestParser(IRuntimeCacheProvider cache, ManifestValueValidatorCollection validators, ILogger logger) + : this(cache, validators, "~/App_Plugins", logger) + { } + + /// + /// Initializes a new instance of the class. + /// + private ManifestParser(IRuntimeCacheProvider cache, ManifestValueValidatorCollection validators, string path, ILogger logger) + { + _cache = cache ?? throw new ArgumentNullException(nameof(cache)); + _validators = validators ?? throw new ArgumentNullException(nameof(validators)); + if (string.IsNullOrWhiteSpace(path)) throw new ArgumentNullOrEmptyException(nameof(path)); + Path = path; + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + public string Path + { + get => _path; + set => _path = value.StartsWith("~/") ? IOHelper.MapPath(value) : value; + } + + /// + /// Gets all manifests, merged into a single manifest object. + /// + /// + public PackageManifest Manifest + => _cache.GetCacheItem("Umbraco.Core.Manifest.ManifestParser::Manifests", () => + { + var manifests = GetManifests(); + return MergeManifests(manifests); + }, new TimeSpan(0, 4, 0)); + + /// + /// Gets all manifests. + /// + private IEnumerable GetManifests() + { + var manifests = new List(); + + foreach (var path in GetManifestFiles()) + { + try + { + var text = File.ReadAllText(path); + text = TrimPreamble(text); + if (string.IsNullOrWhiteSpace(text)) + continue; + var manifest = ParseManifest(text); + manifests.Add(manifest); + } + catch (Exception e) + { + _logger.Error(e, "Failed to parse manifest at '{Path}', ignoring.", path); + } + } + + return manifests; + } + + /// + /// Merges all manifests into one. + /// + private static PackageManifest MergeManifests(IEnumerable manifests) + { + var scripts = new HashSet(); + var stylesheets = new HashSet(); + var propertyEditors = new List(); + var parameterEditors = new List(); + var gridEditors = new List(); + var contentApps = new List(); + var dashboards = new List(); + + foreach (var manifest in manifests) + { + if (manifest.Scripts != null) foreach (var script in manifest.Scripts) scripts.Add(script); + if (manifest.Stylesheets != null) foreach (var stylesheet in manifest.Stylesheets) stylesheets.Add(stylesheet); + if (manifest.PropertyEditors != null) propertyEditors.AddRange(manifest.PropertyEditors); + if (manifest.ParameterEditors != null) parameterEditors.AddRange(manifest.ParameterEditors); + if (manifest.GridEditors != null) gridEditors.AddRange(manifest.GridEditors); + if (manifest.ContentApps != null) contentApps.AddRange(manifest.ContentApps); + if (manifest.Dashboards != null) dashboards.AddRange(manifest.Dashboards); + } + + return new PackageManifest + { + Scripts = scripts.ToArray(), + Stylesheets = stylesheets.ToArray(), + PropertyEditors = propertyEditors.ToArray(), + ParameterEditors = parameterEditors.ToArray(), + GridEditors = gridEditors.ToArray(), + ContentApps = contentApps.ToArray(), + Dashboards = dashboards.ToArray() + }; + } + + // gets all manifest files (recursively) + private IEnumerable GetManifestFiles() + { + if (Directory.Exists(_path) == false) + return new string[0]; + return Directory.GetFiles(_path, "package.manifest", SearchOption.AllDirectories); + } + + private static string TrimPreamble(string text) + { + // strangely StartsWith(preamble) would always return true + if (text.Substring(0, 1) == Utf8Preamble) + text = text.Remove(0, Utf8Preamble.Length); + + return text; + } + + /// + /// Parses a manifest. + /// + internal PackageManifest ParseManifest(string text) + { + if (string.IsNullOrWhiteSpace(text)) + throw new ArgumentNullOrEmptyException(nameof(text)); + + var manifest = JsonConvert.DeserializeObject(text, + new DataEditorConverter(_logger), + new ValueValidatorConverter(_validators), + new ContentAppDefinitionConverter(), + new DashboardAccessRuleConverter()); + + // scripts and stylesheets are raw string, must process here + for (var i = 0; i < manifest.Scripts.Length; i++) + manifest.Scripts[i] = IOHelper.ResolveVirtualUrl(manifest.Scripts[i]); + for (var i = 0; i < manifest.Stylesheets.Length; i++) + manifest.Stylesheets[i] = IOHelper.ResolveVirtualUrl(manifest.Stylesheets[i]); + + // add property editors that are also parameter editors, to the parameter editors list + // (the manifest format is kinda legacy) + var ppEditors = manifest.PropertyEditors.Where(x => (x.Type & EditorType.MacroParameter) > 0).ToList(); + if (ppEditors.Count > 0) + manifest.ParameterEditors = manifest.ParameterEditors.Union(ppEditors).ToArray(); + + return manifest; + } + + // purely for tests + internal IEnumerable ParseGridEditors(string text) + { + return JsonConvert.DeserializeObject>(text); + } + } +} diff --git a/src/Umbraco.Core/Manifest/PackageManifest.cs b/src/Umbraco.Core/Manifest/PackageManifest.cs index 32dae46a9a..95a5c01b6a 100644 --- a/src/Umbraco.Core/Manifest/PackageManifest.cs +++ b/src/Umbraco.Core/Manifest/PackageManifest.cs @@ -1,31 +1,34 @@ -using System; -using Newtonsoft.Json; -using Umbraco.Core.Models.ContentEditing; -using Umbraco.Core.PropertyEditors; - -namespace Umbraco.Core.Manifest -{ - /// - /// Represents the content of a package manifest. - /// - public class PackageManifest - { - [JsonProperty("javascript")] - public string[] Scripts { get; set; } = Array.Empty(); - - [JsonProperty("css")] - public string[] Stylesheets { get; set; }= Array.Empty(); - - [JsonProperty("propertyEditors")] - public IDataEditor[] PropertyEditors { get; set; } = Array.Empty(); - - [JsonProperty("parameterEditors")] - public IDataEditor[] ParameterEditors { get; set; } = Array.Empty(); - - [JsonProperty("gridEditors")] - public GridEditor[] GridEditors { get; set; } = Array.Empty(); - - [JsonProperty("contentApps")] - public IContentAppDefinition[] ContentApps { get; set; } = Array.Empty(); - } -} +using System; +using Newtonsoft.Json; +using Umbraco.Core.Models.ContentEditing; +using Umbraco.Core.PropertyEditors; + +namespace Umbraco.Core.Manifest +{ + /// + /// Represents the content of a package manifest. + /// + public class PackageManifest + { + [JsonProperty("javascript")] + public string[] Scripts { get; set; } = Array.Empty(); + + [JsonProperty("css")] + public string[] Stylesheets { get; set; }= Array.Empty(); + + [JsonProperty("propertyEditors")] + public IDataEditor[] PropertyEditors { get; set; } = Array.Empty(); + + [JsonProperty("parameterEditors")] + public IDataEditor[] ParameterEditors { get; set; } = Array.Empty(); + + [JsonProperty("gridEditors")] + public GridEditor[] GridEditors { get; set; } = Array.Empty(); + + [JsonProperty("contentApps")] + public IContentAppDefinition[] ContentApps { get; set; } = Array.Empty(); + + [JsonProperty("dashboards")] + public ManifestDashboardDefinition[] Dashboards { get; set; } = Array.Empty(); + } +} diff --git a/src/Umbraco.Core/Models/ContentEditing/IContentAppDefinition.cs b/src/Umbraco.Core/Models/ContentEditing/IContentAppDefinition.cs index 2d30fc6ba9..af83c5a2f5 100644 --- a/src/Umbraco.Core/Models/ContentEditing/IContentAppDefinition.cs +++ b/src/Umbraco.Core/Models/ContentEditing/IContentAppDefinition.cs @@ -3,6 +3,7 @@ using Umbraco.Core.Models.Membership; namespace Umbraco.Core.Models.ContentEditing { + /// /// Represents a content app definition. /// diff --git a/src/Umbraco.Core/Persistence/Repositories/IDocumentBlueprintRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IDocumentBlueprintRepository.cs index c52601d629..0148a882fd 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IDocumentBlueprintRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IDocumentBlueprintRepository.cs @@ -1,5 +1,5 @@ namespace Umbraco.Core.Persistence.Repositories { - interface IDocumentBlueprintRepository : IDocumentRepository + public interface IDocumentBlueprintRepository : IDocumentRepository { } } diff --git a/src/Umbraco.Core/PropertyEditors/SliderConfiguration.cs b/src/Umbraco.Core/PropertyEditors/SliderConfiguration.cs index b2bf99bdc6..55ca199121 100644 --- a/src/Umbraco.Core/PropertyEditors/SliderConfiguration.cs +++ b/src/Umbraco.Core/PropertyEditors/SliderConfiguration.cs @@ -8,9 +8,6 @@ [ConfigurationField("enableRange", "Enable range", "boolean")] public bool EnableRange { get; set; } - [ConfigurationField("orientation", "Orientation", "views/propertyeditors/slider/orientation.prevalues.html")] - public string Orientation { get; set; } - [ConfigurationField("initVal1", "Initial value", "number")] public int InitialValue { get; set; } @@ -25,38 +22,5 @@ [ConfigurationField("step", "Step increments", "number")] public int StepIncrements { get; set; } - - [ConfigurationField("precision", "Precision", "number", Description = "The number of digits shown after the decimal. Defaults to the number of digits after the decimal of step value.")] - public int Precision { get; set; } - - [ConfigurationField("handle", "Handle", "views/propertyeditors/slider/handle.prevalues.html", Description = "Handle shape. Default is 'round\'")] - public string Handle { get; set; } - - [ConfigurationField("tooltip", "Tooltip", "views/propertyeditors/slider/tooltip.prevalues.html", Description = "Whether to show the tooltip on drag, hide the tooltip, or always show the tooltip. Accepts: 'show', 'hide', or 'always'")] - public string Tooltip { get; set; } - - [ConfigurationField("tooltipSplit", "Tooltip split", "boolean", Description = "If false show one tootip if true show two tooltips one for each handler")] - public bool TooltipSplit { get; set; } // fixme bool? - - [ConfigurationField("tooltipFormat", "Tooltip format", "textstring", Description = "The value wanted to be displayed in the tooltip. Use {0} and {1} for current values - {1} is only for range slider and if not using tooltip split.")] - public string TooltipFormat { get; set; } - - [ConfigurationField("tooltipPosition", "Tooltip position", "textstring", Description = "Position of tooltip, relative to slider. Accepts 'top'/'bottom' for horizontal sliders and 'left'/'right' for vertically orientated sliders. Default positions are 'top' for horizontal and 'right' for vertical slider.")] - public string TooltipPosition { get; set; } - - [ConfigurationField("reversed", "Reversed", "boolean", Description = "Whether or not the slider should be reversed")] - public bool Reversed { get; set; } // fixme bool? - - [ConfigurationField("ticks", "Ticks", "textstring", Description = "Comma-separated values. Used to define the values of ticks. Tick marks are indicators to denote special values in the range. This option overwrites min and max options.")] - public string Ticks { get; set; } - - [ConfigurationField("ticksPositions", "Ticks positions", "textstring", Description = "Comma-separated values. Defines the positions of the tick values in percentages. The first value should always be 0, the last value should always be 100 percent.")] - public string TicksPositions { get; set; } - - [ConfigurationField("ticksLabels", "Ticks labels", "textstring", Description = "Comma-separated values. Defines the labels below the tick marks. Accepts HTML input.")] - public string TicksLabels { get; set; } - - [ConfigurationField("ticksSnapBounds", "Ticks snap bounds", "number", Description = "Used to define the snap bounds of a tick. Snaps to the tick if value is within these bounds.")] - public int TicksSnapBounds { get; set; } } } diff --git a/src/Umbraco.Core/Services/Implement/ContentService.cs b/src/Umbraco.Core/Services/Implement/ContentService.cs index 0412ecb409..cd15732564 100644 --- a/src/Umbraco.Core/Services/Implement/ContentService.cs +++ b/src/Umbraco.Core/Services/Implement/ContentService.cs @@ -19,7 +19,7 @@ namespace Umbraco.Core.Services.Implement /// /// Implements the content service. /// - internal class ContentService : RepositoryService, IContentService + public class ContentService : RepositoryService, IContentService { private readonly IDocumentRepository _documentRepository; private readonly IEntityRepository _entityRepository; diff --git a/src/Umbraco.Core/Services/MoveOperationStatusType.cs b/src/Umbraco.Core/Services/MoveOperationStatusType.cs index 95ccce93ca..b4b4c2b42e 100644 --- a/src/Umbraco.Core/Services/MoveOperationStatusType.cs +++ b/src/Umbraco.Core/Services/MoveOperationStatusType.cs @@ -7,7 +7,7 @@ /// /// Anything less than 10 = Success! /// - public enum MoveOperationStatusType + public enum MoveOperationStatusType : byte { /// /// The move was successful. diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index e22841ccd6..e737d91f02 100755 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -89,7 +89,7 @@ 1.0.0 - 2.1.2 + 2.2.2 4.0.0 @@ -194,8 +194,8 @@ - - + + @@ -203,7 +203,7 @@ - + @@ -335,7 +335,9 @@ + + diff --git a/src/Umbraco.Tests/Configurations/DashboardSettings/Dashboard.config b/src/Umbraco.Tests/Configurations/DashboardSettings/Dashboard.config index 4040412603..4c86355a1b 100644 --- a/src/Umbraco.Tests/Configurations/DashboardSettings/Dashboard.config +++ b/src/Umbraco.Tests/Configurations/DashboardSettings/Dashboard.config @@ -6,10 +6,10 @@ settings - + views/dashboard/settings/settingsdashboardintro.html - + views/dashboard/settings/settingsdashboardvideos.html @@ -23,10 +23,10 @@ developer - + views/dashboard/developer/developerdashboardintro.html - + views/dashboard/developer/developerdashboardvideos.html @@ -37,7 +37,7 @@ media - + views/dashboard/media/mediafolderbrowser.html @@ -45,13 +45,13 @@ admin - + views/dashboard/media/mediadashboardintro.html - + views/dashboard/media/desktopmediauploader.html - + views/dashboard/media/mediadashboardvideos.html @@ -70,25 +70,25 @@ admin - + views/dashboard/default/startupdashboardintro.html - + views/dashboard/default/startupdashboardkits.html editor writer - + views/dashboard/default/startupdashboardvideos.html - dashboard/latestEdits.ascx + dashboard/latestEdits.ascx - + views/dashboard/changepassword.html @@ -100,13 +100,13 @@ member - + views/dashboard/members/membersdashboardintro.html - + members/membersearch.ascx - + views/dashboard/members/membersdashboardvideos.html diff --git a/src/Umbraco.Tests/Configurations/DashboardSettings/DashboardSettingsTests.cs b/src/Umbraco.Tests/Configurations/DashboardSettings/DashboardSettingsTests.cs index 862dfb3dc2..920de683b4 100644 --- a/src/Umbraco.Tests/Configurations/DashboardSettings/DashboardSettingsTests.cs +++ b/src/Umbraco.Tests/Configurations/DashboardSettings/DashboardSettingsTests.cs @@ -56,11 +56,11 @@ namespace Umbraco.Tests.Configurations.DashboardSettings Assert.AreEqual(3, SettingsSection.Sections.ElementAt(3).AccessRights.Rules.Count()); Assert.AreEqual("translator", SettingsSection.Sections.ElementAt(3).AccessRights.Rules.ElementAt(0).Value); - Assert.AreEqual(AccessType.Deny, SettingsSection.Sections.ElementAt(3).AccessRights.Rules.ElementAt(0).Action); + Assert.AreEqual(AccessRuleType.Deny, SettingsSection.Sections.ElementAt(3).AccessRights.Rules.ElementAt(0).Type); Assert.AreEqual("hello", SettingsSection.Sections.ElementAt(3).AccessRights.Rules.ElementAt(1).Value); - Assert.AreEqual(AccessType.Grant, SettingsSection.Sections.ElementAt(3).AccessRights.Rules.ElementAt(1).Action); + Assert.AreEqual(AccessRuleType.Grant, SettingsSection.Sections.ElementAt(3).AccessRights.Rules.ElementAt(1).Type); Assert.AreEqual("world", SettingsSection.Sections.ElementAt(3).AccessRights.Rules.ElementAt(2).Value); - Assert.AreEqual(AccessType.GrantBySection, SettingsSection.Sections.ElementAt(3).AccessRights.Rules.ElementAt(2).Action); + Assert.AreEqual(AccessRuleType.GrantBySection, SettingsSection.Sections.ElementAt(3).AccessRights.Rules.ElementAt(2).Type); } [Test] @@ -94,21 +94,17 @@ namespace Umbraco.Tests.Configurations.DashboardSettings public void Test_Tab_Access() { Assert.AreEqual(1, SettingsSection.Sections.ElementAt(2).Tabs.ElementAt(1).AccessRights.Rules.Count()); - Assert.AreEqual(AccessType.Grant, SettingsSection.Sections.ElementAt(2).Tabs.ElementAt(1).AccessRights.Rules.ElementAt(0).Action); + Assert.AreEqual(AccessRuleType.Grant, SettingsSection.Sections.ElementAt(2).Tabs.ElementAt(1).AccessRights.Rules.ElementAt(0).Type); Assert.AreEqual("admin", SettingsSection.Sections.ElementAt(2).Tabs.ElementAt(1).AccessRights.Rules.ElementAt(0).Value); } [Test] public void Test_Control() { - Assert.AreEqual(true, SettingsSection.Sections.ElementAt(0).Tabs.ElementAt(0).Controls.ElementAt(0).ShowOnce); - Assert.AreEqual(true, SettingsSection.Sections.ElementAt(0).Tabs.ElementAt(0).Controls.ElementAt(0).AddPanel); Assert.AreEqual("hello", SettingsSection.Sections.ElementAt(0).Tabs.ElementAt(0).Controls.ElementAt(0).PanelCaption); Assert.AreEqual("views/dashboard/settings/settingsdashboardintro.html", SettingsSection.Sections.ElementAt(0).Tabs.ElementAt(0).Controls.ElementAt(0).ControlPath); - Assert.AreEqual(false, SettingsSection.Sections.ElementAt(0).Tabs.ElementAt(0).Controls.ElementAt(1).ShowOnce); - Assert.AreEqual(false, SettingsSection.Sections.ElementAt(0).Tabs.ElementAt(0).Controls.ElementAt(1).AddPanel); Assert.AreEqual("", SettingsSection.Sections.ElementAt(0).Tabs.ElementAt(0).Controls.ElementAt(1).PanelCaption); Assert.AreEqual("views/dashboard/settings/settingsdashboardvideos.html", SettingsSection.Sections.ElementAt(0).Tabs.ElementAt(0).Controls.ElementAt(1).ControlPath); @@ -118,9 +114,9 @@ namespace Umbraco.Tests.Configurations.DashboardSettings public void Test_Control_Access() { Assert.AreEqual(2, SettingsSection.Sections.ElementAt(3).Tabs.ElementAt(0).Controls.ElementAt(1).AccessRights.Rules.Count()); - Assert.AreEqual(AccessType.Deny, SettingsSection.Sections.ElementAt(3).Tabs.ElementAt(0).Controls.ElementAt(1).AccessRights.Rules.ElementAt(0).Action); + Assert.AreEqual(AccessRuleType.Deny, SettingsSection.Sections.ElementAt(3).Tabs.ElementAt(0).Controls.ElementAt(1).AccessRights.Rules.ElementAt(0).Type); Assert.AreEqual("editor", SettingsSection.Sections.ElementAt(3).Tabs.ElementAt(0).Controls.ElementAt(1).AccessRights.Rules.ElementAt(0).Value); - Assert.AreEqual(AccessType.Deny, SettingsSection.Sections.ElementAt(3).Tabs.ElementAt(0).Controls.ElementAt(1).AccessRights.Rules.ElementAt(1).Action); + Assert.AreEqual(AccessRuleType.Deny, SettingsSection.Sections.ElementAt(3).Tabs.ElementAt(0).Controls.ElementAt(1).AccessRights.Rules.ElementAt(1).Type); Assert.AreEqual("writer", SettingsSection.Sections.ElementAt(3).Tabs.ElementAt(0).Controls.ElementAt(1).AccessRights.Rules.ElementAt(1).Value); } } diff --git a/src/Umbraco.Tests/Manifest/ManifestParserTests.cs b/src/Umbraco.Tests/Manifest/ManifestParserTests.cs index 5145b848ed..19aaf79581 100644 --- a/src/Umbraco.Tests/Manifest/ManifestParserTests.cs +++ b/src/Umbraco.Tests/Manifest/ManifestParserTests.cs @@ -8,6 +8,7 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Umbraco.Core.Cache; using Umbraco.Core.Composing; +using Umbraco.Core.Configuration.Dashboard; using Umbraco.Core.Logging; using Umbraco.Core.Manifest; using Umbraco.Core.PropertyEditors; @@ -383,5 +384,53 @@ javascript: ['~/test.js',/*** some note about stuff asd09823-4**09234*/ '~/test2 Assert.AreEqual("icon-bar", app1.Icon); Assert.AreEqual("/App_Plugins/MyPackage/ContentApps/MyApp2.html", app1.View); } + + [Test] + public void CanParseManifest_Dashboards() + { + const string json = @"{'dashboards': [ + { + 'name': 'First One', + 'alias': 'something', + 'view': '~/App_Plugins/MyPackage/Dashboards/one.html', + 'sections': [ 'content' ], + 'access': [ {'grant':'user'}, {'deny':'foo'} ] + + }, + { + 'name': 'Second-One', + 'alias': 'something.else', + 'weight': -1, + 'view': '~/App_Plugins/MyPackage/Dashboards/two.html', + 'sections': [ 'forms' ], + } +]}"; + + var manifest = _parser.ParseManifest(json); + Assert.AreEqual(2, manifest.Dashboards.Length); + + Assert.IsInstanceOf(manifest.Dashboards[0]); + var db0 = manifest.Dashboards[0]; + Assert.AreEqual("something", db0.Alias); + Assert.AreEqual("First One", db0.Name); + Assert.AreEqual(100, db0.Weight); + Assert.AreEqual("/App_Plugins/MyPackage/Dashboards/one.html", db0.View); + Assert.AreEqual(1, db0.Sections.Length); + Assert.AreEqual("content", db0.Sections[0]); + Assert.AreEqual(2, db0.AccessRules.Length); + Assert.AreEqual(AccessRuleType.Grant, db0.AccessRules[0].Type); + Assert.AreEqual("user", db0.AccessRules[0].Value); + Assert.AreEqual(AccessRuleType.Deny, db0.AccessRules[1].Type); + Assert.AreEqual("foo", db0.AccessRules[1].Value); + + Assert.IsInstanceOf(manifest.Dashboards[1]); + var db1 = manifest.Dashboards[1]; + Assert.AreEqual("something.else", db1.Alias); + Assert.AreEqual("Second-One", db1.Name); + Assert.AreEqual(-1, db1.Weight); + Assert.AreEqual("/App_Plugins/MyPackage/Dashboards/two.html", db1.View); + Assert.AreEqual(1, db1.Sections.Length); + Assert.AreEqual("forms", db1.Sections[0]); + } } } diff --git a/src/Umbraco.Web.UI.Client/gulpfile.js b/src/Umbraco.Web.UI.Client/gulpfile.js index cd7f6f4bef..0af327d148 100644 --- a/src/Umbraco.Web.UI.Client/gulpfile.js +++ b/src/Umbraco.Web.UI.Client/gulpfile.js @@ -282,48 +282,29 @@ gulp.task('dependencies', function () { ], "base": "./node_modules/jquery/dist" }, - { - "name": "jquery-migrate", - "src": ["./node_modules/jquery-migrate/dist/jquery-migrate.min.js"], - "base": "./node_modules/jquery-migrate/dist" - }, { "name": "jquery-ui", "src": ["./node_modules/jquery-ui-dist/jquery-ui.min.js"], "base": "./node_modules/jquery-ui-dist" }, { - "name": "jquery-validate", - "src": ["./node_modules/jquery-validation/dist/jquery.validate.min.js"], - "base": "./node_modules/jquery-validation/dist" - }, - { - "name": "jquery-validation-unobtrusive", - "src": ["./node_modules/jquery-validation-unobtrusive/dist/jquery.validate.unobtrusive.min.js"], - "base": "./node_modules/jquery-validation-unobtrusive/dist" + "name": "jquery-ui-touch-punch", + "src": ["./node_modules/jquery-ui-touch-punch/jquery.ui.touch-punch.min.js"], + "base": "./node_modules/jquery-ui-touch-punch" }, { "name": "lazyload-js", "src": ["./node_modules/lazyload-js/lazyload.min.js"], "base": "./node_modules/lazyload-js" }, - // TODO: We can optimize here: - // we don't have to ship with the moment-with-locales libraries - // we lazyload the user locale { "name": "moment", - "src": [ - "./node_modules/moment/min/moment.min.js", - "./node_modules/moment/min/moment-with-locales.js", - "./node_modules/moment/min/moment-with-locales.min.js" - ], + "src": ["./node_modules/moment/min/moment.min.js"], "base": "./node_modules/moment/min" }, { "name": "moment", - "src": [ - "./node_modules/moment/locale/*.js" - ], + "src": ["./node_modules/moment/locale/*.js"], "base": "./node_modules/moment/locale" }, { @@ -331,11 +312,27 @@ gulp.task('dependencies', function () { "src": ["./node_modules/ng-file-upload/dist/ng-file-upload.min.js"], "base": "./node_modules/ng-file-upload/dist" }, + { + "name": "nouislider", + "src": [ + "./node_modules/nouislider/distribute/nouislider.min.js", + "./node_modules/nouislider/distribute/nouislider.min.css" + ], + "base": "./node_modules/nouislider/distribute" + }, { "name": "signalr", "src": ["./node_modules/signalr/jquery.signalR.js"], "base": "./node_modules/signalr" }, + { + "name": "spectrum", + "src": [ + "./node_modules/spectrum-colorpicker/spectrum.js", + "./node_modules/spectrum-colorpicker/spectrum.css" + ], + "base": "./node_modules/spectrum-colorpicker" + }, { "name": "tinymce", "src": [ diff --git a/src/Umbraco.Web.UI.Client/lib/bootstrap/js/bootstrap.2.3.2.js b/src/Umbraco.Web.UI.Client/lib/bootstrap/js/bootstrap.2.3.2.js deleted file mode 100644 index 31701ad5df..0000000000 --- a/src/Umbraco.Web.UI.Client/lib/bootstrap/js/bootstrap.2.3.2.js +++ /dev/null @@ -1,2284 +0,0 @@ -/* =================================================== - * bootstrap-transition.js v2.3.2 - * http://getbootstrap.com/2.3.2/javascript.html#transitions - * =================================================== - * Copyright 2013 Twitter, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * ========================================================== */ - - -!function ($) { - - "use strict"; // jshint ;_; - - - /* CSS TRANSITION SUPPORT (http://www.modernizr.com/) - * ======================================================= */ - - $(function () { - - $.support.transition = (function () { - - var transitionEnd = (function () { - - var el = document.createElement('bootstrap') - , transEndEventNames = { - 'WebkitTransition' : 'webkitTransitionEnd' - , 'MozTransition' : 'transitionend' - , 'OTransition' : 'oTransitionEnd otransitionend' - , 'transition' : 'transitionend' - } - , name - - for (name in transEndEventNames){ - if (el.style[name] !== undefined) { - return transEndEventNames[name] - } - } - - }()) - - return transitionEnd && { - end: transitionEnd - } - - })() - - }) - -}(window.jQuery);/* ========================================================== - * bootstrap-alert.js v2.3.2 - * http://getbootstrap.com/2.3.2/javascript.html#alerts - * ========================================================== - * Copyright 2013 Twitter, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * ========================================================== */ - - -!function ($) { - - "use strict"; // jshint ;_; - - - /* ALERT CLASS DEFINITION - * ====================== */ - - var dismiss = '[data-dismiss="alert"]' - , Alert = function (el) { - $(el).on('click', dismiss, this.close) - } - - Alert.prototype.close = function (e) { - var $this = $(this) - , selector = $this.attr('data-target') - , $parent - - if (!selector) { - selector = $this.attr('href') - selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') //strip for ie7 - } - - $parent = $(selector) - - e && e.preventDefault() - - $parent.length || ($parent = $this.hasClass('alert') ? $this : $this.parent()) - - $parent.trigger(e = $.Event('close')) - - if (e.isDefaultPrevented()) return - - $parent.removeClass('in') - - function removeElement() { - $parent - .trigger('closed') - .remove() - } - - $.support.transition && $parent.hasClass('fade') ? - $parent.on($.support.transition.end, removeElement) : - removeElement() - } - - - /* ALERT PLUGIN DEFINITION - * ======================= */ - - var old = $.fn.alert - - $.fn.alert = function (option) { - return this.each(function () { - var $this = $(this) - , data = $this.data('alert') - if (!data) $this.data('alert', (data = new Alert(this))) - if (typeof option == 'string') data[option].call($this) - }) - } - - $.fn.alert.Constructor = Alert - - - /* ALERT NO CONFLICT - * ================= */ - - $.fn.alert.noConflict = function () { - $.fn.alert = old - return this - } - - - /* ALERT DATA-API - * ============== */ - - $(document).on('click.alert.data-api', dismiss, Alert.prototype.close) - -}(window.jQuery);/* ============================================================ - * bootstrap-button.js v2.3.2 - * http://getbootstrap.com/2.3.2/javascript.html#buttons - * ============================================================ - * Copyright 2013 Twitter, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * ============================================================ */ - - -!function ($) { - - "use strict"; // jshint ;_; - - - /* BUTTON PUBLIC CLASS DEFINITION - * ============================== */ - - var Button = function (element, options) { - this.$element = $(element) - this.options = $.extend({}, $.fn.button.defaults, options) - } - - Button.prototype.setState = function (state) { - var d = 'disabled' - , $el = this.$element - , data = $el.data() - , val = $el.is('input') ? 'val' : 'html' - - state = state + 'Text' - data.resetText || $el.data('resetText', $el[val]()) - - $el[val](data[state] || this.options[state]) - - // push to event loop to allow forms to submit - setTimeout(function () { - state == 'loadingText' ? - $el.addClass(d).attr(d, d) : - $el.removeClass(d).removeAttr(d) - }, 0) - } - - Button.prototype.toggle = function () { - var $parent = this.$element.closest('[data-toggle="buttons-radio"]') - - $parent && $parent - .find('.active') - .removeClass('active') - - this.$element.toggleClass('active') - } - - - /* BUTTON PLUGIN DEFINITION - * ======================== */ - - var old = $.fn.button - - $.fn.button = function (option) { - return this.each(function () { - var $this = $(this) - , data = $this.data('button') - , options = typeof option == 'object' && option - if (!data) $this.data('button', (data = new Button(this, options))) - if (option == 'toggle') data.toggle() - else if (option) data.setState(option) - }) - } - - $.fn.button.defaults = { - loadingText: 'loading...' - } - - $.fn.button.Constructor = Button - - - /* BUTTON NO CONFLICT - * ================== */ - - $.fn.button.noConflict = function () { - $.fn.button = old - return this - } - - - /* BUTTON DATA-API - * =============== */ - - $(document).on('click.button.data-api', '[data-toggle^=button]', function (e) { - var $btn = $(e.target) - if (!$btn.hasClass('btn')) $btn = $btn.closest('.btn') - $btn.button('toggle') - }) - -}(window.jQuery);/* ========================================================== - * bootstrap-carousel.js v2.3.2 - * http://getbootstrap.com/2.3.2/javascript.html#carousel - * ========================================================== - * Copyright 2013 Twitter, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * ========================================================== */ - - -!function ($) { - - "use strict"; // jshint ;_; - - - /* CAROUSEL CLASS DEFINITION - * ========================= */ - - var Carousel = function (element, options) { - this.$element = $(element) - this.$indicators = this.$element.find('.carousel-indicators') - this.options = options - this.options.pause == 'hover' && this.$element - .on('mouseenter', $.proxy(this.pause, this)) - .on('mouseleave', $.proxy(this.cycle, this)) - } - - Carousel.prototype = { - - cycle: function (e) { - if (!e) this.paused = false - if (this.interval) clearInterval(this.interval); - this.options.interval - && !this.paused - && (this.interval = setInterval($.proxy(this.next, this), this.options.interval)) - return this - } - - , getActiveIndex: function () { - this.$active = this.$element.find('.item.active') - this.$items = this.$active.parent().children() - return this.$items.index(this.$active) - } - - , to: function (pos) { - var activeIndex = this.getActiveIndex() - , that = this - - if (pos > (this.$items.length - 1) || pos < 0) return - - if (this.sliding) { - return this.$element.one('slid', function () { - that.to(pos) - }) - } - - if (activeIndex == pos) { - return this.pause().cycle() - } - - return this.slide(pos > activeIndex ? 'next' : 'prev', $(this.$items[pos])) - } - - , pause: function (e) { - if (!e) this.paused = true - if (this.$element.find('.next, .prev').length && $.support.transition.end) { - this.$element.trigger($.support.transition.end) - this.cycle(true) - } - clearInterval(this.interval) - this.interval = null - return this - } - - , next: function () { - if (this.sliding) return - return this.slide('next') - } - - , prev: function () { - if (this.sliding) return - return this.slide('prev') - } - - , slide: function (type, next) { - var $active = this.$element.find('.item.active') - , $next = next || $active[type]() - , isCycling = this.interval - , direction = type == 'next' ? 'left' : 'right' - , fallback = type == 'next' ? 'first' : 'last' - , that = this - , e - - this.sliding = true - - isCycling && this.pause() - - $next = $next.length ? $next : this.$element.find('.item')[fallback]() - - e = $.Event('slide', { - relatedTarget: $next[0] - , direction: direction - }) - - if ($next.hasClass('active')) return - - if (this.$indicators.length) { - this.$indicators.find('.active').removeClass('active') - this.$element.one('slid', function () { - var $nextIndicator = $(that.$indicators.children()[that.getActiveIndex()]) - $nextIndicator && $nextIndicator.addClass('active') - }) - } - - if ($.support.transition && this.$element.hasClass('slide')) { - this.$element.trigger(e) - if (e.isDefaultPrevented()) return - $next.addClass(type) - $next[0].offsetWidth // force reflow - $active.addClass(direction) - $next.addClass(direction) - this.$element.one($.support.transition.end, function () { - $next.removeClass([type, direction].join(' ')).addClass('active') - $active.removeClass(['active', direction].join(' ')) - that.sliding = false - setTimeout(function () { that.$element.trigger('slid') }, 0) - }) - } else { - this.$element.trigger(e) - if (e.isDefaultPrevented()) return - $active.removeClass('active') - $next.addClass('active') - this.sliding = false - this.$element.trigger('slid') - } - - isCycling && this.cycle() - - return this - } - - } - - - /* CAROUSEL PLUGIN DEFINITION - * ========================== */ - - var old = $.fn.carousel - - $.fn.carousel = function (option) { - return this.each(function () { - var $this = $(this) - , data = $this.data('carousel') - , options = $.extend({}, $.fn.carousel.defaults, typeof option == 'object' && option) - , action = typeof option == 'string' ? option : options.slide - if (!data) $this.data('carousel', (data = new Carousel(this, options))) - if (typeof option == 'number') data.to(option) - else if (action) data[action]() - else if (options.interval) data.pause().cycle() - }) - } - - $.fn.carousel.defaults = { - interval: 5000 - , pause: 'hover' - } - - $.fn.carousel.Constructor = Carousel - - - /* CAROUSEL NO CONFLICT - * ==================== */ - - $.fn.carousel.noConflict = function () { - $.fn.carousel = old - return this - } - - /* CAROUSEL DATA-API - * ================= */ - - $(document).on('click.carousel.data-api', '[data-slide], [data-slide-to]', function (e) { - var $this = $(this), href - , $target = $($this.attr('data-target') || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '')) //strip for ie7 - , options = $.extend({}, $target.data(), $this.data()) - , slideIndex - - $target.carousel(options) - - if (slideIndex = $this.attr('data-slide-to')) { - $target.data('carousel').pause().to(slideIndex).cycle() - } - - e.preventDefault() - }) - -}(window.jQuery);/* ============================================================= - * bootstrap-collapse.js v2.3.2 - * http://getbootstrap.com/2.3.2/javascript.html#collapse - * ============================================================= - * Copyright 2013 Twitter, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * ============================================================ */ - - -!function ($) { - - "use strict"; // jshint ;_; - - - /* COLLAPSE PUBLIC CLASS DEFINITION - * ================================ */ - - var Collapse = function (element, options) { - this.$element = $(element) - this.options = $.extend({}, $.fn.collapse.defaults, options) - - if (this.options.parent) { - this.$parent = $(this.options.parent) - } - - this.options.toggle && this.toggle() - } - - Collapse.prototype = { - - constructor: Collapse - - , dimension: function () { - var hasWidth = this.$element.hasClass('width') - return hasWidth ? 'width' : 'height' - } - - , show: function () { - var dimension - , scroll - , actives - , hasData - - if (this.transitioning || this.$element.hasClass('in')) return - - dimension = this.dimension() - scroll = $.camelCase(['scroll', dimension].join('-')) - actives = this.$parent && this.$parent.find('> .accordion-group > .in') - - if (actives && actives.length) { - hasData = actives.data('collapse') - if (hasData && hasData.transitioning) return - actives.collapse('hide') - hasData || actives.data('collapse', null) - } - - this.$element[dimension](0) - this.transition('addClass', $.Event('show'), 'shown') - $.support.transition && this.$element[dimension](this.$element[0][scroll]) - } - - , hide: function () { - var dimension - if (this.transitioning || !this.$element.hasClass('in')) return - dimension = this.dimension() - this.reset(this.$element[dimension]()) - this.transition('removeClass', $.Event('hide'), 'hidden') - this.$element[dimension](0) - } - - , reset: function (size) { - var dimension = this.dimension() - - this.$element - .removeClass('collapse') - [dimension](size || 'auto') - [0].offsetWidth - - this.$element[size !== null ? 'addClass' : 'removeClass']('collapse') - - return this - } - - , transition: function (method, startEvent, completeEvent) { - var that = this - , complete = function () { - if (startEvent.type == 'show') that.reset() - that.transitioning = 0 - that.$element.trigger(completeEvent) - } - - this.$element.trigger(startEvent) - - if (startEvent.isDefaultPrevented()) return - - this.transitioning = 1 - - this.$element[method]('in') - - $.support.transition && this.$element.hasClass('collapse') ? - this.$element.one($.support.transition.end, complete) : - complete() - } - - , toggle: function () { - this[this.$element.hasClass('in') ? 'hide' : 'show']() - } - - } - - - /* COLLAPSE PLUGIN DEFINITION - * ========================== */ - - var old = $.fn.collapse - - $.fn.collapse = function (option) { - return this.each(function () { - var $this = $(this) - , data = $this.data('collapse') - , options = $.extend({}, $.fn.collapse.defaults, $this.data(), typeof option == 'object' && option) - if (!data) $this.data('collapse', (data = new Collapse(this, options))) - if (typeof option == 'string') data[option]() - }) - } - - $.fn.collapse.defaults = { - toggle: true - } - - $.fn.collapse.Constructor = Collapse - - - /* COLLAPSE NO CONFLICT - * ==================== */ - - $.fn.collapse.noConflict = function () { - $.fn.collapse = old - return this - } - - - /* COLLAPSE DATA-API - * ================= */ - - $(document).on('click.collapse.data-api', '[data-toggle=collapse]', function (e) { - var $this = $(this), href - , target = $this.attr('data-target') - || e.preventDefault() - || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '') //strip for ie7 - , option = $(target).data('collapse') ? 'toggle' : $this.data() - $this[$(target).hasClass('in') ? 'addClass' : 'removeClass']('collapsed') - $(target).collapse(option) - }) - -}(window.jQuery);/* ============================================================ - * bootstrap-dropdown.js v2.3.2 - * http://getbootstrap.com/2.3.2/javascript.html#dropdowns - * ============================================================ - * Copyright 2013 Twitter, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * ============================================================ */ - - -!function ($) { - - "use strict"; // jshint ;_; - - - /* DROPDOWN CLASS DEFINITION - * ========================= */ - - var toggle = '[data-toggle=dropdown]' - , Dropdown = function (element) { - var $el = $(element).on('click.dropdown.data-api', this.toggle) - $('html').on('click.dropdown.data-api', function () { - $el.parent().removeClass('open') - }) - } - - Dropdown.prototype = { - - constructor: Dropdown - - , toggle: function (e) { - var $this = $(this) - , $parent - , isActive - - if ($this.is('.disabled, :disabled')) return - - $parent = getParent($this) - - isActive = $parent.hasClass('open') - - clearMenus() - - if (!isActive) { - if ('ontouchstart' in document.documentElement) { - // if mobile we we use a backdrop because click events don't delegate - $(' @@ -148,9 +176,7 @@ -
-				{{model.result.queryExpression}}
-						
+
{{model.result.queryExpression}}
diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/rollback/rollback.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/rollback/rollback.controller.js index 6b8462b583..e179e0acb3 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/rollback/rollback.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/rollback/rollback.controller.js @@ -123,11 +123,14 @@ oldProperty.isObject = true; } - // create new property object used in the diff table + // diff requires a string + property.value = property.value ? property.value : ""; + oldProperty.value = oldProperty.value ? oldProperty.value : ""; + var diffProperty = { "alias": property.alias, "label": property.label, - "diff": (property.value || oldProperty.value) ? JsDiff.diffWords(property.value, oldProperty.value) : "", + "diff": JsDiff.diffWords(property.value, oldProperty.value), "isObject": (property.isObject || oldProperty.isObject) ? true : false }; diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/rollback/rollback.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/rollback/rollback.html index f7ab78b7a0..b5b925b266 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/rollback/rollback.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/rollback/rollback.html @@ -63,7 +63,7 @@ {{property.label}} - + {{part.value}} {{part.value}} diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/treepicker/treepicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/overlays/treepicker/treepicker.controller.js deleted file mode 100644 index 827b2ad4e0..0000000000 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/treepicker/treepicker.controller.js +++ /dev/null @@ -1,518 +0,0 @@ -//used for the media picker dialog -angular.module("umbraco").controller("Umbraco.Overlays.TreePickerController", - function ($scope, $q, entityResource, eventsService, $log, searchService, angularHelper, $timeout, localizationService, treeService, contentResource, mediaResource, memberResource) { - - var tree = null; - var dialogOptions = $scope.model; - $scope.treeReady = false; - $scope.dialogTreeEventHandler = $({}); - $scope.section = dialogOptions.section; - $scope.treeAlias = dialogOptions.treeAlias; - $scope.multiPicker = dialogOptions.multiPicker; - $scope.hideHeader = (typeof dialogOptions.hideHeader) === "boolean" ? dialogOptions.hideHeader : true; - // if you need to load a not initialized tree set this value to false - default is true - $scope.onlyInitialized = dialogOptions.onlyInitialized; - $scope.searchInfo = { - searchFromId: dialogOptions.startNodeId, - searchFromName: null, - showSearch: false, - results: [], - selectedSearchResults: [] - } - - $scope.model.selection = []; - - //Used for toggling an empty-state message - //Some trees can have no items (dictionary & forms email templates) - $scope.hasItems = true; - $scope.emptyStateMessage = dialogOptions.emptyStateMessage; - var node = dialogOptions.currentNode; - - //This is called from ng-init - //it turns out it is called from the angular html : / Have a look at views/common / overlays / contentpicker / contentpicker.html you'll see ng-init. - //this is probably an anti pattern IMO and shouldn't be used - $scope.init = function (contentType) { - - if (contentType === "content") { - $scope.entityType = "Document"; - if (!$scope.model.title) { - $scope.model.title = localizationService.localize("defaultdialogs_selectContent"); - } - } else if (contentType === "member") { - $scope.entityType = "Member"; - if (!$scope.model.title) { - $scope.model.title = localizationService.localize("defaultdialogs_selectMember"); - } - } else if (contentType === "media") { - $scope.entityType = "Media"; - if (!$scope.model.title) { - $scope.model.title = localizationService.localize("defaultdialogs_selectMedia"); - } - } - } - - var searchText = "Search..."; - localizationService.localize("general_search").then(function (value) { - searchText = value + "..."; - }); - - // Allow the entity type to be passed in but defaults to Document for backwards compatibility. - $scope.entityType = dialogOptions.entityType ? dialogOptions.entityType : "Document"; - - - //min / max values - if (dialogOptions.minNumber) { - dialogOptions.minNumber = parseInt(dialogOptions.minNumber, 10); - } - if (dialogOptions.maxNumber) { - dialogOptions.maxNumber = parseInt(dialogOptions.maxNumber, 10); - } - - if (dialogOptions.section === "member") { - $scope.entityType = "Member"; - } - else if (dialogOptions.section === "media") { - $scope.entityType = "Media"; - } - - // Search and listviews is only working for content, media and member section - var searchableSections = ["content", "media", "member"]; - - $scope.enableSearh = searchableSections.indexOf($scope.section) !== -1; - - //if a alternative startnode is used, we need to check if it is a container - if ($scope.enableSearh && dialogOptions.startNodeId && dialogOptions.startNodeId !== -1 && dialogOptions.startNodeId !== "-1") { - entityResource.getById(dialogOptions.startNodeId, $scope.entityType).then(function(node) { - if (node.metaData.IsContainer) { - openMiniListView(node); - } - initTree(); - }); - } - else { - initTree(); - } - - //Configures filtering - if (dialogOptions.filter) { - - dialogOptions.filterExclude = false; - dialogOptions.filterAdvanced = false; - - //used advanced filtering - if (angular.isFunction(dialogOptions.filter)) { - dialogOptions.filterAdvanced = true; - } - else if (angular.isObject(dialogOptions.filter)) { - dialogOptions.filterAdvanced = true; - } - else { - if (dialogOptions.filter.startsWith("!")) { - dialogOptions.filterExclude = true; - dialogOptions.filterTypes = dialogOptions.filter.substring(1); - } else { - dialogOptions.filterExclude = false; - dialogOptions.filterTypes = dialogOptions.filter; - } - - //used advanced filtering - if (dialogOptions.filter.startsWith("{")) { - dialogOptions.filterAdvanced = true; - //convert to object - dialogOptions.filter = angular.fromJson(dialogOptions.filter); - } - } - - $scope.filter = { - filterAdvanced: dialogOptions.filterAdvanced, - filterExclude: dialogOptions.filterExclude, - filter: dialogOptions.filterTypes - }; - } - - function initTree() { - //create the custom query string param for this tree - $scope.customTreeParams = dialogOptions.startNodeId ? "startNodeId=" + dialogOptions.startNodeId : ""; - $scope.customTreeParams += dialogOptions.customTreeParams ? "&" + dialogOptions.customTreeParams : ""; - $scope.treeReady = true; - } - - function nodeExpandedHandler(ev, args) { - - // open mini list view for list views - if (args.node.metaData.isContainer) { - openMiniListView(args.node); - } - - if (angular.isArray(args.children)) { - - //iterate children - _.each(args.children, function (child) { - - //now we need to look in the already selected search results and - // toggle the check boxes for those ones that are listed - var exists = _.find($scope.searchInfo.selectedSearchResults, function (selected) { - return child.id == selected.id; - }); - if (exists) { - child.selected = true; - } - }); - - //check filter - performFiltering(args.children); - } - } - - //gets the tree object when it loads - function treeLoadedHandler(ev, args) { - //args.tree contains children (args.tree.root.children) - $scope.hasItems = args.tree.root.children.length > 0; - - tree = args.tree; - - var nodeHasPath = typeof node !== "undefined" && typeof node.path !== "undefined"; - var startNodeNotDefined = typeof dialogOptions.startNodeId === "undefined" || dialogOptions.startNodeId === "" || dialogOptions.startNodeId === "-1"; - if (startNodeNotDefined && nodeHasPath) { - $scope.dialogTreeEventHandler.syncTree({ path: node.path, activate: false }); - } - - } - - //wires up selection - function nodeSelectHandler(ev, args) { - args.event.preventDefault(); - args.event.stopPropagation(); - - if (args.node.metaData.isSearchResult) { - //check if the item selected was a search result from a list view - - //unselect - select(args.node.name, args.node.id); - - //remove it from the list view children - var listView = args.node.parent(); - listView.children = _.reject(listView.children, function (child) { - return child.id == args.node.id; - }); - - //remove it from the custom tracked search result list - $scope.searchInfo.selectedSearchResults = _.reject($scope.searchInfo.selectedSearchResults, function (i) { - return i.id == args.node.id; - }); - } - else { - eventsService.emit("dialogs.treePickerController.select", args); - - if (args.node.filtered) { - return; - } - - //This is a tree node, so we don't have an entity to pass in, it will need to be looked up - //from the server in this method. - if ($scope.model.select) { - $scope.model.select(args.node) - } else { - select(args.node.name, args.node.id); - //toggle checked state - args.node.selected = args.node.selected === true ? false : true; - } - - } - } - - /** Method used for selecting a node */ - function select(text, id, entity) { - //if we get the root, we just return a constructed entity, no need for server data - if (id < 0) { - - var rootNode = { - alias: null, - icon: "icon-folder", - id: id, - name: text - }; - - if ($scope.multiPicker) { - if (entity) { - multiSelectItem(entity); - } else { - multiSelectItem(rootNode); - } - } - else { - $scope.model.selection.push(rootNode); - $scope.model.submit($scope.model); - } - } - else { - - if ($scope.multiPicker) { - - if (entity) { - multiSelectItem(entity); - } else { - //otherwise we have to get it from the server - entityResource.getById(id, $scope.entityType).then(function (ent) { - multiSelectItem(ent); - }); - } - - } - - else { - - $scope.hideSearch(); - - //if an entity has been passed in, use it - if (entity) { - $scope.model.selection.push(entity); - $scope.model.submit($scope.model); - } else { - //otherwise we have to get it from the server - entityResource.getById(id, $scope.entityType).then(function (ent) { - $scope.model.selection.push(ent); - $scope.model.submit($scope.model); - }); - } - } - } - } - - function multiSelectItem(item) { - - var found = false; - var foundIndex = 0; - - if ($scope.model.selection.length > 0) { - for (var i = 0; $scope.model.selection.length > i; i++) { - var selectedItem = $scope.model.selection[i]; - if (selectedItem.id === item.id) { - found = true; - foundIndex = i; - } - } - } - - if (found) { - $scope.model.selection.splice(foundIndex, 1); - } else { - $scope.model.selection.push(item); - } - - } - - function performFiltering(nodes) { - - if (!dialogOptions.filter) { - return; - } - - //remove any list view search nodes from being filtered since these are special nodes that always must - // be allowed to be clicked on - nodes = _.filter(nodes, function (n) { - return !angular.isObject(n.metaData.listViewNode); - }); - - if (dialogOptions.filterAdvanced) { - - //filter either based on a method or an object - var filtered = angular.isFunction(dialogOptions.filter) - ? _.filter(nodes, dialogOptions.filter) - : _.where(nodes, dialogOptions.filter); - - angular.forEach(filtered, function (value, key) { - value.filtered = true; - if (dialogOptions.filterCssClass) { - if (!value.cssClasses) { - value.cssClasses = []; - } - value.cssClasses.push(dialogOptions.filterCssClass); - } - }); - } else { - var a = dialogOptions.filterTypes.toLowerCase().replace(/\s/g, '').split(','); - angular.forEach(nodes, function (value, key) { - - var found = a.indexOf(value.metaData.contentType.toLowerCase()) >= 0; - - if (!dialogOptions.filterExclude && !found || dialogOptions.filterExclude && found) { - value.filtered = true; - - if (dialogOptions.filterCssClass) { - if (!value.cssClasses) { - value.cssClasses = []; - } - value.cssClasses.push(dialogOptions.filterCssClass); - } - } - }); - } - } - - $scope.multiSubmit = function (result) { - entityResource.getByIds(result, $scope.entityType).then(function (ents) { - $scope.submit(ents); - }); - }; - - /** method to select a search result */ - $scope.selectResult = function (evt, result) { - - if (result.filtered) { - return; - } - - result.selected = result.selected === true ? false : true; - - //since result = an entity, we'll pass it in so we don't have to go back to the server - select(result.name, result.id, result); - - //add/remove to our custom tracked list of selected search results - if (result.selected) { - $scope.searchInfo.selectedSearchResults.push(result); - } - else { - $scope.searchInfo.selectedSearchResults = _.reject($scope.searchInfo.selectedSearchResults, function (i) { - return i.id == result.id; - }); - } - - //ensure the tree node in the tree is checked/unchecked if it already exists there - if (tree) { - var found = treeService.getDescendantNode(tree.root, result.id); - if (found) { - found.selected = result.selected; - } - } - - }; - - $scope.hideSearch = function () { - - //Traverse the entire displayed tree and update each node to sync with the selected search results - if (tree) { - - //we need to ensure that any currently displayed nodes that get selected - // from the search get updated to have a check box! - function checkChildren(children) { - _.each(children, function (child) { - //check if the id is in the selection, if so ensure it's flagged as selected - var exists = _.find($scope.searchInfo.selectedSearchResults, function (selected) { - return child.id == selected.id; - }); - //if the curr node exists in selected search results, ensure it's checked - if (exists) { - child.selected = true; - } - //if the curr node does not exist in the selected search result, and the curr node is a child of a list view search result - else if (child.metaData.isSearchResult) { - //if this tree node is under a list view it means that the node was added - // to the tree dynamically under the list view that was searched, so we actually want to remove - // it all together from the tree - var listView = child.parent(); - listView.children = _.reject(listView.children, function (c) { - return c.id == child.id; - }); - } - - //check if the current node is a list view and if so, check if there's any new results - // that need to be added as child nodes to it based on search results selected - if (child.metaData.isContainer) { - - child.cssClasses = _.reject(child.cssClasses, function (c) { - return c === 'tree-node-slide-up-hide-active'; - }); - - var listViewResults = _.filter($scope.searchInfo.selectedSearchResults, function (i) { - return i.parentId == child.id; - }); - _.each(listViewResults, function (item) { - var childExists = _.find(child.children, function (c) { - return c.id == item.id; - }); - if (!childExists) { - var parent = child; - child.children.unshift({ - id: item.id, - name: item.name, - cssClass: "icon umb-tree-icon sprTree " + item.icon, - level: child.level + 1, - metaData: { - isSearchResult: true - }, - hasChildren: false, - parent: function () { - return parent; - } - }); - } - }); - } - - //recurse - if (child.children && child.children.length > 0) { - checkChildren(child.children); - } - }); - } - checkChildren(tree.root.children); - } - - - $scope.searchInfo.showSearch = false; - $scope.searchInfo.searchFromId = dialogOptions.startNodeId; - $scope.searchInfo.searchFromName = null; - $scope.searchInfo.results = []; - } - - $scope.onSearchResults = function (results) { - - //filter all items - this will mark an item as filtered - performFiltering(results); - - //now actually remove all filtered items so they are not even displayed - results = _.filter(results, function (item) { - return !item.filtered; - }); - - $scope.searchInfo.results = results; - - //sync with the curr selected results - _.each($scope.searchInfo.results, function (result) { - var exists = _.find($scope.model.selection, function (selectedId) { - return result.id == selectedId; - }); - if (exists) { - result.selected = true; - } - }); - - $scope.searchInfo.showSearch = true; - }; - - $scope.dialogTreeEventHandler.bind("treeLoaded", treeLoadedHandler); - $scope.dialogTreeEventHandler.bind("treeNodeExpanded", nodeExpandedHandler); - $scope.dialogTreeEventHandler.bind("treeNodeSelect", nodeSelectHandler); - - $scope.$on('$destroy', function () { - $scope.dialogTreeEventHandler.unbind("treeLoaded", treeLoadedHandler); - $scope.dialogTreeEventHandler.unbind("treeNodeExpanded", nodeExpandedHandler); - $scope.dialogTreeEventHandler.unbind("treeNodeSelect", nodeSelectHandler); - }); - - $scope.selectListViewNode = function (node) { - select(node.name, node.id); - //toggle checked state - node.selected = node.selected === true ? false : true; - }; - - $scope.closeMiniListView = function () { - $scope.miniListView = undefined; - }; - - function openMiniListView(node) { - $scope.miniListView = node; - } - - }); 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 c5167ba964..7d8ce3e13e 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 @@ -161,7 +161,7 @@
- +
@@ -199,7 +199,7 @@
- +
@@ -230,7 +230,7 @@
- +
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/application/umb-navigation.html b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-navigation.html index 9d1da590ab..c5b4f69cef 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/application/umb-navigation.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-navigation.html @@ -19,7 +19,7 @@
+ on-init="onTreeInit()">
@@ -44,6 +44,9 @@
+ +
+
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 3810630fa9..054681d7f1 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 @@ -16,17 +16,20 @@ add-ellipsis={{defaultButton.addEllipsis}}> - + - + + diff --git a/src/Umbraco.Web.UI.Client/src/views/components/buttons/umb-button.html b/src/Umbraco.Web.UI.Client/src/views/components/buttons/umb-button.html index 68d4adef5a..03813c8518 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/buttons/umb-button.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/buttons/umb-button.html @@ -11,6 +11,7 @@ {{vm.buttonLabel}} + @@ -18,6 +19,7 @@ {{vm.buttonLabel}} + @@ -25,6 +27,7 @@ {{vm.buttonLabel}} + diff --git a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-menu.html b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-menu.html index bf9c8fab8c..032e4cd6c3 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-menu.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-menu.html @@ -1,22 +1,20 @@
- - - Actions - - + + - - + + +
diff --git a/src/Umbraco.Web.UI.Client/src/views/content/content.rights.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/content.rights.controller.js index afab478fc4..a8f87ce2c9 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/content.rights.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/content.rights.controller.js @@ -63,6 +63,8 @@ vm.labels.permissionsSetForGroup = value; }); setViewSate("managePermissions"); + // hide dropdown + vm.groupsDropdownOpen = false; } function assignGroupPermissions(group) { diff --git a/src/Umbraco.Web.UI.Client/src/views/content/rights.html b/src/Umbraco.Web.UI.Client/src/views/content/rights.html index afa932e606..35f3d34260 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/rights.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/rights.html @@ -27,20 +27,24 @@

diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/colorpicker/colorpicker.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/colorpicker/colorpicker.html index c86e7f44f7..ef45e99638 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/colorpicker/colorpicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/colorpicker/colorpicker.html @@ -5,10 +5,11 @@ You haven't defined any colors
- + diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.controller.js index 9bed59b0d7..704fccbde1 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.controller.js @@ -1,38 +1,81 @@ function dateTimePickerController($scope, notificationsService, assetsService, angularHelper, userService, $element, dateHelper) { - //setup the default config - var config = { - pickDate: true, - pickTime: true, - useSeconds: true, - format: "YYYY-MM-DD HH:mm:ss", - icons: { - time: "icon-time", - date: "icon-calendar", - up: "icon-chevron-up", - down: "icon-chevron-down" - } + let flatPickr = null; - }; + function onInit() { + + $scope.hasDatetimePickerValue = $scope.model.value ? true : false; + $scope.model.datetimePickerValue = null; + $scope.serverTime = null; + $scope.serverTimeNeedsOffsetting = false; + + // setup the default config + var config = { + pickDate: true, + pickTime: true, + useSeconds: true, + format: "YYYY-MM-DD HH:mm:ss", + icons: { + time: "icon-time", + date: "icon-calendar", + up: "icon-chevron-up", + down: "icon-chevron-down" + } + }; + + // map the user config + $scope.model.config = angular.extend(config, $scope.model.config); + + // ensure the format doesn't get overwritten with an empty string + if ($scope.model.config.format === "" || $scope.model.config.format === undefined || $scope.model.config.format === null) { + $scope.model.config.format = $scope.model.config.pickTime ? "YYYY-MM-DD HH:mm:ss" : "YYYY-MM-DD"; + } + + // check whether a server time offset is needed + if (Umbraco.Sys.ServerVariables.application.serverTimeOffset !== undefined) { + // Will return something like 120 + var serverOffset = Umbraco.Sys.ServerVariables.application.serverTimeOffset; + + // Will return something like -120 + var localOffset = new Date().getTimezoneOffset(); + + // If these aren't equal then offsetting is needed + // note the minus in front of serverOffset needed + // because C# and javascript return the inverse offset + $scope.serverTimeNeedsOffsetting = (-serverOffset !== localOffset); + } + + const dateFormat = $scope.model.config.pickTime ? "Y-m-d H:i:S" : "Y-m-d"; + + // date picker config + $scope.datePickerConfig = { + enableTime: $scope.model.config.pickTime, + dateFormat: dateFormat, + time_24hr: true + }; + + setDatePickerVal(); - //map the user config - $scope.model.config = angular.extend(config, $scope.model.config); - //ensure the format doesn't get overwritten with an empty string - if ($scope.model.config.format === "" || $scope.model.config.format === undefined || $scope.model.config.format === null) { - $scope.model.config.format = $scope.model.config.pickTime ? "YYYY-MM-DD HH:mm:ss" : "YYYY-MM-DD"; } - $scope.hasDatetimePickerValue = $scope.model.value ? true : false; - $scope.datetimePickerValue = null; - - //hide picker if clicking on the document - $scope.hidePicker = function () { - //$element.find("div:first").datetimepicker("hide"); - // Sometimes the statement above fails and generates errors in the browser console. The following statements fix that. - var dtp = $element.find("div:first"); - if (dtp && dtp.datetimepicker) { - dtp.datetimepicker("hide"); + $scope.clearDate = function() { + $scope.hasDatetimePickerValue = false; + if($scope.model) { + $scope.model.datetimePickerValue = null; + $scope.model.value = null; } + if($scope.datePickerForm && $scope.datePickerForm.datepicker) { + $scope.datePickerForm.datepicker.$setValidity("pickerError", true); + } + } + + $scope.datePickerSetup = function(instance) { + flatPickr = instance; + }; + + $scope.datePickerChange = function(date) { + setDate(date); + setDatePickerVal(); }; //here we declare a special method which will be called whenever the value has changed from the server @@ -44,53 +87,45 @@ function dateTimePickerController($scope, notificationsService, assetsService, a var newDate = moment(newVal); if (newDate.isAfter(minDate)) { - applyDate({ date: moment(newVal) }); + setDate(newVal); } else { $scope.clearDate(); } } }; - - //handles the date changing via the date picker - function applyDate(e) { + + function setDate(date) { + const momentDate = moment(date); angularHelper.safeApply($scope, function() { // when a date is changed, update the model - if (e.date && e.date.isValid()) { + if (momentDate && momentDate.isValid()) { $scope.datePickerForm.datepicker.$setValidity("pickerError", true); $scope.hasDatetimePickerValue = true; - $scope.datetimePickerValue = e.date.format($scope.model.config.format); + $scope.model.datetimePickerValue = momentDate.format($scope.model.config.format); } else { $scope.hasDatetimePickerValue = false; - $scope.datetimePickerValue = null; - } - - setModelValue(); - - if (!$scope.model.config.pickTime) { - $element.find("div:first").datetimepicker("hide", 0); + $scope.model.datetimePickerValue = null; } + updateModelValue(date); }); } - //sets the scope model value accordingly - this is the value to be sent up to the server and depends on - // if the picker is configured to offset time. We always format the date/time in a specific format for sending - // to the server, this is different from the format used to display the date/time. - function setModelValue() { + function updateModelValue(date) { + const momentDate = moment(date); if ($scope.hasDatetimePickerValue) { - var elementData = $element.find("div:first").data().DateTimePicker; if ($scope.model.config.pickTime) { //check if we are supposed to offset the time if ($scope.model.value && Object.toBoolean($scope.model.config.offsetTime) && Umbraco.Sys.ServerVariables.application.serverTimeOffset !== undefined) { - $scope.model.value = dateHelper.convertToServerStringTime(elementData.getDate(), Umbraco.Sys.ServerVariables.application.serverTimeOffset); - $scope.serverTime = dateHelper.convertToServerStringTime(elementData.getDate(), Umbraco.Sys.ServerVariables.application.serverTimeOffset, "YYYY-MM-DD HH:mm:ss Z"); + $scope.model.value = dateHelper.convertToServerStringTime(momentDate, Umbraco.Sys.ServerVariables.application.serverTimeOffset); + $scope.serverTime = dateHelper.convertToServerStringTime(momentDate, Umbraco.Sys.ServerVariables.application.serverTimeOffset, "YYYY-MM-DD HH:mm:ss Z"); } else { - $scope.model.value = elementData.getDate().format("YYYY-MM-DD HH:mm:ss"); + $scope.model.value = momentDate.format("YYYY-MM-DD HH:mm:ss"); } } else { - $scope.model.value = elementData.getDate().format("YYYY-MM-DD"); + $scope.model.value = momentDate.format("YYYY-MM-DD"); } } else { @@ -99,7 +134,7 @@ function dateTimePickerController($scope, notificationsService, assetsService, a } /** Sets the value of the date picker control adn associated viewModel objects based on the model value */ - function setDatePickerVal(element) { + function setDatePickerVal() { if ($scope.model.value) { var dateVal; //check if we are supposed to offset the time @@ -112,98 +147,21 @@ function dateTimePickerController($scope, notificationsService, assetsService, a //create a normal moment , no offset required var dateVal = $scope.model.value ? moment($scope.model.value, "YYYY-MM-DD HH:mm:ss") : moment(); } - - element.datetimepicker("setValue", dateVal); - $scope.datetimePickerValue = dateVal.format($scope.model.config.format); + $scope.model.datetimePickerValue = dateVal.format($scope.model.config.format); } else { $scope.clearDate(); } } - $scope.clearDate = function() { - $scope.hasDatetimePickerValue = false; - $scope.datetimePickerValue = null; - $scope.model.value = null; - $scope.datePickerForm.datepicker.$setValidity("pickerError", true); - } - - $scope.serverTime = null; - $scope.serverTimeNeedsOffsetting = false; - if (Umbraco.Sys.ServerVariables.application.serverTimeOffset !== undefined) { - // Will return something like 120 - var serverOffset = Umbraco.Sys.ServerVariables.application.serverTimeOffset; - - // Will return something like -120 - var localOffset = new Date().getTimezoneOffset(); - - // If these aren't equal then offsetting is needed - // note the minus in front of serverOffset needed - // because C# and javascript return the inverse offset - $scope.serverTimeNeedsOffsetting = (-serverOffset !== localOffset); - } - - //get the current user to see if we can localize this picker - userService.getCurrentUser().then(function (user) { - - assetsService.loadCss('lib/datetimepicker/bootstrap-datetimepicker.min.css', $scope).then(function() { - - var filesToLoad = ["lib/datetimepicker/bootstrap-datetimepicker.js"]; - - - $scope.model.config.language = user.locale; - - - assetsService.load(filesToLoad, $scope).then( - function () { - //The Datepicker js and css files are available and all components are ready to use. - - // Get the id of the datepicker button that was clicked - var pickerId = $scope.model.alias; - - var element = $element.find("div:first"); - - // Create the datepicker and add a changeDate eventlistener - element - .datetimepicker(angular.extend({ useCurrent: true }, $scope.model.config)) - .on("dp.change", applyDate) - .on("dp.error", function(a, b, c) { - $scope.hasDatetimePickerValue = false; - $scope.datePickerForm.datepicker.$setValidity("pickerError", false); - }); - - $(document).bind("click", $scope.hidePicker); - - setDatePickerVal(element); - - element.find("input").bind("blur", function() { - //we need to force an apply here - $scope.$apply(); - }); - - $scope.$watch("model.value", function(newVal, oldVal) { - if (newVal !== oldVal) { - $scope.hasDatetimePickerValue = newVal ? true : false; - setDatePickerVal(element); - } - }); - - var unsubscribe = $scope.$on("formSubmitting", function (ev, args) { - setModelValue(); - }); - - //Ensure to remove the event handler when this instance is destroyted - $scope.$on('$destroy', function () { - element.find("input").unbind("blur"); - element.datetimepicker("destroy"); - unsubscribe(); - $(document).unbind("click", $scope.hidePicker); - }); - - }); - }); - + $scope.$watch("model.value", function(newVal, oldVal) { + if (newVal !== oldVal) { + $scope.hasDatetimePickerValue = newVal ? true : false; + setDatePickerVal(); + } }); + + onInit(); } diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.html index 258d2040d5..47c9182650 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.html @@ -1,17 +1,30 @@
-
- +
- - - + + +
+ + + + +
+ +
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/slider/handle.prevalues.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/slider/handle.prevalues.html deleted file mode 100644 index 0821c5e757..0000000000 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/slider/handle.prevalues.html +++ /dev/null @@ -1,13 +0,0 @@ -
- - - - - Required - - -
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/slider/orientation.prevalues.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/slider/orientation.prevalues.html deleted file mode 100644 index 05fb6756d1..0000000000 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/slider/orientation.prevalues.html +++ /dev/null @@ -1,12 +0,0 @@ -
- - - - - Required - - -
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/slider/slider.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/slider/slider.controller.js index 588e2d2b03..c8813ee4c0 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/slider/slider.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/slider/slider.controller.js @@ -1,248 +1,80 @@ -function sliderController($scope, $log, $element, assetsService, angularHelper) { +function sliderController($scope) { - var sliderRef = null; + let sliderRef = null; /** configure some defaults on init */ function configureDefaults() { - - if (!$scope.model.config.orientation) { - $scope.model.config.orientation = "horizontal"; - } - if (!$scope.model.config.enableRange) { - $scope.model.config.enableRange = false; - } - else { - $scope.model.config.enableRange = Object.toBoolean($scope.model.config.enableRange); - } - - if (!$scope.model.config.initVal1) { - $scope.model.config.initVal1 = 0; - } - else { - $scope.model.config.initVal1 = parseFloat($scope.model.config.initVal1); - } - if (!$scope.model.config.initVal2) { - $scope.model.config.initVal2 = 0; - } - else { - $scope.model.config.initVal2 = parseFloat($scope.model.config.initVal2); - } - if (!$scope.model.config.minVal) { - $scope.model.config.minVal = 0; - } - else { - $scope.model.config.minVal = parseFloat($scope.model.config.minVal); - } - if (!$scope.model.config.maxVal) { - $scope.model.config.maxVal = 100; - } - else { - $scope.model.config.maxVal = parseFloat($scope.model.config.maxVal); - } - if (!$scope.model.config.step) { - $scope.model.config.step = 1; - } - else { - $scope.model.config.step = parseFloat($scope.model.config.step); - } - - if (!$scope.model.config.handle) { - $scope.model.config.handle = "round"; - } - - if (!$scope.model.config.reversed) { - $scope.model.config.reversed = false; - } - else { - $scope.model.config.reversed = Object.toBoolean($scope.model.config.reversed); - } - - if (!$scope.model.config.tooltip) { - $scope.model.config.tooltip = "show"; - } - - if (!$scope.model.config.tooltipSplit) { - $scope.model.config.tooltipSplit = false; - } - else { - $scope.model.config.tooltipSplit = Object.toBoolean($scope.model.config.tooltipSplit); - } - - if ($scope.model.config.tooltipFormat) { - $scope.model.config.formatter = function (value) { - if (angular.isArray(value) && $scope.model.config.enableRange) { - return $scope.model.config.tooltipFormat.replace("{0}", value[0]).replace("{1}", value[1]); - } else { - return $scope.model.config.tooltipFormat.replace("{0}", value); - } - } - } - - if (!$scope.model.config.ticks) { - $scope.model.config.ticks = []; - } - else if (angular.isString($scope.model.config.ticks)) { - // returns comma-separated string to an array, e.g. [0, 100, 200, 300, 400] - $scope.model.config.ticks = _.map($scope.model.config.ticks.split(','), function (item) { - return parseInt(item.trim()); - }); - } - - if (!$scope.model.config.ticksPositions) { - $scope.model.config.ticksPositions = []; - } - else if (angular.isString($scope.model.config.ticksPositions)) { - // returns comma-separated string to an array, e.g. [0, 30, 60, 70, 90, 100] - $scope.model.config.ticksPositions = _.map($scope.model.config.ticksPositions.split(','), function (item) { - return parseInt(item.trim()); - }); - } - - if (!$scope.model.config.ticksLabels) { - $scope.model.config.ticksLabels = []; - } - else if (angular.isString($scope.model.config.ticksLabels)) { - // returns comma-separated string to an array, e.g. ['$0', '$100', '$200', '$300', '$400'] - $scope.model.config.ticksLabels = _.map($scope.model.config.ticksLabels.split(','), function (item) { - return item.trim(); - }); - } - - if (!$scope.model.config.ticksSnapBounds) { - $scope.model.config.ticksSnapBounds = 0; - } - else { - $scope.model.config.ticksSnapBounds = parseFloat($scope.model.config.ticksSnapBounds); - } + $scope.model.config.enableRange = $scope.model.config.enableRange ? Object.toBoolean($scope.model.config.enableRange) : false; + $scope.model.config.initVal1 = $scope.model.config.initVal1 ? parseFloat($scope.model.config.initVal1) : 0; + $scope.model.config.initVal2 = $scope.model.config.initVal2 ? parseFloat($scope.model.config.initVal2) : 0; + $scope.model.config.minVal = $scope.model.config.minVal ? parseFloat($scope.model.config.minVal) : 0; + $scope.model.config.maxVal = $scope.model.config.maxVal ? parseFloat($scope.model.config.maxVal) : 100; + $scope.model.config.step = $scope.model.config.step ? parseFloat($scope.model.config.step) : 1; } - function getValueForSlider(val) { - - if (!angular.isArray(val)) { - val = val.toString().split(","); - } - var val1 = val[0]; - var val2 = val.length > 1 ? val[1] : null; - - //configure the model value based on if range is enabled or not - if ($scope.model.config.enableRange == true) { - var i1 = parseFloat(val1); - var i2 = parseFloat(val2); - return [ - isNaN(i1) ? $scope.model.config.minVal : (i1 >= $scope.model.config.minVal ? i1 : $scope.model.config.minVal), - isNaN(i2) ? $scope.model.config.maxVal : (i2 >= i1 ? (i2 <= $scope.model.config.maxVal ? i2 : $scope.model.config.maxVal) : $scope.model.config.maxVal) - ]; - } - else { - return parseFloat(val1); - } + function setModelValue(values) { + $scope.model.value = values ? values.toString() : null; } - /** This creates the slider with the model values - it's called on startup and returns a reference to the slider object */ - function createSlider() { + $scope.setup = function(slider) { + sliderRef = slider; + }; - //the value that we'll give the slider - if it's a range, we store our value as a comma separated val but this slider expects an array - var sliderVal = null; - - //configure the model value based on if range is enabled or not - if ($scope.model.config.enableRange == true) { - //If no value saved yet - then use default value - //If it contains a single value - then also create a new array value - if (!$scope.model.value || $scope.model.value.indexOf(",") == -1) { - sliderVal = getValueForSlider([$scope.model.config.initVal1, $scope.model.config.initVal2]); - } - else { - //this will mean it's a delimited value stored in the db, convert it to an array - sliderVal = getValueForSlider($scope.model.value.split(',')); - } - } - else { - //If no value saved yet - then use default value - if ($scope.model.value) { - sliderVal = getValueForSlider($scope.model.value); - } - else { - sliderVal = getValueForSlider($scope.model.config.initVal1); - } - } - - //initiate slider, add event handler and get the instance reference (stored in data) - var slider = $element.find('.slider-item').bootstrapSlider({ - max: $scope.model.config.maxVal, - min: $scope.model.config.minVal, - orientation: $scope.model.config.orientation, - selection: $scope.model.config.reversed ? "after" : "before", - step: $scope.model.config.step, - precision: $scope.model.config.precision, - tooltip: $scope.model.config.tooltip, - tooltip_split: $scope.model.config.tooltipSplit, - tooltip_position: $scope.model.config.tooltipPosition, - handle: $scope.model.config.handle, - reversed: $scope.model.config.reversed, - ticks: $scope.model.config.ticks, - ticks_positions: $scope.model.config.ticksPositions, - ticks_labels: $scope.model.config.ticksLabels, - ticks_snap_bounds: $scope.model.config.ticksSnapBounds, - formatter: $scope.model.config.formatter, - range: $scope.model.config.enableRange, - //set the slider val - we cannot do this with data- attributes when using ranges - value: sliderVal - }); - - slider.on('slideStop', function (e) { - var value = e.value; - angularHelper.safeApply($scope, function () { - $scope.model.value = getModelValueFromSlider(value); - }); - }).data('slider'); - - return slider; - } - - function getModelValueFromSlider(sliderVal) { - //Get the value from the slider and format it correctly, if it is a range we want a comma delimited value - if ($scope.model.config.enableRange == true) { - return sliderVal.join(","); - } - else { - return sliderVal.toString(); - } - } + $scope.end = function(values) { + setModelValue(values); + }; function init() { + // convert to array + $scope.sliderValue = $scope.model.value ? $scope.model.value.split(',') : null; + configureDefaults(); - //tell the assetsService to load the bootstrap slider - //libs from the plugin folder - assetsService - .loadJs("lib/slider/js/bootstrap-slider.js") - .then(function () { + // format config to fit slider plugin + const start = $scope.model.config.enableRange ? [$scope.model.config.initVal1, $scope.model.config.initVal2] : [$scope.model.config.initVal1]; + const step = $scope.model.config.step; + const tooltips = $scope.model.config.enableRange ? [true, true] : [true]; + const min = $scope.model.config.minVal ? [$scope.model.config.minVal] : [$scope.model.config.minVal]; + const max = $scope.model.config.maxVal ? [$scope.model.config.maxVal] : [$scope.model.config.maxVal]; - var slider = createSlider(); - - // Initialize model value if not set - if (!$scope.model.value) { - var sliderVal = slider.bootstrapSlider('getValue'); - $scope.model.value = getModelValueFromSlider(sliderVal); + // setup default + $scope.sliderOptions = { + "start": start, + "step": step, + "tooltips": tooltips, + "format": { + to: function (value) { + return Math.round(value); + }, + from: function (value) { + return Math.round(value); } + }, + "range": { + "min": min, + "max": max + }, + "pips": { + mode: 'steps', + density: 100, + filter: filterPips + } + }; - //watch for the model value being changed and update the slider value when it does - $scope.$watch("model.value", function (newVal, oldVal) { - if (newVal != oldVal) { - var sliderVal = getModelValueFromSlider(slider.bootstrapSlider('getValue')); - if (newVal !== sliderVal) { - slider.bootstrapSlider('setValue', getValueForSlider(newVal)); - } - } - }); + function filterPips(value) { + // show a pip for min and maximum value + return value === $scope.model.config.minVal || value === $scope.model.config.maxVal ? 1 : -1; + } - }); - - //load the separate css for the editor to avoid it blocking our js loading - assetsService.loadCss("lib/slider/bootstrap-slider.css", $scope); - assetsService.loadCss("lib/slider/bootstrap-slider-custom.css", $scope); } + + $scope.$watch('model.value', function(newValue, oldValue){ + if(newValue && newValue !== oldValue) { + $scope.sliderValue = newValue.split(','); + sliderRef.noUiSlider.set($scope.sliderValue); + } + }) init(); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/slider/slider.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/slider/slider.html index 638ecead4b..c147b30b23 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/slider/slider.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/slider/slider.html @@ -1,5 +1,12 @@ 
- +
+ + +
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/slider/tooltip.prevalues.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/slider/tooltip.prevalues.html deleted file mode 100644 index 415a39d3bd..0000000000 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/slider/tooltip.prevalues.html +++ /dev/null @@ -1,13 +0,0 @@ -
- - - - - Required - - -
diff --git a/src/Umbraco.Web.UI.Client/src/views/templates/edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/templates/edit.controller.js index af5f4db1e6..8ac9dc78e8 100644 --- a/src/Umbraco.Web.UI.Client/src/views/templates/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/templates/edit.controller.js @@ -191,7 +191,7 @@ $timeout(function() { var nameField = angular.element(document.querySelector('[data-element="editor-name-field"]')); if (nameField) { - nameField.bind('blur', function(event) { + nameField.on('blur', function(event) { if (event.target.value) { vm.save(true); } diff --git a/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.controller.js b/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.controller.js index d78095a90f..2b531b7ef2 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.controller.js @@ -531,26 +531,24 @@ // copy to clip board success function copySuccess() { - - if (vm.page.copyPasswordButtonState != "success") { - - vm.page.copyPasswordButtonState = "success"; - + if (vm.page.copyPasswordButtonState !== "success") { + $timeout(function(){ + vm.page.copyPasswordButtonState = "success"; + }); $timeout(function () { - resetClipboardButtonState() + resetClipboardButtonState(); }, 1000); } - } // copy to clip board error function copyError() { - if (vm.page.copyPasswordButtonState != "error") { - - vm.page.copyPasswordButtonState = "error"; - + if (vm.page.copyPasswordButtonState !== "error") { + $timeout(function() { + vm.page.copyPasswordButtonState = "error"; + }); $timeout(function () { - resetClipboardButtonState() + resetClipboardButtonState(); }, 1000); } } diff --git a/src/Umbraco.Web.UI.Client/test/e2e/app/admin/users/users-edit.scenario.js b/src/Umbraco.Web.UI.Client/test/e2e/app/admin/users/users-edit.scenario.js index b078ad0d9b..ea17450a08 100644 --- a/src/Umbraco.Web.UI.Client/test/e2e/app/admin/users/users-edit.scenario.js +++ b/src/Umbraco.Web.UI.Client/test/e2e/app/admin/users/users-edit.scenario.js @@ -4,7 +4,7 @@ describe('admin edit user', function() { browser().navigateTo('/admin/users/new'); input('user.email').enter('admin@abc.com'); input('user.password').enter('changeme'); - element('button.login').click(); + element('button.login').trigger('click'); }); it('enables the save button when the user info is filled in correctly', function() { diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index a16e9544bb..d0433b1a77 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -150,13 +150,6 @@ create.aspx - - UserControlProxy.aspx - ASPXCodeBehind - - - UserControlProxy.aspx - editMacro.aspx ASPXCodeBehind @@ -200,6 +193,8 @@ + + @@ -235,9 +230,9 @@ - + 404handlers.config diff --git a/src/Umbraco.Web.UI/Umbraco/Views/Default.cshtml b/src/Umbraco.Web.UI/Umbraco/Views/Default.cshtml index 0d0c39aba7..4659674c59 100644 --- a/src/Umbraco.Web.UI/Umbraco/Views/Default.cshtml +++ b/src/Umbraco.Web.UI/Umbraco/Views/Default.cshtml @@ -67,7 +67,7 @@
-
+
diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml index 5b39f3d25f..fade57e934 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml @@ -340,6 +340,7 @@ Publishing will make the selected items visible on the site. Unpublishing will remove the selected items and all their descendants from the site. Unpublishing will remove this page and all its descendants from the site. + You have unsaved changes. Making changes to the Document Type will discard the changes. Done diff --git a/src/Umbraco.Web.UI/Umbraco/dashboard/UserControlProxy.aspx b/src/Umbraco.Web.UI/Umbraco/dashboard/UserControlProxy.aspx deleted file mode 100644 index ac59074de3..0000000000 --- a/src/Umbraco.Web.UI/Umbraco/dashboard/UserControlProxy.aspx +++ /dev/null @@ -1,25 +0,0 @@ -<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="UserControlProxy.aspx.cs" Inherits="Umbraco.Web.UI.Umbraco.Dashboard.UserControlProxy" %> -<%@ Register TagPrefix="umb" Namespace="ClientDependency.Core.Controls" Assembly="ClientDependency.Core" %> -<%@ Register TagPrefix="cc1" Namespace="Umbraco.Web.UI.JavaScript" Assembly="umbraco" %> - - - - - - - - - - - - - - - - -
- -
- - - diff --git a/src/Umbraco.Web.UI/Umbraco/dashboard/UserControlProxy.aspx.cs b/src/Umbraco.Web.UI/Umbraco/dashboard/UserControlProxy.aspx.cs deleted file mode 100644 index 40caff36c8..0000000000 --- a/src/Umbraco.Web.UI/Umbraco/dashboard/UserControlProxy.aspx.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System; -using System.Web.UI; -using Umbraco.Core.IO; - - -namespace Umbraco.Web.UI.Umbraco.Dashboard -{ - public partial class UserControlProxy : Pages.UmbracoEnsuredPage - { - protected void Page_Load(object sender, EventArgs e) - { - - } - - protected override void OnInit(EventArgs e) - { - base.OnInit(e); - - var path = Request.QueryString["ctrl"]; - if (string.IsNullOrEmpty(path) == false) - { - path = IOHelper.FindFile(path); - - try - { - var c = LoadControl(path); - container.Controls.Add(c); - } - catch (Exception ee) - { - container.Controls.Add( - new LiteralControl( - "

Could not load control: '" + path + - "'.
Error message: " + - ee.ToString() + "

")); - } - } - } - } -} diff --git a/src/Umbraco.Web.UI/Umbraco/dashboard/UserControlProxy.aspx.designer.cs b/src/Umbraco.Web.UI/Umbraco/dashboard/UserControlProxy.aspx.designer.cs deleted file mode 100644 index 5bfa6983d1..0000000000 --- a/src/Umbraco.Web.UI/Umbraco/dashboard/UserControlProxy.aspx.designer.cs +++ /dev/null @@ -1,69 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace Umbraco.Web.UI.Umbraco.Dashboard { - - - public partial class UserControlProxy { - - /// - /// ClientLoader control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::Umbraco.Web.UI.JavaScript.UmbracoClientDependencyLoader ClientLoader; - - /// - /// CssInclude1 control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::ClientDependency.Core.Controls.CssInclude CssInclude1; - - /// - /// JsInclude1 control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::ClientDependency.Core.Controls.JsInclude JsInclude1; - - /// - /// JsInclude4 control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::ClientDependency.Core.Controls.JsInclude JsInclude4; - - /// - /// form1 control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::System.Web.UI.HtmlControls.HtmlForm form1; - - /// - /// container control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::System.Web.UI.WebControls.PlaceHolder container; - } -} diff --git a/src/Umbraco.Web.UI/config/Dashboard.Release.config b/src/Umbraco.Web.UI/config/Dashboard.Release.config index 0a39395213..fec6ab34ae 100644 --- a/src/Umbraco.Web.UI/config/Dashboard.Release.config +++ b/src/Umbraco.Web.UI/config/Dashboard.Release.config @@ -1,115 +1,107 @@  -
- - settings - - - - views/dashboard/settings/settingsdashboardintro.html - - - - +
+ + settings + + + + views/dashboard/settings/settingsdashboardintro.html + + + + views/dashboard/settings/examinemanagement.html - - - + + + views/dashboard/settings/publishedstatus.html - -
+
+
-
- - forms - - - - views/dashboard/forms/formsdashboardintro.html - - -
+
+ + forms + + + + views/dashboard/forms/formsdashboardintro.html + + +
-
- - developer - -
+
+ + media + + + + views/dashboard/media/mediafolderbrowser.html + + +
-
- - media - - - - views/dashboard/media/mediafolderbrowser.html - - +
+ + translator + + + content + + + + admin + -
+ + views/dashboard/default/startupdashboardintro.html + + +
-
- - translator - - - content - - - - admin - +
+ + member + + + + views/dashboard/members/membersdashboardvideos.html + + +
- - views/dashboard/default/startupdashboardintro.html - +
+ + settings + + + + /App_Plugins/ModelsBuilder/modelsbuilder.htm + + +
-
-
- -
- - member - - - - views/dashboard/members/membersdashboardvideos.html - - -
- -
- - developer - - - - /App_Plugins/ModelsBuilder/modelsbuilder.htm - - -
- -
- - settings - - - - views/dashboard/settings/healthcheck.html - - -
-
- - content - - - - views/dashboard/content/redirecturls.html - - -
+
+ + settings + + + + views/dashboard/settings/healthcheck.html + + +
+
+ + content + + + + views/dashboard/content/redirecturls.html + + +
diff --git a/src/Umbraco.Web.UI/config/Dashboard.config b/src/Umbraco.Web.UI/config/Dashboard.config index fab6fa6ea3..fec6ab34ae 100644 --- a/src/Umbraco.Web.UI/config/Dashboard.config +++ b/src/Umbraco.Web.UI/config/Dashboard.config @@ -1,55 +1,49 @@  +
settings - + views/dashboard/settings/settingsdashboardintro.html - views/dashboard/settings/examinemanagement.html - + views/dashboard/settings/examinemanagement.html + - views/dashboard/settings/publishedstatus.html - + views/dashboard/settings/publishedstatus.html +
+
forms - + views/dashboard/forms/formsdashboardintro.html -
+
+
media - + views/dashboard/media/mediafolderbrowser.html
-
- - forms - - - - views/dashboard/forms/formsdashboardintro.html - - -
+
translator @@ -61,29 +55,35 @@ admin - + + views/dashboard/default/startupdashboardintro.html
+
member - + views/dashboard/members/membersdashboardvideos.html
-
+ +
- contour + settings - - plugins/umbracocontour/formsdashboard.ascx + + + /App_Plugins/ModelsBuilder/modelsbuilder.htm +
+
settings @@ -104,14 +104,4 @@
-
- - settings - - - - /App_Plugins/ModelsBuilder/modelsbuilder.htm - - -
diff --git a/src/Umbraco.Web/Actions/ActionCollection.cs b/src/Umbraco.Web/Actions/ActionCollection.cs index 64cf950c60..89ac8a59f4 100644 --- a/src/Umbraco.Web/Actions/ActionCollection.cs +++ b/src/Umbraco.Web/Actions/ActionCollection.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using System.Globalization; using System.Linq; using Umbraco.Core; using Umbraco.Core.Composing; @@ -22,19 +21,21 @@ namespace Umbraco.Web.Actions internal IEnumerable GetByLetters(IEnumerable letters) { - var all = this.ToArray(); - return letters.Select(x => all.FirstOrDefault(y => y.Letter.ToString(CultureInfo.InvariantCulture) == x)) + var actions = this.ToArray(); // no worry: internally, it's already an array + return letters + .Where(x => x.Length == 1) + .Select(x => actions.FirstOrDefault(y => y.Letter == x[0])) .WhereNotNull() - .ToArray(); + .ToList(); } internal IReadOnlyList FromEntityPermission(EntityPermission entityPermission) { + var actions = this.ToArray(); // no worry: internally, it's already an array return entityPermission.AssignedPermissions .Where(x => x.Length == 1) - .Select(x => x.ToCharArray()[0]) - .SelectMany(c => this.Where(x => x.Letter == c)) - .Where(action => action != null) + .SelectMany(x => actions.Where(y => y.Letter == x[0])) + .WhereNotNull() .ToList(); } } diff --git a/src/Umbraco.Web/Actions/ActionCollectionBuilder.cs b/src/Umbraco.Web/Actions/ActionCollectionBuilder.cs index 6002c8d2b0..079705645d 100644 --- a/src/Umbraco.Web/Actions/ActionCollectionBuilder.cs +++ b/src/Umbraco.Web/Actions/ActionCollectionBuilder.cs @@ -1,11 +1,9 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Reflection; using LightInject; using Umbraco.Core.Composing; - namespace Umbraco.Web.Actions { internal class ActionCollectionBuilder : LazyCollectionBuilderBase @@ -19,13 +17,13 @@ namespace Umbraco.Web.Actions protected override IEnumerable CreateItems(params object[] args) { var items = base.CreateItems(args).ToList(); + //validate the items, no actions should exist that do not either expose notifications or permissions - var invalid = items.Where(x => !x.CanBePermissionAssigned && !x.ShowInNotifier).ToList(); - if (invalid.Count > 0) - { - throw new InvalidOperationException($"Invalid actions '{string.Join(", ", invalid.Select(x => x.Alias))}'. All {typeof(IAction)} implementations must be true for either {nameof(IAction.CanBePermissionAssigned)} or {nameof(IAction.ShowInNotifier)}"); - } - return items; + var invalidItems = items.Where(x => !x.CanBePermissionAssigned && !x.ShowInNotifier).ToList(); + if (invalidItems.Count == 0) return items; + + var invalidActions = string.Join(", ", invalidItems.Select(x => "'" + x.Alias + "'")); + throw new InvalidOperationException($"Invalid actions {invalidActions}'. All {typeof(IAction)} implementations must be true for either {nameof(IAction.CanBePermissionAssigned)} or {nameof(IAction.ShowInNotifier)}."); } } } diff --git a/src/Umbraco.Web/Editors/DashboardController.cs b/src/Umbraco.Web/Editors/DashboardController.cs index e88b9975e7..72b7acc9e7 100644 --- a/src/Umbraco.Web/Editors/DashboardController.cs +++ b/src/Umbraco.Web/Editors/DashboardController.cs @@ -1,126 +1,126 @@ -using System.Collections.Generic; -using Umbraco.Core; -using Umbraco.Core.Configuration; -using Umbraco.Web.Models.ContentEditing; -using Umbraco.Web.Mvc; -using System.Linq; -using Umbraco.Core.IO; -using Newtonsoft.Json.Linq; -using System.Threading.Tasks; -using System.Net.Http; -using System.Web.Http; -using System; -using System.Net; -using System.Text; -using Umbraco.Core.Cache; -using Umbraco.Web.WebApi; -using Umbraco.Web.WebApi.Filters; -using Umbraco.Core.Logging; - -namespace Umbraco.Web.Editors -{ - //we need to fire up the controller like this to enable loading of remote css directly from this controller - [PluginController("UmbracoApi")] - [ValidationFilter] - [AngularJsonOnlyConfiguration] - [IsBackOffice] - [WebApi.UmbracoAuthorize] - public class DashboardController : UmbracoApiController - { - //we have just one instance of HttpClient shared for the entire application - private static readonly HttpClient HttpClient = new HttpClient(); - //we have baseurl as a param to make previewing easier, so we can test with a dev domain from client side - [ValidateAngularAntiForgeryToken] - public async Task GetRemoteDashboardContent(string section, string baseUrl = "https://dashboard.umbraco.org/") - { - var context = UmbracoContext.Current; - if (context == null) - throw new HttpResponseException(HttpStatusCode.InternalServerError); - - var user = Security.CurrentUser; - var allowedSections = string.Join(",", user.AllowedSections); - var language = user.Language; - var version = UmbracoVersion.SemanticVersion.ToSemanticString(); - - var url = string.Format(baseUrl + "{0}?section={0}&allowed={1}&lang={2}&version={3}", section, allowedSections, language, version); - var key = "umbraco-dynamic-dashboard-" + language + allowedSections.Replace(",", "-") + section; - - var content = ApplicationCache.RuntimeCache.GetCacheItem(key); - var result = new JObject(); - if (content != null) - { - result = content; - } - else - { - //content is null, go get it - try - { - //fetch dashboard json and parse to JObject - var json = await HttpClient.GetStringAsync(url); - content = JObject.Parse(json); - result = content; - - ApplicationCache.RuntimeCache.InsertCacheItem(key, () => result, new TimeSpan(0, 30, 0)); - } - catch (HttpRequestException ex) - { - Logger.Error(ex.InnerException ?? ex, "Error getting dashboard content from '{Url}'", url); - - //it's still new JObject() - we return it like this to avoid error codes which triggers UI warnings - ApplicationCache.RuntimeCache.InsertCacheItem(key, () => result, new TimeSpan(0, 5, 0)); - } - } - - return result; - } - - public async Task GetRemoteDashboardCss(string section, string baseUrl = "https://dashboard.umbraco.org/") - { - var url = string.Format(baseUrl + "css/dashboard.css?section={0}", section); - var key = "umbraco-dynamic-dashboard-css-" + section; - - var content = ApplicationCache.RuntimeCache.GetCacheItem(key); - var result = string.Empty; - - if (content != null) - { - result = content; - } - else - { - //content is null, go get it - try - { - //fetch remote css - content = await HttpClient.GetStringAsync(url); - - //can't use content directly, modified closure problem - result = content; - - //save server content for 30 mins - ApplicationCache.RuntimeCache.InsertCacheItem(key, () => result, new TimeSpan(0, 30, 0)); - } - catch (HttpRequestException ex) - { - Logger.Error(ex.InnerException ?? ex, "Error getting dashboard CSS from '{Url}'", url); - - //it's still string.Empty - we return it like this to avoid error codes which triggers UI warnings - ApplicationCache.RuntimeCache.InsertCacheItem(key, () => result, new TimeSpan(0, 5, 0)); - } - } - - return new HttpResponseMessage(HttpStatusCode.OK) - { - Content = new StringContent(result, Encoding.UTF8, "text/css") - }; - } - - [ValidateAngularAntiForgeryToken] - public IEnumerable> GetDashboard(string section) - { - var dashboardHelper = new DashboardHelper(Services.SectionService); - return dashboardHelper.GetDashboard(section, Security.CurrentUser); - } - } -} +using System.Collections.Generic; +using Umbraco.Core; +using Umbraco.Core.Configuration; +using Umbraco.Web.Models.ContentEditing; +using Umbraco.Web.Mvc; +using Newtonsoft.Json.Linq; +using System.Threading.Tasks; +using System.Net.Http; +using System.Web.Http; +using System; +using System.Net; +using System.Text; +using Umbraco.Core.Cache; +using Umbraco.Web.WebApi; +using Umbraco.Web.WebApi.Filters; +using Umbraco.Core.Logging; + +namespace Umbraco.Web.Editors +{ + //we need to fire up the controller like this to enable loading of remote css directly from this controller + [PluginController("UmbracoApi")] + [ValidationFilter] + [AngularJsonOnlyConfiguration] + [IsBackOffice] + [WebApi.UmbracoAuthorize] + public class DashboardController : UmbracoApiController + { + private readonly Dashboards _dashboards; + + public DashboardController(Dashboards dashboards) + { + _dashboards = dashboards; + } + + //we have just one instance of HttpClient shared for the entire application + private static readonly HttpClient HttpClient = new HttpClient(); + //we have baseurl as a param to make previewing easier, so we can test with a dev domain from client side + [ValidateAngularAntiForgeryToken] + public async Task GetRemoteDashboardContent(string section, string baseUrl = "https://dashboard.umbraco.org/") + { + var user = Security.CurrentUser; + var allowedSections = string.Join(",", user.AllowedSections); + var language = user.Language; + var version = UmbracoVersion.SemanticVersion.ToSemanticString(); + + var url = string.Format(baseUrl + "{0}?section={0}&allowed={1}&lang={2}&version={3}", section, allowedSections, language, version); + var key = "umbraco-dynamic-dashboard-" + language + allowedSections.Replace(",", "-") + section; + + var content = ApplicationCache.RuntimeCache.GetCacheItem(key); + var result = new JObject(); + if (content != null) + { + result = content; + } + else + { + //content is null, go get it + try + { + //fetch dashboard json and parse to JObject + var json = await HttpClient.GetStringAsync(url); + content = JObject.Parse(json); + result = content; + + ApplicationCache.RuntimeCache.InsertCacheItem(key, () => result, new TimeSpan(0, 30, 0)); + } + catch (HttpRequestException ex) + { + Logger.Error(ex.InnerException ?? ex, "Error getting dashboard content from '{Url}'", url); + + //it's still new JObject() - we return it like this to avoid error codes which triggers UI warnings + ApplicationCache.RuntimeCache.InsertCacheItem(key, () => result, new TimeSpan(0, 5, 0)); + } + } + + return result; + } + + public async Task GetRemoteDashboardCss(string section, string baseUrl = "https://dashboard.umbraco.org/") + { + var url = string.Format(baseUrl + "css/dashboard.css?section={0}", section); + var key = "umbraco-dynamic-dashboard-css-" + section; + + var content = ApplicationCache.RuntimeCache.GetCacheItem(key); + var result = string.Empty; + + if (content != null) + { + result = content; + } + else + { + //content is null, go get it + try + { + //fetch remote css + content = await HttpClient.GetStringAsync(url); + + //can't use content directly, modified closure problem + result = content; + + //save server content for 30 mins + ApplicationCache.RuntimeCache.InsertCacheItem(key, () => result, new TimeSpan(0, 30, 0)); + } + catch (HttpRequestException ex) + { + Logger.Error(ex.InnerException ?? ex, "Error getting dashboard CSS from '{Url}'", url); + + //it's still string.Empty - we return it like this to avoid error codes which triggers UI warnings + ApplicationCache.RuntimeCache.InsertCacheItem(key, () => result, new TimeSpan(0, 5, 0)); + } + } + + return new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(result, Encoding.UTF8, "text/css") + }; + } + + [ValidateAngularAntiForgeryToken] + public IEnumerable> GetDashboard(string section) + { + return _dashboards.GetDashboards(section, Security.CurrentUser); + } + } +} diff --git a/src/Umbraco.Web/Editors/DashboardHelper.cs b/src/Umbraco.Web/Editors/DashboardHelper.cs deleted file mode 100644 index 75ccda1a9b..0000000000 --- a/src/Umbraco.Web/Editors/DashboardHelper.cs +++ /dev/null @@ -1,99 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Umbraco.Core; -using Umbraco.Core.Configuration; -using Umbraco.Core.IO; -using Umbraco.Core.Models.Membership; -using Umbraco.Core.Services; -using Umbraco.Web.Models.ContentEditing; - -namespace Umbraco.Web.Editors -{ - internal class DashboardHelper - { - private readonly ISectionService _sectionService; - - public DashboardHelper(ISectionService sectionService) - { - if (sectionService == null) throw new ArgumentNullException("sectionService"); - _sectionService = sectionService; - } - - /// - /// Returns the dashboard models per section for the current user and it's access - /// - /// - /// - public IDictionary>> GetDashboards(IUser currentUser) - { - var result = new Dictionary>>(); - foreach (var section in _sectionService.GetSections()) - { - result[section.Alias] = GetDashboard(section.Alias, currentUser); - } - return result; - } - - /// - /// Returns the dashboard model for the given section based on the current user and it's access - /// - /// - /// - /// - public IEnumerable> GetDashboard(string section, IUser currentUser) - { - var tabs = new List>(); - var i = 1; - - //disable packages section dashboard - if (section == "packages") return tabs; - - foreach (var dashboardSection in UmbracoConfig.For.DashboardSettings().Sections.Where(x => x.Areas.Contains(section))) - { - //we need to validate access to this section - if (DashboardSecurity.AuthorizeAccess(dashboardSection, currentUser, _sectionService) == false) - continue; - - //User is authorized - foreach (var tab in dashboardSection.Tabs) - { - //we need to validate access to this tab - if (DashboardSecurity.AuthorizeAccess(tab, currentUser, _sectionService) == false) - continue; - - var dashboardControls = new List(); - - foreach (var control in tab.Controls) - { - if (DashboardSecurity.AuthorizeAccess(control, currentUser, _sectionService) == false) - continue; - - var dashboardControl = new DashboardControl(); - var controlPath = control.ControlPath.Trim(); - dashboardControl.Caption = control.PanelCaption; - dashboardControl.Path = IOHelper.FindFile(controlPath); - if (controlPath.ToLowerInvariant().EndsWith(".ascx".ToLowerInvariant())) - dashboardControl.ServerSide = true; - - dashboardControls.Add(dashboardControl); - } - - tabs.Add(new Tab - { - Id = i, - Alias = tab.Caption.ToSafeAlias(), - IsActive = i == 1, - Label = tab.Caption, - Properties = dashboardControls - }); - - i++; - } - } - - //In case there are no tabs or a user doesn't have access the empty tabs list is returned - return tabs; - } - } -} diff --git a/src/Umbraco.Web/Editors/DashboardSecurity.cs b/src/Umbraco.Web/Editors/DashboardSecurity.cs index 87ed5af196..1481606c7e 100644 --- a/src/Umbraco.Web/Editors/DashboardSecurity.cs +++ b/src/Umbraco.Web/Editors/DashboardSecurity.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Globalization; using System.Linq; using Umbraco.Core; @@ -17,105 +18,97 @@ namespace Umbraco.Web.Editors public static bool AuthorizeAccess(ISection dashboardSection, IUser user, ISectionService sectionService) { - if (user.Id.ToString(CultureInfo.InvariantCulture) == 0.ToInvariantString()) - { - return true; - } - - var denyTypes = dashboardSection.AccessRights.Rules.Where(x => x.Action == AccessType.Deny).ToArray(); - var grantedTypes = dashboardSection.AccessRights.Rules.Where(x => x.Action == AccessType.Grant).ToArray(); - var grantedBySectionTypes = dashboardSection.AccessRights.Rules.Where(x => x.Action == AccessType.GrantBySection).ToArray(); - - return CheckUserAccessByRules(user, sectionService, denyTypes, grantedTypes, grantedBySectionTypes); + return CheckUserAccessByRules(user, sectionService, dashboardSection.AccessRights.Rules); } public static bool AuthorizeAccess(IDashboardTab dashboardTab, IUser user, ISectionService sectionService) { - if (user.Id.ToString(CultureInfo.InvariantCulture) == Constants.System.Root.ToInvariantString()) - { - return true; - } - - var denyTypes = dashboardTab.AccessRights.Rules.Where(x => x.Action == AccessType.Deny).ToArray(); - var grantedTypes = dashboardTab.AccessRights.Rules.Where(x => x.Action == AccessType.Grant).ToArray(); - var grantedBySectionTypes = dashboardTab.AccessRights.Rules.Where(x => x.Action == AccessType.GrantBySection).ToArray(); - - return CheckUserAccessByRules(user, sectionService, denyTypes, grantedTypes, grantedBySectionTypes); + return CheckUserAccessByRules(user, sectionService, dashboardTab.AccessRights.Rules); } - public static bool AuthorizeAccess(IDashboardControl dashboardTab, IUser user, ISectionService sectionService) + public static bool AuthorizeAccess(IDashboardControl dashboardControl, IUser user, ISectionService sectionService) { - if (user.Id.ToString(CultureInfo.InvariantCulture) == Constants.System.Root.ToInvariantString()) - { - return true; - } - - var denyTypes = dashboardTab.AccessRights.Rules.Where(x => x.Action == AccessType.Deny).ToArray(); - var grantedTypes = dashboardTab.AccessRights.Rules.Where(x => x.Action == AccessType.Grant).ToArray(); - var grantedBySectionTypes = dashboardTab.AccessRights.Rules.Where(x => x.Action == AccessType.GrantBySection).ToArray(); - - return CheckUserAccessByRules(user, sectionService, denyTypes, grantedTypes, grantedBySectionTypes); + return CheckUserAccessByRules(user, sectionService, dashboardControl.AccessRights.Rules); } - public static bool CheckUserAccessByRules(IUser user, ISectionService sectionService, IAccessItem[] denyTypes, IAccessItem[] grantedTypes, IAccessItem[] grantedBySectionTypes) + private static (IAccessRule[], IAccessRule[], IAccessRule[]) GroupRules(IEnumerable rules) { - var allowedSoFar = false; + IAccessRule[] denyRules = null, grantRules = null, grantBySectionRules = null; - // if there's no grantBySection or grant rules defined - we allow access so far and skip to checking deny rules - if (grantedBySectionTypes.Any() == false && grantedTypes.Any() == false) + var groupedRules = rules.GroupBy(x => x.Type); + foreach (var group in groupedRules) { - allowedSoFar = true; + var a = group.ToArray(); + switch (group.Key) + { + case AccessRuleType.Deny: + denyRules = a; + break; + case AccessRuleType.Grant: + grantRules = a; + break; + case AccessRuleType.GrantBySection: + grantBySectionRules = a; + break; + default: + throw new Exception("panic"); + } } - // else we check the rules and only allow if one matches - else + + return (denyRules ?? Array.Empty(), grantRules ?? Array.Empty(), grantBySectionRules ?? Array.Empty()); + } + + public static bool CheckUserAccessByRules(IUser user, ISectionService sectionService, IEnumerable rules) + { + if (user.Id == Constants.Security.SuperUserId) + return true; + + var (denyRules, grantRules, grantBySectionRules) = GroupRules(rules); + + var hasAccess = true; + string[] assignedUserGroups = null; + + // if there are no grant rules, then access is granted by default, unless denied + // otherwise, grant rules determine if access can be granted at all + if (grantBySectionRules.Length > 0 || grantRules.Length > 0) { + hasAccess = false; + // check if this item has any grant-by-section arguments. // if so check if the user has access to any of the sections approved, if so they will be allowed to see it (so far) - if (grantedBySectionTypes.Any()) + if (grantBySectionRules.Length > 0) { - var allowedApps = sectionService.GetAllowedSections(Convert.ToInt32(user.Id)) - .Select(x => x.Alias) - .ToArray(); + var allowedSections = sectionService.GetAllowedSections(user.Id).Select(x => x.Alias).ToArray(); + var wantedSections = grantBySectionRules.SelectMany(g => g.Value.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)).ToArray(); - var allApprovedSections = grantedBySectionTypes.SelectMany(g => g.Value.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)).ToArray(); - if (allApprovedSections.Any(allowedApps.Contains)) - { - allowedSoFar = true; - } + if (wantedSections.Intersect(allowedSections).Any()) + hasAccess = true; } // if not already granted access, check if this item as any grant arguments. // if so check if the user is in one of the user groups approved, if so they will be allowed to see it (so far) - if (allowedSoFar == false && grantedTypes.Any()) + if (hasAccess == false && grantRules.Any()) { - var allApprovedUserTypes = grantedTypes.SelectMany(g => g.Value.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)).ToArray(); - foreach (var userGroup in user.Groups) - { - if (allApprovedUserTypes.InvariantContains(userGroup.Alias)) - { - allowedSoFar = true; - break; - } - } + assignedUserGroups = user.Groups.Select(x => x.Alias).ToArray(); + var wantedUserGroups = grantRules.SelectMany(g => g.Value.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)).ToArray(); + + if (wantedUserGroups.Intersect(assignedUserGroups).Any()) + hasAccess = true; } } + if (!hasAccess || denyRules.Length == 0) + return false; + // check if this item has any deny arguments, if so check if the user is in one of the denied user groups, if so they will // be denied to see it no matter what - if (denyTypes.Any()) - { - var allDeniedUserTypes = denyTypes.SelectMany(g => g.Value.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)).ToArray(); - foreach (var userGroup in user.Groups) - { - if (allDeniedUserTypes.InvariantContains(userGroup.Alias)) - { - allowedSoFar = false; - break; - } - } - } + assignedUserGroups = assignedUserGroups ?? user.Groups.Select(x => x.Alias).ToArray(); + var deniedUserGroups = denyRules.SelectMany(g => g.Value.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)).ToArray(); - return allowedSoFar; + if (deniedUserGroups.Intersect(assignedUserGroups).Any()) + hasAccess = false; + + return hasAccess; } } } diff --git a/src/Umbraco.Web/Editors/Dashboards.cs b/src/Umbraco.Web/Editors/Dashboards.cs new file mode 100644 index 0000000000..4bf2d81045 --- /dev/null +++ b/src/Umbraco.Web/Editors/Dashboards.cs @@ -0,0 +1,159 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Umbraco.Core; +using Umbraco.Core.Configuration.Dashboard; +using Umbraco.Core.IO; +using Umbraco.Core.Manifest; +using Umbraco.Core.Models.Membership; +using Umbraco.Core.Services; +using Umbraco.Web.Models.ContentEditing; + +namespace Umbraco.Web.Editors +{ + public class Dashboards + { + private readonly ISectionService _sectionService; + private readonly IDashboardSection _dashboardSection; + private readonly ManifestParser _manifestParser; + + public Dashboards(ISectionService sectionService, IDashboardSection dashboardSection, ManifestParser manifestParser) + { + _sectionService = sectionService ?? throw new ArgumentNullException(nameof(sectionService)); + _dashboardSection = dashboardSection; + _manifestParser = manifestParser; + } + + /// + /// Gets all dashboards, organized by section, for a user. + /// + public IDictionary>> GetDashboards(IUser currentUser) + { + return _sectionService.GetSections().ToDictionary(x => x.Alias, x => GetDashboards(x.Alias, currentUser)); + } + + /// + /// Returns dashboards for a specific section, for a user. + /// + public IEnumerable> GetDashboards(string section, IUser currentUser) + { + var tabId = 1; + var configDashboards = GetDashboardsFromConfig(ref tabId, section, currentUser); + var pluginDashboards = GetDashboardsFromPlugins(ref tabId, section, currentUser); + + // merge dashboards + // both collections contain tab.alias -> controls + var dashboards = configDashboards; + + // until now, it was fine to have duplicate tab.aliases in configDashboard + // so... the rule should be - just merge whatever we get, don't be clever + dashboards.AddRange(pluginDashboards); + + // re-sort by id + dashboards.Sort((tab1, tab2) => tab1.Id > tab2.Id ? 1 : 0); + + // re-assign ids (why?) + var i = 1; + foreach (var tab in dashboards) + { + tab.Id = i++; + tab.IsActive = tab.Id == 1; + } + + return configDashboards; + } + + // note: + // in dashboard.config we have 'sections' which define 'tabs' for 'areas' + // and 'areas' are the true UI sections - and each tab can have more than + // one control + // in a manifest, we directly have 'dashboards' which map to a unique + // control in a tab + + // gets all tabs & controls from the config file + private List> GetDashboardsFromConfig(ref int tabId, string section, IUser currentUser) + { + var tabs = new List>(); + + // disable packages section dashboard + if (section == "packages") return tabs; + + foreach (var dashboardSection in _dashboardSection.Sections.Where(x => x.Areas.InvariantContains(section))) + { + // validate access to this section + if (!DashboardSecurity.AuthorizeAccess(dashboardSection, currentUser, _sectionService)) + continue; + + foreach (var tab in dashboardSection.Tabs) + { + // validate access to this tab + if (!DashboardSecurity.AuthorizeAccess(tab, currentUser, _sectionService)) + continue; + + var dashboardControls = new List(); + + foreach (var control in tab.Controls) + { + // validate access to this control + if (!DashboardSecurity.AuthorizeAccess(control, currentUser, _sectionService)) + continue; + + // create and add control + var dashboardControl = new DashboardControl + { + Caption = control.PanelCaption, + Path = IOHelper.FindFile(control.ControlPath.Trim()) + }; + + if (dashboardControl.Path.InvariantEndsWith(".ascx")) + throw new NotSupportedException("Legacy UserControl (.ascx) dashboards are no longer supported."); + + dashboardControls.Add(dashboardControl); + } + + // create and add tab + tabs.Add(new Tab + { + Id = tabId++, + Alias = tab.Caption.ToSafeAlias(), + Label = tab.Caption, + Properties = dashboardControls + }); + } + } + + return tabs; + } + + private List> GetDashboardsFromPlugins(ref int tabId, string section, IUser currentUser) + { + var tabs = new List>(); + + foreach (var dashboard in _manifestParser.Manifest.Dashboards.Where(x => x.Sections.InvariantContains(section)).OrderBy(x => x.Weight)) + { + // validate access + if (!DashboardSecurity.CheckUserAccessByRules(currentUser, _sectionService, dashboard.AccessRules)) + continue; + + var dashboardControl = new DashboardControl + { + Caption = "", + Path = IOHelper.FindFile(dashboard.View.Trim()) + }; + + if (dashboardControl.Path.InvariantEndsWith(".ascx")) + throw new NotSupportedException("Legacy UserControl (.ascx) dashboards are no longer supported."); + + tabs.Add(new Tab + { + Id = tabId++, + Alias = dashboard.Alias.ToSafeAlias(), + Label = dashboard.Name, + Properties = new[] { dashboardControl } + }); + } + + return tabs; + } + } +} diff --git a/src/Umbraco.Web/Editors/DictionaryController.cs b/src/Umbraco.Web/Editors/DictionaryController.cs index 7d846e68ec..cd3141c7b9 100644 --- a/src/Umbraco.Web/Editors/DictionaryController.cs +++ b/src/Umbraco.Web/Editors/DictionaryController.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Net; using System.Net.Http; using System.Web.Http; @@ -194,7 +195,7 @@ namespace Umbraco.Web.Editors const int level = 0; - foreach (var dictionaryItem in Services.LocalizationService.GetRootDictionaryItems()) + foreach (var dictionaryItem in Services.LocalizationService.GetRootDictionaryItems().OrderBy(ItemSort())) { var item = Mapper.Map(dictionaryItem); item.Level = 0; @@ -220,8 +221,7 @@ namespace Umbraco.Web.Editors /// private void GetChildItemsForList(IDictionaryItem dictionaryItem, int level, List list) { - foreach (var childItem in Services.LocalizationService.GetDictionaryItemChildren( - dictionaryItem.Key)) + foreach (var childItem in Services.LocalizationService.GetDictionaryItemChildren(dictionaryItem.Key).OrderBy(ItemSort())) { var item = Mapper.Map(childItem); item.Level = level; @@ -230,5 +230,7 @@ namespace Umbraco.Web.Editors GetChildItemsForList(childItem, level + 1, list); } } + + private Func ItemSort() => item => item.ItemKey; } } diff --git a/src/Umbraco.Web/Editors/SectionController.cs b/src/Umbraco.Web/Editors/SectionController.cs index dc7ddb7201..bcf451a5bb 100644 --- a/src/Umbraco.Web/Editors/SectionController.cs +++ b/src/Umbraco.Web/Editors/SectionController.cs @@ -1,98 +1,94 @@ -using System.Collections.Generic; -using AutoMapper; -using Umbraco.Web.Models.ContentEditing; -using Umbraco.Web.Mvc; -using System.Linq; -using Umbraco.Core.Composing; -using Umbraco.Core.Models; -using Umbraco.Web.Trees; -using Section = Umbraco.Web.Models.ContentEditing.Section; -using Umbraco.Web.Models.Trees; - -namespace Umbraco.Web.Editors -{ - /// - /// The API controller used for using the list of sections - /// - [PluginController("UmbracoApi")] - public class SectionController : UmbracoAuthorizedJsonController - { - public IEnumerable
GetSections() - { - - var sections = Services.SectionService.GetAllowedSections(Security.GetUserId().ResultOr(0)); - - var sectionModels = sections.Select(Mapper.Map).ToArray(); - - //Check if there are empty dashboards or dashboards that will end up empty based on the current user's access - //and add the meta data about them - var dashboardHelper = new DashboardHelper(Services.SectionService); - - // this is a bit nasty since we'll be proxying via the app tree controller but we sort of have to do that - // since tree's by nature are controllers and require request contextual data - and then we have to - // remember to inject properties - nasty indeed - // fixme - this controller could/should be able to be created from the container and/or from webapi's IHttpControllerTypeResolver - var appTreeController = new ApplicationTreeController(); - Current.Container.InjectProperties(appTreeController); - appTreeController.ControllerContext = ControllerContext; - - var dashboards = dashboardHelper.GetDashboards(Security.CurrentUser); - //now we can add metadata for each section so that the UI knows if there's actually anything at all to render for - //a dashboard for a given section, then the UI can deal with it accordingly (i.e. redirect to the first tree) - foreach (var section in sectionModels) - { - var hasDashboards = false; - if (dashboards.TryGetValue(section.Alias, out var dashboardsForSection)) - { - if (dashboardsForSection.Any()) - hasDashboards = true; - } - - if (hasDashboards == false) - { - //get the first tree in the section and get it's root node route path - var sectionRoot = appTreeController.GetApplicationTrees(section.Alias, null, null).Result; - section.RoutePath = GetRoutePathForFirstTree(sectionRoot); - } - } - - return sectionModels; - } - - /// - /// Returns the first non root/group node's route path - /// - /// - /// - private string GetRoutePathForFirstTree(TreeRootNode rootNode) - { - if (!rootNode.IsContainer || !rootNode.ContainsTrees) - return rootNode.RoutePath; - - foreach(var node in rootNode.Children) - { - if (node is TreeRootNode groupRoot) - return GetRoutePathForFirstTree(groupRoot);//recurse to get the first tree in the group - else - return node.RoutePath; - } - - return string.Empty; - } - - /// - /// Returns all the sections that the user has access to - /// - /// - public IEnumerable
GetAllSections() - { - var sections = Services.SectionService.GetSections(); - var mapped = sections.Select(Mapper.Map); - if (Security.CurrentUser.IsAdmin()) - return mapped; - - return mapped.Where(x => Security.CurrentUser.AllowedSections.Contains(x.Alias)).ToArray(); - } - - } -} +using System.Collections.Generic; +using AutoMapper; +using Umbraco.Web.Models.ContentEditing; +using Umbraco.Web.Mvc; +using System.Linq; +using Umbraco.Core.Composing; +using Umbraco.Core.Models; +using Umbraco.Web.Trees; +using Section = Umbraco.Web.Models.ContentEditing.Section; +using Umbraco.Web.Models.Trees; + +namespace Umbraco.Web.Editors +{ + /// + /// The API controller used for using the list of sections + /// + [PluginController("UmbracoApi")] + public class SectionController : UmbracoAuthorizedJsonController + { + private readonly Dashboards _dashboards; + + public SectionController(Dashboards dashboards) + { + _dashboards = dashboards; + } + + public IEnumerable
GetSections() + { + var sections = Services.SectionService.GetAllowedSections(Security.GetUserId().ResultOr(0)); + + var sectionModels = sections.Select(Mapper.Map).ToArray(); + + // this is a bit nasty since we'll be proxying via the app tree controller but we sort of have to do that + // since tree's by nature are controllers and require request contextual data - and then we have to + // remember to inject properties - nasty indeed + // fixme - this controller could/should be able to be created from the container and/or from webapi's IHttpControllerTypeResolver + var appTreeController = new ApplicationTreeController(); + Current.Container.InjectProperties(appTreeController); + appTreeController.ControllerContext = ControllerContext; + + var dashboards = _dashboards.GetDashboards(Security.CurrentUser); + + //now we can add metadata for each section so that the UI knows if there's actually anything at all to render for + //a dashboard for a given section, then the UI can deal with it accordingly (i.e. redirect to the first tree) + foreach (var section in sectionModels) + { + var hasDashboards = dashboards.TryGetValue(section.Alias, out var dashboardsForSection) && dashboardsForSection.Any(); + if (hasDashboards) continue; + + // get the first tree in the section and get its root node route path + var sectionRoot = appTreeController.GetApplicationTrees(section.Alias, null, null).Result; + section.RoutePath = GetRoutePathForFirstTree(sectionRoot); + } + + return sectionModels; + } + + /// + /// Returns the first non root/group node's route path + /// + /// + /// + private string GetRoutePathForFirstTree(TreeRootNode rootNode) + { + if (!rootNode.IsContainer || !rootNode.ContainsTrees) + return rootNode.RoutePath; + + foreach(var node in rootNode.Children) + { + if (node is TreeRootNode groupRoot) + return GetRoutePathForFirstTree(groupRoot);//recurse to get the first tree in the group + else + return node.RoutePath; + } + + return string.Empty; + } + + /// + /// Returns all the sections that the user has access to + /// + /// + public IEnumerable
GetAllSections() + { + var sections = Services.SectionService.GetSections(); + var mapped = sections.Select(Mapper.Map); + if (Security.CurrentUser.IsAdmin()) + return mapped; + + return mapped.Where(x => Security.CurrentUser.AllowedSections.Contains(x.Alias)).ToArray(); + } + + } +} diff --git a/src/Umbraco.Web/LightInjectExtensions.cs b/src/Umbraco.Web/LightInjectExtensions.cs index 580dc4117d..7ea55587e0 100644 --- a/src/Umbraco.Web/LightInjectExtensions.cs +++ b/src/Umbraco.Web/LightInjectExtensions.cs @@ -1,49 +1,82 @@ -using System.Reflection; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; using System.Web.Http.Controllers; using System.Web.Mvc; using LightInject; -using Umbraco.Core; using Umbraco.Core.Composing; +using Umbraco.Web.Mvc; +using Umbraco.Web.WebApi; namespace Umbraco.Web { internal static class LightInjectExtensions { /// - /// Registers all IControllers using the TypeLoader for scanning and caching found instances for the calling assembly + /// Registers Umbraco controllers. /// - /// - /// - /// - public static void RegisterMvcControllers(this IServiceRegistry container, TypeLoader typeLoader, Assembly assembly) + public static void RegisterUmbracoControllers(this IServiceRegistry container, TypeLoader typeLoader, Assembly umbracoWebAssembly) { - //TODO: We've already scanned for UmbracoApiControllers and SurfaceControllers - should we scan again - // for all controllers? Seems like we should just do this once and then filter. That said here we are - // only scanning our own single assembly. Hrm. + // notes + // + // We scan and auto-registers: + // - every IController and IHttpController that *we* have in Umbraco.Web + // - PluginController and UmbracoApiController in every assembly + // + // We do NOT scan: + // - any IController or IHttpController (anything not PluginController nor UmbracoApiController), outside of Umbraco.Web + // which means that users HAVE to explicitly register their own non-Umbraco controllers + // + // This is because we try to achieve a balance between "simple" and "fast. Scanning for PluginController or + // UmbracoApiController is fast-ish because they both are IDiscoverable. Scanning for IController or IHttpController + // is a full, non-cached scan = expensive, we do it only for 1 assembly. + // + // TODO + // find a way to scan for IController *and* IHttpController in one single pass + // or, actually register them manually so don't require a full scan for these + // 5 are IController but not PluginController + // Umbraco.Web.Mvc.RenderMvcController + // Umbraco.Web.Install.Controllers.InstallController + // Umbraco.Web.Macros.PartialViewMacroController + // Umbraco.Web.Editors.PreviewController + // Umbraco.Web.Editors.BackOfficeController + // 9 are IHttpController but not UmbracoApiController + // Umbraco.Web.Controllers.UmbProfileController + // Umbraco.Web.Controllers.UmbLoginStatusController + // Umbraco.Web.Controllers.UmbRegisterController + // Umbraco.Web.Controllers.UmbLoginController + // Umbraco.Web.Mvc.RenderMvcController + // Umbraco.Web.Install.Controllers.InstallController + // Umbraco.Web.Macros.PartialViewMacroController + // Umbraco.Web.Editors.PreviewController + // Umbraco.Web.Editors.BackOfficeController - container.RegisterControllers(typeLoader, assembly); + // scan and register every IController in Umbraco.Web + var umbracoWebControllers = typeLoader.GetTypes(specificAssemblies: new[] { umbracoWebAssembly }); + //foreach (var controller in umbracoWebControllers.Where(x => !typeof(PluginController).IsAssignableFrom(x))) + // Current.Logger.Debug(typeof(LightInjectExtensions), "IController NOT PluginController: " + controller.FullName); + container.RegisterControllers(umbracoWebControllers); + + // scan and register every PluginController in everything (PluginController is IDiscoverable and IController) + var nonUmbracoWebPluginController = typeLoader.GetTypes().Where(x => x.Assembly != umbracoWebAssembly); + container.RegisterControllers(nonUmbracoWebPluginController); + + // scan and register every IHttpController in Umbraco.Web + var umbracoWebHttpControllers = typeLoader.GetTypes(specificAssemblies: new[] { umbracoWebAssembly }); + //foreach (var controller in umbracoWebControllers.Where(x => !typeof(UmbracoApiController).IsAssignableFrom(x))) + // Current.Logger.Debug(typeof(LightInjectExtensions), "IHttpController NOT UmbracoApiController: " + controller.FullName); + container.RegisterControllers(umbracoWebHttpControllers); + + // scan and register every UmbracoApiController in everything (UmbracoApiController is IDiscoverable and IHttpController) + var nonUmbracoWebApiControllers = typeLoader.GetTypes().Where(x => x.Assembly != umbracoWebAssembly); + container.RegisterControllers(nonUmbracoWebApiControllers); } - /// - /// Registers all IHttpController using the TypeLoader for scanning and caching found instances for the calling assembly - /// - /// - /// - /// - public static void RegisterApiControllers(this IServiceRegistry container, TypeLoader typeLoader, Assembly assembly) + private static void RegisterControllers(this IServiceRegistry container, IEnumerable controllerTypes) { - //TODO: We've already scanned for UmbracoApiControllers and SurfaceControllers - should we scan again - // for all controllers? Seems like we should just do this once and then filter. That said here we are - // only scanning our own single assembly. Hrm. - - container.RegisterControllers(typeLoader, assembly); - } - - private static void RegisterControllers(this IServiceRegistry container, TypeLoader typeLoader, Assembly assembly) - { - var types = typeLoader.GetTypes(specificAssemblies: new[] { assembly }); - foreach (var type in types) - container.Register(type, new PerRequestLifeTime()); + foreach (var controllerType in controllerTypes) + container.Register(controllerType, new PerRequestLifeTime()); } } } diff --git a/src/Umbraco.Web/Models/ContentEditing/DashboardControl.cs b/src/Umbraco.Web/Models/ContentEditing/DashboardControl.cs index d51084fb16..aad6bf2d64 100644 --- a/src/Umbraco.Web/Models/ContentEditing/DashboardControl.cs +++ b/src/Umbraco.Web/Models/ContentEditing/DashboardControl.cs @@ -10,15 +10,6 @@ namespace Umbraco.Web.Models.ContentEditing [DataContract(Name = "control", Namespace = "")] public class DashboardControl { - [DataMember(Name = "showOnce")] - public bool ShowOnce { get; set; } - - [DataMember(Name = "addPanel")] - public bool AddPanel { get; set; } - - [DataMember(Name = "serverSide")] - public bool ServerSide { get; set; } - [DataMember(Name = "path")] public string Path { get; set; } diff --git a/src/Umbraco.Web/Runtime/WebRuntimeComponent.cs b/src/Umbraco.Web/Runtime/WebRuntimeComponent.cs index 1f6210b6a1..63849034a4 100644 --- a/src/Umbraco.Web/Runtime/WebRuntimeComponent.cs +++ b/src/Umbraco.Web/Runtime/WebRuntimeComponent.cs @@ -122,9 +122,10 @@ namespace Umbraco.Web.Runtime composition.Container.EnableMvc(); // does container.EnablePerWebRequestScope() composition.Container.ScopeManagerProvider = smp; // reverts - we will do it last (in WebRuntime) - composition.Container.RegisterMvcControllers(typeLoader, GetType().Assembly); + composition.Container.RegisterSingleton(); + + composition.Container.RegisterUmbracoControllers(typeLoader, GetType().Assembly); composition.Container.EnableWebApi(GlobalConfiguration.Configuration); - composition.Container.RegisterApiControllers(typeLoader, GetType().Assembly); composition.Container.RegisterCollectionBuilder() .Add(() => typeLoader.GetTypes()); // fixme which searchable trees?! @@ -183,7 +184,7 @@ namespace Umbraco.Web.Runtime .Append(); composition.Container.RegisterSingleton(); - + composition.Container.RegisterSingleton(); // register *all* checks, except those marked [HideFromTypeFinder] of course diff --git a/src/Umbraco.Web/Trees/DictionaryTreeController.cs b/src/Umbraco.Web/Trees/DictionaryTreeController.cs index d0a7fce3ad..cac2e7f435 100644 --- a/src/Umbraco.Web/Trees/DictionaryTreeController.cs +++ b/src/Umbraco.Web/Trees/DictionaryTreeController.cs @@ -2,6 +2,7 @@ using System.Linq; using System.Net.Http.Formatting; using Umbraco.Core; +using Umbraco.Core.Models; using Umbraco.Core.Services; using Umbraco.Web.Actions; @@ -52,10 +53,12 @@ namespace Umbraco.Web.Trees var nodes = new TreeNodeCollection(); + Func ItemSort() => item => item.ItemKey; + if (id == Constants.System.Root.ToInvariantString()) { nodes.AddRange( - Services.LocalizationService.GetRootDictionaryItems().Select( + Services.LocalizationService.GetRootDictionaryItems().OrderBy(ItemSort()).Select( x => CreateTreeNode( x.Id.ToInvariantString(), id, @@ -71,7 +74,7 @@ namespace Umbraco.Web.Trees if (parentDictionary == null) return nodes; - nodes.AddRange(Services.LocalizationService.GetDictionaryItemChildren(parentDictionary.Key).ToList().OrderByDescending(item => item.Key).Select( + nodes.AddRange(Services.LocalizationService.GetDictionaryItemChildren(parentDictionary.Key).ToList().OrderBy(ItemSort()).Select( x => CreateTreeNode( x.Id.ToInvariantString(), id, diff --git a/src/Umbraco.Web/UI/JavaScript/JsInitialize.js b/src/Umbraco.Web/UI/JavaScript/JsInitialize.js index eef1d25909..75ca46437a 100644 --- a/src/Umbraco.Web/UI/JavaScript/JsInitialize.js +++ b/src/Umbraco.Web/UI/JavaScript/JsInitialize.js @@ -1,7 +1,7 @@ [ 'lib/jquery/jquery.min.js', 'lib/jquery-ui/jquery-ui.min.js', - 'lib/jquery-ui-touch-punch/jquery.ui.touch-punch.js', + 'lib/jquery-ui-touch-punch/jquery.ui.touch-punch.min.js', 'lib/angular/angular.js', 'lib/underscore/underscore-min.js', @@ -23,7 +23,6 @@ 'lib/ng-file-upload/ng-file-upload.min.js', 'lib/angular-local-storage/angular-local-storage.min.js', - 'lib/bootstrap/js/bootstrap.2.3.2.min.js', 'lib/umbraco/Extensions.js', 'lib/umbraco/NamespaceManager.js', diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 63908a23f9..e8c76a1f45 100755 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -196,7 +196,7 @@ - +