diff --git a/src/Umbraco.Core/Services/EntityService.cs b/src/Umbraco.Core/Services/EntityService.cs index 2919968f9e..a9478f2ade 100644 --- a/src/Umbraco.Core/Services/EntityService.cs +++ b/src/Umbraco.Core/Services/EntityService.cs @@ -396,7 +396,7 @@ namespace Umbraco.Core.Services using (var uow = UowProvider.GetUnitOfWork()) { var repository = RepositoryFactory.CreateEntityRepository(uow); - var query = Query.Builder.Where(x => x.ParentId == parentId); + var query = Query.Builder.Where(x => x.ParentId == parentId && x.Trashed == false); IQuery filterQuery = null; if (filter.IsNullOrWhiteSpace() == false) @@ -410,6 +410,18 @@ namespace Umbraco.Core.Services } } + /// + /// Returns a paged collection of descendants + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// public IEnumerable GetPagedDescendants(int id, UmbracoObjectTypes umbracoObjectType, long pageIndex, int pageSize, out long totalRecords, string orderBy = "path", Direction orderDirection = Direction.Ascending, string filter = "") { @@ -421,8 +433,45 @@ namespace Umbraco.Core.Services var query = Query.Builder; //if the id is System Root, then just get all if (id != Constants.System.Root) + query.Where(x => x.Path.SqlContains(string.Format(",{0},", id), TextColumnType.NVarchar)); + + IQuery filterQuery = null; + if (filter.IsNullOrWhiteSpace() == false) { - query.Where(x => x.Path.SqlContains(string.Format(",{0},", id), TextColumnType.NVarchar)); + filterQuery = Query.Builder.Where(x => x.Name.Contains(filter)); + } + + var contents = repository.GetPagedResultsByQuery(query, objectTypeId, pageIndex, pageSize, out totalRecords, orderBy, orderDirection, filterQuery); + uow.Commit(); + return contents; + } + } + + /// + /// Returns a paged collection of descendants from the root + /// + /// + /// + /// + /// + /// + /// + /// + /// true/false to include trashed objects + /// + public IEnumerable GetPagedDescendantsFromRoot(UmbracoObjectTypes umbracoObjectType, long pageIndex, int pageSize, out long totalRecords, + string orderBy = "path", Direction orderDirection = Direction.Ascending, string filter = "", bool includeTrashed = true) + { + var objectTypeId = umbracoObjectType.GetGuid(); + using (var uow = UowProvider.GetUnitOfWork()) + { + var repository = RepositoryFactory.CreateEntityRepository(uow); + + var query = Query.Builder; + //don't include trashed if specfied + if (includeTrashed == false) + { + query.Where(x => x.Trashed == false); } IQuery filterQuery = null; diff --git a/src/Umbraco.Core/Services/IEntityService.cs b/src/Umbraco.Core/Services/IEntityService.cs index 0310c9f10a..46200287a5 100644 --- a/src/Umbraco.Core/Services/IEntityService.cs +++ b/src/Umbraco.Core/Services/IEntityService.cs @@ -153,9 +153,36 @@ namespace Umbraco.Core.Services IEnumerable GetPagedChildren(int parentId, UmbracoObjectTypes umbracoObjectType, long pageIndex, int pageSize, out long totalRecords, string orderBy = "SortOrder", Direction orderDirection = Direction.Ascending, string filter = ""); + /// + /// Returns a paged collection of descendants + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// IEnumerable GetPagedDescendants(int id, UmbracoObjectTypes umbracoObjectType, long pageIndex, int pageSize, out long totalRecords, string orderBy = "path", Direction orderDirection = Direction.Ascending, string filter = ""); + /// + /// Returns a paged collection of descendants from the root + /// + /// + /// + /// + /// + /// + /// + /// + /// true/false to include trashed objects + /// + IEnumerable GetPagedDescendantsFromRoot(UmbracoObjectTypes umbracoObjectType, long pageIndex, int pageSize, out long totalRecords, + string orderBy = "path", Direction orderDirection = Direction.Ascending, string filter = "", bool includeTrashed = true); + /// /// Gets a collection of descendents by the parents Id /// diff --git a/src/Umbraco.Tests/Services/EntityServiceTests.cs b/src/Umbraco.Tests/Services/EntityServiceTests.cs index b1744de429..de8cc7753d 100644 --- a/src/Umbraco.Tests/Services/EntityServiceTests.cs +++ b/src/Umbraco.Tests/Services/EntityServiceTests.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using NUnit.Framework; using Umbraco.Core; @@ -25,7 +26,7 @@ namespace Umbraco.Tests.Services public override void TearDown() { base.TearDown(); - } + } [Test] public void EntityService_Can_Get_Paged_Content_Children() @@ -85,6 +86,94 @@ namespace Umbraco.Tests.Services Assert.That(total, Is.EqualTo(60)); } + [Test] + public void EntityService_Can_Get_Paged_Content_Descendants_Including_Recycled() + { + var contentType = ServiceContext.ContentTypeService.GetContentType("umbTextpage"); + + var root = MockedContent.CreateSimpleContent(contentType); + ServiceContext.ContentService.Save(root); + var toDelete = new List(); + for (int i = 0; i < 10; i++) + { + var c1 = MockedContent.CreateSimpleContent(contentType, Guid.NewGuid().ToString(), root); + ServiceContext.ContentService.Save(c1); + + if (i % 2 == 0) + { + toDelete.Add(c1); + } + + for (int j = 0; j < 5; j++) + { + var c2 = MockedContent.CreateSimpleContent(contentType, Guid.NewGuid().ToString(), c1); + ServiceContext.ContentService.Save(c2); + } + } + + foreach (var content in toDelete) + { + ServiceContext.ContentService.MoveToRecycleBin(content); + } + + var service = ServiceContext.EntityService; + + long total; + //search at root to see if it returns recycled + var entities = service.GetPagedDescendants(-1, UmbracoObjectTypes.Document, 0, 1000, out total) + .Select(x => x.Id) + .ToArray(); + + foreach (var c in toDelete) + { + Assert.True(entities.Contains(c.Id)); + } + } + + [Test] + public void EntityService_Can_Get_Paged_Content_Descendants_Without_Recycled() + { + var contentType = ServiceContext.ContentTypeService.GetContentType("umbTextpage"); + + var root = MockedContent.CreateSimpleContent(contentType); + ServiceContext.ContentService.Save(root); + var toDelete = new List(); + for (int i = 0; i < 10; i++) + { + var c1 = MockedContent.CreateSimpleContent(contentType, Guid.NewGuid().ToString(), root); + ServiceContext.ContentService.Save(c1); + + if (i % 2 == 0) + { + toDelete.Add(c1); + } + + for (int j = 0; j < 5; j++) + { + var c2 = MockedContent.CreateSimpleContent(contentType, Guid.NewGuid().ToString(), c1); + ServiceContext.ContentService.Save(c2); + } + } + + foreach (var content in toDelete) + { + ServiceContext.ContentService.MoveToRecycleBin(content); + } + + var service = ServiceContext.EntityService; + + long total; + //search at root to see if it returns recycled + var entities = service.GetPagedDescendantsFromRoot(UmbracoObjectTypes.Document, 0, 1000, out total, includeTrashed:false) + .Select(x => x.Id) + .ToArray(); + + foreach (var c in toDelete) + { + Assert.IsFalse(entities.Contains(c.Id)); + } + } + [Test] public void EntityService_Can_Get_Paged_Content_Descendants_With_Search() { @@ -115,7 +204,7 @@ namespace Umbraco.Tests.Services Assert.That(entities.Length, Is.EqualTo(50)); Assert.That(total, Is.EqualTo(50)); } - + [Test] public void EntityService_Can_Get_Paged_Media_Children() { @@ -175,6 +264,96 @@ namespace Umbraco.Tests.Services Assert.That(total, Is.EqualTo(60)); } + [Test] + public void EntityService_Can_Get_Paged_Media_Descendants_Including_Recycled() + { + var folderType = ServiceContext.ContentTypeService.GetMediaType(1031); + var imageMediaType = ServiceContext.ContentTypeService.GetMediaType(1032); + + var root = MockedMedia.CreateMediaFolder(folderType, -1); + ServiceContext.MediaService.Save(root); + var toDelete = new List(); + for (int i = 0; i < 10; i++) + { + var c1 = MockedMedia.CreateMediaImage(imageMediaType, root.Id); + ServiceContext.MediaService.Save(c1); + + if (i % 2 == 0) + { + toDelete.Add(c1); + } + + for (int j = 0; j < 5; j++) + { + var c2 = MockedMedia.CreateMediaImage(imageMediaType, c1.Id); + ServiceContext.MediaService.Save(c2); + } + } + + foreach (var content in toDelete) + { + ServiceContext.MediaService.MoveToRecycleBin(content); + } + + var service = ServiceContext.EntityService; + + long total; + //search at root to see if it returns recycled + var entities = service.GetPagedDescendants(-1, UmbracoObjectTypes.Media, 0, 1000, out total) + .Select(x => x.Id) + .ToArray(); + + foreach (var media in toDelete) + { + Assert.IsTrue(entities.Contains(media.Id)); + } + } + + [Test] + public void EntityService_Can_Get_Paged_Media_Descendants_Without_Recycled() + { + var folderType = ServiceContext.ContentTypeService.GetMediaType(1031); + var imageMediaType = ServiceContext.ContentTypeService.GetMediaType(1032); + + var root = MockedMedia.CreateMediaFolder(folderType, -1); + ServiceContext.MediaService.Save(root); + var toDelete = new List(); + for (int i = 0; i < 10; i++) + { + var c1 = MockedMedia.CreateMediaImage(imageMediaType, root.Id); + ServiceContext.MediaService.Save(c1); + + if (i % 2 == 0) + { + toDelete.Add(c1); + } + + for (int j = 0; j < 5; j++) + { + var c2 = MockedMedia.CreateMediaImage(imageMediaType, c1.Id); + ServiceContext.MediaService.Save(c2); + } + } + + foreach (var content in toDelete) + { + ServiceContext.MediaService.MoveToRecycleBin(content); + } + + var service = ServiceContext.EntityService; + + long total; + //search at root to see if it returns recycled + var entities = service.GetPagedDescendantsFromRoot(UmbracoObjectTypes.Media, 0, 1000, out total, includeTrashed:false) + .Select(x => x.Id) + .ToArray(); + + foreach (var media in toDelete) + { + Assert.IsFalse(entities.Contains(media.Id)); + } + } + [Test] public void EntityService_Can_Get_Paged_Media_Descendants_With_Search() { diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/entity.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/entity.resource.js index 6e2c2ae328..dcaee2fe38 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/entity.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/entity.resource.js @@ -346,8 +346,8 @@ function entityResource($q, $http, umbRequestHelper) { * @param {Int} parentid id of content item to return children of * @param {string} type Object type name * @param {Object} options optional options object - * @param {Int} options.pageSize if paging data, number of nodes per page, default = 0 - * @param {Int} options.pageNumber if paging data, current page index, default = 0 + * @param {Int} options.pageSize if paging data, number of nodes per page, default = 1 + * @param {Int} options.pageNumber if paging data, current page index, default = 100 * @param {String} options.filter if provided, query will only return those with names matching the filter * @param {String} options.orderDirection can be `Ascending` or `Descending` - Default: `Ascending` * @param {String} options.orderBy property to order items by, default: `SortOrder` @@ -357,8 +357,8 @@ function entityResource($q, $http, umbRequestHelper) { getPagedChildren: function (parentId, type, options) { var defaults = { - pageSize: 0, - pageNumber: 0, + pageSize: 1, + pageNumber: 100, filter: '', orderDirection: "Ascending", orderBy: "SortOrder" @@ -390,7 +390,77 @@ function entityResource($q, $http, umbRequestHelper) { pageSize: options.pageSize, orderBy: options.orderBy, orderDirection: options.orderDirection, - filter: options.filter + filter: encodeURIComponent(options.filter) + } + )), + 'Failed to retrieve child data for id ' + parentId); + }, + + /** + * @ngdoc method + * @name umbraco.resources.entityResource#getPagedDescendants + * @methodOf umbraco.resources.entityResource + * + * @description + * Gets paged descendants of a content item with a given id + * + * ##usage + *
+          * entityResource.getPagedDescendants(1234, "Content", {pageSize: 10, pageNumber: 2})
+          *    .then(function(contentArray) {
+          *        var children = contentArray; 
+          *        alert('they are here!');
+          *    });
+          * 
+ * + * @param {Int} parentid id of content item to return descendants of + * @param {string} type Object type name + * @param {Object} options optional options object + * @param {Int} options.pageSize if paging data, number of nodes per page, default = 1 + * @param {Int} options.pageNumber if paging data, current page index, default = 100 + * @param {String} options.filter if provided, query will only return those with names matching the filter + * @param {String} options.orderDirection can be `Ascending` or `Descending` - Default: `Ascending` + * @param {String} options.orderBy property to order items by, default: `SortOrder` + * @returns {Promise} resourcePromise object containing an array of content items. + * + */ + getPagedDescendants: function (parentId, type, options) { + + var defaults = { + pageSize: 1, + pageNumber: 100, + filter: '', + orderDirection: "Ascending", + orderBy: "SortOrder" + }; + if (options === undefined) { + options = {}; + } + //overwrite the defaults if there are any specified + angular.extend(defaults, options); + //now copy back to the options we will use + options = defaults; + //change asc/desct + if (options.orderDirection === "asc") { + options.orderDirection = "Ascending"; + } + else if (options.orderDirection === "desc") { + options.orderDirection = "Descending"; + } + + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "entityApiBaseUrl", + "GetPagedDescendants", + { + id: parentId, + type: type, + pageNumber: options.pageNumber, + pageSize: options.pageSize, + orderBy: options.orderBy, + orderDirection: options.orderDirection, + filter: encodeURIComponent(options.filter) } )), 'Failed to retrieve child data for id ' + parentId); diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/media.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/media.resource.js index 4b56e55801..bbb78cd616 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/media.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/media.resource.js @@ -482,7 +482,49 @@ function mediaResource($q, $http, umbDataFormatter, umbRequestHelper) { "mediaApiBaseUrl", "EmptyRecycleBin")), 'Failed to empty the recycle bin'); + }, + + /** + * @ngdoc method + * @name umbraco.resources.mediaResource#search + * @methodOf umbraco.resources.mediaResource + * + * @description + * Empties the media recycle bin + * + * ##usage + *
+          * mediaResource.search("my search", 1, 100, -1)
+          *    .then(function(searchResult) {
+          *        alert('it's here!');
+          *    });
+          * 
+ * + * @param {string} query The search query + * @param {int} pageNumber The page number + * @param {int} pageSize The number of media items on a page + * @param {int} searchFrom Id to search from + * @returns {Promise} resourcePromise object. + * + */ + search: function (query, pageNumber, pageSize, searchFrom) { + + var args = [ + { "query": query }, + { "pageNumber": pageNumber }, + { "pageSize": pageSize }, + { "searchFrom": searchFrom } + ]; + + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "mediaApiBaseUrl", + "Search", + args)), + 'Failed to retrieve media items for search: ' + query); } + }; } diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/mediaPicker/mediapicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/overlays/mediaPicker/mediapicker.controller.js index 3b6c39e84a..f6ae9a8d6b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/mediaPicker/mediapicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/mediaPicker/mediapicker.controller.js @@ -33,6 +33,14 @@ angular.module("umbraco") $scope.acceptedMediatypes = types; }); + $scope.searchOptions = { + pageNumber: 1, + pageSize: 100, + totalItems: 0, + totalPages: 0, + filter: '', + }; + //preload selected item $scope.target = undefined; if (dialogOptions.currentTarget) { @@ -106,35 +114,9 @@ angular.module("umbraco") $scope.path = []; } - //mediaResource.rootMedia() - mediaResource.getChildren(folder.id) - .then(function(data) { - $scope.searchTerm = ""; - $scope.images = data.items ? data.items : []; - - // set already selected images to selected - for (var folderImageIndex = 0; folderImageIndex < $scope.images.length; folderImageIndex++) { - var folderImage = $scope.images[folderImageIndex]; - var imageIsSelected = false; - - for (var selectedImageIndex = 0; - selectedImageIndex < $scope.model.selectedImages.length; - selectedImageIndex++) { - var selectedImage = $scope.model.selectedImages[selectedImageIndex]; - - if (folderImage.key === selectedImage.key) { - imageIsSelected = true; - } - } - if (imageIsSelected) { - folderImage.selected = true; - } - } - }); + getChildren(folder.id); $scope.currentFolder = folder; - localStorageService.set("umbLastOpenedMediaNodeId", folder.id); - }; $scope.clickHandler = function(image, event, index) { @@ -238,4 +220,106 @@ angular.module("umbraco") $scope.mediaPickerDetailsOverlay = null; }; }; + + $scope.changeSearch = function() { + $scope.loading = true; + debounceSearchMedia(); + }; + + $scope.changePagination = function(pageNumber) { + $scope.loading = true; + $scope.searchOptions.pageNumber = pageNumber; + searchMedia(); + }; + + var debounceSearchMedia = _.debounce(function () { + $scope.$apply(function () { + if ($scope.searchOptions.filter) { + searchMedia(); + } else { + // reset pagination + $scope.searchOptions = { + pageNumber: 1, + pageSize: 100, + totalItems: 0, + totalPages: 0, + filter: '' + }; + getChildren($scope.currentFolder.id); + } + }); + }, 500); + + function searchMedia() { + $scope.loading = true; + entityResource.getPagedDescendants($scope.startNodeId, "Media", $scope.searchOptions) + .then(function (data) { + // update image data to work with image grid + angular.forEach(data.items, function(mediaItem){ + // set thumbnail and src + mediaItem.thumbnail = mediaHelper.resolveFileFromEntity(mediaItem, true); + mediaItem.image = mediaHelper.resolveFileFromEntity(mediaItem, false); + // set properties to match a media object + if (mediaItem.metaData && + mediaItem.metaData.umbracoWidth && + mediaItem.metaData.umbracoHeight) { + + mediaItem.properties = [ + { + alias: "umbracoWidth", + value: mediaItem.metaData.umbracoWidth.Value + }, + { + alias: "umbracoHeight", + value: mediaItem.metaData.umbracoHeight.Value + } + ]; + } + }); + // update images + $scope.images = data.items ? data.items : []; + // update pagination + if (data.pageNumber > 0) + $scope.searchOptions.pageNumber = data.pageNumber; + if (data.pageSize > 0) + $scope.searchOptions.pageSize = data.pageSize; + $scope.searchOptions.totalItems = data.totalItems; + $scope.searchOptions.totalPages = data.totalPages; + // set already selected images to selected + preSelectImages(); + $scope.loading = false; + }); + } + + function getChildren(id) { + $scope.loading = true; + mediaResource.getChildren(id) + .then(function(data) { + $scope.searchOptions.filter = ""; + $scope.images = data.items ? data.items : []; + // set already selected images to selected + preSelectImages(); + $scope.loading = false; + }); + } + + function preSelectImages() { + for (var folderImageIndex = 0; folderImageIndex < $scope.images.length; folderImageIndex++) { + var folderImage = $scope.images[folderImageIndex]; + var imageIsSelected = false; + + for (var selectedImageIndex = 0; + selectedImageIndex < $scope.model.selectedImages.length; + selectedImageIndex++) { + var selectedImage = $scope.model.selectedImages[selectedImageIndex]; + + if (folderImage.key === selectedImage.key) { + imageIsSelected = true; + } + } + if (imageIsSelected) { + folderImage.selected = true; + } + } + } }); diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/mediaPicker/mediapicker.html b/src/Umbraco.Web.UI.Client/src/views/common/overlays/mediaPicker/mediapicker.html index 993ede9587..01355dcf12 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/mediaPicker/mediapicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/mediaPicker/mediapicker.html @@ -10,13 +10,18 @@
+ + + @@ -32,7 +37,7 @@
-
+
  • Media @@ -63,20 +68,20 @@
+
+ + +
+ + + + +
GetPagedDescendants( + int id, + UmbracoEntityTypes type, + int pageNumber, + int pageSize, + string orderBy = "SortOrder", + Direction orderDirection = Direction.Ascending, + string filter = "") + { + if (pageNumber <= 0) + throw new HttpResponseException(HttpStatusCode.NotFound); + if (pageSize <= 0) + throw new HttpResponseException(HttpStatusCode.NotFound); + + var objectType = ConvertToObjectType(type); + if (objectType.HasValue) + { + long totalRecords; + //if it's from root, don't return recycled + var entities = id == Constants.System.Root + ? Services.EntityService.GetPagedDescendantsFromRoot(objectType.Value, pageNumber - 1, pageSize, out totalRecords, orderBy, orderDirection, filter, includeTrashed:false) + : Services.EntityService.GetPagedDescendants(id, objectType.Value, pageNumber - 1, pageSize, out totalRecords, orderBy, orderDirection, filter); + + if (totalRecords == 0) + { + return new PagedResult(0, 0, 0); + } + + var pagedResult = new PagedResult(totalRecords, pageNumber, pageSize) + { + Items = entities.Select(Mapper.Map) + }; + + return pagedResult; + } + + //now we need to convert the unknown ones + switch (type) + { + case UmbracoEntityTypes.PropertyType: + case UmbracoEntityTypes.PropertyGroup: + case UmbracoEntityTypes.Domain: + case UmbracoEntityTypes.Language: + case UmbracoEntityTypes.User: + case UmbracoEntityTypes.Macro: + default: + throw new NotSupportedException("The " + typeof(EntityController) + " does not currently support data for the type " + type); + } + } + public IEnumerable GetAncestors(int id, UmbracoEntityTypes type) { return GetResultForAncestors(id, type); diff --git a/src/Umbraco.Web/Editors/MediaController.cs b/src/Umbraco.Web/Editors/MediaController.cs index 0e191954d6..3db1656c34 100644 --- a/src/Umbraco.Web/Editors/MediaController.cs +++ b/src/Umbraco.Web/Editors/MediaController.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.ComponentModel; +using System.Globalization; using System.IO; using System.Net; using System.Net.Http; @@ -28,7 +29,9 @@ using Umbraco.Web.Mvc; using Umbraco.Web.WebApi; using System.Linq; using System.Runtime.Serialization; +using System.Text.RegularExpressions; using System.Web.Http.Controllers; +using Examine; using Umbraco.Web.WebApi.Binders; using Umbraco.Web.WebApi.Filters; using umbraco;