start link providers + linkpicker

This commit is contained in:
2021-01-29 17:01:32 +01:00
parent 44b6e6109f
commit 77f9ea63c4
13 changed files with 486 additions and 15 deletions
@@ -3,7 +3,7 @@ using System.Collections.Generic;
namespace zero.Core.Entities
{
public class Link
public class Link : ILink
{
public string ProviderAlias { get; set; }
@@ -15,7 +15,7 @@ namespace zero.Core.Entities
public string Title { get; set; }
public Dictionary<string, string> Values { get; set; } = new Dictionary<string, string>();
public Dictionary<string, object> Values { get; set; } = new();
}
public enum LinkTarget
@@ -24,4 +24,20 @@ namespace zero.Core.Entities
Self = 1,
Blank = 2
}
public interface ILink
{
string ProviderAlias { get; set; }
LinkTarget Target { get; set; }
string UrlSuffix { get; set; }
string Label { get; set; }
string Title { get; set; }
Dictionary<string, object> Values { get; set; }
}
}
+72
View File
@@ -0,0 +1,72 @@
//using System;
//using System.Collections.Generic;
//using System.Linq;
//using System.Text;
//using System.Threading.Tasks;
//namespace zero.Core.Entities
//{
// /// <inheritdoc />
// public class LinkArea : ILinkArea
// {
// /// <inheritdoc />
// public string Alias { get; set; }
// /// <inheritdoc />
// public string Name { get; set; }
// /// <inheritdoc />
// public string Icon { get; set; }
// /// <summary>
// /// HEX color (#aabbcc or #abc). Defaults to a neutral color
// /// </summary>
// public string Color { get; set; }
// /// <inheritdoc />
// public IList<IChildSection> Children { get; } = new List<IChildSection>();
// public LinkArea() { }
// public LinkArea(string alias, string name, string icon = null, string color = null)
// {
// Alias = alias;
// Name = name;
// Icon = icon;
// Color = color;
// }
// }
// /// <summary>
// /// A section is a main part of the backoffice application
// /// </summary>
// public interface ILinkArea
// {
// /// <summary>
// /// The section alias which acts as the url slug for navigation
// /// </summary>
// string Alias { get; }
// /// <summary>
// /// The name of the section (either a string or a translation key with @ prefix)
// /// </summary>
// string Name { get; }
// /// <summary>
// /// Icon of the section
// /// </summary>
// string Icon { get; }
// /// <summary>
// /// HEX color (#aabbcc or #abc)
// /// </summary>
// string Color { get; }
// /// <summary>
// /// Children are displayed as a sub-navigation in the main nav area
// /// </summary>
// IList<IChildSection> Children { get; }
// }
//}
+1 -1
View File
@@ -9,7 +9,7 @@ namespace zero.Core.Options
{
SupportedLanguages = new string[2] { "en-US", "de-DE" };
DefaultLanguage = SupportedLanguages[0];
TokenExpiration = 60;
TokenExpiration = 60 * 3;
BackofficePath = "/zero";
ExcludedPaths = new() { };
Raven = new()
+17
View File
@@ -0,0 +1,17 @@
using System.Threading.Tasks;
using zero.Core.Entities;
namespace zero.Core.Routing
{
public interface ILinkProvider
{
string Name { get; }
string Alias { get; }
/// <summary>
///
/// </summary>
Task<string> ResolveLink(ILink link);
}
}
@@ -0,0 +1,49 @@
using Raven.Client.Documents.Session;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using zero.Core.Database;
using zero.Core.Entities;
namespace zero.Core.Routing
{
public class PageLinkProvider : ILinkProvider
{
/// <inheritdoc />
public string Name { get; } = "@links.providers.page";
/// <inheritdoc />
public string Alias { get; } = "zero.pages";
protected IZeroStore Store { get; set; }
protected IRoutes Routes { get; set; }
public PageLinkProvider(IZeroStore store, IRoutes routes)
{
Store = store;
Routes = routes;
}
/// <inheritdoc />
public async Task<string> ResolveLink(ILink link)
{
if (!link.Values.TryGetValue("pageId", out object pageIdObj))
{
return null;
}
string pageId = pageIdObj.ToString();
using IAsyncDocumentSession session = Store.OpenAsyncSession();
IPage page = await session.LoadAsync<IPage>(pageId);
IRoute route = await Routes.GetRoute(page);
return route.Url;
}
}
}
@@ -0,0 +1,160 @@
<template>
<div class="ui-linkpicker" :class="{'is-disabled': disabled }">
<input ref="input" type="hidden" :value="value" />
<!--<div class="ui-pagepicker-previews" v-if="previews.length > 0">
<div v-for="preview in previews" class="ui-pagepicker-preview">
<ui-select-button :icon="preview.icon" :label="preview.name" :description="preview.text" :disabled="disabled" @click="pick(preview.id)" :tokens="{ id: preview.id }" />
<ui-icon-button v-if="!disabled" @click="remove(preview.id)" icon="fth-x" title="@ui.close" />
</div>
</div>-->
<ui-select-button v-if="canAdd" icon="fth-plus" :label="limit > 1 ? '@ui.add' : '@ui.select'" @click="pick()" :disabled="disabled" />
</div>
</template>
<script>
import LinkpickerOverlay from './overlay.vue';
import Overlay from 'zero/helpers/overlay.js';
import { extend as _extend, isArray as _isArray, isEmpty as _isEmpty, clone as _clone } from 'underscore';
export default {
name: 'uiPagepicker',
props: {
value: {
type: [String, Array],
default: null
},
limit: {
type: Number,
default: 1
},
disabled: {
type: Boolean,
default: false
},
options: {
type: Object,
default: () => {}
}
},
data: () => ({
previews: []
}),
watch: {
value()
{
this.updatePreviews();
}
},
computed: {
multiple()
{
return this.limit > 1;
},
canAdd()
{
let count = Array.isArray(this.value) ? this.value.length : (!this.value ? 0 : 1);
return !this.disabled && count < this.limit;
}
},
mounted()
{
this.updatePreviews();
},
methods: {
onChange(value)
{
this.$emit('change', value);
this.$emit('input', value);
// TODO this does not trigger the forms dirty flag
},
updatePreviews()
{
this.previews = [];
//if (!this.value || _isEmpty(this.value))
//{
// this.previews = [];
// return;
//}
//let ids = _isArray(this.value) ? this.value : [this.value];
//PagesApi.getPreviews(ids).then(res =>
//{
// this.previews = res;
//});
},
remove(id)
{
if (Array.isArray(this.value))
{
let index = this.value.indexOf(id);
this.value.splice(index, 1);
this.onChange(this.value);
}
else
{
this.onChange(this.limit > 1 ? [] : null);
}
},
pick(id)
{
if (this.disabled)
{
return;
}
let options = _extend({
title: 'Select a link',
closeLabel: '@ui.close',
component: LinkpickerOverlay,
display: 'editor',
model: this.multiple ? id : this.value
}, typeof this.options === 'object' ? this.options : {});
return Overlay.open(options).then(value =>
{
console.info('confirmed: ' + value);
});
}
}
}
</script>
<style lang="scss">
.ui-linkpicker-preview
{
display: flex;
justify-content: space-between;
align-items: center;
.ui-icon-button
{
height: 24px;
width: 24px;
i
{
font-size: 13px;
}
}
}
.ui-linkpicker-previews + .ui-select-button,
.ui-linkpicker-preview + .ui-linkpicker-preview
{
margin-top: 10px;
}
</style>
@@ -0,0 +1,116 @@
<template>
<ui-overlay-editor class="ui-pagepicker-overlay">
<template v-slot:header>
<ui-header-bar :title="config.title" :back-button="false" :close-button="true" />
</template>
<template v-slot:footer>
<ui-button type="light onbg" :label="config.closeLabel" :parent="config.rootId" @click="config.hide"></ui-button>
</template>
<div v-if="opened" class="ui-box ui-pagepicker-overlay-items">
<ui-tree ref="tree" :get="getItems" :parent="config.rootId" @select="onSelect" />
</div>
</ui-overlay-editor>
</template>
<script>
import PageTreeApi from 'zero/api/page-tree.js'
export default {
props: {
model: String,
config: Object
},
data: () => ({
opened: false
}),
computed: {
disabledIds()
{
return this.config.disabledIds || [];
}
},
mounted()
{
setTimeout(() => this.opened = true, 300);
},
methods: {
onSelect(item)
{
this.config.confirm(item);
},
// get tree items
getItems(parent)
{
return PageTreeApi.getChildren(parent, this.model).then(res =>
{
res = res.filter(x => x.id !== 'recyclebin');
res.forEach(item =>
{
if (item.id === this.model)
{
item.isSelected = true;
}
if (this.disabledIds.indexOf(item.id) > -1)
{
item.disabled = true;
}
item.hasActions = false;
});
return res;
});
}
}
}
</script>
<style lang="scss">
.ui-pagepicker-overlay content
{
padding-top: 0;
}
.ui-box.ui-pagepicker-overlay-items
{
margin: 0;
padding: 20px 0;
.ui-tree-item.is-selected, .ui-tree-item:hover:not(.is-disabled)
{
background: var(--color-bg-xxlight);
}
& +.ui-box
{
margin-top: var(--padding);
}
.ui-tree-item.is-selected
{
&:after
{
font-family: "Feather";
content: "\e83e";
font-size: 16px;
color: var(--color-primary);
}
.ui-tree-item-text
{
font-weight: bold;
}
}
}
</style>
+5
View File
@@ -15,4 +15,9 @@ plugin.addLists(lists);
// add routes
plugin.addRoutes(routes);
plugin.install = (vue, zero) =>
{
console.info(vue, zero);
};
export default plugin;
+2 -2
View File
@@ -1,4 +1,4 @@
<template>
<!--<template>
<input :value="url" @input="onChange($event.target.value)" type="text" class="ui-input" :disabled="disabled" />
</template>
@@ -40,4 +40,4 @@
}
}
}
</script>
</script>-->
@@ -0,0 +1,24 @@
<template>
<ui-linkpicker :config="config" :value="value" @input="$emit('input', $event)" :disabled="disabled" :limit="limit" />
</template>
<script>
export default {
props: {
value: {
type: [String, Array],
default: null
},
disabled: {
type: Boolean,
default: false
},
limit: {
type: Number,
default: 1
},
config: Object
},
}
</script>
+2
View File
@@ -41,6 +41,7 @@ import uiPagepicker from './pickers/pagePicker/pagepicker.vue';
import uiUserpicker from './pickers/userPicker/userpicker.vue';
import uiMailtemplatepicker from './pickers/mailPicker/mailpicker.vue';
import uiSpacepicker from './pickers/spacePicker/spacepicker.vue';
import uiLinkpicker from './pickers/linkPicker/linkpicker.vue';
import uiPick from './pickers/pick.vue';
import uiTable from './tables/table.vue';
@@ -110,6 +111,7 @@ export default {
uiUserpicker,
uiMailtemplatepicker,
uiSpacepicker,
uiLinkpicker,
uiPick,
uiTable,
+13 -10
View File
@@ -20,6 +20,7 @@ import DateRangePicker from '../editor/fields/daterangepicker.vue';
import IconPicker from '../editor/fields/iconPicker.vue';
import LanguagePicker from '../editor/fields/language.vue';
import PagePicker from '../editor/fields/pagepicker.vue';
import LinkPicker from '../editor/fields/linkpicker.vue';
import InputList from '../editor/fields/inputlist.vue';
import Media from '../editor/fields/media.vue';
import Modules from '../editor/fields/modules.vue';
@@ -215,16 +216,6 @@ class EditorField
}
/**
* Render a link picker
* @returns {EditorField}
*/
linkPicker()
{
return this._setComponent(Link, {});
}
/**
* @typedef {object} EditorSelectItem
* @param {object} key - Key/Id of the item
@@ -416,6 +407,18 @@ class EditorField
}
/**
* Render a link picker
* @param {object} [options] - Custom options
* @param {number} [options.limit=1] - Limit of selection
* @returns {EditorField}
*/
linkPicker(options)
{
return this._setComponent(LinkPicker, { ...options });
}
/**
* Create a list of strings
* @param {number} [limit=10] - Limit the inputs
@@ -130,6 +130,13 @@
"raw": "Edit HTML"
},
"links": {
"providers": {
"page": "Pages",
"media": "Media"
}
},
"errors": {
"idnotfound": "Could not find an entity for the given ID",
"onsave": {