page picker

This commit is contained in:
2022-01-12 23:52:37 +01:00
parent 71bbd39153
commit c419655ccc
13 changed files with 609 additions and 28 deletions
@@ -82,6 +82,15 @@ public class PagesController : ZeroApiTreeEntityStoreController<Page, IPagesStor
return result;
}
[HttpGet("")]
[ZeroAuthorize(PagePermissions.Read)]
public virtual Task<ActionResult<Paged>> Get([FromQuery] ListQuery<Page> query)
{
query.SearchFor(x => x.Name);
query.OrderQuery = q => q.OrderByDescending(x => x.Sort).ThenByDescending(x => x.CreatedDate);
return GetModelsByIndex<zero_Api_Pages_Listing>(query);
}
[HttpPost("")]
[ZeroAuthorize(PagePermissions.Create)]
public virtual Task<ActionResult<Result>> Create(Page saveModel) => CreateModel(saveModel);
+1 -1
View File
@@ -131,7 +131,7 @@
const schema = typeof this.config === 'string' ? await this.zero.getSchema(this.config) : this.config;
this.editorConfig = compileEditor(this.zero, schema);
this.onConfigure(this);
this.onConfigure(this.editorConfig, this);
this.loaded = true;
},
@@ -1,7 +1,7 @@
<template>
<div class="ui-module-item" v-if="!loading" :data-module="alias" :class="{'can-edit': canEdit, 'no-preview': !preview }">
<div class="ui-module-item-content" v-if="module" @click="emit('edit')">
<span v-if="!preview || !preview.hideLabel" class="ui-module-item-header" v-localize="module.name"><ui-icon :symbol="module.icon" /> <span v-localize="module.name"></span></span>
<span v-if="!preview || !preview.hideLabel" class="ui-module-item-header"><ui-icon :symbol="module.icon" /> <span v-localize="module.name"></span></span>
<module-preview-inner v-if="tryRender && typeof preview.template === 'string'" :template="preview.template" :value="value" :options="preview" />
<div v-if="tryRender && typeof preview.template !== 'string'" class="ui-module-preview-inner">
<component :is="preview.template" :model="value" :options="preview" />
@@ -10,12 +10,16 @@ export default {
getById: (id: string, changeVector?: string, config?: ApiRequestConfig) => get('pages/' + id, { ...config, params: { changeVector } }),
getByQuery: (query: ApiRequestQuery, config?: ApiRequestConfig) => get('pages', { ...config, params: { ...query } }),
getChildren: (id: string, query: ApiRequestQuery, config?: ApiRequestConfig) => get(`pages/${id}/children`, { ...config, params: { ...query } }),
getAllowedFlavors: (id: string, config?: ApiRequestConfig) => get(`pages/${id}/flavors`, { ...config }),
getDependencies: (id: string, config?: ApiRequestConfig) => get(`backoffice/pages/${id}/dependencies`, { ...config }),
getPreviews: (ids: string[], config?: ApiRequestConfig) => get(`backoffice/pages/previews`, { ...config, params: { ids } }),
create: (model: any, config?: ApiRequestConfig) => post('pages', model, config),
+35 -19
View File
@@ -26,12 +26,8 @@
<script lang="ts">
import { defineComponent } from 'vue';
import api from './api';
//import PageInfoTab from './page-info.vue';
//import Overlay from 'zero/helpers/overlay.js'
//import MoveOverlay from './overlays/move.vue'
//import CopyOverlay from './overlays/copy.vue'
//import Strings from 'zero/helpers/strings.js';
import actions from './actions';
import PageInfoTab from './partials/page-info.vue';
export default defineComponent({
@@ -144,22 +140,42 @@
},
async move(item)
{
await actions.move(item);
},
async copy(item)
{
await actions.copy(item);
},
async remove(item)
{
await actions.remove(item);
},
onEditorConfigure(editor)
{
//if (this.isFolder)
//{
// return;
//}
if (this.isFolder)
{
return;
}
//editor.tabs.push({
// alias: 'zero.info',
// name: '@page.info_tab',
// class: 'is-info is-blank',
// count: value => null,
// disabled: value => false,
// component: PageInfoTab,
// fields: []
//});
editor.tabs.push({
alias: 'zero.info',
name: '@page.info_tab',
class: 'is-info is-blank',
sort: 99999,
fieldsets: [],
hidden: _ => false,
count: _ => null,
disabled: _ => false,
component: PageInfoTab
});
}
}
})
@@ -0,0 +1,122 @@
<template>
<div class="page-editor-info">
<div class="ui-box" v-if="!isCreate && urls.length">
<ui-property label="@page.infotab.links" :is-text="true" :vertical="false">
<a v-for="url in urls" class="ui-link" :href="url" target="_blank"><ui-icon symbol="fth-external-link"></ui-icon> {{url}}</a>
</ui-property>
<!--<ui-property label="@page.infotab.revisions" :is-text="true" :vertical="false">-->
<!--<ui-revisions :get="getRevisions" />-->
<!--</ui-property>-->
</div>
<div class="ui-box">
<!--<ui-property label="@page.schedule.label" :is-text="true" :vertical="false">
<ui-daterangepicker :value="{ from: value.publishDate, to: value.unpublishDate }" @input="onRangeChange" :class="{ 'is-primary': value.publishDate || value.unpublishDate }" :disabled="disabled" />
</ui-property>-->
<ui-property label="@page.type" :is-text="true" v-if="pageType" :vertical="false">
<ui-icon :symbol="pageType.icon" style="margin-right:6px;"></ui-icon>{{pageType.name}}
</ui-property>
<ui-property v-if="!isCreate" label="@ui.id" :is-text="true" :vertical="false">
{{value.id}}
</ui-property>
<ui-property v-if="!isCreate" label="@ui.createdDate" :is-text="true" :vertical="false">
<ui-date v-model="value.createdDate" />
</ui-property>
<ui-property v-if="!isCreate" label="@ui.modifiedDate" :is-text="true" :vertical="false">
<ui-date v-model="value.lastModifiedDate" />
</ui-property>
<ui-property v-if="!isCreate" label="@ui.entityfields.alias" :is-text="true" :vertical="false">
{{value.alias}}
</ui-property>
</div>
</div>
</template>
<script>
import api from '../api';
export default {
props: {
value: {
type: [ Object, Array ]
},
disabled: {
type: Boolean,
default: false
}
},
data: () => ({
pageType: null,
urls: []
}),
computed: {
isCreate()
{
return !this.value.id;
}
},
mounted()
{
//api.getPageType(this.value.pageTypeAlias).then(pageType =>
//{
// this.pageType = pageType;
//});
//if (this.value.id)
//{
// api.getUrls(this.value.id).then(urls =>
// {
// this.urls = urls;
// });
//}
},
methods: {
getRevisions(page)
{
//return api.getRevisions(this.value.id, page);
},
onRangeChange(value)
{
this.value.publishDate = value.from;
this.value.unpublishDate = value.to;
},
}
}
</script>
<style lang="scss">
.page-editor-info
{
padding: 0 !important;
}
.page-editor-info .ui-view-box-aside
{
padding: 0;
}
.page-editor-info .ui-box
{
margin: 0;
}
.page-editor-info .ui-box + .ui-box
{
margin-top: var(--padding-s);
}
.page-editor-info .ui-property + .ui-property
{
/*border-top: none;
margin-top: 0;
padding-top: var(--padding-s);*/
}
.page-editor-info .ui-box:last-child
{
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
</style>
@@ -91,6 +91,7 @@
if (item.id === 'recyclebin')
{
item.hasActions = false;
item.disabled = true;
//item.url = {
// name: 'recyclebin'
//};
@@ -0,0 +1,22 @@
<template>
<ui-pagepicker :value="value" @input="$emit('input', $event)" :model="config.model" :limit="limit" :disabled="config.disabled" :root-id="rootId" :disabled-ids="disabledIds" />
</template>
<script>
export default {
props: {
value: [String, Array],
config: Object,
limit: {
type: Number,
default: 1
},
rootId: [String, Function],
disabledIds: {
type: [Array, Function],
default: []
}
}
}
</script>
@@ -0,0 +1,132 @@
<template>
<ui-trinity 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.close"></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-trinity>
</template>
<script>
import api from '../api';
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
async getItems(parent)
{
const result = await api.tree.getChildren((parent || 'root'), this.model);
result.data.forEach(item =>
{
if (item.id === this.model)
{
item.isSelected = true;
}
if (this.disabledIds.indexOf(item.id) > -1 && item.id !== this.model)
{
item.disabled = true;
}
if (item.id === 'recyclebin')
{
item.disabled = true;
}
item.hasActions = false;
});
if (!parent)
{
result.data.splice(0, 0, {
id: null,
parentId: null,
image: null,
name: '@page.root',
children: 0,
root: true,
sort: 0,
icon: 'fth-arrow-down-circle',
root: true
});
}
return result.data;
}
}
}
</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>
@@ -0,0 +1,212 @@
<template>
<div class="ui-pagepicker" :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 lang="ts">
import api from '../api';
import * as overlays from '../../../services/overlay';
export default {
name: 'uiPagepicker',
props: {
value: {
type: [String, Array],
default: null
},
model: Object,
limit: {
type: Number,
default: 1
},
disabledIds: {
type: [Array, Function],
default: []
},
disabled: {
type: Boolean,
default: false
},
rootId: {
type: [String, Function],
default: null
},
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);
this.$emit('update:value', value);
// TODO this does not trigger the forms dirty flag
},
async updatePreviews()
{
if (!this.value || (Array.isArray(this.value) && !this.value.length))
{
this.previews = [];
return;
}
let ids = Array.isArray(this.value) ? this.value : [this.value];
const result = await api.getPreviews(ids);
this.previews = result.data;
},
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);
}
},
async pick(id)
{
if (this.disabled)
{
return;
}
let disabledIds = [];
if (!!this.value && !Array.isArray(this.value))
{
disabledIds = [this.value];
}
else if (Array.isArray(this.value))
{
disabledIds = this.value;
}
disabledIds.push(this.model.id);
if (typeof this.disabledIds === 'function')
{
let moreDisabledIds = this.disabledIds(this.model) || [];
disabledIds.push(...moreDisabledIds);
}
else if (this.disabledIds.length > 0)
{
disabledIds.push(...this.disabledIds);
}
const result = await overlays.open({
title: '@page.picker.headline',
closeLabel: '@ui.close',
component: () => import('./overlay.vue'),
display: 'editor',
model: this.multiple ? id : this.value,
rootId: typeof this.rootId === 'function' ? this.rootId(this.model) : this.rootId,
disabledIds: disabledIds
});
if (result.eventType == 'confirm')
{
if (this.multiple)
{
if (!this.value || !Array.isArray(this.value))
{
this.onChange([result.value.id]);
}
else if (this.value.indexOf(result.value.id) < 0)
{
if (id)
{
this.remove(id);
}
this.value.push(result.value.id);
this.onChange(this.value);
}
}
else
{
this.onChange(result.value ? result.value.id : null);
}
}
}
}
}
</script>
<style lang="scss">
.ui-pagepicker-preview
{
display: flex;
justify-content: space-between;
align-items: center;
.ui-icon-button
{
height: 24px;
width: 24px;
//background: none;
i
{
font-size: 13px;
}
}
}
.ui-pagepicker-previews + .ui-select-button,
.ui-pagepicker-preview + .ui-pagepicker-preview
{
margin-top: 10px;
}
</style>
+22 -5
View File
@@ -6,9 +6,6 @@ export default {
install(app: ZeroPluginOptions)
{
//app.vue.component('ui-countrypicker', defineAsyncComponent(() => import('./ui-countrypicker.vue')));
//app.schema('spaces:default', () => import('./schemas/list-default'));
app.schema('pages:zero.folder', () => import('./schemas/folder-editor'));
app.linkArea('zero.pages', '@zero.config.linkareas.pages', () => import('./partials/linkpicker.vue'));
@@ -33,6 +30,26 @@ export default {
]
});
app.link
app.vue.component('ui-pagepicker', defineAsyncComponent(() => import('./picker/ui-pagepicker.vue')));
app.fieldType('pagePicker', defineAsyncComponent(() => import('./picker/field-pagepicker.vue')));
}
} as ZeroPlugin;
} as ZeroPlugin;
declare module 'zero/schemas'
{
export interface ZeroEditorField
{
/**
* Renders a page picker
* @param {PagePickerFieldOptions} [options] - Custom options
*/
pagePicker(options?: PagePickerFieldOptions): ZeroEditorField;
}
export interface PagePickerFieldOptions extends PickerFieldOptions
{
rootId?: string | ((model: any) => string);
disabledIds?: string[] | ((model: any) => string[]);
}
}
@@ -8,12 +8,16 @@ public class PagesController : ZeroBackofficeController
{
readonly IPagesStore Store;
readonly IPageTreeService PageTreeService;
readonly IPageTypeService PageTypeService;
readonly IRoutes Routes;
public PagesController(IPagesStore store, IPageTreeService pageTreeService)
public PagesController(IPagesStore store, IPageTreeService pageTreeService, IPageTypeService pageTypeService, IRoutes routes)
{
Store = store;
PageTreeService = pageTreeService;
PageTypeService = pageTypeService;
Routes = routes;
}
@@ -37,4 +41,45 @@ public class PagesController : ZeroBackofficeController
pages = descendantCount + 1
};
}
[HttpGet("previews")]
public async Task<ActionResult<List<LinkPreview>>> GetPreviews([FromQuery] List<string> ids)
{
IEnumerable<FlavorConfig> pageTypes = PageTypeService.GetAll();
Dictionary<string, Page> pages = await Store.Load(ids.ToArray());
Dictionary<Page, Route> routes = await Routes.GetRoutes(pages.Where(x => x.Value != null).Select(x => x.Value).ToArray());
List<LinkPreview> previews = new();
foreach ((string id, Page page) in pages)
{
if (page != null)
{
routes.TryGetValue(page, out Route route);
FlavorConfig pageType = PageTypeService.GetByAlias(page.Flavor);
previews.Add(new()
{
Name = page.Name,
Text = route?.Url.Or("@page.picker.urlnotfound"),
Id = page.Id,
Icon = pageType?.Icon.Or("fth-folder")
});
}
else
{
previews.Add(new()
{
HasError = true,
Icon = "fth-alert-circle color-red",
Id = "tmp_" + IdGenerator.Create(),
Name = "@errors.preview.notfound",
Text = "@errors.preview.notfound_text"
});
}
}
return previews;
}
}
@@ -524,7 +524,8 @@
},
"picker": {
"headline": "Select a page"
"headline": "Select a page",
"urlnotfound": "URL not found"
},
"infotab": {