update media library to include shared items

This commit is contained in:
2021-03-22 13:40:48 +01:00
parent 9651502cba
commit 358903c49f
14 changed files with 404 additions and 108 deletions
+2 -2
View File
@@ -94,7 +94,7 @@ namespace zero.Core.Api
public async Task<ListResult<MediaListItem>> GetListByQuery(MediaListItemQuery query)
{
bool hasSearch = !query.Search.IsNullOrWhiteSpace();
bool isRoot = !query.IsShared && query.FolderId.IsNullOrWhiteSpace();
bool isRoot = query.FolderId.IsNullOrWhiteSpace();
query.SearchFor(entity => entity.Name);
@@ -102,7 +102,7 @@ namespace zero.Core.Api
.OrderByDescending(x => x.IsFolder)
.ThenBy(query.OrderBy, query.OrderIsDescending, query.OrderType == ListQueryOrderType.String ? OrderingType.String : OrderingType.Double);
using IAsyncDocumentSession session = Store.OpenAsyncSession(query.IsShared ? Backoffice.Options.Raven.Database : null);
using IAsyncDocumentSession session = Store.OpenAsyncSession();
session.Advanced.MaxNumberOfRequestsPerSession = query.PageSize + 1;
IRavenQueryable<MediaListItem> dbQuery = session.Query<MediaListItem, Media_ByParent>().ProjectInto<MediaListItem>();
+7 -16
View File
@@ -222,8 +222,6 @@ namespace zero.Core.Collections
.OrderByDescending(x => x.IsFolder)
.ThenBy(query.OrderBy, query.OrderIsDescending, query.OrderType == ListQueryOrderType.String ? OrderingType.String : OrderingType.Double);
Session.Advanced.MaxNumberOfRequestsPerSession = query.PageSize + 1;
IRavenQueryable<MediaListItem> dbQuery = Session.Query<MediaListItem, Media_ByParent>().ProjectInto<MediaListItem>();
if (!hasSearch || !query.SearchIsGlobal)
@@ -233,22 +231,15 @@ namespace zero.Core.Collections
ListResult<MediaListItem> result = await dbQuery.ToQueriedListAsyncX(query);
string[] ids = result.Items.Where(x => x.IsFolder).Select(x => x.Id).ToArray();
List<Media_ByChildren.Result> children = await Session.Query<Media_ByChildren.Result, Media_ByChildren>()
.Where(x => x.ParentId.In(ids))
.ToListAsync();
foreach (MediaListItem item in result.Items)
{
if (item.IsFolder)
{
item.Children = await Session.Query<MediaListItem, Media_ByParent>().CountAsync(x => x.ParentId == item.Id);
}
}
// TODO this is only inserted when we have a core/shared database and a multi-tenancy setup
if (isRoot && Database != Context.Options.Raven.Database)
{
result.Items.Insert(0, new MediaListItem()
{
Id = "shared",
IsFolder = true
});
item.Children = children.FirstOrDefault(x => x.ParentId == item.Id)?.ChildrenCount ?? 0;
}
return result;
@@ -0,0 +1,51 @@
using Raven.Client.Documents.Indexes;
using System.Linq;
using zero.Core.Entities;
namespace zero.Core.Database.Indexes
{
public class Media_ByChildren : AbstractMultiMapIndexCreationTask<Media_ByChildren.Result>
{
public class Result : IZeroIdEntity, IZeroDbConventions
{
public string Id { get; set; }
public string ParentId { get; set; }
public int ChildrenCount { get; set; }
public string[] ChildrenIds { get; set; }
}
public Media_ByChildren()
{
AddMap<IMedia>(items => items.Select(item => new Result()
{
Id = item.Id,
ParentId = item.FolderId,
ChildrenCount = 1,
ChildrenIds = new string[] { }
}));
AddMap<IMediaFolder>(items => items.Select(item => new Result()
{
Id = item.Id,
ParentId = item.ParentId,
ChildrenCount = 1,
ChildrenIds = new string[] { }
}));
Reduce = results => results.GroupBy(x => new { x.ParentId }).Select(group => new Result()
{
Id = null,
ParentId = group.Key.ParentId,
ChildrenCount = group.Sum(x => x.ChildrenCount),
ChildrenIds = group.Select(x => x.Id).ToArray()
});
StoreAllFields(FieldStorage.Yes);
Index(x => x.ParentId, FieldIndexing.Exact);
}
}
}
+6 -2
View File
@@ -18,7 +18,9 @@ namespace zero.Core.Database.Indexes
Name = item.Name,
Image = null,
Children = 0,
Size = 0
Size = 0,
IsShared = item.Blueprint != null,
AspectRatio = 0
}));
AddMap<IMedia>(items => items.Select(item => new MediaListItem
@@ -30,7 +32,9 @@ namespace zero.Core.Database.Indexes
Name = item.Name,
Image = item.PreviewSource,
Children = 0,
Size = item.Size
Size = item.Size,
IsShared = item.Blueprint != null,
AspectRatio = item.ImageMeta != null ? (float)item.ImageMeta.Width / item.ImageMeta.Height : 0
}));
StoreAllFields(FieldStorage.Yes);
@@ -21,5 +21,9 @@ namespace zero.Core.Entities
public int Children { get; set; }
public bool HasTransparency { get; set; }
public float AspectRatio { get; set; }
public bool IsShared { get; set; }
}
}
@@ -11,7 +11,5 @@
public string FolderId { get; set; }
public bool SearchIsGlobal { get; set; }
public bool IsShared { get; set; }
}
}
@@ -303,6 +303,12 @@
}
}
this.$emit('select', this.selected, this);
},
clearSelection()
{
this.selected = [];
this.$emit('select', this.selected, this);
}
}
+1 -7
View File
@@ -87,12 +87,11 @@
& + .ui-view-box
{
padding-top: 0;
padding-top: 0;
}
.app-tree &
{
height: 90px;
margin-bottom: 8px;
}
}
@@ -146,11 +145,6 @@
margin: 0;
font-size: var(--font-size-l);
font-weight: 700;
//display: flex;
//align-items: center;
font-size: var(--font-size-xl);
font-weight: 900;
//text-transform: uppercase;
&.is-empty, .-minor
{
@@ -4,7 +4,7 @@
<div class="app-overlay-outer" :display="instance.display" v-for="(instance, index) in instances" :key="instance.id" :style="{ transform: instance.display !== 'editor' ? null : 'translateX(' + (editorLength - index - 1) * -60 + 'px)' }">
<div class="app-overlay-bg" @click="close(instance)"></div>
<dialog open class="app-overlay" :style="{ width: instance.width + 'px' }" :class="'theme-' + instance.theme" :display="instance.display">
<component :is="instance.component" :model.sync="instance.model" :config="instance"></component>
<component :is="instance.component" :model.sync="instance.model" :config="instance" v-bind="instance"></component>
</dialog>
</div>
</transition-group>
@@ -95,6 +95,15 @@
font-size: var(--font-size);
}
.app-overlay .ui-loading
{
position: relative;
left: 50%;
margin-left: -16px;
margin-top: 20px;
margin-bottom: 20px;
}
.app-overlay[display="dialog"] .ui-form-loading
{
height: 200px;
@@ -0,0 +1,167 @@
<template>
<div class="ui-select-overlay">
<!--<h2 class="ui-headline" v-localize="configuration.title"></h2>-->
<ui-loading v-if="loading" />
<div v-if="!loading">
<div v-if="list.length && configuration.parent" class="ui-select-overlay-parent">
<span v-localize="title"></span>: <strong>{{configuration.parent.name}}</strong>
</div>
<div class="ui-select-overlay-items">
<button type="button" v-for="item in list" class="ui-select-overlay-item" @click="onSelect(item)">
<ui-icon class="ui-select-overlay-item-icon" :symbol="item[configuration.iconKey]" :size="22" />
<span class="ui-select-overlay-item-text">
{{item[configuration.labelKey] | localize}}
<span v-if="item[configuration.descriptionKey]" v-localize="item[configuration.descriptionKey]"></span>
</span>
</button>
</div>
<ui-message type="error" v-if="!list.length" text="@page.create.nonavailable" />
<!--<div class="app-confirm-buttons">
<ui-button type="light" :label="config.closeLabel" @click="config.close"></ui-button>
</div>-->
</div>
</div>
</template>
<script>
import ChannelsApi from 'shop/api/channels.js';
import Overlay from 'zero/helpers/overlay.js';
const defaultConfig = {
title: '@ui.create',
labelKey: 'name',
descriptionKey: 'description',
iconKey: 'icon',
items: null
};
export default {
props: {
config: Object
},
data: () => ({
configuration: defaultConfig,
list: [],
loading: false,
disabled: false
}),
mounted()
{
this.configuration = { ...defaultConfig, ...this.config };
this.loading = true;
this.load();
},
methods: {
load()
{
if (typeof this.configuration.items === 'function')
{
this.configuration.items().then(res =>
{
this.list = res;
this.loading = false;
});
}
else
{
this.list = JSON.parse(JSON.stringify(this.configuration.items));
this.loading = false;
}
},
onSelect(item)
{
this.config.confirm(item);
},
}
}
</script>
<style lang="scss">
.ui-select-overlay
{
text-align: left;
.ui-message
{
margin: 0;
}
}
.ui-select-overlay-parent
{
margin: 30px 0 -10px 0;
border-radius: var(--radius);
/*border: 1px solid var(--color-line-light);*/
background: var(--color-box-nested);
line-height: 1.4;
color: var(--color-text-dim);
padding: 14px 16px;
font-size: var(--font-size);
strong
{
color: var(--color-text);
}
}
.ui-select-overlay-items
{
margin: 0 -16px;
//margin-top: var(--padding);
max-height: 490px;
overflow-y: auto;
}
.ui-select-overlay-item
{
display: grid;
width: 100%;
grid-template-columns: 70px 1fr auto;
gap: 12px;
align-items: center;
position: relative;
color: var(--color-text);
padding: 16px;
border-radius: var(--radius);
&:hover, &:focus
{
background: var(--color-tree-selected);
}
& + .ui-select-overlay-item
{
margin-top: 2px;
}
}
.ui-select-overlay-item-text
{
display: flex;
flex-direction: column;
span
{
color: var(--color-text-dim);
margin-top: 3px;
}
}
.ui-select-overlay-item-icon
{
color: var(--color-text);
width: 60px;
height: 60px;
padding: 20px;
border-radius: var(--radius);
background: var(--color-bg-shade-3);
}
</style>
+39 -27
View File
@@ -1,24 +1,22 @@
<template>
<router-link :to="link" class="media-item">
<div class="media-item-preview" :class="{'media-pattern': value.image }">
<div class="media-item-preview" :class="{'media-pattern': value.image, 'is-covered': covered }">
<span class="media-item-check"><ui-icon symbol="fth-check" :size="14" /></span>
<img class="media-item-image" v-if="value.image" :src="value.image" />
<span class="media-item-icon" v-if="!value.image"><ui-icon :symbol="shared ? 'fth-globe' : (value.isFolder ? 'fth-folder' : 'fth-file')" :size="36" :stroke-width="1.5" /></span>
<span class="media-item-icon" v-if="!value.image"><ui-icon :symbol="(value.isFolder ? 'fth-folder' : 'fth-file')" :size="36" :stroke-width="1.5" /></span>
</div>
<p class="media-item-text" v-if="!shared">
<span :title="value.name">{{value.name}}</span>
<p class="media-item-text">
<span :title="value.name">{{value.name}} <ui-icon symbol="fth-cloud" v-if="value.isShared" :size="15" class="media-item-shared" /></span>
<span class="-minor" v-if="!value.isFolder"><br><span v-filesize="value.size"></span></span>
<span class="-minor" v-if="value.isFolder"><br><span v-localize="{ key: value.children === 1 ? '@media.child_count_1' : '@media.child_count_x', tokens: { count: value.children }}"></span></span>
</p>
<p class="media-item-text" v-if="shared">
<span :title="value.name">Shared</span>
<span class="-minor"><br>Media for all apps</span>
</p>
</router-link>
</template>
<script>
export default {
name: 'mediaItem',
props: {
value: {
type: Object,
@@ -33,14 +31,6 @@
computed: {
link()
{
if (this.shared)
{
return {
name: 'media',
query: { scope: 'shared' }
};
}
return {
name: this.value.isFolder ? 'media' : 'media-edit',
params: {
@@ -49,9 +39,9 @@
query: this.$route.query
};
},
shared()
covered()
{
return this.value.id === 'shared';
return this.value.aspectRatio < 1.2 && this.value.aspectRatio > 0.8;
}
}
};
@@ -72,6 +62,16 @@
border-radius: var(--radius);
}
.media-items.is-selecting .media-item
{
opacity: .5;
}
.media-items.is-selecting .media-item.is-selected
{
opacity: 1;
}
.media-item-preview
{
display: flex;
@@ -90,37 +90,41 @@
.media-item.is-selected .media-item-preview
{
transform: scale(0.95);
border: 3px solid var(--color-primary);
//border: 3px solid var(--color-primary);
}
.media-item-image
{
width: 100%;
height: 100%;
object-fit: cover;
object-fit: contain;
position: relative;
border-radius: var(--radius);
z-index: 1;
}
.media-item-preview.is-covered .media-item-image
{
object-fit: cover;
}
.media-item-check
{
display: none;
justify-content: center;
align-items: center;
width: 24px;
height: 24px;
width: 30px;
height: 30px;
border-radius: 20px;
border: 5px solid var(--color-bg);
position: absolute;
z-index: 2;
left: -12px;
top: -12px;
left: -13px;
top: -13px;
background: var(--color-primary);
color: var(--color-primary-text);
box-shadow: 1px 1px 0 1px var(--color-shadow);
font-size: 11px;
.is-selected &
{
display: inline-flex;
@@ -129,7 +133,7 @@
.media-item-text
{
white-space: nowrap;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin: 0;
@@ -142,4 +146,12 @@
color: var(--color-text-dim);
}
}
.media-item-shared
{
margin-left: .4em;
color: var(--color-synchronized);
position: relative;
top: 2px;
}
</style>
+100 -49
View File
@@ -1,39 +1,47 @@
<template>
<div class="media-content">
<ui-header-bar :back-button="!!id || shared" :count="count">
<ui-header-bar :back-button="!!id" :count="count">
<template v-slot:title>
<h2 class="ui-header-bar-title">
<router-link :to="{ name: 'media' }" class="media-items-hierarchy-item" v-if="!!id"><i class="fth-home"></i></router-link>
<router-link :to="{ name: 'media', params: { id: item.id } }" v-for="(item, index) in hierarchy" :key="item.id" class="media-items-hierarchy-item" v-localize="item.name"></router-link>
<span v-for="(item, index) in hierarchy" :key="item.id" class="media-items-hierarchy-item">
<router-link :to="{ name: 'media', params: { id: item.id } }" v-localize="item.name"></router-link>
<ui-icon v-if="index < hierarchy.length - 1" symbol="fth-chevron-right" />
</span>
</h2>
</template>
<template>
<ui-search v-if="!selecting" v-model="gridConfig.search" class="onbg" />
<ui-dropdown v-if="!!id && !selecting" align="right" :disabled="!!gridConfig.search">
<template v-slot:button>
<ui-button type="light onbg" label="Folder" caret="down" :disabled="!!gridConfig.search" />
</template>
<ui-dropdown-button label="@ui.edit.title" icon="fth-edit-2" @click="edit(current, true)" />
<ui-dropdown-button label="@ui.move.title" icon="fth-corner-down-right" @click="move(current, true)" />
<ui-dropdown-separator />
<ui-dropdown-button label="@ui.delete" icon="fth-trash" @click="remove(current, true)" />
</ui-dropdown>
<ui-dropdown v-if="selecting" align="right">
<template v-slot:button>
<ui-button type="light onbg" :label="selectedText" caret="down" />
</template>
<!--<slot name="actions"></slot>-->
</ui-dropdown>
<ui-button v-if="!selecting" type="primary" label="Add folder" @click="addFolder(id)" />
<div v-if="!!id && !selecting" type="button" class="ui-button has-state type-primary state-default has-icon">
<span class="ui-button-text" v-localize="'Add file'"></span>
<input class="media-item-upload" type="file" multiple @change="onUpload" />
</div>
<template v-if="!selecting">
<ui-search v-model="gridConfig.search" class="onbg" />
<ui-dropdown v-if="!!id" align="right" :disabled="!!gridConfig.search">
<template v-slot:button>
<ui-button type="light onbg" label="@media.actions.folderdropdown" caret="down" :disabled="!!gridConfig.search" />
</template>
<ui-dropdown-button label="@ui.edit.title" icon="fth-edit-2" @click="edit(current, true)" />
<ui-dropdown-button label="@ui.move.title" icon="fth-corner-down-right" @click="move(current, true)" />
<ui-dropdown-separator />
<ui-dropdown-button label="@ui.delete" icon="fth-trash" @click="remove(current, true)" />
</ui-dropdown>
<ui-button type="primary" label="@ui.add" @click="add" />
<!--<div v-if="!!id" type="button" class="ui-button has-state type-primary state-default has-icon">
<span class="ui-button-text" v-localize="'@media.actions.addfile'"></span>
<input class="media-item-upload" type="file" multiple @change="onUpload" />
</div>-->
</template>
<template v-if="selecting">
<ui-button type="blank" label="@media.selection.clear" @click="clearSelection" />
<ui-dropdown align="right">
<template v-slot:button>
<ui-button type="primary" :label="selectedText" caret="down" />
</template>
<ui-dropdown-button label="@ui.move.title" icon="fth-corner-down-right" @click="move(current, true)" />
<ui-dropdown-button label="@ui.delete" icon="fth-trash" @click="remove(current, true)" />
</ui-dropdown>
</template>
</template>
</ui-header-bar>
<div class="ui-view-box">
<div class="media-items">
<div class="media-items" :class="{ 'is-selecting': selecting }">
<ui-datagrid ref="grid" v-model="gridConfig" @select="onSelected" @count="count = $event">
<template 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)" />
@@ -61,6 +69,7 @@
import UploadStatusOverlay from './overlays/upload-status.vue';
import EventHub from 'zero/helpers/eventhub.js';
import Notification from 'zero/helpers/notification.js';
import SelectOverlay from 'zero/components/overlays/select-overlay.vue';
import { each as _each, extend as _extend, debounce as _debounce, isArray as _isArray } from 'underscore';
export default {
@@ -88,10 +97,6 @@
selecting()
{
return this.selectedCount > 0;
},
shared()
{
return this.$route.query.scope === 'shared';
}
},
@@ -106,6 +111,12 @@
}
},
//beforeRouteUpdate(to, from, next)
//{
// this.clearSelection();
// next();
//},
created()
{
this.gridConfig.items = this.getItems;
@@ -116,6 +127,7 @@
initialize()
{
//this.clearSelection();
//if (!this.scope)
//{
// this.$route.params.scope = 'local';
@@ -134,11 +146,10 @@
query.search = this.gridConfig.search;
query.folderId = this.$route.params.id;
query.searchIsGlobal = true;
query.isShared = this.shared;
this.getFolderHierarchy(query.folderId, !!query.search);
return MediaApi.getListByQuery(query).then(response => // .scope(this.shared)
return MediaApi.getListByQuery(query).then(response =>
{
return Promise.resolve(response);
});
@@ -166,8 +177,12 @@
return;
}
MediaFolderApi.getHierarchy(id, this.shared).then(res =>
MediaFolderApi.getHierarchy(id).then(res =>
{
res.splice(0, 0, {
id: null,
name: '@media.list'
});
this.hierarchy = res;
this.current = res[res.length - 1];
});
@@ -184,6 +199,36 @@
},
// shows add overlay
add()
{
//< !--< div v -if= "!!id" type = "button" class="ui-button has-state type-primary state-default has-icon" >
// <span class="ui-button-text" v-localize="'@media.actions.addfile'"></span>
// <input class="media-item-upload" type="file" multiple @change="onUpload" />
// </div > -->
Overlay.open({
component: SelectOverlay,
width: 480,
theme: 'dark',
items: [
{
name: '@media.actions.addfile',
description: '@media.actions.addfile_text',
icon: 'fth-upload-cloud'
},
{
name: '@media.actions.addfolder',
description: '@media.actions.addfolder_text',
icon: 'fth-folder-plus'
}
]
}).then(item =>
{
}, () => { });
},
// adds a new folder
addFolder(parentId)
{
@@ -322,6 +367,15 @@
}
});
});
},
clearSelection()
{
if (this.$refs.grid)
{
this.$refs.grid.clearSelection();
}
}
}
}
@@ -365,25 +419,22 @@
margin: 0;
font-size: var(--font-size-l);
font-weight: 400;
color: var(--color-text-dim);
&:last-child
color: var(--color-text-dim);
{
font-weight: 700;
color: var(--color-text);
}
a
{
color: var(--color-text-dim);
}
& + .media-items-hierarchy-item:before
{
content: '/';
margin: 0 0.5em;
color: var(--color-text-dim);
font-weight: 400;
}
&:last-child a
{
font-weight: 700;
color: var(--color-text);
}
&:hover
{
color: var(--color-text);
}
a:hover
{
color: var(--color-text);
}
}
</style>
+1 -2
View File
@@ -2,7 +2,7 @@
<div class="media-upload">
<div v-if="entity.source" class="media-upload-preview" :data-type="entity.type">
<span ref="image" v-if="entity.type === 'image'" class="media-upload-preview-image media-bg-pattern" @click="setFocalPoint" @dblclick="entity.focalPoint = null">
<span ref="image" v-if="entity.type === 'image'" class="media-upload-preview-image media-pattern" @click="setFocalPoint" @dblclick="entity.focalPoint = null">
<span class="media-upload-preview-focal-point" :style="getFocalPointStyle(entity.focalPoint)"></span>
<img :src="entity.previewSource" :alt="entity.name" />
</span>
@@ -125,7 +125,6 @@
{
padding: 0;
border-radius: var(--radius);
background: var(--color-bg-dim);
display: inline-block;
position: relative;
cursor: pointer;
@@ -591,6 +591,16 @@
"name": "Media",
"addfolder": "Add a new folder",
"editfolder": "Edit folder",
"actions": {
"folderdropdown": "Folder",
"addfolder": "Create folder",
"addfolder_text": "Adds a new folder in the current parent folder",
"addfile": "Upload files",
"addfile_text": "Add one or more files to the current filter"
},
"selection": {
"clear": "Clear selection"
},
"fields": {
"foldername_placeholder": "Enter a name ...",
"caption": "Caption",