From a6acb4c55828d378ed0b2f10e9840480c293a771 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 9 Jul 2013 18:51:45 +1000 Subject: [PATCH] Got tree menu items loading async so the menu is not part of the treenode json. Created docs for using Umbraco resources and how to use promises properly. Created a helper: angularHelper.resourcePromise for use with all of our resources (still need to convert the rest). Got the legacy tree data working with seperate requests for menu items (not too easy!) --- src/Umbraco.Core/StringExtensions.cs | 27 +++++ .../docs/src/using-promises-resources.md | 103 +++++++++++++++++ .../common/directives/umbtree.directive.js | 26 ++--- .../directives/umbtreeitem.directive.js | 3 - .../src/common/mocks/resources/tree.mocks.js | 58 ++++++---- .../src/common/resources/auth.resource.js | 3 + .../src/common/resources/tree.resource.js | 55 +++++---- .../src/common/services/navigation.service.js | 13 ++- .../src/common/services/tree.service.js | 76 ++++++------ .../src/common/services/utill.service.js | 27 ++++- .../Trees/ApplicationTreeExtensions.cs | 62 ++++++++-- .../Trees/LegacyTreeApiController.cs | 67 +++++++++-- .../Trees/LegacyTreeDataConverter.cs | 108 +++++++++++++----- src/Umbraco.Web/Trees/MenuItem.cs | 10 +- src/Umbraco.Web/Trees/MenuItemCollection.cs | 100 ++++++++++++++++ src/Umbraco.Web/Trees/TreeApiController.cs | 39 ++++++- src/Umbraco.Web/Trees/TreeNode.cs | 88 ++------------ src/Umbraco.Web/Trees/UrlHelperExtensions.cs | 10 ++ src/Umbraco.Web/Umbraco.Web.csproj | 1 + 19 files changed, 632 insertions(+), 244 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/docs/src/using-promises-resources.md create mode 100644 src/Umbraco.Web/Trees/MenuItemCollection.cs diff --git a/src/Umbraco.Core/StringExtensions.cs b/src/Umbraco.Core/StringExtensions.cs index 33a5730444..7f8cc95a13 100644 --- a/src/Umbraco.Core/StringExtensions.cs +++ b/src/Umbraco.Core/StringExtensions.cs @@ -28,6 +28,33 @@ namespace Umbraco.Core [UmbracoWillObsolete("Do not use this constants. See IShortStringHelper.CleanStringForSafeAliasJavaScriptCode.")] public const string UmbracoInvalidFirstCharacters = "01234567890"; + /// + /// This will append the query string to the URL + /// + /// + /// + /// + /// + /// This methods ensures that the resulting URL is structured correctly, that there's only one '?' and that things are + /// delimited properly with '&' + /// + internal static string AppendQueryStringToUrl(this string url, params string[] queryStrings) + { + //remove any prefixed '&' or '?' + for (var i = 0; i < queryStrings.Length; i++) + { + queryStrings[i] = queryStrings[i].TrimStart('?', '&').TrimEnd('&'); + } + + var nonEmpty = queryStrings.Where(x => !x.IsNullOrWhiteSpace()).ToArray(); + + if (url.Contains("?")) + { + return url + string.Join("&", nonEmpty).EnsureStartsWith('&'); + } + return url + string.Join("&", nonEmpty).EnsureStartsWith('?'); + } + /// /// Encrypt the string using the MachineKey in medium trust /// diff --git a/src/Umbraco.Web.UI.Client/docs/src/using-promises-resources.md b/src/Umbraco.Web.UI.Client/docs/src/using-promises-resources.md new file mode 100644 index 0000000000..ad84149dd3 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/docs/src/using-promises-resources.md @@ -0,0 +1,103 @@ +#Using AngularJS Promises and Umbraco Resources + +##Promises in Umbraco Resources + +All Umbraco resource methods utilize a helper method: + + angularHelper.resourcePromise + +This method accepts 2 arguments: + +* The angular HttpPromise which is created with a call to $http.get (post, etc..) +* The error message that is bubbled to the UI when the http call fails + +Here's an example of the usage in an Umbraco resource. This example is the method of the treeResource that fetches data to display the menu for a tree node: + + /** Loads in the data to display the nodes menu */ + loadMenu: function (node) { + + return angularHelper.resourcePromise( + $http.get(getTreeMenuUrl(node)), + "Failed to retreive data for a node's menu " + node.id); + } + +HTTP error handling is performed automatically inside of the `angularHelper.resourcePromise` and inside of Umbraco's response interceptors. + +##Consuming Umbraco resources + +When consuming Umbraco resources, a normal angular promise will be returned based on the above `angularHelper.resourcePromise`. The success callback will always receive the RAW json data from the server and the error callback will always receive an object containing these properties: + +* erroMsg = the error message that can be used to show in the UI +* data = the original data object used to create the promise + +Error handling will be done automatically in the Umbraco resource. Http error handling should not be done during the consumption of an Umbraco resource. + +###Simple example + +An simple example of consuming an Umbraco resource: + + treeResource.loadMenu(treeItem.node) + .then(function(data) { + scope.menu = data; + }); + +###Transforming result data + +Sometimes the consumption of an Umbraco resource needs to return a promise itself. This is required in some circumstances such as: + +The data from a result of an http resource might need to be transformed into something usable in the UI so a Service may need to call a resource, transform the result and continue to return it's own promise (since everything happens async). + +This is actually very simple to do, the Service (or whatever is consuming the resource) just returns the result of their 'then' call. Example - this example is the `getActions` method of the `treeService` that consumes the treeResource, transforms the result and continues to return it's own promise: + + getActions: function(treeItem, section) { + + return treeResource.loadMenu(treeItem.node) + .then(function(data) { + //need to convert the icons to new ones + for (var i = 0; i < data.length; i++) { + data[i].cssclass = iconHelper.convertFromLegacyIcon(data[i].cssclass); + } + return data; + }); + } + +Notice that this is just returning the call to 'then' which will return a promise that resolves the data from it's return statement. + +###Error hanlding + +Ok, what about error handling ? This is really simple as well, we just add an additional method to the .then call. A simple example: + + treeResource.loadMenu(treeItem.node) + .then(function(data) { + scope.menu = data; + }, function(err) { + //display the error + notificationsService.error(err.errorMsg); + }); + +###Error handling when transforming result data + +This is one of those things that is important to note! If you need to return a custom promise based on the result of an Umbraco resource (like the example above in Transforming result data) then you will need to 'throw' an error if you want to 'bubble' the error to the handler of your custom promise. + +The good news is, this is very simple to do, example: + + getActions: function(treeItem, section) { + + return treeResource.loadMenu(treeItem.node) + .then(function(data) { + //need to convert the icons to new ones + for (var i = 0; i < data.length; i++) { + data[i].cssclass = iconHelper.convertFromLegacyIcon(data[i].cssclass); + } + return data; + }, function(err) { + //display the error + notificationsService.error(err.errorMsg); + + //since we want the handler of this promise to be notified of this error + // we just need to rethrow it: + throw err; + }); + } + +The next thing that is important to note is that **you don't have to do anything** if you don't want to do anything with the error but still want the error bubbled up to your promises handlers. So for example, if you are expecting the handler of this promise to handle the error and display something in the UI, just leave out the function(err) callback which would look exactly the same as the example for 'Transforming result data' \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/umbtree.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/umbtree.directive.js index e7189cf780..297603cf1f 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/umbtree.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/umbtree.directive.js @@ -37,22 +37,20 @@ angular.module("umbraco.directives") return function (scope, element, attrs, controller) { - function loadTree(){ - if(scope.section){ + function loadTree() { + if (scope.section) { - $q.when(treeService.getTree({ section: scope.section, cachekey: scope.cachekey })) - .then(function (data) { - //set the data once we have it - scope.tree = data; - }, function (reason) { + //use $q.when because a promise OR raw data might be returned. - notificationsService.error("Tree Error", reason); - return; - }); - - // scope.tree = treeService.getTree({section:scope.section, cachekey: scope.cachekey}); - } - } + $q.when(treeService.getTree({ section: scope.section, cachekey: scope.cachekey })) + .then(function(data) { + //set the data once we have it + scope.tree = data; + }, function(reason) { + notificationsService.error("Tree Error", reason); + }); + } + } //watch for section changes diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/umbtreeitem.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/umbtreeitem.directive.js index 47206c4b07..c8bb7a7ece 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/umbtreeitem.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/umbtreeitem.directive.js @@ -90,9 +90,6 @@ angular.module("umbraco.directives") // $(arrow).parent().remove(loader); node.loading = false; notificationsService.error(reason); - - //alert(reason); - return; }); } }; diff --git a/src/Umbraco.Web.UI.Client/src/common/mocks/resources/tree.mocks.js b/src/Umbraco.Web.UI.Client/src/common/mocks/resources/tree.mocks.js index 11b2b24d24..3f648ea8f6 100644 --- a/src/Umbraco.Web.UI.Client/src/common/mocks/resources/tree.mocks.js +++ b/src/Umbraco.Web.UI.Client/src/common/mocks/resources/tree.mocks.js @@ -3,7 +3,8 @@ angular.module('umbraco.mocks'). 'use strict'; function getMenuItems() { - return [ + + var menu = [ { name: "Create", cssclass: "plus", alias: "create" }, { seperator: true, name: "Delete", cssclass: "remove", alias: "delete" }, @@ -23,6 +24,8 @@ angular.module('umbraco.mocks'). { seperator: true, name: "Reload", cssclass: "refresh", alias: "users" } ]; + + return [200, menu, null]; } function returnChildren(status, data, headers) { @@ -32,6 +35,8 @@ angular.module('umbraco.mocks'). var level = mocksUtills.getParameterByName(data, "level")+1; var url = "/umbraco/UmbracoTrees/ApplicationTreeApi/GetChildren?treeType=" + section + "&id=1234&level=" + level; + var menuUrl = "/umbraco/UmbracoTrees/ApplicationTreeApi/GetMenu?treeType=" + section + "&id=1234&parentId=456"; + //hack to have create as default content action var action; if (section === "content") { @@ -39,10 +44,10 @@ angular.module('umbraco.mocks'). } var children = [ - { name: "child-of-" + section, childNodesUrl:url, id: level + "" + 1234, icon: "icon-file-alt", view: section + "/edit/" + level + "" + 1234, children: [], expanded: false, hasChildren: true, level: level, defaultAction: action, menu: getMenuItems() }, - { name: "random-name-" + section, childNodesUrl:url, id: level + "" + 1235, icon: "icon-file-alt", view: section + "/edit/" + level + "" + 1235, children: [], expanded: false, hasChildren: true, level: level, defaultAction: action, menu: getMenuItems() }, - { name: "random-name-" + section, childNodesUrl:url, id: level + "" + 1236, icon: "icon-file-alt", view: section + "/edit/" + level + "" + 1236, children: [], expanded: false, hasChildren: true, level: level, defaultAction: action, menu: getMenuItems() }, - { name: "random-name-" + section, childNodesUrl:url, id: level + "" + 1237, icon: "icon-file-alt", view: "common/legacy/1237?p=" + encodeURI("developer/contentType.aspx?idequal1234"), children: [], expanded: false, hasChildren: true, level: level, defaultAction: action, menu: getMenuItems() } + { name: "child-of-" + section, childNodesUrl: url, id: level + "" + 1234, icon: "icon-file-alt", view: section + "/edit/" + level + "" + 1234, children: [], expanded: false, hasChildren: true, level: level, defaultAction: action, menuUrl: menuUrl }, + { name: "random-name-" + section, childNodesUrl: url, id: level + "" + 1235, icon: "icon-file-alt", view: section + "/edit/" + level + "" + 1235, children: [], expanded: false, hasChildren: true, level: level, defaultAction: action, menuUrl: menuUrl }, + { name: "random-name-" + section, childNodesUrl: url, id: level + "" + 1236, icon: "icon-file-alt", view: section + "/edit/" + level + "" + 1236, children: [], expanded: false, hasChildren: true, level: level, defaultAction: action, menuUrl: menuUrl }, + { name: "random-name-" + section, childNodesUrl: url, id: level + "" + 1237, icon: "icon-file-alt", view: "common/legacy/1237?p=" + encodeURI("developer/contentType.aspx?idequal1234"), children: [], expanded: false, hasChildren: true, level: level, defaultAction: action, menuUrl: menuUrl } ]; return [200, children, null]; @@ -51,42 +56,43 @@ angular.module('umbraco.mocks'). function returnApplicationTrees(status, data, headers) { var section = mocksUtills.getParameterByName(data, "application"); var url = "/umbraco/UmbracoTrees/ApplicationTreeApi/GetChildren?treeType=" + section + "&id=1234&level=1"; + var menuUrl = "/umbraco/UmbracoTrees/ApplicationTreeApi/GetMenu?treeType=" + section + "&id=1234&parentId=456"; var t; switch (section) { case "content": t = [ - { name: "My website", id: 1234, childNodesUrl:url, icon: "icon-home", view: section + "/edit/" + 1234, children: [], expanded: false, hasChildren: true, level: 1, defaultAction: "create", menu: getMenuItems() }, - { name: "Components", id: 1235, childNodesUrl:url, icon: "icon-cogs", view: section + "/edit/" + 1235, children: [], expanded: false, hasChildren: true, level: 1, defaultAction: "create", menu: getMenuItems() }, - { name: "Archieve", id: 1236, childNodesUrl:url, icon: "icon-folder-close", view: section + "/edit/" + 1236, children: [], expanded: false, hasChildren: true, level: 1, defaultAction: "create", menu: getMenuItems() }, - { name: "Recycle Bin", id: 1237, childNodesUrl:url, icon: "icon-trash", view: section + "/trash/view/", children: [], expanded: false, hasChildren: true, level: 1, defaultAction: "create", menu: getMenuItems() } + { name: "My website", id: 1234, childNodesUrl: url, icon: "icon-home", view: section + "/edit/" + 1234, children: [], expanded: false, hasChildren: true, level: 1, defaultAction: "create", menuUrl: menuUrl }, + { name: "Components", id: 1235, childNodesUrl: url, icon: "icon-cogs", view: section + "/edit/" + 1235, children: [], expanded: false, hasChildren: true, level: 1, defaultAction: "create", menuUrl: menuUrl }, + { name: "Archieve", id: 1236, childNodesUrl: url, icon: "icon-folder-close", view: section + "/edit/" + 1236, children: [], expanded: false, hasChildren: true, level: 1, defaultAction: "create", menuUrl: menuUrl }, + { name: "Recycle Bin", id: 1237, childNodesUrl: url, icon: "icon-trash", view: section + "/trash/view/", children: [], expanded: false, hasChildren: true, level: 1, defaultAction: "create", menuUrl: menuUrl } ]; break; case "developer": t = [ - { name: "Data types", childNodesUrl:url, id: 1234, icon: "icon-folder-close", view: section + "/edit/" + 1234, children: [], expanded: false, hasChildren: true, level: 1, menu: getMenuItems() }, - { name: "Macros", childNodesUrl:url, id: 1235, icon: "icon-folder-close", view: section + "/edit/" + 1235, children: [], expanded: false, hasChildren: true, level: 1, menu: getMenuItems() }, - { name: "Pacakges", childNodesUrl:url, id: 1236, icon: "icon-folder-close", view: section + "/edit/" + 1236, children: [], expanded: false, hasChildren: true, level: 1, menu: getMenuItems() }, - { name: "XSLT Files", childNodesUrl:url, id: 1237, icon: "icon-folder-close", view: section + "/edit/" + 1237, children: [], expanded: false, hasChildren: true, level: 1, menu: getMenuItems() }, - { name: "Razor Files", childNodesUrl:url, id: 1237, icon: "icon-folder-close", view: section + "/edit/" + 1237, children: [], expanded: false, hasChildren: true, level: 1, menu: getMenuItems() } + { name: "Data types", childNodesUrl: url, id: 1234, icon: "icon-folder-close", view: section + "/edit/" + 1234, children: [], expanded: false, hasChildren: true, level: 1, menuUrl: menuUrl }, + { name: "Macros", childNodesUrl: url, id: 1235, icon: "icon-folder-close", view: section + "/edit/" + 1235, children: [], expanded: false, hasChildren: true, level: 1, menuUrl: menuUrl }, + { name: "Pacakges", childNodesUrl: url, id: 1236, icon: "icon-folder-close", view: section + "/edit/" + 1236, children: [], expanded: false, hasChildren: true, level: 1, menuUrl: menuUrl }, + { name: "XSLT Files", childNodesUrl: url, id: 1237, icon: "icon-folder-close", view: section + "/edit/" + 1237, children: [], expanded: false, hasChildren: true, level: 1, menuUrl: menuUrl }, + { name: "Razor Files", childNodesUrl: url, id: 1237, icon: "icon-folder-close", view: section + "/edit/" + 1237, children: [], expanded: false, hasChildren: true, level: 1, menuUrl: menuUrl } ]; break; case "settings": t = [ - { name: "Stylesheets", childNodesUrl:url, id: 1234, icon: "icon-folder-close", view: section + "/edit/" + 1234, children: [], expanded: false, hasChildren: true, level: 1, menu: getMenuItems() }, - { name: "Templates", childNodesUrl:url, id: 1235, icon: "icon-folder-close", view: section + "/edit/" + 1235, children: [], expanded: false, hasChildren: true, level: 1, menu: getMenuItems() }, - { name: "Dictionary", childNodesUrl:url, id: 1236, icon: "icon-folder-close", view: section + "/edit/" + 1236, children: [], expanded: false, hasChildren: true, level: 1, menu: getMenuItems() }, - { name: "Media types", childNodesUrl:url, id: 1237, icon: "icon-folder-close", view: section + "/edit/" + 1237, children: [], expanded: false, hasChildren: true, level: 1, menu: getMenuItems() }, - { name: "Document types", childNodesUrl:url, id: 1237, icon: "icon-folder-close", view: section + "/edit/" + 1237, children: [], expanded: false, hasChildren: true, level: 1, menu: getMenuItems() } + { name: "Stylesheets", childNodesUrl: url, id: 1234, icon: "icon-folder-close", view: section + "/edit/" + 1234, children: [], expanded: false, hasChildren: true, level: 1, menuUrl: menuUrl }, + { name: "Templates", childNodesUrl: url, id: 1235, icon: "icon-folder-close", view: section + "/edit/" + 1235, children: [], expanded: false, hasChildren: true, level: 1, menuUrl: menuUrl }, + { name: "Dictionary", childNodesUrl: url, id: 1236, icon: "icon-folder-close", view: section + "/edit/" + 1236, children: [], expanded: false, hasChildren: true, level: 1, menuUrl: menuUrl }, + { name: "Media types", childNodesUrl: url, id: 1237, icon: "icon-folder-close", view: section + "/edit/" + 1237, children: [], expanded: false, hasChildren: true, level: 1, menuUrl: menuUrl }, + { name: "Document types", childNodesUrl: url, id: 1237, icon: "icon-folder-close", view: section + "/edit/" + 1237, children: [], expanded: false, hasChildren: true, level: 1, menuUrl: menuUrl } ]; break; default: t = [ - { name: "random-name-" + section, childNodesUrl:url, id: 1234, icon: "icon-home", defaultAction: "create", view: section + "/edit/" + 1234, children: [], expanded: false, hasChildren: true, level: 1, menu: getMenuItems() }, - { name: "random-name-" + section, childNodesUrl:url, id: 1235, icon: "icon-folder-close", defaultAction: "create", view: section + "/edit/" + 1235, children: [], expanded: false, hasChildren: true, level: 1, menu: getMenuItems() }, - { name: "random-name-" + section, childNodesUrl:url, id: 1236, icon: "icon-folder-close", defaultAction: "create", view: section + "/edit/" + 1236, children: [], expanded: false, hasChildren: true, level: 1, menu: getMenuItems() }, - { name: "random-name-" + section, childNodesUrl:url, id: 1237, icon: "icon-folder-close", defaultAction: "create", view: section + "/edit/" + 1237, children: [], expanded: false, hasChildren: true, level: 1, menu: getMenuItems() } + { name: "random-name-" + section, childNodesUrl: url, id: 1234, icon: "icon-home", defaultAction: "create", view: section + "/edit/" + 1234, children: [], expanded: false, hasChildren: true, level: 1, menuUrl: menuUrl }, + { name: "random-name-" + section, childNodesUrl: url, id: 1235, icon: "icon-folder-close", defaultAction: "create", view: section + "/edit/" + 1235, children: [], expanded: false, hasChildren: true, level: 1, menuUrl: menuUrl }, + { name: "random-name-" + section, childNodesUrl: url, id: 1236, icon: "icon-folder-close", defaultAction: "create", view: section + "/edit/" + 1236, children: [], expanded: false, hasChildren: true, level: 1, menuUrl: menuUrl }, + { name: "random-name-" + section, childNodesUrl: url, id: 1237, icon: "icon-folder-close", defaultAction: "create", view: section + "/edit/" + 1237, children: [], expanded: false, hasChildren: true, level: 1, menuUrl: menuUrl } ]; break; } @@ -105,7 +111,11 @@ angular.module('umbraco.mocks'). $httpBackend .whenGET(mocksUtills.urlRegex('/umbraco/UmbracoTrees/ApplicationTreeApi/GetChildren')) - .respond(returnChildren); + .respond(returnChildren); + + $httpBackend + .whenGET(mocksUtills.urlRegex('/umbraco/UmbracoTrees/ApplicationTreeApi/GetMenu')) + .respond(getMenuItems); } }; diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/auth.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/auth.resource.js index 5d1941da3a..4829dc004d 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/auth.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/auth.resource.js @@ -49,6 +49,9 @@ function authResource($q, $http, umbDataFormatter, umbRequestHelper) { deferred.resolve(data); }). error(function (data, status, headers, config) { + + //Change this to 200 ! we're just checking auth. + if (status === 401) { //if it's unauthorized it just means we are not authenticated so we'll just return null deferred.resolve(null); diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/tree.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/tree.resource.js index cd145df1a2..b0b484e1f5 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/tree.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/tree.resource.js @@ -3,23 +3,40 @@ * @name umbraco.resources.treeResource * @description Loads in data for trees **/ -function treeResource($q, $http) { +function treeResource($q, $http, angularHelper) { /** internal method to get the tree app url */ function getTreeAppUrl(section) { return Umbraco.Sys.ServerVariables.treeApplicationApiBaseUrl + "GetApplicationTrees?application=" + section; } - + /** internal method to get the tree node's children url */ function getTreeNodesUrl(node) { - if (!node.childNodesUrl){ + if (!node.childNodesUrl) { throw "No childNodesUrl property found on the tree node, cannot load child nodes"; } return node.childNodesUrl; } + /** internal method to get the tree menu url */ + function getTreeMenuUrl(node) { + if (!node.menuUrl) { + throw "No menuUrl property found on the tree node, cannot load menu"; + } + return node.menuUrl; + } + //the factory object returned return { + + /** Loads in the data to display the nodes menu */ + loadMenu: function (node) { + + return angularHelper.resourcePromise( + $http.get(getTreeMenuUrl(node)), + "Failed to retreive data for a node's menu " + node.id); + }, + /** Loads in the data to display the nodes for an application */ loadApplication: function (options) { @@ -27,19 +44,11 @@ function treeResource($q, $http) { throw "The object specified for does not contain a 'section' property"; } - var deferred = $q.defer(); - - //go and get the tree data - $http.get(getTreeAppUrl(options.section)). - success(function (data, status, headers, config) { - deferred.resolve(data); - }). - error(function (data, status, headers, config) { - deferred.reject('Failed to retreive data for application tree ' + options.section); - }); - - return deferred.promise; + return angularHelper.resourcePromise( + $http.get(getTreeAppUrl(options.section)), + 'Failed to retreive data for application tree ' + options.section); }, + /** Loads in the data to display the child nodes for a given node */ loadNodes: function (options) { @@ -47,19 +56,9 @@ function treeResource($q, $http) { throw "The options parameter object does not contain the required properties: 'node' and 'section'"; } - var deferred = $q.defer(); - - //go and get the tree data - $http.get(getTreeNodesUrl(options.node)). - success(function (data, status, headers, config) { - deferred.resolve(data); - }). - error(function (data, status, headers, config) { - deferred.reject('Failed to retreive data for child nodes ' + options.node.nodeId); - }); - - return deferred.promise; - + return angularHelper.resourcePromise( + $http.get(getTreeNodesUrl(options.node)), + 'Failed to retreive data for child nodes ' + options.node.nodeId); } }; } diff --git a/src/Umbraco.Web.UI.Client/src/common/services/navigation.service.js b/src/Umbraco.Web.UI.Client/src/common/services/navigation.service.js index 5256f29a1c..88acba0bdd 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/navigation.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/navigation.service.js @@ -1,5 +1,5 @@ angular.module('umbraco.services') -.factory('navigationService', function ($rootScope, $routeParams, $log, $location, dialogService, treeService, sectionResource) { +.factory('navigationService', function ($rootScope, $routeParams, $log, $location, dialogService, treeService, sectionResource, notificationsService) { var currentSection = $routeParams.section; var currentId = $routeParams.id; @@ -111,8 +111,15 @@ angular.module('umbraco.services') }); } else { - setMode("menu"); - ui.actions = treeService.getActions({node: args.node, section: this.ui.currentTree}); + setMode("menu"); + + treeService.getActions({ node: args.node, section: this.ui.currentTree }) + .then(function(data) { + ui.actions = data; + }, function (err) { + //display the error + notificationsService.error(err.errorMsg); + }); this.ui.currentNode = args.node; diff --git a/src/Umbraco.Web.UI.Client/src/common/services/tree.service.js b/src/Umbraco.Web.UI.Client/src/common/services/tree.service.js index 8d73d41954..1f7eb973b1 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/tree.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/tree.service.js @@ -35,44 +35,40 @@ function treeService($q, treeResource, iconHelper) { var cacheKey = options.cachekey || ''; cacheKey += "_" + section; - var deferred = $q.defer(); - //return the cache if it exists if (treeArray[cacheKey] !== undefined){ return treeArray[cacheKey]; } - - treeResource.loadApplication(options) - .then(function (data) { - //this will be called once the tree app data has loaded - var result = { - name: section, - alias: section, - children: data - }; - //ensure the view is added to each tree node - ensureLevelAndView(result.children, section); - //cache this result - //TODO: We'll need to un-cache this in many circumstances - treeArray[cacheKey] = result; - //return the data result as promised - deferred.resolve(treeArray[cacheKey]); - }, function (reason) { - //bubble up the rejection - deferred.reject(reason); - return; - }); - - return deferred.promise; + + return treeResource.loadApplication(options) + .then(function(data) { + //this will be called once the tree app data has loaded + var result = { + name: section, + alias: section, + children: data + }; + //ensure the view is added to each tree node + ensureLevelAndView(result.children, section); + //cache this result + //TODO: We'll need to un-cache this in many circumstances + treeArray[cacheKey] = result; + //return the data result as promised + //deferred.resolve(treeArray[cacheKey]); + return treeArray[cacheKey]; + }); }, getActions: function(treeItem, section) { - //need to convert the icons to new ones - for (var i = 0; i < treeItem.node.menu.length; i++) { - treeItem.node.menu[i].cssclass = iconHelper.convertFromLegacyIcon(treeItem.node.menu[i].cssclass); - } - return treeItem.node.menu; + return treeResource.loadMenu(treeItem.node) + .then(function(data) { + //need to convert the icons to new ones + for (var i = 0; i < data.length; i++) { + data[i].cssclass = iconHelper.convertFromLegacyIcon(data[i].cssclass); + } + return data; + }); }, getChildren: function (options) { @@ -97,20 +93,12 @@ function treeService($q, treeResource, iconHelper) { throw "No node defined"; } - var deferred = $q.defer(); - - treeResource.loadNodes( {section: section, node:treeItem} ) - .then(function (data) { - //now that we have the data, we need to add the level property to each item and the view - ensureLevelAndView(data, section, treeItem.level + 1); - deferred.resolve(data); - }, function (reason) { - //bubble up the rejection - deferred.reject(reason); - return; - }); - - return deferred.promise; + return treeResource.loadNodes({ section: section, node: treeItem }) + .then(function(data) { + //now that we have the data, we need to add the level property to each item and the view + ensureLevelAndView(data, section, treeItem.level + 1); + return data; + }); } }; } diff --git a/src/Umbraco.Web.UI.Client/src/common/services/utill.service.js b/src/Umbraco.Web.UI.Client/src/common/services/utill.service.js index 7a4b2af506..270ab58194 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/utill.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/utill.service.js @@ -8,9 +8,34 @@ * @description * Some angular helper/extension methods */ -function angularHelper($log) { +function angularHelper($log, $q) { return { + resourcePromise: function (httpPromise, errorMsg) { + var deferred = $q.defer(); + + httpPromise.success(function (data, status, headers, config) { + + //when it's successful, just return the data + deferred.resolve(data); + + }).error(function(data, status, headers, config) { + + //when there's an erorr... + // TODO: Deal with the error in a global way + + //return an error object including the error message for UI + deferred.reject({ + errorMsg: errorMsg, + data: data + }); + + }); + + return deferred.promise; + + }, + /** * @ngdoc function * @name safeApply diff --git a/src/Umbraco.Web/Trees/ApplicationTreeExtensions.cs b/src/Umbraco.Web/Trees/ApplicationTreeExtensions.cs index 283f8216db..860214adf8 100644 --- a/src/Umbraco.Web/Trees/ApplicationTreeExtensions.cs +++ b/src/Umbraco.Web/Trees/ApplicationTreeExtensions.cs @@ -74,17 +74,28 @@ namespace Umbraco.Web.Trees } internal static Attempt TryGetRootNodeFromLegacyTree(this ApplicationTree appTree, FormDataCollection formCollection, UrlHelper urlHelper) + { + var xmlTreeNodeAttempt = TryGetRootXmlNodeFromLegacyTree(appTree, formCollection, urlHelper); + if (xmlTreeNodeAttempt.Success == false) + { + return new Attempt(xmlTreeNodeAttempt.Error); + } + return new Attempt(true, + LegacyTreeDataConverter.ConvertFromLegacy(xmlTreeNodeAttempt.Result.NodeID, xmlTreeNodeAttempt.Result, urlHelper, isRoot: true)); + } + + internal static Attempt TryGetRootXmlNodeFromLegacyTree(this ApplicationTree appTree, FormDataCollection formCollection, UrlHelper urlHelper) { var treeDefAttempt = appTree.TryGetLegacyTreeDef(); if (treeDefAttempt.Success == false) { - return new Attempt(treeDefAttempt.Error); + return new Attempt(treeDefAttempt.Error); } var treeDef = treeDefAttempt.Result; var bTree = treeDef.CreateInstance(); var treeParams = new LegacyTreeParams(formCollection); bTree.SetTreeParameters(treeParams); - return new Attempt(true, LegacyTreeDataConverter.ConvertFromLegacy(bTree.RootNode, urlHelper)); + return new Attempt(true, bTree.RootNode); } private static Attempt TryGetLegacyTreeDef(this ApplicationTree appTree) @@ -98,11 +109,49 @@ namespace Umbraco.Web.Trees internal static Attempt TryLoadFromLegacyTree(this ApplicationTree appTree, string id, FormDataCollection formCollection, UrlHelper urlHelper) { - var treeDefAttempt = appTree.TryGetLegacyTreeDef(); + var xTreeAttempt = appTree.TryGetXmlTree(id, formCollection); + if (xTreeAttempt.Success == false) + { + return new Attempt(xTreeAttempt.Error); + } + return new Attempt(true, LegacyTreeDataConverter.ConvertFromLegacy(id, xTreeAttempt.Result, urlHelper)); + } + + internal static Attempt TryGetMenuFromLegacyTreeRootNode(this ApplicationTree appTree, FormDataCollection formCollection, UrlHelper urlHelper) + { + var rootAttempt = appTree.TryGetRootXmlNodeFromLegacyTree(formCollection, urlHelper); + if (rootAttempt.Success == false) + { + return new Attempt(rootAttempt.Error); + } + + var result = LegacyTreeDataConverter.ConvertFromLegacyMenu(rootAttempt.Result); + return new Attempt(true, result); + } + + internal static Attempt TryGetMenuFromLegacyTreeNode(this ApplicationTree appTree, string parentId, string nodeId, FormDataCollection formCollection, UrlHelper urlHelper) + { + var xTreeAttempt = appTree.TryGetXmlTree(parentId, formCollection); + if (xTreeAttempt.Success == false) + { + return new Attempt(xTreeAttempt.Error); + } + + var result = LegacyTreeDataConverter.ConvertFromLegacyMenu(nodeId, xTreeAttempt.Result); + if (result == null) + { + return new Attempt(new ApplicationException("Could not find the node with id " + nodeId + " in the collection of nodes contained with parent id " + parentId)); + } + return new Attempt(true, result); + } + + private static Attempt TryGetXmlTree(this ApplicationTree appTree, string id, FormDataCollection formCollection) + { + var treeDefAttempt = appTree.TryGetLegacyTreeDef(); if (treeDefAttempt.Success == false) { - return new Attempt(treeDefAttempt.Error); - } + return new Attempt(treeDefAttempt.Error); + } var treeDef = treeDefAttempt.Result; //This is how the legacy trees worked.... var bTree = treeDef.CreateInstance(); @@ -122,8 +171,7 @@ namespace Umbraco.Web.Trees var xTree = new XmlTree(); bTree.SetTreeParameters(treeParams); bTree.Render(ref xTree); - - return new Attempt(true, LegacyTreeDataConverter.ConvertFromLegacy(xTree, urlHelper)); + return new Attempt(true, xTree); } } diff --git a/src/Umbraco.Web/Trees/LegacyTreeApiController.cs b/src/Umbraco.Web/Trees/LegacyTreeApiController.cs index 8f6fb8a1da..b93afdb7b6 100644 --- a/src/Umbraco.Web/Trees/LegacyTreeApiController.cs +++ b/src/Umbraco.Web/Trees/LegacyTreeApiController.cs @@ -2,6 +2,8 @@ using System.Globalization; using System.Linq; using System.Net.Http.Formatting; +using Umbraco.Core.Logging; +using Umbraco.Core.Models; using Umbraco.Core.Services; using Umbraco.Web.Mvc; using Umbraco.Web.WebApi; @@ -24,6 +26,62 @@ namespace Umbraco.Web.Trees /// [HttpQueryStringFilter("queryStrings")] public TreeNodeCollection GetNodes(string id, FormDataCollection queryStrings) + { + var tree = GetTree(queryStrings); + var attempt = tree.TryLoadFromLegacyTree(id, queryStrings, Url); + if (attempt.Success == false) + { + var msg = "Could not render tree " + queryStrings.GetRequiredString("treeType") + " for node id " + id; + LogHelper.Error(msg, attempt.Error); + throw new ApplicationException(msg); + } + + return attempt.Result; + } + + /// + /// This will return the menu item collection for the tree node with the specified Id + /// + /// + /// + /// + /// + /// Due to the nature of legacy trees this means that we need to lookup the parent node, render + /// the TreeNodeCollection and then find the node we're looking for and render it's menu. + /// + [HttpQueryStringFilter("queryStrings")] + public MenuItemCollection GetMenu(string id, FormDataCollection queryStrings) + { + //get the parent id from the query strings + var parentId = queryStrings.GetRequiredString("parentId"); + var tree = GetTree(queryStrings); + + //if the id and the parentId are both -1 then we need to get the menu for the root node + if (id == "-1" && parentId == "-1") + { + var attempt = tree.TryGetMenuFromLegacyTreeRootNode(queryStrings, Url); + if (attempt.Success == false) + { + var msg = "Could not render menu for root node for treeType " + queryStrings.GetRequiredString("treeType"); + LogHelper.Error(msg, attempt.Error); + throw new ApplicationException(msg); + } + return attempt.Result; + } + else + { + var attempt = tree.TryGetMenuFromLegacyTreeNode(parentId, id, queryStrings, Url); + if (attempt.Success == false) + { + var msg = "Could not render menu for treeType " + queryStrings.GetRequiredString("treeType") + " for node id " + parentId; + LogHelper.Error(msg, attempt.Error); + throw new ApplicationException(msg); + } + return attempt.Result; + } + } + + private ApplicationTree GetTree(FormDataCollection queryStrings) { //need to ensure we have a tree type var treeType = queryStrings.GetRequiredString("treeType"); @@ -31,14 +89,7 @@ namespace Umbraco.Web.Trees var tree = Services.ApplicationTreeService.GetByAlias(treeType); if (tree == null) throw new InvalidOperationException("No tree found with alias " + treeType); - - var attempt = tree.TryLoadFromLegacyTree(id, queryStrings, Url); - if (attempt.Success == false) - { - throw new ApplicationException("Could not render tree " + treeType + " for node id " + id); - } - - return attempt.Result; + return tree; } } diff --git a/src/Umbraco.Web/Trees/LegacyTreeDataConverter.cs b/src/Umbraco.Web/Trees/LegacyTreeDataConverter.cs index af12749def..d57759bf0d 100644 --- a/src/Umbraco.Web/Trees/LegacyTreeDataConverter.cs +++ b/src/Umbraco.Web/Trees/LegacyTreeDataConverter.cs @@ -1,8 +1,10 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Web.Http.Routing; using Umbraco.Core; using umbraco.BusinessLogic.Actions; +using umbraco.cms.helpers; using umbraco.cms.presentation.Trees; using umbraco.controls.Tree; using umbraco.interfaces; @@ -15,21 +17,87 @@ namespace Umbraco.Web.Trees internal class LegacyTreeDataConverter { - internal static TreeNode ConvertFromLegacy(XmlTreeNode xmlTreeNode, UrlHelper urlHelper) + /// + /// Gets the menu item collection from a legacy tree node based on it's parent node's child collection + /// + /// The node id + /// The node collection that contains the node id + /// + internal static MenuItemCollection ConvertFromLegacyMenu(string nodeId, XmlTree xmlTree) + { + var xmlTreeNode = xmlTree.treeCollection.FirstOrDefault(x => x.NodeID == nodeId); + if (xmlTreeNode == null) + { + return null; + } + + return ConvertFromLegacyMenu(xmlTreeNode); + } + + /// + /// Gets the menu item collection from a legacy tree node + /// + /// + /// + internal static MenuItemCollection ConvertFromLegacyMenu(XmlTreeNode xmlTreeNode) + { + var collection = new MenuItemCollection(); + + var menuItems = xmlTreeNode.Menu.ToArray(); + var numAdded = 0; + var seperators = new List(); + foreach (var t in menuItems) + { + if (t is ContextMenuSeperator && numAdded > 0) + { + //store the index for which the seperator should be placed + seperators.Add(collection.Count()); + } + else + { + collection.AddMenuItem(t); + numAdded++; + } + } + var length = collection.Count(); + foreach (var s in seperators) + { + if (length >= s) + { + collection.ElementAt(s).Seperator = true; + } + } + + return collection; + } + + internal static TreeNode ConvertFromLegacy(string parentId, XmlTreeNode xmlTreeNode, UrlHelper urlHelper, bool isRoot = false) { // /umbraco/tree.aspx?rnd=d0d0ff11a1c347dabfaa0fc75effcc2a&id=1046&treeType=content&contextMenu=false&isDialog=false //we need to convert the node source to our legacy tree controller - var source = urlHelper.GetUmbracoApiService("GetNodes"); + var childNodesSource = urlHelper.GetUmbracoApiService("GetNodes"); + + var childQuery = (xmlTreeNode.Source.IsNullOrWhiteSpace() || xmlTreeNode.Source.IndexOf('?') == -1) + ? "" + : xmlTreeNode.Source.Substring(xmlTreeNode.Source.IndexOf('?')); + //append the query strings - var query = xmlTreeNode.Source.IsNullOrWhiteSpace() - ? new string[] { } - : xmlTreeNode.Source.Split(new[] { '?' }, StringSplitOptions.RemoveEmptyEntries); - source += query.Length > 1 ? query[1].EnsureStartsWith('?') : ""; + childNodesSource = childNodesSource.AppendQueryStringToUrl(childQuery); + + //for the menu source we need to detect if this is a root node since we'll need to set the parentId and id to -1 + // for which we'll handle correctly on the server side. + var menuSource = urlHelper.GetUmbracoApiService("GetMenu"); + menuSource = menuSource.AppendQueryStringToUrl(new[] + { + "id=" + (isRoot ? "-1" : xmlTreeNode.NodeID), + "treeType=" + xmlTreeNode.TreeType, + "parentId=" + (isRoot ? "-1" : parentId) + }); //TODO: Might need to add stuff to additional attributes - - var node = new TreeNode(xmlTreeNode.NodeID, source) + + var node = new TreeNode(xmlTreeNode.NodeID, childNodesSource, menuSource) { HasChildren = xmlTreeNode.HasChildren, Icon = xmlTreeNode.Icon, @@ -44,36 +112,18 @@ namespace Umbraco.Web.Trees { node.OnClickCallback = xmlTreeNode.Action; } - - var menuItems = xmlTreeNode.Menu.ToArray(); - var numAdded = 0; - foreach (var t in menuItems) - { - if (t is ContextMenuSeperator && numAdded > 0) - { - //if it is a seperator, then update the previous menu item that we've added to be flagged - //with a seperator - node.Menu.ElementAt(numAdded - 1).Seperator = true; - } - else - { - node.AddMenuItem(t); - numAdded++; - } - } - return node; } - internal static TreeNodeCollection ConvertFromLegacy(XmlTree xmlTree, UrlHelper urlHelper) + internal static TreeNodeCollection ConvertFromLegacy(string parentId, XmlTree xmlTree, UrlHelper urlHelper) { //TODO: Once we get the editor URL stuff working we'll need to figure out how to convert // that over to use the old school ui.xml stuff for these old trees and however the old menu items worked. var collection = new TreeNodeCollection(); foreach (var x in xmlTree.treeCollection) - { - collection.Add(ConvertFromLegacy(x, urlHelper)); + { + collection.Add(ConvertFromLegacy(parentId, x, urlHelper)); } return collection; } diff --git a/src/Umbraco.Web/Trees/MenuItem.cs b/src/Umbraco.Web/Trees/MenuItem.cs index 37690d630e..3608a06319 100644 --- a/src/Umbraco.Web/Trees/MenuItem.cs +++ b/src/Umbraco.Web/Trees/MenuItem.cs @@ -1,6 +1,7 @@ using System.ComponentModel.DataAnnotations; using System.Runtime.Serialization; using umbraco.interfaces; +using System.Collections.Generic; namespace Umbraco.Web.Trees { @@ -9,7 +10,7 @@ namespace Umbraco.Web.Trees { public MenuItem() { - + AdditionalData = new Dictionary(); } public MenuItem(IAction legacyMenu) @@ -20,6 +21,13 @@ namespace Umbraco.Web.Trees Icon = legacyMenu.Icon; } + /// + /// A dictionary to support any additional meta data that should be rendered for the node which is + /// useful for custom action commands such as 'create', 'copy', etc... + /// + [DataMember(Name = "metaData")] + public Dictionary AdditionalData { get; private set; } + [DataMember(Name = "name", IsRequired = true)] [Required] public string Name { get; set; } diff --git a/src/Umbraco.Web/Trees/MenuItemCollection.cs b/src/Umbraco.Web/Trees/MenuItemCollection.cs new file mode 100644 index 0000000000..0b94eaa4e0 --- /dev/null +++ b/src/Umbraco.Web/Trees/MenuItemCollection.cs @@ -0,0 +1,100 @@ +using System.Collections; +using System.Collections.Generic; +using System.Runtime.Serialization; +using Umbraco.Core; +using umbraco.interfaces; + +namespace Umbraco.Web.Trees +{ + [CollectionDataContract(Name = "menuItems", Namespace = "")] + public class MenuItemCollection : IEnumerable + { + public MenuItemCollection() + { + + } + + private readonly List _menuItems = new List(); + + /// + /// Adds a menu item + /// + public void AddMenuItem(IAction action) + { + _menuItems.Add(new MenuItem(action)); + } + + /// + /// Adds a menu item + /// + public void AddMenuItem(MenuItem item) + { + _menuItems.Add(item); + } + + //TODO: Implement more overloads for MenuItem with dictionary vals + + /// + /// Adds a menu item + /// + /// + public void AddMenuItem() + where T : IAction + { + AddMenuItem(null); + } + + /// + /// Adds a menu item with a key value pair which is merged to the AdditionalData bag + /// + /// + /// + /// + public void AddMenuItem(string key, string value) + where T : IAction + { + AddMenuItem(new Dictionary { { key, value } }); + } + + /// + /// Adds a menu item with a dictionary which is merged to the AdditionalData bag + /// + /// + /// + public void AddMenuItem(IDictionary additionalData) + where T : IAction + { + var item = ActionsResolver.Current.GetAction(); + if (item != null) + { + var menuItem = new MenuItem(item); + + if (additionalData != null) + { + foreach (var i in additionalData) + { + menuItem.AdditionalData[i.Key] = i.Value; + } + } + + _menuItems.Add(menuItem); + + //TODO: Once we implement 'real' menu items, not just IActions we can implement this since + // people may need to pass specific data to their menu items + + ////validate the data in the meta data bag + //item.ValidateRequiredData(AdditionalData); + } + } + + public IEnumerator GetEnumerator() + { + return _menuItems.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Trees/TreeApiController.cs b/src/Umbraco.Web/Trees/TreeApiController.cs index 157d400eb2..c6e9e59585 100644 --- a/src/Umbraco.Web/Trees/TreeApiController.cs +++ b/src/Umbraco.Web/Trees/TreeApiController.cs @@ -59,6 +59,14 @@ namespace Umbraco.Web.Trees /// protected abstract TreeNodeCollection GetTreeData(string id, FormDataCollection queryStrings); + /// + /// Returns the menu structure for the node + /// + /// + /// + /// + protected abstract MenuItemCollection GetMenuForNode(string id, FormDataCollection queryStrings); + ///// ///// Returns the root node for the tree ///// @@ -91,20 +99,45 @@ namespace Umbraco.Web.Trees : GetTreeData(id, queryStrings); } + /// + /// The action called to render the menu for a tree node + /// + /// + /// + /// + [HttpQueryStringFilter("queryStrings")] + public MenuItemCollection GetMenu(string id, FormDataCollection queryStrings) + { + if (queryStrings == null) queryStrings = new FormDataCollection(""); + + return GetMenuForNode(id, queryStrings); + } + /// /// Helper method to create a root model for a tree /// /// protected virtual TreeNode CreateRootNode(FormDataCollection queryStrings) { + var rootNodeAsString = Constants.System.Root.ToString(CultureInfo.InvariantCulture); + var getChildNodesUrl = Url.GetTreeUrl( - GetType(), - Constants.System.Root.ToString(CultureInfo.InvariantCulture), + GetType(), + rootNodeAsString, + queryStrings); + + var getMenuUrl = Url.GetTreeUrl( + GetType(), + rootNodeAsString, queryStrings); var isDialog = queryStrings.GetValue(TreeQueryStringParameters.DialogMode); + //var node = new TreeNode(RootNodeId, BackOfficeRequestContext.RegisteredComponents.MenuItems, jsonUrl) - var node = new TreeNode(Constants.System.Root.ToString(CultureInfo.InvariantCulture), getChildNodesUrl) + var node = new TreeNode( + rootNodeAsString, + getChildNodesUrl, + getMenuUrl) { HasChildren = true, diff --git a/src/Umbraco.Web/Trees/TreeNode.cs b/src/Umbraco.Web/Trees/TreeNode.cs index b821c727dd..f19e271b31 100644 --- a/src/Umbraco.Web/Trees/TreeNode.cs +++ b/src/Umbraco.Web/Trees/TreeNode.cs @@ -1,9 +1,7 @@ using System.Collections.Generic; using System.Runtime.Serialization; using Newtonsoft.Json; -using Umbraco.Core; using Umbraco.Core.IO; -using umbraco.interfaces; namespace Umbraco.Web.Trees { @@ -13,15 +11,16 @@ namespace Umbraco.Web.Trees [DataContract(Name = "node", Namespace = "")] public class TreeNode { - private readonly List _menuItems = new List(); + //private readonly List _menuItems = new List(); - public TreeNode(string nodeId, string getChildNodesUrl) + public TreeNode(string nodeId, string getChildNodesUrl, string menuUrl) { //_menuItems = menuItems; //Style = new NodeStyle(); NodeId = nodeId; AdditionalData = new Dictionary(); ChildNodesUrl = getChildNodesUrl; + MenuUrl = menuUrl; } /// @@ -118,6 +117,12 @@ namespace Umbraco.Web.Trees [DataMember(Name = "childNodesUrl")] public string ChildNodesUrl { get; set; } + /// + /// The JSON url to load the menu from + /// + [DataMember(Name = "menuUrl")] + public string MenuUrl { get; set; } + ///// ///// The UI style to give the model ///// @@ -130,80 +135,5 @@ namespace Umbraco.Web.Trees [DataMember(Name = "metaData")] public Dictionary AdditionalData { get; private set; } - /// - /// A collection of context menu actions to apply for the model - /// - [DataMember(Name = "menu")] - public IEnumerable Menu - { - get { return _menuItems; } - } - - /// - /// Adds a menu item - /// - public void AddMenuItem(IAction action) - { - _menuItems.Add(new MenuItem(action)); - } - - /// - /// Adds a menu item - /// - public void AddMenuItem(MenuItem item) - { - _menuItems.Add(item); - } - - //TODO: Implement more overloads for MenuItem with dictionary vals - - /// - /// Adds a menu item - /// - /// - public void AddMenuItem() - where T : IAction - { - AddMenuItem(null); - } - - /// - /// Adds a menu item with a key value pair which is merged to the AdditionalData bag - /// - /// - /// - /// - public void AddMenuItem(string key, string value) - where T : IAction - { - AddMenuItem(new Dictionary { { key, value } }); - } - - /// - /// Adds a menu item with a dictionary which is merged to the AdditionalData bag - /// - /// - /// - public void AddMenuItem(IDictionary additionalData) - where T : IAction - { - var item = ActionsResolver.Current.GetAction(); - if (item != null) - { - _menuItems.Add(new MenuItem(item)); - if (additionalData != null) - { - //merge the additional data! - AdditionalData = AdditionalData.MergeLeft(additionalData); - } - - //TODO: Once we implement 'real' menu items, not just IActions we can implement this since - // people may need to pass specific data to their menu items - - ////validate the data in the meta data bag - //item.ValidateRequiredData(AdditionalData); - } - } - } } diff --git a/src/Umbraco.Web/Trees/UrlHelperExtensions.cs b/src/Umbraco.Web/Trees/UrlHelperExtensions.cs index b6dfdda5cc..5dccf2b65e 100644 --- a/src/Umbraco.Web/Trees/UrlHelperExtensions.cs +++ b/src/Umbraco.Web/Trees/UrlHelperExtensions.cs @@ -17,5 +17,15 @@ namespace Umbraco.Web.Trees return actionUrl; } + public static string GetMenuUrl(this UrlHelper urlHelper, Type treeType, string nodeId, FormDataCollection queryStrings) + { + var actionUrl = urlHelper.GetUmbracoApiService("GetMenu", treeType) + .EnsureEndsWith('?'); + + //now we need to append the query strings + actionUrl += "id=" + nodeId.EnsureEndsWith('&') + queryStrings.ToQueryString("id"); + return actionUrl; + } + } } \ No newline at end of file diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index e481ad5a80..21abfb738b 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -328,6 +328,7 @@ +