output list collections

This commit is contained in:
2020-04-29 13:18:53 +02:00
parent e74d8893c4
commit f694c7e910
21 changed files with 564 additions and 89 deletions
+54
View File
@@ -0,0 +1,54 @@
using Microsoft.Extensions.Options;
using Raven.Client.Documents;
using Raven.Client.Documents.Session;
using System.Collections.Generic;
using System.Threading.Tasks;
using zero.Core.Entities;
namespace zero.Core.Api
{
public class ListsApi : IListsApi
{
protected IDocumentStore Raven { get; private set; }
protected ZeroOptions Options { get; private set; }
public ListsApi(IDocumentStore raven, IOptionsMonitor<ZeroOptions> options)
{
Raven = raven;
Options = options.CurrentValue;
}
/// <inheritdoc />
public ListCollections GetCollections()
{
return Options.Lists;
}
/// <inheritdoc />
public async Task<IList<T>> GetAll<T>(string alias) where T : ListItem
{
using (IAsyncDocumentSession session = Raven.OpenAsyncSession())
{
return await session.Query<ListItem>().ProjectInto<T>().ToListAsync();
}
}
}
public interface IListsApi
{
/// <summary>
/// Get all list collections
/// </summary>
ListCollections GetCollections();
/// <summary>
/// Get all list items by a list collection alias
/// </summary>
Task<IList<T>> GetAll<T>(string alias) where T : ListItem;
}
}
@@ -0,0 +1,17 @@
using System;
namespace zero.Core.Entities
{
public class ListCollection
{
public string Alias { get; set; }
public Type Type { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public string Icon { get; set; }
}
}
@@ -0,0 +1,20 @@
using System;
using System.Collections.Generic;
namespace zero.Core.Entities
{
public class ListCollections : List<ListCollection>
{
public void Add<T>(string alias, string name, string description, string icon) where T : ListItem, new()
{
Add(new ListCollection()
{
Alias = alias,
Name = name,
Description = description,
Icon = icon,
Type = typeof(T)
});
}
}
}
+11
View File
@@ -0,0 +1,11 @@
namespace zero.Core.Entities
{
/// <summary>
/// A list item can consist of unlimited properties and be rendered as you wish
/// The backoffice rendering is done by an IRenderer
/// </summary>
public abstract class ListItem : DatabaseEntity
{
}
}
+2
View File
@@ -10,6 +10,8 @@ namespace zero.Core
public SectionCollection Sections { get; private set; } = new SectionCollection();
public ListCollections Lists { get; private set; } = new ListCollections();
public IList<SettingsGroup> SettingsAreas { get; private set; } = new List<SettingsGroup>();
public ZeroAuthorizationOptions Authorization { get; private set; } = new ZeroAuthorizationOptions();
+9
View File
@@ -0,0 +1,9 @@
using zero.Core.Entities;
namespace zero.TestData
{
public class News : ListItem
{
}
}
+15
View File
@@ -0,0 +1,15 @@
using zero.Core.Entities;
namespace zero.TestData
{
public class TeamMember : ListItem
{
public string Position { get; set; }
public Media Image { get; set; }
public string Email { get; set; }
public string VideoUri { get; set; }
}
}
+11
View File
@@ -0,0 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\zero.Core\zero.Core.csproj" />
</ItemGroup>
</Project>
+1
View File
@@ -5,6 +5,7 @@
{{ permissionCollection.label | localize }}
<span v-if="permissionCollection.description" class="-minor">{{ permissionCollection.description | localize }}</span>
</h3>
<ui-error field="Claims" />
<ui-property v-for="permission in permissionCollection.items" class="role-permission-toggle" :label="permission.label" :description="permission.description">
<ui-toggle v-if="permission.valueType === 'boolean'" :disabled="disabled" v-model="permission.value" @input="onChange" />
<ui-state-button v-if="permission.valueType === 'readWrite'" :disabled="disabled" :items="stateItems" v-model="permission.value" @input="onChange" />
+107
View File
@@ -0,0 +1,107 @@
<template>
<div class="list">
<ui-header-bar title="Lists">
<ui-button type="light" label="Add list" icon="fth-plus" />
<ui-table-filter v-model="tableConfig" />
</ui-header-bar>
<div class="ui-blank-box">
<ui-table v-model="tableConfig" />
</div>
</div>
</template>
<script>
export default {
name: 'app-lists',
data: () => ({
tableConfig: {}
}),
created()
{
this.tableConfig = {
labelPrefix: '@order.fields.',
items: this.getItems,
columns: {
no: 'text',
createdDate: {
label: '@ui.createdDate',
as: 'datetime'
},
username: {
as: 'text'
},
price: {
as: 'price',
sort: false
},
isPublished: {
label: 'Published',
as: 'bool'
},
status: {
as: 'html',
render: item => '<b>' + item.status + '</b>'
}
}
};
},
methods: {
getItems(config)
{
return new Promise(resolve =>
{
resolve({
totalPages: 1,
totalItems: 5,
items: [
{
no: 1,
createdDate: '2020-03-05T10:17:25.229+01:00',
username: 'Tobias Klika',
price: 70.90,
status: 'Open',
isPublished: true
},
{
no: 2,
createdDate: '2020-03-05T10:17:25.229+01:00',
username: 'Fox Tales',
price: 12.95,
status: 'Processing',
isPublished: true
},
{
no: 3,
createdDate: '2020-03-07T17:17:25.229+01:00',
username: 'Christian Klika, das ist mein Name und der könnte noch viel länger sein',
price: 123.00,
status: 'Completed'
},
{
no: 1,
createdDate: '2020-03-05T10:17:25.229+01:00',
username: 'Tobias Klika',
price: 70.90,
status: 'Open',
isPublished: true
},
{
no: 2,
createdDate: '2020-03-05T10:17:25.229+01:00',
username: 'Fox Tales',
price: 12.95,
status: 'Processing'
},
]});
});
}
}
}
</script>
+36
View File
@@ -0,0 +1,36 @@
import { find as _find } from 'underscore';
const alias = zero.alias.sections.lists;
const section = _find(zero.sections, section => section.alias === alias);
let routes = [];
if (section)
{
routes.push({
path: 'edit/:id',
props: true,
name: 'listitem',
component: () => import('zero/pages/' + alias + '/item')
});
routes.push({
path: ':alias',
props: true,
name: 'list',
component: () => import('zero/pages/' + alias + '/list')
});
//routes.push({
// path: 'history',
// name: 'history',
// component: () => import('zero/pages/' + alias + '/history'),
// meta: {
// name: '@page.history.name'
// }
//});
}
export default {
section: alias,
routes: routes
};
+18
View File
@@ -0,0 +1,18 @@
import Axios from 'axios';
export default {
// get all list collections
getCollections()
{
return Axios.get('lists/getCollections').then(res => Promise.resolve(res.data));
},
// get all list items in a collection
getAll(alias, query)
{
query.alias = alias;
return Axios.get('lists/getAll', { params: query }).then(res => Promise.resolve(res.data));
}
};
+208 -86
View File
@@ -1,107 +1,229 @@
<template>
<div class="lists">
<ui-header-bar title="Lists">
<ui-button type="light" label="Add list" icon="fth-plus" />
<ui-table-filter v-model="tableConfig" />
</ui-header-bar>
<div class="ui-blank-box">
<ui-table v-model="tableConfig" />
<div class="lists-tree" v-resizable="resizable">
<ui-header-bar title="Lists" :back-button="false" />
<div class="lists-tree-items">
<div v-for="item in lists" class="lists-tree-item" :class="getClasses(item)">
<router-link :to="{ name: 'list', params: { alias: item.alias } }" class="lists-tree-item-link">
<i class="lists-tree-item-icon" :class="item.icon"></i>
<span class="lists-tree-item-text">
{{item.name | localize}}
<span v-if="item.description" v-localize="item.description"></span>
</span>
</router-link>
<ui-dot-button class="lists-tree-item-actions" />
</div>
</div>
<!--<div class="lists-tree-actions">
<ui-button label="Add list" icon="fth-plus" />
</div>-->
<div class="lists-tree-resizable ui-resizable"></div>
</div>
<router-view v-if="!isOverview"></router-view>
<div v-if="isOverview" class="lists-overview">
</div>
</div>
</template>
<script>
export default {
name: 'app-lists',
import ListsApi from 'zero/resources/lists.js';
export default {
data: () => ({
tableConfig: {}
tableConfig: {},
lists: [],
resizable: {
axis: 'x',
min: 260,
max: 520,
save: 'lists-tree',
handle: '.ui-resizable'
}
}),
computed: {
isOverview()
{
return !this.$route.params.id && this.$route.name !== 'list' && this.$route.name !== 'listitem';
}
},
created()
{
this.tableConfig = {
labelPrefix: '@order.fields.',
items: this.getItems,
columns: {
no: 'text',
createdDate: {
label: '@ui.createdDate',
as: 'datetime'
},
username: {
as: 'text'
},
price: {
as: 'price',
sort: false
},
isPublished: {
label: 'Published',
as: 'bool'
},
status: {
as: 'html',
render: item => '<b>' + item.status + '</b>'
}
}
};
ListsApi.getCollections().then(response =>
{
this.lists = response;
});
//this.tableConfig = {
// labelPrefix: '@lists.fields.',
// allowOrder: false,
// search: null,
// columns: {
// name: 'text'
// },
// items: ListsApi.getCollections
//};
},
methods: {
getItems(config)
// get all classes for a tree item
getClasses(item)
{
return new Promise(resolve =>
{
resolve({
totalPages: 1,
totalItems: 5,
items: [
{
no: 1,
createdDate: '2020-03-05T10:17:25.229+01:00',
username: 'Tobias Klika',
price: 70.90,
status: 'Open',
isPublished: true
},
{
no: 2,
createdDate: '2020-03-05T10:17:25.229+01:00',
username: 'Fox Tales',
price: 12.95,
status: 'Processing',
isPublished: true
},
{
no: 3,
createdDate: '2020-03-07T17:17:25.229+01:00',
username: 'Christian Klika, das ist mein Name und der könnte noch viel länger sein',
price: 123.00,
status: 'Completed'
},
{
no: 1,
createdDate: '2020-03-05T10:17:25.229+01:00',
username: 'Tobias Klika',
price: 70.90,
status: 'Open',
isPublished: true
},
{
no: 2,
createdDate: '2020-03-05T10:17:25.229+01:00',
username: 'Fox Tales',
price: 12.95,
status: 'Processing'
},
]});
});
return {
'has-children': item.hasChildren,
'is-open': item.isOpen
};
}
}
}
</script>
</script>
<style lang="scss">
.lists
{
display: grid;
grid-template-columns: auto 1fr;
grid-gap: 2px;
justify-content: stretch;
height: 100vh;
}
.lists-tree
{
width: 340px;
background: var(--color-bg-light);
padding: 0;
position: relative;
overflow-y: auto;
height: 100vh;
.ui-header-bar + .ui-tree
{
margin-top: 2px;
}
}
.lists-tree-actions
{
padding: var(--padding);
}
.lists-tree-resizable
{
position: absolute;
top: 0;
bottom: 0;
background: var(--color-fg);
opacity: 0;
right: 0;
width: 6px;
cursor: ew-resize;
transition: opacity 0.15s ease 0s;
&:hover
{
transition-delay: 0.2s;
opacity: 0.04;
}
}
.lists-overview
{
padding: 95px 0 0 60px;
}
.lists-tree-item
{
display: grid;
grid-template-columns: 1fr auto;
align-items: center;
font-size: var(--font-size);
padding: 0 var(--padding);
height: 50px;
color: var(--color-fg);
position: relative;
transition: color 0.2s ease;
&:hover > .lists-tree-item-actions
{
transition-delay: 0.2s;
opacity: 1;
}
& + .lists-tree-item
{
margin-top: 15px;
}
}
.lists-tree-item-link
{
display: grid;
grid-template-columns: 30px 1fr auto;
grid-gap: 6px;
height: 100%;
align-items: center;
position: relative;
color: var(--color-fg);
&.is-active
{
font-weight: bold;
}
}
.lists-tree-item-text
{
display: flex;
flex-direction: column;
span
{
color: var(--color-fg-light);
}
}
.lists-tree-item-toggle
{
position: absolute;
color: var(--color-fg-mid);
height: 100%;
top: 0;
left: 0;
width: 30px;
text-align: right;
padding-right: 5px;
outline: none !important;
transition: color 0.2s ease;
&:hover
{
color: var(--color-fg);
}
}
.lists-tree-item-icon
{
font-size: 18px;
line-height: 1;
font-weight: 400;
position: relative;
top: -2px;
color: var(--color-fg-reverse-mid);
transition: color 0.2s ease;
}
.lists-tree-item-actions
{
transition: opacity 0.2s ease 0;
opacity: 0;
color: var(--color-fg-mid);
}
</style>
+40
View File
@@ -0,0 +1,40 @@
using Microsoft.AspNetCore.Mvc;
using System.Threading.Tasks;
using zero.Core;
using zero.Core.Api;
using zero.Core.Entities;
using zero.Core.Identity;
using zero.Web.Mapper;
using zero.Web.Models;
namespace zero.Web.Controllers
{
[ZeroAuthorize(Permissions.Sections.Lists, PermissionsValue.True)]
public class ListsController : BackofficeController
{
private IListsApi Api { get; set; }
public ListsController(IZeroConfiguration config, IListsApi api, IMapper mapper, IToken token) : base(config, mapper, token)
{
Api = api;
}
/// <summary>
/// Get all list collections
/// </summary>
public IActionResult GetCollections([FromQuery] string id)
{
return Json(Api.GetCollections());
}
/// <summary>
/// Get list items in a collection
/// </summary>
//public async Task<IActionResult> GetAll([FromQuery] string alias, [FromQuery] ListQuery<Country> query = null)
//{
// return await As<Country, CountryListModel>(await Api.GetAll(alias));
//}
}
}
+5
View File
@@ -15,6 +15,7 @@ using System;
using zero.Core;
using zero.Core.Api;
using zero.Core.Extensions;
using zero.TestData;
namespace zero.Web
{
@@ -75,6 +76,9 @@ namespace zero.Web
//services.AddCore(appConfig, env);
services.AddZero(opts =>
{
opts.Lists.Add<TeamMember>("team", "Team", "Our team members", "fth-users");
opts.Lists.Add<News>("news", "News", "Articles about the company", "fth-edit");
//var commercePermissions = new Core.Identity.PermissionCollection()
//{
// Label = "Commerce"
@@ -133,6 +137,7 @@ namespace zero.Web
services.AddTransient<IUserApi, UserApi>();
services.AddTransient<IUserRolesApi, UserRolesApi>();
services.AddTransient<IToken, Token>();
services.AddTransient<IListsApi, ListsApi>();
}
/// <summary>
+1 -1
View File
@@ -56,7 +56,7 @@ namespace zero.Web
//opts.Cookie.Path = // TODO use backoffice path
opts.Cookie.Name = Constants.Auth.CookieName;
opts.SlidingExpiration = true;
opts.ExpireTimeSpan = TimeSpan.FromMinutes(10);
opts.ExpireTimeSpan = TimeSpan.FromMinutes(60);
// override redirect to login page (handled by vue frontend) and return a 401 instead
opts.Events.OnRedirectToLogin = (context) =>
+1
View File
@@ -44,6 +44,7 @@
<ItemGroup>
<ProjectReference Include="..\zero.Core\zero.Core.csproj" />
<ProjectReference Include="..\zero.TestData\zero.TestData.csproj" />
</ItemGroup>
</Project>
+8 -2
View File
@@ -3,9 +3,11 @@ Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.29709.97
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "zero.Web", "zero.Web\zero.Web.csproj", "{8C14E4B1-8C00-4B4B-869B-4D2084ABE1F3}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "zero.Web", "zero.Web\zero.Web.csproj", "{8C14E4B1-8C00-4B4B-869B-4D2084ABE1F3}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "zero.Core", "zero.Core\zero.Core.csproj", "{7BFF3DDE-F910-4CE1-9B94-846059CE80DA}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "zero.Core", "zero.Core\zero.Core.csproj", "{7BFF3DDE-F910-4CE1-9B94-846059CE80DA}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "zero.TestData", "zero.TestData\zero.TestData.csproj", "{2DF370E9-63E9-4213-8490-97BDA5CB5C01}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -21,6 +23,10 @@ Global
{7BFF3DDE-F910-4CE1-9B94-846059CE80DA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7BFF3DDE-F910-4CE1-9B94-846059CE80DA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7BFF3DDE-F910-4CE1-9B94-846059CE80DA}.Release|Any CPU.Build.0 = Release|Any CPU
{2DF370E9-63E9-4213-8490-97BDA5CB5C01}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2DF370E9-63E9-4213-8490-97BDA5CB5C01}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2DF370E9-63E9-4213-8490-97BDA5CB5C01}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2DF370E9-63E9-4213-8490-97BDA5CB5C01}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE