From 3ae16e717cdaa5e49cb3e98436b9e5cce7f81e9f Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 30 May 2017 00:48:41 +0200 Subject: [PATCH] Merge branch 'dev-v7-pluggable-ISearchableTree' of https://github.com/efabioli/Umbraco-CMS into efabioli-dev-v7-pluggable-ISearchableTree # Conflicts: # src/Umbraco.Web/Editors/EntityController.cs # src/Umbraco.Web/Models/ContentEditing/EntityBasic.cs --- .../src/common/services/search.service.js | 251 ++++++------- src/Umbraco.Web/Editors/EntityController.cs | 352 +++--------------- .../ContentEditing/EntityTypeSearchResult.cs | 19 - .../Models/ContentEditing/SearchResultItem.cs | 30 +- .../Models/ContentEditing/TreeSearchResult.cs | 35 ++ .../Models/Mapping/EntityModelMapper.cs | 11 +- src/Umbraco.Web/PluginManagerExtensions.cs | 23 +- .../Trees/ApplicationTreeController.cs | 2 +- .../Trees/ContentTreeController.cs | 67 ++-- src/Umbraco.Web/Trees/ISearchableTree.cs | 10 - src/Umbraco.Web/Trees/MediaTreeController.cs | 13 +- src/Umbraco.Web/Trees/MemberTreeController.cs | 12 +- src/Umbraco.Web/Trees/TreeControllerBase.cs | 1 + src/Umbraco.Web/Umbraco.Web.csproj | 9 +- src/Umbraco.Web/WebBootManager.cs | 3 + 15 files changed, 301 insertions(+), 537 deletions(-) delete mode 100644 src/Umbraco.Web/Models/ContentEditing/EntityTypeSearchResult.cs create mode 100644 src/Umbraco.Web/Models/ContentEditing/TreeSearchResult.cs delete mode 100644 src/Umbraco.Web/Trees/ISearchableTree.cs diff --git a/src/Umbraco.Web.UI.Client/src/common/services/search.service.js b/src/Umbraco.Web.UI.Client/src/common/services/search.service.js index f945070a2a..ad30e48335 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/search.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/search.service.js @@ -21,154 +21,135 @@ * */ angular.module('umbraco.services') -.factory('searchService', function ($q, $log, entityResource, contentResource, umbRequestHelper) { + .factory('searchService', function ($q, $log, entityResource, contentResource, umbRequestHelper, $injector, searchResultFormatter) { - function configureMemberResult(member) { - member.menuUrl = umbRequestHelper.getApiUrl("memberTreeBaseUrl", "GetMenu", [{ id: member.id }, { application: 'member' }]); - member.editorPath = "member/member/edit/" + (member.key ? member.key : member.id); - angular.extend(member.metaData, { treeAlias: "member" }); - member.subTitle = member.metaData.Email; - } - - function configureMediaResult(media) - { - media.menuUrl = umbRequestHelper.getApiUrl("mediaTreeBaseUrl", "GetMenu", [{ id: media.id }, { application: 'media' }]); - media.editorPath = "media/media/edit/" + media.id; - angular.extend(media.metaData, { treeAlias: "media" }); - } - - function configureContentResult(content) { - content.menuUrl = umbRequestHelper.getApiUrl("contentTreeBaseUrl", "GetMenu", [{ id: content.id }, { application: 'content' }]); - content.editorPath = "content/content/edit/" + content.id; - angular.extend(content.metaData, { treeAlias: "content" }); - content.subTitle = content.metaData.Url; - } + return { - return { + /** + * @ngdoc method + * @name umbraco.services.searchService#searchMembers + * @methodOf umbraco.services.searchService + * + * @description + * Searches the default member search index + * @param {Object} args argument object + * @param {String} args.term seach term + * @returns {Promise} returns promise containing all matching members + */ + searchMembers: function (args) { - /** - * @ngdoc method - * @name umbraco.services.searchService#searchMembers - * @methodOf umbraco.services.searchService - * - * @description - * Searches the default member search index - * @param {Object} args argument object - * @param {String} args.term seach term - * @returns {Promise} returns promise containing all matching members - */ - searchMembers: function(args) { + if (!args.term) { + throw "args.term is required"; + } - if (!args.term) { - throw "args.term is required"; - } - - return entityResource.search(args.term, "Member", args.searchFrom).then(function (data) { - _.each(data, function(item) { - configureMemberResult(item); - }); - return data; - }); - }, - - /** - * @ngdoc method - * @name umbraco.services.searchService#searchContent - * @methodOf umbraco.services.searchService - * - * @description - * Searches the default internal content search index - * @param {Object} args argument object - * @param {String} args.term seach term - * @returns {Promise} returns promise containing all matching content items - */ - searchContent: function(args) { - - if (!args.term) { - throw "args.term is required"; - } - - return entityResource.search(args.term, "Document", args.searchFrom, args.canceler).then(function (data) { - _.each(data, function (item) { - configureContentResult(item); + return entityResource.search(args.term, "Member", args.searchFrom).then(function (data) { + _.each(data, function (item) { + configureMemberResult(item); + }); + return data; }); - return data; - }); - }, + }, - /** - * @ngdoc method - * @name umbraco.services.searchService#searchMedia - * @methodOf umbraco.services.searchService - * - * @description - * Searches the default media search index - * @param {Object} args argument object - * @param {String} args.term seach term - * @returns {Promise} returns promise containing all matching media items - */ - searchMedia: function(args) { + /** + * @ngdoc method + * @name umbraco.services.searchService#searchContent + * @methodOf umbraco.services.searchService + * + * @description + * Searches the default internal content search index + * @param {Object} args argument object + * @param {String} args.term seach term + * @returns {Promise} returns promise containing all matching content items + */ + searchContent: function (args) { - if (!args.term) { - throw "args.term is required"; - } + if (!args.term) { + throw "args.term is required"; + } - return entityResource.search(args.term, "Media", args.searchFrom).then(function (data) { - _.each(data, function (item) { - configureMediaResult(item); + return entityResource.search(args.term, "Document", args.searchFrom, args.canceler).then(function (data) { + _.each(data, function (item) { + configureContentResult(item); + }); + return data; }); - return data; - }); - }, + }, - /** - * @ngdoc method - * @name umbraco.services.searchService#searchAll - * @methodOf umbraco.services.searchService - * - * @description - * Searches all available indexes and returns all results in one collection - * @param {Object} args argument object - * @param {String} args.term seach term - * @returns {Promise} returns promise containing all matching items - */ - searchAll: function (args) { - - if (!args.term) { - throw "args.term is required"; - } + /** + * @ngdoc method + * @name umbraco.services.searchService#searchMedia + * @methodOf umbraco.services.searchService + * + * @description + * Searches the default media search index + * @param {Object} args argument object + * @param {String} args.term seach term + * @returns {Promise} returns promise containing all matching media items + */ + searchMedia: function (args) { - return entityResource.searchAll(args.term, args.canceler).then(function (data) { + if (!args.term) { + throw "args.term is required"; + } - _.each(data, function(resultByType) { - switch(resultByType.type) { - case "Document": - _.each(resultByType.results, function (item) { - configureContentResult(item); - }); - break; - case "Media": - _.each(resultByType.results, function (item) { - configureMediaResult(item); - }); - break; - case "Member": - _.each(resultByType.results, function (item) { - configureMemberResult(item); - }); - break; - } + return entityResource.search(args.term, "Media", args.searchFrom).then(function (data) { + _.each(data, function (item) { + configureMediaResult(item); + }); + return data; + }); + }, + + /** + * @ngdoc method + * @name umbraco.services.searchService#searchAll + * @methodOf umbraco.services.searchService + * + * @description + * Searches all available indexes and returns all results in one collection + * @param {Object} args argument object + * @param {String} args.term seach term + * @returns {Promise} returns promise containing all matching items + */ + searchAll: function (args) { + + if (!args.term) { + throw "args.term is required"; + } + + return entityResource.searchAll(args.term, args.canceler).then(function (data) { + + _.each(data, function (resultByType) { + + var formatterMethod = searchResultFormatter.configureDefaultResult; + if (resultByType.jsSvc) { + var searchFormatterService = $injector.get(resultByType.jsSvc); + if (searchFormatterService) { + if (!resultByType.jsMethod) { + resultByType.jsMethod = "format"; + } + formatterMethod = searchFormatterService[resultByType.jsMethod]; + + if (!formatterMethod) { + throw "The method " + resultByType.jsMethod + " on the angular service " + resultByType.jsSvc + " could not be found"; + } + } + } + + _.each(resultByType.results, function (item) { + formatterMethod.apply(this, [item, resultByType.treeAlias, resultByType.appAlias]); + }); + }); + + return data; }); - return data; - }); - - }, + }, - //TODO: This doesn't do anything! - setCurrent: function(sectionAlias) { + //TODO: This doesn't do anything! + setCurrent: function (sectionAlias) { - var currentSection = sectionAlias; - } - }; -}); \ No newline at end of file + var currentSection = sectionAlias; + } + }; + }); \ No newline at end of file diff --git a/src/Umbraco.Web/Editors/EntityController.cs b/src/Umbraco.Web/Editors/EntityController.cs index 48da53e263..a26fead877 100644 --- a/src/Umbraco.Web/Editors/EntityController.cs +++ b/src/Umbraco.Web/Editors/EntityController.cs @@ -20,6 +20,7 @@ using System.Text.RegularExpressions; using Umbraco.Core.Persistence.DatabaseModelDefinitions; using System.Web.Http.Controllers; using Umbraco.Core.Xml; +using Umbraco.Web.Search; namespace Umbraco.Web.Editors { @@ -50,7 +51,9 @@ namespace Umbraco.Web.Editors new ParameterSwapControllerActionSelector.ParameterSwapInfo("GetById", "id", typeof(int), typeof(Guid), typeof(Udi)), new ParameterSwapControllerActionSelector.ParameterSwapInfo("GetByIds", "ids", typeof(int[]), typeof(Guid[]), typeof(Udi[])))); } - } + } + + private readonly UmbracoTreeSearcher _treeSearcher = new UmbracoTreeSearcher(); /// /// Returns an Umbraco alias given a string @@ -104,40 +107,49 @@ namespace Umbraco.Web.Editors /// methods might be used in things like pickers in the content editor. /// [HttpGet] - public IEnumerable SearchAll(string query) + public IEnumerable SearchAll(string query) { if (string.IsNullOrEmpty(query)) - return Enumerable.Empty(); + return Enumerable.Empty(); + var result = new List(); var allowedSections = Security.CurrentUser.AllowedSections.ToArray(); - - var result = new List(); - - if (allowedSections.InvariantContains(Constants.Applications.Content)) + var searchableTrees = SearchableTreeResolver.Current.SearchableTrees; + foreach (var searchableTree in searchableTrees) { - result.Add(new EntityTypeSearchResult - { - Results = ExamineSearch(query, UmbracoEntityTypes.Document), - EntityType = UmbracoEntityTypes.Document.ToString() + if (allowedSections.Contains(searchableTree.AppAlias)) + { + var attribute = searchableTree.SearchableTree.GetType().GetCustomAttribute(false); + + int total; + result.Add(new TreeSearchResult + { + Results = searchableTree.SearchableTree.Search(query, 200, 0, out total), + TreeAlias = searchableTree.TreeAlias, + AppAlias = searchableTree.AppAlias, + JsFormatterService = attribute == null ? "" : attribute.ServiceName, + JsFormatterMethod = attribute == null ? "" : attribute.MethodName }); - } - if (allowedSections.InvariantContains(Constants.Applications.Media)) - { - result.Add(new EntityTypeSearchResult - { - Results = ExamineSearch(query, UmbracoEntityTypes.Media), - EntityType = UmbracoEntityTypes.Media.ToString() - }); - } - if (allowedSections.InvariantContains(Constants.Applications.Members)) - { - result.Add(new EntityTypeSearchResult - { - Results = ExamineSearch(query, UmbracoEntityTypes.Member), - EntityType = UmbracoEntityTypes.Member.ToString() - }); + } + } - } + //if (allowedSections.InvariantContains(Constants.Applications.Media)) + //{ + // result.Add(new TreeSearchResult + // { + // Results = ExamineSearch(query, UmbracoEntityTypes.Media), + // TreeTypeAlias = UmbracoEntityTypes.Media.ToString() + // }); + //} + //if (allowedSections.InvariantContains(Constants.Applications.Members)) + //{ + // result.Add(new TreeSearchResult + // { + // Results = ExamineSearch(query, UmbracoEntityTypes.Member), + // TreeTypeAlias = UmbracoEntityTypes.Member.ToString() + // }); + + //} return result; } @@ -458,7 +470,7 @@ namespace Umbraco.Web.Editors //quite a bit of plumbing to do in the Services/Repository, we'll revert to a paged search int total; - var searchResult = ExamineSearch(filter ?? "", type, pageSize, pageNumber - 1, out total, id); + var searchResult = _treeSearcher.ExamineSearch(Umbraco, filter ?? "", type, pageSize, pageNumber - 1, out total, id); return new PagedResult(total, pageNumber, pageSize) { @@ -591,288 +603,12 @@ namespace Umbraco.Web.Editors /// /// /// - private IEnumerable ExamineSearch(string query, UmbracoEntityTypes entityType, string searchFrom = null) + private IEnumerable ExamineSearch(string query, UmbracoEntityTypes entityType, string searchFrom = null) { int total; - return ExamineSearch(query, entityType, 200, 0, out total, searchFrom); + return _treeSearcher.ExamineSearch(Umbraco, query, entityType, 200, 0, out total, searchFrom); } - - /// - /// Searches for results based on the entity type - /// - /// - /// - /// - /// - /// A starting point for the search, generally a node id, but for members this is a member type alias - /// - /// - /// - /// - private IEnumerable ExamineSearch(string query, UmbracoEntityTypes entityType, int pageSize, int pageIndex, out int totalFound, string searchFrom = null) - { - //TODO: We need to update this to support paging - - var sb = new StringBuilder(); - - string type; - var searcher = Constants.Examine.InternalSearcher; - var fields = new[] { "id", "__NodeId" }; - - //TODO: WE should really just allow passing in a lucene raw query - switch (entityType) - { - case UmbracoEntityTypes.Member: - searcher = Constants.Examine.InternalMemberSearcher; - type = "member"; - fields = new[] { "id", "__NodeId", "email", "loginName"}; - if (searchFrom != null && searchFrom != Constants.Conventions.MemberTypes.AllMembersListId && searchFrom.Trim() != "-1") - { - sb.Append("+__NodeTypeAlias:"); - sb.Append(searchFrom); - sb.Append(" "); - } - break; - case UmbracoEntityTypes.Media: - type = "media"; - - var mediaSearchFrom = int.MinValue; - - if (Security.CurrentUser.StartMediaId > 0 || - //if searchFrom is specified and it is greater than 0 - (searchFrom != null && int.TryParse(searchFrom, out mediaSearchFrom) && mediaSearchFrom > 0)) - { - sb.Append("+__Path: \\-1*\\,"); - sb.Append(mediaSearchFrom > 0 - ? mediaSearchFrom.ToString(CultureInfo.InvariantCulture) - : Security.CurrentUser.StartMediaId.ToString(CultureInfo.InvariantCulture)); - sb.Append("\\,* "); - } - break; - case UmbracoEntityTypes.Document: - type = "content"; - - var contentSearchFrom = int.MinValue; - - if (Security.CurrentUser.StartContentId > 0 || - //if searchFrom is specified and it is greater than 0 - (searchFrom != null && int.TryParse(searchFrom, out contentSearchFrom) && contentSearchFrom > 0)) - { - sb.Append("+__Path: \\-1*\\,"); - sb.Append(contentSearchFrom > 0 - ? contentSearchFrom.ToString(CultureInfo.InvariantCulture) - : Security.CurrentUser.StartContentId.ToString(CultureInfo.InvariantCulture)); - sb.Append("\\,* "); - } - break; - default: - throw new NotSupportedException("The " + typeof(EntityController) + " currently does not support searching against object type " + entityType); - } - - var internalSearcher = ExamineManager.Instance.SearchProviderCollection[searcher]; - - //build a lucene query: - // the __nodeName will be boosted 10x without wildcards - // then __nodeName will be matched normally with wildcards - // the rest will be normal without wildcards - - - //check if text is surrounded by single or double quotes, if so, then exact match - var surroundedByQuotes = Regex.IsMatch(query, "^\".*?\"$") - || Regex.IsMatch(query, "^\'.*?\'$"); - - if (surroundedByQuotes) - { - //strip quotes, escape string, the replace again - query = query.Trim(new[] { '\"', '\'' }); - - query = Lucene.Net.QueryParsers.QueryParser.Escape(query); - - //nothing to search - if (searchFrom.IsNullOrWhiteSpace() && query.IsNullOrWhiteSpace()) - { - totalFound = 0; - return new List(); - } - - //update the query with the query term - if (query.IsNullOrWhiteSpace() == false) - { - //add back the surrounding quotes - query = string.Format("{0}{1}{0}", "\"", query); - - //node name exactly boost x 10 - sb.Append("+(__nodeName: ("); - sb.Append(query.ToLower()); - sb.Append(")^10.0 "); - - foreach (var f in fields) - { - //additional fields normally - sb.Append(f); - sb.Append(": ("); - sb.Append(query); - sb.Append(") "); - } - - sb.Append(") "); - } - } - else - { - var trimmed = query.Trim(new[] {'\"', '\''}); - - //nothing to search - if (searchFrom.IsNullOrWhiteSpace() && trimmed.IsNullOrWhiteSpace()) - { - totalFound = 0; - return new List(); - } - - //update the query with the query term - if (trimmed.IsNullOrWhiteSpace() == false) - { - query = Lucene.Net.QueryParsers.QueryParser.Escape(query); - - var querywords = query.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); - - //node name exactly boost x 10 - sb.Append("+(__nodeName:"); - sb.Append("\""); - sb.Append(query.ToLower()); - sb.Append("\""); - sb.Append("^10.0 "); - - //node name normally with wildcards - sb.Append(" __nodeName:"); - sb.Append("("); - foreach (var w in querywords) - { - sb.Append(w.ToLower()); - sb.Append("* "); - } - sb.Append(") "); - - - foreach (var f in fields) - { - //additional fields normally - sb.Append(f); - sb.Append(":"); - sb.Append("("); - foreach (var w in querywords) - { - sb.Append(w.ToLower()); - sb.Append("* "); - } - sb.Append(")"); - sb.Append(" "); - } - - sb.Append(") "); - } - } - - //must match index type - sb.Append("+__IndexType:"); - sb.Append(type); - - var raw = internalSearcher.CreateSearchCriteria().RawQuery(sb.ToString()); - - var result = internalSearcher - //only return the number of items specified to read up to the amount of records to fill from 0 -> the number of items on the page requested - .Search(raw, pageSize * (pageIndex + 1)); - - totalFound = result.TotalItemCount; - - var pagedResult = result.Skip(pageIndex); - - switch (entityType) - { - case UmbracoEntityTypes.Member: - return MemberFromSearchResults(pagedResult.ToArray()); - case UmbracoEntityTypes.Media: - return MediaFromSearchResults(pagedResult); - case UmbracoEntityTypes.Document: - return ContentFromSearchResults(pagedResult); - default: - throw new NotSupportedException("The " + typeof(EntityController) + " currently does not support searching against object type " + entityType); - } - } - - /// - /// Returns a collection of entities for media based on search results - /// - /// - /// - private IEnumerable MemberFromSearchResults(SearchResult[] results) - { - var mapped = Mapper.Map>(results).ToArray(); - //add additional data - foreach (var m in mapped) - { - //if no icon could be mapped, it will be set to document, so change it to picture - if (m.Icon == "icon-document") - { - m.Icon = "icon-user"; - } - - var searchResult = results.First(x => x.Id.ToInvariantString() == m.Id.ToString()); - if (searchResult.Fields.ContainsKey("email") && searchResult.Fields["email"] != null) - { - m.AdditionalData["Email"] = results.First(x => x.Id.ToInvariantString() == m.Id.ToString()).Fields["email"]; - } - if (searchResult.Fields.ContainsKey("__key") && searchResult.Fields["__key"] != null) - { - Guid key; - if (Guid.TryParse(searchResult.Fields["__key"], out key)) - { - m.Key = key; - } - } - } - return mapped; - } - - /// - /// Returns a collection of entities for media based on search results - /// - /// - /// - private IEnumerable MediaFromSearchResults(IEnumerable results) - { - var mapped = Mapper.Map>(results).ToArray(); - //add additional data - foreach (var m in mapped) - { - //if no icon could be mapped, it will be set to document, so change it to picture - if (m.Icon == "icon-document") - { - m.Icon = "icon-picture"; - } - } - return mapped; - } - - /// - /// Returns a collection of entities for content based on search results - /// - /// - /// - private IEnumerable ContentFromSearchResults(IEnumerable results) - { - var mapped = Mapper.Map>(results).ToArray(); - //add additional data - foreach (var m in mapped) - { - var intId = m.Id.TryConvertTo(); - if (intId.Success) - { - m.AdditionalData["Url"] = Umbraco.NiceUrl(intId.Result); - } - } - return mapped; - } + private IEnumerable GetResultForChildren(int id, UmbracoEntityTypes entityType) { diff --git a/src/Umbraco.Web/Models/ContentEditing/EntityTypeSearchResult.cs b/src/Umbraco.Web/Models/ContentEditing/EntityTypeSearchResult.cs deleted file mode 100644 index 47dd5005a6..0000000000 --- a/src/Umbraco.Web/Models/ContentEditing/EntityTypeSearchResult.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System.Collections.Generic; -using System.Runtime.Serialization; -using Examine; - -namespace Umbraco.Web.Models.ContentEditing -{ - /// - /// Represents a search result by entity type - /// - [DataContract(Name = "searchResult", Namespace = "")] - public class EntityTypeSearchResult - { - [DataMember(Name = "type")] - public string EntityType { get; set; } - - [DataMember(Name = "results")] - public IEnumerable Results { get; set; } - } -} \ No newline at end of file diff --git a/src/Umbraco.Web/Models/ContentEditing/SearchResultItem.cs b/src/Umbraco.Web/Models/ContentEditing/SearchResultItem.cs index b846812e88..495c5d0c30 100644 --- a/src/Umbraco.Web/Models/ContentEditing/SearchResultItem.cs +++ b/src/Umbraco.Web/Models/ContentEditing/SearchResultItem.cs @@ -1,25 +1,15 @@ -namespace Umbraco.Web.Models.ContentEditing +using System.Runtime.Serialization; + +namespace Umbraco.Web.Models.ContentEditing { - public class SearchResultItem - { + [DataContract(Name = "searchResult", Namespace = "")] + public class SearchResultItem : EntityBasic + { /// - /// The string representation of the ID, used for Web responses + /// The score of the search result /// - public string Id { get; set; } - - /// - /// The name/title of the search result item - /// - public string Title { get; set; } - - /// - /// The rank of the search result - /// - public int Rank { get; set; } - - /// - /// Description/Synopsis of the item - /// - public string Description { get; set; } + [DataMember(Name = "score")] + public float Score { get; set; } + } } \ No newline at end of file diff --git a/src/Umbraco.Web/Models/ContentEditing/TreeSearchResult.cs b/src/Umbraco.Web/Models/ContentEditing/TreeSearchResult.cs new file mode 100644 index 0000000000..1da9d61c98 --- /dev/null +++ b/src/Umbraco.Web/Models/ContentEditing/TreeSearchResult.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; +using System.Runtime.Serialization; +using Examine; + +namespace Umbraco.Web.Models.ContentEditing +{ + /// + /// Represents a search result by entity type + /// + [DataContract(Name = "searchResult", Namespace = "")] + public class TreeSearchResult + { + [DataMember(Name = "appAlias")] + public string AppAlias { get; set; } + + [DataMember(Name = "treeAlias")] + public string TreeAlias { get; set; } + + /// + /// This is optional but if specified should be the name of an angular service to format the search result. + /// + [DataMember(Name = "jsSvc")] + public string JsFormatterService { get; set; } + + /// + /// This is optional but if specified should be the name of a method on the jsSvc angular service to use, if not + /// specfied than it will expect the method to be called `format(searchResult, appAlias, treeAlias)` + /// + [DataMember(Name = "jsMethod")] + public string JsFormatterMethod { get; set; } + + [DataMember(Name = "results")] + public IEnumerable Results { get; set; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Models/Mapping/EntityModelMapper.cs b/src/Umbraco.Web/Models/Mapping/EntityModelMapper.cs index 5be9d550e5..3158530b71 100644 --- a/src/Umbraco.Web/Models/Mapping/EntityModelMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/EntityModelMapper.cs @@ -89,8 +89,9 @@ namespace Umbraco.Web.Models.Mapping .ForMember(dto => dto.Trashed, expression => expression.Ignore()) .ForMember(x => x.AdditionalData, expression => expression.Ignore()); - config.CreateMap() + config.CreateMap() //default to document icon + .ForMember(x => x.Score, expression => expression.MapFrom(result => result.Score)) .ForMember(x => x.Udi, expression => expression.Ignore()) .ForMember(x => x.Icon, expression => expression.Ignore()) .ForMember(x => x.Id, expression => expression.MapFrom(result => result.Id)) @@ -156,11 +157,11 @@ namespace Umbraco.Web.Models.Mapping } }); - config.CreateMap>() - .ConvertUsing(results => results.Select(Mapper.Map).ToList()); + config.CreateMap>() + .ConvertUsing(results => results.Select(Mapper.Map).ToList()); - config.CreateMap, IEnumerable>() - .ConvertUsing(results => results.Select(Mapper.Map).ToList()); + config.CreateMap, IEnumerable>() + .ConvertUsing(results => results.Select(Mapper.Map).ToList()); } } } \ No newline at end of file diff --git a/src/Umbraco.Web/PluginManagerExtensions.cs b/src/Umbraco.Web/PluginManagerExtensions.cs index bda4b3a95f..76b71245e5 100644 --- a/src/Umbraco.Web/PluginManagerExtensions.cs +++ b/src/Umbraco.Web/PluginManagerExtensions.cs @@ -9,6 +9,7 @@ using Umbraco.Web.Trees; using Umbraco.Web.WebApi; using umbraco; using umbraco.interfaces; +using Umbraco.Web.Search; namespace Umbraco.Web { @@ -48,12 +49,22 @@ namespace Umbraco.Web return resolver.ResolveTypes(); } - /// - /// Returns all classes attributed with RestExtensionAttribute attribute - /// - /// - /// - internal static IEnumerable ResolveRestExtensions(this PluginManager resolver) + /// + /// Returns all available in application + /// + /// + /// + internal static IEnumerable ResolveSearchableTrees(this PluginManager resolver) + { + return resolver.ResolveTypes(); + } + + /// + /// Returns all classes attributed with RestExtensionAttribute attribute + /// + /// + /// + internal static IEnumerable ResolveRestExtensions(this PluginManager resolver) { return resolver.ResolveAttributedTypes(); } diff --git a/src/Umbraco.Web/Trees/ApplicationTreeController.cs b/src/Umbraco.Web/Trees/ApplicationTreeController.cs index f6b5e82fc3..60948726ec 100644 --- a/src/Umbraco.Web/Trees/ApplicationTreeController.cs +++ b/src/Umbraco.Web/Trees/ApplicationTreeController.cs @@ -42,7 +42,7 @@ namespace Umbraco.Web.Trees //find all tree definitions that have the current application alias var appTrees = ApplicationContext.Current.Services.ApplicationTreeService.GetApplicationTrees(application, onlyInitialized).ToArray(); - if (appTrees.Count() == 1 || string.IsNullOrEmpty(tree) == false ) + if (appTrees.Length == 1 || string.IsNullOrEmpty(tree) == false ) { var apptree = string.IsNullOrEmpty(tree) == false ? appTrees.SingleOrDefault(x => x.Alias == tree) diff --git a/src/Umbraco.Web/Trees/ContentTreeController.cs b/src/Umbraco.Web/Trees/ContentTreeController.cs index a9f0bda805..bac1cb0d3c 100644 --- a/src/Umbraco.Web/Trees/ContentTreeController.cs +++ b/src/Umbraco.Web/Trees/ContentTreeController.cs @@ -18,6 +18,8 @@ using umbraco.BusinessLogic.Actions; using umbraco.businesslogic; using umbraco.cms.businesslogic.web; using umbraco.interfaces; +using Umbraco.Web.Models.ContentEditing; +using Umbraco.Web.Search; using Constants = Umbraco.Core.Constants; namespace Umbraco.Web.Trees @@ -25,8 +27,8 @@ namespace Umbraco.Web.Trees //We will not allow the tree to render unless the user has access to any of the sections that the tree gets rendered // this is not ideal but until we change permissions to be tree based (not section) there's not much else we can do here. [UmbracoApplicationAuthorize( - Constants.Applications.Content, - Constants.Applications.Media, + Constants.Applications.Content, + Constants.Applications.Media, Constants.Applications.Users, Constants.Applications.Settings, Constants.Applications.Developer, @@ -34,14 +36,17 @@ namespace Umbraco.Web.Trees [LegacyBaseTree(typeof(loadContent))] [Tree(Constants.Applications.Content, Constants.Trees.Content)] [PluginController("UmbracoTrees")] - [CoreTree] - public class ContentTreeController : ContentTreeControllerBase - { - + [CoreTree] + [SearchableTree("searchResultFormatter", "configureContentResult")] + public class ContentTreeController : ContentTreeControllerBase, ISearchableTree + { + + private readonly UmbracoTreeSearcher _treeSearcher = new UmbracoTreeSearcher(); + protected override TreeNode CreateRootNode(FormDataCollection queryStrings) { - var node = base.CreateRootNode(queryStrings); - //if the user's start node is not default, then ensure the root doesn't have a menu + var node = base.CreateRootNode(queryStrings); + //if the user's start node is not default, then ensure the root doesn't have a menu if (Security.CurrentUser.StartContentId != Constants.System.Root) { node.MenuUrl = ""; @@ -63,8 +68,8 @@ namespace Umbraco.Web.Trees protected override int UserStartNode { get { return Security.CurrentUser.StartContentId; } - } - + } + /// /// Creates a tree node for a content item based on an UmbracoEntity /// @@ -74,7 +79,7 @@ namespace Umbraco.Web.Trees /// protected override TreeNode GetSingleTreeNode(IUmbracoEntity e, string parentId, FormDataCollection queryStrings) { - var entity = (UmbracoEntity) e; + var entity = (UmbracoEntity)e; var allowedUserOptions = GetAllowedUserMenuItemsForNode(e); if (CanUserAccessNode(e, allowedUserOptions)) @@ -87,7 +92,7 @@ namespace Umbraco.Web.Trees entity, Constants.ObjectTypes.DocumentGuid, parentId, - queryStrings, + queryStrings, entity.HasChildren && (isContainer == false)); node.AdditionalData.Add("contentType", entity.ContentTypeAlias); @@ -96,9 +101,9 @@ namespace Umbraco.Web.Trees { node.AdditionalData.Add("isContainer", true); node.SetContainerStyle(); - } - - + } + + if (entity.IsPublished == false) node.SetNotPublishedStyle(); @@ -149,8 +154,8 @@ namespace Umbraco.Web.Trees // add default actions for *all* users menu.Items.Add(ui.Text("actions", ActionRePublish.Instance.Alias)).ConvertLegacyMenuItem(null, "content", "content"); - menu.Items.Add(ui.Text("actions", ActionRefresh.Instance.Alias), true); - + menu.Items.Add(ui.Text("actions", ActionRefresh.Instance.Alias), true); + return menu; } @@ -168,12 +173,12 @@ namespace Umbraco.Web.Trees } var nodeMenu = GetAllNodeMenuItems(item); - var allowedMenuItems = GetAllowedUserMenuItemsForNode(item); - + var allowedMenuItems = GetAllowedUserMenuItemsForNode(item); + FilterUserAllowedMenuItems(nodeMenu, allowedMenuItems); //if the media item is in the recycle bin, don't have a default menu, just show the regular menu - if (item.Path.Split(new[] {','}, StringSplitOptions.RemoveEmptyEntries).Contains(RecycleBinId.ToInvariantString())) + if (item.Path.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Contains(RecycleBinId.ToInvariantString())) { nodeMenu.DefaultMenuAlias = null; nodeMenu.Items.Insert(2, new MenuItem(ActionRestore.Instance, ui.Text("actions", ActionRestore.Instance.Alias))); @@ -181,10 +186,10 @@ namespace Umbraco.Web.Trees else { //set the default to create - nodeMenu.DefaultMenuAlias = ActionNew.Instance.Alias; - } - - + nodeMenu.DefaultMenuAlias = ActionNew.Instance.Alias; + } + + return nodeMenu; } @@ -224,9 +229,9 @@ namespace Umbraco.Web.Trees { var menu = new MenuItemCollection(); menu.Items.Add(ui.Text("actions", ActionNew.Instance.Alias)); - menu.Items.Add(ui.Text("actions", ActionDelete.Instance.Alias)); - - //need to ensure some of these are converted to the legacy system - until we upgrade them all to be angularized. + menu.Items.Add(ui.Text("actions", ActionDelete.Instance.Alias)); + + //need to ensure some of these are converted to the legacy system - until we upgrade them all to be angularized. menu.Items.Add(ui.Text("actions", ActionMove.Instance.Alias), true); menu.Items.Add(ui.Text("actions", ActionCopy.Instance.Alias)); menu.Items.Add(ui.Text("actions", ActionChangeDocType.Instance.Alias)).ConvertLegacyMenuItem(item, "content", "content"); @@ -239,8 +244,8 @@ namespace Umbraco.Web.Trees menu.Items.Add(ui.Text("actions", ActionToPublish.Instance.Alias)).ConvertLegacyMenuItem(item, "content", "content"); menu.Items.Add(ui.Text("actions", ActionAssignDomain.Instance.Alias)).ConvertLegacyMenuItem(item, "content", "content"); menu.Items.Add(ui.Text("actions", ActionRights.Instance.Alias)).ConvertLegacyMenuItem(item, "content", "content"); - menu.Items.Add(ui.Text("actions", ActionProtect.Instance.Alias), true).ConvertLegacyMenuItem(item, "content", "content"); - + menu.Items.Add(ui.Text("actions", ActionProtect.Instance.Alias), true).ConvertLegacyMenuItem(item, "content", "content"); + menu.Items.Add(ui.Text("actions", ActionNotify.Instance.Alias), true).ConvertLegacyMenuItem(item, "content", "content"); menu.Items.Add(ui.Text("actions", ActionSendToTranslate.Instance.Alias)).ConvertLegacyMenuItem(item, "content", "content"); @@ -249,5 +254,9 @@ namespace Umbraco.Web.Trees return menu; } + public IEnumerable Search(string query, int pageSize, int pageIndex, out int totalFound, string searchFrom = null) + { + return _treeSearcher.ExamineSearch(Umbraco, query, UmbracoEntityTypes.Document, pageSize, pageIndex, out totalFound, searchFrom); + } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Trees/ISearchableTree.cs b/src/Umbraco.Web/Trees/ISearchableTree.cs deleted file mode 100644 index 680256f304..0000000000 --- a/src/Umbraco.Web/Trees/ISearchableTree.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Collections.Generic; -using Umbraco.Web.Models.ContentEditing; - -namespace Umbraco.Web.Trees -{ - public interface ISearchableTree - { - IEnumerable Search(string searchText); - } -} \ No newline at end of file diff --git a/src/Umbraco.Web/Trees/MediaTreeController.cs b/src/Umbraco.Web/Trees/MediaTreeController.cs index ed72857a40..854ba664c5 100644 --- a/src/Umbraco.Web/Trees/MediaTreeController.cs +++ b/src/Umbraco.Web/Trees/MediaTreeController.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Http.Formatting; @@ -13,6 +14,8 @@ using Umbraco.Web.Mvc; using Umbraco.Web.WebApi.Filters; using umbraco; using umbraco.BusinessLogic.Actions; +using Umbraco.Web.Models.ContentEditing; +using Umbraco.Web.Search; using Constants = Umbraco.Core.Constants; namespace Umbraco.Web.Trees @@ -29,8 +32,11 @@ namespace Umbraco.Web.Trees [Tree(Constants.Applications.Media, Constants.Trees.Media)] [PluginController("UmbracoTrees")] [CoreTree] - public class MediaTreeController : ContentTreeControllerBase + [SearchableTree("searchResultFormatter", "configureMediaResult")] + public class MediaTreeController : ContentTreeControllerBase, ISearchableTree { + private readonly UmbracoTreeSearcher _treeSearcher = new UmbracoTreeSearcher(); + protected override TreeNode CreateRootNode(FormDataCollection queryStrings) { var node = base.CreateRootNode(queryStrings); @@ -164,5 +170,10 @@ namespace Umbraco.Web.Trees } return Security.CurrentUser.HasPathAccess(media); } + + public IEnumerable Search(string query, int pageSize, int pageIndex, out int totalFound, string searchFrom = null) + { + return _treeSearcher.ExamineSearch(Umbraco, query, UmbracoEntityTypes.Media, pageSize, pageIndex, out totalFound, searchFrom); + } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Trees/MemberTreeController.cs b/src/Umbraco.Web/Trees/MemberTreeController.cs index fe7aa8a689..d9c9eec36e 100644 --- a/src/Umbraco.Web/Trees/MemberTreeController.cs +++ b/src/Umbraco.Web/Trees/MemberTreeController.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Net; @@ -16,6 +17,8 @@ using Umbraco.Web.Mvc; using Umbraco.Web.WebApi.Filters; using umbraco; using umbraco.BusinessLogic.Actions; +using Umbraco.Web.Models.ContentEditing; +using Umbraco.Web.Search; using Constants = Umbraco.Core.Constants; namespace Umbraco.Web.Trees @@ -30,7 +33,8 @@ namespace Umbraco.Web.Trees [Tree(Constants.Applications.Members, Constants.Trees.Members, null, sortOrder: 0)] [PluginController("UmbracoTrees")] [CoreTree] - public class MemberTreeController : TreeController + [SearchableTree("searchResultFormatter", "configureMemberResult")] + public class MemberTreeController : TreeController, ISearchableTree { public MemberTreeController() { @@ -38,6 +42,7 @@ namespace Umbraco.Web.Trees _isUmbracoProvider = _provider.IsUmbracoMembershipProvider(); } + private readonly UmbracoTreeSearcher _treeSearcher = new UmbracoTreeSearcher(); private readonly MembershipProvider _provider; private readonly bool _isUmbracoProvider; @@ -178,5 +183,10 @@ namespace Umbraco.Web.Trees return menu; } + + public IEnumerable Search(string query, int pageSize, int pageIndex, out int totalFound, string searchFrom = null) + { + return _treeSearcher.ExamineSearch(Umbraco, query, UmbracoEntityTypes.Member, pageSize, pageIndex, out totalFound, searchFrom); + } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Trees/TreeControllerBase.cs b/src/Umbraco.Web/Trees/TreeControllerBase.cs index 751f83db16..4547e5e32d 100644 --- a/src/Umbraco.Web/Trees/TreeControllerBase.cs +++ b/src/Umbraco.Web/Trees/TreeControllerBase.cs @@ -9,6 +9,7 @@ using Umbraco.Web.WebApi; using Umbraco.Web.WebApi.Filters; using Umbraco.Core.Models; using Umbraco.Core.Models.EntityBase; +using Umbraco.Web.Search; namespace Umbraco.Web.Trees { diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 0b5652e509..32c6234c8d 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -420,6 +420,11 @@ + + + + + @@ -565,7 +570,7 @@ - + @@ -842,7 +847,7 @@ - + diff --git a/src/Umbraco.Web/WebBootManager.cs b/src/Umbraco.Web/WebBootManager.cs index 29565065e2..a8f2180411 100644 --- a/src/Umbraco.Web/WebBootManager.cs +++ b/src/Umbraco.Web/WebBootManager.cs @@ -51,6 +51,7 @@ using Umbraco.Core.Services; using Umbraco.Web.Editors; using Umbraco.Web.HealthCheck; using Umbraco.Web.Profiling; +using Umbraco.Web.Search; using GlobalSettings = Umbraco.Core.Configuration.GlobalSettings; using ProfilingViewEngine = Umbraco.Core.Profiling.ProfilingViewEngine; @@ -374,6 +375,8 @@ namespace Umbraco.Web { base.InitializeResolvers(); + SearchableTreeResolver.Current = new SearchableTreeResolver(ServiceProvider, LoggerResolver.Current.Logger, ApplicationContext.Services.ApplicationTreeService, () => PluginManager.ResolveSearchableTrees()); + XsltExtensionsResolver.Current = new XsltExtensionsResolver(ServiceProvider, LoggerResolver.Current.Logger, () => PluginManager.ResolveXsltExtensions()); EditorValidationResolver.Current= new EditorValidationResolver(ServiceProvider, LoggerResolver.Current.Logger, () => PluginManager.ResolveTypes());