diff --git a/zero.Api/Abstractions/ZeroApiTreeEntityStoreController.cs b/zero.Api/Abstractions/ZeroApiTreeEntityStoreController.cs index 80581d73..0acc5b55 100644 --- a/zero.Api/Abstractions/ZeroApiTreeEntityStoreController.cs +++ b/zero.Api/Abstractions/ZeroApiTreeEntityStoreController.cs @@ -52,39 +52,39 @@ public abstract class ZeroApiTreeEntityStoreController : ZeroApi } - protected async Task> MoveModel(string pageId, string newParentId) + protected async Task> MoveModel(string id, string newParentId) { - return await PutOperation(async () => await Store.Move(pageId, newParentId)); + return await PutOperation(async () => await Store.Move(id, newParentId)); } - protected async Task> MoveModel(string pageId, string newParentId) + protected async Task> MoveModel(string id, string newParentId) { - return await PutOperation(async () => await Store.Move(pageId, newParentId)); + return await PutOperation(async () => await Store.Move(id, newParentId)); } - protected async Task> CopyModel(string pageId, string newParentId) + protected async Task> CopyModel(string id, string newParentId) { - return await PutOperation(async () => await Store.Copy(pageId, newParentId)); + return await PutOperation(async () => await Store.Copy(id, newParentId)); } - protected async Task> CopyModel(string pageId, string newParentId) + protected async Task> CopyModel(string id, string newParentId) { - return await PutOperation(async () => await Store.Copy(pageId, newParentId)); + return await PutOperation(async () => await Store.Copy(id, newParentId)); } - protected async Task> CopyModelWithDescendants(string pageId, string newParentId) + protected async Task> CopyModelWithDescendants(string id, string newParentId) { - return await PutOperation(async () => await Store.CopyWithDescendants(pageId, newParentId)); + return await PutOperation(async () => await Store.CopyWithDescendants(id, newParentId)); } - protected async Task> CopyModelWithDescendants(string pageId, string newParentId) + protected async Task> CopyModelWithDescendants(string id, string newParentId) { - return await PutOperation(async () => await Store.CopyWithDescendants(pageId, newParentId)); + return await PutOperation(async () => await Store.CopyWithDescendants(id, newParentId)); } diff --git a/zero.Api/Endpoints/Media/Indexes/zero_Api_Media_ChildCounts.cs b/zero.Api/Endpoints/Media/Indexes/zero_Api_Media_ChildCounts.cs index 44eac38a..08ac9d2d 100644 --- a/zero.Api/Endpoints/Media/Indexes/zero_Api_Media_ChildCounts.cs +++ b/zero.Api/Endpoints/Media/Indexes/zero_Api_Media_ChildCounts.cs @@ -7,6 +7,8 @@ public class zero_Api_Media_ChildCounts : ZeroIndex items.Where(x => x.ParentId != null).Select(item => new Result { Id = item.ParentId, - ChildCount = 1 + ChildCount = 1, + ChildFolderCount = item.IsFolder ? 1 : 0 }); Reduce = results => results.GroupBy(x => new { x.Id }).Select(group => new Result() { Id = group.Key.Id, - ChildCount = group.Sum(x => x.ChildCount) + ChildCount = group.Sum(x => x.ChildCount), + ChildFolderCount = group.Sum(x => x.ChildFolderCount) }); StoreAllFields(FieldStorage.Yes); diff --git a/zero.Api/Endpoints/Media/Indexes/zero_Api_Media_Listing.cs b/zero.Api/Endpoints/Media/Indexes/zero_Api_Media_Listing.cs index cafad3f8..a0ce4f20 100644 --- a/zero.Api/Endpoints/Media/Indexes/zero_Api_Media_Listing.cs +++ b/zero.Api/Endpoints/Media/Indexes/zero_Api_Media_Listing.cs @@ -11,12 +11,12 @@ public class zero_Api_Media_Listing : ZeroIndex Name = item.Name, ParentId = item.ParentId, CreatedDate = item.CreatedDate, - Type = item.Type + IsFolder = item.IsFolder }); Index(x => x.Name, FieldIndexing.Search); Index(x => x.ParentId, FieldIndexing.Exact); Index(x => x.CreatedDate, FieldIndexing.Exact); - Index(x => x.Type, FieldIndexing.Exact); + Index(x => x.IsFolder, FieldIndexing.Exact); } } \ No newline at end of file diff --git a/zero.Api/Endpoints/Media/Maps/MediaBulkDeleteOperation.cs b/zero.Api/Endpoints/Media/Maps/MediaBulkDeleteOperation.cs new file mode 100644 index 00000000..5436cd0d --- /dev/null +++ b/zero.Api/Endpoints/Media/Maps/MediaBulkDeleteOperation.cs @@ -0,0 +1,6 @@ +namespace zero.Api.Endpoints.Media; + +public class MediaBulkDeleteOperation +{ + public string[] Ids { get; set; } +} \ No newline at end of file diff --git a/zero.Api/Endpoints/Media/Maps/MediaBulkMoveOperation.cs b/zero.Api/Endpoints/Media/Maps/MediaBulkMoveOperation.cs new file mode 100644 index 00000000..27203fd2 --- /dev/null +++ b/zero.Api/Endpoints/Media/Maps/MediaBulkMoveOperation.cs @@ -0,0 +1,8 @@ +namespace zero.Api.Endpoints.Media; + +public class MediaBulkMoveOperation +{ + public string ParentId { get; set; } + + public string[] Ids { get; set; } +} \ No newline at end of file diff --git a/zero.Api/Endpoints/Media/Maps/MediaEdit.cs b/zero.Api/Endpoints/Media/Maps/MediaEdit.cs index bea025fc..dfec7c05 100644 --- a/zero.Api/Endpoints/Media/Maps/MediaEdit.cs +++ b/zero.Api/Endpoints/Media/Maps/MediaEdit.cs @@ -2,7 +2,7 @@ public class MediaEdit : DisplayModel { - public MediaType Type { get; set; } + public bool IsFolder { get; set; } public string ParentId { get; set; } } \ No newline at end of file diff --git a/zero.Api/Endpoints/Media/Maps/MediaMapperProfile.cs b/zero.Api/Endpoints/Media/Maps/MediaMapperProfile.cs index f7ec0cca..15eae4fc 100644 --- a/zero.Api/Endpoints/Media/Maps/MediaMapperProfile.cs +++ b/zero.Api/Endpoints/Media/Maps/MediaMapperProfile.cs @@ -26,7 +26,7 @@ public class MediaMapperProfile : ZeroMapperProfile target.Id = source.Id; target.Name = source.Name; target.ParentId = source.ParentId; - target.IsFolder = source.Type == MediaType.Folder; + target.IsFolder = source.IsFolder; target.Children = 0; target.Size = source.Size; @@ -40,7 +40,7 @@ public class MediaMapperProfile : ZeroMapperProfile protected virtual void Map(zero.Media.Media source, MediaEdit target, IZeroMapperContext ctx) { this.MapDisplayData(source, target); - target.Type = source.Type; + target.IsFolder = source.IsFolder; target.ParentId = source.ParentId; } diff --git a/zero.Api/Endpoints/Media/Maps/MediaSave.cs b/zero.Api/Endpoints/Media/Maps/MediaSave.cs index 9b003e17..d1cd9276 100644 --- a/zero.Api/Endpoints/Media/Maps/MediaSave.cs +++ b/zero.Api/Endpoints/Media/Maps/MediaSave.cs @@ -2,5 +2,5 @@ public class MediaSave : SaveModel { - public MediaType Type { get; set; } + public bool IsFolder { get; set; } } \ No newline at end of file diff --git a/zero.Api/Endpoints/Media/MediaController.cs b/zero.Api/Endpoints/Media/MediaController.cs index 441dfe1f..70a7231f 100644 --- a/zero.Api/Endpoints/Media/MediaController.cs +++ b/zero.Api/Endpoints/Media/MediaController.cs @@ -19,7 +19,7 @@ public class MediaController : ZeroApiTreeEntityStoreController> Empty(MediaType type, string flavor = null) => await EmptyModel(flavor, x => x.Type = type); + public virtual async Task> Empty(bool folder = false, string flavor = null) => await EmptyModel(flavor, x => x.IsFolder = folder); [HttpGet("{id}")] @@ -29,12 +29,12 @@ public class MediaController : ZeroApiTreeEntityStoreController> GetChildren(string id, [FromQuery] ListQuery query) + public virtual async Task> GetChildren(string id, [FromQuery] ListQuery query, [FromQuery] bool folders = false) { id = id == "root" ? null : id; - query.OrderQuery = q => q.OrderBy(x => x.Type).OrderByDescending(x => x.CreatedDate); - Paged result = await Store.LoadChildren(id, query.Page, query.PageSize, q => q.Filter(query)); + query.OrderQuery = q => q.OrderByDescending(x => x.IsFolder).ThenByDescending(x => x.CreatedDate); + Paged result = await Store.LoadChildren(id, query.Page, query.PageSize, q => q.WhereIf(x => x.IsFolder, folders).Filter(query)); Paged mappedResult = Mapper.Map(result); // get children for all folders @@ -48,7 +48,12 @@ public class MediaController : ZeroApiTreeEntityStoreController x.Id == item.Id)?.ChildCount ?? 0; + zero_Api_Media_ChildCounts.Result childCounts = children.FirstOrDefault(x => x.Id == item.Id); + + if (childCounts != null) + { + item.Children = folders ? childCounts.ChildFolderCount : childCounts.ChildCount; + } } } @@ -65,6 +70,38 @@ public class MediaController : ZeroApiTreeEntityStoreController>> BulkMove(MediaBulkMoveOperation operation) + { + List> results = new(); + + foreach (string id in operation.Ids) + { + Result result = await Store.Move(id, operation.ParentId); + results.Add(result.ConvertTo(id)); + } + + return results; + } + + + [HttpDelete("bulk/delete")] + [ZeroAuthorize(MediaPermissions.Update)] + public virtual async Task>> BulkDelete(MediaBulkDeleteOperation operation) + { + List> results = new(); + + foreach (string id in operation.Ids) + { + Result result = await Store.DeleteWithDescendants(id); + results.Add(result.ConvertTo(id)); + } + + return results; + } + + //[HttpGet("")] //[ZeroAuthorize(MediaPermissions.Read)] //public virtual Task> Get([FromQuery] ListQuery query) diff --git a/zero.Api/Filters/ApiResponseFilter.cs b/zero.Api/Filters/ApiResponseFilter.cs index c56196d3..230cbbe8 100644 --- a/zero.Api/Filters/ApiResponseFilter.cs +++ b/zero.Api/Filters/ApiResponseFilter.cs @@ -70,6 +70,35 @@ public class ApiResponseFilterAttribute : ResultFilterAttribute result.Value = response; } + // format bulk patch results + else if (result.Value is IEnumerable> list) + { + int countSucceeded = list.Count(x => x.IsSuccess); + int countFailed = list.Count() - countSucceeded; + + BulkOperationApiResponse response = countFailed > 0 ? new BulkOperationWithErrorsApiResponse() : new BulkOperationApiResponse(); + + response.CountSucceeded = countSucceeded; + response.CountFailed = countFailed; + + if (countFailed > 0 && response is BulkOperationWithErrorsApiResponse errorResponse) + { + errorResponse.Errors = list.Where(x => !x.IsSuccess).SelectMany(x => x.Errors.Select(e => new BulkOperationErrorApiResponseError() + { + AffectedId = x.Model, + Category = ApiErrorCodes.Categories.Validation, + Code = "// TODO", + Property = e.Property, + Message = e.Message + })).ToList(); + } + + response.Success = countSucceeded > 0; + response.Status = countSucceeded < 1 ? StatusCodes.Status400BadRequest : result.StatusCode.Value; + result.StatusCode = response.Status; + result.Value = response; + } + // format model results else { diff --git a/zero.Api/Models/Responses/BulkOperationApiResponse.cs b/zero.Api/Models/Responses/BulkOperationApiResponse.cs new file mode 100644 index 00000000..6cdb43f0 --- /dev/null +++ b/zero.Api/Models/Responses/BulkOperationApiResponse.cs @@ -0,0 +1,19 @@ +namespace zero.Api.Models; + + +public class BulkOperationApiResponse : ApiResponse +{ + public int CountSucceeded { get; set; } + + public int CountFailed { get; set; } +} + +public class BulkOperationWithErrorsApiResponse : BulkOperationApiResponse +{ + public List Errors { get; set; } = new(); +} + +public class BulkOperationErrorApiResponseError : ErrorApiResponseError +{ + public string AffectedId { get; set; } +} \ No newline at end of file diff --git a/zero.Backoffice.UI/app/components/ui-datagrid.vue b/zero.Backoffice.UI/app/components/ui-datagrid.vue index 6356136c..1950393e 100644 --- a/zero.Backoffice.UI/app/components/ui-datagrid.vue +++ b/zero.Backoffice.UI/app/components/ui-datagrid.vue @@ -1,5 +1,5 @@  - - - {{selected}} + +
-
+
-