From 3ae16e717cdaa5e49cb3e98436b9e5cce7f81e9f Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 30 May 2017 00:48:41 +0200 Subject: [PATCH 1/5] 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()); From a70c9226fccdbac2994135b8d3af40474f91c43d Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 31 May 2017 12:25:05 +0200 Subject: [PATCH 2/5] Updates lots of trees for searching and gets results grouped --- .../Umbraco.Tests.Benchmarks.csproj | 5 - .../src/common/services/search.service.js | 11 +- .../services/searchresultformatter.service.js | 37 ++ .../src/controllers/search.controller.js | 74 +-- src/Umbraco.Web.UI.Client/src/less/tree.less | 48 +- .../application/umb-navigation.html | 178 +++---- src/Umbraco.Web.UI/umbraco/config/lang/en.xml | 482 ++++++++++-------- .../umbraco/config/lang/en_us.xml | 2 + src/Umbraco.Web/Editors/EntityController.cs | 64 +-- .../Models/ContentEditing/SearchResultItem.cs | 8 +- .../Models/Mapping/EntityModelMapper.cs | 38 +- src/Umbraco.Web/Search/ISearchableTree.cs | 21 + .../Search/SearchableApplicationTree.cs | 17 + .../Search/SearchableTreeAttribute.cs | 35 ++ .../Search/SearchableTreeCollection.cs | 12 + .../Search/SearchableTreeResolver.cs | 60 +++ src/Umbraco.Web/Search/UmbracoTreeSearcher.cs | 299 +++++++++++ .../Trees/ApplicationTreeExtensions.cs | 44 ++ .../Trees/ContentTreeController.cs | 12 +- .../Trees/ContentTypeTreeController.cs | 12 +- .../Trees/DataTypeTreeController.cs | 11 +- src/Umbraco.Web/Trees/MediaTreeController.cs | 2 +- .../Trees/MediaTypeTreeController.cs | 14 +- src/Umbraco.Web/Trees/MemberTreeController.cs | 2 +- .../Trees/TemplatesTreeController.cs | 11 +- src/Umbraco.Web/Trees/TreeController.cs | 31 +- 26 files changed, 1091 insertions(+), 439 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/common/services/searchresultformatter.service.js create mode 100644 src/Umbraco.Web/Search/ISearchableTree.cs create mode 100644 src/Umbraco.Web/Search/SearchableApplicationTree.cs create mode 100644 src/Umbraco.Web/Search/SearchableTreeAttribute.cs create mode 100644 src/Umbraco.Web/Search/SearchableTreeCollection.cs create mode 100644 src/Umbraco.Web/Search/SearchableTreeResolver.cs create mode 100644 src/Umbraco.Web/Search/UmbracoTreeSearcher.cs diff --git a/src/Umbraco.Tests.Benchmarks/Umbraco.Tests.Benchmarks.csproj b/src/Umbraco.Tests.Benchmarks/Umbraco.Tests.Benchmarks.csproj index f474e79876..64fef90ffa 100644 --- a/src/Umbraco.Tests.Benchmarks/Umbraco.Tests.Benchmarks.csproj +++ b/src/Umbraco.Tests.Benchmarks/Umbraco.Tests.Benchmarks.csproj @@ -116,7 +116,6 @@ - @@ -136,10 +135,6 @@ - - - - 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 ad30e48335..c692fda7ba 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 @@ -120,8 +120,12 @@ angular.module('umbraco.services') return entityResource.searchAll(args.term, args.canceler).then(function (data) { _.each(data, function (resultByType) { - + + //we need to format the search result data to include things like the subtitle, urls, etc... + // this is done with registered angular services as part of the SearchableTreeAttribute, if that + // is not found, than we format with the default formatter var formatterMethod = searchResultFormatter.configureDefaultResult; + //check if a custom formatter is specified... if (resultByType.jsSvc) { var searchFormatterService = $injector.get(resultByType.jsSvc); if (searchFormatterService) { @@ -135,10 +139,11 @@ angular.module('umbraco.services') } } } - + //now apply the formatter for each result _.each(resultByType.results, function (item) { formatterMethod.apply(this, [item, resultByType.treeAlias, resultByType.appAlias]); - }); + }); + }); return data; diff --git a/src/Umbraco.Web.UI.Client/src/common/services/searchresultformatter.service.js b/src/Umbraco.Web.UI.Client/src/common/services/searchresultformatter.service.js new file mode 100644 index 0000000000..9bf9f3762c --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/services/searchresultformatter.service.js @@ -0,0 +1,37 @@ + +function searchResultFormatter(umbRequestHelper) { + + function configureDefaultResult(content, treeAlias, appAlias) { + content.editorPath = appAlias + "/" + treeAlias + "/edit/" + content.id; + angular.extend(content.metaData, { treeAlias: treeAlias }); + } + + function configureContentResult(content, treeAlias, appAlias) { + content.menuUrl = umbRequestHelper.getApiUrl("contentTreeBaseUrl", "GetMenu", [{ id: content.id }, { application: appAlias }]); + content.editorPath = appAlias + "/" + treeAlias + "/edit/" + content.id; + angular.extend(content.metaData, { treeAlias: treeAlias }); + content.subTitle = content.metaData.Url; + } + + function configureMemberResult(member, treeAlias, appAlias) { + member.menuUrl = umbRequestHelper.getApiUrl("memberTreeBaseUrl", "GetMenu", [{ id: member.id }, { application: appAlias }]); + member.editorPath = appAlias + "/" + treeAlias + "/edit/" + (member.key ? member.key : member.id); + angular.extend(member.metaData, { treeAlias: treeAlias }); + member.subTitle = member.metaData.Email; + } + + function configureMediaResult(media, treeAlias, appAlias) { + media.menuUrl = umbRequestHelper.getApiUrl("mediaTreeBaseUrl", "GetMenu", [{ id: media.id }, { application: appAlias }]); + media.editorPath = appAlias + "/" + treeAlias + "/edit/" + media.id; + angular.extend(media.metaData, { treeAlias: treeAlias }); + } + + return { + configureContentResult: configureContentResult, + configureMemberResult: configureMemberResult, + configureMediaResult: configureMediaResult, + configureDefaultResult: configureDefaultResult + }; +} + +angular.module('umbraco.services').factory('searchResultFormatter', searchResultFormatter); \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/controllers/search.controller.js b/src/Umbraco.Web.UI.Client/src/controllers/search.controller.js index 54519f353b..d5d1ca58e2 100644 --- a/src/Umbraco.Web.UI.Client/src/controllers/search.controller.js +++ b/src/Umbraco.Web.UI.Client/src/controllers/search.controller.js @@ -15,21 +15,21 @@ function SearchController($scope, searchService, $log, $location, navigationServ $scope.selectedResult = -1; - $scope.navigateResults = function(ev){ + $scope.navigateResults = function (ev) { //38: up 40: down, 13: enter - switch(ev.keyCode){ + switch (ev.keyCode) { case 38: - iterateResults(true); + iterateResults(true); break; case 40: - iterateResults(false); + iterateResults(false); break; case 13: if ($scope.selectedItem) { $location.path($scope.selectedItem.editorPath); navigationService.hideSearch(); - } + } break; } }; @@ -39,50 +39,50 @@ function SearchController($scope, searchService, $log, $location, navigationServ var groupIndex = -1; var itemIndex = -1; $scope.selectedItem = undefined; - - function iterateResults(up){ + + function iterateResults(up) { //default group - if(!group){ + if (!group) { group = $scope.groups[0]; groupIndex = 0; } - if(up){ - if(itemIndex === 0){ - if(groupIndex === 0){ - gotoGroup($scope.groups.length-1, true); - }else{ - gotoGroup(groupIndex-1, true); + if (up) { + if (itemIndex === 0) { + if (groupIndex === 0) { + gotoGroup($scope.groups.length - 1, true); + } else { + gotoGroup(groupIndex - 1, true); } - }else{ - gotoItem(itemIndex-1); + } else { + gotoItem(itemIndex - 1); } - }else{ - if(itemIndex < group.results.length-1){ - gotoItem(itemIndex+1); - }else{ - if(groupIndex === $scope.groups.length-1){ + } else { + if (itemIndex < group.results.length - 1) { + gotoItem(itemIndex + 1); + } else { + if (groupIndex === $scope.groups.length - 1) { gotoGroup(0); - }else{ - gotoGroup(groupIndex+1); + } else { + gotoGroup(groupIndex + 1); } } } } - function gotoGroup(index, up){ + function gotoGroup(index, up) { groupIndex = index; group = $scope.groups[groupIndex]; - - if(up){ - gotoItem(group.results.length-1); - }else{ - gotoItem(0); + + if (up) { + gotoItem(group.results.length - 1); + } else { + gotoItem(0); } } - function gotoItem(index){ + function gotoItem(index) { itemIndex = index; $scope.selectedItem = group.results[itemIndex]; } @@ -91,7 +91,7 @@ function SearchController($scope, searchService, $log, $location, navigationServ var canceler = null; $scope.$watch("searchTerm", _.debounce(function (newVal, oldVal) { - $scope.$apply(function() { + $scope.$apply(function () { if ($scope.searchTerm) { if (newVal !== null && newVal !== undefined && newVal !== oldVal) { $scope.isSearching = true; @@ -107,8 +107,16 @@ function SearchController($scope, searchService, $log, $location, navigationServ canceler = $q.defer(); } - searchService.searchAll({ term: $scope.searchTerm, canceler: canceler }).then(function(result) { - $scope.groups = _.filter(result, function (group) { return group.results.length > 0; }); + searchService.searchAll({ term: $scope.searchTerm, canceler: canceler }).then(function (result) { + + //result is a dictionary of group Title and it's results + var filtered = {}; + _.each(result, function (value, key) { + if (value.results.length > 0) { + filtered[key] = value; + } + }); + $scope.groups = filtered; //set back to null so it can be re-created canceler = null; }); diff --git a/src/Umbraco.Web.UI.Client/src/less/tree.less b/src/Umbraco.Web.UI.Client/src/less/tree.less index df1bc4114a..e5d367bbd2 100644 --- a/src/Umbraco.Web.UI.Client/src/less/tree.less +++ b/src/Umbraco.Web.UI.Client/src/less/tree.less @@ -53,22 +53,21 @@ border-color: @turquoise-d1; } -.umb-tree li.root > div { - padding: 0; +.umb-tree li.root > div:first-child { + padding: 0; } -.umb-tree li.root > div h5 { - margin: 0; - width: 100%; - - display: flex; - align-items: center; +.umb-tree li.root > div h5, .umb-tree li.root > div h6 { + margin: 0; + width: 100%; + display: flex; + align-items: center; } -.umb-tree li.root > div h5 > a, .umb-tree-header { - display: flex; - padding: 20px 0 20px 20px; - box-sizing: border-box; +.umb-tree li.root > div:first-child h5 > a, .umb-tree-header { + display: flex; + padding: 20px 0 20px 20px; + box-sizing: border-box; } .umb-tree * { @@ -96,14 +95,22 @@ text-decoration: none } -.umb-tree div { +/*.umb-tree div.tree-node { padding: 5px 0 5px 0; position: relative; overflow: hidden; display: flex; flex-wrap: nowrap; align-items: center; +}*/ +.umb-tree div { + padding: 5px 0 5px 0; + position: relative; + overflow: hidden; + display: flex; + flex-wrap: nowrap; + align-items: center; } .umb-tree a.noSpr { @@ -192,6 +199,21 @@ /*color:@turquoise;*/ } +.umb-tree div.umb-search-group { + position: inherit; + display: inherit; +} + +.umb-tree div.umb-search-group:hover { + background: inherit; +} +.umb-tree div.umb-search-group h6 { + /*color: @gray-5;*/ + padding: 10px 0 10px 20px; + font-weight: inherit; + background:@gray-10; +} + .umb-tree .umb-search-group-item { padding-left: 20px; } diff --git a/src/Umbraco.Web.UI.Client/src/views/components/application/umb-navigation.html b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-navigation.html index db8fd92008..4f07a2b976 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/application/umb-navigation.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-navigation.html @@ -1,104 +1,106 @@
+ ng-mouseleave="leaveTree($event)" ng-mouseenter="enterTree($event)"> - - + + - -