This commit is contained in:
2021-01-06 20:46:20 +01:00
13 changed files with 461 additions and 226 deletions
+27 -6
View File
@@ -1,7 +1,10 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using zero.Core.Entities;
using zero.Core.Extensions;
using zero.Core.Handlers;
using zero.Core.Options;
namespace zero.Core.Api
@@ -10,23 +13,41 @@ namespace zero.Core.Api
{
protected IZeroOptions Options { get; private set; }
public ModulesApi(IZeroOptions options, IBackofficeStore store) : base(store)
protected IHandlerHolder Handler { get; private set; }
public ModulesApi(IZeroOptions options, IBackofficeStore store, IHandlerHolder handler) : base(store)
{
Options = options;
Handler = handler;
}
/// <inheritdoc />
public IList<ModuleType> GetModuleTypes(string[] tags = default)
public async Task<IList<ModuleType>> GetModuleTypes(string[] tags = default, string pageId = default)
{
IEnumerable<ModuleType> modules = Options.Modules.GetAllItems();
IEnumerable<ModuleType> types = Options.Modules.GetAllItems();
List<ModuleType> modules = types.ToList();
IPage page = null;
if (!pageId.IsNullOrEmpty())
{
page = await GetById<IPage>(pageId);
}
if (tags?.Length > 0)
{
modules = modules.Where(x => x.Tags.Any(t => tags.Contains(t, StringComparer.InvariantCultureIgnoreCase)));
modules = types.Where(x => x.Tags.Any(t => tags.Contains(t, StringComparer.InvariantCultureIgnoreCase))).ToList();
}
return modules.ToList();
IModuleTypeHandler handler = Handler.Get<IModuleTypeHandler>();
// if there is no registered handler we just allow all page types
if (handler == null)
{
return modules;
}
return handler.GetAllowedModuleTypes(Backoffice.Context.Application, types, page, tags)?.ToList() ?? new();
}
@@ -43,7 +64,7 @@ namespace zero.Core.Api
/// <summary>
/// Get all available module types (can be limited to the passed tags)
/// </summary>
IList<ModuleType> GetModuleTypes(string[] tags = default);
Task<IList<ModuleType>> GetModuleTypes(string[] tags = default, string pageId = default);
/// <summary>
/// Get a specific module type by alias
-1
View File
@@ -118,7 +118,6 @@ namespace zero.Core.Api
return types.ToList();
}
return handler.GetAllowedPageTypes(Backoffice.Context.Application, types, parents)?.ToList() ?? new();
}
+10
View File
@@ -0,0 +1,10 @@
using System.Collections.Generic;
using zero.Core.Entities;
namespace zero.Core.Handlers
{
public interface IModuleTypeHandler : IHandler
{
IEnumerable<ModuleType> GetAllowedModuleTypes(IApplication application, IEnumerable<ModuleType> registeredTypes, IPage page = default, string[] tags = default);
}
}
+1 -1
View File
@@ -2,7 +2,7 @@
<div class="ui-header-bar">
<div class="ui-header-bar-inner">
<div class="ui-header-bar-main">
<ui-icon-button v-if="backButton" type="light onbg" @click="onBack" />
<ui-icon-button v-if="false && backButton" type="light onbg" @click="onBack" />
<div class="ui-header-bar-main-title">
<slot name="title">
<h2 class="ui-header-bar-title" :class="{'is-empty': !title && titleEmpty}">
@@ -0,0 +1,270 @@
<template>
<div class="ui-modules-start">
<button v-if="!isSelecting" type="button" class="ui-modules-start-button" @click="startSelection">
<i class="ui-modules-start-button-icon fth-plus"></i>
<p class="ui-modules-start-button-text"><strong>Add content</strong> <!--<br>Compose the page by adding modules--></p>
</button>
<div class="ui-modules-select" v-if="isSelecting">
<ui-icon-button class="ui-modules-select-close" @click="isSelecting=false" icon="fth-x" title="@ui.close" />
<ui-inline-tabs class="ui-modules-select-groups">
<ui-tab v-for="group in moduleTypes" :key="group.key" :label="group.name" :count="group.count">
<div class="ui-modules-select-items">
<button v-for="item in group.items" :key="item.alias" type="button" class="ui-modules-select-item" :disabled="item.isDisabled" @click="editModule(item, true)">
<div class="ui-modules-select-item-icon">
<i :class="item.icon"></i>
</div>
<div class="ui-modules-select-item-text">
<strong v-localize="item.name"></strong>
<span class="is-minor" v-localize="item.description"></span>
</div>
<span v-if="item.isDisabled" class="ui-modules-select-item-disabled">Not allowed <i class="fth-slash"></i></span>
</button>
</div>
</ui-tab>
</ui-inline-tabs>
</div>
</div>
</template>
<script>
import { groupBy as _groupBy, keys as _keys, each as _each } from 'underscore';
import Notification from 'zero/helpers/notification.js';
export default {
name: 'uiModuleSelect',
props: {
value: Array,
config: Object,
types: {
type: Array,
default: () => []
},
disabled: {
type: Boolean,
default: false
}
},
data: () => ({
canAdd: true,
isSelecting: false,
moduleTypes: [],
activeGroup: null
}),
watch: {
types(val)
{
this.rebuildGroups(val);
}
},
created()
{
this.rebuildGroups(this.types);
},
methods: {
rebuildGroups(value)
{
let groups = _groupBy(value, val => val.group);
let index = 0;
_each(groups, (items, key) =>
{
this.moduleTypes.push({
name: !key || key === 'null' ? '@modules.default_group' : key,
index: index++,
count: items.length,
items: items,
isDisabled: false
});
});
this.activeGroup = this.moduleTypes[0];
},
startSelection()
{
if (this.types.length > 1)
{
this.isSelecting = true;
}
else if (this.types.length === 1)
{
this.$emit('selected', this.types[0], true);
}
else
{
Notification.error('No modules allowed', 'There are no modules configured which are allowed for this data type.', { duration: 5000 });
}
},
selectGroup(group)
{
this.activeGroup = group;
},
editModule(module, isAdd)
{
this.$emit('selected', module, isAdd);
},
reset()
{
this.isSelecting = false;
this.activeGroup = this.moduleTypes[0];
}
}
}
</script>
<style lang="scss">
.ui-modules-start
{
margin: 0;
display: flex;
.ui-modules-inner-sortable + &
{
margin-top: var(--padding);
}
}
.ui-modules-start-button
{
color: var(--color-primary);
font-size: var(--font-size);
display: inline-grid;
grid-template-columns: auto 1fr;
gap: 25px;
align-items: center;
}
.ui-modules-start-button-icon
{
width: 52px;
height: 52px;
line-height: 50px !important;
font-size: 20px;
text-align: center;
background: var(--color-button-light);
border-radius: var(--radius);
}
.ui-modules-start-button-text
{
line-height: 1.3;
color: var(--color-text-dim);
margin: 0;
font-size: var(--font-size-s);
strong
{
display: inline-block;
margin-bottom: 2px;
color: var(--color-text);
font-size: var(--font-size);
}
}
.ui-modules-select
{
width: 100%;
position: relative;
.ui-inline-tabs-list
{
padding-right: 50px;
}
}
.ui-modules-select-close
{
position: absolute;
right: 0;
top: 0;
background: none !important;
}
.ui-modules-select-items
{
display: grid;
gap: 10px;
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
align-items: stretch;
}
.ui-modules-select-item
{
display: grid;
grid-template-columns: 76px 1fr;
align-items: center;
height: 100px;
border-radius: var(--radius);
background: var(--color-button-light);
padding: 10px var(--padding) 10px 0;
position: relative;
&[disabled]
{
opacity: .6;
}
}
.ui-modules-select-item-icon
{
display: inline-flex;
justify-content: center;
color: var(--color-text);
font-size: 26px;
}
.ui-modules-select-item-text
{
strong
{
display: block;
margin-bottom: 3px;
}
.is-minor
{
color: var(--color-text-dim);
font-size: var(--font-size-s);
}
}
.ui-modules-select-item-disabled
{
position: absolute;
right: 5px;
top: 5px;
display: inline-flex;
align-items: center;
font-size: 8px;
text-transform: uppercase;
color: var(--color-text-dim-one);
line-height: 1;
font-weight: 600;
i
{
margin-left: 6px;
font-size: 13px;
}
}
</style>
+61 -5
View File
@@ -3,7 +3,10 @@
<div v-if="items.length" class="ui-modules-inner-sortable" v-sortable="{ onUpdate: onSortingUpdated }">
<module-preview v-for="item in items" :key="item.id" :types="moduleTypes" :value="item" @edit="edit" @remove="remove" :disabled="disabled" />
</div>
<module-select ref="moduleSelect" :types="moduleTypes" :value="value" v-if="canAdd" @selected="onAdd" />
<button v-if="canAdd" type="button" class="ui-modules-start-button" @click="selectModule">
<i class="ui-modules-start-button-icon fth-plus"></i>
<p class="ui-modules-start-button-text"><strong>Add content</strong> <!--<br>Compose the page by adding modules--></p>
</button>
</div>
</template>
@@ -12,14 +15,14 @@
import ModulesApi from 'zero/api/modules.js';
import EditModuleOverlay from './edit-module.vue';
import ModulePreview from './module-preview.vue';
import ModuleSelect from './module-select.vue';
import ModuleSelectOverlay from './module-select.vue';
import Overlay from 'zero/helpers/overlay.js';
import Arrays from 'zero/helpers/arrays.js';
export default {
name: 'uiModules',
components: { ModulePreview, ModuleSelect },
components: { ModulePreview },
props: {
value: {
@@ -78,6 +81,16 @@
},
selectModule()
{
Overlay.open({
component: ModuleSelectOverlay,
types: this.moduleTypes,
width: 480
}).then(module => this.onAdd(module), () => { });
},
onAdd(module)
{
this.edit(module, null, true);
@@ -98,6 +111,8 @@
const alias = 'modules.' + module.alias;
const editor = this.zero.getEditor(alias);
console.info(editor);
if (!editor)
{
// TODO throw error
@@ -109,7 +124,6 @@
return ModulesApi.getEmpty(module.alias).then(res =>
{
this.items.push(res.entity);
this.$refs.moduleSelect.reset();
this.onChange();
});
}
@@ -127,7 +141,6 @@
if (isAdd)
{
this.items.push(value);
this.$refs.moduleSelect.reset();
}
else
{
@@ -159,4 +172,47 @@
{
margin-top: -32px;
}
.ui-modules-start-button
{
color: var(--color-primary);
font-size: var(--font-size);
display: inline-grid;
grid-template-columns: auto 1fr;
gap: 25px;
align-items: center;
}
.ui-modules-inner-sortable + .ui-modules-start-button
{
margin-top: var(--padding);
}
.ui-modules-start-button-icon
{
width: 52px;
height: 52px;
line-height: 50px !important;
font-size: 20px;
text-align: center;
background: var(--color-button-light);
border-radius: var(--radius);
}
.ui-modules-start-button-text
{
line-height: 1.3;
color: var(--color-text-dim);
margin: 0;
font-size: var(--font-size-s);
strong
{
display: inline-block;
margin-bottom: 2px;
color: var(--color-text);
font-size: var(--font-size);
}
}
</style>
@@ -118,7 +118,7 @@
{
display: grid;
width: 100%;
grid-template-columns: 48px 1fr auto;
grid-template-columns: 40px 1fr auto;
gap: 12px;
align-items: center;
position: relative;
@@ -157,6 +157,6 @@
position: relative;
top: -2px;
left: 4px;
color: var(--color-text);
color: var(--color-text);
}
</style>
+1 -1
View File
@@ -126,7 +126,7 @@
{
Overlay.open({
component: CreateOverlay,
width: 520,
width: 480,
parent: parent
}).then(() =>
{
+2 -2
View File
@@ -12,12 +12,12 @@
// foreground colors
$color-fg: #fff;
$color-fg-shade-1: #a8abad; // dim
$color-fg-shade-1: #7b848a; // dim
$color-fg-shade-2: #8c9094; // dim-two
// background colors
$color-bg: #191e25;
$color-bg-shade-0: #1f2329;
$color-bg-shade-0: #1d2229;
$color-bg-shade-1: #22272e; // bright
$color-bg-shade-2: #1d2229; // dim
$color-bg-shade-3: #2a2f36; // bright-two
+1 -1
View File
@@ -3,7 +3,7 @@ import { get } from '../helpers/request.ts';
const base = 'modules/';
export default {
getModuleTypes: async tags => await get(base + 'getModuleTypes', { params: { tags } }),
getModuleTypes: async (tags, pageId) => await get(base + 'getModuleTypes', { params: { tags, pageId } }),
getModuleType: async alias => await get(base + 'getModuleType', { params: { alias } }),
@@ -1,270 +1,148 @@
<template>
<div class="ui-modules-start">
<button v-if="!isSelecting" type="button" class="ui-modules-start-button" @click="startSelection">
<i class="ui-modules-start-button-icon fth-plus"></i>
<p class="ui-modules-start-button-text"><strong>Add content</strong> <!--<br>Compose the page by adding modules--></p>
</button>
<div class="ui-modules-select" v-if="isSelecting">
<ui-icon-button class="ui-modules-select-close" @click="isSelecting=false" icon="fth-x" title="@ui.close" />
<ui-inline-tabs class="ui-modules-select-groups">
<ui-tab v-for="group in moduleTypes" :key="group.key" :label="group.name" :count="group.count">
<div class="ui-modules-select-items">
<button v-for="item in group.items" :key="item.alias" type="button" class="ui-modules-select-item" :disabled="item.isDisabled" @click="editModule(item, true)">
<div class="ui-modules-select-item-icon">
<i :class="item.icon"></i>
</div>
<div class="ui-modules-select-item-text">
<strong v-localize="item.name"></strong>
<span class="is-minor" v-localize="item.description"></span>
</div>
<span v-if="item.isDisabled" class="ui-modules-select-item-disabled">Not allowed <i class="fth-slash"></i></span>
</button>
</div>
</ui-tab>
</ui-inline-tabs>
<div class="ui-modules-select">
<h2 class="ui-headline">Add module</h2>
<div v-if="!loading">
<div class="ui-modules-select-items">
<button type="button" v-for="item in types" class="ui-modules-select-item" @click="onSelect(item)">
<i class="ui-modules-select-item-icon" :class="item.icon"></i>
<span class="ui-modules-select-item-text">
{{item.name | localize}}
<span v-if="item.description" v-localize="item.description"></span>
</span>
</button>
</div>
<ui-message type="error" v-if="!types.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 { groupBy as _groupBy, keys as _keys, each as _each } from 'underscore';
import Notification from 'zero/helpers/notification.js';
import ModulesApi from 'zero/api/modules.js';
import Overlay from 'zero/helpers/overlay.js';
export default {
name: 'uiModuleSelect',
props: {
value: Array,
config: Object,
types: {
type: Array,
default: () => []
},
disabled: {
type: Boolean,
default: false
}
config: Object
},
data: () => ({
canAdd: true,
isSelecting: false,
moduleTypes: [],
activeGroup: null
model: {
name: null,
parentId: null,
pageTypeAlias: null
},
loading: false,
item: {},
disabled: false,
types: []
}),
watch: {
types(val)
{
this.rebuildGroups(val);
}
},
created()
{
this.rebuildGroups(this.types);
this.types = this.config.types;
this.model.parentId = this.config.parent ? this.config.parent.id : null;
},
methods: {
rebuildGroups(value)
onSelect(item)
{
let groups = _groupBy(value, val => val.group);
let index = 0;
_each(groups, (items, key) =>
{
this.moduleTypes.push({
name: !key || key === 'null' ? '@modules.default_group' : key,
index: index++,
count: items.length,
items: items,
isDisabled: false
});
});
this.activeGroup = this.moduleTypes[0];
//this.config.close();
this.config.confirm(item);
//this.$router.push({
// name: 'ui-modules-select',
// params: { type: item.alias, parent: this.model.parentId }
//});
},
startSelection()
{
if (this.types.length > 1)
{
this.isSelecting = true;
}
else if (this.types.length === 1)
{
this.$emit('selected', this.types[0], true);
}
else
{
Notification.error('No modules allowed', 'There are no modules configured which are allowed for this data type.', { duration: 5000 });
}
},
selectGroup(group)
{
this.activeGroup = group;
},
editModule(module, isAdd)
{
this.$emit('selected', module, isAdd);
},
reset()
{
this.isSelecting = false;
this.activeGroup = this.moduleTypes[0];
}
}
}
</script>
<style lang="scss">
.ui-modules-start
.ui-modules-select
{
margin: 0;
display: flex;
text-align: left;
.ui-modules-inner-sortable + &
.ui-message
{
margin-top: var(--padding);
margin: 0;
}
}
.ui-modules-start-button
.ui-modules-select-parent
{
color: var(--color-primary);
font-size: var(--font-size);
display: inline-grid;
grid-template-columns: auto 1fr;
gap: 25px;
align-items: center;
}
.ui-modules-start-button-icon
{
width: 52px;
height: 52px;
line-height: 50px !important;
font-size: 20px;
text-align: center;
background: var(--color-button-light);
margin: 30px 0 -10px 0;
border-radius: var(--radius);
}
.ui-modules-start-button-text
{
line-height: 1.3;
/*border: 1px solid var(--color-line-light);*/
background: var(--color-box-nested);
line-height: 1.4;
color: var(--color-text-dim);
margin: 0;
font-size: var(--font-size-s);
padding: 14px 16px;
font-size: var(--font-size);
strong
{
display: inline-block;
margin-bottom: 2px;
color: var(--color-text);
font-size: var(--font-size);
}
}
.ui-modules-select
{
width: 100%;
position: relative;
.ui-inline-tabs-list
{
padding-right: 50px;
}
}
.ui-modules-select-close
{
position: absolute;
right: 0;
top: 0;
background: none !important;
}
.ui-modules-select-items
{
display: grid;
gap: 10px;
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
align-items: stretch;
margin: 0 -16px;
margin-top: var(--padding);
max-height: 600px;
overflow-y: auto;
}
.ui-modules-select-item
{
display: grid;
grid-template-columns: 76px 1fr;
width: 100%;
grid-template-columns: 40px 1fr auto;
gap: 12px;
align-items: center;
height: 100px;
border-radius: var(--radius);
background: var(--color-button-light);
padding: 10px var(--padding) 10px 0;
position: relative;
color: var(--color-text);
padding: 16px;
border-radius: var(--radius);
&[disabled]
&:hover, &:focus
{
opacity: .6;
background: var(--color-tree-selected);
}
& + .ui-modules-select-item
{
margin-top: 5px;
}
}
.ui-modules-select-item-text
{
display: flex;
flex-direction: column;
span
{
color: var(--color-text-dim);
margin-top: 3px;
}
}
.ui-modules-select-item-icon
{
display: inline-flex;
justify-content: center;
color: var(--color-text);
font-size: 26px;
}
.ui-modules-select-item-text
{
strong
{
display: block;
margin-bottom: 3px;
}
.is-minor
{
color: var(--color-text-dim);
font-size: var(--font-size-s);
}
}
.ui-modules-select-item-disabled
{
position: absolute;
right: 5px;
top: 5px;
display: inline-flex;
align-items: center;
font-size: 8px;
text-transform: uppercase;
color: var(--color-text-dim-one);
font-size: 22px;
line-height: 1;
font-weight: 600;
i
{
margin-left: 6px;
font-size: 13px;
}
font-weight: 400;
position: relative;
top: -2px;
left: 4px;
color: var(--color-text);
}
</style>
+1 -1
View File
@@ -1,5 +1,5 @@
import Vue from 'vue';
//import AppConfirm from 'zero/components/overlays/confirm.vue';
import AppConfirm from 'zero/components/overlays/confirm.vue'; // TODO importing vue files in js/ts files causes a Rollup production build error
import Strings from 'zero/helpers/strings.js';
import { find as _find, extend as _extend } from 'underscore';
+2 -1
View File
@@ -1,6 +1,7 @@
using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using zero.Core.Api;
using zero.Core.Entities;
using zero.Core.Utils;
@@ -18,7 +19,7 @@ namespace zero.Web.Controllers
}
public IList<ModuleType> GetModuleTypes([FromQuery] string[] tags = default) => Api.GetModuleTypes(tags);
public async Task<IList<ModuleType>> GetModuleTypes([FromQuery] string[] tags = default, [FromQuery] string pageId = default) => await Api.GetModuleTypes(tags, pageId);
public ModuleType GetModuleType([FromQuery] string alias) => Api.GetModuleType(alias);