From 0ce059d5bd8483b34cafdd67090fe05b285b6490 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 2 Jan 2018 17:28:16 +1100 Subject: [PATCH 01/31] U4-10789 Headless User Editor extensibility --- .../src/views/users/user.controller.js | 2 +- .../src/views/users/user.html | 469 ++---------------- .../src/views/users/views/user/details.html | 361 ++++++++++++++ .../Editors/EditorModelEventArgs.cs | 33 ++ .../Editors/EditorModelEventManager.cs | 43 +- src/Umbraco.Web/Editors/UsersController.cs | 1 + .../Models/ContentEditing/EditorNavigation.cs | 26 + .../Models/ContentEditing/UserDetail.cs | 10 +- .../Models/ContentEditing/UserDisplay.cs | 7 +- .../Models/Mapping/UserModelMapper.cs | 26 +- src/Umbraco.Web/Umbraco.Web.csproj | 2 + 11 files changed, 512 insertions(+), 468 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/views/users/views/user/details.html create mode 100644 src/Umbraco.Web/Editors/EditorModelEventArgs.cs create mode 100644 src/Umbraco.Web/Models/ContentEditing/EditorNavigation.cs diff --git a/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js b/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js index fb689e37c3..f2376e3acc 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js @@ -16,7 +16,7 @@ vm.maxFileSize = Umbraco.Sys.ServerVariables.umbracoSettings.maxFileSize + "KB"; vm.acceptedFileTypes = mediaHelper.formatFileTypes(Umbraco.Sys.ServerVariables.umbracoSettings.imageFileTypes); vm.usernameIsEmail = Umbraco.Sys.ServerVariables.umbracoSettings.usernameIsEmail; - + //create the initial model for change password vm.changePasswordModel = { config: {}, diff --git a/src/Umbraco.Web.UI.Client/src/views/users/user.html b/src/Umbraco.Web.UI.Client/src/views/users/user.html index 7243586bd3..32c1c3d641 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/user.html +++ b/src/Umbraco.Web.UI.Client/src/views/users/user.html @@ -1,406 +1,27 @@
+ +
- + - + - - - - -
-
+
-
- - - - - - - - - - - Required - - - - - - Required - - - - - - Required - - - - - - - - - - - - - - - - Add - - - - - - - - - - - - - - Add - - - - - - - - - - - - - - Add - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -
- -
- - -
- - - - - - - - - - - - -
- - -
- -
- - -
- -
- - -
- -
- - -
- - - - - - - - - - - - - - -
-


Password reset to value: {{vm.user.resetPasswordValue}}

-
- -
- - -
-
- Status: -
-
- - {{vm.user.userDisplayState.name}} - -
-
- -
-
- Last login: -
-
- {{ vm.user.formattedLastLogin }} - {{ vm.user.name | umbWordLimit:1 }} has not logged in yet -
-
- -
-
- Failed login attempts: -
-
- {{ vm.user.failedPasswordAttempts }} -
-
- -
-
- Last lockout date: -
-
- - {{ vm.user.name | umbWordLimit:1 }} hasn't been locked out - - {{ vm.user.formattedLastLockoutDate }} -
-
- -
-
- Password is last changed: -
-
- - The password hasn't been changed - - {{ vm.user.formattedLastPasswordChangeDate }} -
-
- -
-
- User is created: -
-
- {{ vm.user.formattedCreateDate }} -
-
- -
-
- User is last updated: -
-
- {{ vm.user.formattedUpdateDate }} -
-
- -
- -
- -
+ +
@@ -410,33 +31,30 @@ - + - + - + @@ -447,25 +65,22 @@ - + - + - +
\ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/users/views/user/details.html b/src/Umbraco.Web.UI.Client/src/views/users/views/user/details.html new file mode 100644 index 0000000000..4f03b755f5 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/users/views/user/details.html @@ -0,0 +1,361 @@ +
+ +
+ + + + + + + + + + + Required + + + + + + Required + + + + + + Required + + + + + + + + + + + + + + + + Add + + + + + + + + + + + + + + Add + + + + + + + + + + + + + + Add + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+ + +
+ + + + + + + + + + + + +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + + + + + + + + + + + + + +
+


Password reset to value: {{model.user.resetPasswordValue}}

+
+ +
+ + +
+
+ Status: +
+
+ + {{model.user.userDisplayState.name}} + +
+
+ +
+
+ Last login: +
+
+ {{ model.user.formattedLastLogin }} + {{ model.user.name | umbWordLimit:1 }} has not logged in yet +
+
+ +
+
+ Failed login attempts: +
+
+ {{ model.user.failedPasswordAttempts }} +
+
+ +
+
+ Last lockout date: +
+
+ + {{ model.user.name | umbWordLimit:1 }} hasn't been locked out + + {{ model.user.formattedLastLockoutDate }} +
+
+ +
+
+ Password is last changed: +
+
+ + The password hasn't been changed + + {{ model.user.formattedLastPasswordChangeDate }} +
+
+ +
+
+ User is created: +
+
+ {{ model.user.formattedCreateDate }} +
+
+ +
+
+ User is last updated: +
+
+ {{ model.user.formattedUpdateDate }} +
+
+ +
+ +
+ +
\ No newline at end of file diff --git a/src/Umbraco.Web/Editors/EditorModelEventArgs.cs b/src/Umbraco.Web/Editors/EditorModelEventArgs.cs new file mode 100644 index 0000000000..153a2d8786 --- /dev/null +++ b/src/Umbraco.Web/Editors/EditorModelEventArgs.cs @@ -0,0 +1,33 @@ +using System; + +namespace Umbraco.Web.Editors +{ + public sealed class EditorModelEventArgs : EditorModelEventArgs + { + public EditorModelEventArgs(EditorModelEventArgs baseArgs) + : base(baseArgs.Model, baseArgs.UmbracoContext) + { + Model = (T)baseArgs.Model; + } + + public EditorModelEventArgs(T model, UmbracoContext umbracoContext) + : base(model, umbracoContext) + { + Model = model; + } + + public new T Model { get; private set; } + } + + public class EditorModelEventArgs : EventArgs + { + public EditorModelEventArgs(object model, UmbracoContext umbracoContext) + { + Model = model; + UmbracoContext = umbracoContext; + } + + public object Model { get; private set; } + public UmbracoContext UmbracoContext { get; private set; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Editors/EditorModelEventManager.cs b/src/Umbraco.Web/Editors/EditorModelEventManager.cs index 44454ca6c3..e204bdc044 100644 --- a/src/Umbraco.Web/Editors/EditorModelEventManager.cs +++ b/src/Umbraco.Web/Editors/EditorModelEventManager.cs @@ -1,39 +1,9 @@ -using System; using System.Web.Http.Filters; using Umbraco.Core.Events; using Umbraco.Web.Models.ContentEditing; namespace Umbraco.Web.Editors { - public class EditorModelEventArgs : EventArgs - { - public EditorModelEventArgs(object model, UmbracoContext umbracoContext) - { - Model = model; - UmbracoContext = umbracoContext; - } - - public object Model { get; private set; } - public UmbracoContext UmbracoContext { get; private set; } - } - - public sealed class EditorModelEventArgs : EditorModelEventArgs - { - public EditorModelEventArgs(EditorModelEventArgs baseArgs) - : base(baseArgs.Model, baseArgs.UmbracoContext) - { - Model = (T)baseArgs.Model; - } - - public EditorModelEventArgs(T model, UmbracoContext umbracoContext) - : base(model, umbracoContext) - { - Model = model; - } - - public new T Model { get; private set; } - } - /// /// Used to emit events for editor models in the back office /// @@ -42,6 +12,13 @@ namespace Umbraco.Web.Editors public static event TypedEventHandler> SendingContentModel; public static event TypedEventHandler> SendingMediaModel; public static event TypedEventHandler> SendingMemberModel; + public static event TypedEventHandler> SendingUserModel; + + private static void OnSendingUserModel(HttpActionExecutedContext sender, EditorModelEventArgs e) + { + var handler = SendingUserModel; + if (handler != null) handler(sender, e); + } private static void OnSendingContentModel(HttpActionExecutedContext sender, EditorModelEventArgs e) { @@ -85,6 +62,12 @@ namespace Umbraco.Web.Editors { OnSendingMemberModel(sender, new EditorModelEventArgs(e)); } + + var userDisplay = e.Model as UserDisplay; + if (userDisplay != null) + { + OnSendingUserModel(sender, new EditorModelEventArgs(e)); + } } } diff --git a/src/Umbraco.Web/Editors/UsersController.cs b/src/Umbraco.Web/Editors/UsersController.cs index 2a8e052f26..e4f79a21a6 100644 --- a/src/Umbraco.Web/Editors/UsersController.cs +++ b/src/Umbraco.Web/Editors/UsersController.cs @@ -182,6 +182,7 @@ namespace Umbraco.Web.Editors /// /// /// + [OutgoingEditorModelEvent] public UserDisplay GetById(int id) { var user = Services.UserService.GetUserById(id); diff --git a/src/Umbraco.Web/Models/ContentEditing/EditorNavigation.cs b/src/Umbraco.Web/Models/ContentEditing/EditorNavigation.cs new file mode 100644 index 0000000000..29922750cf --- /dev/null +++ b/src/Umbraco.Web/Models/ContentEditing/EditorNavigation.cs @@ -0,0 +1,26 @@ +using System.Runtime.Serialization; + +namespace Umbraco.Web.Models.ContentEditing +{ + /// + /// A model representing the navigation ("apps") inside an editor in the back office + /// + [DataContract(Name = "user", Namespace = "")] + public class EditorNavigation + { + [DataMember(Name = "name")] + public string Name { get; set; } + + [DataMember(Name = "alias")] + public string Alias { get; set; } + + [DataMember(Name = "icon")] + public string Icon { get; set; } + + [DataMember(Name = "view")] + public string View { get; set; } + + [DataMember(Name = "active")] + public bool Active { get; set; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Models/ContentEditing/UserDetail.cs b/src/Umbraco.Web/Models/ContentEditing/UserDetail.cs index 75e1462ec3..5acad7ea49 100644 --- a/src/Umbraco.Web/Models/ContentEditing/UserDetail.cs +++ b/src/Umbraco.Web/Models/ContentEditing/UserDetail.cs @@ -5,7 +5,7 @@ using System.ComponentModel.DataAnnotations; using System.Runtime.Serialization; namespace Umbraco.Web.Models.ContentEditing -{ +{ /// /// Represents information for the current user /// @@ -29,7 +29,7 @@ namespace Umbraco.Web.Models.ContentEditing [Obsolete("This should not be used it exists for legacy reasons only, use user groups instead, it will be removed in future versions")] [EditorBrowsable(EditorBrowsableState.Never)] [ReadOnly(true)] - [DataMember(Name = "userType")] + [DataMember(Name = "userType")] public string UserType { get; set; } [ReadOnly(true)] @@ -64,8 +64,8 @@ namespace Umbraco.Web.Models.ContentEditing /// A list of sections the user is allowed to view. /// [DataMember(Name = "allowedSections")] - public IEnumerable AllowedSections { get; set; } - - + public IEnumerable AllowedSections { get; set; } + + } } \ No newline at end of file diff --git a/src/Umbraco.Web/Models/ContentEditing/UserDisplay.cs b/src/Umbraco.Web/Models/ContentEditing/UserDisplay.cs index 8a79344c8e..1464d9580f 100644 --- a/src/Umbraco.Web/Models/ContentEditing/UserDisplay.cs +++ b/src/Umbraco.Web/Models/ContentEditing/UserDisplay.cs @@ -18,8 +18,13 @@ namespace Umbraco.Web.Models.ContentEditing AvailableCultures = new Dictionary(); StartContentIds = new List(); StartMediaIds = new List(); + Navigation = new List(); } - + + [DataMember(Name = "navigation")] + [ReadOnly(true)] + public IEnumerable Navigation { get; set; } + /// /// Gets the available cultures (i.e. to populate a drop down) /// The key is the culture stored in the database, the value is the Name diff --git a/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs b/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs index 80990fed5b..9262e7eebc 100644 --- a/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Linq.Expressions; using AutoMapper; using Umbraco.Core; using Umbraco.Core.Models.Mapping; @@ -231,12 +232,14 @@ namespace Umbraco.Web.Models.Mapping }); //Important! Currently we are never mapping to multiple UserDisplay objects but if we start doing that - // this will cause an N+1 and we'll need to change how this works. + // this will cause an N+1 and we'll need to change how this works. + config.CreateMap() .ForMember(detail => detail.Avatars, opt => opt.MapFrom(user => user.GetUserAvatarUrls(applicationContext.ApplicationCache.RuntimeCache))) .ForMember(detail => detail.Username, opt => opt.MapFrom(user => user.Username)) .ForMember(detail => detail.LastLoginDate, opt => opt.MapFrom(user => user.LastLoginDate == default(DateTime) ? null : (DateTime?) user.LastLoginDate)) .ForMember(detail => detail.UserGroups, opt => opt.MapFrom(user => user.Groups)) + .ForMember(detail => detail.Navigation, opt => opt.MapFrom(user => CreateUserEditorNavigation(applicationContext.Services.TextService))) .ForMember( detail => detail.CalculatedStartContentIds, opt => opt.MapFrom(user => GetStartNodeValues( @@ -255,7 +258,7 @@ namespace Umbraco.Web.Models.Mapping "media/mediaRoot"))) .ForMember( detail => detail.StartContentIds, - opt => opt.MapFrom(user => GetStartNodeValues( + opt => opt.MapFrom(user => GetStartNodeValues( user.StartContentIds.ToArray(), applicationContext.Services.TextService, applicationContext.Services.EntityService, @@ -263,7 +266,7 @@ namespace Umbraco.Web.Models.Mapping "content/contentRoot"))) .ForMember( detail => detail.StartMediaIds, - opt => opt.MapFrom(user => GetStartNodeValues( + opt => opt.MapFrom(user => GetStartNodeValues( user.StartMediaIds.ToArray(), applicationContext.Services.TextService, applicationContext.Services.EntityService, @@ -330,7 +333,7 @@ namespace Umbraco.Web.Models.Mapping //the best we can do here is to return the user's first user group as a IUserType object //but we should attempt to return any group that is the built in ones first var groups = user.Groups.ToArray(); - detail.UserGroups = user.Groups.Select(x => x.Alias).ToArray(); + detail.UserGroups = user.Groups.Select(x => x.Alias).ToArray(); if (groups.Length == 0) { @@ -358,6 +361,21 @@ namespace Umbraco.Web.Models.Mapping .ForMember(detail => detail.UserId, opt => opt.MapFrom(profile => GetIntId(profile.Id))); } + private IEnumerable CreateUserEditorNavigation(ILocalizedTextService textService) + { + return new[] + { + new EditorNavigation + { + Active = true, + Alias = "details", + Icon = "icon-umb-users", + Name = textService.Localize("general/user"), + View = "views/users/views/user/details.html" + } + }; + } + private IEnumerable GetStartNodeValues(int[] startNodeIds, ILocalizedTextService textService, IEntityService entityService, UmbracoObjectTypes objectType, string localizedKey) diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 326ed3baa9..5461069713 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -318,6 +318,7 @@ + @@ -374,6 +375,7 @@ + From bd9adffaa55706cffcedb6dd1a539f62695d81e0 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 3 Jan 2018 13:33:33 +1100 Subject: [PATCH 02/31] Converts macro tree to use a proper controller so we can disable it --- src/Umbraco.Core/Constants-Applications.cs | 5 ++ src/Umbraco.Web.UI/config/trees.config | 6 +- src/Umbraco.Web/Trees/MacroTreeController.cs | 67 +++++++++++++++++++ src/Umbraco.Web/Trees/ScriptTreeController.cs | 2 +- src/Umbraco.Web/Umbraco.Web.csproj | 1 + .../umbraco/Trees/loadMacros.cs | 7 +- .../developer/Macros/editMacro.aspx.cs | 8 +-- 7 files changed, 84 insertions(+), 12 deletions(-) create mode 100644 src/Umbraco.Web/Trees/MacroTreeController.cs diff --git a/src/Umbraco.Core/Constants-Applications.cs b/src/Umbraco.Core/Constants-Applications.cs index 3911a6d204..1a53df8b09 100644 --- a/src/Umbraco.Core/Constants-Applications.cs +++ b/src/Umbraco.Core/Constants-Applications.cs @@ -73,6 +73,11 @@ /// public const string Media = "media"; + /// + /// alias for the macro tree. + /// + public const string Macros = "macros"; + /// /// alias for the datatype tree. /// diff --git a/src/Umbraco.Web.UI/config/trees.config b/src/Umbraco.Web.UI/config/trees.config index 5ba3254549..dcd4eccbfb 100644 --- a/src/Umbraco.Web.UI/config/trees.config +++ b/src/Umbraco.Web.UI/config/trees.config @@ -20,11 +20,11 @@ - + - + @@ -39,5 +39,5 @@ - + \ No newline at end of file diff --git a/src/Umbraco.Web/Trees/MacroTreeController.cs b/src/Umbraco.Web/Trees/MacroTreeController.cs new file mode 100644 index 0000000000..5026aec4c8 --- /dev/null +++ b/src/Umbraco.Web/Trees/MacroTreeController.cs @@ -0,0 +1,67 @@ +using System; +using System.Net.Http.Formatting; +using umbraco; +using Umbraco.Core; +using Umbraco.Core.Models; +using Umbraco.Web.Models.Trees; +using Umbraco.Web.Mvc; +using System.Linq; +using umbraco.BusinessLogic.Actions; +using Umbraco.Web.WebApi.Filters; +using Umbraco.Core.Services; +using Constants = Umbraco.Core.Constants; + +namespace Umbraco.Web.Trees +{ + [UmbracoTreeAuthorize(Constants.Trees.Macros)] + [Tree(Constants.Applications.Developer, Constants.Trees.Macros, null, sortOrder: 2)] + [LegacyBaseTree(typeof(loadMacros))] + [PluginController("UmbracoTrees")] + [CoreTree] + public class MacroTreeController : TreeController + { + protected override TreeNodeCollection GetTreeNodes(string id, FormDataCollection queryStrings) + { + var intId = id.TryConvertTo(); + if (intId == false) throw new InvalidOperationException("Id must be an integer"); + + var nodes = new TreeNodeCollection(); + + nodes.AddRange( + Services.MacroService.GetAll() + .OrderBy(entity => entity.Name) + .Select(macro => + { + var node = CreateTreeNode(macro.Id.ToInvariantString(), id, queryStrings, macro.Name, "icon-settings-alt", false); + node.Path = "-1," + macro.Id; + node.AssignLegacyJsCallback("javascript:UmbClientMgr.contentFrame('developer/macros/editMacro.aspx?macroID=" + macro.Id + "');"); + return node; + })); + + return nodes; + } + + protected override MenuItemCollection GetMenuForNode(string id, FormDataCollection queryStrings) + { + var menu = new MenuItemCollection(); + + if (id == Constants.System.Root.ToInvariantString()) + { + //set the default to create + menu.DefaultMenuAlias = ActionNew.Instance.Alias; + + // root actions + menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionNew.Instance.Alias))) + .ConvertLegacyMenuItem(null, Constants.Trees.Macros, queryStrings.GetValue("application")); + + //menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionNew.Instance.Alias))); + + menu.Items.Add(ui.Text("actions", ActionRefresh.Instance.Alias), true); + return menu; + } + + menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionDelete.Instance.Alias))); + return menu; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Trees/ScriptTreeController.cs b/src/Umbraco.Web/Trees/ScriptTreeController.cs index 171688bd1a..c7a3dd5f04 100644 --- a/src/Umbraco.Web/Trees/ScriptTreeController.cs +++ b/src/Umbraco.Web/Trees/ScriptTreeController.cs @@ -8,7 +8,7 @@ using Constants = Umbraco.Core.Constants; namespace Umbraco.Web.Trees { [UmbracoTreeAuthorize(Constants.Trees.Scripts)] - [Tree(Constants.Applications.Settings, "scripts", null, sortOrder: 4)] + [Tree(Constants.Applications.Settings, Constants.Trees.Scripts, null, sortOrder: 4)] [LegacyBaseTree(typeof(loadScripts))] [PluginController("UmbracoTrees")] [CoreTree] diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 326ed3baa9..9677a9720f 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -493,6 +493,7 @@ + diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/Trees/loadMacros.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/Trees/loadMacros.cs index 18754f6c75..7e61810078 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/Trees/loadMacros.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/Trees/loadMacros.cs @@ -1,6 +1,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.ComponentModel; using System.Data; using System.IO; using System.Text; @@ -29,10 +30,8 @@ using Umbraco.Core; namespace umbraco { - /// - /// Handles loading of the cache application into the developer application tree - /// - [Tree(Constants.Applications.Developer, "macros", "Macros", sortOrder: 2)] + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("This is no longer used and will be removed from the codebase in the future")] public class loadMacros : BaseTree { diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/developer/Macros/editMacro.aspx.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/developer/Macros/editMacro.aspx.cs index 73800ca117..2939bd0b31 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/developer/Macros/editMacro.aspx.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/developer/Macros/editMacro.aspx.cs @@ -42,8 +42,8 @@ namespace umbraco.cms.presentation.developer if (IsPostBack == false) { ClientTools - .SetActiveTreeType(TreeDefinitionCollection.Instance.FindTree().Tree.Alias) - .SyncTree("-1,init," + _macro.Id, false); + .SetActiveTreeType(Constants.Trees.Macros) + .SyncTree("-1," + _macro.Id, false); string tempMacroAssembly = _macro.ControlAssembly ?? ""; string tempMacroType = _macro.ControlType ?? ""; @@ -321,8 +321,8 @@ namespace umbraco.cms.presentation.developer Page.Validate(); ClientTools - .SetActiveTreeType(TreeDefinitionCollection.Instance.FindTree().Tree.Alias) - .SyncTree("-1,init," + _macro.Id.ToInvariantString(), true); //true forces the reload + .SetActiveTreeType(Constants.Trees.Macros) + .SyncTree("-1," + _macro.Id.ToInvariantString(), true); //true forces the reload var tempMacroAssembly = macroAssembly.Text; var tempMacroType = macroType.Text; From 88c20c7c5fbfcbfca632f32f8d9c45a2156abb51 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 3 Jan 2018 13:49:36 +1100 Subject: [PATCH 03/31] Allows for disabling of any umbraco based Controller on startup, this also takes into account disabled trees so if they are disabled, they will not be shown --- src/Umbraco.Web/Features/DisabledFeatures.cs | 18 ++++++++++++ src/Umbraco.Web/Features/FeaturesResolver.cs | 28 +++++++++++++++++++ src/Umbraco.Web/Features/TypeList.cs | 25 +++++++++++++++++ src/Umbraco.Web/Features/UmbracoFeatures.cs | 17 +++++++++++ .../Trees/ApplicationTreeController.cs | 17 ++++++++--- src/Umbraco.Web/Umbraco.Web.csproj | 5 ++++ .../Filters/FeaturesAuthorizeAttribute.cs | 18 ++++++++++++ .../WebApi/UmbracoApiControllerBase.cs | 4 ++- src/Umbraco.Web/WebBootManager.cs | 3 ++ 9 files changed, 130 insertions(+), 5 deletions(-) create mode 100644 src/Umbraco.Web/Features/DisabledFeatures.cs create mode 100644 src/Umbraco.Web/Features/FeaturesResolver.cs create mode 100644 src/Umbraco.Web/Features/TypeList.cs create mode 100644 src/Umbraco.Web/Features/UmbracoFeatures.cs create mode 100644 src/Umbraco.Web/WebApi/Filters/FeaturesAuthorizeAttribute.cs diff --git a/src/Umbraco.Web/Features/DisabledFeatures.cs b/src/Umbraco.Web/Features/DisabledFeatures.cs new file mode 100644 index 0000000000..85d8e12b38 --- /dev/null +++ b/src/Umbraco.Web/Features/DisabledFeatures.cs @@ -0,0 +1,18 @@ +using Umbraco.Web.Trees; +using Umbraco.Web.WebApi; + +namespace Umbraco.Web.Features +{ + /// + /// Represents Umbraco features that can be disabled + /// + public class DisabledFeatures + { + public DisabledFeatures() + { + Controllers = new TypeList(); + } + + public TypeList Controllers { get; private set; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Features/FeaturesResolver.cs b/src/Umbraco.Web/Features/FeaturesResolver.cs new file mode 100644 index 0000000000..3b7b0b3b36 --- /dev/null +++ b/src/Umbraco.Web/Features/FeaturesResolver.cs @@ -0,0 +1,28 @@ +using Umbraco.Core.ObjectResolution; + +namespace Umbraco.Web.Features +{ + public class FeaturesResolver : SingleObjectResolverBase + { + public FeaturesResolver(UmbracoFeatures value) : base(value) + { + } + /// + /// Sets the disabled features + /// + /// + /// For developers, at application startup. + public void SetFeatures(UmbracoFeatures finder) + { + Value = finder; + } + + /// + /// Gets the features + /// + public UmbracoFeatures Features + { + get { return Value; } + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Features/TypeList.cs b/src/Umbraco.Web/Features/TypeList.cs new file mode 100644 index 0000000000..86b24533ac --- /dev/null +++ b/src/Umbraco.Web/Features/TypeList.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; + +namespace Umbraco.Web.Features +{ + /// + /// Maintains a list of strongly typed types + /// + /// + public class TypeList + { + private readonly List _disabled = new List(); + + public void Add() + where TType : T + { + _disabled.Add(typeof(TType)); + } + + public bool Contains(Type type) + { + return _disabled.Contains(type); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Features/UmbracoFeatures.cs b/src/Umbraco.Web/Features/UmbracoFeatures.cs new file mode 100644 index 0000000000..5d4570c4f2 --- /dev/null +++ b/src/Umbraco.Web/Features/UmbracoFeatures.cs @@ -0,0 +1,17 @@ +namespace Umbraco.Web.Features +{ + /// + /// Represents Umbraco features that can be toggled + /// + public class UmbracoFeatures + { + public UmbracoFeatures() + { + DisabledFeatures = new DisabledFeatures(); + } + + public DisabledFeatures DisabledFeatures { get; set; } + + //NOTE: Currently we can only Disable features but maybe some day we could enable non standard features too + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Trees/ApplicationTreeController.cs b/src/Umbraco.Web/Trees/ApplicationTreeController.cs index 89a615f353..37d880f262 100644 --- a/src/Umbraco.Web/Trees/ApplicationTreeController.cs +++ b/src/Umbraco.Web/Trees/ApplicationTreeController.cs @@ -84,10 +84,19 @@ namespace Umbraco.Web.Trees private async Task GetRootForMultipleAppTree(ApplicationTree configTree, FormDataCollection queryStrings) { if (configTree == null) throw new ArgumentNullException("configTree"); - var byControllerAttempt = await configTree.TryGetRootNodeFromControllerTree(queryStrings, ControllerContext); - if (byControllerAttempt.Success) - { - return byControllerAttempt.Result; + try + { + var byControllerAttempt = await configTree.TryGetRootNodeFromControllerTree(queryStrings, ControllerContext); + if (byControllerAttempt.Success) + { + return byControllerAttempt.Result; + } + } + catch (HttpResponseException) + { + //if this occurs its because the user isn't authorized to view that tree, in this case since we are loading multiple trees we + //will just return null so that it's not added to the list. + return null; } var legacyAttempt = configTree.TryGetRootNodeFromLegacyTree(queryStrings, Url, configTree.ApplicationAlias); diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 9677a9720f..82df4332e5 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -315,6 +315,7 @@ + @@ -335,6 +336,9 @@ + + + @@ -792,6 +796,7 @@ + diff --git a/src/Umbraco.Web/WebApi/Filters/FeaturesAuthorizeAttribute.cs b/src/Umbraco.Web/WebApi/Filters/FeaturesAuthorizeAttribute.cs new file mode 100644 index 0000000000..4148c61a03 --- /dev/null +++ b/src/Umbraco.Web/WebApi/Filters/FeaturesAuthorizeAttribute.cs @@ -0,0 +1,18 @@ +using System.Web.Http; +using System.Web.Http.Controllers; +using Umbraco.Web.Features; + +namespace Umbraco.Web.WebApi.Filters +{ + /// + /// Will return unauthorized for the controller if it's been globally disabled + /// + public sealed class FeaturesAuthorizeAttribute : AuthorizeAttribute + { + protected override bool IsAuthorized(HttpActionContext actionContext) + { + var controllerType = actionContext.ControllerContext.ControllerDescriptor.ControllerType; + return FeaturesResolver.Current.Features.DisabledFeatures.Controllers.Contains(controllerType) == false; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/WebApi/UmbracoApiControllerBase.cs b/src/Umbraco.Web/WebApi/UmbracoApiControllerBase.cs index f82d73edf7..c39ee9fce3 100644 --- a/src/Umbraco.Web/WebApi/UmbracoApiControllerBase.cs +++ b/src/Umbraco.Web/WebApi/UmbracoApiControllerBase.cs @@ -6,12 +6,14 @@ using Umbraco.Core; using Umbraco.Core.Logging; using Umbraco.Core.Services; using Umbraco.Web.Security; +using Umbraco.Web.WebApi.Filters; namespace Umbraco.Web.WebApi { /// /// The base class for API controllers that expose Umbraco services - THESE ARE NOT AUTO ROUTED /// + [FeaturesAuthorize] public abstract class UmbracoApiControllerBase : ApiController { protected UmbracoApiControllerBase() @@ -35,7 +37,7 @@ namespace Umbraco.Web.WebApi InstanceId = Guid.NewGuid(); _umbraco = umbracoHelper; } - + private UmbracoHelper _umbraco; /// diff --git a/src/Umbraco.Web/WebBootManager.cs b/src/Umbraco.Web/WebBootManager.cs index c38540b6a2..2bb21bc848 100644 --- a/src/Umbraco.Web/WebBootManager.cs +++ b/src/Umbraco.Web/WebBootManager.cs @@ -38,6 +38,7 @@ using Umbraco.Core.Persistence.UnitOfWork; using Umbraco.Core.Scoping; using Umbraco.Core.Services; using Umbraco.Web.Editors; +using Umbraco.Web.Features; using Umbraco.Web.HealthCheck; using Umbraco.Web.Profiling; using Umbraco.Web.Search; @@ -352,6 +353,8 @@ namespace Umbraco.Web protected override void InitializeResolvers() { base.InitializeResolvers(); + + FeaturesResolver.Current = new FeaturesResolver(new UmbracoFeatures()); SearchableTreeResolver.Current = new SearchableTreeResolver(ServiceProvider, LoggerResolver.Current.Logger, ApplicationContext.Services.ApplicationTreeService, () => PluginManager.ResolveSearchableTrees()); From 1dc88658e8966b0ca3f0d4c636f96b5854a07f95 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 4 Jan 2018 18:32:25 +1100 Subject: [PATCH 04/31] Updates how the user.controller saves a user and instead of using the contentEditingHelper we just call the code ourselves which is actually much simpler. Then once the user is saved we call any extendedSave methods that have been registered. --- src/SolutionInfo.cs | 2 +- .../src/views/users/user.controller.js | 98 +++++++++++++------ src/Umbraco.Web/Editors/UsersController.cs | 1 + 3 files changed, 70 insertions(+), 31 deletions(-) diff --git a/src/SolutionInfo.cs b/src/SolutionInfo.cs index a3d52dd9e8..f6ca8ba95f 100644 --- a/src/SolutionInfo.cs +++ b/src/SolutionInfo.cs @@ -12,4 +12,4 @@ using System.Resources; [assembly: AssemblyVersion("1.0.*")] [assembly: AssemblyFileVersion("7.8.0")] -[assembly: AssemblyInformationalVersion("7.8.0-beta")] \ No newline at end of file +[assembly: AssemblyInformationalVersion("7.8.0-beta001")] \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js b/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js index f2376e3acc..dfd809431f 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js @@ -1,7 +1,7 @@ (function () { "use strict"; - function UserEditController($scope, $timeout, $location, $routeParams, formHelper, usersResource, userService, contentEditingHelper, localizationService, notificationsService, mediaHelper, Upload, umbRequestHelper, usersHelper, authResource, dateHelper) { + function UserEditController($scope, $q, $timeout, $location, $routeParams, formHelper, usersResource, userService, contentEditingHelper, localizationService, notificationsService, mediaHelper, Upload, umbRequestHelper, usersHelper, authResource, dateHelper) { var vm = this; @@ -33,6 +33,9 @@ vm.unlockUser = unlockUser; vm.clearAvatar = clearAvatar; vm.save = save; + //a list of methods invoked with promises during the save operation, each must have a key + vm.extendedSaveMethods = {}; + vm.toggleChangePassword = toggleChangePassword; function init() { @@ -117,38 +120,73 @@ function save() { - vm.page.saveButtonState = "busy"; - vm.user.resetPasswordValue = null; + if (formHelper.submitForm({ scope: $scope, statusMessage: vm.labels.saving })) { - //anytime a user is changing another user's password, we are in effect resetting it so we need to set that flag here - if(vm.user.changePassword) { - vm.user.changePassword.reset = !vm.user.changePassword.oldPassword && !vm.user.isCurrentUser; + //anytime a user is changing another user's password, we are in effect resetting it so we need to set that flag here + if (vm.user.changePassword) { + vm.user.changePassword.reset = !vm.user.changePassword.oldPassword && !vm.user.isCurrentUser; + } + + vm.page.saveButtonState = "busy"; + vm.user.resetPasswordValue = null; + + usersResource.saveUser(vm.user) + .then(function (saved) { + + //if the user saved, then try to execute all extended save options + extendedSave().then(function(result) { + //if all is good, then reset the form + formHelper.resetForm({ scope: $scope, notifications: saved.notifications }); + }, function(err) { + //otherwise show the notifications for the user being saved + formHelper.showNotifications(saved); + }); + + vm.user = saved; + setUserDisplayState(); + formatDatesToLocal(vm.user); + + vm.changePasswordModel.isChanging = false; + //the user has a password if they are not states: Invited, NoCredentials + vm.changePasswordModel.config.hasPassword = vm.user.userState !== 3 && vm.user.userState !== 4; + + vm.page.saveButtonState = "success"; + + }, function (err) { + + contentEditingHelper.handleSaveError({ + redirectOnFailure: false, + err: err + }); + //show any notifications + if (err.data) { + formHelper.showNotifications(err.data); + } + vm.page.saveButtonState = "error"; + }); } - - contentEditingHelper.contentEditorPerformSave({ - statusMessage: vm.labels.saving, - saveMethod: usersResource.saveUser, - scope: $scope, - content: vm.user, - // We do not redirect on failure for users - this is because it is not possible to actually save a user - // when server side validation fails - as opposed to content where we are capable of saving the content - // item if server side validation fails - redirectOnFailure: false, - rebindCallback: function (orignal, saved) { } - }).then(function (saved) { - - vm.user = saved; - setUserDisplayState(); - formatDatesToLocal(vm.user); - - vm.changePasswordModel.isChanging = false; - vm.page.saveButtonState = "success"; - - //the user has a password if they are not states: Invited, NoCredentials - vm.changePasswordModel.config.hasPassword = vm.user.userState !== 3 && vm.user.userState !== 4; - }, function (err) { - vm.page.saveButtonState = "error"; + } + + function extendedSave() { + //create a promise for each save method + var savePromises = {}; + angular.forEach(vm.extendedSaveMethods, function (val, key) { + var deferred = $q.defer(); + savePromises[key] = deferred; }); + var allPromises = _.map(savePromises, function (p) { + return p.promise; + }) + + //await all promises to complete + var resultPromise = $q.all(allPromises); + + //execute all promises by passing them to the save methods + angular.forEach(vm.extendedSaveMethods, function (func, key) { + func(savePromises[key]); + }); + + return resultPromise; } function goToPage(ancestor) { diff --git a/src/Umbraco.Web/Editors/UsersController.cs b/src/Umbraco.Web/Editors/UsersController.cs index e4f79a21a6..2e6cd5dbbf 100644 --- a/src/Umbraco.Web/Editors/UsersController.cs +++ b/src/Umbraco.Web/Editors/UsersController.cs @@ -495,6 +495,7 @@ namespace Umbraco.Web.Editors /// /// /// + [OutgoingEditorModelEvent] public async Task PostSaveUser(UserSave userSave) { if (userSave == null) throw new ArgumentNullException("userSave"); From c81e14758d1d8f43cd9718749f1724892da282b5 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 4 Jan 2018 22:57:38 +1100 Subject: [PATCH 05/31] make sure that the navigation index isn't reset --- .../src/views/users/user.controller.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js b/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js index dfd809431f..0b76d2df2e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js @@ -130,6 +130,9 @@ vm.page.saveButtonState = "busy"; vm.user.resetPasswordValue = null; + //save current nav to be restored later so that the tabs dont change + var currentNav = vm.user.navigation; + usersResource.saveUser(vm.user) .then(function (saved) { @@ -142,7 +145,9 @@ formHelper.showNotifications(saved); }); - vm.user = saved; + vm.user = _.omit(saved, "navigation"); + //restore + vm.user.navigation = currentNav; setUserDisplayState(); formatDatesToLocal(vm.user); From 0310782d7dd554bf9ef9a15eee6b724436af0450 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 5 Jan 2018 00:05:16 +1100 Subject: [PATCH 06/31] bumps version --- src/SolutionInfo.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SolutionInfo.cs b/src/SolutionInfo.cs index f6ca8ba95f..6162dd9db3 100644 --- a/src/SolutionInfo.cs +++ b/src/SolutionInfo.cs @@ -12,4 +12,4 @@ using System.Resources; [assembly: AssemblyVersion("1.0.*")] [assembly: AssemblyFileVersion("7.8.0")] -[assembly: AssemblyInformationalVersion("7.8.0-beta001")] \ No newline at end of file +[assembly: AssemblyInformationalVersion("7.8.0-beta002")] \ No newline at end of file From 8d4958e3954ccb8e11668580ddb4e25862555234 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 5 Jan 2018 10:22:46 +1100 Subject: [PATCH 07/31] Changes the user controller saving extensions operation to use events instead --- .../src/common/services/events.service.js | 40 +---------------- .../src/views/users/user.controller.js | 45 ++++++++++--------- 2 files changed, 27 insertions(+), 58 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/services/events.service.js b/src/Umbraco.Web.UI.Client/src/common/services/events.service.js index 174cc8abe2..e28970336f 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/events.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/events.service.js @@ -16,52 +16,16 @@ function eventsService($q, $rootScope) { return { - /** raise an event with a given name, returns an array of promises for each listener */ - emit: function (name, args) { + /** raise an event with a given name */ + emit: function (name, args) { //there are no listeners if (!$rootScope.$$listeners[name]) { return; - //return []; } //send the event $rootScope.$emit(name, args); - - - //PP: I've commented out the below, since we currently dont - // expose the eventsService as a documented api - // and think we need to figure out our usecases for this - // since the below modifies the return value of the then on() method - /* - //setup a deferred promise for each listener - var deferred = []; - for (var i = 0; i < $rootScope.$$listeners[name].length; i++) { - deferred.push($q.defer()); - }*/ - - //create a new event args object to pass to the - // $emit containing methods that will allow listeners - // to return data in an async if required - /* - var eventArgs = { - args: args, - reject: function (a) { - deferred.pop().reject(a); - }, - resolve: function (a) { - deferred.pop().resolve(a); - } - };*/ - - - - /* - //return an array of promises - var promises = _.map(deferred, function(p) { - return p.promise; - }); - return promises;*/ }, /** subscribe to a method, or use scope.$on = same thing */ diff --git a/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js b/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js index 0b76d2df2e..b3a58034f7 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js @@ -1,7 +1,7 @@ (function () { "use strict"; - function UserEditController($scope, $q, $timeout, $location, $routeParams, formHelper, usersResource, userService, contentEditingHelper, localizationService, notificationsService, mediaHelper, Upload, umbRequestHelper, usersHelper, authResource, dateHelper) { + function UserEditController($scope, eventsService, $q, $timeout, $location, $routeParams, formHelper, usersResource, userService, contentEditingHelper, localizationService, notificationsService, mediaHelper, Upload, umbRequestHelper, usersHelper, authResource, dateHelper) { var vm = this; @@ -137,7 +137,7 @@ .then(function (saved) { //if the user saved, then try to execute all extended save options - extendedSave().then(function(result) { + extendedSave(saved).then(function(result) { //if all is good, then reset the form formHelper.resetForm({ scope: $scope, notifications: saved.notifications }); }, function(err) { @@ -171,26 +171,31 @@ }); } } - - function extendedSave() { - //create a promise for each save method - var savePromises = {}; - angular.forEach(vm.extendedSaveMethods, function (val, key) { - var deferred = $q.defer(); - savePromises[key] = deferred; - }); - var allPromises = _.map(savePromises, function (p) { - return p.promise; - }) + /** + * Used to emit the save event and await any async operations being performed by editor extensions + * @param {any} savedUser + */ + function extendedSave(savedUser) { + + //used to track any promises added by the event handlers to be awaited + var promises = []; + + var args = { + //getPromise: getPromise, + user: savedUser, + //a promise can be added by the event handler if the handler needs an async operation to be awaited + addPromise: function (p) { + promises.push(p); + } + }; + + //emit the event + eventsService.emit("editors.user.editController.save", args); + //await all promises to complete - var resultPromise = $q.all(allPromises); - - //execute all promises by passing them to the save methods - angular.forEach(vm.extendedSaveMethods, function (func, key) { - func(savePromises[key]); - }); - + var resultPromise = $q.all(promises); + return resultPromise; } From b32e7d4cccf8eb13e417f4b08e511c5f4a54543c Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 5 Jan 2018 10:45:06 +1100 Subject: [PATCH 08/31] running the build changed the bower.json file, i wonder if this will 'fix' vsts? --- src/Umbraco.Web.UI.Client/bower.json | 27 +++++-------------- .../src/views/users/user.controller.js | 2 -- 2 files changed, 7 insertions(+), 22 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/bower.json b/src/Umbraco.Web.UI.Client/bower.json index 2991149c7e..f397c7f7ca 100644 --- a/src/Umbraco.Web.UI.Client/bower.json +++ b/src/Umbraco.Web.UI.Client/bower.json @@ -34,60 +34,47 @@ "font-awesome": "~4.2", "angular-moment": "^1.0.1" }, - "install": { - "path": "lib-bower", - "ignore": [ "font-awesome", "angular", "bootstrap", "codemirror" ], - "sources": { "moment": "bower_components/moment/min/moment-with-locales.js", - "underscore": [ "bower_components/underscore/underscore-min.js", "bower_components/underscore/underscore-min.map" ], - "jquery": [ "bower_components/jquery/dist/jquery.min.js", "bower_components/jquery/dist/jquery.min.map" ], - "angular-dynamic-locale": [ "bower_components/angular-dynamic-locale/tmhDynamicLocale.min.js", "bower_components/angular-dynamic-locale/tmhDynamicLocale.min.js.map" ], - "angular-local-storage": [ "bower_components/angular-local-storage/dist/angular-local-storage.min.js", "bower_components/angular-local-storage/dist/angular-local-storage.min.js.map" ], - "angular-moment": [ "bower_components/angular-moment/angular-moment.min.js" ], - "tinymce": [ "bower_components/tinymce/tinymce.min.js" ], - "typeahead.js": "bower_components/typeahead.js/dist/typeahead.bundle.min.js", - - "rgrove-lazyload":"bower_components/rgrove-lazyload/lazyload.js", - - "ng-file-upload":"bower_components/ng-file-upload/ng-file-upload.min.js", - - "jquery-ui":"bower_components/jquery-ui/jquery-ui.min.js", - - "jquery-migrate":"bower_components/jquery-migrate/jquery-migrate.min.js", - + "rgrove-lazyload": "bower_components/rgrove-lazyload/lazyload.js", + "ng-file-upload": "bower_components/ng-file-upload/ng-file-upload.min.js", + "jquery-ui": "bower_components/jquery-ui/jquery-ui.min.js", + "jquery-migrate": "bower_components/jquery-migrate/jquery-migrate.min.js", "clipboard": "bower_components/clipboard/dist/clipboard.min.js" } + }, + "resolutions": { + "moment": ">=2.19.3 <3.0.0" } } diff --git a/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js b/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js index b3a58034f7..5d1fa33a63 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js @@ -33,8 +33,6 @@ vm.unlockUser = unlockUser; vm.clearAvatar = clearAvatar; vm.save = save; - //a list of methods invoked with promises during the save operation, each must have a key - vm.extendedSaveMethods = {}; vm.toggleChangePassword = toggleChangePassword; From a9713dc6fcc3444de20afccf9b7f235640cfaa5e Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 5 Jan 2018 13:44:57 +1100 Subject: [PATCH 09/31] Converts the xslt tree to a controller, ensures all config for trees is correct and transforms for upgrades applied --- build/NuSpecs/tools/trees.config.install.xdt | 6 +- src/Umbraco.Core/Constants-Applications.cs | 2 +- .../config/trees.Release.config | 4 +- src/Umbraco.Web.UI/config/trees.config | 6 +- src/Umbraco.Web/Models/Trees/MenuItem.cs | 2 +- .../Trees/FileSystemTreeController.cs | 83 ++++++++++++------- .../Trees/LegacyTreeDataConverter.cs | 5 +- src/Umbraco.Web/Trees/MacroTreeController.cs | 14 +++- src/Umbraco.Web/Trees/XsltTreeController.cs | 80 ++++++++++++++++++ src/Umbraco.Web/Umbraco.Web.csproj | 1 + .../umbraco/Trees/loadXslt.cs | 7 +- .../umbraco/developer/Xslt/editXslt.aspx.cs | 4 +- src/umbraco.cms/helpers/DeepLink.cs | 14 +++- 13 files changed, 172 insertions(+), 56 deletions(-) create mode 100644 src/Umbraco.Web/Trees/XsltTreeController.cs diff --git a/build/NuSpecs/tools/trees.config.install.xdt b/build/NuSpecs/tools/trees.config.install.xdt index 21dc791636..5a549e3fd8 100644 --- a/build/NuSpecs/tools/trees.config.install.xdt +++ b/build/NuSpecs/tools/trees.config.install.xdt @@ -79,14 +79,14 @@ - - - diff --git a/src/Umbraco.Core/Constants-Applications.cs b/src/Umbraco.Core/Constants-Applications.cs index 1a53df8b09..a5c67fba93 100644 --- a/src/Umbraco.Core/Constants-Applications.cs +++ b/src/Umbraco.Core/Constants-Applications.cs @@ -77,7 +77,7 @@ /// alias for the macro tree. /// public const string Macros = "macros"; - + /// /// alias for the datatype tree. /// diff --git a/src/Umbraco.Web.UI/config/trees.Release.config b/src/Umbraco.Web.UI/config/trees.Release.config index bce7604637..c0205c7db4 100644 --- a/src/Umbraco.Web.UI/config/trees.Release.config +++ b/src/Umbraco.Web.UI/config/trees.Release.config @@ -22,9 +22,9 @@ - + - + diff --git a/src/Umbraco.Web.UI/config/trees.config b/src/Umbraco.Web.UI/config/trees.config index dcd4eccbfb..a36df8bbff 100644 --- a/src/Umbraco.Web.UI/config/trees.config +++ b/src/Umbraco.Web.UI/config/trees.config @@ -21,9 +21,9 @@ - - - + + + diff --git a/src/Umbraco.Web/Models/Trees/MenuItem.cs b/src/Umbraco.Web/Models/Trees/MenuItem.cs index 1717ecb1b6..d06fb92ecd 100644 --- a/src/Umbraco.Web/Models/Trees/MenuItem.cs +++ b/src/Umbraco.Web/Models/Trees/MenuItem.cs @@ -187,7 +187,7 @@ namespace Umbraco.Web.Models.Trees nodeType, item == null ? "" : item.Name, currentSection), action => LaunchDialogUrl(action.Url, action.DialogTitle)) - .OnFailure(() => LegacyTreeDataConverter.GetLegacyConfirmView(Action, currentSection), + .OnFailure(() => LegacyTreeDataConverter.GetLegacyConfirmView(Action), view => LaunchDialogView( view, ui.GetText("defaultdialogs", "confirmdelete") + " '" + (item == null ? "" : item.Name) + "' ?")); diff --git a/src/Umbraco.Web/Trees/FileSystemTreeController.cs b/src/Umbraco.Web/Trees/FileSystemTreeController.cs index 4f414a941d..f26e255a41 100644 --- a/src/Umbraco.Web/Trees/FileSystemTreeController.cs +++ b/src/Umbraco.Web/Trees/FileSystemTreeController.cs @@ -72,23 +72,64 @@ namespace Umbraco.Web.Trees return nodes; } - protected override MenuItemCollection GetMenuForNode(string id, FormDataCollection queryStrings) + protected virtual MenuItemCollection GetMenuForRootNode(FormDataCollection queryStrings) { var menu = new MenuItemCollection(); + //set the default to create + menu.DefaultMenuAlias = ActionNew.Instance.Alias; + //create action + menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionNew.Instance.Alias))); + //refresh action + menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionRefresh.Instance.Alias)), true); + + return menu; + } + + protected virtual MenuItemCollection GetMenuForFolder(string path, FormDataCollection queryStrings) + { + var menu = new MenuItemCollection(); + + //set the default to create + menu.DefaultMenuAlias = ActionNew.Instance.Alias; + //create action + menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionNew.Instance.Alias))); + + var hasChildren = FileSystem.GetFiles(path).Any() || FileSystem.GetDirectories(path).Any(); + + //We can only delete folders if it doesn't have any children (folders or files) + if (hasChildren == false) + { + //delete action + menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionDelete.Instance.Alias)), true); + } + + //refresh action + menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionRefresh.Instance.Alias)), true); + + return menu; + } + + protected virtual MenuItemCollection GetMenuForFile(string path, FormDataCollection queryStrings) + { + var menu = new MenuItemCollection(); + + //if it's not a directory then we only allow to delete the item + menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionDelete.Instance.Alias))); + + return menu; + } + + protected override MenuItemCollection GetMenuForNode(string id, FormDataCollection queryStrings) + { //if root node no need to visit the filesystem so lets just create the menu and return it if (id == Constants.System.Root.ToInvariantString()) { - //set the default to create - menu.DefaultMenuAlias = ActionNew.Instance.Alias; - //create action - menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionNew.Instance.Alias))); - //refresh action - menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionRefresh.Instance.Alias)), true); - - return menu; + return GetMenuForRootNode(queryStrings); } + var menu = new MenuItemCollection(); + var path = string.IsNullOrEmpty(id) == false && id != Constants.System.Root.ToInvariantString() ? HttpUtility.UrlDecode(id).TrimStart("/") : ""; @@ -98,30 +139,10 @@ namespace Umbraco.Web.Trees if (isDirectory) { - //set the default to create - menu.DefaultMenuAlias = ActionNew.Instance.Alias; - //create action - menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionNew.Instance.Alias))); - - var hasChildren = FileSystem.GetFiles(path).Any() || FileSystem.GetDirectories(path).Any(); - - //We can only delete folders if it doesn't have any children (folders or files) - if (hasChildren == false) - { - //delete action - menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionDelete.Instance.Alias)), true); - } - - //refresh action - menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionRefresh.Instance.Alias)), true); - } - else if (isFile) - { - //if it's not a directory then we only allow to delete the item - menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionDelete.Instance.Alias))); + return GetMenuForFolder(path, queryStrings); } - return menu; + return isFile ? GetMenuForFile(path, queryStrings) : menu; } } } diff --git a/src/Umbraco.Web/Trees/LegacyTreeDataConverter.cs b/src/Umbraco.Web/Trees/LegacyTreeDataConverter.cs index 32a7ad5d99..c6be75f124 100644 --- a/src/Umbraco.Web/Trees/LegacyTreeDataConverter.cs +++ b/src/Umbraco.Web/Trees/LegacyTreeDataConverter.cs @@ -134,7 +134,7 @@ namespace Umbraco.Web.Trees Attempt .Try(GetUrlAndTitleFromLegacyAction(currentAction, xmlTreeNode.NodeID, xmlTreeNode.NodeType, xmlTreeNode.Text, currentSection), action => menuItem.LaunchDialogUrl(action.Url, action.DialogTitle)) - .OnFailure(() => GetLegacyConfirmView(currentAction, currentSection), + .OnFailure(() => GetLegacyConfirmView(currentAction), view => menuItem.LaunchDialogView( view, ui.GetText("defaultdialogs", "confirmdelete") + " '" + xmlTreeNode.Text + "' ?")) @@ -164,9 +164,8 @@ namespace Umbraco.Web.Trees /// This will look at the legacy IAction's JsFunctionName and convert it to a confirmation dialog view if possible /// /// - /// /// - internal static Attempt GetLegacyConfirmView(IAction action, string currentSection) + internal static Attempt GetLegacyConfirmView(IAction action) { if (action.JsFunctionName.IsNullOrWhiteSpace()) { diff --git a/src/Umbraco.Web/Trees/MacroTreeController.cs b/src/Umbraco.Web/Trees/MacroTreeController.cs index 5026aec4c8..4a6ca8ad3d 100644 --- a/src/Umbraco.Web/Trees/MacroTreeController.cs +++ b/src/Umbraco.Web/Trees/MacroTreeController.cs @@ -53,14 +53,20 @@ namespace Umbraco.Web.Trees // root actions menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionNew.Instance.Alias))) .ConvertLegacyMenuItem(null, Constants.Trees.Macros, queryStrings.GetValue("application")); - - //menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionNew.Instance.Alias))); - + menu.Items.Add(ui.Text("actions", ActionRefresh.Instance.Alias), true); return menu; } - menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionDelete.Instance.Alias))); + //TODO: This is all hacky ... don't have time to convert the tree, views and dialogs over properly so we'll keep using the legacy dialogs + var menuItem = menu.Items.Add(ActionDelete.Instance, Services.TextService.Localize(string.Format("actions/{0}", ActionDelete.Instance.Alias))); + var legacyConfirmView = LegacyTreeDataConverter.GetLegacyConfirmView(ActionDelete.Instance); + if (legacyConfirmView == false) + throw new InvalidOperationException("Could not resolve the confirmation view for the legacy action " + ActionDelete.Instance.Alias); + menuItem.LaunchDialogView( + legacyConfirmView.Result, + Services.TextService.Localize("general/delete")); + return menu; } } diff --git a/src/Umbraco.Web/Trees/XsltTreeController.cs b/src/Umbraco.Web/Trees/XsltTreeController.cs new file mode 100644 index 0000000000..f35dc42b3a --- /dev/null +++ b/src/Umbraco.Web/Trees/XsltTreeController.cs @@ -0,0 +1,80 @@ +using System; +using System.Net.Http.Formatting; +using umbraco; +using umbraco.BusinessLogic.Actions; +using Umbraco.Core.IO; +using Umbraco.Core.Services; +using Umbraco.Web.Models.Trees; +using Umbraco.Web.Mvc; +using Umbraco.Web.WebApi.Filters; +using Constants = Umbraco.Core.Constants; + +namespace Umbraco.Web.Trees +{ + [UmbracoTreeAuthorize(Constants.Trees.Xslt)] + [Tree(Constants.Applications.Developer, Constants.Trees.Xslt, null, sortOrder: 5)] + [LegacyBaseTree(typeof(loadXslt))] + [PluginController("UmbracoTrees")] + [CoreTree] + public class XsltTreeController : FileSystemTreeController + { + protected override void OnRenderFileNode(ref TreeNode treeNode) + { + ////TODO: This is all hacky ... don't have time to convert the tree, views and dialogs over properly so we'll keep using the legacy views + treeNode.AssignLegacyJsCallback("javascript:UmbClientMgr.contentFrame('developer/xslt/editXslt.aspx?file=" + treeNode.Id + "');"); + } + + protected override void OnRenderFolderNode(ref TreeNode treeNode) + { + //TODO: This is all hacky ... don't have time to convert the tree, views and dialogs over properly so we'll keep using the legacy views + treeNode.AssignLegacyJsCallback("javascript:void(0);"); + } + + protected override MenuItemCollection GetMenuForFile(string path, FormDataCollection queryStrings) + { + var menu = new MenuItemCollection(); + + //TODO: This is all hacky ... don't have time to convert the tree, views and dialogs over properly so we'll keep using the legacy dialogs + var menuItem = menu.Items.Add(ActionDelete.Instance, Services.TextService.Localize(string.Format("actions/{0}", ActionDelete.Instance.Alias))); + var legacyConfirmView = LegacyTreeDataConverter.GetLegacyConfirmView(ActionDelete.Instance); + if (legacyConfirmView == false) + throw new InvalidOperationException("Could not resolve the confirmation view for the legacy action " + ActionDelete.Instance.Alias); + menuItem.LaunchDialogView( + legacyConfirmView.Result, + Services.TextService.Localize("general/delete")); + + return menu; + } + + protected override MenuItemCollection GetMenuForRootNode(FormDataCollection queryStrings) + { + var menu = new MenuItemCollection(); + + //set the default to create + menu.DefaultMenuAlias = ActionNew.Instance.Alias; + + // root actions + menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionNew.Instance.Alias))) + .ConvertLegacyMenuItem(null, Constants.Trees.Xslt, queryStrings.GetValue("application")); + + menu.Items.Add(ui.Text("actions", ActionRefresh.Instance.Alias), true); + return menu; + } + + protected override IFileSystem2 FileSystem + { + get { return FileSystemProviderManager.Current.XsltFileSystem; } + } + + private static readonly string[] ExtensionsStatic = { "xslt" }; + + protected override string[] Extensions + { + get { return ExtensionsStatic; } + } + protected override string FileIcon + { + get { return "icon-code"; } + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 4766d99d7a..4b05f2956b 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -508,6 +508,7 @@ + diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/Trees/loadXslt.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/Trees/loadXslt.cs index 4a03b31551..9b26de52ec 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/Trees/loadXslt.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/Trees/loadXslt.cs @@ -1,6 +1,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.ComponentModel; using System.Data; using System.IO; using System.Text; @@ -29,10 +30,8 @@ using Umbraco.Core; namespace umbraco { - /// - /// Handles loading of the xslt files into the application tree - /// - [Tree(Constants.Applications.Developer, "xslt", "XSLT Files", sortOrder: 5)] + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("This is no longer used and will be removed from the codebase in the future")] public class loadXslt : FileSystemTree { diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/developer/Xslt/editXslt.aspx.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/developer/Xslt/editXslt.aspx.cs index 03ce088941..b1414ddb68 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/developer/Xslt/editXslt.aspx.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/developer/Xslt/editXslt.aspx.cs @@ -33,9 +33,9 @@ namespace umbraco.cms.presentation.developer if (!IsPostBack) { string file = Request.QueryString["file"]; - string path = DeepLink.GetTreePathFromFilePath(file); + string path = DeepLink.GetTreePathFromFilePath(file, false, true); ClientTools - .SetActiveTreeType(TreeDefinitionCollection.Instance.FindTree().Tree.Alias) + .SetActiveTreeType(Constants.Trees.Xslt) .SyncTree(path, false); } diff --git a/src/umbraco.cms/helpers/DeepLink.cs b/src/umbraco.cms/helpers/DeepLink.cs index ac0fdce5b7..4b0878c355 100644 --- a/src/umbraco.cms/helpers/DeepLink.cs +++ b/src/umbraco.cms/helpers/DeepLink.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Net; using System.Text; using umbraco.BusinessLogic; using System.Web; @@ -11,15 +12,24 @@ namespace umbraco.cms.helpers public class DeepLink { public static string GetTreePathFromFilePath(string filePath) + { + return GetTreePathFromFilePath(filePath, true, false); + } + + internal static string GetTreePathFromFilePath(string filePath, bool includeInit, bool urlEncode) { List treePath = new List(); treePath.Add("-1"); - treePath.Add("init"); + if (includeInit) + treePath.Add("init"); string[] pathPaths = filePath.Split('/'); for (int p = 0; p < pathPaths.Length; p++) { - treePath.Add(string.Join("/", pathPaths.Take(p + 1).ToArray())); + var s = string.Join("/", pathPaths.Take(p + 1).ToArray()); + if (urlEncode) + s = WebUtility.UrlEncode(s); + treePath.Add(s); } string sPath = string.Join(",", treePath.ToArray()); return sPath; From 5cb2dd00364a1c384ae820b401d3bb9b91fc2cba Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 5 Jan 2018 15:19:16 +1100 Subject: [PATCH 10/31] Makes the features internal --- src/Umbraco.Web/Features/DisabledFeatures.cs | 2 +- src/Umbraco.Web/Features/FeaturesResolver.cs | 2 +- src/Umbraco.Web/Features/TypeList.cs | 2 +- src/Umbraco.Web/Features/UmbracoFeatures.cs | 2 +- src/Umbraco.Web/Properties/AssemblyInfo.cs | 1 + 5 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Web/Features/DisabledFeatures.cs b/src/Umbraco.Web/Features/DisabledFeatures.cs index 85d8e12b38..09b68f59fd 100644 --- a/src/Umbraco.Web/Features/DisabledFeatures.cs +++ b/src/Umbraco.Web/Features/DisabledFeatures.cs @@ -6,7 +6,7 @@ namespace Umbraco.Web.Features /// /// Represents Umbraco features that can be disabled /// - public class DisabledFeatures + internal class DisabledFeatures { public DisabledFeatures() { diff --git a/src/Umbraco.Web/Features/FeaturesResolver.cs b/src/Umbraco.Web/Features/FeaturesResolver.cs index 3b7b0b3b36..e7dd5a4a88 100644 --- a/src/Umbraco.Web/Features/FeaturesResolver.cs +++ b/src/Umbraco.Web/Features/FeaturesResolver.cs @@ -2,7 +2,7 @@ using Umbraco.Core.ObjectResolution; namespace Umbraco.Web.Features { - public class FeaturesResolver : SingleObjectResolverBase + internal class FeaturesResolver : SingleObjectResolverBase { public FeaturesResolver(UmbracoFeatures value) : base(value) { diff --git a/src/Umbraco.Web/Features/TypeList.cs b/src/Umbraco.Web/Features/TypeList.cs index 86b24533ac..f415b257f7 100644 --- a/src/Umbraco.Web/Features/TypeList.cs +++ b/src/Umbraco.Web/Features/TypeList.cs @@ -7,7 +7,7 @@ namespace Umbraco.Web.Features /// Maintains a list of strongly typed types /// /// - public class TypeList + internal class TypeList { private readonly List _disabled = new List(); diff --git a/src/Umbraco.Web/Features/UmbracoFeatures.cs b/src/Umbraco.Web/Features/UmbracoFeatures.cs index 5d4570c4f2..04bc6da363 100644 --- a/src/Umbraco.Web/Features/UmbracoFeatures.cs +++ b/src/Umbraco.Web/Features/UmbracoFeatures.cs @@ -3,7 +3,7 @@ namespace Umbraco.Web.Features /// /// Represents Umbraco features that can be toggled /// - public class UmbracoFeatures + internal class UmbracoFeatures { public UmbracoFeatures() { diff --git a/src/Umbraco.Web/Properties/AssemblyInfo.cs b/src/Umbraco.Web/Properties/AssemblyInfo.cs index c016d328a9..9f235e00d5 100644 --- a/src/Umbraco.Web/Properties/AssemblyInfo.cs +++ b/src/Umbraco.Web/Properties/AssemblyInfo.cs @@ -40,6 +40,7 @@ using System.Security; [assembly: InternalsVisibleTo("Umbraco.Deploy.Cloud")] [assembly: InternalsVisibleTo("Umbraco.ModelsBuilder")] [assembly: InternalsVisibleTo("Umbraco.ModelsBuilder.AspNet")] +[assembly: InternalsVisibleTo("Umbraco.Headless")] [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] [assembly: InternalsVisibleTo("Umbraco.Forms.Core")] From 8c70578764641460fe8c0fea0e0fc623998d0183 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 5 Jan 2018 16:22:40 +1100 Subject: [PATCH 11/31] fix tests --- src/Umbraco.Tests/Web/Controllers/UsersControllerTests.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Umbraco.Tests/Web/Controllers/UsersControllerTests.cs b/src/Umbraco.Tests/Web/Controllers/UsersControllerTests.cs index 2da6d8291c..b4ccc01002 100644 --- a/src/Umbraco.Tests/Web/Controllers/UsersControllerTests.cs +++ b/src/Umbraco.Tests/Web/Controllers/UsersControllerTests.cs @@ -17,6 +17,7 @@ using Umbraco.Tests.TestHelpers; using Umbraco.Tests.TestHelpers.ControllerTesting; using Umbraco.Tests.TestHelpers.Entities; using Umbraco.Web.Editors; +using Umbraco.Web.Features; using Umbraco.Web.Models.ContentEditing; using IUser = Umbraco.Core.Models.Membership.IUser; @@ -26,6 +27,11 @@ namespace Umbraco.Tests.Web.Controllers [TestFixture] public class UsersControllerTests : BaseDatabaseFactoryTest { + protected override void FreezeResolution() + { + FeaturesResolver.Current = new FeaturesResolver(new UmbracoFeatures()); + base.FreezeResolution(); + } [Test] public async void Save_User() From 070fa11f8cb4e8c8f3149d37a4b37cb873d0924e Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Mon, 8 Jan 2018 14:11:53 +0100 Subject: [PATCH 12/31] fixes: U4-10707 Config to order tour groups --- .../src/common/services/tour.service.js | 29 ++++++++++++++++++- .../common/drawers/help/help.controller.js | 6 ++-- .../src/views/common/drawers/help/help.html | 18 ++++++------ 3 files changed, 40 insertions(+), 13 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/services/tour.service.js b/src/Umbraco.Web.UI.Client/src/common/services/tour.service.js index 1955cf7219..5ad997ce76 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/tour.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/tour.service.js @@ -218,7 +218,34 @@ var deferred = $q.defer(); var tours = getTours(); setTourStatuses(tours).then(function() { - var groupedTours = _.groupBy(tours, "group"); + var groupedTours = []; + var sortedTours = _.sortBy(tours, 'groupOrder'); + + sortedTours.forEach(function (item) { + + var groupExists = false; + var newGroup = { + "group": "", + "tours": [] + }; + + groupedTours.forEach(function(group){ + // extend existing group if it is already added + if(group.group === item.group) { + groupExists = true; + group.tours.push(item) + } + }); + + // push new group to array if it doesn't exist + if(!groupExists) { + newGroup.group = item.group; + newGroup.tours.push(item); + groupedTours.push(newGroup); + } + + }); + deferred.resolve(groupedTours); }); return deferred.promise; diff --git a/src/Umbraco.Web.UI.Client/src/views/common/drawers/help/help.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/drawers/help/help.controller.js index 34a3d8a811..4d2b43c078 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/drawers/help/help.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/drawers/help/help.controller.js @@ -124,7 +124,7 @@ function showTourButton(index, tourGroup) { if(index !== 0) { - var prevTour = tourGroup[index - 1]; + var prevTour = tourGroup.tours[index - 1]; if(prevTour.completed) { return true; } @@ -147,12 +147,12 @@ // Finding out, how many tours are completed for the progress circle angular.forEach(vm.tours, function(group){ var completedTours = 0; - angular.forEach(group, function(tour){ + angular.forEach(group.tours, function(tour){ if(tour.completed) { completedTours++; } }); - group.completedPercentage = Math.round((completedTours/group.length)*100); + group.completedPercentage = Math.round((completedTours/group.tours.length)*100); }); } diff --git a/src/Umbraco.Web.UI.Client/src/views/common/drawers/help/help.html b/src/Umbraco.Web.UI.Client/src/views/common/drawers/help/help.html index 5e1d5d271e..e755512b02 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/drawers/help/help.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/drawers/help/help.html @@ -12,23 +12,23 @@
Tours
-
+
- -
- {{key}} - Other + +
+ {{tourGroup.group}} + Other
-
-
+
+
{{ $index + 1 }}
@@ -36,7 +36,7 @@ {{ tour.name }}
- +
From b15def99c7caae750b629f55b1d610ec1afc48a3 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Tue, 9 Jan 2018 11:53:21 +0100 Subject: [PATCH 13/31] format as json --- .../BackOfficeTours/getting-started.json | 398 +++++++++--------- 1 file changed, 199 insertions(+), 199 deletions(-) diff --git a/src/Umbraco.Web.UI/config/BackOfficeTours/getting-started.json b/src/Umbraco.Web.UI/config/BackOfficeTours/getting-started.json index 1e75e21cc0..72c11f4937 100644 --- a/src/Umbraco.Web.UI/config/BackOfficeTours/getting-started.json +++ b/src/Umbraco.Web.UI/config/BackOfficeTours/getting-started.json @@ -88,111 +88,111 @@ "group": "Getting Started", "steps": [ { - title: "Create your first Document Type", - content: "

Step 1 of any site is to create a Document Type.
A Document Type is a template for content. For each type of content you want to create you'll create a Document Type. This will define were content based on this Document Type can be created, how many properties it holds and what the input method should be for these properties.

When you have at least one Document type in place you can start creating content and this content can the be used in a template.

In this tour you will learn how to set up a basic Document Type with a property to enter a short text.

", - type: "intro" + "title": "Create your first Document Type", + "content": "

Step 1 of any site is to create a Document Type.
A Document Type is a template for content. For each type of content you want to create you'll create a Document Type. This will define were content based on this Document Type can be created, how many properties it holds and what the input method should be for these properties.

When you have at least one Document type in place you can start creating content and this content can the be used in a template.

In this tour you will learn how to set up a basic Document Type with a property to enter a short text.

", + "type": "intro" }, { - element: "#applications [data-element='section-settings']", - title: "Navigate to the Settings sections", - content: "In the Settings section you can create and manage Document types.", - event: "click", - backdropOpacity: 0.6 + "element": "#applications [data-element='section-settings']", + "title": "Navigate to the Settings sections", + "content": "In the Settings section you can create and manage Document types.", + "event": "click", + "backdropOpacity": 0.6 }, { - element: "#tree [data-element='tree-item-documentTypes']", - title: "Create Document Type", - content: "

Hover the Document Type tree and click the three small dots to open the context menu.

", - event: "click", - eventElement: "#tree [data-element='tree-item-documentTypes'] [data-element='tree-item-options']" + "element": "#tree [data-element='tree-item-documentTypes']", + "title": "Create Document Type", + "content": "

Hover the Document Type tree and click the three small dots to open the context menu.

", + "event": "click", + "eventElement": "#tree [data-element='tree-item-documentTypes'] [data-element='tree-item-options']" }, { - element: "#dialog [data-element='action-documentType']", - title: "Create Document Type", - content: "

Click Document Type to create a new document type with a template. The template will be automatically created and set as the default template for this Document Type

You will use the template in a later tour render content.

", - event: "click" + "element": "#dialog [data-element='action-documentType']", + "title": "Create Document Type", + "content": "

Click Document Type to create a new document type with a template. The template will be automatically created and set as the default template for this Document Type

You will use the template in a later tour render content.

", + "event": "click" }, { - element: "[data-element='editor-name-field']", - title: "Enter a name", - content: "

Your Document Type needs a name. Enter My Home Page in the field and click Next.", - view: "doctypename" + "element": "[data-element='editor-name-field']", + "title": "Enter a name", + "content": "

Your Document Type needs a name. Enter My Home Page in the field and click Next.", + "view": "doctypename" }, { - element: "[data-element='editor-description']", - title: "Enter a description", - content: "

A description helps to pick the right document type when creating content.

Write a description to our Home page. It could be:

The home page of the website

" + "element": "[data-element='editor-description']", + "title": "Enter a description", + "content": "

A description helps to pick the right document type when creating content.

Write a description to our Home page. It could be:

The home page of the website

" }, { - element: "[data-element='group-add']", - title: "Add tab", - content: "Tabs are used to organize properties on content in the Content section. Click Add new tab to add a tab.", - event: "click" + "element": "[data-element='group-add']", + "title": "Add tab", + "content": "Tabs are used to organize properties on content in the Content section. Click Add new tab to add a tab.", + "event": "click" }, { - element: "[data-element='group-name-field']", - title: "Name the tab", - content: "

Enter Home in the tab name.

You can name a tab anything you want and if you have a lot of properties it can be useful to add multiple tabs.

", - view: "tabName" + "element": "[data-element='group-name-field']", + "title": "Name the tab", + "content": "

Enter Home in the tab name.

You can name a tab anything you want and if you have a lot of properties it can be useful to add multiple tabs.

", + "view": "tabName" }, { - element: "[data-element='property-add']", - title: "Add a property", - content: "

Properties are the different input fields on a content page.

On our Home Page we wan't to add a welcome text.

Click Add property to open the property dialog.

", - event: "click" + "element": "[data-element='property-add']", + "title": "Add a property", + "content": "

Properties are the different input fields on a content page.

On our Home Page we wan't to add a welcome text.

Click Add property to open the property dialog.

", + "event": "click" }, { - element: "[data-element~='overlay-property-settings'] [data-element='property-name']", - title: "Name the property", - content: "Enter Welcome Text as the name for the property.", - view: "propertyname" + "element": "[data-element~='overlay-property-settings'] [data-element='property-name']", + "title": "Name the property", + "content": "Enter Welcome Text as the name for the property.", + "view": "propertyname" }, { - element: "[data-element~='overlay-property-settings'] [data-element='property-description']", - title: "Enter a description", - content: "

A description will help to fill in the right content.

Enter a description for the property editor. It could be:

Write a nice introduction text so the visitors feel welcome

" + "element": "[data-element~='overlay-property-settings'] [data-element='property-description']", + "title": "Enter a description", + "content": "

A description will help to fill in the right content.

Enter a description for the property editor. It could be:

Write a nice introduction text so the visitors feel welcome

" }, { - element: "[data-element~='overlay-property-settings'] [data-element='editor-add']", - title: "Add editor", - content: "When you add an editor you choose what the input method for this property will be. Click Add editor to open the editor picker dialog.", - event: "click" + "element": "[data-element~='overlay-property-settings'] [data-element='editor-add']", + "title": "Add editor", + "content": "When you add an editor you choose what the input method for this property will be. Click Add editor to open the editor picker dialog.", + "event": "click" }, { - element: "[data-element~='overlay-editor-picker']", - elementPreventClick: true, - title: "Editor picker", - content: "

In the editor picker dialog we can pick one of the many build in editor.

You can choose from preconfigured data types (Reuse) or create a new configuration (Available editors)

" + "element": "[data-element~='overlay-editor-picker']", + "elementPreventClick": true, + "title": "Editor picker", + "content": "

In the editor picker dialog we can pick one of the many build in editor.

You can choose from preconfigured data types (Reuse) or create a new configuration (Available editors)

" }, { - element: "[data-element~='overlay-editor-picker'] [data-element='editor-Textarea']", - title: "Select editor", - content: "Select the Textarea editor. This will add a textarea to the Welcome Text property.", - event: "click" + "element": "[data-element~='overlay-editor-picker'] [data-element='editor-Textarea']", + "title": "Select editor", + "content": "Select the Textarea editor. This will add a textarea to the Welcome Text property.", + "event": "click" }, { - element: "[data-element~='overlay-editor-settings']", - elementPreventClick: true, - title: "Editor settings", - content: "Each property editor can have individual settings. For the textarea editor you can set a charachter limit but in this case it is not needed" + "element": "[data-element~='overlay-editor-settings']", + "elementPreventClick": true, + "title": "Editor settings", + "content": "Each property editor can have individual settings. For the textarea editor you can set a charachter limit but in this case it is not needed" }, { - element: "[data-element~='overlay-editor-settings'] [data-element='button-overlaySubmit']", - title: "Save editor", - content: "Click Submit to save the editor.", - event: "click" + "element": "[data-element~='overlay-editor-settings'] [data-element='button-overlaySubmit']", + "title": "Save editor", + "content": "Click Submit to save the editor.", + "event": "click" }, { - element: "[data-element~='overlay-property-settings'] [data-element='button-overlaySubmit']", - title: "Add property to document type", - content: "Click Submit to add the property to the document type.", - event: "click" + "element": "[data-element~='overlay-property-settings'] [data-element='button-overlaySubmit']", + "title": "Add property to document type", + "content": "Click Submit to add the property to the document type.", + "event": "click" }, { - element: "[data-element='button-save']", - title: "Save the document type", - content: "All we need now is to save the document type. Click Save to create and save your new document type.", - event: "click" + "element": "[data-element='button-save']", + "title": "Save the document type", + "content": "All we need now is to save the document type. Click Save to create and save your new document type.", + "event": "click" } ] }, @@ -202,46 +202,46 @@ "group": "Getting Started", "steps": [ { - title: "Creating your first content node", - content: "

In this tour you will learn how to create the home page for your website. It will use the Home Page Document type you created in the previous tour.

", - type: "intro" + "title": "Creating your first content node", + "content": "

In this tour you will learn how to create the home page for your website. It will use the Home Page Document type you created in the previous tour.

", + "type": "intro" }, { - element: "#applications [data-element='section-content']", - title: "Navigate to the Content section", - content: "

In the Content section you can create and manage the content of the website.

The Content section contains the content of your website. Content is displayed as nodes in the content tree.

", - event: "click", - backdropOpacity: 0.6 + "element": "#applications [data-element='section-content']", + "title": "Navigate to the Content section", + "content": "

In the Content section you can create and manage the content of the website.

The Content section contains the content of your website. Content is displayed as nodes in the content tree.

", + "event": "click", + "backdropOpacity": 0.6 }, { - element: "[data-element='tree-root']", - title: "Open context menu", - content: "

Open the context menu by hovering the root of the content section.

Now click the three small dots to the right.

", - event: "click", - eventElement: "[data-element='tree-root'] [data-element='tree-item-options']" + "element": "[data-element='tree-root']", + "title": "Open context menu", + "content": "

Open the context menu by hovering the root of the content section.

Now click the three small dots to the right.

", + "event": "click", + "eventElement": "[data-element='tree-root'] [data-element='tree-item-options']" }, { - element: "[data-element='action-create-homePage']", - title: "Create Home page", - content: "

The context menu shows you all the actions that are available on a node

Click on Home Page to create a new page of type Home Page.

", - event: "click" + "element": "[data-element='action-create-homePage']", + "title": "Create Home page", + "content": "

The context menu shows you all the actions that are available on a node

Click on Home Page to create a new page of type Home Page.

", + "event": "click" }, { - element: "[data-element='editor-content'] [data-element='editor-name-field']", - title: "Give your new page a name", - content: "

Our new page needs a name. Enter Home in the field and click Next.

", - view: "nodename" + "element": "[data-element='editor-content'] [data-element='editor-name-field']", + "title": "Give your new page a name", + "content": "

Our new page needs a name. Enter Home in the field and click Next.

", + "view": "nodename" }, { - element: "[data-element='editor-content'] [data-element='property-welcomeText']", - title: "Add a welcome text", - content: "

Add content to the Welcome Text field

If you don't have any ideas here is a start:

I am learning Umbraco. High Five I Rock #H5IR
.

" + "element": "[data-element='editor-content'] [data-element='property-welcomeText']", + "title": "Add a welcome text", + "content": "

Add content to the Welcome Text field

If you don't have any ideas here is a start:

I am learning Umbraco. High Five I Rock #H5IR
.

" }, { - element: "[data-element='editor-content'] [data-element='button-saveAndPublish']", - title: "Save and Publish", - content: "

Now click the Save and publish button to save and publish your changes.

", - event: "click" + "element": "[data-element='editor-content'] [data-element='button-saveAndPublish']", + "title": "Save and Publish", + "content": "

Now click the Save and publish button to save and publish your changes.

", + "event": "click" } ] }, @@ -251,42 +251,42 @@ "group": "Getting Started", "steps": [ { - title: "Render your content in a template", - content: "

Templating in Umbraco builds on the concept of Razor Views from asp.net MVC. - This tour is a sneak peak on how to write templates in Umbraco.

In this tour you will learn how to render content from the Home Page document type so you can see the content added to our Home content page.

", - type: "intro" + "title": "Render your content in a template", + "content": "

Templating in Umbraco builds on the concept of Razor Views from asp.net MVC. - This tour is a sneak peak on how to write templates in Umbraco.

In this tour you will learn how to render content from the Home Page document type so you can see the content added to our Home content page.

", + "type": "intro" }, { - element: "#applications [data-element='section-settings']", - title: "Navigate to the Settings section", - content: "

In the Settings section you will find all the templates

It is of course also possible to edit all your code files in your favorite code editor.

", - event: "click", - backdropOpacity: 0.6 + "element": "#applications [data-element='section-settings']", + "title": "Navigate to the Settings section", + "content": "

In the Settings section you will find all the templates

It is of course also possible to edit all your code files in your favorite code editor.

", + "event": "click", + "backdropOpacity": 0.6 }, { - element: "#tree [data-element='tree-item-templates']", - title: "Expand the Templates node", - content: "

To see all our templates click the small triangle to the left of the templates node.

", - event: "click", - eventElement: "#tree [data-element='tree-item-templates'] [data-element='tree-item-expand']", - view: "templatetree" + "element": "#tree [data-element='tree-item-templates']", + "title": "Expand the Templates node", + "content": "

To see all our templates click the small triangle to the left of the templates node.

", + "event": "click", + "eventElement": "#tree [data-element='tree-item-templates'] [data-element='tree-item-expand']", + "view": "templatetree" }, { - element: "#tree [data-element='tree-item-templates'] [data-element='tree-item-Home Page']", - title: "Open Home template", - content: "

Click the Home Page template to open and edit it.

", - eventElement: "#tree [data-element='tree-item-templates'] [data-element='tree-item-Home Page'] a.umb-tree-item__label", - event: "click" + "element": "#tree [data-element='tree-item-templates'] [data-element='tree-item-Home Page']", + "title": "Open Home template", + "content": "

Click the Home Page template to open and edit it.

", + "eventElement": "#tree [data-element='tree-item-templates'] [data-element='tree-item-Home Page'] a.umb-tree-item__label", + "event": "click" }, { - element: "[data-element='editor-templates'] [data-element='code-editor']", - title: "Edit template", - content: '

The template can be edited here or in your favorite code editor.

To render the field from the document type add the following to the template:

<h1>@Model.Content.Name</h1>
<p>@Model.Content.WelcomeText</p>

' + "element": "[data-element='editor-templates'] [data-element='code-editor']", + "title": "Edit template", + "content": "

The template can be edited here or in your favorite code editor.

To render the field from the document type add the following to the template:

<h1>@Model.Content.Name</h1>
<p>@Model.Content.WelcomeText</p>

" }, { - element: "[data-element='editor-templates'] [data-element='button-save']", - title: "Save the template", - content: "Click the Save button and your template will be saved.", - event: "click" + "element": "[data-element='editor-templates'] [data-element='button-save']", + "title": "Save the template", + "content": "Click the Save button and your template will be saved.", + "event": "click" } ] }, @@ -296,36 +296,36 @@ "group": "Getting Started", "steps": [ { - title: "View your Umbraco site", - content: "

Our three main components to a page is done: Document type, Template, and Content - it is now time to see the result.

In this tour you will learn how to see your published website.

", - type: "intro" + "title": "View your Umbraco site", + "content": "

Our three main components to a page is done: Document type, Template, and Content - it is now time to see the result.

In this tour you will learn how to see your published website.

", + "type": "intro" }, { - element: "#applications [data-element='section-content']", - title: "Navigate to the content sections", - content: "In the Content section you will find the content of our website.", - event: "click", - backdropOpacity: 0.6 + "element": "#applications [data-element='section-content']", + "title": "Navigate to the content sections", + "content": "In the Content section you will find the content of our website.", + "event": "click", + "backdropOpacity": 0.6 }, { - element: "#tree [data-element='tree-item-Home']", - title: "Open the Home page", - content: "

Click the Home page to open it

", - event: "click", - eventElement: "#tree [data-element='tree-item-Home'] a.umb-tree-item__label" + "element": "#tree [data-element='tree-item-Home']", + "title": "Open the Home page", + "content": "

Click the Home page to open it

", + "event": "click", + "eventElement": "#tree [data-element='tree-item-Home'] a.umb-tree-item__label" }, { - element: "[data-element='editor-content'] [data-element='tab-_umb_infoTab']", - title: "Info", - content: "

Under the info tab you will find the default information about a content item.

", - event: "click" + "element": "[data-element='editor-content'] [data-element='tab-_umb_infoTab']", + "title": "Info", + "content": "

Under the info tab you will find the default information about a content item.

", + "event": "click" }, { - element: "[data-element='editor-content'] [data-element='node-info-urls']", - title: "Open page", - content: "

Click the Link to document to view your page.

Tip: Click the preview button in the bottom right corner to preview changes without publishing them.

", - event: "click", - eventElement: "[data-element='editor-content'] [data-element='node-info-urls'] a[target='_blank']" + "element": "[data-element='editor-content'] [data-element='node-info-urls']", + "title": "Open page", + "content": "

Click the Link to document to view your page.

Tip: Click the preview button in the bottom right corner to preview changes without publishing them.

", + "event": "click", + "eventElement": "[data-element='editor-content'] [data-element='node-info-urls'] a[target='_blank']" } ] }, @@ -335,84 +335,84 @@ "group": "Getting Started", "steps": [ { - title: "How to use the media library", - content: "

A website would be boring without media content. In Umbraco you can manage all your images, documents, videos etc. in the Media section. Here you can upload and organise your media items and see details about each item.

In this tour you will learn how to upload and organise your Media library in Umbraco. It will also show you how to view details about a specific media item.

", - type: "intro" + "title": "How to use the media library", + "content": "

A website would be boring without media content. In Umbraco you can manage all your images, documents, videos etc. in the Media section. Here you can upload and organise your media items and see details about each item.

In this tour you will learn how to upload and organise your Media library in Umbraco. It will also show you how to view details about a specific media item.

", + "type": "intro" }, { - element: "#applications [data-element='section-media']", - title: "Navigate to the Media section", - content: "The media section is where you manage all your media items.", - event: "click", - backdropOpacity: 0.6 + "element": "#applications [data-element='section-media']", + "title": "Navigate to the Media section", + "content": "The media section is where you manage all your media items.", + "event": "click", + "backdropOpacity": 0.6 }, { - element: "#tree [data-element='tree-root']", - title: "Create a new folder", - content: "

First create a folder for your images. Hover the media root node and click the three small dots on the right side of the item.

", - event: "click", - eventElement: "#tree [data-element='tree-root'] [data-element='tree-item-options']" + "element": "#tree [data-element='tree-root']", + "title": "Create a new folder", + "content": "

First create a folder for your images. Hover the media root node and click the three small dots on the right side of the item.

", + "event": "click", + "eventElement": "#tree [data-element='tree-root'] [data-element='tree-item-options']" }, { - element: "#dialog [data-element='action-Folder']", - title: "Create a new folder", - content: "

Select the Folder option to select the type folder.

", - event: "click" + "element": "#dialog [data-element='action-Folder']", + "title": "Create a new folder", + "content": "

Select the Folder option to select the type folder.

", + "event": "click" }, { - element: "[data-element='editor-media'] [data-element='editor-name-field']", - title: "Enter a name", - content: "

Enter My Images in the field.

" + "element": "[data-element='editor-media'] [data-element='editor-name-field']", + "title": "Enter a name", + "content": "

Enter My Images in the field.

" }, { - element: "[data-element='editor-media'] [data-element='button-save']", - title: "Save the folder", - content: "

Click the Save button to create the new folder

", - event: "click" + "element": "[data-element='editor-media'] [data-element='button-save']", + "title": "Save the folder", + "content": "

Click the Save button to create the new folder

", + "event": "click" }, { - element: "[data-element='editor-media'] [data-element='dropzone']", - title: "Upload images", - content: "

In the upload area you can upload your media items.

Click the Click here to choose files-button and select a couple of images on your computer and upload them.

", - view: "uploadimages" + "element": "[data-element='editor-media'] [data-element='dropzone']", + "title": "Upload images", + "content": "

In the upload area you can upload your media items.

Click the Click here to choose files-button and select a couple of images on your computer and upload them.

", + "view": "uploadimages" }, { - element: "[data-element='editor-media'] [data-element='media-grid-item-0']", - title: "View media item details", - content: "Hover the media item and Click the purple bar to view details about the media item", - event: "click", - eventElement: "[data-element='editor-media'] [data-element='media-grid-item-0'] [data-element='media-grid-item-edit']" + "element": "[data-element='editor-media'] [data-element='media-grid-item-0']", + "title": "View media item details", + "content": "Hover the media item and Click the purple bar to view details about the media item", + "event": "click", + "eventElement": "[data-element='editor-media'] [data-element='media-grid-item-0'] [data-element='media-grid-item-edit']" }, { - element: "[data-element='editor-media'] [data-element='property-umbracoFile']", - title: "The uploaded image", - content: "

Here you can see the image you have uploaded.

" + "element": "[data-element='editor-media'] [data-element='property-umbracoFile']", + "title": "The uploaded image", + "content": "

Here you can see the image you have uploaded.

" }, { - element: "[data-element='editor-media'] [data-element='property-umbracoBytes']", - title: "Image size", - content: "

You will also find other details about the image, like the size.

Media items work in much the same way as content. So you can add extra properties to an image by creating or editing the Media types in the Settings section.

" + "element": "[data-element='editor-media'] [data-element='property-umbracoBytes']", + "title": "Image size", + "content": "

You will also find other details about the image, like the size.

Media items work in much the same way as content. So you can add extra properties to an image by creating or editing the Media types in the Settings section.

" }, { - element: "[data-element='editor-media'] [data-element='tab-_umb_infoTab']", - title: "Info", - content: "Like the content section you can also find default information about the media item. You will find these under the info tab.", - event: "click" + "element": "[data-element='editor-media'] [data-element='tab-_umb_infoTab']", + "title": "Info", + "content": "Like the content section you can also find default information about the media item. You will find these under the info tab.", + "event": "click" }, { - element: "[data-element='editor-media'] [data-element='node-info-urls']", - title: "Link to media", - content: "The path to the media item..." + "element": "[data-element='editor-media'] [data-element='node-info-urls']", + "title": "Link to media", + "content": "The path to the media item..." }, { - element: "[data-element='editor-media'] [data-element='node-info-update-date']", - title: "Last edited", - content: "...and information about when the media item has been created and edited." + "element": "[data-element='editor-media'] [data-element='node-info-update-date']", + "title": "Last edited", + "content": "...and information about when the media item has been created and edited." }, { - element: "[data-element='editor-container']", - title: "Using media items", - content: "You can reference a media item directly in a template by using the path or try adding a Media Picker to a document type property so you can select media items from the content section." + "element": "[data-element='editor-container']", + "title": "Using media items", + "content": "You can reference a media item directly in a template by using the path or try adding a Media Picker to a document type property so you can select media items from the content section." } ] } From 0cfd5773c3c8affbf6da1a21e5fdbd6f54b82146 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Tue, 9 Jan 2018 14:12:33 +0100 Subject: [PATCH 14/31] set groupOrder on group so we don't order the tours in each group --- .../src/common/services/tour.service.js | 10 +++++++--- .../src/views/common/drawers/help/help.html | 2 +- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/services/tour.service.js b/src/Umbraco.Web.UI.Client/src/common/services/tour.service.js index 5ad997ce76..97e6f4a3b7 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/tour.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/tour.service.js @@ -219,9 +219,7 @@ var tours = getTours(); setTourStatuses(tours).then(function() { var groupedTours = []; - var sortedTours = _.sortBy(tours, 'groupOrder'); - - sortedTours.forEach(function (item) { + tours.forEach(function (item) { var groupExists = false; var newGroup = { @@ -232,6 +230,9 @@ groupedTours.forEach(function(group){ // extend existing group if it is already added if(group.group === item.group) { + if(item.groupOrder) { + group.groupOrder = item.groupOrder + } groupExists = true; group.tours.push(item) } @@ -240,6 +241,9 @@ // push new group to array if it doesn't exist if(!groupExists) { newGroup.group = item.group; + if(item.groupOrder) { + newGroup.groupOrder = item.groupOrder + } newGroup.tours.push(item); groupedTours.push(newGroup); } diff --git a/src/Umbraco.Web.UI.Client/src/views/common/drawers/help/help.html b/src/Umbraco.Web.UI.Client/src/views/common/drawers/help/help.html index e755512b02..58f091a9ab 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/drawers/help/help.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/drawers/help/help.html @@ -12,7 +12,7 @@
Tours
-
+
From ea70e18a916075b045c88f88f4969df1f18c9f28 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Tue, 9 Jan 2018 14:13:27 +0100 Subject: [PATCH 15/31] set "Getting Started"-tour group to order 100 so it is possible be add groups before and after --- .../config/BackOfficeTours/getting-started.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Umbraco.Web.UI/config/BackOfficeTours/getting-started.json b/src/Umbraco.Web.UI/config/BackOfficeTours/getting-started.json index 72c11f4937..1617bba0cb 100644 --- a/src/Umbraco.Web.UI/config/BackOfficeTours/getting-started.json +++ b/src/Umbraco.Web.UI/config/BackOfficeTours/getting-started.json @@ -3,6 +3,7 @@ "name": "Introduction", "alias": "umbIntroIntroduction", "group": "Getting Started", + "groupOrder": 100, "allowDisable": true, "steps": [ { @@ -86,6 +87,7 @@ "name": "Create document type", "alias": "umbIntroCreateDocType", "group": "Getting Started", + "groupOrder": 100, "steps": [ { "title": "Create your first Document Type", @@ -200,6 +202,7 @@ "name": "Create Content", "alias": "umbIntroCreateContent", "group": "Getting Started", + "groupOrder": 100, "steps": [ { "title": "Creating your first content node", @@ -249,6 +252,7 @@ "name": "Render in template", "alias": "umbIntroRenderInTemplate", "group": "Getting Started", + "groupOrder": 100, "steps": [ { "title": "Render your content in a template", @@ -294,6 +298,7 @@ "name": "View Home page", "alias": "umbIntroViewHomePage", "group": "Getting Started", + "groupOrder": 100, "steps": [ { "title": "View your Umbraco site", @@ -333,6 +338,7 @@ "name": "The Media library", "alias": "umbIntroMediaSection", "group": "Getting Started", + "groupOrder": 100, "steps": [ { "title": "How to use the media library", From a5ec5f46ac74f15575e6ea53c81db6d8a37fa9b8 Mon Sep 17 00:00:00 2001 From: Claus Date: Tue, 9 Jan 2018 15:49:19 +0100 Subject: [PATCH 16/31] filtering out tours. --- src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 1 + src/Umbraco.Web/Editors/TourController.cs | 47 ++++++++++++++----- src/Umbraco.Web/Editors/TourFilterResolver.cs | 33 +++++++++++++ src/Umbraco.Web/Models/Tour.cs | 22 +++++++++ src/Umbraco.Web/Models/TourStep.cs | 18 +++++++ src/Umbraco.Web/Umbraco.Web.csproj | 3 ++ 6 files changed, 111 insertions(+), 13 deletions(-) create mode 100644 src/Umbraco.Web/Editors/TourFilterResolver.cs create mode 100644 src/Umbraco.Web/Models/Tour.cs create mode 100644 src/Umbraco.Web/Models/TourStep.cs diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index f522fb467b..49bc478461 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -548,6 +548,7 @@ HealthChecks.config Designer + umbracoSettings.config Designer diff --git a/src/Umbraco.Web/Editors/TourController.cs b/src/Umbraco.Web/Editors/TourController.cs index ad9b6b3430..ed1ed4c9c8 100644 --- a/src/Umbraco.Web/Editors/TourController.cs +++ b/src/Umbraco.Web/Editors/TourController.cs @@ -1,9 +1,13 @@ using System; +using System.Collections.Generic; using System.IO; using System.Linq; -using Newtonsoft.Json.Linq; +using Newtonsoft.Json; +using Umbraco.Core; using Umbraco.Core.Configuration; using Umbraco.Core.IO; +using Umbraco.Core.Logging; +using Umbraco.Web.Models; using Umbraco.Web.Mvc; using Umbraco.Web.WebApi.Filters; using Constants = Umbraco.Core.Constants; @@ -11,29 +15,46 @@ using Constants = Umbraco.Core.Constants; namespace Umbraco.Web.Editors { [PluginController("UmbracoApi")] - [UmbracoApplicationAuthorize(Constants.Applications.Content)] + [UmbracoApplicationAuthorize(Constants.Applications.Content)] public class TourController : UmbracoAuthorizedJsonController { - //TODO: Strongly type this for final release! - public JArray GetTours() + public IEnumerable GetTours() { - //TODO: Add error checking to this for final release! - - var result = new JArray(); + var tours = new List(); if (UmbracoConfig.For.UmbracoSettings().BackOffice.Tours.EnableTours == false) - return result; + return tours; - var tourFiles = Directory.GetFiles( - Path.Combine(IOHelper.MapPath(SystemDirectories.Config), "BackOfficeTours"), "*.json") + var toursPath = Path.Combine(IOHelper.MapPath(SystemDirectories.Config), "BackOfficeTours"); + if (Directory.Exists(toursPath) == false) + return tours; + + var tourFiles = Directory.GetFiles(toursPath, "*.json") .OrderBy(x => x, StringComparer.InvariantCultureIgnoreCase); + var disabledTours = TourFilterResolver.Current.DisabledTours; + foreach (var tourFile in tourFiles) { - var contents = File.ReadAllText(tourFile); - result.Add(JArray.Parse(contents)); + try + { + var contents = File.ReadAllText(tourFile); + var tourArray = JsonConvert.DeserializeObject(contents); + tours.Add(tourArray.Where(x => + disabledTours.Contains(x.Alias, StringComparer.InvariantCultureIgnoreCase) == false).ToArray()); + } + catch (IOException e) + { + Logger.Error("Error while trying to read file: " + tourFile, e); + throw new IOException("Error while trying to read file: " + tourFile, e); + } + catch (JsonReaderException e) + { + Logger.Error("Error while trying to parse content as tour data: " + tourFile, e); + throw new JsonReaderException("Error while trying to parse content as tour data: " + tourFile, e); + } } - return result; + return tours; } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Editors/TourFilterResolver.cs b/src/Umbraco.Web/Editors/TourFilterResolver.cs new file mode 100644 index 0000000000..5eafc19b3c --- /dev/null +++ b/src/Umbraco.Web/Editors/TourFilterResolver.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using System.Linq; + +namespace Umbraco.Web.Editors +{ + //TODO: find out where this should live + public class TourFilterResolver + { + private static TourFilterResolver _current; + + private readonly HashSet _disabledTours; + + public TourFilterResolver() + { + _disabledTours = new HashSet(); + } + + public static TourFilterResolver Current + { + get { return _current ?? (_current = new TourFilterResolver()); } + } + + public void Disable(string tour) + { + _disabledTours.Add(tour); + } + + public IEnumerable DisabledTours + { + get { return _disabledTours.ToArray(); } + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Models/Tour.cs b/src/Umbraco.Web/Models/Tour.cs new file mode 100644 index 0000000000..f130ccfc24 --- /dev/null +++ b/src/Umbraco.Web/Models/Tour.cs @@ -0,0 +1,22 @@ +using System.Runtime.Serialization; + +namespace Umbraco.Web.Models +{ + /// + /// A model representing a tour. + /// + [DataContract(Name = "tour", Namespace = "")] + public class Tour + { + [DataMember(Name = "name")] + public string Name { get; set; } + [DataMember(Name = "alias")] + public string Alias { get; set; } + [DataMember(Name = "group")] + public string Group { get; set; } + [DataMember(Name = "groupOrder")] + public int GroupOrder { get; set; } + [DataMember(Name = "steps")] + public TourStep[] Steps { get; set; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Models/TourStep.cs b/src/Umbraco.Web/Models/TourStep.cs new file mode 100644 index 0000000000..8ed75872d9 --- /dev/null +++ b/src/Umbraco.Web/Models/TourStep.cs @@ -0,0 +1,18 @@ +using System.Runtime.Serialization; + +namespace Umbraco.Web.Models +{ + /// + /// A model representing a step in a tour. + /// + [DataContract(Name = "step", Namespace = "")] + public class TourStep + { + [DataMember(Name = "title")] + public string Title { get; set; } + [DataMember(Name = "content")] + public string Content { get; set; } + [DataMember(Name = "type")] + public string Type { get; set; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 326ed3baa9..956071aa40 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -328,6 +328,7 @@ + @@ -428,6 +429,8 @@ + + From 3858656015efaecf01abfdfa41950af661ae2525 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Tue, 9 Jan 2018 16:15:38 +0100 Subject: [PATCH 17/31] clear tours before registering new tours --- src/Umbraco.Web.UI.Client/src/common/services/tour.service.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Umbraco.Web.UI.Client/src/common/services/tour.service.js b/src/Umbraco.Web.UI.Client/src/common/services/tour.service.js index 97e6f4a3b7..6b43eddae3 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/tour.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/tour.service.js @@ -17,6 +17,7 @@ * Registers all tours from the server and returns a promise */ function registerAllTours() { + tours = []; return tourResource.getTours().then(function(tourFiles) { angular.forEach(tourFiles, function (tourFile) { angular.forEach(tourFile, function(newTour) { From eb94572361e7c116f8760b4e9ad6dd482fd89eb3 Mon Sep 17 00:00:00 2001 From: Claus Date: Wed, 10 Jan 2018 10:55:46 +0100 Subject: [PATCH 18/31] updating pr to fit with base. --- src/Umbraco.Web/Editors/TourController.cs | 78 +++++++++---------- src/Umbraco.Web/Editors/TourFilterResolver.cs | 2 +- src/Umbraco.Web/Models/BackOfficeTour.cs | 11 +-- src/Umbraco.Web/Models/BackOfficeTourFile.cs | 3 + src/Umbraco.Web/Models/BackOfficeTourStep.cs | 3 + src/Umbraco.Web/Models/Tour.cs | 22 ------ src/Umbraco.Web/Models/TourStep.cs | 18 ----- src/Umbraco.Web/Umbraco.Web.csproj | 2 - 8 files changed, 51 insertions(+), 88 deletions(-) delete mode 100644 src/Umbraco.Web/Models/Tour.cs delete mode 100644 src/Umbraco.Web/Models/TourStep.cs diff --git a/src/Umbraco.Web/Editors/TourController.cs b/src/Umbraco.Web/Editors/TourController.cs index fa1f2a1d2d..2addf0b07e 100644 --- a/src/Umbraco.Web/Editors/TourController.cs +++ b/src/Umbraco.Web/Editors/TourController.cs @@ -2,11 +2,10 @@ using System.Collections.Generic; using System.IO; using System.Linq; -using System.Text.RegularExpressions; using Newtonsoft.Json; -using Newtonsoft.Json.Linq; using Umbraco.Core.Configuration; using Umbraco.Core.IO; +using Umbraco.Core.Logging; using Umbraco.Web.Models; using Umbraco.Web.Mvc; using Umbraco.Web.WebApi.Filters; @@ -25,43 +24,19 @@ namespace Umbraco.Web.Editors if (UmbracoConfig.For.UmbracoSettings().BackOffice.Tours.EnableTours == false) return result; - var toursPath = Path.Combine(IOHelper.MapPath(SystemDirectories.Config), "BackOfficeTours"); - if (Directory.Exists(toursPath) == false) - return result; - - var tourFiles = Directory.GetFiles(toursPath, "*.json") - .OrderBy(x => x, StringComparer.InvariantCultureIgnoreCase); - var disabledTours = TourFilterResolver.Current.DisabledTours; - - var coreTourFiles = Directory.GetFiles( - Path.Combine(IOHelper.MapPath(SystemDirectories.Config), "BackOfficeTours"), "*.json"); - - foreach (var tourFile in coreTourFiles) + //add core tour files + var coreToursPath = Path.Combine(IOHelper.MapPath(SystemDirectories.Config), "BackOfficeTours"); + if (Directory.Exists(coreToursPath)) { - try - { - var contents = File.ReadAllText(tourFile); + var coreTourFiles = Directory.GetFiles(coreToursPath, "*.json"); - result.Add(new BackOfficeTourFile + foreach (var tourFile in coreTourFiles) { - FileName = Path.GetFileNameWithoutExtension(tourFile), - Tours = JsonConvert.DeserializeObject(contents) - }); + TryParseTourFile(tourFile, result); } - catch (IOException e) - { - Logger.Error("Error while trying to read file: " + tourFile, e); - throw new IOException("Error while trying to read file: " + tourFile, e); - } - catch (JsonReaderException e) - { - Logger.Error("Error while trying to parse content as tour data: " + tourFile, e); - throw new JsonReaderException("Error while trying to parse content as tour data: " + tourFile, e); - } - } - //collect all tour files in packges + //collect all tour files in packages foreach (var plugin in Directory.EnumerateDirectories(IOHelper.MapPath(SystemDirectories.AppPlugins))) { var pluginName = Path.GetFileName(plugin.TrimEnd('\\')); @@ -72,13 +47,7 @@ namespace Umbraco.Web.Editors { foreach (var tourFile in Directory.EnumerateFiles(tourDir, "*.json")) { - var contents = File.ReadAllText(tourFile); - result.Add(new BackOfficeTourFile - { - FileName = Path.GetFileNameWithoutExtension(tourFile), - PluginName = pluginName, - Tours = JsonConvert.DeserializeObject(contents) - }); + TryParseTourFile(tourFile, result, pluginName); } } } @@ -86,5 +55,34 @@ namespace Umbraco.Web.Editors return result.OrderBy(x => x.FileName, StringComparer.InvariantCultureIgnoreCase); } + + private void TryParseTourFile(string tourFile, List result, string pluginName = null) + { + try + { + var contents = File.ReadAllText(tourFile); + var tours = JsonConvert.DeserializeObject(contents); + var disabledTours = TourFilterResolver.Current.DisabledTours; + + result.Add(new BackOfficeTourFile + { + FileName = Path.GetFileNameWithoutExtension(tourFile), + PluginName = pluginName, + Tours = tours + .Where(x => disabledTours.Contains(x.Alias, StringComparer.InvariantCultureIgnoreCase) == false) + .ToArray() + }); + } + catch (IOException e) + { + Logger.Error("Error while trying to read file: " + tourFile, e); + throw new IOException("Error while trying to read file: " + tourFile, e); + } + catch (JsonReaderException e) + { + Logger.Error("Error while trying to parse content as tour data: " + tourFile, e); + throw new JsonReaderException("Error while trying to parse content as tour data: " + tourFile, e); + } + } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Editors/TourFilterResolver.cs b/src/Umbraco.Web/Editors/TourFilterResolver.cs index 5eafc19b3c..238df8c6c1 100644 --- a/src/Umbraco.Web/Editors/TourFilterResolver.cs +++ b/src/Umbraco.Web/Editors/TourFilterResolver.cs @@ -25,7 +25,7 @@ namespace Umbraco.Web.Editors _disabledTours.Add(tour); } - public IEnumerable DisabledTours + public string[] DisabledTours { get { return _disabledTours.ToArray(); } } diff --git a/src/Umbraco.Web/Models/BackOfficeTour.cs b/src/Umbraco.Web/Models/BackOfficeTour.cs index 1e0f345b52..a973a92429 100644 --- a/src/Umbraco.Web/Models/BackOfficeTour.cs +++ b/src/Umbraco.Web/Models/BackOfficeTour.cs @@ -1,11 +1,10 @@ -using System; -using System.Linq; -using System.Runtime.Serialization; -using System.Text; -using System.Threading.Tasks; +using System.Runtime.Serialization; namespace Umbraco.Web.Models { + /// + /// A model representing a tour. + /// [DataContract(Name = "tour", Namespace = "")] public class BackOfficeTour { @@ -15,6 +14,8 @@ namespace Umbraco.Web.Models public string Alias { get; set; } [DataMember(Name = "group")] public string Group { get; set; } + [DataMember(Name = "groupOrder")] + public int GroupOrder { get; set; } [DataMember(Name = "allowDisable")] public bool AllowDisable { get; set; } [DataMember(Name = "steps")] diff --git a/src/Umbraco.Web/Models/BackOfficeTourFile.cs b/src/Umbraco.Web/Models/BackOfficeTourFile.cs index 69b35c8088..7291a89ff4 100644 --- a/src/Umbraco.Web/Models/BackOfficeTourFile.cs +++ b/src/Umbraco.Web/Models/BackOfficeTourFile.cs @@ -3,6 +3,9 @@ using System.Runtime.Serialization; namespace Umbraco.Web.Models { + /// + /// A model representing the file used to load a tour. + /// [DataContract(Name = "tourFile", Namespace = "")] public class BackOfficeTourFile { diff --git a/src/Umbraco.Web/Models/BackOfficeTourStep.cs b/src/Umbraco.Web/Models/BackOfficeTourStep.cs index e0371bf4b5..55af21cfd4 100644 --- a/src/Umbraco.Web/Models/BackOfficeTourStep.cs +++ b/src/Umbraco.Web/Models/BackOfficeTourStep.cs @@ -2,6 +2,9 @@ namespace Umbraco.Web.Models { + /// + /// A model representing a step in a tour. + /// [DataContract(Name = "step", Namespace = "")] public class BackOfficeTourStep { diff --git a/src/Umbraco.Web/Models/Tour.cs b/src/Umbraco.Web/Models/Tour.cs deleted file mode 100644 index f130ccfc24..0000000000 --- a/src/Umbraco.Web/Models/Tour.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System.Runtime.Serialization; - -namespace Umbraco.Web.Models -{ - /// - /// A model representing a tour. - /// - [DataContract(Name = "tour", Namespace = "")] - public class Tour - { - [DataMember(Name = "name")] - public string Name { get; set; } - [DataMember(Name = "alias")] - public string Alias { get; set; } - [DataMember(Name = "group")] - public string Group { get; set; } - [DataMember(Name = "groupOrder")] - public int GroupOrder { get; set; } - [DataMember(Name = "steps")] - public TourStep[] Steps { get; set; } - } -} \ No newline at end of file diff --git a/src/Umbraco.Web/Models/TourStep.cs b/src/Umbraco.Web/Models/TourStep.cs deleted file mode 100644 index 8ed75872d9..0000000000 --- a/src/Umbraco.Web/Models/TourStep.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.Runtime.Serialization; - -namespace Umbraco.Web.Models -{ - /// - /// A model representing a step in a tour. - /// - [DataContract(Name = "step", Namespace = "")] - public class TourStep - { - [DataMember(Name = "title")] - public string Title { get; set; } - [DataMember(Name = "content")] - public string Content { get; set; } - [DataMember(Name = "type")] - public string Type { get; set; } - } -} \ No newline at end of file diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 089f024021..d37e4972fd 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -431,9 +431,7 @@ - - From c6443c477fbccdd84e3728b29657977eb7df2e75 Mon Sep 17 00:00:00 2001 From: Claus Date: Wed, 10 Jan 2018 11:13:28 +0100 Subject: [PATCH 19/31] no need to log when throwing - that gets logged. --- src/Umbraco.Web/Editors/TourController.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Umbraco.Web/Editors/TourController.cs b/src/Umbraco.Web/Editors/TourController.cs index 2addf0b07e..5e43b56159 100644 --- a/src/Umbraco.Web/Editors/TourController.cs +++ b/src/Umbraco.Web/Editors/TourController.cs @@ -75,12 +75,10 @@ namespace Umbraco.Web.Editors } catch (IOException e) { - Logger.Error("Error while trying to read file: " + tourFile, e); throw new IOException("Error while trying to read file: " + tourFile, e); } catch (JsonReaderException e) { - Logger.Error("Error while trying to parse content as tour data: " + tourFile, e); throw new JsonReaderException("Error while trying to parse content as tour data: " + tourFile, e); } } From ce11a377e6b8d91a6a9054efac99626e9299cbd9 Mon Sep 17 00:00:00 2001 From: Claus Date: Wed, 10 Jan 2018 11:37:37 +0100 Subject: [PATCH 20/31] refactor --- src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 1 - src/Umbraco.Web/Editors/TourController.cs | 1 - src/Umbraco.Web/{Editors => }/TourFilterResolver.cs | 3 +-- src/Umbraco.Web/Umbraco.Web.csproj | 2 +- 4 files changed, 2 insertions(+), 5 deletions(-) rename src/Umbraco.Web/{Editors => }/TourFilterResolver.cs (90%) diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index dc9939c422..c3d655f194 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -548,7 +548,6 @@ HealthChecks.config Designer - umbracoSettings.config Designer diff --git a/src/Umbraco.Web/Editors/TourController.cs b/src/Umbraco.Web/Editors/TourController.cs index 5e43b56159..3355093659 100644 --- a/src/Umbraco.Web/Editors/TourController.cs +++ b/src/Umbraco.Web/Editors/TourController.cs @@ -5,7 +5,6 @@ using System.Linq; using Newtonsoft.Json; using Umbraco.Core.Configuration; using Umbraco.Core.IO; -using Umbraco.Core.Logging; using Umbraco.Web.Models; using Umbraco.Web.Mvc; using Umbraco.Web.WebApi.Filters; diff --git a/src/Umbraco.Web/Editors/TourFilterResolver.cs b/src/Umbraco.Web/TourFilterResolver.cs similarity index 90% rename from src/Umbraco.Web/Editors/TourFilterResolver.cs rename to src/Umbraco.Web/TourFilterResolver.cs index 238df8c6c1..d30c6b6093 100644 --- a/src/Umbraco.Web/Editors/TourFilterResolver.cs +++ b/src/Umbraco.Web/TourFilterResolver.cs @@ -1,9 +1,8 @@ using System.Collections.Generic; using System.Linq; -namespace Umbraco.Web.Editors +namespace Umbraco.Web { - //TODO: find out where this should live public class TourFilterResolver { private static TourFilterResolver _current; diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index d37e4972fd..8046e10ff8 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -328,7 +328,7 @@ - + From d0d4885812c2bc77798e7f689a03dc9a59f54d12 Mon Sep 17 00:00:00 2001 From: Robert Date: Wed, 10 Jan 2018 13:36:53 +0100 Subject: [PATCH 21/31] Created slim version of DisposableObject and fixed some code indentation DisposableObjectSlim has no finalizer --- src/Umbraco.Core/DisposableObject.cs | 90 ++++++++++++------------ src/Umbraco.Core/DisposableObjectSlim.cs | 42 +++++++++++ 2 files changed, 87 insertions(+), 45 deletions(-) create mode 100644 src/Umbraco.Core/DisposableObjectSlim.cs diff --git a/src/Umbraco.Core/DisposableObject.cs b/src/Umbraco.Core/DisposableObject.cs index 516a9712e5..71477996ef 100644 --- a/src/Umbraco.Core/DisposableObject.cs +++ b/src/Umbraco.Core/DisposableObject.cs @@ -2,60 +2,60 @@ using System; using System.Threading; namespace Umbraco.Core -{ - /// - /// Abstract implementation of IDisposable. - /// - /// - /// Can also be used as a pattern for when inheriting is not possible. - /// +{ + /// + /// Abstract implementation of IDisposable. + /// + /// + /// Can also be used as a pattern for when inheriting is not possible. + /// /// See also: https://msdn.microsoft.com/en-us/library/b1yfkh5e%28v=vs.110%29.aspx /// See also: https://lostechies.com/chrispatterson/2012/11/29/idisposable-done-right/ /// /// Note: if an object's ctor throws, it will never be disposed, and so if that ctor /// has allocated disposable objects, it should take care of disposing them. - /// - public abstract class DisposableObject : IDisposable - { - private bool _disposed; - private readonly object _locko = new object(); - - // gets a value indicating whether this instance is disposed. - // for internal tests only (not thread safe) - //TODO make this internal + rename "Disposed" when we can break compatibility - public bool IsDisposed { get { return _disposed; } } - - // implements IDisposable - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - // finalizer - ~DisposableObject() - { - Dispose(false); - } + /// + public abstract class DisposableObject : IDisposable + { + private bool _disposed; + private readonly object _locko = new object(); + + // gets a value indicating whether this instance is disposed. + // for internal tests only (not thread safe) + //TODO make this internal + rename "Disposed" when we can break compatibility + public bool IsDisposed { get { return _disposed; } } + + // implements IDisposable + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + // finalizer + ~DisposableObject() + { + Dispose(false); + } //TODO make this private, non-virtual when we can break compatibility - protected virtual void Dispose(bool disposing) - { - lock (_locko) - { - if (_disposed) return; - _disposed = true; - } + protected virtual void Dispose(bool disposing) + { + lock (_locko) + { + if (_disposed) return; + _disposed = true; + } DisposeUnmanagedResources(); if (disposing) - DisposeResources(); - } - - protected abstract void DisposeResources(); - - protected virtual void DisposeUnmanagedResources() - { } - } + DisposeResources(); + } + + protected abstract void DisposeResources(); + + protected virtual void DisposeUnmanagedResources() + { } + } } \ No newline at end of file diff --git a/src/Umbraco.Core/DisposableObjectSlim.cs b/src/Umbraco.Core/DisposableObjectSlim.cs new file mode 100644 index 0000000000..bcec6165db --- /dev/null +++ b/src/Umbraco.Core/DisposableObjectSlim.cs @@ -0,0 +1,42 @@ +using System; + +namespace Umbraco.Core +{ + public abstract class DisposableObjectSlim : IDisposable + { + private bool _disposed; + private readonly object _locko = new object(); + + // gets a value indicating whether this instance is disposed. + // for internal tests only (not thread safe) + //TODO make this internal + rename "Disposed" when we can break compatibility + public bool IsDisposed { get { return _disposed; } } + + // implements IDisposable + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + //TODO make this private, non-virtual when we can break compatibility + protected virtual void Dispose(bool disposing) + { + lock (_locko) + { + if (_disposed) return; + _disposed = true; + } + + DisposeUnmanagedResources(); + + if (disposing) + DisposeResources(); + } + + protected abstract void DisposeResources(); + + protected virtual void DisposeUnmanagedResources() + { } + } +} From bba58ea0c0bd493f9db24d313b1333db3e64ef8d Mon Sep 17 00:00:00 2001 From: Robert Date: Wed, 10 Jan 2018 13:48:55 +0100 Subject: [PATCH 22/31] Replaced all usages of DisposableObject with DisposableObjectSlim --- .../Cache/HttpRequestCacheProvider.cs | 2 +- src/Umbraco.Core/DisposableTimer.cs | 16 ++++++++-------- src/Umbraco.Core/Events/EventMessages.cs | 2 +- src/Umbraco.Core/HashGenerator.cs | 2 +- src/Umbraco.Core/Manifest/ManifestWatcher.cs | 2 +- .../Persistence/DefaultDatabaseFactory.cs | 2 +- .../Persistence/Repositories/EntityRepository.cs | 2 +- .../Persistence/Repositories/FileRepository.cs | 2 +- .../Persistence/Repositories/RepositoryBase.cs | 2 +- .../Persistence/UnitOfWork/ScopeUnitOfWork.cs | 2 +- src/Umbraco.Core/Security/BackOfficeUserStore.cs | 4 ++-- src/Umbraco.Core/Umbraco.Core.csproj | 1 + .../Scheduling/LatchedBackgroundTaskBase.cs | 2 +- src/Umbraco.Web/Security/WebSecurity.cs | 2 +- src/Umbraco.Web/UmbracoContext.cs | 2 +- .../Packager/Repositories/Repository.cs | 4 ++-- 16 files changed, 25 insertions(+), 24 deletions(-) diff --git a/src/Umbraco.Core/Cache/HttpRequestCacheProvider.cs b/src/Umbraco.Core/Cache/HttpRequestCacheProvider.cs index 792b5982b1..ac64e88e8a 100644 --- a/src/Umbraco.Core/Cache/HttpRequestCacheProvider.cs +++ b/src/Umbraco.Core/Cache/HttpRequestCacheProvider.cs @@ -146,7 +146,7 @@ namespace Umbraco.Core.Cache #region Insert #endregion - private class NoopLocker : DisposableObject + private class NoopLocker : DisposableObjectSlim { protected override void DisposeResources() { } diff --git a/src/Umbraco.Core/DisposableTimer.cs b/src/Umbraco.Core/DisposableTimer.cs index c7e8874449..dab99584f3 100644 --- a/src/Umbraco.Core/DisposableTimer.cs +++ b/src/Umbraco.Core/DisposableTimer.cs @@ -11,8 +11,8 @@ namespace Umbraco.Core /// /// Starts the timer and invokes a callback upon disposal. Provides a simple way of timing an operation by wrapping it in a using (C#) statement. /// - public class DisposableTimer : DisposableObject - { + public class DisposableTimer : DisposableObjectSlim + { private readonly ILogger _logger; private readonly LogType? _logType; private readonly IProfiler _profiler; @@ -209,13 +209,13 @@ namespace Umbraco.Core loggerType, startMessage(), completeMessage()); - } + } #endregion - - /// - /// Handles the disposal of resources. Derived from abstract class which handles common required locking logic. - /// - protected override void DisposeResources() + + /// + /// Handles the disposal of resources. Derived from abstract class which handles common required locking logic. + /// + protected override void DisposeResources() { if (_profiler != null) { diff --git a/src/Umbraco.Core/Events/EventMessages.cs b/src/Umbraco.Core/Events/EventMessages.cs index 2900f3d471..6348fc0444 100644 --- a/src/Umbraco.Core/Events/EventMessages.cs +++ b/src/Umbraco.Core/Events/EventMessages.cs @@ -5,7 +5,7 @@ namespace Umbraco.Core.Events /// /// Event messages collection /// - public sealed class EventMessages : DisposableObject + public sealed class EventMessages : DisposableObjectSlim { private readonly List _msgs = new List(); diff --git a/src/Umbraco.Core/HashGenerator.cs b/src/Umbraco.Core/HashGenerator.cs index 7306dc9045..bbb06987f2 100644 --- a/src/Umbraco.Core/HashGenerator.cs +++ b/src/Umbraco.Core/HashGenerator.cs @@ -14,7 +14,7 @@ namespace Umbraco.Core /// This will use the crypto libs to generate the hash and will try to ensure that /// strings, etc... are not re-allocated so it's not consuming much memory. /// - internal class HashGenerator : DisposableObject + internal class HashGenerator : DisposableObjectSlim { public HashGenerator() { diff --git a/src/Umbraco.Core/Manifest/ManifestWatcher.cs b/src/Umbraco.Core/Manifest/ManifestWatcher.cs index 24dff148be..e18ec068b6 100644 --- a/src/Umbraco.Core/Manifest/ManifestWatcher.cs +++ b/src/Umbraco.Core/Manifest/ManifestWatcher.cs @@ -6,7 +6,7 @@ using Umbraco.Core.Logging; namespace Umbraco.Core.Manifest { - internal class ManifestWatcher : DisposableObject + internal class ManifestWatcher : DisposableObjectSlim { private readonly ILogger _logger; private readonly List _fws = new List(); diff --git a/src/Umbraco.Core/Persistence/DefaultDatabaseFactory.cs b/src/Umbraco.Core/Persistence/DefaultDatabaseFactory.cs index c27147895c..46c7dae1d9 100644 --- a/src/Umbraco.Core/Persistence/DefaultDatabaseFactory.cs +++ b/src/Umbraco.Core/Persistence/DefaultDatabaseFactory.cs @@ -12,7 +12,7 @@ namespace Umbraco.Core.Persistence /// it will create one per context, otherwise it will be a global singleton object which is NOT thread safe /// since we need (at least) a new instance of the database object per thread. /// - internal class DefaultDatabaseFactory : DisposableObject, IDatabaseFactory2 + internal class DefaultDatabaseFactory : DisposableObjectSlim, IDatabaseFactory2 { private readonly string _connectionStringName; private readonly ILogger _logger; diff --git a/src/Umbraco.Core/Persistence/Repositories/EntityRepository.cs b/src/Umbraco.Core/Persistence/Repositories/EntityRepository.cs index 8f3b65595b..40d35d2fee 100644 --- a/src/Umbraco.Core/Persistence/Repositories/EntityRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/EntityRepository.cs @@ -18,7 +18,7 @@ namespace Umbraco.Core.Persistence.Repositories /// /// This is limited to objects that are based in the umbracoNode-table. /// - internal class EntityRepository : DisposableObject, IEntityRepository + internal class EntityRepository : DisposableObjectSlim, IEntityRepository { private readonly IDatabaseUnitOfWork _work; diff --git a/src/Umbraco.Core/Persistence/Repositories/FileRepository.cs b/src/Umbraco.Core/Persistence/Repositories/FileRepository.cs index 03a8b7e824..32b486f3b5 100644 --- a/src/Umbraco.Core/Persistence/Repositories/FileRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/FileRepository.cs @@ -9,7 +9,7 @@ using Umbraco.Core.Persistence.UnitOfWork; namespace Umbraco.Core.Persistence.Repositories { - internal abstract class FileRepository : DisposableObject, IUnitOfWorkRepository, IRepository + internal abstract class FileRepository : DisposableObjectSlim, IUnitOfWorkRepository, IRepository where TEntity : IFile { private IUnitOfWork _work; diff --git a/src/Umbraco.Core/Persistence/Repositories/RepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/RepositoryBase.cs index f514ee80df..7a9f854679 100644 --- a/src/Umbraco.Core/Persistence/Repositories/RepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/RepositoryBase.cs @@ -10,7 +10,7 @@ using Umbraco.Core.Scoping; namespace Umbraco.Core.Persistence.Repositories { - internal abstract class RepositoryBase : DisposableObject + internal abstract class RepositoryBase : DisposableObjectSlim { private readonly IScopeUnitOfWork _work; private readonly CacheHelper _globalCache; diff --git a/src/Umbraco.Core/Persistence/UnitOfWork/ScopeUnitOfWork.cs b/src/Umbraco.Core/Persistence/UnitOfWork/ScopeUnitOfWork.cs index f8d168cdaa..8d15772fc3 100644 --- a/src/Umbraco.Core/Persistence/UnitOfWork/ScopeUnitOfWork.cs +++ b/src/Umbraco.Core/Persistence/UnitOfWork/ScopeUnitOfWork.cs @@ -10,7 +10,7 @@ namespace Umbraco.Core.Persistence.UnitOfWork /// /// Represents a scoped unit of work. /// - internal class ScopeUnitOfWork : DisposableObject, IScopeUnitOfWork + internal class ScopeUnitOfWork : DisposableObjectSlim, IScopeUnitOfWork { private readonly Queue _operations = new Queue(); private readonly IsolationLevel _isolationLevel; diff --git a/src/Umbraco.Core/Security/BackOfficeUserStore.cs b/src/Umbraco.Core/Security/BackOfficeUserStore.cs index 26c5393bfc..9db8347040 100644 --- a/src/Umbraco.Core/Security/BackOfficeUserStore.cs +++ b/src/Umbraco.Core/Security/BackOfficeUserStore.cs @@ -18,7 +18,7 @@ using Task = System.Threading.Tasks.Task; namespace Umbraco.Core.Security { - public class BackOfficeUserStore : DisposableObject, + public class BackOfficeUserStore : DisposableObjectSlim, IUserStore, IUserPasswordStore, IUserEmailStore, @@ -58,7 +58,7 @@ namespace Umbraco.Core.Security } /// - /// Handles the disposal of resources. Derived from abstract class which handles common required locking logic. + /// Handles the disposal of resources. Derived from abstract class which handles common required locking logic. /// protected override void DisposeResources() { diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 345ee8da8d..00ff785891 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -348,6 +348,7 @@ + diff --git a/src/Umbraco.Web/Scheduling/LatchedBackgroundTaskBase.cs b/src/Umbraco.Web/Scheduling/LatchedBackgroundTaskBase.cs index e0cda08e47..b3a23d8081 100644 --- a/src/Umbraco.Web/Scheduling/LatchedBackgroundTaskBase.cs +++ b/src/Umbraco.Web/Scheduling/LatchedBackgroundTaskBase.cs @@ -12,7 +12,7 @@ namespace Umbraco.Web.Scheduling /// depending on whether the task is implemented as a sync or async method, and then /// optionnally overriding RunsOnShutdown, to indicate whether the latched task should run /// immediately on shutdown, or just be abandonned (default). - public abstract class LatchedBackgroundTaskBase : DisposableObject, ILatchedBackgroundTask + public abstract class LatchedBackgroundTaskBase : DisposableObjectSlim, ILatchedBackgroundTask { private TaskCompletionSource _latch; diff --git a/src/Umbraco.Web/Security/WebSecurity.cs b/src/Umbraco.Web/Security/WebSecurity.cs index 460ca536eb..dd7e757c8d 100644 --- a/src/Umbraco.Web/Security/WebSecurity.cs +++ b/src/Umbraco.Web/Security/WebSecurity.cs @@ -23,7 +23,7 @@ namespace Umbraco.Web.Security /// /// A utility class used for dealing with USER security in Umbraco /// - public class WebSecurity : DisposableObject + public class WebSecurity : DisposableObjectSlim { private HttpContextBase _httpContext; private ApplicationContext _applicationContext; diff --git a/src/Umbraco.Web/UmbracoContext.cs b/src/Umbraco.Web/UmbracoContext.cs index dbb165a526..3532575f38 100644 --- a/src/Umbraco.Web/UmbracoContext.cs +++ b/src/Umbraco.Web/UmbracoContext.cs @@ -20,7 +20,7 @@ namespace Umbraco.Web /// /// Class that encapsulates Umbraco information of a specific HTTP request /// - public class UmbracoContext : DisposableObject, IDisposeOnRequestEnd + public class UmbracoContext : DisposableObjectSlim, IDisposeOnRequestEnd { internal const string HttpContextItemName = "Umbraco.Web.UmbracoContext"; private static readonly object Locker = new object(); diff --git a/src/umbraco.cms/businesslogic/Packager/Repositories/Repository.cs b/src/umbraco.cms/businesslogic/Packager/Repositories/Repository.cs index 28cc814a02..8609504313 100644 --- a/src/umbraco.cms/businesslogic/Packager/Repositories/Repository.cs +++ b/src/umbraco.cms/businesslogic/Packager/Repositories/Repository.cs @@ -14,7 +14,7 @@ using Umbraco.Core.IO; namespace umbraco.cms.businesslogic.packager.repositories { [Obsolete("This should not be used and will be removed in future Umbraco versions")] - public class Repository : DisposableObject + public class Repository : DisposableObjectSlim { public string Guid { get; private set; } @@ -289,7 +289,7 @@ namespace umbraco.cms.businesslogic.packager.repositories } /// - /// Handles the disposal of resources. Derived from abstract class which handles common required locking logic. + /// Handles the disposal of resources. Derived from abstract class which handles common required locking logic. /// protected override void DisposeResources() { From 774f333c70941f59163e1af42aed61b0659f4ff8 Mon Sep 17 00:00:00 2001 From: Stephan Date: Wed, 10 Jan 2018 15:24:58 +0100 Subject: [PATCH 23/31] U4-10789 - cleanup + beta003 --- src/SolutionInfo.cs | 2 +- src/Umbraco.Core/Collections/TypeList.cs | 33 +++++++++++++++++++ src/Umbraco.Core/Properties/AssemblyInfo.cs | 10 +++--- src/Umbraco.Core/Umbraco.Core.csproj | 1 + src/Umbraco.Web/Features/DisabledFeatures.cs | 10 ++++-- src/Umbraco.Web/Features/FeaturesResolver.cs | 16 ++++----- src/Umbraco.Web/Features/TypeList.cs | 25 -------------- src/Umbraco.Web/Features/UmbracoFeatures.cs | 33 ++++++++++++++++--- src/Umbraco.Web/Umbraco.Web.csproj | 3 +- ...ribute.cs => FeatureAuthorizeAttribute.cs} | 7 ++-- .../WebApi/UmbracoApiControllerBase.cs | 2 +- 11 files changed, 91 insertions(+), 51 deletions(-) create mode 100644 src/Umbraco.Core/Collections/TypeList.cs delete mode 100644 src/Umbraco.Web/Features/TypeList.cs rename src/Umbraco.Web/WebApi/Filters/{FeaturesAuthorizeAttribute.cs => FeatureAuthorizeAttribute.cs} (58%) diff --git a/src/SolutionInfo.cs b/src/SolutionInfo.cs index 6162dd9db3..1a3d7f6902 100644 --- a/src/SolutionInfo.cs +++ b/src/SolutionInfo.cs @@ -12,4 +12,4 @@ using System.Resources; [assembly: AssemblyVersion("1.0.*")] [assembly: AssemblyFileVersion("7.8.0")] -[assembly: AssemblyInformationalVersion("7.8.0-beta002")] \ No newline at end of file +[assembly: AssemblyInformationalVersion("7.8.0-beta003")] \ No newline at end of file diff --git a/src/Umbraco.Core/Collections/TypeList.cs b/src/Umbraco.Core/Collections/TypeList.cs new file mode 100644 index 0000000000..37ca427ba1 --- /dev/null +++ b/src/Umbraco.Core/Collections/TypeList.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; + +namespace Umbraco.Core.Collections +{ + /// + /// Represents a list of types. + /// + /// Types in the list are, or derive from, or implement, the base type. + /// The base type. + internal class TypeList + { + private readonly List _list = new List(); + + /// + /// Adds a type to the list. + /// + /// The type to add. + public void Add() + where T : TBase + { + _list.Add(typeof(T)); + } + + /// + /// Determines whether a type is in the list. + /// + public bool Contains(Type type) + { + return _list.Contains(type); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Properties/AssemblyInfo.cs b/src/Umbraco.Core/Properties/AssemblyInfo.cs index 35ff0beb22..6c54d5c8b5 100644 --- a/src/Umbraco.Core/Properties/AssemblyInfo.cs +++ b/src/Umbraco.Core/Properties/AssemblyInfo.cs @@ -1,10 +1,8 @@ using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -using System.Security; -using System.Security.Permissions; -// General Information about an assembly is controlled through the following +// General Information about an assembly is controlled through the following // set of attributes. Change these attribute values to modify the information // associated with an assembly. [assembly: AssemblyTitle("Umbraco.Core")] @@ -12,8 +10,8 @@ using System.Security.Permissions; [assembly: AssemblyConfiguration("")] [assembly: AssemblyProduct("Umbraco CMS")] -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from // COM, set the ComVisible attribute to true on that type. [assembly: ComVisible(false)] @@ -50,6 +48,8 @@ using System.Security.Permissions; [assembly: InternalsVisibleTo("Umbraco.Forms.Core.Providers")] [assembly: InternalsVisibleTo("Umbraco.Forms.Web")] +[assembly: InternalsVisibleTo("Umbraco.Headless")] + //allow this to be mocked in our unit tests [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 345ee8da8d..b470075a4d 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -178,6 +178,7 @@ + diff --git a/src/Umbraco.Web/Features/DisabledFeatures.cs b/src/Umbraco.Web/Features/DisabledFeatures.cs index 09b68f59fd..d771c101f4 100644 --- a/src/Umbraco.Web/Features/DisabledFeatures.cs +++ b/src/Umbraco.Web/Features/DisabledFeatures.cs @@ -1,18 +1,24 @@ -using Umbraco.Web.Trees; +using Umbraco.Core.Collections; using Umbraco.Web.WebApi; namespace Umbraco.Web.Features { /// - /// Represents Umbraco features that can be disabled + /// Represents disabled features. /// internal class DisabledFeatures { + /// + /// Initializes a new instance of the class. + /// public DisabledFeatures() { Controllers = new TypeList(); } + /// + /// Gets the disabled controllers. + /// public TypeList Controllers { get; private set; } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Features/FeaturesResolver.cs b/src/Umbraco.Web/Features/FeaturesResolver.cs index e7dd5a4a88..cdb6fae9ea 100644 --- a/src/Umbraco.Web/Features/FeaturesResolver.cs +++ b/src/Umbraco.Web/Features/FeaturesResolver.cs @@ -4,21 +4,21 @@ namespace Umbraco.Web.Features { internal class FeaturesResolver : SingleObjectResolverBase { - public FeaturesResolver(UmbracoFeatures value) : base(value) - { - } + public FeaturesResolver(UmbracoFeatures value) + : base(value) + { } + /// - /// Sets the disabled features + /// Sets the features. /// - /// /// For developers, at application startup. - public void SetFeatures(UmbracoFeatures finder) + public void SetFeatures(UmbracoFeatures features) { - Value = finder; + Value = features; } /// - /// Gets the features + /// Gets the features. /// public UmbracoFeatures Features { diff --git a/src/Umbraco.Web/Features/TypeList.cs b/src/Umbraco.Web/Features/TypeList.cs deleted file mode 100644 index f415b257f7..0000000000 --- a/src/Umbraco.Web/Features/TypeList.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace Umbraco.Web.Features -{ - /// - /// Maintains a list of strongly typed types - /// - /// - internal class TypeList - { - private readonly List _disabled = new List(); - - public void Add() - where TType : T - { - _disabled.Add(typeof(TType)); - } - - public bool Contains(Type type) - { - return _disabled.Contains(type); - } - } -} \ No newline at end of file diff --git a/src/Umbraco.Web/Features/UmbracoFeatures.cs b/src/Umbraco.Web/Features/UmbracoFeatures.cs index 04bc6da363..5dc64ff5e4 100644 --- a/src/Umbraco.Web/Features/UmbracoFeatures.cs +++ b/src/Umbraco.Web/Features/UmbracoFeatures.cs @@ -1,17 +1,42 @@ +using System; +using Umbraco.Web.WebApi; + namespace Umbraco.Web.Features { /// - /// Represents Umbraco features that can be toggled + /// Represents the Umbraco features. /// internal class UmbracoFeatures { + /// + /// Initializes a new instance of the class. + /// public UmbracoFeatures() { - DisabledFeatures = new DisabledFeatures(); + Disabled = new DisabledFeatures(); } - public DisabledFeatures DisabledFeatures { get; set; } + // note + // currently, the only thing a FeatureSet does is list disabled controllers, + // but eventually we could enable and disable more parts of Umbraco. and then + // we would need some logic to figure out what's enabled/disabled - hence it's + // better to use IsEnabled, where the logic would go, rather than directly + // accessing the Disabled collection. - //NOTE: Currently we can only Disable features but maybe some day we could enable non standard features too + /// + /// Gets the disabled features. + /// + public DisabledFeatures Disabled { get; set; } + + /// + /// Determines whether a feature is enabled. + /// + public bool IsEnabled(Type feature) + { + if (typeof(UmbracoApiControllerBase).IsAssignableFrom(feature)) + return Disabled.Controllers.Contains(feature) == false; + + throw new NotSupportedException("Not a supported feature type."); + } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 4b05f2956b..35b9dea9c4 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -337,7 +337,6 @@ - @@ -799,7 +798,7 @@ - + diff --git a/src/Umbraco.Web/WebApi/Filters/FeaturesAuthorizeAttribute.cs b/src/Umbraco.Web/WebApi/Filters/FeatureAuthorizeAttribute.cs similarity index 58% rename from src/Umbraco.Web/WebApi/Filters/FeaturesAuthorizeAttribute.cs rename to src/Umbraco.Web/WebApi/Filters/FeatureAuthorizeAttribute.cs index 4148c61a03..58495a06b3 100644 --- a/src/Umbraco.Web/WebApi/Filters/FeaturesAuthorizeAttribute.cs +++ b/src/Umbraco.Web/WebApi/Filters/FeatureAuthorizeAttribute.cs @@ -5,14 +5,15 @@ using Umbraco.Web.Features; namespace Umbraco.Web.WebApi.Filters { /// - /// Will return unauthorized for the controller if it's been globally disabled + /// Ensures that the controller is an authorized feature. /// - public sealed class FeaturesAuthorizeAttribute : AuthorizeAttribute + /// Else returns unauthorized. + public sealed class FeatureAuthorizeAttribute : AuthorizeAttribute { protected override bool IsAuthorized(HttpActionContext actionContext) { var controllerType = actionContext.ControllerContext.ControllerDescriptor.ControllerType; - return FeaturesResolver.Current.Features.DisabledFeatures.Controllers.Contains(controllerType) == false; + return FeaturesResolver.Current.Features.IsEnabled(controllerType); } } } \ No newline at end of file diff --git a/src/Umbraco.Web/WebApi/UmbracoApiControllerBase.cs b/src/Umbraco.Web/WebApi/UmbracoApiControllerBase.cs index c39ee9fce3..48b1bc5d46 100644 --- a/src/Umbraco.Web/WebApi/UmbracoApiControllerBase.cs +++ b/src/Umbraco.Web/WebApi/UmbracoApiControllerBase.cs @@ -13,7 +13,7 @@ namespace Umbraco.Web.WebApi /// /// The base class for API controllers that expose Umbraco services - THESE ARE NOT AUTO ROUTED /// - [FeaturesAuthorize] + [FeatureAuthorize] public abstract class UmbracoApiControllerBase : ApiController { protected UmbracoApiControllerBase() From d7f2041ee984d24b8ccfdd7726d3548610b9fec5 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 11 Jan 2018 18:00:42 +1100 Subject: [PATCH 24/31] Makes the TourFilterResolver a real resolver --- .../ManyObjectsResolverBase.cs | 2 +- src/Umbraco.Web/Editors/TourController.cs | 56 +++++++++++--- .../Models/BackOfficeTourFilter.cs | 61 +++++++++++++++ src/Umbraco.Web/TourFilterResolver.cs | 75 +++++++++++++++---- src/Umbraco.Web/Umbraco.Web.csproj | 1 + src/Umbraco.Web/WebBootManager.cs | 2 + 6 files changed, 172 insertions(+), 25 deletions(-) create mode 100644 src/Umbraco.Web/Models/BackOfficeTourFilter.cs diff --git a/src/Umbraco.Core/ObjectResolution/ManyObjectsResolverBase.cs b/src/Umbraco.Core/ObjectResolution/ManyObjectsResolverBase.cs index ad35b81ffb..6b64172633 100644 --- a/src/Umbraco.Core/ObjectResolution/ManyObjectsResolverBase.cs +++ b/src/Umbraco.Core/ObjectResolution/ManyObjectsResolverBase.cs @@ -394,7 +394,7 @@ namespace Umbraco.Core.ObjectResolution /// WARNING! Do not use this unless you know what you are doing, clear all types registered and instances /// created. Typically only used if a resolver is no longer used in an application and memory is to be GC'd /// - internal void ResetCollections() + internal virtual void ResetCollections() { using (new WriteLock(_lock)) { diff --git a/src/Umbraco.Web/Editors/TourController.cs b/src/Umbraco.Web/Editors/TourController.cs index 3355093659..b0677ed78e 100644 --- a/src/Umbraco.Web/Editors/TourController.cs +++ b/src/Umbraco.Web/Editors/TourController.cs @@ -23,15 +23,21 @@ namespace Umbraco.Web.Editors if (UmbracoConfig.For.UmbracoSettings().BackOffice.Tours.EnableTours == false) return result; + var filters = TourFilterResolver.Current.Filters.ToList(); + + //get all filters that will be applied to all tour aliases + var aliasOnlyFilters = filters.Where(x => x.PluginName == null && x.TourFileName == null).ToList(); + + //don't pass in any filters for core tours that have a plugin name assigned + var nonPluginFilters = filters.Where(x => x.PluginName == null).ToList(); + //add core tour files var coreToursPath = Path.Combine(IOHelper.MapPath(SystemDirectories.Config), "BackOfficeTours"); if (Directory.Exists(coreToursPath)) { - var coreTourFiles = Directory.GetFiles(coreToursPath, "*.json"); - - foreach (var tourFile in coreTourFiles) + foreach (var tourFile in Directory.EnumerateFiles(coreToursPath, "*.json")) { - TryParseTourFile(tourFile, result); + TryParseTourFile(tourFile, result, nonPluginFilters, aliasOnlyFilters); } } @@ -39,6 +45,14 @@ namespace Umbraco.Web.Editors foreach (var plugin in Directory.EnumerateDirectories(IOHelper.MapPath(SystemDirectories.AppPlugins))) { var pluginName = Path.GetFileName(plugin.TrimEnd('\\')); + var pluginFilters = filters.Where(x => x.PluginName != null && x.PluginName.IsMatch(pluginName)).ToList(); + + //If there is any filter applied to match the plugin only (no file or tour alias) then ignore the plugin entirely + var isPluginFiltered = pluginFilters.Any(x => x.TourFileName == null && x.TourAlias == null); + if (isPluginFiltered) continue; + + //combine matched package filters with filters not specific to a package + var combinedFilters = nonPluginFilters.Concat(pluginFilters).ToList(); foreach (var backofficeDir in Directory.EnumerateDirectories(plugin, "backoffice")) { @@ -46,7 +60,7 @@ namespace Umbraco.Web.Editors { foreach (var tourFile in Directory.EnumerateFiles(tourDir, "*.json")) { - TryParseTourFile(tourFile, result, pluginName); + TryParseTourFile(tourFile, result, combinedFilters, aliasOnlyFilters, pluginName); } } } @@ -55,22 +69,44 @@ namespace Umbraco.Web.Editors return result.OrderBy(x => x.FileName, StringComparer.InvariantCultureIgnoreCase); } - private void TryParseTourFile(string tourFile, List result, string pluginName = null) + private void TryParseTourFile(string tourFile, + ICollection result, + List filters, + List aliasOnlyFilters, + string pluginName = null) { + var fileName = Path.GetFileNameWithoutExtension(tourFile); + if (fileName == null) return; + + //get the filters specific to this file + var fileFilters = filters.Where(x => x.TourFileName != null && x.TourFileName.IsMatch(fileName)).ToList(); + + //If there is any filter applied to match the file only (no tour alias) then ignore the file entirely + var isFileFiltered = fileFilters.Any(x => x.TourAlias == null); + if (isFileFiltered) return; + + //now combine all aliases to filter below + var aliasFilters = aliasOnlyFilters.Concat(filters.Where(x => x.TourAlias != null)) + .Select(x => x.TourAlias) + .ToList(); + try { var contents = File.ReadAllText(tourFile); var tours = JsonConvert.DeserializeObject(contents); - var disabledTours = TourFilterResolver.Current.DisabledTours; - result.Add(new BackOfficeTourFile + var tour = new BackOfficeTourFile { FileName = Path.GetFileNameWithoutExtension(tourFile), PluginName = pluginName, Tours = tours - .Where(x => disabledTours.Contains(x.Alias, StringComparer.InvariantCultureIgnoreCase) == false) + .Where(x => aliasFilters.Count == 0 || aliasFilters.All(filter => filter.IsMatch(x.Alias)) == false) .ToArray() - }); + }; + + //don't add if all of the tours are filtered + if (tour.Tours.Any()) + result.Add(tour); } catch (IOException e) { diff --git a/src/Umbraco.Web/Models/BackOfficeTourFilter.cs b/src/Umbraco.Web/Models/BackOfficeTourFilter.cs new file mode 100644 index 0000000000..994cdb6d29 --- /dev/null +++ b/src/Umbraco.Web/Models/BackOfficeTourFilter.cs @@ -0,0 +1,61 @@ +using System.Text.RegularExpressions; + +namespace Umbraco.Web.Models +{ + public class BackOfficeTourFilter + { + public Regex PluginName { get; private set; } + public Regex TourFileName { get; private set; } + public Regex TourAlias { get; private set; } + + /// + /// Create a filter to filter out a whole plugin's tours + /// + /// + /// + public static BackOfficeTourFilter FilterPlugin(Regex pluginName) + { + return new BackOfficeTourFilter(pluginName, null, null); + } + + /// + /// Create a filter to filter out a whole tour file + /// + /// + /// + public static BackOfficeTourFilter FilterFile(Regex tourFileName) + { + return new BackOfficeTourFilter(null, tourFileName, null); + } + + /// + /// Create a filter to filter out a tour alias, this will filter out the same alias found in all files + /// + /// + /// + public static BackOfficeTourFilter FilterAlias(Regex tourAlias) + { + return new BackOfficeTourFilter(null, null, tourAlias); + } + + /// + /// Constructor to create a tour filter + /// + /// Value to filter out tours by a plugin, can be null + /// Value to filter out a tour file, can be null + /// Value to filter out a tour alias, can be null + /// + /// Depending on what is null will depend on how the filter is applied. + /// If pluginName is not NULL and it's matched then we check if tourFileName is not NULL and it's matched then we check tour alias is not NULL and then match it, + /// if any steps is NULL then the filters upstream are applied. + /// Example, pluginName = "hello", tourFileName="stuff", tourAlias=NULL = we will filter out the tour file "stuff" from the plugin "hello" but not from other plugins if the same file name exists. + /// Example, tourAlias="test.*" = we will filter out all tour aliases that start with the word "test" regardless of the plugin or file name + /// + public BackOfficeTourFilter(Regex pluginName, Regex tourFileName, Regex tourAlias) + { + PluginName = pluginName; + TourFileName = tourFileName; + TourAlias = tourAlias; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/TourFilterResolver.cs b/src/Umbraco.Web/TourFilterResolver.cs index d30c6b6093..586d0cb89f 100644 --- a/src/Umbraco.Web/TourFilterResolver.cs +++ b/src/Umbraco.Web/TourFilterResolver.cs @@ -1,32 +1,79 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; +using Umbraco.Core.Logging; +using Umbraco.Core.ObjectResolution; +using Umbraco.Web.Models; namespace Umbraco.Web { - public class TourFilterResolver + /// + /// Allows for adding filters for tours during startup + /// + public class TourFilterResolver : ManyObjectsResolverBase { - private static TourFilterResolver _current; - - private readonly HashSet _disabledTours; - - public TourFilterResolver() + public TourFilterResolver(IServiceProvider serviceProvider, ILogger logger) : base(serviceProvider, logger) { - _disabledTours = new HashSet(); } - public static TourFilterResolver Current + private readonly HashSet _instances = new HashSet(); + + public IEnumerable Filters { - get { return _current ?? (_current = new TourFilterResolver()); } + get { return Values; } } - public void Disable(string tour) + /// + /// Adds a filter instance + /// + /// + public void AddFilter(BackOfficeTourFilter filter) { - _disabledTours.Add(tour); + using (Resolution.Configuration) + _instances.Add(filter); } - public string[] DisabledTours + /// + /// Removes a filter instance + /// + /// + public void RemoveFilter(BackOfficeTourFilter filter) { - get { return _disabledTours.ToArray(); } + using (Resolution.Configuration) + _instances.Remove(filter); + } + + /// + /// Removes a filter instance based on callback + /// + /// + public void RemoveFilterWhere(Func filter) + { + using (Resolution.Configuration) + _instances.RemoveWhere(new Predicate(filter)); + } + + /// + /// + /// Overridden to return the combined created instances based on the resolved Types and the Concrete values added with AddFilter + /// + /// + protected override IEnumerable CreateInstances() + { + var createdInstances = base.CreateInstances(); + return createdInstances.Concat(_instances); + } + + public override void Clear() + { + base.Clear(); + _instances.Clear(); + } + + internal override void ResetCollections() + { + base.ResetCollections(); + _instances.Clear(); } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 8046e10ff8..d2c3918edf 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -328,6 +328,7 @@ + diff --git a/src/Umbraco.Web/WebBootManager.cs b/src/Umbraco.Web/WebBootManager.cs index c38540b6a2..03934992bb 100644 --- a/src/Umbraco.Web/WebBootManager.cs +++ b/src/Umbraco.Web/WebBootManager.cs @@ -353,6 +353,8 @@ namespace Umbraco.Web { base.InitializeResolvers(); + TourFilterResolver.Current = new TourFilterResolver(ServiceProvider, LoggerResolver.Current.Logger); + SearchableTreeResolver.Current = new SearchableTreeResolver(ServiceProvider, LoggerResolver.Current.Logger, ApplicationContext.Services.ApplicationTreeService, () => PluginManager.ResolveSearchableTrees()); XsltExtensionsResolver.Current = new XsltExtensionsResolver(ServiceProvider, LoggerResolver.Current.Logger, () => PluginManager.ResolveXsltExtensions()); From 928426942893d248b0f0cb0a865f9ae469106e8f Mon Sep 17 00:00:00 2001 From: Robert Date: Thu, 11 Jan 2018 10:53:45 +0100 Subject: [PATCH 25/31] TODO's for DisposableObject and DisposableObjectSlim done --- src/Umbraco.Core/DisposableObject.cs | 5 ++--- src/Umbraco.Core/DisposableObjectSlim.cs | 14 +++++--------- .../Scheduling/BackgroundTaskRunnerTests.cs | 2 +- src/Umbraco.Web/Scheduling/RecurringTaskBase.cs | 2 +- 4 files changed, 9 insertions(+), 14 deletions(-) diff --git a/src/Umbraco.Core/DisposableObject.cs b/src/Umbraco.Core/DisposableObject.cs index 71477996ef..370d939c9e 100644 --- a/src/Umbraco.Core/DisposableObject.cs +++ b/src/Umbraco.Core/DisposableObject.cs @@ -23,7 +23,7 @@ namespace Umbraco.Core // gets a value indicating whether this instance is disposed. // for internal tests only (not thread safe) //TODO make this internal + rename "Disposed" when we can break compatibility - public bool IsDisposed { get { return _disposed; } } + internal bool Disposed { get { return _disposed; } } // implements IDisposable public void Dispose() @@ -38,8 +38,7 @@ namespace Umbraco.Core Dispose(false); } - //TODO make this private, non-virtual when we can break compatibility - protected virtual void Dispose(bool disposing) + private void Dispose(bool disposing) { lock (_locko) { diff --git a/src/Umbraco.Core/DisposableObjectSlim.cs b/src/Umbraco.Core/DisposableObjectSlim.cs index bcec6165db..1ecb325100 100644 --- a/src/Umbraco.Core/DisposableObjectSlim.cs +++ b/src/Umbraco.Core/DisposableObjectSlim.cs @@ -2,6 +2,9 @@ namespace Umbraco.Core { + /// + /// This should not be use if there are ubmanaged resources to be disposed, use DisposableObject instead + /// public abstract class DisposableObjectSlim : IDisposable { private bool _disposed; @@ -9,8 +12,7 @@ namespace Umbraco.Core // gets a value indicating whether this instance is disposed. // for internal tests only (not thread safe) - //TODO make this internal + rename "Disposed" when we can break compatibility - public bool IsDisposed { get { return _disposed; } } + internal bool Disposed { get { return _disposed; } } // implements IDisposable public void Dispose() @@ -19,8 +21,7 @@ namespace Umbraco.Core GC.SuppressFinalize(this); } - //TODO make this private, non-virtual when we can break compatibility - protected virtual void Dispose(bool disposing) + private void Dispose(bool disposing) { lock (_locko) { @@ -28,15 +29,10 @@ namespace Umbraco.Core _disposed = true; } - DisposeUnmanagedResources(); - if (disposing) DisposeResources(); } protected abstract void DisposeResources(); - - protected virtual void DisposeUnmanagedResources() - { } } } diff --git a/src/Umbraco.Tests/Scheduling/BackgroundTaskRunnerTests.cs b/src/Umbraco.Tests/Scheduling/BackgroundTaskRunnerTests.cs index 7eb4442541..dee8a26743 100644 --- a/src/Umbraco.Tests/Scheduling/BackgroundTaskRunnerTests.cs +++ b/src/Umbraco.Tests/Scheduling/BackgroundTaskRunnerTests.cs @@ -548,7 +548,7 @@ namespace Umbraco.Tests.Scheduling runner.Shutdown(false, true); // check that task has been disposed (timer has been killed, etc) - Assert.IsTrue(task.IsDisposed); + Assert.IsTrue(task.Disposed); } } diff --git a/src/Umbraco.Web/Scheduling/RecurringTaskBase.cs b/src/Umbraco.Web/Scheduling/RecurringTaskBase.cs index d9bd33dd30..447a98527e 100644 --- a/src/Umbraco.Web/Scheduling/RecurringTaskBase.cs +++ b/src/Umbraco.Web/Scheduling/RecurringTaskBase.cs @@ -72,7 +72,7 @@ namespace Umbraco.Web.Scheduling if (_runner.TryAdd(this)) _timer.Change(_periodMilliseconds, 0); else - Dispose(true); + Dispose(); } /// From fa058f348dca53c4060157ec345eafdac149d104 Mon Sep 17 00:00:00 2001 From: Robert Date: Thu, 11 Jan 2018 11:02:05 +0100 Subject: [PATCH 26/31] Forgot to removed TODO --- src/Umbraco.Core/DisposableObject.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Umbraco.Core/DisposableObject.cs b/src/Umbraco.Core/DisposableObject.cs index 370d939c9e..16c39e34d8 100644 --- a/src/Umbraco.Core/DisposableObject.cs +++ b/src/Umbraco.Core/DisposableObject.cs @@ -22,7 +22,6 @@ namespace Umbraco.Core // gets a value indicating whether this instance is disposed. // for internal tests only (not thread safe) - //TODO make this internal + rename "Disposed" when we can break compatibility internal bool Disposed { get { return _disposed; } } // implements IDisposable From 7dc06c1bea3fc90838351cc9110e27f41bb7203f Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 11 Jan 2018 22:29:25 +1100 Subject: [PATCH 27/31] bumps version --- src/SolutionInfo.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SolutionInfo.cs b/src/SolutionInfo.cs index 1a3d7f6902..547ba27a93 100644 --- a/src/SolutionInfo.cs +++ b/src/SolutionInfo.cs @@ -12,4 +12,4 @@ using System.Resources; [assembly: AssemblyVersion("1.0.*")] [assembly: AssemblyFileVersion("7.8.0")] -[assembly: AssemblyInformationalVersion("7.8.0-beta003")] \ No newline at end of file +[assembly: AssemblyInformationalVersion("7.8.0-beta004")] \ No newline at end of file From e26cbccfe0a0d952699c22f9ac33d8d7da91dceb Mon Sep 17 00:00:00 2001 From: Robert Date: Thu, 11 Jan 2018 13:22:23 +0100 Subject: [PATCH 28/31] Reverted DisposableObject.cs to its original state --- src/Umbraco.Core/DisposableObject.cs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/Umbraco.Core/DisposableObject.cs b/src/Umbraco.Core/DisposableObject.cs index 16c39e34d8..0a3ae1c782 100644 --- a/src/Umbraco.Core/DisposableObject.cs +++ b/src/Umbraco.Core/DisposableObject.cs @@ -21,8 +21,9 @@ namespace Umbraco.Core private readonly object _locko = new object(); // gets a value indicating whether this instance is disposed. - // for internal tests only (not thread safe) - internal bool Disposed { get { return _disposed; } } + // for internal tests only (not thread safe) + //TODO make this internal + rename "Disposed" when we can break compatibility + public bool IsDisposed { get { return _disposed; } } // implements IDisposable public void Dispose() @@ -32,12 +33,13 @@ namespace Umbraco.Core } // finalizer - ~DisposableObject() - { - Dispose(false); - } + ~DisposableObject() + { + Dispose(false); + } - private void Dispose(bool disposing) + //TODO make this private, non-virtual when we can break compatibility + protected virtual void Dispose(bool disposing) { lock (_locko) { From 14b58386e58559a8add1686840d5e80be44a5f14 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Thu, 11 Jan 2018 14:14:03 +0100 Subject: [PATCH 29/31] fix tour backdropOpacity --- src/Umbraco.Web/Models/BackOfficeTourStep.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web/Models/BackOfficeTourStep.cs b/src/Umbraco.Web/Models/BackOfficeTourStep.cs index 55af21cfd4..a6b104d882 100644 --- a/src/Umbraco.Web/Models/BackOfficeTourStep.cs +++ b/src/Umbraco.Web/Models/BackOfficeTourStep.cs @@ -19,7 +19,7 @@ namespace Umbraco.Web.Models [DataMember(Name = "elementPreventClick")] public bool ElementPreventClick { get; set; } [DataMember(Name = "backdropOpacity")] - public float BackdropOpacity { get; set; } + public float? BackdropOpacity { get; set; } [DataMember(Name = "event")] public string Event { get; set; } } From 9ad8e6a876f167a6ea7bc725f43c4cb474121b8e Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 12 Jan 2018 00:20:06 +1100 Subject: [PATCH 30/31] fixes nuspec for tour files --- build/NuSpecs/UmbracoCms.nuspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/NuSpecs/UmbracoCms.nuspec b/build/NuSpecs/UmbracoCms.nuspec index fc8f52d1fa..c9f8c4fba2 100644 --- a/build/NuSpecs/UmbracoCms.nuspec +++ b/build/NuSpecs/UmbracoCms.nuspec @@ -32,7 +32,7 @@ - + From 83294b65941d8bd4e846a7ace0cbce358997270f Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 12 Jan 2018 00:32:00 +1100 Subject: [PATCH 31/31] bump version --- src/SolutionInfo.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SolutionInfo.cs b/src/SolutionInfo.cs index 547ba27a93..d2a75f8488 100644 --- a/src/SolutionInfo.cs +++ b/src/SolutionInfo.cs @@ -12,4 +12,4 @@ using System.Resources; [assembly: AssemblyVersion("1.0.*")] [assembly: AssemblyFileVersion("7.8.0")] -[assembly: AssemblyInformationalVersion("7.8.0-beta004")] \ No newline at end of file +[assembly: AssemblyInformationalVersion("7.8.0-beta005")] \ No newline at end of file