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:
Lars-Erik Aabech
2018-12-05 23:02:59 +01:00
88 changed files with 6922 additions and 9899 deletions

View File

@@ -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>

View File

@@ -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

View File

@@ -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?

View File

@@ -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;
}
}

View File

@@ -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; }
}
}

View 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; }
}
}

View 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
}
}

View File

@@ -1,9 +0,0 @@
namespace Umbraco.Core.Configuration.Dashboard
{
public enum AccessType
{
Grant,
Deny,
GrantBySection
}
}

View File

@@ -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;
}
}

View File

@@ -4,6 +4,6 @@ namespace Umbraco.Core.Configuration.Dashboard
{
public interface IAccess
{
IEnumerable<IAccessItem> Rules { get; }
IEnumerable<IAccessRule> Rules { get; }
}
}

View File

@@ -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; }
}
}

View 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; }
}
}

View File

@@ -2,10 +2,6 @@
{
public interface IDashboardControl
{
bool ShowOnce { get; }
bool AddPanel { get; }
string PanelCaption { get; }
string ControlPath { get; }

View 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>();
}
}
}

View 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>();
}
}

View File

@@ -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);
}
}
}

View File

@@ -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>();
}
}

View File

@@ -3,6 +3,7 @@ using Umbraco.Core.Models.Membership;
namespace Umbraco.Core.Models.ContentEditing
{
/// <summary>
/// Represents a content app definition.
/// </summary>

View File

@@ -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" />

View File

@@ -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>

View File

@@ -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);
}
}

View File

@@ -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]);
}
}
}

View File

@@ -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

View File

@@ -1,180 +0,0 @@
/*!
* jQuery UI Touch Punch 0.2.3
*
* Copyright 20112014, 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

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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"
}
}

View File

@@ -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
}

View File

@@ -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) {

View File

@@ -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 = {

View File

@@ -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);
});
}

View File

@@ -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);

View File

@@ -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");
});
}
});

View File

@@ -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);

View File

@@ -25,7 +25,7 @@ angular.module("umbraco.directives")
});
}
$(element).click(function (event) {
$(element).on("click", function (event) {
if (event.metaKey || event.ctrlKey) {
return;
}

View File

@@ -16,7 +16,7 @@ angular.module("umbraco.directives")
});
}
$(element).keypress(function (event) {
$(element).on("keypress", function (event) {
if (event.which === 13) {
event.preventDefault();
}

View File

@@ -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);
});

View File

@@ -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");
}
};

View File

@@ -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

View File

@@ -67,7 +67,7 @@ angular.module("umbraco.directives")
$timeout(function () {
if (scope.value === null) {
editor.focus();
editor.trigger("focus");
}
}, 400);

View File

@@ -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();
});

View File

@@ -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);

View File

@@ -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');
});
}

View File

@@ -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;

View File

@@ -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 });

View File

@@ -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");
}
}

View File

@@ -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]();
}

View File

@@ -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');
});
},

View File

@@ -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);

View File

@@ -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");

View File

@@ -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;
}
},

View File

@@ -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!

View File

@@ -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
*/

View File

@@ -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";

View File

@@ -0,0 +1,7 @@
/*
WORD BREAK
*/
.word-normal { word-break: normal; }
.word-wrap { word-break: break-all; }
.word-nowrap { word-break: keep-all; }

View File

@@ -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>

View File

@@ -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) {

View File

@@ -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
};

View File

@@ -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>

View File

@@ -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;
}
});

View File

@@ -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}">

View File

@@ -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>

View File

@@ -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" />

View File

@@ -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);
}

View File

@@ -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);
}
}

View File

@@ -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() {

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>"));
}
}
}
}
}

View File

@@ -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;
}
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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
}
}

View File

@@ -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);
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}
}

View 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;
}
}
}

View File

@@ -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;

View File

@@ -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; }

View File

@@ -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>());

View File

@@ -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',

View File

@@ -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" />