From 458de2b0beef8e0b3040c4c9665699bf57c4df5c Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Sun, 7 Jun 2020 09:34:32 +0200 Subject: [PATCH] Migrated tree related classes to .NET core --- src/Umbraco.Core/Trees/ITree.cs | 2 +- src/Umbraco.Core/Trees/Tree.cs | 2 +- .../Controllers/CodeFileController.cs | 24 +- .../Trees/MenuRenderingEventArgs.cs | 25 ++ .../Trees/TreeAttribute.cs | 56 +++ .../Trees/TreeController.cs | 69 +++ .../Trees/TreeControllerBase.cs | 399 ++++++++++++++++++ .../Trees/TreeNodeRenderingEventArgs.cs | 16 + .../Trees/TreeNodesRenderingEventArgs.cs | 16 + .../Trees/TreeQueryStringParameters.cs | 13 + .../Trees/TreeRenderingEventArgs.cs | 15 + .../Trees/UrlHelperExtensions.cs | 29 +- .../Extensions/FormCollectionExtensions.cs | 105 +++++ .../FormDataCollectionExtensions.cs | 2 +- .../Trees/MenuRenderingEventArgs.cs | 1 + src/Umbraco.Web/Trees/TreeAttribute.cs | 1 + src/Umbraco.Web/Trees/TreeController.cs | 1 + src/Umbraco.Web/Trees/TreeControllerBase.cs | 1 + .../Trees/TreeNodeRenderingEventArgs.cs | 1 + .../Trees/TreeNodesRenderingEventArgs.cs | 1 + .../Trees/TreeQueryStringParameters.cs | 1 + .../Trees/TreeRenderingEventArgs.cs | 1 + src/Umbraco.Web/Trees/UrlHelperExtensions.cs | 1 + 23 files changed, 766 insertions(+), 16 deletions(-) create mode 100644 src/Umbraco.Web.BackOffice/Trees/MenuRenderingEventArgs.cs create mode 100644 src/Umbraco.Web.BackOffice/Trees/TreeAttribute.cs create mode 100644 src/Umbraco.Web.BackOffice/Trees/TreeController.cs create mode 100644 src/Umbraco.Web.BackOffice/Trees/TreeControllerBase.cs create mode 100644 src/Umbraco.Web.BackOffice/Trees/TreeNodeRenderingEventArgs.cs create mode 100644 src/Umbraco.Web.BackOffice/Trees/TreeNodesRenderingEventArgs.cs create mode 100644 src/Umbraco.Web.BackOffice/Trees/TreeQueryStringParameters.cs create mode 100644 src/Umbraco.Web.BackOffice/Trees/TreeRenderingEventArgs.cs create mode 100644 src/Umbraco.Web.Common/Extensions/FormCollectionExtensions.cs diff --git a/src/Umbraco.Core/Trees/ITree.cs b/src/Umbraco.Core/Trees/ITree.cs index 567accbd9e..7f7ff816be 100644 --- a/src/Umbraco.Core/Trees/ITree.cs +++ b/src/Umbraco.Core/Trees/ITree.cs @@ -3,7 +3,7 @@ // TODO: we don't really use this, it is nice to have the treecontroller, attribute and ApplicationTree streamlined to implement this but it's not used // leave as internal for now, maybe we'll use in the future, means we could pass around ITree // TODO: We should make this a thing, a tree should just be an interface *not* a controller - internal interface ITree + public interface ITree { /// /// Gets or sets the sort order. diff --git a/src/Umbraco.Core/Trees/Tree.cs b/src/Umbraco.Core/Trees/Tree.cs index 4747d2495b..b528443eb1 100644 --- a/src/Umbraco.Core/Trees/Tree.cs +++ b/src/Umbraco.Core/Trees/Tree.cs @@ -45,7 +45,7 @@ namespace Umbraco.Web.Trees /// public Type TreeControllerType { get; } - internal static string GetRootNodeDisplayName(ITree tree, ILocalizedTextService textService) + public static string GetRootNodeDisplayName(ITree tree, ILocalizedTextService textService) { var label = $"[{tree.TreeAlias}]"; diff --git a/src/Umbraco.Web.BackOffice/Controllers/CodeFileController.cs b/src/Umbraco.Web.BackOffice/Controllers/CodeFileController.cs index 2d0ffc5d33..56a4ca7def 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/CodeFileController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/CodeFileController.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -6,25 +6,25 @@ using System.Net; using System.Net.Http; using Microsoft.AspNetCore.Mvc; using Umbraco.Core; - using Umbraco.Core.Configuration; - using Umbraco.Core.Configuration.Legacy; +using Umbraco.Core.Configuration; +using Umbraco.Core.Configuration.Legacy; using Umbraco.Core.IO; - using Umbraco.Core.Mapping; - using Umbraco.Core.Models; +using Umbraco.Core.Mapping; +using Umbraco.Core.Models; using Umbraco.Core.Services; - using Umbraco.Core.Strings; - using Umbraco.Core.Strings.Css; - using Umbraco.Extensions; - using Umbraco.Web.Models.ContentEditing; +using Umbraco.Core.Strings; +using Umbraco.Core.Strings.Css; +using Umbraco.Web.Models.ContentEditing; using Stylesheet = Umbraco.Core.Models.Stylesheet; using StylesheetRule = Umbraco.Web.Models.ContentEditing.StylesheetRule; using Umbraco.Web.BackOffice.Filters; - using Umbraco.Web.Common.ActionsResults; - using Umbraco.Web.Common.Attributes; +using Umbraco.Web.Common.ActionsResults; +using Umbraco.Web.Common.Attributes; using Umbraco.Web.Common.Exceptions; using Umbraco.Web.Editors; +using Umbraco.Web.BackOffice.Trees; - namespace Umbraco.Web.BackOffice.Controllers +namespace Umbraco.Web.BackOffice.Controllers { // TODO: Put some exception filters in our webapi to return 404 instead of 500 when we throw ArgumentNullException // ref: https://www.exceptionnotfound.net/the-asp-net-web-api-exception-handling-pipeline-a-guided-tour/ diff --git a/src/Umbraco.Web.BackOffice/Trees/MenuRenderingEventArgs.cs b/src/Umbraco.Web.BackOffice/Trees/MenuRenderingEventArgs.cs new file mode 100644 index 0000000000..74a557854f --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Trees/MenuRenderingEventArgs.cs @@ -0,0 +1,25 @@ +using Microsoft.AspNetCore.Http; +using Umbraco.Web.Models.Trees; + +namespace Umbraco.Web.BackOffice.Trees +{ + public class MenuRenderingEventArgs : TreeRenderingEventArgs + { + /// + /// The tree node id that the menu is rendering for + /// + public string NodeId { get; private set; } + + /// + /// The menu being rendered + /// + public MenuItemCollection Menu { get; private set; } + + public MenuRenderingEventArgs(string nodeId, MenuItemCollection menu, FormCollection queryStrings) + : base(queryStrings) + { + NodeId = nodeId; + Menu = menu; + } + } +} diff --git a/src/Umbraco.Web.BackOffice/Trees/TreeAttribute.cs b/src/Umbraco.Web.BackOffice/Trees/TreeAttribute.cs new file mode 100644 index 0000000000..ba24dea1c1 --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Trees/TreeAttribute.cs @@ -0,0 +1,56 @@ +using System; +using Umbraco.Web.Trees; + +namespace Umbraco.Web.BackOffice.Trees +{ + /// + /// Identifies a section tree. + /// + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] + public class TreeAttribute : Attribute, ITree + { + /// + /// Initializes a new instance of the class. + /// + public TreeAttribute(string sectionAlias, string treeAlias) + { + SectionAlias = sectionAlias; + TreeAlias = treeAlias; + } + + /// + /// Gets the section alias. + /// + public string SectionAlias { get; } + + /// + /// Gets the tree alias. + /// + public string TreeAlias { get; } + + /// + /// Gets or sets the tree title. + /// + public string TreeTitle { get; set; } + + /// + /// Gets or sets the group of the tree. + /// + public string TreeGroup { get; set; } + + /// + /// Gets the usage of the tree. + /// + public TreeUse TreeUse { get; set; } = TreeUse.Main | TreeUse.Dialog; + + /// + /// Gets or sets the tree sort order. + /// + public int SortOrder { get; set; } + + /// + /// Gets or sets a value indicating whether the tree is a single-node tree (no child nodes, full screen app). + /// + public bool IsSingleNodeTree { get; set; } + } +} diff --git a/src/Umbraco.Web.BackOffice/Trees/TreeController.cs b/src/Umbraco.Web.BackOffice/Trees/TreeController.cs new file mode 100644 index 0000000000..b28cf5843c --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Trees/TreeController.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Concurrent; +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Core; +using Umbraco.Core.Services; +using Umbraco.Web.Trees; + +namespace Umbraco.Web.BackOffice.Trees +{ + /// + /// The base controller for all tree requests + /// + public abstract class TreeController : TreeControllerBase + { + private static readonly ConcurrentDictionary _treeAttributeCache = new ConcurrentDictionary(); + + private readonly TreeAttribute _treeAttribute; + + private readonly ILocalizedTextService _textService; + + protected TreeController() + { + var serviceProvider = HttpContext.RequestServices; + _textService = serviceProvider.GetService(); + _treeAttribute = GetTreeAttribute(); + } + + protected TreeController(ILocalizedTextService textService) + { + _textService = textService ?? throw new ArgumentNullException(nameof(textService)); + _treeAttribute = GetTreeAttribute(); + } + + /// + public override string RootNodeDisplayName => Tree.GetRootNodeDisplayName(this, _textService); + + /// + public override string TreeGroup => _treeAttribute.TreeGroup; + + /// + public override string TreeAlias => _treeAttribute.TreeAlias; + + /// + public override string TreeTitle => _treeAttribute.TreeTitle; + + /// + public override TreeUse TreeUse => _treeAttribute.TreeUse; + + /// + public override string SectionAlias => _treeAttribute.SectionAlias; + + /// + public override int SortOrder => _treeAttribute.SortOrder; + + /// + public override bool IsSingleNodeTree => _treeAttribute.IsSingleNodeTree; + + private TreeAttribute GetTreeAttribute() + { + return _treeAttributeCache.GetOrAdd(GetType(), type => + { + var treeAttribute = type.GetCustomAttribute(false); + if (treeAttribute == null) + throw new InvalidOperationException("The Tree controller is missing the " + typeof(TreeAttribute).FullName + " attribute"); + return treeAttribute; + }); + } + } +} diff --git a/src/Umbraco.Web.BackOffice/Trees/TreeControllerBase.cs b/src/Umbraco.Web.BackOffice/Trees/TreeControllerBase.cs new file mode 100644 index 0000000000..ab0dacfe9a --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Trees/TreeControllerBase.cs @@ -0,0 +1,399 @@ +using System; +using System.Linq; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Core; +using Umbraco.Core.Events; +using Umbraco.Core.Models; +using Umbraco.Core.Models.Entities; +using Umbraco.Core.Persistence; +using Umbraco.Web.BackOffice.Controllers; +using Umbraco.Web.Common.Filters; +using Umbraco.Web.Common.ModelBinders; +using Umbraco.Web.Models.Trees; +using Umbraco.Web.Trees; +using Umbraco.Web.WebApi; + +namespace Umbraco.Web.BackOffice.Trees +{ + /// + /// A base controller reference for non-attributed trees (un-registered). + /// + /// + /// Developers should generally inherit from TreeController. + /// + [TypeFilter(typeof(AngularJsonOnlyConfigurationAttribute))] + public abstract class TreeControllerBase : UmbracoAuthorizedApiController, ITree + { + // TODO: Need to set this, but from where? + // Presumably not injecting as this will be a base controller for package/solution developers. + private readonly UmbracoApiControllerTypeCollection _apiControllers; + + protected TreeControllerBase() + { + } + + /// + /// The method called to render the contents of the tree structure + /// + /// + /// + /// All of the query string parameters passed from jsTree + /// + /// + /// We are allowing an arbitrary number of query strings to be passed in so that developers are able to persist custom data from the front-end + /// to the back end to be used in the query for model data. + /// + protected abstract TreeNodeCollection GetTreeNodes(string id, [ModelBinder(typeof(HttpQueryStringModelBinder))]FormCollection queryStrings); + + /// + /// Returns the menu structure for the node + /// + /// + /// + /// + protected abstract MenuItemCollection GetMenuForNode(string id, [ModelBinder(typeof(HttpQueryStringModelBinder))]FormCollection queryStrings); + + /// + /// The name to display on the root node + /// + public abstract string RootNodeDisplayName { get; } + + /// + public abstract string TreeGroup { get; } + + /// + public abstract string TreeAlias { get; } + + /// + public abstract string TreeTitle { get; } + + /// + public abstract TreeUse TreeUse { get; } + + /// + public abstract string SectionAlias { get; } + + /// + public abstract int SortOrder { get; } + + /// + public abstract bool IsSingleNodeTree { get; } + + /// + /// Returns the root node for the tree + /// + /// + /// + public TreeNode GetRootNode([ModelBinder(typeof(HttpQueryStringModelBinder))]FormCollection queryStrings) + { + if (queryStrings == null) queryStrings = FormCollection.Empty; + var node = CreateRootNode(queryStrings); + + //add the tree alias to the root + node.AdditionalData["treeAlias"] = TreeAlias; + + AddQueryStringsToAdditionalData(node, queryStrings); + + //check if the tree is searchable and add that to the meta data as well + if (this is ISearchableTree) + node.AdditionalData.Add("searchable", "true"); + + //now update all data based on some of the query strings, like if we are running in dialog mode + if (IsDialog(queryStrings)) + node.RoutePath = "#"; + + OnRootNodeRendering(this, new TreeNodeRenderingEventArgs(node, queryStrings)); + + return node; + } + + /// + /// The action called to render the contents of the tree structure + /// + /// + /// + /// All of the query string parameters passed from jsTree + /// + /// JSON markup for jsTree + /// + /// We are allowing an arbitrary number of query strings to be passed in so that developers are able to persist custom data from the front-end + /// to the back end to be used in the query for model data. + /// + public TreeNodeCollection GetNodes(string id, [ModelBinder(typeof(HttpQueryStringModelBinder))]FormCollection queryStrings) + { + if (queryStrings == null) queryStrings = FormCollection.Empty; + var nodes = GetTreeNodes(id, queryStrings); + + foreach (var node in nodes) + AddQueryStringsToAdditionalData(node, queryStrings); + + //now update all data based on some of the query strings, like if we are running in dialog mode + if (IsDialog(queryStrings)) + foreach (var node in nodes) + node.RoutePath = "#"; + + //raise the event + OnTreeNodesRendering(this, new TreeNodesRenderingEventArgs(nodes, queryStrings)); + + return nodes; + } + + /// + /// The action called to render the menu for a tree node + /// + /// + /// + /// + public MenuItemCollection GetMenu(string id, [ModelBinder(typeof(HttpQueryStringModelBinder))]FormCollection queryStrings) + { + if (queryStrings == null) queryStrings = FormCollection.Empty; + var menu = GetMenuForNode(id, queryStrings); + //raise the event + OnMenuRendering(this, new MenuRenderingEventArgs(id, menu, queryStrings)); + return menu; + } + + /// + /// Helper method to create a root model for a tree + /// + /// + protected virtual TreeNode CreateRootNode(FormCollection queryStrings) + { + var rootNodeAsString = Constants.System.RootString; + queryStrings.TryGetValue(TreeQueryStringParameters.Application, out var currApp); + + var node = new TreeNode( + rootNodeAsString, + null, //this is a root node, there is no parent + Url.GetTreeUrl(_apiControllers, GetType(), rootNodeAsString, queryStrings), + Url.GetMenuUrl(_apiControllers, GetType(), rootNodeAsString, queryStrings)) + { + HasChildren = true, + RoutePath = currApp, + Name = RootNodeDisplayName + }; + + return node; + } + + #region Create TreeNode methods + + /// + /// Helper method to create tree nodes + /// + /// + /// + /// + /// + /// + public TreeNode CreateTreeNode(string id, string parentId, FormCollection queryStrings, string title) + { + var jsonUrl = Url.GetTreeUrl(_apiControllers, GetType(), id, queryStrings); + var menuUrl = Url.GetMenuUrl(_apiControllers, GetType(), id, queryStrings); + var node = new TreeNode(id, parentId, jsonUrl, menuUrl) { Name = title }; + return node; + } + + /// + /// Helper method to create tree nodes + /// + /// + /// + /// + /// + /// + /// + public TreeNode CreateTreeNode(string id, string parentId, FormCollection queryStrings, string title, string icon) + { + var jsonUrl = Url.GetTreeUrl(_apiControllers, GetType(), id, queryStrings); + var menuUrl = Url.GetMenuUrl(_apiControllers, GetType(), id, queryStrings); + var node = new TreeNode(id, parentId, jsonUrl, menuUrl) + { + Name = title, + Icon = icon, + NodeType = TreeAlias + }; + return node; + } + + /// + /// Helper method to create tree nodes + /// + /// + /// + /// + /// + /// + /// + /// + public TreeNode CreateTreeNode(string id, string parentId, FormCollection queryStrings, string title, string icon, string routePath) + { + var jsonUrl = Url.GetTreeUrl(_apiControllers, GetType(), id, queryStrings); + var menuUrl = Url.GetMenuUrl(_apiControllers, GetType(), id, queryStrings); + var node = new TreeNode(id, parentId, jsonUrl, menuUrl) { Name = title, RoutePath = routePath, Icon = icon }; + return node; + } + + /// + /// Helper method to create tree nodes and automatically generate the json url + UDI + /// + /// + /// + /// + /// + /// + /// + public TreeNode CreateTreeNode(IEntitySlim entity, Guid entityObjectType, string parentId, FormCollection queryStrings, bool hasChildren) + { + var contentTypeIcon = entity is IContentEntitySlim contentEntity ? contentEntity.ContentTypeIcon : null; + var treeNode = CreateTreeNode(entity.Id.ToInvariantString(), parentId, queryStrings, entity.Name, contentTypeIcon); + treeNode.Path = entity.Path; + treeNode.Udi = Udi.Create(ObjectTypes.GetUdiType(entityObjectType), entity.Key); + treeNode.HasChildren = hasChildren; + treeNode.Trashed = entity.Trashed; + return treeNode; + } + + /// + /// Helper method to create tree nodes and automatically generate the json url + UDI + /// + /// + /// + /// + /// + /// + /// + /// + public TreeNode CreateTreeNode(IUmbracoEntity entity, Guid entityObjectType, string parentId, FormCollection queryStrings, string icon, bool hasChildren) + { + var treeNode = CreateTreeNode(entity.Id.ToInvariantString(), parentId, queryStrings, entity.Name, icon); + treeNode.Udi = Udi.Create(ObjectTypes.GetUdiType(entityObjectType), entity.Key); + treeNode.Path = entity.Path; + treeNode.HasChildren = hasChildren; + return treeNode; + } + + /// + /// Helper method to create tree nodes and automatically generate the json url + /// + /// + /// + /// + /// + /// + /// + /// + public TreeNode CreateTreeNode(string id, string parentId, FormCollection queryStrings, string title, string icon, bool hasChildren) + { + var treeNode = CreateTreeNode(id, parentId, queryStrings, title, icon); + treeNode.HasChildren = hasChildren; + return treeNode; + } + + /// + /// Helper method to create tree nodes and automatically generate the json url + /// + /// + /// + /// + /// + /// + /// + /// + /// + public TreeNode CreateTreeNode(string id, string parentId, FormCollection queryStrings, string title, string icon, bool hasChildren, string routePath) + { + var treeNode = CreateTreeNode(id, parentId, queryStrings, title, icon); + treeNode.HasChildren = hasChildren; + treeNode.RoutePath = routePath; + return treeNode; + } + + /// + /// Helper method to create tree nodes and automatically generate the json url + UDI + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public TreeNode CreateTreeNode(string id, string parentId, FormCollection queryStrings, string title, string icon, bool hasChildren, string routePath, Udi udi) + { + var treeNode = CreateTreeNode(id, parentId, queryStrings, title, icon); + treeNode.HasChildren = hasChildren; + treeNode.RoutePath = routePath; + treeNode.Udi = udi; + return treeNode; + } + + #endregion + + /// + /// The AdditionalData of a node is always populated with the query string data, this method performs this + /// operation and ensures that special values are not inserted or that duplicate keys are not added. + /// + /// + /// + protected void AddQueryStringsToAdditionalData(TreeNode node, FormCollection queryStrings) + { + foreach (var q in queryStrings.Where(x => node.AdditionalData.ContainsKey(x.Key) == false)) + node.AdditionalData.Add(q.Key, q.Value); + } + + /// + /// If the request is for a dialog mode tree + /// + /// + /// + protected bool IsDialog(FormCollection queryStrings) + { + queryStrings.TryGetValue(TreeQueryStringParameters.Use, out var use); + return use == "dialog"; + } + + /// + /// An event that allows developers to modify the tree node collection that is being rendered + /// + /// + /// Developers can add/remove/replace/insert/update/etc... any of the tree items in the collection. + /// + public static event TypedEventHandler TreeNodesRendering; + + private static void OnTreeNodesRendering(TreeControllerBase instance, TreeNodesRenderingEventArgs e) + { + var handler = TreeNodesRendering; + handler?.Invoke(instance, e); + } + + /// + /// An event that allows developer to modify the root tree node that is being rendered + /// + public static event TypedEventHandler RootNodeRendering; + + // internal for temp class below - kill eventually! + internal static void OnRootNodeRendering(TreeControllerBase instance, TreeNodeRenderingEventArgs e) + { + var handler = RootNodeRendering; + handler?.Invoke(instance, e); + } + + /// + /// An event that allows developers to modify the menu that is being rendered + /// + /// + /// Developers can add/remove/replace/insert/update/etc... any of the tree items in the collection. + /// + public static event TypedEventHandler MenuRendering; + + private static void OnMenuRendering(TreeControllerBase instance, MenuRenderingEventArgs e) + { + var handler = MenuRendering; + handler?.Invoke(instance, e); + } + } +} diff --git a/src/Umbraco.Web.BackOffice/Trees/TreeNodeRenderingEventArgs.cs b/src/Umbraco.Web.BackOffice/Trees/TreeNodeRenderingEventArgs.cs new file mode 100644 index 0000000000..50d7b627d9 --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Trees/TreeNodeRenderingEventArgs.cs @@ -0,0 +1,16 @@ +using Microsoft.AspNetCore.Http; +using Umbraco.Web.Models.Trees; + +namespace Umbraco.Web.BackOffice.Trees +{ + public class TreeNodeRenderingEventArgs : TreeRenderingEventArgs + { + public TreeNode Node { get; private set; } + + public TreeNodeRenderingEventArgs(TreeNode node, FormCollection queryStrings) + : base(queryStrings) + { + Node = node; + } + } +} diff --git a/src/Umbraco.Web.BackOffice/Trees/TreeNodesRenderingEventArgs.cs b/src/Umbraco.Web.BackOffice/Trees/TreeNodesRenderingEventArgs.cs new file mode 100644 index 0000000000..8c9cfebd83 --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Trees/TreeNodesRenderingEventArgs.cs @@ -0,0 +1,16 @@ +using Microsoft.AspNetCore.Http; +using Umbraco.Web.Models.Trees; + +namespace Umbraco.Web.BackOffice.Trees +{ + public class TreeNodesRenderingEventArgs : TreeRenderingEventArgs + { + public TreeNodeCollection Nodes { get; private set; } + + public TreeNodesRenderingEventArgs(TreeNodeCollection nodes, FormCollection queryStrings) + : base(queryStrings) + { + Nodes = nodes; + } + } +} diff --git a/src/Umbraco.Web.BackOffice/Trees/TreeQueryStringParameters.cs b/src/Umbraco.Web.BackOffice/Trees/TreeQueryStringParameters.cs new file mode 100644 index 0000000000..80fba4bb34 --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Trees/TreeQueryStringParameters.cs @@ -0,0 +1,13 @@ +namespace Umbraco.Web.BackOffice.Trees +{ + /// + /// Common query string parameters used for tree query strings + /// + internal struct TreeQueryStringParameters + { + public const string Use = "use"; + public const string Application = "application"; + public const string StartNodeId = "startNodeId"; + public const string DataTypeKey = "dataTypeKey"; + } +} diff --git a/src/Umbraco.Web.BackOffice/Trees/TreeRenderingEventArgs.cs b/src/Umbraco.Web.BackOffice/Trees/TreeRenderingEventArgs.cs new file mode 100644 index 0000000000..a132e52dad --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Trees/TreeRenderingEventArgs.cs @@ -0,0 +1,15 @@ +using System; +using Microsoft.AspNetCore.Http; + +namespace Umbraco.Web.BackOffice.Trees +{ + public class TreeRenderingEventArgs : EventArgs + { + public FormCollection QueryStrings { get; private set; } + + public TreeRenderingEventArgs(FormCollection queryStrings) + { + QueryStrings = queryStrings; + } + } +} diff --git a/src/Umbraco.Web.BackOffice/Trees/UrlHelperExtensions.cs b/src/Umbraco.Web.BackOffice/Trees/UrlHelperExtensions.cs index 3c94e3f9a0..eef45976f0 100644 --- a/src/Umbraco.Web.BackOffice/Trees/UrlHelperExtensions.cs +++ b/src/Umbraco.Web.BackOffice/Trees/UrlHelperExtensions.cs @@ -2,10 +2,14 @@ using System.Linq; using System.Text; using System.Web; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Umbraco.Core; +using Umbraco.Extensions; +using Umbraco.Web.Common.Extensions; +using Umbraco.Web.WebApi; -namespace Umbraco.Extensions +namespace Umbraco.Web.BackOffice.Trees { public static class UrlHelperExtensions { @@ -38,5 +42,28 @@ namespace Umbraco.Extensions } return sb.ToString().TrimEnd(","); } + + public static string GetTreeUrl(this IUrlHelper urlHelper, UmbracoApiControllerTypeCollection umbracoApiControllerTypeCollection, Type treeType, string nodeId, FormCollection queryStrings) + { + var actionUrl = urlHelper.GetUmbracoApiService(umbracoApiControllerTypeCollection, "GetNodes", treeType) + .EnsureEndsWith('?'); + + //now we need to append the query strings + actionUrl += "id=" + nodeId.EnsureEndsWith('&') + queryStrings.ToQueryString("id", + //Always ignore the custom start node id when generating URLs for tree nodes since this is a custom once-only parameter + // that should only ever be used when requesting a tree to render (root), not a tree node + TreeQueryStringParameters.StartNodeId); + return actionUrl; + } + + public static string GetMenuUrl(this IUrlHelper urlHelper, UmbracoApiControllerTypeCollection umbracoApiControllerTypeCollection, Type treeType, string nodeId, FormCollection queryStrings) + { + var actionUrl = urlHelper.GetUmbracoApiService(umbracoApiControllerTypeCollection, "GetMenu", treeType) + .EnsureEndsWith('?'); + + //now we need to append the query strings + actionUrl += "id=" + nodeId.EnsureEndsWith('&') + queryStrings.ToQueryString("id"); + return actionUrl; + } } } diff --git a/src/Umbraco.Web.Common/Extensions/FormCollectionExtensions.cs b/src/Umbraco.Web.Common/Extensions/FormCollectionExtensions.cs new file mode 100644 index 0000000000..099c2416fc --- /dev/null +++ b/src/Umbraco.Web.Common/Extensions/FormCollectionExtensions.cs @@ -0,0 +1,105 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Microsoft.AspNetCore.Http; +using Umbraco.Core; +using Umbraco.Web.Common.Extensions; + +namespace Umbraco.Web.Common.Extensions +{ + public static class FormCollectionExtensions + { + /// + /// Converts a dictionary object to a query string representation such as: + /// firstname=shannon&lastname=deminick + /// + /// + /// Any keys found in this collection will be removed from the output + /// + public static string ToQueryString(this FormCollection items, params string[] keysToIgnore) + { + if (items == null) return ""; + if (items.Any() == false) return ""; + + var builder = new StringBuilder(); + foreach (var i in items.Where(i => keysToIgnore.InvariantContains(i.Key) == false)) + builder.Append(string.Format("{0}={1}&", i.Key, i.Value)); + return builder.ToString().TrimEnd('&'); + } + + /// + /// Converts the FormCollection to a dictionary + /// + /// + /// + public static IDictionary ToDictionary(this FormCollection items) + { + return items.ToDictionary(x => x.Key, x => (object)x.Value); + } + + /// + /// Returns the value of a mandatory item in the FormCollection + /// + /// + /// + /// + public static string GetRequiredString(this FormCollection items, string key) + { + if (items.HasKey(key) == false) + throw new ArgumentNullException("The " + key + " query string parameter was not found but is required"); + return items.Single(x => x.Key.InvariantEquals(key)).Value; + } + + /// + /// Checks if the collection contains the key + /// + /// + /// + /// + public static bool HasKey(this FormCollection items, string key) + { + return items.Any(x => x.Key.InvariantEquals(key)); + } + + /// + /// Returns the object based in the collection based on it's key. This does this with a conversion so if it doesn't convert a null object is returned. + /// + /// + /// + /// + /// + public static T GetValue(this FormCollection items, string key) + { + if (items.TryGetValue(key, out var val) == false || string.IsNullOrEmpty(val)) + { + return default; + } + + var converted = val.TryConvertTo(); + return converted.Success + ? converted.Result + : default; + } + + /// + /// Returns the object based in the collection based on it's key. This does this with a conversion so if it doesn't convert or the query string is no there an exception is thrown + /// + /// + /// + /// + /// + public static T GetRequiredValue(this FormCollection items, string key) + { + if (items.TryGetValue(key, out var val) == false || string.IsNullOrEmpty(val)) + { + throw new InvalidOperationException($"The required query string parameter {key} is missing"); + } + + var converted = val.TryConvertTo(); + return converted.Success + ? converted.Result + : throw new InvalidOperationException($"The required query string parameter {key} cannot be converted to type {typeof(T)}"); + } + } +} diff --git a/src/Umbraco.Web/FormDataCollectionExtensions.cs b/src/Umbraco.Web/FormDataCollectionExtensions.cs index 52f86dcc59..3f2840c8ca 100644 --- a/src/Umbraco.Web/FormDataCollectionExtensions.cs +++ b/src/Umbraco.Web/FormDataCollectionExtensions.cs @@ -7,7 +7,7 @@ using Umbraco.Core; namespace Umbraco.Web { - + // Migrated to .NET Core (as FormCollectionExtensions) public static class FormDataCollectionExtensions { /// diff --git a/src/Umbraco.Web/Trees/MenuRenderingEventArgs.cs b/src/Umbraco.Web/Trees/MenuRenderingEventArgs.cs index 078eb73026..bb7c43dcdb 100644 --- a/src/Umbraco.Web/Trees/MenuRenderingEventArgs.cs +++ b/src/Umbraco.Web/Trees/MenuRenderingEventArgs.cs @@ -3,6 +3,7 @@ using Umbraco.Web.Models.Trees; namespace Umbraco.Web.Trees { + // Migrated to .NET Core public class MenuRenderingEventArgs : TreeRenderingEventArgs { /// diff --git a/src/Umbraco.Web/Trees/TreeAttribute.cs b/src/Umbraco.Web/Trees/TreeAttribute.cs index 25921b06f8..1170de5cfa 100644 --- a/src/Umbraco.Web/Trees/TreeAttribute.cs +++ b/src/Umbraco.Web/Trees/TreeAttribute.cs @@ -5,6 +5,7 @@ namespace Umbraco.Web.Trees /// /// Identifies a section tree. /// + /// // Migrated to .NET Core [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] public class TreeAttribute : Attribute, ITree { diff --git a/src/Umbraco.Web/Trees/TreeController.cs b/src/Umbraco.Web/Trees/TreeController.cs index e76b45e623..730fd04bf2 100644 --- a/src/Umbraco.Web/Trees/TreeController.cs +++ b/src/Umbraco.Web/Trees/TreeController.cs @@ -14,6 +14,7 @@ namespace Umbraco.Web.Trees /// /// The base controller for all tree requests /// + /// // Migrated to .NET Core public abstract class TreeController : TreeControllerBase { private static readonly ConcurrentDictionary TreeAttributeCache = new ConcurrentDictionary(); diff --git a/src/Umbraco.Web/Trees/TreeControllerBase.cs b/src/Umbraco.Web/Trees/TreeControllerBase.cs index 49f7eea845..18cb25c912 100644 --- a/src/Umbraco.Web/Trees/TreeControllerBase.cs +++ b/src/Umbraco.Web/Trees/TreeControllerBase.cs @@ -27,6 +27,7 @@ namespace Umbraco.Web.Trees /// /// Developers should generally inherit from TreeController. /// + /// Migrated to .NET Core [AngularJsonOnlyConfiguration] public abstract class TreeControllerBase : UmbracoAuthorizedApiController, ITree { diff --git a/src/Umbraco.Web/Trees/TreeNodeRenderingEventArgs.cs b/src/Umbraco.Web/Trees/TreeNodeRenderingEventArgs.cs index 30ef008cf7..1914d427e9 100644 --- a/src/Umbraco.Web/Trees/TreeNodeRenderingEventArgs.cs +++ b/src/Umbraco.Web/Trees/TreeNodeRenderingEventArgs.cs @@ -3,6 +3,7 @@ using Umbraco.Web.Models.Trees; namespace Umbraco.Web.Trees { + // Migrated to .NET Core public class TreeNodeRenderingEventArgs : TreeRenderingEventArgs { public TreeNode Node { get; private set; } diff --git a/src/Umbraco.Web/Trees/TreeNodesRenderingEventArgs.cs b/src/Umbraco.Web/Trees/TreeNodesRenderingEventArgs.cs index 9eaaad18ee..753e727b05 100644 --- a/src/Umbraco.Web/Trees/TreeNodesRenderingEventArgs.cs +++ b/src/Umbraco.Web/Trees/TreeNodesRenderingEventArgs.cs @@ -3,6 +3,7 @@ using Umbraco.Web.Models.Trees; namespace Umbraco.Web.Trees { + // Migrated to .NET Core public class TreeNodesRenderingEventArgs : TreeRenderingEventArgs { public TreeNodeCollection Nodes { get; private set; } diff --git a/src/Umbraco.Web/Trees/TreeQueryStringParameters.cs b/src/Umbraco.Web/Trees/TreeQueryStringParameters.cs index 02a198401b..130f4c2486 100644 --- a/src/Umbraco.Web/Trees/TreeQueryStringParameters.cs +++ b/src/Umbraco.Web/Trees/TreeQueryStringParameters.cs @@ -3,6 +3,7 @@ /// /// Common query string parameters used for tree query strings /// + /// Migrated to .NET Core internal struct TreeQueryStringParameters { public const string Use = "use"; diff --git a/src/Umbraco.Web/Trees/TreeRenderingEventArgs.cs b/src/Umbraco.Web/Trees/TreeRenderingEventArgs.cs index d8b67890a3..3f26c8cf50 100644 --- a/src/Umbraco.Web/Trees/TreeRenderingEventArgs.cs +++ b/src/Umbraco.Web/Trees/TreeRenderingEventArgs.cs @@ -3,6 +3,7 @@ using System.Net.Http.Formatting; namespace Umbraco.Web.Trees { + // Migrated to .NET Core public class TreeRenderingEventArgs : EventArgs { public FormDataCollection QueryStrings { get; private set; } diff --git a/src/Umbraco.Web/Trees/UrlHelperExtensions.cs b/src/Umbraco.Web/Trees/UrlHelperExtensions.cs index f4b516a764..52f7668aef 100644 --- a/src/Umbraco.Web/Trees/UrlHelperExtensions.cs +++ b/src/Umbraco.Web/Trees/UrlHelperExtensions.cs @@ -8,6 +8,7 @@ using Umbraco.Core; namespace Umbraco.Web.Trees { + // Migrated to .NET Core public static class UrlHelperExtensions { public static string GetTreeUrl(this UrlHelper urlHelper, Type treeType, string nodeId, FormDataCollection queryStrings)