start media section
This commit is contained in:
@@ -25,7 +25,7 @@ public abstract class ZeroApiEntityStoreController<TModel, TStore> : ZeroApiCont
|
||||
}
|
||||
|
||||
|
||||
protected async Task<ActionResult<T>> EmptyModel<T>(string flavorAlias = null, Action<T> modify = null) where T : DisplayModel<TModel>
|
||||
protected async Task<ActionResult<T>> EmptyModel<T>(string flavorAlias = null, Action<T> modify = null)
|
||||
{
|
||||
TModel model = await Store.Empty(flavorAlias);
|
||||
|
||||
@@ -54,7 +54,7 @@ public abstract class ZeroApiEntityStoreController<TModel, TStore> : ZeroApiCont
|
||||
}
|
||||
|
||||
|
||||
protected async Task<ActionResult<T>> GetModel<T>(string id, string changeVector = null) where T : DisplayModel<TModel>
|
||||
protected async Task<ActionResult<T>> GetModel<T>(string id, string changeVector = null)
|
||||
{
|
||||
TModel model = await Store.Load(id, changeVector);
|
||||
|
||||
@@ -84,7 +84,7 @@ public abstract class ZeroApiEntityStoreController<TModel, TStore> : ZeroApiCont
|
||||
}
|
||||
|
||||
|
||||
protected async Task<ActionResult<Paged>> GetModels<T>(ListQuery<TModel> query) where T : BasicModel<TModel>
|
||||
protected async Task<ActionResult<Paged>> GetModels<T>(ListQuery<TModel> query)
|
||||
{
|
||||
query.OrderQuery ??= q => q.OrderByDescending(x => x.CreatedDate);
|
||||
query.SearchSelector ??= x => x.Name;
|
||||
@@ -103,7 +103,7 @@ public abstract class ZeroApiEntityStoreController<TModel, TStore> : ZeroApiCont
|
||||
}
|
||||
|
||||
|
||||
protected async Task<ActionResult<Paged>> GetModelsByIndex<T, TIndex>(ListQuery<TModel> query) where T : BasicModel<TModel> where TIndex : AbstractCommonApiForIndexes, new()
|
||||
protected async Task<ActionResult<Paged>> GetModelsByIndex<T, TIndex>(ListQuery<TModel> query) where TIndex : AbstractCommonApiForIndexes, new()
|
||||
{
|
||||
query.OrderQuery ??= q => q.OrderByDescending(x => x.CreatedDate);
|
||||
query.SearchSelector ??= x => x.Name;
|
||||
@@ -122,7 +122,7 @@ public abstract class ZeroApiEntityStoreController<TModel, TStore> : ZeroApiCont
|
||||
}
|
||||
|
||||
|
||||
protected async Task<ActionResult<Result>> CreateModel<T, TEdit>(T saveModel) where T : SaveModel<TModel> where TEdit : DisplayModel<TModel>
|
||||
protected async Task<ActionResult<Result>> CreateModel<T, TEdit>(T saveModel) where T : ISupportsFlavors
|
||||
{
|
||||
TModel emptyModel = saveModel.Flavor.IsNullOrEmpty() ? await Store.Empty() : await Store.Empty(saveModel.Flavor);
|
||||
TModel model = Mapper.Map(saveModel, emptyModel);
|
||||
@@ -156,7 +156,7 @@ public abstract class ZeroApiEntityStoreController<TModel, TStore> : ZeroApiCont
|
||||
}
|
||||
|
||||
|
||||
protected async Task<ActionResult<Result>> UpdateModel<T, TEdit>(string id, T updateModel, string changeToken = null) where T : SaveModel<TModel> where TEdit : DisplayModel<TModel>
|
||||
protected async Task<ActionResult<Result>> UpdateModel<T, TEdit>(string id, T updateModel, string changeToken = null) where T : ZeroIdEntity
|
||||
{
|
||||
if (id != updateModel.Id)
|
||||
{
|
||||
|
||||
@@ -10,7 +10,7 @@ public abstract class ZeroApiTreeEntityStoreController<TModel, TStore> : ZeroApi
|
||||
public ZeroApiTreeEntityStoreController(TStore store) : base(store) { }
|
||||
|
||||
|
||||
protected async Task<ActionResult<Paged>> GetChildModels<T>(string parentId, ListQuery<TModel> query) where T : BasicModel<TModel>
|
||||
protected async Task<ActionResult<Paged>> GetChildModels<T>(string parentId, ListQuery<TModel> query)
|
||||
{
|
||||
query.OrderQuery ??= q => q.OrderByDescending(x => x.CreatedDate);
|
||||
Paged<TModel> result = await Store.LoadChildren(parentId, query.Page, query.PageSize, q => q.Filter(query));
|
||||
@@ -28,7 +28,7 @@ public abstract class ZeroApiTreeEntityStoreController<TModel, TStore> : ZeroApi
|
||||
}
|
||||
|
||||
|
||||
protected async Task<ActionResult<Paged>> GetChildModelsByIndex<T, TIndex>(string parentId, ListQuery<TModel> query) where T : BasicModel<TModel> where TIndex : AbstractCommonApiForIndexes, new()
|
||||
protected async Task<ActionResult<Paged>> GetChildModelsByIndex<T, TIndex>(string parentId, ListQuery<TModel> query) where TIndex : AbstractCommonApiForIndexes, new()
|
||||
{
|
||||
query.OrderQuery ??= q => q.OrderByDescending(x => x.CreatedDate);
|
||||
Paged<TModel> result = await Store.LoadChildren<TIndex>(parentId, query.Page, query.PageSize, q => q.Filter(query));
|
||||
@@ -52,7 +52,7 @@ public abstract class ZeroApiTreeEntityStoreController<TModel, TStore> : ZeroApi
|
||||
}
|
||||
|
||||
|
||||
protected async Task<ActionResult<Result>> MoveModel<TEdit>(string pageId, string newParentId) where TEdit : DisplayModel<TModel>
|
||||
protected async Task<ActionResult<Result>> MoveModel<TEdit>(string pageId, string newParentId)
|
||||
{
|
||||
return await PutOperation<TEdit>(async () => await Store.Move(pageId, newParentId));
|
||||
}
|
||||
@@ -64,7 +64,7 @@ public abstract class ZeroApiTreeEntityStoreController<TModel, TStore> : ZeroApi
|
||||
}
|
||||
|
||||
|
||||
protected async Task<ActionResult<Result>> CopyModel<TEdit>(string pageId, string newParentId) where TEdit : DisplayModel<TModel>
|
||||
protected async Task<ActionResult<Result>> CopyModel<TEdit>(string pageId, string newParentId)
|
||||
{
|
||||
return await PutOperation<TEdit>(async () => await Store.Copy(pageId, newParentId));
|
||||
}
|
||||
@@ -76,7 +76,7 @@ public abstract class ZeroApiTreeEntityStoreController<TModel, TStore> : ZeroApi
|
||||
}
|
||||
|
||||
|
||||
protected async Task<ActionResult<Result>> CopyModelWithDescendants<TEdit>(string pageId, string newParentId) where TEdit : DisplayModel<TModel>
|
||||
protected async Task<ActionResult<Result>> CopyModelWithDescendants<TEdit>(string pageId, string newParentId)
|
||||
{
|
||||
return await PutOperation<TEdit>(async () => await Store.CopyWithDescendants(pageId, newParentId));
|
||||
}
|
||||
@@ -103,7 +103,7 @@ public abstract class ZeroApiTreeEntityStoreController<TModel, TStore> : ZeroApi
|
||||
}
|
||||
|
||||
|
||||
async Task<ActionResult<Result>> PutOperation<TEdit>(Func<Task<Result<TModel>>> action) where TEdit : DisplayModel<TModel>
|
||||
async Task<ActionResult<Result>> PutOperation<TEdit>(Func<Task<Result<TModel>>> action)
|
||||
{
|
||||
Result<TModel> result = await action();
|
||||
|
||||
|
||||
@@ -9,10 +9,14 @@ public class zero_Api_Media_Listing : ZeroIndex<zero.Media.Media>
|
||||
Map = items => items.Select(item => new
|
||||
{
|
||||
Name = item.Name,
|
||||
ParentId = item.ParentId
|
||||
ParentId = item.ParentId,
|
||||
CreatedDate = item.CreatedDate,
|
||||
Type = item.Type
|
||||
});
|
||||
|
||||
Index(x => x.Name, FieldIndexing.Search);
|
||||
Index(x => x.ParentId, FieldIndexing.Exact);
|
||||
Index(x => x.CreatedDate, FieldIndexing.Exact);
|
||||
Index(x => x.Type, FieldIndexing.Exact);
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,14 @@
|
||||
namespace zero.Api.Endpoints.Media;
|
||||
|
||||
public class MediaBasic : BasicModel<zero.Media.Media>
|
||||
public class MediaBasic : ZeroIdEntity
|
||||
{
|
||||
public bool IsPreferred { get; set; }
|
||||
public string ParentId { get; set; }
|
||||
|
||||
public string Code { get; set; }
|
||||
public string Name { get; set; }
|
||||
|
||||
public bool IsFolder { get; set; }
|
||||
|
||||
public string Image { get; set; }
|
||||
|
||||
public int Children { get; set; }
|
||||
}
|
||||
@@ -14,8 +14,12 @@ public class MediaMapperProfile : ZeroMapperProfile
|
||||
|
||||
protected virtual void Map(zero.Media.Media source, MediaBasic target, IZeroMapperContext ctx)
|
||||
{
|
||||
this.MapBasicData(source, target);
|
||||
|
||||
target.Id = source.Id;
|
||||
target.Name = source.Name;
|
||||
target.ParentId = source.ParentId;
|
||||
target.IsFolder = source.Type == MediaType.Folder;
|
||||
target.Image = null;
|
||||
target.Children = 0;
|
||||
}
|
||||
|
||||
protected virtual void Map(zero.Media.Media source, MediaEdit target, IZeroMapperContext ctx)
|
||||
|
||||
@@ -17,28 +17,20 @@ public class MediaController : ZeroApiTreeEntityStoreController<zero.Media.Media
|
||||
|
||||
[HttpGet("empty")]
|
||||
[ZeroAuthorize(MediaPermissions.Create)]
|
||||
public virtual async Task<ActionResult<MediaEdit>> Empty(MediaType type, string flavor = null) => await EmptyModel<MediaEdit>(flavor, x => x.Type = type);
|
||||
public virtual async Task<ActionResult<zero.Media.Media>> Empty(MediaType type, string flavor = null) => await EmptyModel(flavor, x => x.Type = type);
|
||||
|
||||
|
||||
[HttpGet("{id}")]
|
||||
[ZeroAuthorize(MediaPermissions.Read)]
|
||||
public virtual async Task<ActionResult<MediaEdit>> Get(string id, string changeVector = null)
|
||||
public virtual async Task<ActionResult<zero.Media.Media>> Get(string id, string changeVector = null) => await GetModel(id, changeVector);
|
||||
|
||||
|
||||
[HttpGet("{parentId}/children")]
|
||||
[ZeroAuthorize(MediaPermissions.Read)]
|
||||
public virtual async Task<ActionResult<Paged>> GetChildren(string parentId, [FromQuery] ListQuery<zero.Media.Media> query)
|
||||
{
|
||||
zero.Media.Media model = await Store.Load(id, changeVector);
|
||||
|
||||
if (model == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
HttpContext.Items[ApiConstants.ChangeToken] = Store.GetChangeToken(model);
|
||||
|
||||
if (model.Type == MediaType.Folder)
|
||||
{
|
||||
return Mapper.Map<zero.Media.Media, MediaFolderEdit>(model);
|
||||
}
|
||||
|
||||
return Mapper.Map<zero.Media.Media, MediaFileEdit>(model);
|
||||
query.OrderQuery = q => q.OrderBy(x => x.Type).OrderByDescending(x => x.CreatedDate);
|
||||
return await GetChildModelsByIndex<MediaBasic, zero_Api_Media_Listing>(parentId == "root" ? null : parentId, query);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import registerComponents from '../components/register';
|
||||
import registerFormComponents from '../forms/register';
|
||||
import registerEditorComponents from '../editor/register';
|
||||
import { getRouterConfig, appendRouterGuards } from './router/routerConfig';
|
||||
import { countryPlugin, applicationPlugin, settingsPlugin, languagePlugin } from '../modules';
|
||||
import { countryPlugin, applicationPlugin, settingsPlugin, languagePlugin, mediaPlugin } from '../modules';
|
||||
import { ZeroSchema } from 'zero/schemas';
|
||||
import { ZeroSchemaProp } from './zero';
|
||||
import * as zeroOptions from '../options';
|
||||
@@ -86,6 +86,7 @@ export class ZeroRuntime implements Zero
|
||||
applicationPlugin.install(pluginOptions);
|
||||
settingsPlugin.install(pluginOptions);
|
||||
languagePlugin.install(pluginOptions);
|
||||
mediaPlugin.install(pluginOptions);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -2,4 +2,5 @@
|
||||
export * from './countries';
|
||||
export * from './applications';
|
||||
export * from './settings';
|
||||
export * from './languages';
|
||||
export * from './languages';
|
||||
export * from './media';
|
||||
@@ -0,0 +1,17 @@
|
||||
import { MediaType } from 'zero/media';
|
||||
import { get, post, put, del, ApiRequestConfig, ApiRequestQuery } from '../../services/request';
|
||||
|
||||
export default {
|
||||
|
||||
getEmpty: (type: MediaType, flavor?: string, config?: ApiRequestConfig) => get("media/empty", { ...config, params: { type, flavor } }),
|
||||
|
||||
getById: (id: string, changeVector?: string, config?: ApiRequestConfig) => get('media/' + id, { ...config, params: { changeVector } }),
|
||||
|
||||
getChildren: (parentId: string, query: ApiRequestQuery, config?: ApiRequestConfig) => get(`media/${parentId}/children`, { ...config, params: { ...query } }),
|
||||
|
||||
//create: (model: any, config?: ApiRequestConfig) => post('countries', model, config),
|
||||
|
||||
//update: (model: any, config?: ApiRequestConfig) => put('countries/' + model.id, model, config),
|
||||
|
||||
//delete: (id: string, config?: ApiRequestConfig) => del('countries/' + id, config),
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
<template>
|
||||
<div>
|
||||
media {{id}}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: ['id']
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,10 @@
|
||||
import api from './api';
|
||||
import editor from './schemas/editor';
|
||||
//import list from './schemas/list';
|
||||
import plugin from './plugin';
|
||||
|
||||
export {
|
||||
api as mediaApi,
|
||||
editor as mediaSchema,
|
||||
plugin as mediaPlugin
|
||||
};
|
||||
@@ -0,0 +1,89 @@
|
||||
<template>
|
||||
<div class="media-content">
|
||||
<ui-header-bar :back-button="!!parentId" title="Media">
|
||||
<!--<template v-slot:title>
|
||||
<h2 class="ui-header-bar-title">
|
||||
<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>-->
|
||||
</ui-header-bar>
|
||||
|
||||
<div class="ui-view-box">
|
||||
<div class="media-items">
|
||||
<div class="media-items-grid">
|
||||
<media-item v-for="item in items" :key="item.id" :value="item" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import api from './api';
|
||||
import MediaItem from './partials/overview-item.vue';
|
||||
|
||||
export default defineComponent({
|
||||
props: ['parentId'],
|
||||
|
||||
components: { MediaItem },
|
||||
|
||||
data: () => ({
|
||||
items: [],
|
||||
paging: {}
|
||||
}),
|
||||
|
||||
|
||||
watch: {
|
||||
'$route': async function (val)
|
||||
{
|
||||
await this.setup();
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
async mounted()
|
||||
{
|
||||
await this.setup();
|
||||
},
|
||||
|
||||
|
||||
methods: {
|
||||
async setup()
|
||||
{
|
||||
const response = await api.getChildren(this.$route.params.parentId || 'root', {});
|
||||
this.items = response.data;
|
||||
this.paging = response.paging;
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
<style lang="scss">
|
||||
.media
|
||||
{
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.media-content
|
||||
{
|
||||
height: 100vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.media-items-grid
|
||||
{
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
||||
align-items: stretch;
|
||||
gap: var(--padding);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,156 @@
|
||||
<template>
|
||||
<router-link :to="link" class="media-item">
|
||||
<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="(value.isFolder ? 'fth-folder' : 'fth-file')" :size="36" :stroke-width="1.5" /></span>
|
||||
</div>
|
||||
<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>
|
||||
</router-link>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'mediaOverviewItem',
|
||||
|
||||
props: {
|
||||
value: {
|
||||
type: Object,
|
||||
default: () => { }
|
||||
},
|
||||
selected: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
link()
|
||||
{
|
||||
if (this.value.isFolder)
|
||||
{
|
||||
return { name: 'media', params: { parentId: this.value.id } };
|
||||
}
|
||||
|
||||
return { name: 'media-edit', params: { id: this.value.id } };
|
||||
},
|
||||
covered()
|
||||
{
|
||||
return false; //this.value.aspectRatio < 1.2 && this.value.aspectRatio > 0.8;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.media-item
|
||||
{
|
||||
width: 100%;
|
||||
min-height: 200px;
|
||||
display: grid;
|
||||
grid-template-rows: auto 1fr;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
line-height: 1.4;
|
||||
color: var(--color-text);
|
||||
font-size: var(--font-size);
|
||||
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;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
height: 200px;
|
||||
width: 100%;
|
||||
background: var(--color-box);
|
||||
border-radius: var(--radius);
|
||||
overflow: visible !important;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
box-shadow: var(--shadow-short);
|
||||
}
|
||||
|
||||
.media-item.is-selected .media-item-preview
|
||||
{
|
||||
//border: 3px solid var(--color-primary);
|
||||
}
|
||||
|
||||
.media-item-image
|
||||
{
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
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: 30px;
|
||||
height: 30px;
|
||||
border-radius: 20px;
|
||||
border: 5px solid var(--color-bg);
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
.media-item-text
|
||||
{
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
margin: 0;
|
||||
padding-right: 16px;
|
||||
font-weight: bold;
|
||||
|
||||
.-minor
|
||||
{
|
||||
font-weight: 400;
|
||||
color: var(--color-text-dim);
|
||||
}
|
||||
}
|
||||
|
||||
.media-item-shared
|
||||
{
|
||||
margin-left: .4em;
|
||||
color: var(--color-synchronized);
|
||||
position: relative;
|
||||
top: 2px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,17 @@
|
||||
import { ZeroPlugin, ZeroPluginOptions } from '../../core';
|
||||
import { defineAsyncComponent } from 'vue';
|
||||
|
||||
export default {
|
||||
name: "zero.countries",
|
||||
|
||||
install(app: ZeroPluginOptions)
|
||||
{
|
||||
//app.vue.component('ui-countrypicker', defineAsyncComponent(() => import('./ui-countrypicker.vue')));
|
||||
|
||||
app.route({ name: 'media', path: '/media/:parentId?', component: () => import('./overview.vue'), props: true });
|
||||
app.route({ name: 'media-edit', path: '/media/edit/:id?', component: () => import('./detail.vue'), props: true });
|
||||
|
||||
//app.schema('countries', () => import('./schemas/list'));
|
||||
//app.schema('countries:edit', () => import('./schemas/editor'));
|
||||
}
|
||||
} as ZeroPlugin;
|
||||
@@ -0,0 +1,12 @@
|
||||
|
||||
import Editor from '../../../editor/editor';
|
||||
|
||||
const editor = new Editor('countries:edit', '@country.fields.');
|
||||
editor.blueprintAlias = 'country';
|
||||
|
||||
editor.field('name', { label: '@ui.name' }).text(120).required();
|
||||
//editor.field('alias', { label: '@ui.alias' }).text().required();
|
||||
editor.field('code').text(2).required();
|
||||
editor.field('isPreferred').toggle();
|
||||
|
||||
export default editor;
|
||||
@@ -0,0 +1,19 @@
|
||||
|
||||
import List from '../../../schemas/list/list';
|
||||
import api from '../api';
|
||||
|
||||
const list = new List('countries');
|
||||
const prefix = '@country.fields.';
|
||||
|
||||
list.templateLabel = x => prefix + x;
|
||||
list.link = 'countries-edit';
|
||||
|
||||
list.onFetch(filter => api.getByQuery(filter));
|
||||
|
||||
list.column('flag', { width: 62, canSort: false, hideLabel: true }).custom((value, model) => `<i class="ui-icon" data-symbol="flag-${model.code.toLowerCase()}"></i>`, true, 'flag');
|
||||
list.column('name').name();
|
||||
list.column('code').custom(val => val.toUpperCase());
|
||||
list.column('isPreferred').boolean();
|
||||
list.column('isActive').active();
|
||||
|
||||
export default list;
|
||||
@@ -0,0 +1,61 @@
|
||||
<template>
|
||||
<div class="ui-countrypicker" :class="{'is-disabled': disabled }">
|
||||
country picker
|
||||
<!--<ui-pick :config="pickerConfig" :value="value" @input="onChange" :disabled="disabled" />-->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
import api from './api';
|
||||
|
||||
export default {
|
||||
name: 'uiCountrypicker',
|
||||
|
||||
props: {
|
||||
modelValue: {
|
||||
type: [String, Array],
|
||||
default: null
|
||||
},
|
||||
limit: {
|
||||
type: Number,
|
||||
default: 1
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
options: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
}
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
previews: [],
|
||||
pickerConfig: {}
|
||||
}),
|
||||
|
||||
//created()
|
||||
//{
|
||||
// this.pickerConfig = _extend({
|
||||
// scope: 'country',
|
||||
// items: CountriesApi.getForPicker,
|
||||
// previews: CountriesApi.getPreviews,
|
||||
// limit: this.limit,
|
||||
// multiple: this.limit > 1,
|
||||
// preview: {
|
||||
|
||||
// }
|
||||
// }, this.options);
|
||||
//},
|
||||
|
||||
|
||||
//methods: {
|
||||
// onChange(value)
|
||||
// {
|
||||
// this.$emit('input', value);
|
||||
// }
|
||||
//}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,5 @@
|
||||
|
||||
declare module 'zero/media'
|
||||
{
|
||||
export type MediaType = 'folder' | 'file' | 'image';
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
<template>
|
||||
<div class="ui-languagepicker" :class="{'is-disabled': disabled }">
|
||||
<ui-pick :config="pickerConfig" :value="value" @input="onChange" :disabled="disabled" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
import LanguagesApi from 'zero/api/languages.js'
|
||||
import { extend as _extend } from 'underscore'
|
||||
|
||||
export default {
|
||||
name: 'uiLanguagepicker',
|
||||
|
||||
props: {
|
||||
value: {
|
||||
type: [String, Array],
|
||||
default: null
|
||||
},
|
||||
limit: {
|
||||
type: Number,
|
||||
default: 1
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
options: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
}
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
previews: [],
|
||||
pickerConfig: {}
|
||||
}),
|
||||
|
||||
created()
|
||||
{
|
||||
this.pickerConfig = _extend({
|
||||
scope: 'language',
|
||||
items: LanguagesApi.getForPicker,
|
||||
previews: LanguagesApi.getPreviews,
|
||||
limit: this.limit,
|
||||
multiple: this.limit > 1
|
||||
}, this.options);
|
||||
},
|
||||
|
||||
|
||||
methods: {
|
||||
onChange(value)
|
||||
{
|
||||
this.$emit('input', value);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,769 +0,0 @@
|
||||
<template>
|
||||
<div class="ui-pick" :class="{'is-disabled': disabled, 'is-combined': configuration.preview.combined }">
|
||||
|
||||
<!-- previews -->
|
||||
<div class="ui-pick-previews" v-if="configuration.preview.enabled && previews.length > 0 && !configuration.preview.combined" v-sortable="{ onUpdate: onSortingUpdated, enabled: configuration.sortable }">
|
||||
<div v-for="preview in previews" :key="preview[configuration.keys.id]" class="ui-pick-preview">
|
||||
<ui-select-button :icon="getPreviewIcon(preview)" :icon-as-image="configuration.preview.iconAsImage" :label="preview[configuration.keys.name]" :description="getPreviewDescription(preview)" :disabled="disabled" @click="pick(preview[configuration.keys.id])" :tokens="preview" />
|
||||
<ui-icon-button v-if="!disabled && configuration.preview.delete" @click="remove(preview[configuration.keys.id])" icon="fth-x" title="@ui.close" :size="14" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- combined preview -->
|
||||
<div class="ui-pick-previews" v-if="configuration.preview.enabled && configuration.preview.combined">
|
||||
<div class="ui-pick-preview">
|
||||
<ui-select-button icon="fth-check-circle" :label="combinedTitle" :disabled="disabled" @click="pick()" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- add button -->
|
||||
<div class="ui-pick-add" v-if="canAdd && configuration.addButton.enabled && !configuration.preview.combined">
|
||||
<slot name="add">
|
||||
<ui-select-button icon="fth-plus" :label="configuration.addButton.label" @click="pick()" :disabled="disabled" />
|
||||
</slot>
|
||||
</div>
|
||||
|
||||
<!-- overlay -->
|
||||
<ui-dropdown ref="overlay" class="ui-pick-overlay" @opened="overlayOpened">
|
||||
|
||||
<!-- headline -->
|
||||
<div class="ui-pick-overlay-head">
|
||||
<div class="ui-pick-overlay-head-title">
|
||||
<span class="-name" v-localize="title"></span>
|
||||
<span class="-max" v-if="configuration.limit > 1" v-localize="{ key: '@ui.pick.max', tokens: { count: configuration.limit }}"></span>
|
||||
</div>
|
||||
<ui-icon-button @click="hide" icon="fth-x" title="@ui.close" />
|
||||
</div>
|
||||
|
||||
<!-- search -->
|
||||
<ui-search v-if="configuration.search.enabled" ref="search" class="ui-pick-overlay-search" :value="searchValue" @input="onSearch" @submit="onSearchSubmit">
|
||||
<template v-slot:button="search" v-if="configuration.autocomplete">
|
||||
<button type="button" class="ui-searchinput-button" v-localize:title="'@ui.search.button'" @click="search.onSubmit"><ui-icon symbol="fth-check"></ui-icon></button>
|
||||
</template>
|
||||
</ui-search>
|
||||
|
||||
<!-- items -->
|
||||
<div class="ui-pick-overlay-items">
|
||||
<button v-for="item in items" :key="item[configuration.keys.id]" type="button" class="ui-pick-overlay-item" @click="select(item)" :class="{'is-selected': isSelected(item) }">
|
||||
<ui-icon v-if="item[configuration.keys.icon]" class="-icon" :symbol="item[configuration.keys.icon]" />
|
||||
<div class="ui-pick-overlay-item-title">
|
||||
<span class="-name" v-localize="item[configuration.keys.name]"></span>
|
||||
<span v-if="item[configuration.keys.description] && configuration.list.description" class="-text" v-localize="item[configuration.keys.description]"></span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- loading + empty states -->
|
||||
<div class="ui-pick-overlay-center" v-if="isLoading || (!configuration.autocomplete && !items.length)">
|
||||
<div v-if="!isLoading && !configuration.autocomplete && !items.length" class="ui-pick-overlay-message">
|
||||
<ui-icon class="ui-pick-overlay-message-icon" symbol="fth-list" :size="18" />
|
||||
No items found
|
||||
</div>
|
||||
<ui-loading v-if="isLoading" />
|
||||
</div>
|
||||
</ui-dropdown>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
import { extend as _extend, filter as _filter, debounce as _debounce, isArray as _isArray, clone as _clone, find as _find, map as _map } from 'underscore';
|
||||
import Arrays from 'zero/helpers/arrays.js';
|
||||
|
||||
const defaultConfig = {
|
||||
// picker items, can either be a static list or a promise
|
||||
items: [],
|
||||
// preview items, can either be a static list or a promise. If null, take the items list
|
||||
previews: [],
|
||||
// exclude Ids from the picker selection items
|
||||
excludedIds: [],
|
||||
// autocomplete allows entering custom texts
|
||||
autocomplete: false,
|
||||
// maximum selection count
|
||||
limit: 10,
|
||||
// multiple selection
|
||||
multiple: true,
|
||||
// whether the previews/results are sortable or not
|
||||
sortable: true,
|
||||
// close picker when an item is clicked
|
||||
closeOnClick: true,
|
||||
// title in dropdown
|
||||
title: null,
|
||||
// automatically open picker
|
||||
autoOpen: false,
|
||||
// picker has pagination
|
||||
paging: false,
|
||||
|
||||
keys: {
|
||||
// id key
|
||||
id: 'id',
|
||||
// name key
|
||||
name: 'name',
|
||||
// description key
|
||||
description: 'text',
|
||||
// icon key
|
||||
icon: 'icon'
|
||||
},
|
||||
|
||||
addButton: {
|
||||
// hide the add button
|
||||
enabled: true,
|
||||
// text key for the add button
|
||||
label: '@ui.select'
|
||||
},
|
||||
|
||||
list: {
|
||||
// output description text if available (second line)
|
||||
description: false
|
||||
},
|
||||
|
||||
preview: {
|
||||
// output previews
|
||||
enabled: true,
|
||||
// output all selected items in one line (comma-separated)
|
||||
combined: false,
|
||||
// prefixed title when combine=true
|
||||
combinedTitle: null,
|
||||
// default icon used for previews
|
||||
defaultIcon: 'fth-square',
|
||||
// hides the icon in the preview
|
||||
icon: true,
|
||||
// hides the delete icon in preview
|
||||
delete: true,
|
||||
// displays an image instead of an icon in the preview
|
||||
iconAsImage: false,
|
||||
// output description text if available (second line)
|
||||
description: true
|
||||
},
|
||||
|
||||
search: {
|
||||
// hides the search input
|
||||
enabled: true,
|
||||
// can force a local search for remote items (via promise)
|
||||
local: true,
|
||||
// sets the current model as the search input when opened
|
||||
setDefaultValue: false,
|
||||
// focus search input on open
|
||||
focus: false,
|
||||
// placeholder key in search input
|
||||
placeholder: null //vm.options.autocomplete ? 'ui_search_autocomplete' : 'ui_search',
|
||||
}
|
||||
};
|
||||
|
||||
export default {
|
||||
name: 'uiPick',
|
||||
|
||||
props: {
|
||||
value: {
|
||||
type: [String, Array],
|
||||
default: null
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
config: {
|
||||
type: Object,
|
||||
default: () =>
|
||||
{
|
||||
return defaultConfig;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
watch: {
|
||||
config: {
|
||||
deep: true,
|
||||
handler(newval, oldval)
|
||||
{
|
||||
if (JSON.stringify(newval) !== JSON.stringify(oldval))
|
||||
{
|
||||
this.buildConfig();
|
||||
this.loaded = false;
|
||||
}
|
||||
}
|
||||
},
|
||||
value(val)
|
||||
{
|
||||
this.onValueChanged(val);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
computed: {
|
||||
multiple()
|
||||
{
|
||||
return this.configuration.multiple;
|
||||
},
|
||||
canAdd()
|
||||
{
|
||||
return (!this.value && !this.multiple) || (!this.value || this.value.length < this.configuration.limit);
|
||||
},
|
||||
isRemote()
|
||||
{
|
||||
return typeof this.configuration.items === 'function';
|
||||
},
|
||||
title()
|
||||
{
|
||||
if (!!this.configuration.title) return this.configuration.title;
|
||||
if (this.configuration.autocomplete) return '@ui.pick.title_autocomplete';
|
||||
if (this.multiple) return '@ui.pick.title_multiple';
|
||||
return '@ui.pick.title';
|
||||
},
|
||||
combinedTitle()
|
||||
{
|
||||
let html = this.configuration.preview.combinedTitle ? this.configuration.preview.combinedTitle : '';
|
||||
|
||||
if (this.previews.length > 0)
|
||||
{
|
||||
html += ': <b>' + _map(this.previews, p => p[this.configuration.keys.name]).join(', ') + '</b>';
|
||||
}
|
||||
|
||||
return html;
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
data: () => ({
|
||||
configuration: {},
|
||||
previews: [],
|
||||
allItems: [],
|
||||
items: [],
|
||||
selected: [],
|
||||
loaded: false,
|
||||
isLoading: false,
|
||||
debouncedUpdate: null,
|
||||
searchValue: ''
|
||||
}),
|
||||
|
||||
|
||||
created()
|
||||
{
|
||||
this.buildConfig();
|
||||
this.debouncedUpdate = _debounce(this.loadSuggestions, 300);
|
||||
this.onValueChanged(this.value);
|
||||
},
|
||||
|
||||
|
||||
mounted()
|
||||
{
|
||||
if (this.configuration.autoOpen)
|
||||
{
|
||||
this.pick();
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
methods: {
|
||||
|
||||
refresh()
|
||||
{
|
||||
this.buildConfig();
|
||||
this.loaded = false;
|
||||
},
|
||||
|
||||
onValueChanged(val)
|
||||
{
|
||||
if (this.multiple)
|
||||
{
|
||||
this.selected = _isArray(val) && val.length ? _clone(val) : [];
|
||||
}
|
||||
else
|
||||
{
|
||||
this.selected = val ? [val] : [];
|
||||
}
|
||||
|
||||
this.loadPreviews();
|
||||
},
|
||||
|
||||
|
||||
buildConfig()
|
||||
{
|
||||
var config = JSON.parse(JSON.stringify(defaultConfig));
|
||||
this.configuration = _extend(JSON.parse(JSON.stringify(config)), this.config);
|
||||
this.configuration.search = _extend(config.search, this.config.search || {});
|
||||
this.configuration.addButton = _extend(config.addButton, this.config.addButton || {});
|
||||
this.configuration.preview = _extend(config.preview, this.config.preview || {});
|
||||
this.configuration.list = _extend(config.list, this.config.list || {});
|
||||
this.configuration.keys = _extend(config.keys, this.config.keys || {});
|
||||
},
|
||||
|
||||
|
||||
overlayOpened()
|
||||
{
|
||||
if (!this.loaded)
|
||||
{
|
||||
this.load();
|
||||
}
|
||||
if (this.configuration.search.enabled && this.configuration.search.focus)
|
||||
{
|
||||
this.$nextTick(() => this.$refs.search.focus());
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
pick()
|
||||
{
|
||||
this.$refs.overlay.toggle();
|
||||
|
||||
if (!this.loaded)
|
||||
{
|
||||
this.load();
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
hide()
|
||||
{
|
||||
this.$refs.overlay.hide();
|
||||
},
|
||||
|
||||
|
||||
remove(id)
|
||||
{
|
||||
let index = this.selected.indexOf(id);
|
||||
this.selected.splice(index, 1);
|
||||
this.onChange(this.multiple ? this.selected : null);
|
||||
},
|
||||
|
||||
|
||||
clear()
|
||||
{
|
||||
this.selected = [];
|
||||
this.onChange(this.multiple ? this.selected : null);
|
||||
},
|
||||
|
||||
|
||||
// loads remote items or items from the given configuration
|
||||
// this will store both all items and the reduced search results
|
||||
load()
|
||||
{
|
||||
if (this.isLoading)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
this.isLoading = true;
|
||||
this.allItems = [];
|
||||
|
||||
let onLoaded = (items) =>
|
||||
{
|
||||
if (this.configuration.paging && typeof items === 'object' && !Array.isArray(items))
|
||||
{
|
||||
// TODO we need to store metadata to allow pagination
|
||||
items = items.items;
|
||||
}
|
||||
this.allItems = items;
|
||||
this.loadSuggestions();
|
||||
this.loaded = true;
|
||||
this.isLoading = false;
|
||||
};
|
||||
|
||||
if (this.isRemote)
|
||||
{
|
||||
this.configuration.items().then(onLoaded);
|
||||
}
|
||||
else
|
||||
{
|
||||
onLoaded(this.configuration.items);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
loadSuggestions()
|
||||
{
|
||||
let items = [];
|
||||
let search = this.searchValue;
|
||||
|
||||
let handleResult = (res) =>
|
||||
{
|
||||
if (this.configuration.paging && typeof res === 'object' && !Array.isArray(res))
|
||||
{
|
||||
// TODO we need to store metadata to allow pagination
|
||||
res = res.items;
|
||||
}
|
||||
|
||||
if (this.configuration.excludedIds && this.configuration.excludedIds.length)
|
||||
{
|
||||
res = _filter(res, (item) => this.configuration.excludedIds.indexOf(item[this.configuration.keys.id]) < 0);
|
||||
}
|
||||
|
||||
this.items = res;
|
||||
};
|
||||
|
||||
if (!search)
|
||||
{
|
||||
items = this.allItems;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!this.isRemote || this.configuration.search.local)
|
||||
{
|
||||
items = _filter(this.allItems, (item) => item.name.toLowerCase().indexOf(search) > -1);
|
||||
}
|
||||
else if (this.isRemote)
|
||||
{
|
||||
this.configuration.items(search).then(res => handleResult(res));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
handleResult(items);
|
||||
},
|
||||
|
||||
|
||||
loadPreviews()
|
||||
{
|
||||
let onLoaded = (items, needsFilter) =>
|
||||
{
|
||||
if (this.configuration.paging && typeof items === 'object' && !Array.isArray(items))
|
||||
{
|
||||
// TODO we need to store metadata to allow pagination
|
||||
items = items.items;
|
||||
}
|
||||
|
||||
if (needsFilter)
|
||||
{
|
||||
this.previews = [];
|
||||
this.selected.forEach(id =>
|
||||
{
|
||||
let res = _find(items, item => item[this.configuration.keys.id] === id);
|
||||
|
||||
if (!res)
|
||||
{
|
||||
// TODO push error output
|
||||
}
|
||||
else
|
||||
{
|
||||
this.previews.push(_clone(res));
|
||||
}
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
this.previews = items;
|
||||
}
|
||||
|
||||
//console.info(JSON.parse(JSON.stringify(this.previews)));
|
||||
};
|
||||
|
||||
if (this.configuration.autocomplete)
|
||||
{
|
||||
onLoaded(_map(this.selected, s =>
|
||||
{
|
||||
let item = { };
|
||||
item[this.configuration.keys.id] = s;
|
||||
item[this.configuration.keys.name] = s;
|
||||
return item;
|
||||
}), false);
|
||||
}
|
||||
else
|
||||
{
|
||||
let promise = (_isArray(this.configuration.previews) && this.configuration.previews.length) || typeof this.configuration.previews === 'function' ? this.configuration.previews : this.configuration.items;
|
||||
let isFunc = typeof promise === 'function';
|
||||
|
||||
if (isFunc && this.selected.length > 0)
|
||||
{
|
||||
promise(this.selected).then(onLoaded);
|
||||
}
|
||||
else if (isFunc)
|
||||
{
|
||||
onLoaded([]);
|
||||
}
|
||||
else
|
||||
{
|
||||
onLoaded(promise, true);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
onSearch(value)
|
||||
{
|
||||
this.searchValue = value;
|
||||
if (!this.isRemote || this.configuration.search.local)
|
||||
{
|
||||
this.loadSuggestions();
|
||||
}
|
||||
else
|
||||
{
|
||||
this.debouncedUpdate();
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
onSearchSubmit()
|
||||
{
|
||||
let value = this.searchValue.trim();
|
||||
let item = {};
|
||||
item[this.configuration.keys.id] = value;
|
||||
item[this.configuration.keys.name] = value;
|
||||
this.select(item);
|
||||
},
|
||||
|
||||
|
||||
select(item)
|
||||
{
|
||||
let value = this.configuration.autocomplete ? item[this.configuration.keys.name] : item[this.configuration.keys.id];
|
||||
|
||||
if (this.multiple)
|
||||
{
|
||||
if (!this.canAdd)
|
||||
{
|
||||
if (this.limit > 1)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
this.selected = [];
|
||||
}
|
||||
|
||||
var index = this.selected.indexOf(value);
|
||||
|
||||
if (index > -1)
|
||||
{
|
||||
this.selected.splice(index, 1);
|
||||
this.onDeselected(value, index);
|
||||
}
|
||||
else
|
||||
{
|
||||
this.selected.push(value);
|
||||
this.onSelected(value, item);
|
||||
}
|
||||
|
||||
this.onChange(this.selected);
|
||||
}
|
||||
else
|
||||
{
|
||||
this.selected = [value];
|
||||
this.onChange(value);
|
||||
this.onSelected(value, item);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
onSelected(value, item)
|
||||
{
|
||||
this.$emit('select', value, item);
|
||||
},
|
||||
|
||||
|
||||
onDeselected(value, index)
|
||||
{
|
||||
this.$emit('deselect', value, index);
|
||||
},
|
||||
|
||||
|
||||
onChange(value)
|
||||
{
|
||||
this.$emit('input', value);
|
||||
this.$emit('change', value);
|
||||
|
||||
if (this.configuration.closeOnClick)
|
||||
{
|
||||
this.$refs.overlay.hide();
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
isSelected(item)
|
||||
{
|
||||
let value = this.configuration.autocomplete ? item[this.configuration.keys.name] : item[this.configuration.keys.id];
|
||||
return this.selected.indexOf(value) > -1;
|
||||
},
|
||||
|
||||
|
||||
getPreviewIcon(preview)
|
||||
{
|
||||
return this.configuration.preview.icon ? (preview[this.configuration.keys.icon] || this.configuration.preview.defaultIcon) : null;
|
||||
},
|
||||
|
||||
|
||||
getPreviewDescription(preview)
|
||||
{
|
||||
return this.configuration.preview.description ? preview[this.configuration.keys.description] : null;
|
||||
},
|
||||
|
||||
|
||||
onSortingUpdated(ev)
|
||||
{
|
||||
this.selected = Arrays.move(this.selected, ev.oldIndex, ev.newIndex);
|
||||
this.onChange(this.multiple ? this.selected : this.selected[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.ui-pick-overlay-search
|
||||
{
|
||||
margin: 15px 15px 5px;
|
||||
|
||||
.ui-input
|
||||
{
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.ui-pick-overlay
|
||||
{
|
||||
.ui-dropdown
|
||||
{
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
max-width: 380px;
|
||||
}
|
||||
}
|
||||
|
||||
.ui-pick-overlay-center
|
||||
{
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin: 30px 0;
|
||||
}
|
||||
|
||||
.ui-pick-overlay-head
|
||||
{
|
||||
padding: 15px 15px 5px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.ui-icon-button
|
||||
{
|
||||
background: none;
|
||||
}
|
||||
}
|
||||
|
||||
.ui-pick-overlay-head-title
|
||||
{
|
||||
font-size: var(--font-size-l);
|
||||
font-weight: 600;
|
||||
|
||||
.-max
|
||||
{
|
||||
font-size: var(--font-size-s);
|
||||
font-weight: 400;
|
||||
color: var(--color-text-dim);
|
||||
margin-left: .6em;
|
||||
}
|
||||
}
|
||||
|
||||
.ui-pick-overlay-message
|
||||
{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
padding: 0 15px;
|
||||
}
|
||||
|
||||
.ui-pick-overlay-message-icon
|
||||
{
|
||||
font-size: 18px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.ui-pick-overlay-items
|
||||
{
|
||||
margin: 15px 0;
|
||||
max-height: 290px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.ui-pick-overlay-item
|
||||
{
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
padding: 10px 18px;
|
||||
min-height: 48px;
|
||||
width: 100%;
|
||||
border-radius: var(--radius);
|
||||
align-items: center;
|
||||
transition: background .2s, transform .2s, opacity .2s;
|
||||
position: relative;
|
||||
|
||||
&:hover
|
||||
{
|
||||
background: var(--color-tree-selected);
|
||||
}
|
||||
|
||||
&.is-selected
|
||||
{
|
||||
padding-right: 32px;
|
||||
|
||||
.-name
|
||||
{
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-selected:after
|
||||
{
|
||||
font-family: "Feather";
|
||||
content: "\e83e";
|
||||
font-size: 16px;
|
||||
color: var(--color-primary);
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
}
|
||||
|
||||
.-icon
|
||||
{
|
||||
font-size: var(--font-size-l);
|
||||
margin-right: var(--padding-s);
|
||||
}
|
||||
}
|
||||
|
||||
.ui-pick-overlay-item-title
|
||||
{
|
||||
.-name
|
||||
{
|
||||
display: block;
|
||||
}
|
||||
|
||||
.-text
|
||||
{
|
||||
display: block;
|
||||
color: var(--color-text-dim);
|
||||
font-size: var(--font-size-s);
|
||||
margin-top: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
.ui-pick-preview
|
||||
{
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.ui-pick-preview + .ui-pick-preview,
|
||||
.ui-pick-previews + .ui-select-button,
|
||||
.ui-pick-previews + .ui-pick-add
|
||||
{
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.ui-pick.is-combined .ui-select-button-label
|
||||
{
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.ui-pick-previews
|
||||
{
|
||||
.ui-icon-button
|
||||
{
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
flex-shrink: 0;
|
||||
|
||||
.ui-button-icon
|
||||
{
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user