Merge branch 'temp8-u4-11427-remove-propertyinjection' of https://github.com/lars-erik/Umbraco-CMS into temp8-u4-11427-remove-propertyinjection
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
<section alias="StartupSettingsDashboardSection" xdt:Locator="Match(alias)" xdt:Transform="InsertIfMissing">
|
||||
<tab caption="Get Started" xdt:Locator="Match(caption)" xdt:Transform="Remove" />
|
||||
<tab caption="Welcome" xdt:Locator="Match(caption)" xdt:Transform="InsertIfMissing">
|
||||
<control showOnce="true" addPanel="true" panelCaption="" xdt:Transform="InsertIfMissing">
|
||||
<control panelCaption="" xdt:Transform="InsertIfMissing">
|
||||
views/dashboard/settings/settingsdashboardintro.html
|
||||
</control>
|
||||
</tab>
|
||||
@@ -14,7 +14,7 @@
|
||||
<area>forms</area>
|
||||
</areas>
|
||||
<tab caption="Install Umbraco Forms">
|
||||
<control showOnce="true" addPanel="true" panelCaption="">
|
||||
<control panelCaption="">
|
||||
views/dashboard/forms/formsdashboardintro.html
|
||||
</control>
|
||||
</tab>
|
||||
@@ -28,7 +28,7 @@
|
||||
|
||||
<section alias="StartupDeveloperDashboardSection">
|
||||
<tab caption="Get Started" xdt:Locator="Match(caption)" xdt:Transform="Replace">
|
||||
<control showOnce="true" addPanel="true" panelCaption="">
|
||||
<control panelCaption="">
|
||||
views/dashboard/developer/developerdashboardvideos.html
|
||||
</control>
|
||||
</tab>
|
||||
@@ -47,7 +47,7 @@
|
||||
<tab caption="Get Started" xdt:Locator="Match(caption)" xdt:Transform="Remove" />
|
||||
<tab caption="Content" xdt:Locator="Match(caption)" xdt:Transform="InsertIfMissing" />
|
||||
<tab caption="Content" xdt:Locator="Match(caption)" xdt:Transform="Replace">
|
||||
<control showOnce="false" addPanel="false" panelCaption="">
|
||||
<control panelCaption="">
|
||||
views/dashboard/media/mediafolderbrowser.html
|
||||
</control>
|
||||
</tab>
|
||||
@@ -56,7 +56,7 @@
|
||||
<section alias="StartupMemberDashboardSection" xdt:Locator="Match(alias)" xdt:Transform="InsertIfMissing">
|
||||
<tab caption="Get Started" xdt:Locator="Match(caption)" xdt:Transform="Remove" />
|
||||
<tab caption="Get Started" xdt:Transform="Insert">
|
||||
<control showOnce="true" addPanel="true" panelCaption="">
|
||||
<control panelCaption="">
|
||||
views/dashboard/members/membersdashboardvideos.html
|
||||
</control>
|
||||
</tab>
|
||||
@@ -92,4 +92,4 @@
|
||||
</control>
|
||||
</tab>
|
||||
</section>
|
||||
</dashBoard>
|
||||
</dashBoard>
|
||||
|
||||
@@ -85,12 +85,20 @@ namespace Umbraco.Core.Components
|
||||
/// <inheritdoc />
|
||||
public IFactory CreateFactory()
|
||||
{
|
||||
foreach (var onCreating in OnCreatingFactory.Values)
|
||||
onCreating();
|
||||
|
||||
foreach (var unique in _uniques.Values)
|
||||
unique.RegisterTo(_register);
|
||||
|
||||
return _register.CreateFactory();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a dictionary of action to execute when creating the factory.
|
||||
/// </summary>
|
||||
public Dictionary<string, Action> OnCreatingFactory { get; } = new Dictionary<string, Action>();
|
||||
|
||||
#endregion
|
||||
|
||||
#region Unique
|
||||
|
||||
@@ -16,6 +16,7 @@ namespace Umbraco.Core.Composing.Composers
|
||||
composition.Register(factory => factory.GetInstance<IUmbracoSettingsSection>().Templates);
|
||||
composition.Register(factory => factory.GetInstance<IUmbracoSettingsSection>().RequestHandler);
|
||||
composition.Register(factory => UmbracoConfig.For.GlobalSettings());
|
||||
composition.Register(factory => UmbracoConfig.For.DashboardSettings());
|
||||
|
||||
// fixme - other sections we need to add?
|
||||
|
||||
|
||||
@@ -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<IAccessItem> Rules
|
||||
public IEnumerable<IAccessRule> Rules
|
||||
{
|
||||
get
|
||||
{
|
||||
var result = new List<AccessItem>();
|
||||
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<AccessRule>();
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
namespace Umbraco.Core.Configuration.Dashboard
|
||||
{
|
||||
internal class AccessItem : IAccessItem
|
||||
{
|
||||
/// <summary>
|
||||
/// This can be grant, deny or grantBySection
|
||||
/// </summary>
|
||||
public AccessType Action { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The value of the action
|
||||
/// </summary>
|
||||
public string Value { get; set; }
|
||||
}
|
||||
}
|
||||
14
src/Umbraco.Core/Configuration/Dashboard/AccessRule.cs
Normal file
14
src/Umbraco.Core/Configuration/Dashboard/AccessRule.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
namespace Umbraco.Core.Configuration.Dashboard
|
||||
{
|
||||
/// <summary>
|
||||
/// Implements <see cref="IAccessRule"/>.
|
||||
/// </summary>
|
||||
internal class AccessRule : IAccessRule
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public AccessRuleType Type { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Value { get; set; }
|
||||
}
|
||||
}
|
||||
28
src/Umbraco.Core/Configuration/Dashboard/AccessRuleType.cs
Normal file
28
src/Umbraco.Core/Configuration/Dashboard/AccessRuleType.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
namespace Umbraco.Core.Configuration.Dashboard
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines dashboard access rules type.
|
||||
/// </summary>
|
||||
public enum AccessRuleType
|
||||
{
|
||||
/// <summary>
|
||||
/// Unknown (default value).
|
||||
/// </summary>
|
||||
Unknown = 0,
|
||||
|
||||
/// <summary>
|
||||
/// Grant access to the dashboard if user belongs to the specified user group.
|
||||
/// </summary>
|
||||
Grant,
|
||||
|
||||
/// <summary>
|
||||
/// Deny access to the dashboard if user belongs to the specified user group.
|
||||
/// </summary>
|
||||
Deny,
|
||||
|
||||
/// <summary>
|
||||
/// Grant access to the dashboard if user has access to the specified section.
|
||||
/// </summary>
|
||||
GrantBySection
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
namespace Umbraco.Core.Configuration.Dashboard
|
||||
{
|
||||
public enum AccessType
|
||||
{
|
||||
Grant,
|
||||
Deny,
|
||||
GrantBySection
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,6 @@ namespace Umbraco.Core.Configuration.Dashboard
|
||||
{
|
||||
public interface IAccess
|
||||
{
|
||||
IEnumerable<IAccessItem> Rules { get; }
|
||||
IEnumerable<IAccessRule> Rules { get; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
namespace Umbraco.Core.Configuration.Dashboard
|
||||
{
|
||||
public interface IAccessItem
|
||||
{
|
||||
/// <summary>
|
||||
/// This can be grant, deny or grantBySection
|
||||
/// </summary>
|
||||
AccessType Action { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The value of the action
|
||||
/// </summary>
|
||||
string Value { get; set; }
|
||||
}
|
||||
}
|
||||
18
src/Umbraco.Core/Configuration/Dashboard/IAccessRule.cs
Normal file
18
src/Umbraco.Core/Configuration/Dashboard/IAccessRule.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
namespace Umbraco.Core.Configuration.Dashboard
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents an access rule.
|
||||
/// </summary>
|
||||
public interface IAccessRule
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the rule type.
|
||||
/// </summary>
|
||||
AccessRuleType Type { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the value for the rule.
|
||||
/// </summary>
|
||||
string Value { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -2,10 +2,6 @@
|
||||
{
|
||||
public interface IDashboardControl
|
||||
{
|
||||
bool ShowOnce { get; }
|
||||
|
||||
bool AddPanel { get; }
|
||||
|
||||
string PanelCaption { get; }
|
||||
|
||||
string ControlPath { get; }
|
||||
|
||||
45
src/Umbraco.Core/Manifest/DashboardAccessRuleConverter.cs
Normal file
45
src/Umbraco.Core/Manifest/DashboardAccessRuleConverter.cs
Normal file
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Implements a json read converter for <see cref="IAccessRule"/>.
|
||||
/// </summary>
|
||||
internal class DashboardAccessRuleConverter : JsonReadConverter<IAccessRule>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override IAccessRule Create(Type objectType, string path, JObject jObject)
|
||||
{
|
||||
return new AccessRule();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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<string>();
|
||||
}
|
||||
}
|
||||
}
|
||||
36
src/Umbraco.Core/Manifest/ManifestDashboardDefinition.cs
Normal file
36
src/Umbraco.Core/Manifest/ManifestDashboardDefinition.cs
Normal file
@@ -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<string>();
|
||||
|
||||
[JsonProperty("access")]
|
||||
public IAccessRule[] AccessRules { get; set; } = Array.Empty<IAccessRule>();
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Parses the Main.js file and replaces all tokens accordingly.
|
||||
/// </summary>
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ManifestParser"/> class.
|
||||
/// </summary>
|
||||
public ManifestParser(IRuntimeCacheProvider cache, ManifestValueValidatorCollection validators, ILogger logger)
|
||||
: this(cache, validators, "~/App_Plugins", logger)
|
||||
{ }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ManifestParser"/> class.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all manifests, merged into a single manifest object.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public PackageManifest Manifest
|
||||
=> _cache.GetCacheItem<PackageManifest>("Umbraco.Core.Manifest.ManifestParser::Manifests", () =>
|
||||
{
|
||||
var manifests = GetManifests();
|
||||
return MergeManifests(manifests);
|
||||
}, new TimeSpan(0, 4, 0));
|
||||
|
||||
/// <summary>
|
||||
/// Gets all manifests.
|
||||
/// </summary>
|
||||
private IEnumerable<PackageManifest> GetManifests()
|
||||
{
|
||||
var manifests = new List<PackageManifest>();
|
||||
|
||||
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<ManifestParser>(e, "Failed to parse manifest at '{Path}', ignoring.", path);
|
||||
}
|
||||
}
|
||||
|
||||
return manifests;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Merges all manifests into one.
|
||||
/// </summary>
|
||||
private static PackageManifest MergeManifests(IEnumerable<PackageManifest> manifests)
|
||||
{
|
||||
var scripts = new HashSet<string>();
|
||||
var stylesheets = new HashSet<string>();
|
||||
var propertyEditors = new List<IDataEditor>();
|
||||
var parameterEditors = new List<IDataEditor>();
|
||||
var gridEditors = new List<GridEditor>();
|
||||
var contentApps = new List<IContentAppDefinition>();
|
||||
|
||||
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<string> 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses a manifest.
|
||||
/// </summary>
|
||||
internal PackageManifest ParseManifest(string text)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
throw new ArgumentNullOrEmptyException(nameof(text));
|
||||
|
||||
var manifest = JsonConvert.DeserializeObject<PackageManifest>(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<GridEditor> ParseGridEditors(string text)
|
||||
{
|
||||
return JsonConvert.DeserializeObject<IEnumerable<GridEditor>>(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
|
||||
{
|
||||
/// <summary>
|
||||
/// Parses the Main.js file and replaces all tokens accordingly.
|
||||
/// </summary>
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ManifestParser"/> class.
|
||||
/// </summary>
|
||||
public ManifestParser(IRuntimeCacheProvider cache, ManifestValueValidatorCollection validators, ILogger logger)
|
||||
: this(cache, validators, "~/App_Plugins", logger)
|
||||
{ }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ManifestParser"/> class.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all manifests, merged into a single manifest object.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public PackageManifest Manifest
|
||||
=> _cache.GetCacheItem<PackageManifest>("Umbraco.Core.Manifest.ManifestParser::Manifests", () =>
|
||||
{
|
||||
var manifests = GetManifests();
|
||||
return MergeManifests(manifests);
|
||||
}, new TimeSpan(0, 4, 0));
|
||||
|
||||
/// <summary>
|
||||
/// Gets all manifests.
|
||||
/// </summary>
|
||||
private IEnumerable<PackageManifest> GetManifests()
|
||||
{
|
||||
var manifests = new List<PackageManifest>();
|
||||
|
||||
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<ManifestParser>(e, "Failed to parse manifest at '{Path}', ignoring.", path);
|
||||
}
|
||||
}
|
||||
|
||||
return manifests;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Merges all manifests into one.
|
||||
/// </summary>
|
||||
private static PackageManifest MergeManifests(IEnumerable<PackageManifest> manifests)
|
||||
{
|
||||
var scripts = new HashSet<string>();
|
||||
var stylesheets = new HashSet<string>();
|
||||
var propertyEditors = new List<IDataEditor>();
|
||||
var parameterEditors = new List<IDataEditor>();
|
||||
var gridEditors = new List<GridEditor>();
|
||||
var contentApps = new List<IContentAppDefinition>();
|
||||
var dashboards = new List<ManifestDashboardDefinition>();
|
||||
|
||||
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<string> 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses a manifest.
|
||||
/// </summary>
|
||||
internal PackageManifest ParseManifest(string text)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
throw new ArgumentNullOrEmptyException(nameof(text));
|
||||
|
||||
var manifest = JsonConvert.DeserializeObject<PackageManifest>(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<GridEditor> ParseGridEditors(string text)
|
||||
{
|
||||
return JsonConvert.DeserializeObject<IEnumerable<GridEditor>>(text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,31 +1,34 @@
|
||||
using System;
|
||||
using Newtonsoft.Json;
|
||||
using Umbraco.Core.Models.ContentEditing;
|
||||
using Umbraco.Core.PropertyEditors;
|
||||
|
||||
namespace Umbraco.Core.Manifest
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents the content of a package manifest.
|
||||
/// </summary>
|
||||
public class PackageManifest
|
||||
{
|
||||
[JsonProperty("javascript")]
|
||||
public string[] Scripts { get; set; } = Array.Empty<string>();
|
||||
|
||||
[JsonProperty("css")]
|
||||
public string[] Stylesheets { get; set; }= Array.Empty<string>();
|
||||
|
||||
[JsonProperty("propertyEditors")]
|
||||
public IDataEditor[] PropertyEditors { get; set; } = Array.Empty<IDataEditor>();
|
||||
|
||||
[JsonProperty("parameterEditors")]
|
||||
public IDataEditor[] ParameterEditors { get; set; } = Array.Empty<IDataEditor>();
|
||||
|
||||
[JsonProperty("gridEditors")]
|
||||
public GridEditor[] GridEditors { get; set; } = Array.Empty<GridEditor>();
|
||||
|
||||
[JsonProperty("contentApps")]
|
||||
public IContentAppDefinition[] ContentApps { get; set; } = Array.Empty<IContentAppDefinition>();
|
||||
}
|
||||
}
|
||||
using System;
|
||||
using Newtonsoft.Json;
|
||||
using Umbraco.Core.Models.ContentEditing;
|
||||
using Umbraco.Core.PropertyEditors;
|
||||
|
||||
namespace Umbraco.Core.Manifest
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents the content of a package manifest.
|
||||
/// </summary>
|
||||
public class PackageManifest
|
||||
{
|
||||
[JsonProperty("javascript")]
|
||||
public string[] Scripts { get; set; } = Array.Empty<string>();
|
||||
|
||||
[JsonProperty("css")]
|
||||
public string[] Stylesheets { get; set; }= Array.Empty<string>();
|
||||
|
||||
[JsonProperty("propertyEditors")]
|
||||
public IDataEditor[] PropertyEditors { get; set; } = Array.Empty<IDataEditor>();
|
||||
|
||||
[JsonProperty("parameterEditors")]
|
||||
public IDataEditor[] ParameterEditors { get; set; } = Array.Empty<IDataEditor>();
|
||||
|
||||
[JsonProperty("gridEditors")]
|
||||
public GridEditor[] GridEditors { get; set; } = Array.Empty<GridEditor>();
|
||||
|
||||
[JsonProperty("contentApps")]
|
||||
public IContentAppDefinition[] ContentApps { get; set; } = Array.Empty<IContentAppDefinition>();
|
||||
|
||||
[JsonProperty("dashboards")]
|
||||
public ManifestDashboardDefinition[] Dashboards { get; set; } = Array.Empty<ManifestDashboardDefinition>();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ using Umbraco.Core.Models.Membership;
|
||||
|
||||
namespace Umbraco.Core.Models.ContentEditing
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Represents a content app definition.
|
||||
/// </summary>
|
||||
|
||||
@@ -202,8 +202,8 @@
|
||||
<Compile Include="Configuration\CommaDelimitedConfigurationElement.cs" />
|
||||
<Compile Include="Configuration\CoreDebug.cs" />
|
||||
<Compile Include="Configuration\Dashboard\AccessElement.cs" />
|
||||
<Compile Include="Configuration\Dashboard\AccessItem.cs" />
|
||||
<Compile Include="Configuration\Dashboard\AccessType.cs" />
|
||||
<Compile Include="Configuration\Dashboard\AccessRule.cs" />
|
||||
<Compile Include="Configuration\Dashboard\AccessRuleType.cs" />
|
||||
<Compile Include="Configuration\Dashboard\AreaCollection.cs" />
|
||||
<Compile Include="Configuration\Dashboard\AreaElement.cs" />
|
||||
<Compile Include="Configuration\Dashboard\AreasElement.cs" />
|
||||
@@ -211,7 +211,7 @@
|
||||
<Compile Include="Configuration\Dashboard\ControlElement.cs" />
|
||||
<Compile Include="Configuration\Dashboard\DashboardSection.cs" />
|
||||
<Compile Include="Configuration\Dashboard\IAccess.cs" />
|
||||
<Compile Include="Configuration\Dashboard\IAccessItem.cs" />
|
||||
<Compile Include="Configuration\Dashboard\IAccessRule.cs" />
|
||||
<Compile Include="Configuration\Dashboard\IArea.cs" />
|
||||
<Compile Include="Configuration\Dashboard\IDashboardControl.cs" />
|
||||
<Compile Include="Configuration\Dashboard\IDashboardSection.cs" />
|
||||
@@ -346,7 +346,9 @@
|
||||
<Compile Include="Logging\Serilog\LoggerConfigExtensions.cs" />
|
||||
<Compile Include="Logging\Serilog\Enrichers\Log4NetLevelMapperEnricher.cs" />
|
||||
<Compile Include="Manifest\ContentAppDefinitionConverter.cs" />
|
||||
<Compile Include="Manifest\DashboardAccessRuleConverter.cs" />
|
||||
<Compile Include="Manifest\ManifestContentAppDefinition.cs" />
|
||||
<Compile Include="Manifest\ManifestDashboardDefinition.cs" />
|
||||
<Compile Include="Migrations\IncompleteMigrationExpressionException.cs" />
|
||||
<Compile Include="Migrations\MigrationBase_Extra.cs" />
|
||||
<Compile Include="Migrations\Upgrade\V_7_10_0\RenamePreviewFolder.cs" />
|
||||
|
||||
@@ -6,10 +6,10 @@
|
||||
<area>settings</area>
|
||||
</areas>
|
||||
<tab caption="Get Started">
|
||||
<control showOnce="true" addPanel="true" panelCaption="hello">
|
||||
<control panelCaption="hello">
|
||||
views/dashboard/settings/settingsdashboardintro.html
|
||||
</control>
|
||||
<control showOnce="false" addPanel="false" panelCaption="">
|
||||
<control panelCaption="">
|
||||
views/dashboard/settings/settingsdashboardvideos.html
|
||||
</control>
|
||||
</tab>
|
||||
@@ -23,10 +23,10 @@
|
||||
<area>developer</area>
|
||||
</areas>
|
||||
<tab caption="Get Started">
|
||||
<control showOnce="true" addPanel="true" panelCaption="">
|
||||
<control panelCaption="">
|
||||
views/dashboard/developer/developerdashboardintro.html
|
||||
</control>
|
||||
<control showOnce="true" addPanel="true" panelCaption="">
|
||||
<control panelCaption="">
|
||||
views/dashboard/developer/developerdashboardvideos.html
|
||||
</control>
|
||||
</tab>
|
||||
@@ -37,7 +37,7 @@
|
||||
<area>media</area>
|
||||
</areas>
|
||||
<tab caption="Content">
|
||||
<control showOnce="false" addPanel="false" panelCaption="">
|
||||
<control panelCaption="">
|
||||
views/dashboard/media/mediafolderbrowser.html
|
||||
</control>
|
||||
</tab>
|
||||
@@ -45,13 +45,13 @@
|
||||
<access>
|
||||
<grant>admin</grant>
|
||||
</access>
|
||||
<control showOnce="true" addPanel="true" panelCaption="">
|
||||
<control panelCaption="">
|
||||
views/dashboard/media/mediadashboardintro.html
|
||||
</control>
|
||||
<control showOnce="true" addPanel="true" panelCaption="">
|
||||
<control panelCaption="">
|
||||
views/dashboard/media/desktopmediauploader.html
|
||||
</control>
|
||||
<control showOnce="true" addPanel="true" panelCaption="">
|
||||
<control panelCaption="">
|
||||
views/dashboard/media/mediadashboardvideos.html
|
||||
</control>
|
||||
</tab>
|
||||
@@ -70,25 +70,25 @@
|
||||
<access>
|
||||
<grant>admin</grant>
|
||||
</access>
|
||||
<control showOnce="true" addPanel="true" panelCaption="">
|
||||
<control panelCaption="">
|
||||
views/dashboard/default/startupdashboardintro.html
|
||||
</control>
|
||||
<control showOnce="true" addPanel="true" panelCaption="">
|
||||
<control panelCaption="">
|
||||
views/dashboard/default/startupdashboardkits.html
|
||||
<access>
|
||||
<deny>editor</deny>
|
||||
<deny>writer</deny>
|
||||
</access>
|
||||
</control>
|
||||
<control showOnce="true" addPanel="true" panelCaption="">
|
||||
<control panelCaption="">
|
||||
views/dashboard/default/startupdashboardvideos.html
|
||||
</control>
|
||||
</tab>
|
||||
<tab caption="Last Edits">
|
||||
<control addPanel="true" MaxRecords="30">dashboard/latestEdits.ascx</control>
|
||||
<control MaxRecords="30">dashboard/latestEdits.ascx</control>
|
||||
</tab>
|
||||
<tab caption="Change Password">
|
||||
<control addPanel="true">
|
||||
<control >
|
||||
views/dashboard/changepassword.html
|
||||
</control>
|
||||
</tab>
|
||||
@@ -100,13 +100,13 @@
|
||||
<area>member</area>
|
||||
</areas>
|
||||
<tab caption="Get Started">
|
||||
<control showOnce="true" addPanel="true" panelCaption="">
|
||||
<control panelCaption="">
|
||||
views/dashboard/members/membersdashboardintro.html
|
||||
</control>
|
||||
<control showOnce="true" addPanel="true" panelCaption="">
|
||||
<control panelCaption="">
|
||||
members/membersearch.ascx
|
||||
</control>
|
||||
<control showOnce="true" addPanel="true" panelCaption="">
|
||||
<control panelCaption="">
|
||||
views/dashboard/members/membersdashboardvideos.html
|
||||
</control>
|
||||
</tab>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,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;
|
||||
@@ -380,5 +381,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<ManifestDashboardDefinition>(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<ManifestDashboardDefinition>(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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
{
|
||||
@@ -344,6 +325,14 @@ gulp.task('dependencies', function () {
|
||||
"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": [
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@@ -1,180 +0,0 @@
|
||||
/*!
|
||||
* jQuery UI Touch Punch 0.2.3
|
||||
*
|
||||
* Copyright 2011–2014, Dave Furfero
|
||||
* Dual licensed under the MIT or GPL Version 2 licenses.
|
||||
*
|
||||
* Depends:
|
||||
* jquery.ui.widget.js
|
||||
* jquery.ui.mouse.js
|
||||
*/
|
||||
(function ($) {
|
||||
|
||||
// Detect touch support
|
||||
$.support.touch = 'ontouchend' in document;
|
||||
|
||||
// Ignore browsers without touch support
|
||||
if (!$.support.touch) {
|
||||
return;
|
||||
}
|
||||
|
||||
var mouseProto = $.ui.mouse.prototype,
|
||||
_mouseInit = mouseProto._mouseInit,
|
||||
_mouseDestroy = mouseProto._mouseDestroy,
|
||||
touchHandled;
|
||||
|
||||
/**
|
||||
* Simulate a mouse event based on a corresponding touch event
|
||||
* @param {Object} event A touch event
|
||||
* @param {String} simulatedType The corresponding mouse event
|
||||
*/
|
||||
function simulateMouseEvent (event, simulatedType) {
|
||||
|
||||
// Ignore multi-touch events
|
||||
if (event.originalEvent.touches.length > 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
var touch = event.originalEvent.changedTouches[0],
|
||||
simulatedEvent = document.createEvent('MouseEvents');
|
||||
|
||||
// Initialize the simulated mouse event using the touch event's coordinates
|
||||
simulatedEvent.initMouseEvent(
|
||||
simulatedType, // type
|
||||
true, // bubbles
|
||||
true, // cancelable
|
||||
window, // view
|
||||
1, // detail
|
||||
touch.screenX, // screenX
|
||||
touch.screenY, // screenY
|
||||
touch.clientX, // clientX
|
||||
touch.clientY, // clientY
|
||||
false, // ctrlKey
|
||||
false, // altKey
|
||||
false, // shiftKey
|
||||
false, // metaKey
|
||||
0, // button
|
||||
null // relatedTarget
|
||||
);
|
||||
|
||||
// Dispatch the simulated event to the target element
|
||||
event.target.dispatchEvent(simulatedEvent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the jQuery UI widget's touchstart events
|
||||
* @param {Object} event The widget element's touchstart event
|
||||
*/
|
||||
mouseProto._touchStart = function (event) {
|
||||
|
||||
var self = this;
|
||||
|
||||
// Ignore the event if another widget is already being handled
|
||||
if (touchHandled || !self._mouseCapture(event.originalEvent.changedTouches[0])) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Set the flag to prevent other widgets from inheriting the touch event
|
||||
touchHandled = true;
|
||||
|
||||
// Track movement to determine if interaction was a click
|
||||
self._touchMoved = false;
|
||||
|
||||
// Simulate the mouseover event
|
||||
simulateMouseEvent(event, 'mouseover');
|
||||
|
||||
// Simulate the mousemove event
|
||||
simulateMouseEvent(event, 'mousemove');
|
||||
|
||||
// Simulate the mousedown event
|
||||
simulateMouseEvent(event, 'mousedown');
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle the jQuery UI widget's touchmove events
|
||||
* @param {Object} event The document's touchmove event
|
||||
*/
|
||||
mouseProto._touchMove = function (event) {
|
||||
|
||||
// Ignore event if not handled
|
||||
if (!touchHandled) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Interaction was not a click
|
||||
this._touchMoved = true;
|
||||
|
||||
// Simulate the mousemove event
|
||||
simulateMouseEvent(event, 'mousemove');
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle the jQuery UI widget's touchend events
|
||||
* @param {Object} event The document's touchend event
|
||||
*/
|
||||
mouseProto._touchEnd = function (event) {
|
||||
|
||||
// Ignore event if not handled
|
||||
if (!touchHandled) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Simulate the mouseup event
|
||||
simulateMouseEvent(event, 'mouseup');
|
||||
|
||||
// Simulate the mouseout event
|
||||
simulateMouseEvent(event, 'mouseout');
|
||||
|
||||
// If the touch interaction did not move, it should trigger a click
|
||||
if (!this._touchMoved) {
|
||||
|
||||
// Simulate the click event
|
||||
simulateMouseEvent(event, 'click');
|
||||
}
|
||||
|
||||
// Unset the flag to allow other widgets to inherit the touch event
|
||||
touchHandled = false;
|
||||
};
|
||||
|
||||
/**
|
||||
* A duck punch of the $.ui.mouse _mouseInit method to support touch events.
|
||||
* This method extends the widget with bound touch event handlers that
|
||||
* translate touch events to mouse events and pass them to the widget's
|
||||
* original mouse event handling methods.
|
||||
*/
|
||||
mouseProto._mouseInit = function () {
|
||||
|
||||
var self = this;
|
||||
|
||||
// Delegate the touch handlers to the widget's element
|
||||
self.element.bind({
|
||||
touchstart: $.proxy(self, '_touchStart'),
|
||||
touchmove: $.proxy(self, '_touchMove'),
|
||||
touchend: $.proxy(self, '_touchEnd')
|
||||
});
|
||||
|
||||
// Call the original $.ui.mouse init method
|
||||
_mouseInit.call(self);
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove the touch event handlers
|
||||
*/
|
||||
mouseProto._mouseDestroy = function () {
|
||||
|
||||
var self = this;
|
||||
|
||||
// Delegate the touch handlers to the widget's element
|
||||
self.element.unbind({
|
||||
touchstart: $.proxy(self, '_touchStart'),
|
||||
touchmove: $.proxy(self, '_touchMove'),
|
||||
touchend: $.proxy(self, '_touchEnd')
|
||||
});
|
||||
|
||||
// Call the original $.ui.mouse destroy method
|
||||
_mouseDestroy.call(self);
|
||||
};
|
||||
|
||||
})(jQuery);
|
||||
File diff suppressed because one or more lines are too long
@@ -1,519 +0,0 @@
|
||||
/***
|
||||
Spectrum Colorpicker v1.3.3
|
||||
https://github.com/bgrins/spectrum
|
||||
Author: Brian Grinstead
|
||||
License: MIT
|
||||
***/
|
||||
|
||||
.sp-container {
|
||||
position:absolute;
|
||||
top:0;
|
||||
left:0;
|
||||
display:inline-block;
|
||||
*display: inline;
|
||||
*zoom: 1;
|
||||
/* https://github.com/bgrins/spectrum/issues/40 */
|
||||
z-index: 9999994;
|
||||
overflow: hidden;
|
||||
}
|
||||
.sp-container.sp-flat {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Fix for * { box-sizing: border-box; } */
|
||||
.sp-container,
|
||||
.sp-container * {
|
||||
-webkit-box-sizing: content-box;
|
||||
-moz-box-sizing: content-box;
|
||||
box-sizing: content-box;
|
||||
}
|
||||
|
||||
/* http://ansciath.tumblr.com/post/7347495869/css-aspect-ratio */
|
||||
.sp-top {
|
||||
position:relative;
|
||||
width: 100%;
|
||||
display:inline-block;
|
||||
}
|
||||
.sp-top-inner {
|
||||
position:absolute;
|
||||
top:0;
|
||||
left:0;
|
||||
bottom:0;
|
||||
right:0;
|
||||
}
|
||||
.sp-color {
|
||||
position: absolute;
|
||||
top:0;
|
||||
left:0;
|
||||
bottom:0;
|
||||
right:20%;
|
||||
}
|
||||
.sp-hue {
|
||||
position: absolute;
|
||||
top:0;
|
||||
right:0;
|
||||
bottom:0;
|
||||
left:84%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.sp-clear-enabled .sp-hue {
|
||||
top:33px;
|
||||
height: 77.5%;
|
||||
}
|
||||
|
||||
.sp-fill {
|
||||
padding-top: 80%;
|
||||
}
|
||||
.sp-sat, .sp-val {
|
||||
position: absolute;
|
||||
top:0;
|
||||
left:0;
|
||||
right:0;
|
||||
bottom:0;
|
||||
}
|
||||
|
||||
.sp-alpha-enabled .sp-top {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
.sp-alpha-enabled .sp-alpha {
|
||||
display: block;
|
||||
}
|
||||
.sp-alpha-handle {
|
||||
position:absolute;
|
||||
top:-4px;
|
||||
bottom: -4px;
|
||||
width: 6px;
|
||||
left: 50%;
|
||||
cursor: pointer;
|
||||
border: 1px solid black;
|
||||
background: white;
|
||||
opacity: .8;
|
||||
}
|
||||
.sp-alpha {
|
||||
display: none;
|
||||
position: absolute;
|
||||
bottom: -14px;
|
||||
right: 0;
|
||||
left: 0;
|
||||
height: 8px;
|
||||
}
|
||||
.sp-alpha-inner {
|
||||
border: solid 1px #333;
|
||||
}
|
||||
|
||||
.sp-clear {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sp-clear.sp-clear-display {
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
.sp-clear-enabled .sp-clear {
|
||||
display: block;
|
||||
position:absolute;
|
||||
top:0px;
|
||||
right:0;
|
||||
bottom:0;
|
||||
left:84%;
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
/* Don't allow text selection */
|
||||
.sp-container, .sp-replacer, .sp-preview, .sp-dragger, .sp-slider, .sp-alpha, .sp-clear, .sp-alpha-handle, .sp-container.sp-dragging .sp-input, .sp-container button {
|
||||
-webkit-user-select:none;
|
||||
-moz-user-select: -moz-none;
|
||||
-o-user-select:none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.sp-container.sp-input-disabled .sp-input-container {
|
||||
display: none;
|
||||
}
|
||||
.sp-container.sp-buttons-disabled .sp-button-container {
|
||||
display: none;
|
||||
}
|
||||
.sp-palette-only .sp-picker-container {
|
||||
display: none;
|
||||
}
|
||||
.sp-palette-disabled .sp-palette-container {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sp-initial-disabled .sp-initial {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
/* Gradients for hue, saturation and value instead of images. Not pretty... but it works */
|
||||
.sp-sat {
|
||||
background-image: -webkit-gradient(linear, 0 0, 100% 0, from(#FFF), to(rgba(204, 154, 129, 0)));
|
||||
background-image: -webkit-linear-gradient(left, #FFF, rgba(204, 154, 129, 0));
|
||||
background-image: -moz-linear-gradient(left, #fff, rgba(204, 154, 129, 0));
|
||||
background-image: -o-linear-gradient(left, #fff, rgba(204, 154, 129, 0));
|
||||
background-image: -ms-linear-gradient(left, #fff, rgba(204, 154, 129, 0));
|
||||
background-image: linear-gradient(to right, #fff, rgba(204, 154, 129, 0));
|
||||
-ms-filter: "progid:DXImageTransform.Microsoft.gradient(GradientType = 1, startColorstr=#FFFFFFFF, endColorstr=#00CC9A81)";
|
||||
filter : progid:DXImageTransform.Microsoft.gradient(GradientType = 1, startColorstr='#FFFFFFFF', endColorstr='#00CC9A81');
|
||||
}
|
||||
.sp-val {
|
||||
background-image: -webkit-gradient(linear, 0 100%, 0 0, from(#000000), to(rgba(204, 154, 129, 0)));
|
||||
background-image: -webkit-linear-gradient(bottom, #000000, rgba(204, 154, 129, 0));
|
||||
background-image: -moz-linear-gradient(bottom, #000, rgba(204, 154, 129, 0));
|
||||
background-image: -o-linear-gradient(bottom, #000, rgba(204, 154, 129, 0));
|
||||
background-image: -ms-linear-gradient(bottom, #000, rgba(204, 154, 129, 0));
|
||||
background-image: linear-gradient(to top, #000, rgba(204, 154, 129, 0));
|
||||
-ms-filter: "progid:DXImageTransform.Microsoft.gradient(startColorstr=#00CC9A81, endColorstr=#FF000000)";
|
||||
filter : progid:DXImageTransform.Microsoft.gradient(startColorstr='#00CC9A81', endColorstr='#FF000000');
|
||||
}
|
||||
|
||||
.sp-hue {
|
||||
background: -moz-linear-gradient(top, #ff0000 0%, #ffff00 17%, #00ff00 33%, #00ffff 50%, #0000ff 67%, #ff00ff 83%, #ff0000 100%);
|
||||
background: -ms-linear-gradient(top, #ff0000 0%, #ffff00 17%, #00ff00 33%, #00ffff 50%, #0000ff 67%, #ff00ff 83%, #ff0000 100%);
|
||||
background: -o-linear-gradient(top, #ff0000 0%, #ffff00 17%, #00ff00 33%, #00ffff 50%, #0000ff 67%, #ff00ff 83%, #ff0000 100%);
|
||||
background: -webkit-gradient(linear, left top, left bottom, from(#ff0000), color-stop(0.17, #ffff00), color-stop(0.33, #00ff00), color-stop(0.5, #00ffff), color-stop(0.67, #0000ff), color-stop(0.83, #ff00ff), to(#ff0000));
|
||||
background: -webkit-linear-gradient(top, #ff0000 0%, #ffff00 17%, #00ff00 33%, #00ffff 50%, #0000ff 67%, #ff00ff 83%, #ff0000 100%);
|
||||
}
|
||||
|
||||
/* IE filters do not support multiple color stops.
|
||||
Generate 6 divs, line them up, and do two color gradients for each.
|
||||
Yes, really.
|
||||
*/
|
||||
.sp-1 {
|
||||
height:17%;
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0000', endColorstr='#ffff00');
|
||||
}
|
||||
.sp-2 {
|
||||
height:16%;
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffff00', endColorstr='#00ff00');
|
||||
}
|
||||
.sp-3 {
|
||||
height:17%;
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#00ff00', endColorstr='#00ffff');
|
||||
}
|
||||
.sp-4 {
|
||||
height:17%;
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#00ffff', endColorstr='#0000ff');
|
||||
}
|
||||
.sp-5 {
|
||||
height:16%;
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#0000ff', endColorstr='#ff00ff');
|
||||
}
|
||||
.sp-6 {
|
||||
height:17%;
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff00ff', endColorstr='#ff0000');
|
||||
}
|
||||
|
||||
.sp-hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Clearfix hack */
|
||||
.sp-cf:before, .sp-cf:after { content: ""; display: table; }
|
||||
.sp-cf:after { clear: both; }
|
||||
.sp-cf { *zoom: 1; }
|
||||
|
||||
/* Mobile devices, make hue slider bigger so it is easier to slide */
|
||||
@media (max-device-width: 480px) {
|
||||
.sp-color { right: 40%; }
|
||||
.sp-hue { left: 63%; }
|
||||
.sp-fill { padding-top: 60%; }
|
||||
}
|
||||
.sp-dragger {
|
||||
border-radius: 5px;
|
||||
height: 5px;
|
||||
width: 5px;
|
||||
border: 1px solid #fff;
|
||||
background: #000;
|
||||
cursor: pointer;
|
||||
position:absolute;
|
||||
top:0;
|
||||
left: 0;
|
||||
}
|
||||
.sp-slider {
|
||||
position: absolute;
|
||||
top:0;
|
||||
cursor:pointer;
|
||||
height: 3px;
|
||||
left: -1px;
|
||||
right: -1px;
|
||||
border: 1px solid #000;
|
||||
background: white;
|
||||
opacity: .8;
|
||||
}
|
||||
|
||||
/*
|
||||
Theme authors:
|
||||
Here are the basic themeable display options (colors, fonts, global widths).
|
||||
See http://bgrins.github.io/spectrum/themes/ for instructions.
|
||||
*/
|
||||
|
||||
.sp-container {
|
||||
border-radius: 0;
|
||||
background-color: #ECECEC;
|
||||
border: solid 1px #f0c49B;
|
||||
padding: 0;
|
||||
}
|
||||
.sp-container, .sp-container button, .sp-container input, .sp-color, .sp-hue, .sp-clear
|
||||
{
|
||||
font: normal 12px "Lucida Grande", "Lucida Sans Unicode", "Lucida Sans", Geneva, Verdana, sans-serif;
|
||||
-webkit-box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
-ms-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.sp-top
|
||||
{
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
.sp-color, .sp-hue, .sp-clear
|
||||
{
|
||||
border: solid 1px #666;
|
||||
}
|
||||
|
||||
/* Input */
|
||||
.sp-input-container {
|
||||
float:right;
|
||||
width: 100px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.sp-initial-disabled .sp-input-container {
|
||||
width: 100%;
|
||||
}
|
||||
.sp-input {
|
||||
font-size: 12px !important;
|
||||
border: 1px inset;
|
||||
padding: 4px 5px;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
background:transparent;
|
||||
border-radius: 3px;
|
||||
color: #222;
|
||||
}
|
||||
.sp-input:focus {
|
||||
border: 1px solid orange;
|
||||
}
|
||||
.sp-input.sp-validation-error
|
||||
{
|
||||
border: 1px solid red;
|
||||
background: #fdd;
|
||||
}
|
||||
.sp-picker-container , .sp-palette-container
|
||||
{
|
||||
float:left;
|
||||
position: relative;
|
||||
padding: 10px;
|
||||
padding-bottom: 300px;
|
||||
margin-bottom: -290px;
|
||||
}
|
||||
.sp-picker-container
|
||||
{
|
||||
width: 172px;
|
||||
border-left: solid 1px #fff;
|
||||
}
|
||||
|
||||
/* Palettes */
|
||||
.sp-palette-container
|
||||
{
|
||||
border-right: solid 1px #ccc;
|
||||
}
|
||||
|
||||
.sp-palette .sp-thumb-el {
|
||||
display: block;
|
||||
position:relative;
|
||||
float:left;
|
||||
width: 24px;
|
||||
height: 15px;
|
||||
margin: 3px;
|
||||
cursor: pointer;
|
||||
border:solid 2px transparent;
|
||||
}
|
||||
.sp-palette .sp-thumb-el:hover, .sp-palette .sp-thumb-el.sp-thumb-active {
|
||||
border-color: orange;
|
||||
}
|
||||
.sp-thumb-el
|
||||
{
|
||||
position:relative;
|
||||
}
|
||||
|
||||
/* Initial */
|
||||
.sp-initial
|
||||
{
|
||||
float: left;
|
||||
border: solid 1px #333;
|
||||
}
|
||||
.sp-initial span {
|
||||
width: 30px;
|
||||
height: 25px;
|
||||
border:none;
|
||||
display:block;
|
||||
float:left;
|
||||
margin:0;
|
||||
}
|
||||
|
||||
.sp-initial .sp-clear-display {
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.sp-button-container {
|
||||
float: right;
|
||||
}
|
||||
|
||||
/* Replacer (the little preview div that shows up instead of the <input>) */
|
||||
.sp-replacer {
|
||||
margin:0;
|
||||
overflow:hidden;
|
||||
cursor:pointer;
|
||||
padding: 4px;
|
||||
display:inline-block;
|
||||
*zoom: 1;
|
||||
*display: inline;
|
||||
border: solid 1px #91765d;
|
||||
background: #eee;
|
||||
color: #333;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.sp-replacer:hover, .sp-replacer.sp-active {
|
||||
border-color: #F0C49B;
|
||||
color: #111;
|
||||
}
|
||||
.sp-replacer.sp-disabled {
|
||||
cursor:default;
|
||||
border-color: silver;
|
||||
color: silver;
|
||||
}
|
||||
.sp-dd {
|
||||
padding: 2px 0;
|
||||
height: 16px;
|
||||
line-height: 16px;
|
||||
float:left;
|
||||
font-size:10px;
|
||||
}
|
||||
.sp-preview
|
||||
{
|
||||
position:relative;
|
||||
width:25px;
|
||||
height: 20px;
|
||||
border: solid 1px #222;
|
||||
margin-right: 5px;
|
||||
float:left;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.sp-palette
|
||||
{
|
||||
*width: 220px;
|
||||
max-width: 220px;
|
||||
}
|
||||
.sp-palette .sp-thumb-el
|
||||
{
|
||||
width:16px;
|
||||
height: 16px;
|
||||
margin:2px 1px;
|
||||
border: solid 1px #d0d0d0;
|
||||
}
|
||||
|
||||
.sp-container
|
||||
{
|
||||
padding-bottom:0;
|
||||
}
|
||||
|
||||
|
||||
/* Buttons: http://hellohappy.org/css3-buttons/ */
|
||||
.sp-container button {
|
||||
background-color: #eeeeee;
|
||||
background-image: -webkit-linear-gradient(top, #eeeeee, #cccccc);
|
||||
background-image: -moz-linear-gradient(top, #eeeeee, #cccccc);
|
||||
background-image: -ms-linear-gradient(top, #eeeeee, #cccccc);
|
||||
background-image: -o-linear-gradient(top, #eeeeee, #cccccc);
|
||||
background-image: linear-gradient(to bottom, #eeeeee, #cccccc);
|
||||
border: 1px solid #ccc;
|
||||
border-bottom: 1px solid #bbb;
|
||||
border-radius: 3px;
|
||||
color: #333;
|
||||
font-size: 14px;
|
||||
line-height: 1;
|
||||
padding: 5px 4px;
|
||||
text-align: center;
|
||||
text-shadow: 0 1px 0 #eee;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.sp-container button:hover {
|
||||
background-color: #dddddd;
|
||||
background-image: -webkit-linear-gradient(top, #dddddd, #bbbbbb);
|
||||
background-image: -moz-linear-gradient(top, #dddddd, #bbbbbb);
|
||||
background-image: -ms-linear-gradient(top, #dddddd, #bbbbbb);
|
||||
background-image: -o-linear-gradient(top, #dddddd, #bbbbbb);
|
||||
background-image: linear-gradient(to bottom, #dddddd, #bbbbbb);
|
||||
border: 1px solid #bbb;
|
||||
border-bottom: 1px solid #999;
|
||||
cursor: pointer;
|
||||
text-shadow: 0 1px 0 #ddd;
|
||||
}
|
||||
.sp-container button:active {
|
||||
border: 1px solid #aaa;
|
||||
border-bottom: 1px solid #888;
|
||||
-webkit-box-shadow: inset 0 0 5px 2px #aaaaaa, 0 1px 0 0 #eeeeee;
|
||||
-moz-box-shadow: inset 0 0 5px 2px #aaaaaa, 0 1px 0 0 #eeeeee;
|
||||
-ms-box-shadow: inset 0 0 5px 2px #aaaaaa, 0 1px 0 0 #eeeeee;
|
||||
-o-box-shadow: inset 0 0 5px 2px #aaaaaa, 0 1px 0 0 #eeeeee;
|
||||
box-shadow: inset 0 0 5px 2px #aaaaaa, 0 1px 0 0 #eeeeee;
|
||||
}
|
||||
.sp-cancel
|
||||
{
|
||||
font-size: 11px;
|
||||
color: #d93f3f !important;
|
||||
margin:0;
|
||||
padding:2px;
|
||||
margin-right: 5px;
|
||||
vertical-align: middle;
|
||||
text-decoration:none;
|
||||
|
||||
}
|
||||
.sp-cancel:hover
|
||||
{
|
||||
color: #d93f3f !important;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
|
||||
.sp-palette span:hover, .sp-palette span.sp-thumb-active
|
||||
{
|
||||
border-color: #000;
|
||||
}
|
||||
|
||||
.sp-preview, .sp-alpha, .sp-thumb-el
|
||||
{
|
||||
position:relative;
|
||||
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAGUlEQVQYV2M4gwH+YwCGIasIUwhT25BVBADtzYNYrHvv4gAAAABJRU5ErkJggg==);
|
||||
}
|
||||
.sp-preview-inner, .sp-alpha-inner, .sp-thumb-inner
|
||||
{
|
||||
display:block;
|
||||
position:absolute;
|
||||
top:0;left:0;bottom:0;right:0;
|
||||
}
|
||||
|
||||
.sp-palette .sp-thumb-inner
|
||||
{
|
||||
background-position: 50% 50%;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.sp-palette .sp-thumb-light.sp-thumb-active .sp-thumb-inner
|
||||
{
|
||||
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAYAAABWzo5XAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAIVJREFUeNpiYBhsgJFMffxAXABlN5JruT4Q3wfi/0DsT64h8UD8HmpIPCWG/KemIfOJCUB+Aoacx6EGBZyHBqI+WsDCwuQ9mhxeg2A210Ntfo8klk9sOMijaURm7yc1UP2RNCMbKE9ODK1HM6iegYLkfx8pligC9lCD7KmRof0ZhjQACDAAceovrtpVBRkAAAAASUVORK5CYII=);
|
||||
}
|
||||
|
||||
.sp-palette .sp-thumb-dark.sp-thumb-active .sp-thumb-inner
|
||||
{
|
||||
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAYAAABWzo5XAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAadEVYdFNvZnR3YXJlAFBhaW50Lk5FVCB2My41LjEwMPRyoQAAAMdJREFUOE+tkgsNwzAMRMugEAahEAahEAZhEAqlEAZhEAohEAYh81X2dIm8fKpEspLGvudPOsUYpxE2BIJCroJmEW9qJ+MKaBFhEMNabSy9oIcIPwrB+afvAUFoK4H0tMaQ3XtlrggDhOVVMuT4E5MMG0FBbCEYzjYT7OxLEvIHQLY2zWwQ3D+9luyOQTfKDiFD3iUIfPk8VqrKjgAiSfGFPecrg6HN6m/iBcwiDAo7WiBeawa+Kwh7tZoSCGLMqwlSAzVDhoK+6vH4G0P5wdkAAAAASUVORK5CYII=);
|
||||
}
|
||||
|
||||
.sp-clear-display {
|
||||
background-repeat:no-repeat;
|
||||
background-position: center;
|
||||
background-image: url(data:image/gif;base64,R0lGODlhFAAUAPcAAAAAAJmZmZ2dnZ6enqKioqOjo6SkpKWlpaampqenp6ioqKmpqaqqqqurq/Hx8fLy8vT09PX19ff39/j4+Pn5+fr6+vv7+wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAP8ALAAAAAAUABQAAAihAP9FoPCvoMGDBy08+EdhQAIJCCMybCDAAYUEARBAlFiQQoMABQhKUJBxY0SPICEYHBnggEmDKAuoPMjS5cGYMxHW3IiT478JJA8M/CjTZ0GgLRekNGpwAsYABHIypcAgQMsITDtWJYBR6NSqMico9cqR6tKfY7GeBCuVwlipDNmefAtTrkSzB1RaIAoXodsABiZAEFB06gIBWC1mLVgBa0AAOw==);
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
11268
src/Umbraco.Web.UI.Client/package-lock.json
generated
11268
src/Umbraco.Web.UI.Client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -5,7 +5,7 @@
|
||||
"build": "gulp"
|
||||
},
|
||||
"dependencies": {
|
||||
"ace-builds": "1.3.3",
|
||||
"ace-builds": "1.4.2",
|
||||
"angular": "1.7.5",
|
||||
"angular-animate": "1.7.5",
|
||||
"angular-cookies": "1.7.5",
|
||||
@@ -17,57 +17,55 @@
|
||||
"angular-route": "1.7.5",
|
||||
"angular-sanitize": "1.7.5",
|
||||
"angular-touch": "1.7.5",
|
||||
"angular-ui-sortable": "0.15.0",
|
||||
"angular-ui-sortable": "0.19.0",
|
||||
"animejs": "2.2.0",
|
||||
"bootstrap-social": "4.8.0",
|
||||
"clipboard": "2.0.0",
|
||||
"diff": "3.4.0",
|
||||
"bootstrap-social": "5.1.1",
|
||||
"clipboard": "2.0.4",
|
||||
"diff": "3.5.0",
|
||||
"flatpickr": "4.5.2",
|
||||
"font-awesome": "4.7.0",
|
||||
"jquery": "2.2.4",
|
||||
"jquery-migrate": "1.4.0",
|
||||
"jquery": "3.3.1",
|
||||
"jquery-ui-dist": "1.12.1",
|
||||
"jquery-validation": "1.17.0",
|
||||
"jquery-validation-unobtrusive": "3.2.10",
|
||||
"jquery-ui-touch-punch": "0.2.3",
|
||||
"lazyload-js": "1.0.0",
|
||||
"moment": "2.10.6",
|
||||
"moment": "2.22.2",
|
||||
"ng-file-upload": "12.2.13",
|
||||
"nouislider": "12.1.0",
|
||||
"npm": "^6.4.1",
|
||||
"signalr": "2.3.0",
|
||||
"tinymce": "4.8.3",
|
||||
"typeahead.js": "0.10.5",
|
||||
"signalr": "2.4.0",
|
||||
"spectrum-colorpicker": "1.8.0",
|
||||
"tinymce": "4.9.0",
|
||||
"typeahead.js": "0.11.1",
|
||||
"underscore": "1.9.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.1.2",
|
||||
"@babel/preset-env": "^7.1.0",
|
||||
"autoprefixer": "^6.5.0",
|
||||
"bower-installer": "^1.2.0",
|
||||
"gulp-clean-css": "3.10.0",
|
||||
"cssnano": "^3.7.6",
|
||||
"gulp": "^3.9.1",
|
||||
"gulp-babel": "^8.0.0-beta.2",
|
||||
"gulp-concat": "^2.6.0",
|
||||
"gulp-connect": "5.0.0",
|
||||
"@babel/core": "7.1.6",
|
||||
"@babel/preset-env": "7.1.6",
|
||||
"autoprefixer": "9.3.1",
|
||||
"gulp-clean-css": "4.0.0",
|
||||
"cssnano": "4.1.7",
|
||||
"gulp": "3.9.1",
|
||||
"gulp-babel": "8.0.0",
|
||||
"gulp-concat": "2.6.1",
|
||||
"gulp-connect": "5.6.1",
|
||||
"gulp-eslint": "^5.0.0",
|
||||
"gulp-imagemin": "^4.1.0",
|
||||
"gulp-less": "^3.5.0",
|
||||
"gulp-ngdocs": "^0.3.0",
|
||||
"gulp-open": "^2.1.0",
|
||||
"gulp-postcss": "^6.2.0",
|
||||
"gulp-rename": "^1.2.2",
|
||||
"gulp-sort": "^2.0.0",
|
||||
"gulp-watch": "^4.3.10",
|
||||
"gulp-wrap": "^0.13.0",
|
||||
"gulp-wrap-js": "^0.4.1",
|
||||
"jasmine-core": "3.1.0",
|
||||
"karma": "^2.0.2",
|
||||
"karma-jasmine": "^1.1.2",
|
||||
"karma-phantomjs-launcher": "^1.0.4",
|
||||
"less": "^2.7.3",
|
||||
"lodash": "^4.17.5",
|
||||
"merge-stream": "^1.0.1",
|
||||
"run-sequence": "^2.2.1"
|
||||
"gulp-imagemin": "5.0.3",
|
||||
"gulp-less": "4.0.1",
|
||||
"gulp-ngdocs": "0.3.0",
|
||||
"gulp-open": "3.0.1",
|
||||
"gulp-postcss": "8.0.0",
|
||||
"gulp-rename": "1.4.0",
|
||||
"gulp-sort": "2.0.0",
|
||||
"gulp-watch": "5.0.1",
|
||||
"gulp-wrap": "0.14.0",
|
||||
"gulp-wrap-js": "0.4.1",
|
||||
"jasmine-core": "3.3.0",
|
||||
"karma": "3.1.1",
|
||||
"karma-jasmine": "2.0.1",
|
||||
"karma-phantomjs-launcher": "1.0.4",
|
||||
"less": "3.9.0",
|
||||
"lodash": "4.17.11",
|
||||
"merge-stream": "1.0.1",
|
||||
"run-sequence": "2.2.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,7 +161,6 @@
|
||||
vm.errorMsg = "";
|
||||
resetInputValidation();
|
||||
vm.view = "login";
|
||||
setFieldFocus("loginForm", "username");
|
||||
}
|
||||
|
||||
function showRequestPasswordReset() {
|
||||
@@ -169,14 +168,12 @@
|
||||
resetInputValidation();
|
||||
vm.view = "request-password-reset";
|
||||
vm.showEmailResetConfirmation = false;
|
||||
setFieldFocus("requestPasswordResetForm", "email");
|
||||
}
|
||||
|
||||
function showSetPassword() {
|
||||
vm.errorMsg = "";
|
||||
resetInputValidation();
|
||||
vm.view = "set-password";
|
||||
setFieldFocus("setPasswordForm", "password");
|
||||
}
|
||||
|
||||
function loginSubmit(login, password) {
|
||||
@@ -395,12 +392,6 @@
|
||||
});
|
||||
}
|
||||
|
||||
function setFieldFocus(form, field) {
|
||||
$timeout(function () {
|
||||
$("form[name='" + form + "'] input[name='" + field + "']").focus();
|
||||
});
|
||||
}
|
||||
|
||||
function show2FALoginDialog(view, callback) {
|
||||
// TODO: show 2FA window
|
||||
}
|
||||
|
||||
@@ -97,13 +97,6 @@
|
||||
eventsService.unsubscribe(evts[e]);
|
||||
}
|
||||
|
||||
evts.push(eventsService.on("editors.documentType.saved", function (name, args) {
|
||||
// if this content item uses the updated doc type we need to reload the content item
|
||||
if (args && args.documentType && args.documentType.key === $scope.content.documentType.key) {
|
||||
loadContent();
|
||||
}
|
||||
}));
|
||||
|
||||
evts.push(eventsService.on("editors.content.reload", function (name, args) {
|
||||
// if this content item uses the updated doc type we need to reload the content item
|
||||
if(args && args.node && args.node.key === $scope.content.key) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
function ContentNodeInfoDirective($timeout, logResource, eventsService, userService, localizationService, dateHelper, editorService, redirectUrlsResource) {
|
||||
function ContentNodeInfoDirective($timeout, logResource, eventsService, userService, localizationService, dateHelper, editorService, redirectUrlsResource, overlayService) {
|
||||
|
||||
function link(scope, element, attrs, umbVariantContentCtrl) {
|
||||
|
||||
@@ -35,7 +35,9 @@
|
||||
"content_unpublished",
|
||||
"content_published",
|
||||
"content_publishedPendingChanges",
|
||||
"content_notCreated"
|
||||
"content_notCreated",
|
||||
"prompt_unsavedChanges",
|
||||
"prompt_doctypeChangeWarning"
|
||||
];
|
||||
|
||||
localizationService.localizeMany(keys)
|
||||
@@ -45,6 +47,8 @@
|
||||
labels.published = data[2];
|
||||
labels.publishedPendingChanges = data[3];
|
||||
labels.notCreated = data[4];
|
||||
labels.unsavedChanges = data[5];
|
||||
labels.doctypeChangeWarning = data[6];
|
||||
|
||||
setNodePublishStatus(scope.node);
|
||||
|
||||
@@ -87,9 +91,40 @@
|
||||
};
|
||||
|
||||
scope.openDocumentType = function (documentType) {
|
||||
var editor = {
|
||||
|
||||
const variantIsDirty = _.some(scope.node.variants, function(variant) {
|
||||
return variant.isDirty;
|
||||
});
|
||||
|
||||
// add confirmation dialog before opening the doc type editor
|
||||
if(variantIsDirty) {
|
||||
const confirm = {
|
||||
title: labels.unsavedChanges,
|
||||
view: "default",
|
||||
content: labels.doctypeChangeWarning,
|
||||
submitButtonLabelKey: "general_continue",
|
||||
closeButtonLabelKey: "general_cancel",
|
||||
submit: function() {
|
||||
openDocTypeEditor(documentType);
|
||||
overlayService.close();
|
||||
},
|
||||
close: function() {
|
||||
overlayService.close();
|
||||
}
|
||||
};
|
||||
overlayService.open(confirm);
|
||||
} else {
|
||||
openDocTypeEditor(documentType);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
function openDocTypeEditor(documentType) {
|
||||
const editor = {
|
||||
id: documentType.id,
|
||||
submit: function(model) {
|
||||
const args = { node: scope.node };
|
||||
eventsService.emit('editors.content.reload', args);
|
||||
editorService.close();
|
||||
},
|
||||
close: function() {
|
||||
@@ -97,7 +132,7 @@
|
||||
}
|
||||
};
|
||||
editorService.documentTypeEditor(editor);
|
||||
};
|
||||
}
|
||||
|
||||
scope.openTemplate = function () {
|
||||
var templateEditor = {
|
||||
|
||||
@@ -14,7 +14,7 @@ angular.module("umbraco.directives")
|
||||
};
|
||||
|
||||
|
||||
element.bind("focus", function(){
|
||||
element.on("focus", function(){
|
||||
|
||||
var range = document.createRange();
|
||||
range.selectNodeContents(element[0]);
|
||||
@@ -25,7 +25,7 @@ angular.module("umbraco.directives")
|
||||
|
||||
});
|
||||
|
||||
element.bind("blur keyup change", function() {
|
||||
element.on("blur keyup change", function() {
|
||||
scope.$apply(read);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ function fixNumber($parse) {
|
||||
return;
|
||||
}
|
||||
|
||||
elem.bind('input', function (e) {
|
||||
elem.on('input', function (e) {
|
||||
var validity = elem.prop('validity');
|
||||
scope.$apply(function () {
|
||||
ctrl.$setValidity('number', !validity.badInput);
|
||||
|
||||
@@ -5,7 +5,7 @@ angular.module("umbraco.directives").directive('focusWhen', function ($timeout)
|
||||
attrs.$observe("focusWhen", function (newValue) {
|
||||
if (newValue === "true") {
|
||||
$timeout(function () {
|
||||
elm.focus();
|
||||
elm.trigger("focus");
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -43,15 +43,15 @@ angular.module("umbraco.directives")
|
||||
|
||||
// when keycombo is enter and a link or button has focus - click the link or button instead of using the hotkey
|
||||
if (keyCombo === "enter" && clickableElements.indexOf(activeElementType) === 0) {
|
||||
document.activeElement.click();
|
||||
document.activeElement.trigger( "click" );
|
||||
} else {
|
||||
element.click();
|
||||
element.trigger("click");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
} else {
|
||||
element.focus();
|
||||
element.trigger("focus");
|
||||
}
|
||||
|
||||
}, options);
|
||||
|
||||
@@ -25,7 +25,7 @@ angular.module("umbraco.directives")
|
||||
});
|
||||
}
|
||||
|
||||
$(element).click(function (event) {
|
||||
$(element).on("click", function (event) {
|
||||
if (event.metaKey || event.ctrlKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ angular.module("umbraco.directives")
|
||||
});
|
||||
}
|
||||
|
||||
$(element).keypress(function (event) {
|
||||
$(element).on("keypress", function (event) {
|
||||
if (event.which === 13) {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
angular.module("umbraco.directives")
|
||||
.directive('selectOnFocus', function () {
|
||||
return function (scope, el, attrs) {
|
||||
$(el).bind("click", function () {
|
||||
$(el).on("click", function () {
|
||||
var editmode = $(el).data("editmode");
|
||||
//If editmode is true a click is handled like a normal click
|
||||
if (!editmode) {
|
||||
@@ -11,7 +11,7 @@ angular.module("umbraco.directives")
|
||||
$(el).data("editmode", true);
|
||||
}
|
||||
}).
|
||||
bind("blur", function () {
|
||||
on("blur", function () {
|
||||
//Reset on focus lost
|
||||
$(el).data("editmode", false);
|
||||
});
|
||||
|
||||
@@ -5,7 +5,7 @@ angular.module("umbraco.directives")
|
||||
var update = function() {
|
||||
//if it uses its default naming
|
||||
if(element.val() === "" || attr.focusOnFilled){
|
||||
element.focus();
|
||||
element.trigger("focus");
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -142,8 +142,8 @@ angular.module("umbraco.directives")
|
||||
});
|
||||
|
||||
scope.$on('$destroy', function() {
|
||||
element.unbind('keyup keydown keypress change', update);
|
||||
element.unbind('blur', update(true));
|
||||
element.off('keyup keydown keypress change', update);
|
||||
element.off('blur', update(true));
|
||||
unbindModelWatcher();
|
||||
|
||||
// clean up IE dom element
|
||||
|
||||
@@ -67,7 +67,7 @@ angular.module("umbraco.directives")
|
||||
|
||||
$timeout(function () {
|
||||
if (scope.value === null) {
|
||||
editor.focus();
|
||||
editor.trigger("focus");
|
||||
}
|
||||
}, 400);
|
||||
|
||||
|
||||
@@ -266,7 +266,7 @@ angular.module("umbraco.directives")
|
||||
//ie hack
|
||||
if(window.navigator.userAgent.indexOf("MSIE ") >= 0){
|
||||
var ranger = element.find("input");
|
||||
ranger.bind("change",function(){
|
||||
ranger.on("change",function(){
|
||||
scope.$apply(function(){
|
||||
scope.dimensions.scale.current = ranger.val();
|
||||
});
|
||||
|
||||
@@ -500,7 +500,7 @@ Opens an overlay to show a custom YSOD. </br>
|
||||
|
||||
overlayNumber = overlayHelper.registerOverlay();
|
||||
|
||||
$(document).bind("keydown.overlay-" + overlayNumber, function(event) {
|
||||
$(document).on("keydown.overlay-" + overlayNumber, function(event) {
|
||||
|
||||
if (event.which === 27) {
|
||||
|
||||
@@ -527,7 +527,7 @@ Opens an overlay to show a custom YSOD. </br>
|
||||
var submitOnEnterValue = submitOnEnter ? document.activeElement.getAttribute("overlay-submit-on-enter") : "";
|
||||
|
||||
if(clickableElements.indexOf(activeElementType) === 0) {
|
||||
document.activeElement.click();
|
||||
document.activeElement.trigger("click");
|
||||
event.preventDefault();
|
||||
} else if(activeElementType === "TEXTAREA" && !submitOnEnter) {
|
||||
|
||||
@@ -557,7 +557,7 @@ Opens an overlay to show a custom YSOD. </br>
|
||||
|
||||
overlayHelper.unregisterOverlay();
|
||||
|
||||
$(document).unbind("keydown.overlay-" + overlayNumber);
|
||||
$(document).off("keydown.overlay-" + overlayNumber);
|
||||
|
||||
isRegistered = false;
|
||||
}
|
||||
@@ -582,12 +582,12 @@ Opens an overlay to show a custom YSOD. </br>
|
||||
|
||||
var overlayIndex = overlayNumber - 1;
|
||||
var indentSize = overlayIndex * 20;
|
||||
var overlayWidth = el.context.clientWidth;
|
||||
var overlayWidth = el[0].clientWidth;
|
||||
|
||||
el.css('width', overlayWidth - indentSize);
|
||||
|
||||
if(scope.position === "center" && overlayIndex > 0 || scope.position === "target" && overlayIndex > 0) {
|
||||
var overlayTopPosition = el.context.offsetTop;
|
||||
var overlayTopPosition = el[0].offsetTop;
|
||||
el.css('top', overlayTopPosition + indentSize);
|
||||
}
|
||||
|
||||
@@ -621,8 +621,8 @@ Opens an overlay to show a custom YSOD. </br>
|
||||
mousePositionClickY = scope.model.event.pageY;
|
||||
|
||||
// element size
|
||||
elementHeight = el.context.clientHeight;
|
||||
elementWidth = el.context.clientWidth;
|
||||
elementHeight = el[0].clientHeight;
|
||||
elementWidth = el[0].clientWidth;
|
||||
|
||||
// move element to this position
|
||||
position.left = mousePositionClickX - (elementWidth / 2);
|
||||
|
||||
@@ -128,12 +128,12 @@ Use this directive to render a tabs navigation.
|
||||
});
|
||||
}
|
||||
|
||||
$(window).bind('resize.tabsNav', function () {
|
||||
$(window).on('resize.tabsNav', function () {
|
||||
calculateWidth();
|
||||
});
|
||||
|
||||
scope.$on('$destroy', function() {
|
||||
$(window).unbind('resize.tabsNav');
|
||||
$(window).off('resize.tabsNav');
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
@@ -104,8 +104,8 @@ Use this directive to render a tooltip.
|
||||
};
|
||||
|
||||
// element size
|
||||
elementHeight = el.context.clientHeight;
|
||||
elementWidth = el.context.clientWidth;
|
||||
elementHeight = el[0].clientHeight;
|
||||
elementWidth = el[0].clientWidth;
|
||||
|
||||
position.left = event.pageX - (elementWidth / 2);
|
||||
position.top = event.pageY;
|
||||
|
||||
@@ -12,7 +12,7 @@ function umbFileUpload() {
|
||||
restrict: "A",
|
||||
scope: true, //create a new scope
|
||||
link: function (scope, el, attrs) {
|
||||
el.bind('change', function (event) {
|
||||
el.on('change', function (event) {
|
||||
var files = event.target.files;
|
||||
//emit event upward
|
||||
scope.$emit("filesSelected", { files: files });
|
||||
|
||||
@@ -87,7 +87,7 @@ angular.module('umbraco.directives')
|
||||
if (focusSet) {
|
||||
currentIndex++;
|
||||
}
|
||||
listItems[currentIndex].focus();
|
||||
listItems[currentIndex].trigger("focus");
|
||||
focusSet = true;
|
||||
}
|
||||
}
|
||||
@@ -95,7 +95,7 @@ angular.module('umbraco.directives')
|
||||
function arrowUp() {
|
||||
if (currentIndex > 0) {
|
||||
currentIndex--;
|
||||
listItems[currentIndex].focus();
|
||||
listItems[currentIndex].trigger("focus");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
}));
|
||||
|
||||
//no isolate scope to listen to element destroy
|
||||
element.bind('$destroy', function () {
|
||||
element.on('$destroy', function () {
|
||||
for (var u in unsubscribe) {
|
||||
unsubscribe[u]();
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ angular.module('umbraco.directives.validation')
|
||||
.directive('valTriggerChange', function($sniffer) {
|
||||
return {
|
||||
link : function(scope, elem, attrs) {
|
||||
elem.bind('click', function(){
|
||||
elem.on('click', function(){
|
||||
$(attrs.valTriggerChange).trigger($sniffer.hasEvent('input') ? 'input' : 'change');
|
||||
});
|
||||
},
|
||||
|
||||
@@ -96,7 +96,7 @@ angular.module('umbraco.services')
|
||||
function loadLocales(locales, supportedLocales) {
|
||||
var localeUrls = getMomentLocales(locales, supportedLocales);
|
||||
if (localeUrls.length >= 1) {
|
||||
return assetsService.load(localeUrls, $rootScope);
|
||||
return service.load(localeUrls, $rootScope);
|
||||
}
|
||||
else {
|
||||
$q.when(true);
|
||||
|
||||
@@ -45,8 +45,6 @@ function navigationService($routeParams, $location, $q, $timeout, $injector, eve
|
||||
appState.setMenuState("showMenuDialog", false);
|
||||
appState.setGlobalState("stickyNavigation", false);
|
||||
appState.setGlobalState("showTray", false);
|
||||
|
||||
//$("#search-form input").focus();
|
||||
break;
|
||||
case 'menu':
|
||||
appState.setGlobalState("navMode", "menu");
|
||||
@@ -69,12 +67,6 @@ function navigationService($routeParams, $location, $q, $timeout, $injector, eve
|
||||
appState.setMenuState("showMenu", false);
|
||||
appState.setSectionState("showSearchResults", true);
|
||||
appState.setMenuState("showMenuDialog", false);
|
||||
|
||||
//TODO: This would be much better off in the search field controller listening to appState changes
|
||||
$timeout(function() {
|
||||
$("#search-field").focus();
|
||||
});
|
||||
|
||||
break;
|
||||
default:
|
||||
appState.setGlobalState("navMode", "default");
|
||||
|
||||
@@ -31,7 +31,7 @@ function windowResizeListener($rootScope) {
|
||||
register: function (fn) {
|
||||
registered.push(fn);
|
||||
if (inited === false) {
|
||||
$(window).bind('resize', resize);
|
||||
$(window).on('resize', resize);
|
||||
inited = true;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -13,7 +13,7 @@ function MainController($scope, $location, appState, treeService, notificationsS
|
||||
//the null is important because we do an explicit bool check on this in the view
|
||||
$scope.authenticated = null;
|
||||
$scope.touchDevice = appState.getGlobalState("touchDevice");
|
||||
$scope.editors = [];
|
||||
$scope.infiniteMode = false;
|
||||
$scope.overlay = {};
|
||||
$scope.drawer = {};
|
||||
$scope.search = {};
|
||||
@@ -160,12 +160,12 @@ function MainController($scope, $location, appState, treeService, notificationsS
|
||||
}));
|
||||
|
||||
// event for infinite editors
|
||||
evts.push(eventsService.on("appState.editors.add", function (name, args) {
|
||||
$scope.editors = args.editors;
|
||||
evts.push(eventsService.on("appState.editors.open", function (name, args) {
|
||||
$scope.infiniteMode = args && args.editors.length > 0 ? true : false;
|
||||
}));
|
||||
|
||||
evts.push(eventsService.on("appState.editors.remove", function (name, args) {
|
||||
$scope.editors = args.editors;
|
||||
evts.push(eventsService.on("appState.editors.close", function (name, args) {
|
||||
$scope.infiniteMode = args && args.editors.length > 0 ? true : false;
|
||||
}));
|
||||
|
||||
//ensure to unregister from all events!
|
||||
|
||||
@@ -241,6 +241,15 @@ function NavigationController($scope, $rootScope, $location, $log, $q, $routePar
|
||||
init();
|
||||
}));
|
||||
|
||||
// event for infinite editors
|
||||
evts.push(eventsService.on("appState.editors.open", function (name, args) {
|
||||
$scope.infiniteMode = args && args.editors.length > 0 ? true : false;
|
||||
}));
|
||||
|
||||
evts.push(eventsService.on("appState.editors.close", function (name, args) {
|
||||
$scope.infiniteMode = args && args.editors.length > 0 ? true : false;
|
||||
}));
|
||||
|
||||
/**
|
||||
* Based on the current state of the application, this configures the scope variables that control the main tree and language drop down
|
||||
*/
|
||||
|
||||
@@ -177,6 +177,7 @@
|
||||
@import "utilities/theme/_opacity.less";
|
||||
@import "utilities/typography/_text-decoration.less";
|
||||
@import "utilities/typography/_white-space.less";
|
||||
@import "utilities/typography/_word-break.less";
|
||||
@import "utilities/_flexbox.less";
|
||||
@import "utilities/_spacing.less";
|
||||
@import "utilities/_text-align.less";
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
/*
|
||||
WORD BREAK
|
||||
*/
|
||||
|
||||
.word-normal { word-break: normal; }
|
||||
.word-wrap { word-break: break-all; }
|
||||
.word-nowrap { word-break: keep-all; }
|
||||
@@ -18,18 +18,13 @@
|
||||
|
||||
<umb-tab-content ng-repeat="tab in dashboard.tabs" ng-if="tab.active" tab="tab" class="row-fluid">
|
||||
|
||||
<div ng-repeat="property in tab.properties" ng-switch on="property.serverSide">
|
||||
<div ng-repeat="property in tab.properties">
|
||||
|
||||
<div class="clearfix" ng-switch-when="false">
|
||||
<div class="clearfix">
|
||||
<h3 ng-show="property.caption">{{property.caption}}</h3>
|
||||
<div ng-include="property.path"></div>
|
||||
</div>
|
||||
|
||||
<div class="umb-dashboard-control clearfix" ng-switch-when="true">
|
||||
<h3 ng-show="property.caption">{{property.caption}}</h3>
|
||||
<iframe ng-src="dashboard/usercontrolproxy.aspx?ctrl={{ property.path}}"></iframe>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</umb-tab-content>
|
||||
@@ -40,4 +35,4 @@
|
||||
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -101,7 +101,7 @@ angular.module("umbraco")
|
||||
}
|
||||
|
||||
$scope.upload = function(v) {
|
||||
angular.element(".umb-file-dropzone-directive .file-select").click();
|
||||
angular.element(".umb-file-dropzone-directive .file-select").trigger("click");
|
||||
};
|
||||
|
||||
$scope.dragLeave = function(el, event) {
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@
|
||||
</tr>
|
||||
<tr ng-repeat="property in vm.diff.properties track by property.alias">
|
||||
<td class="bold">{{property.label}}</td>
|
||||
<td ng-class="{'pre-line': property.isObject}">
|
||||
<td ng-class="{'pre-line': property.isObject, 'word-wrap': !property.isObject}">
|
||||
<span ng-repeat="part in property.diff">
|
||||
<ins ng-if="part.added">{{part.value}}</ins>
|
||||
<del ng-if="part.removed">{{part.value}}</del>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
});
|
||||
@@ -161,7 +161,7 @@
|
||||
|
||||
<div class="control-group" ng-class="{error: vm.loginForm.username.$invalid}">
|
||||
<label><localize key="general_username">Username</localize></label>
|
||||
<input type="text" ng-model="vm.login" name="username" class="-full-width-input" localize="placeholder" placeholder="@placeholders_usernameHint" />
|
||||
<input type="text" ng-model="vm.login" name="username" class="-full-width-input" localize="placeholder" placeholder="@placeholders_usernameHint" focus-when="{{vm.view === 'login'}}" />
|
||||
</div>
|
||||
|
||||
<div class="control-group" ng-class="{error: vm.loginForm.password.$invalid}">
|
||||
@@ -199,7 +199,7 @@
|
||||
<form method="POST" name="vm.requestPasswordResetForm" ng-submit="vm.requestPasswordResetSubmit(email)">
|
||||
<div class="control-group" ng-class="{error: requestPasswordResetForm.email.$invalid}">
|
||||
<label><localize key="general_email">Email</localize></label>
|
||||
<input type="email" val-email ng-model="email" name="email" class="-full-width-input" localize="placeholder" placeholder="@placeholders_email" />
|
||||
<input type="email" val-email ng-model="email" name="email" class="-full-width-input" localize="placeholder" placeholder="@placeholders_email" focus-when="{{vm.view === 'request-password-reset'}}" />
|
||||
</div>
|
||||
|
||||
<div class="control-group" ng-show="requestPasswordResetForm.$invalid">
|
||||
@@ -230,7 +230,7 @@
|
||||
|
||||
<div ng-hide="vm.resetComplete" class="control-group" ng-class="{error: vm.setPasswordForm.password.$invalid}">
|
||||
<label><localize key="user_newPassword">New password</localize></label>
|
||||
<input type="password" ng-model="vm.password" name="password" class="-full-width-input" localize="placeholder" placeholder="@placeholders_password" />
|
||||
<input type="password" ng-model="vm.password" name="password" class="-full-width-input" localize="placeholder" placeholder="@placeholders_password" focus-when="{{vm.view === 'set-password'}}" />
|
||||
</div>
|
||||
|
||||
<div ng-hide="vm.resetComplete" class="control-group" ng-class="{error: vm.setPasswordForm.confirmPassword.$invalid}">
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
<div id="tree" ng-show="authenticated">
|
||||
<umb-tree
|
||||
api="treeApi"
|
||||
on-init="onTreeInit()" >
|
||||
on-init="onTreeInit()">
|
||||
</umb-tree>
|
||||
</div>
|
||||
</div>
|
||||
@@ -44,6 +44,9 @@
|
||||
</umb-context-dialog>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="umb-editor__overlay" ng-show="infiniteMode"></div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -5,10 +5,11 @@
|
||||
<localize key="colorpicker_noColors">You haven't defined any colors</localize>
|
||||
</div>
|
||||
|
||||
<umb-color-swatches colors="model.config.items"
|
||||
selected-color="model.value.value"
|
||||
size="m"
|
||||
use-label="model.useLabel">
|
||||
<umb-color-swatches
|
||||
colors="model.config.items"
|
||||
selected-color="model.value"
|
||||
size="m"
|
||||
use-label="model.useLabel">
|
||||
</umb-color-swatches>
|
||||
|
||||
<input type="hidden" name="modelValue" ng-model="model.value" val-property-validator="validateMandatory" />
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -151,13 +151,6 @@
|
||||
<Compile Include="Umbraco\Create.aspx.designer.cs">
|
||||
<DependentUpon>create.aspx</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="Umbraco\Dashboard\UserControlProxy.aspx.cs">
|
||||
<DependentUpon>UserControlProxy.aspx</DependentUpon>
|
||||
<SubType>ASPXCodeBehind</SubType>
|
||||
</Compile>
|
||||
<Compile Include="Umbraco\Dashboard\UserControlProxy.aspx.designer.cs">
|
||||
<DependentUpon>UserControlProxy.aspx</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="Umbraco\Developer\Macros\EditMacro.aspx.cs">
|
||||
<DependentUpon>editMacro.aspx</DependentUpon>
|
||||
<SubType>ASPXCodeBehind</SubType>
|
||||
@@ -201,6 +194,8 @@
|
||||
<Content Include="App_Plugins\ModelsBuilder\modelsbuilder.controller.js" />
|
||||
<Content Include="App_Plugins\ModelsBuilder\modelsbuilder.htm" />
|
||||
<Content Include="App_Plugins\ModelsBuilder\modelsbuilder.resource.js" />
|
||||
<Content Include="App_Plugins\Test\two.html" />
|
||||
<Content Include="App_Plugins\Test\one.html" />
|
||||
<Content Include="Config\grid.editors.config.js" />
|
||||
<Content Include="Config\Lang\cs-CZ.user.xml" />
|
||||
<Content Include="Config\Lang\da-DK.user.xml" />
|
||||
@@ -236,9 +231,9 @@
|
||||
</Compile>
|
||||
<Content Include="Config\Splashes\booting.aspx" />
|
||||
<Content Include="Config\Splashes\noNodes.aspx" />
|
||||
<Content Include="Umbraco\Dashboard\UserControlProxy.aspx" />
|
||||
<Content Include="Umbraco\Install\Views\Web.config" />
|
||||
<Content Include="App_Plugins\ModelsBuilder\package.manifest" />
|
||||
<Content Include="App_Plugins\Test\package.manifest" />
|
||||
<None Include="Config\404handlers.Release.config">
|
||||
<DependentUpon>404handlers.config</DependentUpon>
|
||||
</None>
|
||||
|
||||
@@ -67,7 +67,7 @@
|
||||
<div id="contentcolumn">
|
||||
|
||||
<div class="umb-editor" ng-view></div>
|
||||
<div class="umb-editor__overlay" ng-if="editors.length > 0"></div>
|
||||
<div class="umb-editor__overlay" ng-if="infiniteMode"></div>
|
||||
|
||||
<umb-editors></umb-editors>
|
||||
</div>
|
||||
|
||||
@@ -340,6 +340,7 @@
|
||||
<key alias="confirmListViewPublish">Publishing will make the selected items visible on the site.</key>
|
||||
<key alias="confirmListViewUnpublish">Unpublishing will remove the selected items and all their descendants from the site.</key>
|
||||
<key alias="confirmUnpublish">Unpublishing will remove this page and all its descendants from the site.</key>
|
||||
<key alias="doctypeChangeWarning">You have unsaved changes. Making changes to the Document Type will discard the changes.</key>
|
||||
</area>
|
||||
<area alias="bulk">
|
||||
<key alias="done">Done</key>
|
||||
|
||||
@@ -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" %>
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head runat="server">
|
||||
<title></title>
|
||||
|
||||
<cc1:UmbracoClientDependencyLoader runat="server" ID="ClientLoader" />
|
||||
|
||||
<umb:CssInclude ID="CssInclude1" runat="server" FilePath="assets/css/umbraco.css" PathNameAlias="UmbracoRoot" />
|
||||
|
||||
<umb:JsInclude ID="JsInclude1" runat="server" FilePath="Application/NamespaceManager.js" PathNameAlias="UmbracoClient" Priority="0" />
|
||||
<umb:JsInclude ID="JsInclude4" runat="server" FilePath="lib/jquery-migrate/jquery-migrate.min.js" PathNameAlias="UmbracoRoot" Priority="1" />
|
||||
|
||||
</head>
|
||||
<body style="overflow: scroll">
|
||||
<form id="form1" runat="server">
|
||||
<div>
|
||||
<asp:PlaceHolder ID="container" runat="server" />
|
||||
</div>
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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(
|
||||
"<p class=\"umbracoErrorMessage\">Could not load control: '" + path +
|
||||
"'. <br/><span class=\"guiDialogTiny\"><strong>Error message:</strong> " +
|
||||
ee.ToString() + "</span></p>"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// This code was generated by a tool.
|
||||
//
|
||||
// Changes to this file may cause incorrect behavior and will be lost if
|
||||
// the code is regenerated.
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace Umbraco.Web.UI.Umbraco.Dashboard {
|
||||
|
||||
|
||||
public partial class UserControlProxy {
|
||||
|
||||
/// <summary>
|
||||
/// ClientLoader control.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Auto-generated field.
|
||||
/// To modify move field declaration from designer file to code-behind file.
|
||||
/// </remarks>
|
||||
protected global::Umbraco.Web.UI.JavaScript.UmbracoClientDependencyLoader ClientLoader;
|
||||
|
||||
/// <summary>
|
||||
/// CssInclude1 control.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Auto-generated field.
|
||||
/// To modify move field declaration from designer file to code-behind file.
|
||||
/// </remarks>
|
||||
protected global::ClientDependency.Core.Controls.CssInclude CssInclude1;
|
||||
|
||||
/// <summary>
|
||||
/// JsInclude1 control.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Auto-generated field.
|
||||
/// To modify move field declaration from designer file to code-behind file.
|
||||
/// </remarks>
|
||||
protected global::ClientDependency.Core.Controls.JsInclude JsInclude1;
|
||||
|
||||
/// <summary>
|
||||
/// JsInclude4 control.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Auto-generated field.
|
||||
/// To modify move field declaration from designer file to code-behind file.
|
||||
/// </remarks>
|
||||
protected global::ClientDependency.Core.Controls.JsInclude JsInclude4;
|
||||
|
||||
/// <summary>
|
||||
/// form1 control.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Auto-generated field.
|
||||
/// To modify move field declaration from designer file to code-behind file.
|
||||
/// </remarks>
|
||||
protected global::System.Web.UI.HtmlControls.HtmlForm form1;
|
||||
|
||||
/// <summary>
|
||||
/// container control.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Auto-generated field.
|
||||
/// To modify move field declaration from designer file to code-behind file.
|
||||
/// </remarks>
|
||||
protected global::System.Web.UI.WebControls.PlaceHolder container;
|
||||
}
|
||||
}
|
||||
@@ -1,115 +1,107 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<dashBoard>
|
||||
|
||||
<section alias="StartupSettingsDashboardSection">
|
||||
<areas>
|
||||
<area>settings</area>
|
||||
</areas>
|
||||
<tab caption="Welcome">
|
||||
<control showOnce="true" addPanel="true" panelCaption="">
|
||||
views/dashboard/settings/settingsdashboardintro.html
|
||||
</control>
|
||||
</tab>
|
||||
<tab caption="Examine Management">
|
||||
<control>
|
||||
<section alias="StartupSettingsDashboardSection">
|
||||
<areas>
|
||||
<area>settings</area>
|
||||
</areas>
|
||||
<tab caption="Welcome">
|
||||
<control panelCaption="">
|
||||
views/dashboard/settings/settingsdashboardintro.html
|
||||
</control>
|
||||
</tab>
|
||||
<tab caption="Examine Management">
|
||||
<control>
|
||||
views/dashboard/settings/examinemanagement.html
|
||||
</control>
|
||||
</tab>
|
||||
<tab caption="Published Status">
|
||||
<control>
|
||||
</tab>
|
||||
<tab caption="Published Status">
|
||||
<control>
|
||||
views/dashboard/settings/publishedstatus.html
|
||||
</control>
|
||||
</tab>
|
||||
</section>
|
||||
</tab>
|
||||
</section>
|
||||
|
||||
<section alias="StartupFormsDashboardSection">
|
||||
<areas>
|
||||
<area>forms</area>
|
||||
</areas>
|
||||
<tab caption="Install Umbraco Forms">
|
||||
<control showOnce="true" addPanel="true" panelCaption="">
|
||||
views/dashboard/forms/formsdashboardintro.html
|
||||
</control>
|
||||
</tab>
|
||||
</section>
|
||||
<section alias="StartupFormsDashboardSection">
|
||||
<areas>
|
||||
<area>forms</area>
|
||||
</areas>
|
||||
<tab caption="Install Umbraco Forms">
|
||||
<control panelCaption="">
|
||||
views/dashboard/forms/formsdashboardintro.html
|
||||
</control>
|
||||
</tab>
|
||||
</section>
|
||||
|
||||
<section alias="StartupDeveloperDashboardSection">
|
||||
<areas>
|
||||
<area>developer</area>
|
||||
</areas>
|
||||
</section>
|
||||
<section alias="StartupMediaDashboardSection">
|
||||
<areas>
|
||||
<area>media</area>
|
||||
</areas>
|
||||
<tab caption="Content">
|
||||
<control panelCaption="">
|
||||
views/dashboard/media/mediafolderbrowser.html
|
||||
</control>
|
||||
</tab>
|
||||
</section>
|
||||
|
||||
<section alias="StartupMediaDashboardSection">
|
||||
<areas>
|
||||
<area>media</area>
|
||||
</areas>
|
||||
<tab caption="Content">
|
||||
<control showOnce="false" addPanel="false" panelCaption="">
|
||||
views/dashboard/media/mediafolderbrowser.html
|
||||
</control>
|
||||
</tab>
|
||||
<section alias="StartupDashboardSection">
|
||||
<access>
|
||||
<deny>translator</deny>
|
||||
</access>
|
||||
<areas>
|
||||
<area>content</area>
|
||||
</areas>
|
||||
<tab caption="Get Started">
|
||||
<access>
|
||||
<grant>admin</grant>
|
||||
</access>
|
||||
|
||||
</section>
|
||||
<control panelCaption="">
|
||||
views/dashboard/default/startupdashboardintro.html
|
||||
</control>
|
||||
</tab>
|
||||
</section>
|
||||
|
||||
<section alias="StartupDashboardSection">
|
||||
<access>
|
||||
<deny>translator</deny>
|
||||
</access>
|
||||
<areas>
|
||||
<area>content</area>
|
||||
</areas>
|
||||
<tab caption="Get Started">
|
||||
<access>
|
||||
<grant>admin</grant>
|
||||
</access>
|
||||
<section alias="StartupMemberDashboardSection">
|
||||
<areas>
|
||||
<area>member</area>
|
||||
</areas>
|
||||
<tab caption="Get Started">
|
||||
<control panelCaption="">
|
||||
views/dashboard/members/membersdashboardvideos.html
|
||||
</control>
|
||||
</tab>
|
||||
</section>
|
||||
|
||||
<control showOnce="true" addPanel="true" panelCaption="">
|
||||
views/dashboard/default/startupdashboardintro.html
|
||||
</control>
|
||||
<section alias="UmbracoModelsBuilder">
|
||||
<areas>
|
||||
<area>settings</area>
|
||||
</areas>
|
||||
<tab caption="Models Builder">
|
||||
<control>
|
||||
/App_Plugins/ModelsBuilder/modelsbuilder.htm
|
||||
</control>
|
||||
</tab>
|
||||
</section>
|
||||
|
||||
</tab>
|
||||
</section>
|
||||
|
||||
<section alias="StartupMemberDashboardSection">
|
||||
<areas>
|
||||
<area>member</area>
|
||||
</areas>
|
||||
<tab caption="Get Started">
|
||||
<control showOnce="true" addPanel="true" panelCaption="">
|
||||
views/dashboard/members/membersdashboardvideos.html
|
||||
</control>
|
||||
</tab>
|
||||
</section>
|
||||
|
||||
<section alias="UmbracoModelsBuilder">
|
||||
<areas>
|
||||
<area>developer</area>
|
||||
</areas>
|
||||
<tab caption="Models Builder">
|
||||
<control>
|
||||
/App_Plugins/ModelsBuilder/modelsbuilder.htm
|
||||
</control>
|
||||
</tab>
|
||||
</section>
|
||||
|
||||
<section alias="UmbracoHealthCheck">
|
||||
<areas>
|
||||
<area>settings</area>
|
||||
</areas>
|
||||
<tab caption="Health Check">
|
||||
<control>
|
||||
views/dashboard/settings/healthcheck.html
|
||||
</control>
|
||||
</tab>
|
||||
</section>
|
||||
<section alias="RedirectUrlManagement">
|
||||
<areas>
|
||||
<area>content</area>
|
||||
</areas>
|
||||
<tab caption="Redirect URL Management">
|
||||
<control>
|
||||
views/dashboard/content/redirecturls.html
|
||||
</control>
|
||||
</tab>
|
||||
</section>
|
||||
<section alias="UmbracoHealthCheck">
|
||||
<areas>
|
||||
<area>settings</area>
|
||||
</areas>
|
||||
<tab caption="Health Check">
|
||||
<control>
|
||||
views/dashboard/settings/healthcheck.html
|
||||
</control>
|
||||
</tab>
|
||||
</section>
|
||||
<section alias="RedirectUrlManagement">
|
||||
<areas>
|
||||
<area>content</area>
|
||||
</areas>
|
||||
<tab caption="Redirect URL Management">
|
||||
<control>
|
||||
views/dashboard/content/redirecturls.html
|
||||
</control>
|
||||
</tab>
|
||||
</section>
|
||||
</dashBoard>
|
||||
|
||||
@@ -1,55 +1,49 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<dashBoard>
|
||||
|
||||
<section alias="StartupSettingsDashboardSection">
|
||||
<areas>
|
||||
<area>settings</area>
|
||||
</areas>
|
||||
<tab caption="Welcome">
|
||||
<control showOnce="true" addPanel="true" panelCaption="">
|
||||
<control panelCaption="">
|
||||
views/dashboard/settings/settingsdashboardintro.html
|
||||
</control>
|
||||
</tab>
|
||||
<tab caption="Examine Management">
|
||||
<control>
|
||||
views/dashboard/settings/examinemanagement.html
|
||||
</control>
|
||||
views/dashboard/settings/examinemanagement.html
|
||||
</control>
|
||||
</tab>
|
||||
<tab caption="Published Status">
|
||||
<control>
|
||||
views/dashboard/settings/publishedstatus.html
|
||||
</control>
|
||||
views/dashboard/settings/publishedstatus.html
|
||||
</control>
|
||||
</tab>
|
||||
</section>
|
||||
|
||||
<section alias="StartupFormsDashboardSection">
|
||||
<areas>
|
||||
<area>forms</area>
|
||||
</areas>
|
||||
<tab caption="Install Umbraco Forms">
|
||||
<control showOnce="true" addPanel="true" panelCaption="">
|
||||
<control panelCaption="">
|
||||
views/dashboard/forms/formsdashboardintro.html
|
||||
</control>
|
||||
</tab>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<section alias="StartupMediaDashboardSection">
|
||||
<areas>
|
||||
<area>media</area>
|
||||
</areas>
|
||||
<tab caption="Content">
|
||||
<control showOnce="false" addPanel="false" panelCaption="">
|
||||
<control panelCaption="">
|
||||
views/dashboard/media/mediafolderbrowser.html
|
||||
</control>
|
||||
</tab>
|
||||
</section>
|
||||
<section alias="StartupFormsDashboardSection">
|
||||
<areas>
|
||||
<area>forms</area>
|
||||
</areas>
|
||||
<tab caption="Install Umbraco Forms">
|
||||
<control showOnce="true" addPanel="true" panelCaption="">
|
||||
views/dashboard/forms/formsdashboardintro.html
|
||||
</control>
|
||||
</tab>
|
||||
</section>
|
||||
|
||||
<section alias="StartupDashboardSection">
|
||||
<access>
|
||||
<deny>translator</deny>
|
||||
@@ -61,29 +55,35 @@
|
||||
<access>
|
||||
<grant>admin</grant>
|
||||
</access>
|
||||
<control showOnce="true" addPanel="true" panelCaption="">
|
||||
|
||||
<control panelCaption="">
|
||||
views/dashboard/default/startupdashboardintro.html
|
||||
</control>
|
||||
</tab>
|
||||
</section>
|
||||
|
||||
<section alias="StartupMemberDashboardSection">
|
||||
<areas>
|
||||
<area>member</area>
|
||||
</areas>
|
||||
<tab caption="Get Started">
|
||||
<control showOnce="true" addPanel="true" panelCaption="">
|
||||
<control panelCaption="">
|
||||
views/dashboard/members/membersdashboardvideos.html
|
||||
</control>
|
||||
</tab>
|
||||
</section>
|
||||
<section alias="contour">
|
||||
|
||||
<section alias="UmbracoModelsBuilder">
|
||||
<areas>
|
||||
<area>contour</area>
|
||||
<area>settings</area>
|
||||
</areas>
|
||||
<tab caption="Dashboard">
|
||||
<control>plugins/umbracocontour/formsdashboard.ascx</control>
|
||||
<tab caption="Models Builder">
|
||||
<control>
|
||||
/App_Plugins/ModelsBuilder/modelsbuilder.htm
|
||||
</control>
|
||||
</tab>
|
||||
</section>
|
||||
|
||||
<section alias="UmbracoHealthCheck">
|
||||
<areas>
|
||||
<area>settings</area>
|
||||
@@ -104,14 +104,4 @@
|
||||
</control>
|
||||
</tab>
|
||||
</section>
|
||||
<section alias="UmbracoModelsBuilder">
|
||||
<areas>
|
||||
<area>settings</area>
|
||||
</areas>
|
||||
<tab caption="Models Builder">
|
||||
<control>
|
||||
/App_Plugins/ModelsBuilder/modelsbuilder.htm
|
||||
</control>
|
||||
</tab>
|
||||
</section>
|
||||
</dashBoard>
|
||||
|
||||
@@ -184,6 +184,30 @@ namespace Umbraco.Core.Components
|
||||
composition.RegisterUnique(_ => helper);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the default controller for rendering template views.
|
||||
/// </summary>
|
||||
/// <typeparam name="TController">The type of the controller.</typeparam>
|
||||
/// <param name="composition">The composition.</param>
|
||||
/// <remarks>The controller type is registered to the container by the composition.</remarks>
|
||||
public static void SetDefaultRenderMvcController<TController>(this Composition composition)
|
||||
=> composition.SetDefaultRenderMvcController(typeof(TController));
|
||||
|
||||
/// <summary>
|
||||
/// Sets the default controller for rendering template views.
|
||||
/// </summary>
|
||||
/// <param name="composition">The composition.</param>
|
||||
/// <param name="controllerType">The type of the controller.</param>
|
||||
/// <remarks>The controller type is registered to the container by the composition.</remarks>
|
||||
public static void SetDefaultRenderMvcController(this Composition composition, Type controllerType)
|
||||
{
|
||||
composition.OnCreatingFactory["Umbraco.Core.DefaultRenderMvcController"] = () =>
|
||||
{
|
||||
composition.Register(controllerType, Lifetime.Request);
|
||||
Current.DefaultRenderMvcControllerType = controllerType;
|
||||
};
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,8 @@ namespace Umbraco.Web.Editors
|
||||
[WebApi.UmbracoAuthorize]
|
||||
public class DashboardController : UmbracoApiController
|
||||
{
|
||||
private readonly Dashboards _dashboards;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DashboardController"/> with auto dependencies.
|
||||
/// </summary>
|
||||
@@ -36,9 +38,11 @@ namespace Umbraco.Web.Editors
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DashboardController"/> with all its dependencies.
|
||||
/// </summary>
|
||||
public DashboardController(IGlobalSettings globalSettings, IUmbracoContextAccessor umbracoContextAccessor, ISqlContext sqlContext, ServiceContext services, CacheHelper applicationCache, IProfilingLogger logger, IRuntimeState runtimeState)
|
||||
public DashboardController(IGlobalSettings globalSettings, IUmbracoContextAccessor umbracoContextAccessor, ISqlContext sqlContext, ServiceContext services, CacheHelper applicationCache, IProfilingLogger logger, IRuntimeState runtimeState, Dashboards dashboards)
|
||||
: base(globalSettings, umbracoContextAccessor, sqlContext, services, applicationCache, logger, runtimeState)
|
||||
{ }
|
||||
{
|
||||
_dashboards = dashboards;
|
||||
}
|
||||
|
||||
//we have just one instance of HttpClient shared for the entire application
|
||||
private static readonly HttpClient HttpClient = new HttpClient();
|
||||
@@ -47,10 +51,6 @@ namespace Umbraco.Web.Editors
|
||||
[ValidateAngularAntiForgeryToken]
|
||||
public async Task<JObject> 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;
|
||||
@@ -133,8 +133,7 @@ namespace Umbraco.Web.Editors
|
||||
[ValidateAngularAntiForgeryToken]
|
||||
public IEnumerable<Tab<DashboardControl>> GetDashboard(string section)
|
||||
{
|
||||
var dashboardHelper = new DashboardHelper(Services.SectionService);
|
||||
return dashboardHelper.GetDashboard(section, Security.CurrentUser);
|
||||
return _dashboards.GetDashboards(section, Security.CurrentUser);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the dashboard models per section for the current user and it's access
|
||||
/// </summary>
|
||||
/// <param name="currentUser"></param>
|
||||
/// <returns></returns>
|
||||
public IDictionary<string, IEnumerable<Tab<DashboardControl>>> GetDashboards(IUser currentUser)
|
||||
{
|
||||
var result = new Dictionary<string, IEnumerable<Tab<DashboardControl>>>();
|
||||
foreach (var section in _sectionService.GetSections())
|
||||
{
|
||||
result[section.Alias] = GetDashboard(section.Alias, currentUser);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the dashboard model for the given section based on the current user and it's access
|
||||
/// </summary>
|
||||
/// <param name="section"></param>
|
||||
/// <param name="currentUser"></param>
|
||||
/// <returns></returns>
|
||||
public IEnumerable<Tab<DashboardControl>> GetDashboard(string section, IUser currentUser)
|
||||
{
|
||||
var tabs = new List<Tab<DashboardControl>>();
|
||||
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<DashboardControl>();
|
||||
|
||||
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<DashboardControl>
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<IAccessRule> 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<IAccessRule>(), grantRules ?? Array.Empty<IAccessRule>(), grantBySectionRules ?? Array.Empty<IAccessRule>());
|
||||
}
|
||||
|
||||
public static bool CheckUserAccessByRules(IUser user, ISectionService sectionService, IEnumerable<IAccessRule> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
159
src/Umbraco.Web/Editors/Dashboards.cs
Normal file
159
src/Umbraco.Web/Editors/Dashboards.cs
Normal file
@@ -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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all dashboards, organized by section, for a user.
|
||||
/// </summary>
|
||||
public IDictionary<string, IEnumerable<Tab<DashboardControl>>> GetDashboards(IUser currentUser)
|
||||
{
|
||||
return _sectionService.GetSections().ToDictionary(x => x.Alias, x => GetDashboards(x.Alias, currentUser));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns dashboards for a specific section, for a user.
|
||||
/// </summary>
|
||||
public IEnumerable<Tab<DashboardControl>> 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<Tab<DashboardControl>> GetDashboardsFromConfig(ref int tabId, string section, IUser currentUser)
|
||||
{
|
||||
var tabs = new List<Tab<DashboardControl>>();
|
||||
|
||||
// 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<DashboardControl>();
|
||||
|
||||
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<DashboardControl>
|
||||
{
|
||||
Id = tabId++,
|
||||
Alias = tab.Caption.ToSafeAlias(),
|
||||
Label = tab.Caption,
|
||||
Properties = dashboardControls
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return tabs;
|
||||
}
|
||||
|
||||
private List<Tab<DashboardControl>> GetDashboardsFromPlugins(ref int tabId, string section, IUser currentUser)
|
||||
{
|
||||
var tabs = new List<Tab<DashboardControl>>();
|
||||
|
||||
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<DashboardControl>
|
||||
{
|
||||
Id = tabId++,
|
||||
Alias = dashboard.Alias.ToSafeAlias(),
|
||||
Label = dashboard.Name,
|
||||
Properties = new[] { dashboardControl }
|
||||
});
|
||||
}
|
||||
|
||||
return tabs;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,38 +15,35 @@ namespace Umbraco.Web.Editors
|
||||
[PluginController("UmbracoApi")]
|
||||
public class SectionController : UmbracoAuthorizedJsonController
|
||||
{
|
||||
private readonly Dashboards _dashboards;
|
||||
|
||||
public SectionController(Dashboards dashboards)
|
||||
{
|
||||
_dashboards = dashboards;
|
||||
}
|
||||
|
||||
public IEnumerable<Section> GetSections()
|
||||
{
|
||||
var sections = Services.SectionService.GetAllowedSections(Security.GetUserId().ResultOr(0));
|
||||
|
||||
var sectionModels = sections.Select(Mapper.Map<Core.Models.Section, Section>).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
|
||||
var appTreeController = new ApplicationTreeController { ControllerContext = ControllerContext };
|
||||
|
||||
var dashboards = dashboardHelper.GetDashboards(Security.CurrentUser);
|
||||
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 = false;
|
||||
if (dashboards.TryGetValue(section.Alias, out var dashboardsForSection))
|
||||
{
|
||||
if (dashboardsForSection.Any())
|
||||
hasDashboards = true;
|
||||
}
|
||||
var hasDashboards = dashboards.TryGetValue(section.Alias, out var dashboardsForSection) && dashboardsForSection.Any();
|
||||
if (hasDashboards) continue;
|
||||
|
||||
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);
|
||||
}
|
||||
// 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;
|
||||
|
||||
@@ -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; }
|
||||
|
||||
|
||||
@@ -120,8 +120,12 @@ namespace Umbraco.Web.Runtime
|
||||
// configure the container for web
|
||||
composition.ConfigureForWeb();
|
||||
|
||||
|
||||
composition.RegisterUnique<Dashboards>();
|
||||
|
||||
composition
|
||||
.ComposeUmbracoControllers(GetType().Assembly);
|
||||
.ComposeUmbracoControllers(GetType().Assembly)
|
||||
.SetDefaultRenderMvcController<RenderMvcController>(); // default controller for template views
|
||||
|
||||
composition.WithCollectionBuilder<SearchableTreeCollectionBuilder>()
|
||||
.Add(() => composition.TypeLoader.GetTypes<ISearchableTree>()); // fixme which searchable trees?!
|
||||
@@ -133,9 +137,6 @@ namespace Umbraco.Web.Runtime
|
||||
|
||||
composition.RegisterUnique<UmbracoFeatures>();
|
||||
|
||||
// set the default RenderMvcController
|
||||
Current.DefaultRenderMvcControllerType = typeof(RenderMvcController); // fixme WRONG!
|
||||
|
||||
composition.WithCollectionBuilder<ActionCollectionBuilder>()
|
||||
.Add(() => composition.TypeLoader.GetTypes<IAction>());
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -193,7 +193,7 @@
|
||||
<Compile Include="Models\ContentEditing\IContentSave.cs" />
|
||||
<Compile Include="WebApi\TrimModelBinder.cs" />
|
||||
<Compile Include="Editors\CodeFileController.cs" />
|
||||
<Compile Include="Editors\DashboardHelper.cs" />
|
||||
<Compile Include="Editors\Dashboards.cs" />
|
||||
<Compile Include="Editors\DictionaryController.cs" />
|
||||
<Compile Include="Editors\EditorModelEventArgs.cs" />
|
||||
<Compile Include="Editors\EditorValidatorCollection.cs" />
|
||||
|
||||
Reference in New Issue
Block a user