bulk move for media almost complete
This commit is contained in:
@@ -52,39 +52,39 @@ public abstract class ZeroApiTreeEntityStoreController<TModel, TStore> : ZeroApi
|
||||
}
|
||||
|
||||
|
||||
protected async Task<ActionResult<Result>> MoveModel<TEdit>(string pageId, string newParentId)
|
||||
protected async Task<ActionResult<Result>> MoveModel<TEdit>(string id, string newParentId)
|
||||
{
|
||||
return await PutOperation<TEdit>(async () => await Store.Move(pageId, newParentId));
|
||||
return await PutOperation<TEdit>(async () => await Store.Move(id, newParentId));
|
||||
}
|
||||
|
||||
|
||||
protected async Task<ActionResult<Result>> MoveModel(string pageId, string newParentId)
|
||||
protected async Task<ActionResult<Result>> 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<ActionResult<Result>> CopyModel<TEdit>(string pageId, string newParentId)
|
||||
protected async Task<ActionResult<Result>> CopyModel<TEdit>(string id, string newParentId)
|
||||
{
|
||||
return await PutOperation<TEdit>(async () => await Store.Copy(pageId, newParentId));
|
||||
return await PutOperation<TEdit>(async () => await Store.Copy(id, newParentId));
|
||||
}
|
||||
|
||||
|
||||
protected async Task<ActionResult<Result>> CopyModel(string pageId, string newParentId)
|
||||
protected async Task<ActionResult<Result>> 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<ActionResult<Result>> CopyModelWithDescendants<TEdit>(string pageId, string newParentId)
|
||||
protected async Task<ActionResult<Result>> CopyModelWithDescendants<TEdit>(string id, string newParentId)
|
||||
{
|
||||
return await PutOperation<TEdit>(async () => await Store.CopyWithDescendants(pageId, newParentId));
|
||||
return await PutOperation<TEdit>(async () => await Store.CopyWithDescendants(id, newParentId));
|
||||
}
|
||||
|
||||
|
||||
protected async Task<ActionResult<Result>> CopyModelWithDescendants(string pageId, string newParentId)
|
||||
protected async Task<ActionResult<Result>> CopyModelWithDescendants(string id, string newParentId)
|
||||
{
|
||||
return await PutOperation(async () => await Store.CopyWithDescendants(pageId, newParentId));
|
||||
return await PutOperation(async () => await Store.CopyWithDescendants(id, newParentId));
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -7,6 +7,8 @@ public class zero_Api_Media_ChildCounts : ZeroIndex<zero.Media.Media, zero_Api_M
|
||||
public class Result : ZeroIdEntity
|
||||
{
|
||||
public int ChildCount { get; set; }
|
||||
|
||||
public int ChildFolderCount { get; set; }
|
||||
}
|
||||
|
||||
protected override void Create()
|
||||
@@ -14,13 +16,15 @@ public class zero_Api_Media_ChildCounts : ZeroIndex<zero.Media.Media, zero_Api_M
|
||||
Map = items => 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);
|
||||
|
||||
@@ -11,12 +11,12 @@ public class zero_Api_Media_Listing : ZeroIndex<zero.Media.Media>
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace zero.Api.Endpoints.Media;
|
||||
|
||||
public class MediaBulkDeleteOperation
|
||||
{
|
||||
public string[] Ids { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace zero.Api.Endpoints.Media;
|
||||
|
||||
public class MediaBulkMoveOperation
|
||||
{
|
||||
public string ParentId { get; set; }
|
||||
|
||||
public string[] Ids { get; set; }
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
public class MediaEdit : DisplayModel<zero.Media.Media>
|
||||
{
|
||||
public MediaType Type { get; set; }
|
||||
public bool IsFolder { get; set; }
|
||||
|
||||
public string ParentId { get; set; }
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
|
||||
public class MediaSave : SaveModel<zero.Media.Media>
|
||||
{
|
||||
public MediaType Type { get; set; }
|
||||
public bool IsFolder { get; set; }
|
||||
}
|
||||
@@ -19,7 +19,7 @@ public class MediaController : ZeroApiTreeEntityStoreController<zero.Media.Media
|
||||
|
||||
[HttpGet("empty")]
|
||||
[ZeroAuthorize(MediaPermissions.Create)]
|
||||
public virtual async Task<ActionResult<zero.Media.Media>> Empty(MediaType type, string flavor = null) => await EmptyModel(flavor, x => x.Type = type);
|
||||
public virtual async Task<ActionResult<zero.Media.Media>> Empty(bool folder = false, string flavor = null) => await EmptyModel(flavor, x => x.IsFolder = folder);
|
||||
|
||||
|
||||
[HttpGet("{id}")]
|
||||
@@ -29,12 +29,12 @@ public class MediaController : ZeroApiTreeEntityStoreController<zero.Media.Media
|
||||
|
||||
[HttpGet("{id}/children")]
|
||||
[ZeroAuthorize(MediaPermissions.Read)]
|
||||
public virtual async Task<ActionResult<Paged>> GetChildren(string id, [FromQuery] ListQuery<zero.Media.Media> query)
|
||||
public virtual async Task<ActionResult<Paged>> GetChildren(string id, [FromQuery] ListQuery<zero.Media.Media> query, [FromQuery] bool folders = false)
|
||||
{
|
||||
id = id == "root" ? null : id;
|
||||
|
||||
query.OrderQuery = q => q.OrderBy(x => x.Type).OrderByDescending(x => x.CreatedDate);
|
||||
Paged<zero.Media.Media> result = await Store.LoadChildren<zero_Api_Media_Listing>(id, query.Page, query.PageSize, q => q.Filter(query));
|
||||
query.OrderQuery = q => q.OrderByDescending(x => x.IsFolder).ThenByDescending(x => x.CreatedDate);
|
||||
Paged<zero.Media.Media> result = await Store.LoadChildren<zero_Api_Media_Listing>(id, query.Page, query.PageSize, q => q.WhereIf(x => x.IsFolder, folders).Filter(query));
|
||||
Paged<MediaBasic> mappedResult = Mapper.Map<zero.Media.Media, MediaBasic>(result);
|
||||
|
||||
// get children for all folders
|
||||
@@ -48,7 +48,12 @@ public class MediaController : ZeroApiTreeEntityStoreController<zero.Media.Media
|
||||
{
|
||||
if (item.IsFolder)
|
||||
{
|
||||
item.Children = children.FirstOrDefault(x => 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<zero.Media.Media
|
||||
}
|
||||
|
||||
|
||||
[HttpPut("bulk/move")]
|
||||
[ZeroAuthorize(MediaPermissions.Update)]
|
||||
public virtual async Task<ActionResult<IEnumerable<Result>>> BulkMove(MediaBulkMoveOperation operation)
|
||||
{
|
||||
List<Result<string>> results = new();
|
||||
|
||||
foreach (string id in operation.Ids)
|
||||
{
|
||||
Result<zero.Media.Media> result = await Store.Move(id, operation.ParentId);
|
||||
results.Add(result.ConvertTo(id));
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
|
||||
[HttpDelete("bulk/delete")]
|
||||
[ZeroAuthorize(MediaPermissions.Update)]
|
||||
public virtual async Task<ActionResult<IEnumerable<Result>>> BulkDelete(MediaBulkDeleteOperation operation)
|
||||
{
|
||||
List<Result<string>> results = new();
|
||||
|
||||
foreach (string id in operation.Ids)
|
||||
{
|
||||
Result<string[]> result = await Store.DeleteWithDescendants(id);
|
||||
results.Add(result.ConvertTo(id));
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
|
||||
//[HttpGet("")]
|
||||
//[ZeroAuthorize(MediaPermissions.Read)]
|
||||
//public virtual Task<ActionResult<Paged>> Get([FromQuery] ListQuery<zero.Media.Media> query)
|
||||
|
||||
@@ -70,6 +70,35 @@ public class ApiResponseFilterAttribute : ResultFilterAttribute
|
||||
result.Value = response;
|
||||
}
|
||||
|
||||
// format bulk patch results
|
||||
else if (result.Value is IEnumerable<Result<string>> 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
|
||||
{
|
||||
|
||||
@@ -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<BulkOperationErrorApiResponseError> Errors { get; set; } = new();
|
||||
}
|
||||
|
||||
public class BulkOperationErrorApiResponseError : ErrorApiResponseError
|
||||
{
|
||||
public string AffectedId { get; set; }
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="ui-datagrid-outer">
|
||||
<div class="ui-datagrid-outer" :class="{'is-selecting': configuration.selectable && selected.length > 0}">
|
||||
<div class="ui-datagrid">
|
||||
<div class="ui-datagrid-items" :style="'grid-template-columns: repeat(auto-fill, minmax(' + configuration.width + 'px, 1fr))'" :class="{'is-block': configuration.block }">
|
||||
<div class="ui-datagrid-item" v-for="(item, index) in items" :key="index" v-on:contextmenu="onRightClicked(item, $event)">
|
||||
@@ -107,7 +107,8 @@
|
||||
},
|
||||
debouncedUpdate: null,
|
||||
actionProps: {
|
||||
item: null
|
||||
item: null,
|
||||
selected: false
|
||||
},
|
||||
selected: []
|
||||
}),
|
||||
@@ -233,6 +234,7 @@
|
||||
|
||||
this.actionProps.item = item;
|
||||
this.actionProps.event = ev;
|
||||
this.actionProps.selected = this.configuration.selectable && this.selected.indexOf(item) > -1;
|
||||
|
||||
dropdown.toggle();
|
||||
|
||||
|
||||
@@ -82,7 +82,7 @@
|
||||
computed: {
|
||||
actionsDefined()
|
||||
{
|
||||
return this.$scopedSlots.hasOwnProperty('actions');
|
||||
return this.$slots.hasOwnProperty('actions');
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -12,5 +12,5 @@ export default {
|
||||
|
||||
update: async (model: any, config?: ApiRequestConfig) => await put('applications/' + model.id, model, { ...config, system: true }),
|
||||
|
||||
delete: async (id: string, config?: ApiRequestConfig) => await del('applications/' + id, { ...config, system: true }),
|
||||
delete: async (id: string, config?: ApiRequestConfig) => await del('applications/' + id, null, { ...config, system: true }),
|
||||
};
|
||||
@@ -12,5 +12,5 @@ export default {
|
||||
|
||||
update: (model: any, config?: ApiRequestConfig) => put('countries/' + model.id, model, config),
|
||||
|
||||
delete: (id: string, config?: ApiRequestConfig) => del('countries/' + id, config),
|
||||
delete: (id: string, config?: ApiRequestConfig) => del('countries/' + id, null, config),
|
||||
};
|
||||
@@ -12,5 +12,5 @@ export default {
|
||||
|
||||
update: (model: any, config?: ApiRequestConfig) => put('languages/' + model.id, model, config),
|
||||
|
||||
delete: (id: string, config?: ApiRequestConfig) => del('languages/' + id, config),
|
||||
delete: (id: string, config?: ApiRequestConfig) => del('languages/' + id, null, config),
|
||||
};
|
||||
@@ -9,8 +9,13 @@ export default {
|
||||
|
||||
getChildren: (id: string, query: ApiRequestQuery, config?: ApiRequestConfig) => get(`media/${id}/children`, { ...config, params: { ...query } }),
|
||||
|
||||
getFolderChildren: (id: string, query: ApiRequestQuery, config?: ApiRequestConfig) => get(`media/${id}/children`, { ...config, params: { ...query, folders: true } }),
|
||||
|
||||
getHierarchy: (id: string, config?: ApiRequestConfig) => get(`media/${id}/hierarchy`, { ...config }),
|
||||
|
||||
bulkDelete: (ids: string[], config?: ApiRequestConfig) => del(`media/bulk/delete`, { ids }, { ...config }),
|
||||
|
||||
bulkMove: (ids: string[], parentId: string, config?: ApiRequestConfig) => put(`media/bulk/move`, { parentId, ids }, { ...config }),
|
||||
|
||||
//create: (model: any, config?: ApiRequestConfig) => post('countries', model, config),
|
||||
|
||||
|
||||
@@ -0,0 +1,211 @@
|
||||
<template>
|
||||
<ui-trinity class="pages-move">
|
||||
<template v-slot:header>
|
||||
<ui-header-bar title="@ui.move.title" :back-button="false" :close-button="true" />
|
||||
</template>
|
||||
<template v-slot:footer>
|
||||
<ui-button type="light onbg" label="@ui.close" @click="config.close" />
|
||||
<ui-button type="accent" label="@ui.move.action" @click="onSave" :state="state" :disabled="!loaded" />
|
||||
</template>
|
||||
|
||||
<p class="pages-move-text" v-localize:html="{ key: '@ui.move.text', tokens: { name: model.name } }"></p>
|
||||
<div class="ui-box pages-move-items">
|
||||
<ui-tree v-if="loaded" ref="tree" :get="getItems" @select="onSelect" :selection="selected" />
|
||||
</div>
|
||||
</ui-trinity>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
import api from '../api';
|
||||
//import MediaFolderApi from 'zero/api/media-folder.js';
|
||||
//import MediaApi from 'zero/api/media.js';
|
||||
//import Notification from 'zero/helpers/notification.js'
|
||||
|
||||
export default {
|
||||
|
||||
props: {
|
||||
model: Object,
|
||||
config: Object
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
hierarchy: [],
|
||||
items: [],
|
||||
selected: [],
|
||||
state: 'default',
|
||||
cache: {},
|
||||
prevItem: null,
|
||||
loaded: false,
|
||||
ids: [],
|
||||
newParentId: null
|
||||
}),
|
||||
|
||||
|
||||
async mounted()
|
||||
{
|
||||
this.newParentId = this.model.parentId;
|
||||
this.selected = [this.model.parentId];
|
||||
this.hierarchy = (await api.getHierarchy(this.model.parentId)).data.map(x => x.id);
|
||||
this.hierarchy.splice(this.hierarchy.length - 1, 1);
|
||||
this.ids = this.model.items.map(x => x.id);
|
||||
this.loaded = true;
|
||||
},
|
||||
|
||||
|
||||
methods: {
|
||||
|
||||
onSelect(item)
|
||||
{
|
||||
item.isSelected = true;
|
||||
|
||||
if (this.prevItem && this.prevItem.id != item.id)
|
||||
{
|
||||
this.prevItem.isSelected = false;
|
||||
}
|
||||
|
||||
this.prevItem = item;
|
||||
this.newParentId = item.id;
|
||||
this.selected = [item];
|
||||
//this.config.confirm(item);
|
||||
},
|
||||
|
||||
async getItems(parent)
|
||||
{
|
||||
const id = !parent ? 'root' : parent;
|
||||
|
||||
if (this.cache[id])
|
||||
{
|
||||
return Promise.resolve(this.cache[id]);
|
||||
}
|
||||
|
||||
const response = await api.getFolderChildren(id, { pageSize: 50 });
|
||||
const items = response.data;
|
||||
|
||||
if (!parent)
|
||||
{
|
||||
items.splice(0, 0, {
|
||||
id: null,
|
||||
parentId: null,
|
||||
image: null,
|
||||
name: '@page.root',
|
||||
children: 0,
|
||||
isFolder: true,
|
||||
root: true
|
||||
});
|
||||
}
|
||||
|
||||
const result = items.map(item =>
|
||||
{
|
||||
return {
|
||||
id: item.id,
|
||||
parentId: item.parentId,
|
||||
sort: 0,
|
||||
name: item.name,
|
||||
icon: item.root ? 'fth-arrow-down-circle' : (item.isFolder ? 'fth-folder' : 'fth-file'),
|
||||
isOpen: this.hierarchy.indexOf(item.id) > -1,
|
||||
modifier: null,
|
||||
hasChildren: item.children > 0,
|
||||
childCount: item.children,
|
||||
isInactive: false,
|
||||
hasActions: false,
|
||||
disabled: item.id == 'recyclebin' || this.ids.indexOf(item.id) > -1
|
||||
};
|
||||
})
|
||||
|
||||
this.cache[id] = result;
|
||||
|
||||
return result;
|
||||
|
||||
//response.forEach(item =>
|
||||
//{
|
||||
// //item.disabled = true;
|
||||
// item.isSelected = this.model.parentId == item.id;
|
||||
|
||||
// if (item.isSelected)
|
||||
// {
|
||||
// this.prevItem = item;
|
||||
// }
|
||||
// item.disabled = item.id === 'recyclebin' || item.id == this.model.id;
|
||||
// item.hasActions = false;
|
||||
//});
|
||||
},
|
||||
|
||||
onSave()
|
||||
{
|
||||
if (this.model.parentId == this.newParentId)
|
||||
{
|
||||
this.config.close();
|
||||
return;
|
||||
}
|
||||
|
||||
this.state = 'loading';
|
||||
|
||||
// TODO
|
||||
// 1. bulk move
|
||||
// 2. close overlay
|
||||
// 3. update current output
|
||||
// 4. show notification
|
||||
// 5. eventually bulk result
|
||||
|
||||
//(this.config.isFolder ? MediaFolderApi : MediaApi).move(this.model.id, this.selected.id).then(res =>
|
||||
//{
|
||||
// if (res.success)
|
||||
// {
|
||||
// this.state = 'success';
|
||||
// this.config.confirm(res.model);
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// this.state = 'error';
|
||||
// Notification.error(res.errors[0].message);
|
||||
// }
|
||||
//});
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.pages-move .ui-box
|
||||
{
|
||||
margin: 0;
|
||||
padding: 16px 0;
|
||||
|
||||
.ui-tree-item.is-disabled
|
||||
{
|
||||
opacity: .5;
|
||||
}
|
||||
|
||||
.ui-tree-item.is-selected, .ui-tree-item:hover:not(.is-disabled)
|
||||
{
|
||||
background: var(--color-tree-selected);
|
||||
}
|
||||
|
||||
.ui-tree-item.is-selected
|
||||
{
|
||||
&:after
|
||||
{
|
||||
font-family: "Feather";
|
||||
content: "\e83e";
|
||||
font-size: 16px;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.ui-tree-item-text
|
||||
{
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.pages-move content
|
||||
{
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.pages-move-text
|
||||
{
|
||||
margin: 0 0 20px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,36 @@
|
||||
|
||||
import api from '../../api';
|
||||
import * as overlays from '../../../../services/overlay';
|
||||
|
||||
|
||||
export class Actions
|
||||
{
|
||||
constructor()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
|
||||
remove(items: any[])
|
||||
{
|
||||
console.info({ action: 'remove', items });
|
||||
}
|
||||
|
||||
|
||||
async move(items: any[])
|
||||
{
|
||||
const result = await overlays.open({
|
||||
component: () => import('../../overlays/move.vue'),
|
||||
display: 'editor',
|
||||
width: 560,
|
||||
model: {
|
||||
parentId: items[0].parentId,
|
||||
name: items.length + ' items',
|
||||
items: items
|
||||
}
|
||||
});
|
||||
|
||||
console.info(result);
|
||||
|
||||
}
|
||||
};
|
||||
@@ -61,12 +61,12 @@
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
|
||||
.media-items.is-selecting .media-item
|
||||
.ui-datagrid-outer.is-selecting .media-item
|
||||
{
|
||||
opacity: .5;
|
||||
opacity: .55;
|
||||
}
|
||||
|
||||
.media-items.is-selecting .media-item.is-selected
|
||||
.ui-datagrid-outer.is-selecting .media-item.is-selected
|
||||
{
|
||||
opacity: 1;
|
||||
}
|
||||
@@ -121,9 +121,10 @@
|
||||
left: -10px;
|
||||
top: -10px;
|
||||
background: var(--color-accent);
|
||||
color: white;
|
||||
color: var(--color-accent-fg);
|
||||
box-shadow: 1px 1px 0 1px var(--color-shadow);
|
||||
font-size: 11px;
|
||||
|
||||
.is-selected &
|
||||
{
|
||||
display: inline-flex;
|
||||
|
||||
@@ -1,26 +1,57 @@
|
||||
<template>
|
||||
<div>
|
||||
selection
|
||||
<div class="media-selection">
|
||||
<ui-button type="light onbg" label="@media.selection.clear" @click="clearSelection" />
|
||||
<ui-dropdown align="right">
|
||||
<template v-slot:button>
|
||||
<ui-button type="accent" :label="localize('@media.selection.selected', { tokens: { count: selected.length }})" caret="down" />
|
||||
</template>
|
||||
<ui-dropdown-button label="@ui.move.title" icon="fth-corner-down-right" @click="move" />
|
||||
<ui-dropdown-button label="@ui.delete" icon="fth-trash" @click="remove" />
|
||||
</ui-dropdown>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { localize } from '../../../../services/localization';
|
||||
|
||||
export default {
|
||||
name: 'mediaOverviewSelection',
|
||||
|
||||
props: {
|
||||
value: {
|
||||
type: Object,
|
||||
default: () => { }
|
||||
},
|
||||
selected: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
localize,
|
||||
|
||||
clearSelection()
|
||||
{
|
||||
this.$emit('clear');
|
||||
},
|
||||
|
||||
|
||||
move()
|
||||
{
|
||||
this.$emit('move', this.selected);
|
||||
},
|
||||
|
||||
|
||||
remove()
|
||||
{
|
||||
this.$emit('remove', this.selected);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
.media-selection
|
||||
{
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -14,19 +14,20 @@
|
||||
</h2>
|
||||
</template>
|
||||
|
||||
<ui-search v-model="gridConfig.search" class="onbg" />
|
||||
|
||||
{{selected}}
|
||||
<template v-if="selected.length < 1">
|
||||
<ui-search v-model="gridConfig.search" class="onbg" />
|
||||
</template>
|
||||
<media-selection v-else :selected="selected" @clear="clearSelection" @move="actions.move" @remove="actions.remove" />
|
||||
</ui-header-bar>
|
||||
|
||||
<div class="ui-view-box">
|
||||
<div class="media-items" :class="{ 'is-selecting': selecting }">
|
||||
<div class="media-items" :class="{ 'is-selecting': selected.length > 0 }">
|
||||
<ui-datagrid ref="grid" v-model="gridConfig" @select="onSelected" @count="count = $event">
|
||||
<template v-slot:actions="props">
|
||||
<template v-if="selected.length < 1" v-slot:actions="props">
|
||||
<ui-dropdown-button v-if="props.item && props.item.isFolder" label="@ui.open.title" icon="fth-arrow-right" @click="goToFolder(props.item.id)" />
|
||||
<ui-dropdown-button label="@ui.edit.title" icon="fth-edit-2" @click="edit(props.item, props.item.isFolder)" />
|
||||
<ui-dropdown-button label="@ui.move.title" icon="fth-corner-down-right" @click="move(props.item, props.item.isFolder)" />
|
||||
<ui-dropdown-button label="Select" icon="fth-check-circle" @click="$refs.grid.select(props.item)" />
|
||||
<ui-dropdown-button label="@ui.selection.select" icon="fth-check-circle-2" @click="$refs.grid.select(props.item)" />
|
||||
<ui-dropdown-separator />
|
||||
<ui-dropdown-button label="@ui.delete" icon="fth-trash" @click="remove(props.item, props.item.isFolder)" />
|
||||
</template>
|
||||
@@ -44,13 +45,16 @@
|
||||
import { defineComponent } from 'vue';
|
||||
import api from '../../api';
|
||||
import MediaItem from './overview-item.vue';
|
||||
import MediaSelection from './overview-selection.vue';
|
||||
import { Actions } from './overview-actions';
|
||||
|
||||
export default defineComponent({
|
||||
props: ['parentId'],
|
||||
|
||||
components: { MediaItem },
|
||||
components: { MediaItem, MediaSelection },
|
||||
|
||||
data: () => ({
|
||||
actions: null,
|
||||
paging: {},
|
||||
hierarchy: {},
|
||||
search: null,
|
||||
@@ -72,12 +76,26 @@
|
||||
},
|
||||
|
||||
|
||||
watch: {
|
||||
'$route': function (val)
|
||||
{
|
||||
this.clearSelection();
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
created()
|
||||
{
|
||||
this.gridConfig.items = this.getItems;
|
||||
},
|
||||
|
||||
|
||||
mounted()
|
||||
{
|
||||
this.actions = new Actions();
|
||||
},
|
||||
|
||||
|
||||
methods: {
|
||||
async getItems(query)
|
||||
{
|
||||
@@ -89,6 +107,7 @@
|
||||
query.search = this.gridConfig.search;
|
||||
query.folderId = this.parentId;
|
||||
query.searchIsGlobal = true;
|
||||
query.pageSize = 50;
|
||||
|
||||
const hierarchy = await api.getHierarchy(this.id);
|
||||
this.hierarchy = hierarchy.data;
|
||||
@@ -99,6 +118,14 @@
|
||||
onSelected(selection)
|
||||
{
|
||||
this.selected = selection;
|
||||
},
|
||||
|
||||
clearSelection()
|
||||
{
|
||||
if (this.$refs.grid)
|
||||
{
|
||||
this.$refs.grid.clearSelection();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,9 +15,9 @@ export function post(url: string, data: any, config?: ApiRequestConfig)
|
||||
return send({ method: 'post', url, data, ...config });
|
||||
}
|
||||
|
||||
export function del(url: string, config?: ApiRequestConfig)
|
||||
export function del(url: string, data: any, config?: ApiRequestConfig)
|
||||
{
|
||||
return send({ method: 'delete', url, ...config });
|
||||
return send({ method: 'delete', url, data, ...config });
|
||||
}
|
||||
|
||||
export function put(url: string, data: any, config?: ApiRequestConfig)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// theme-agnostic colors
|
||||
:root
|
||||
{
|
||||
--color-accent: #F9AA19; // #F9AA19
|
||||
--color-accent: #67bdb1; // #F9AA19 // #67bdb1
|
||||
--color-accent-fg: #ffffff;
|
||||
|
||||
--color-negative: rgb(216, 40, 83);
|
||||
|
||||
@@ -51,6 +51,10 @@
|
||||
},
|
||||
"name": "Name",
|
||||
"select": "Select",
|
||||
"selection": {
|
||||
"select": "Select",
|
||||
"deselect": "Deselect"
|
||||
},
|
||||
"icon": "Icon",
|
||||
"icon_select": "Select an icon",
|
||||
"tab_general": "General",
|
||||
@@ -646,7 +650,9 @@
|
||||
"addfile_text": "Add one or more files to the current filter"
|
||||
},
|
||||
"selection": {
|
||||
"clear": "Clear selection"
|
||||
"clear": "Clear selection",
|
||||
"selected": "{count} selected",
|
||||
"selectaction": "Action"
|
||||
},
|
||||
"fields": {
|
||||
"foldername_placeholder": "Enter a name ...",
|
||||
|
||||
@@ -40,7 +40,7 @@ public class MediaCreator : IMediaCreator
|
||||
Media model = await Store.Empty();
|
||||
model.Name = normalizedFilename;
|
||||
model.ParentId = folderId;
|
||||
model.Type = isImage ? MediaType.Image : MediaType.File;
|
||||
model.IsFolder = false;
|
||||
|
||||
// create directory which hosts the media file
|
||||
// the media directory is a flat folder where each folder contains one media file (+ thumbs)
|
||||
|
||||
@@ -62,7 +62,7 @@ public class MediaManagement : IMediaManagement
|
||||
public virtual async Task<Media> GetFile(string id)
|
||||
{
|
||||
Media file = await Store.Load(id);
|
||||
return file != null && file.Type != MediaType.Folder ? file : null;
|
||||
return file != null && !file.IsFolder ? file : null;
|
||||
}
|
||||
|
||||
|
||||
@@ -93,7 +93,7 @@ public class MediaManagement : IMediaManagement
|
||||
public virtual async Task<Media> GetFolder(string id)
|
||||
{
|
||||
Media folder = await Store.Load(id);
|
||||
return folder != null && folder.Type == MediaType.Folder ? folder : null;
|
||||
return folder != null && folder.IsFolder ? folder : null;
|
||||
}
|
||||
|
||||
|
||||
@@ -101,7 +101,7 @@ public class MediaManagement : IMediaManagement
|
||||
public virtual async Task<Result<Media>> CreateFolder(Media folder)
|
||||
{
|
||||
folder.IsActive = true;
|
||||
folder.Type = MediaType.Folder;
|
||||
folder.IsFolder = true;
|
||||
return await Store.Create(folder);
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ public class MediaStore : TreeEntityStore<Media>, IMediaStore
|
||||
}
|
||||
|
||||
Media parent = await Load(parentId);
|
||||
return parent != null && parent.Type == MediaType.Folder;
|
||||
return parent != null && parent.IsFolder;
|
||||
}
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ public class MediaStore : TreeEntityStore<Media>, IMediaStore
|
||||
validator.RuleFor(x => x.Name).Length(2, 120);
|
||||
validator.RuleFor(x => x.IsActive).Equal(true);
|
||||
|
||||
validator.When(x => x.Type == MediaType.Folder, () =>
|
||||
validator.When(x => x.IsFolder, () =>
|
||||
{
|
||||
validator.RuleFor(x => x.ParentId).Exists(Context.Store);
|
||||
});
|
||||
|
||||
@@ -11,6 +11,11 @@ public class Media : ZeroEntity, ISupportsTrees
|
||||
IsActive = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether this media item is a folder or a file
|
||||
/// </summary>
|
||||
public bool IsFolder { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Id/name of the phyiscal folder which is stored on disk/cloud
|
||||
/// </summary>
|
||||
@@ -56,9 +61,4 @@ public class Media : ZeroEntity, ISupportsTrees
|
||||
/// Optional focal point for an image
|
||||
/// </summary>
|
||||
public MediaFocalPoint FocalPoint { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Type of the media
|
||||
/// </summary>
|
||||
public MediaType Type { get; set; }
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
namespace zero.Media;
|
||||
|
||||
public enum MediaType
|
||||
{
|
||||
Folder = 0,
|
||||
File = 1,
|
||||
Image = 2
|
||||
}
|
||||
@@ -56,7 +56,9 @@ public partial class StoreOperations : IStoreOperations
|
||||
IRavenQueryable<T> queryable = Session.Query<T, TIndex>().Where(x => x.ParentId == parentId).Statistics(out QueryStatistics statistics);
|
||||
querySelector ??= x => x.OrderBy(x => x.Sort);
|
||||
|
||||
List<T> result = await querySelector(queryable).Paging(pageNumber, pageSize).ToListAsync();
|
||||
var query = querySelector(queryable).Paging(pageNumber, pageSize);
|
||||
|
||||
List<T> result = await query.ToListAsync();
|
||||
return new Paged<T>(result, statistics.TotalResults, pageNumber, pageSize);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user