login works + nav bar output

This commit is contained in:
2021-12-09 14:18:38 +01:00
parent a8d0ef59d7
commit d8a766e5e4
89 changed files with 1652 additions and 3489 deletions
+28
View File
@@ -0,0 +1,28 @@
declare module 'zero/account'
{
export interface AccountUser
{
id: string;
currentAppId: string;
isSuper: boolean;
avatarId: string;
email: string;
name: string;
isActive: boolean;
createdDate: string;
flavor?: string;
}
export interface AccountStoreState
{
user?: AccountUser;
}
export interface AccountLoginModel
{
email: string;
password: string;
isPersistent: boolean;
}
}
+13
View File
@@ -0,0 +1,13 @@
import { AccountLoginModel, AccountUser } from 'zero/account';
import { get, post, ZeroRequestConfig } from '../services/request';
export default {
getUser: async (config?: ZeroRequestConfig): Promise<AccountUser> => await get('backoffice/account/user', { ...config }),
isAuthenticated: async (config?: ZeroRequestConfig) => await get('backoffice/account/loggedin', { ...config }),
login: async (model: AccountLoginModel, config?: ZeroRequestConfig) => await post('backoffice/account/login', model, { ...config }),
logout: async (config?: ZeroRequestConfig) => await post('backoffice/account/logout', null, { ...config })
};
@@ -1,8 +1,7 @@
<template>
<div class="app-auth">
<i class="fth-home app-auth-font-trigger"></i>
<span></span>
<ui-form class="app-auth-inner" v-slot="form" @submit="onSubmit">
<form class="app-auth-inner" @submit.prevent="onSubmit">
<div>
<div class="app-auth-logo">
<span class="app-nav-logo-circle"></span>
@@ -10,8 +9,8 @@
<img src="/Assets/zero-dark.svg" class="app-auth-image show-dark" v-localize:alt="'@zero.name'" />
</div>
<ui-error :catch-remaining="true" />
<ui-message type="info" v-if="rejectReason" :text="rejectReason" />
<!--<ui-error :catch-remaining="true" />
<ui-message type="info" v-if="rejectReason" :text="rejectReason" />-->
<ui-property field="email" label="@login.fields.email" :vertical="true">
<input v-model="model.email" type="text" class="ui-input" maxlength="120" v-localize:placeholder="'@login.fields.email_placeholder'" />
@@ -24,50 +23,62 @@
</div>
<div class="app-auth-bottom">
<ui-button class="app-auth-confirm" type="accent" :submit="true" label="@login.button" :state="form.state" />
<ui-button type="blank" label="@login.button_forgot" />
<ui-button class="app-auth-confirm" type="accent" :submit="true" label="@login.button" :state="state" />
<!--<ui-button type="blank" label="@login.button_forgot" />-->
</div>
</ui-form>
</form>
</div>
</template>
<script>
import AuthApi from 'zero/helpers/auth.js';
<script lang="ts">
import { defineComponent } from 'vue';
import api from './api';
import { useAccountStore } from './store';
import { useUiStore } from '../ui/store';
export default {
export default defineComponent({
name: 'app-login',
data: () => ({
rejectReason: null,
state: 'default',
model: {
email: null,
password: null,
email: '',
password: '',
isPersistent: true
}
}),
created()
{
this.rejectReason = AuthApi.rejectReason;
//this.rejectReason = api.rejectReason;
},
methods: {
onSubmit(form)
async onSubmit(form)
{
this.rejectReason = null;
const accountStore = useAccountStore();
form.handle(AuthApi.login(this.model)).then(res =>
this.rejectReason = null;
this.state = 'loading';
try
{
window.location.reload();
AuthApi.$emit('apprebuild');
}, errors =>
const response = await api.login(this.model);
this.state = 'success';
await useUiStore().setup();
accountStore.user = await api.getUser();
}
catch
{
console.info('login: error', errors);
});
accountStore.user = null;
this.state = 'error';
return;
}
}
}
}
})
</script>
@@ -97,15 +108,6 @@
}*/
}
.app-auth-font-trigger
{
position: absolute;
pointer-events: none;
opacity: 0.001;
bottom: 0;
right: 0;
}
.app-auth-inner
{
display: grid;
+18
View File
@@ -0,0 +1,18 @@
import { defineStore } from 'pinia';
import { AccountStoreState } from 'zero/account';
export type TranslationState = {
culture: string,
translations: Record<string, string>
};
export const useAccountStore = defineStore('zero.account', {
state: () => ({
user: undefined
} as AccountStoreState),
getters: {
isAuthenticated: state => state.user != null
}
});
+35 -75
View File
@@ -1,92 +1,52 @@
<template>
<div class="app">
<div class="ui-box" style="width: 600px">
nav:
<router-link to="/">Home</router-link>
<router-link to="/about">About</router-link>
<router-link to="/countries">Countries</router-link><br />
view:
<router-view></router-view>
</div>
<div class="ui-box">
<input type="text" v-model="input" />
<br />
output: {{output}}
<br />
date: <span v-date="'2020-12-11T00:28:00'"></span>
<br />
name: <span v-localize="'@name'"></span>
<br />
version: {{zero.version}}
<br />
icon: <ui-icon symbol="fth-home" />
<br />
loading: <ui-loading />
<br />
tabs:
<ui-tabs>
<ui-tab label="Content">
This is my content
</ui-tab>
<ui-tab :label="tabLabel">
Our settings <button type="button" @click="tabLabel = 'New Settings'">change label</button>
</ui-tab>
</ui-tabs>
<br /><br /><br />
buttons:
<ui-button label="Click" type="accent"></ui-button>
<ui-icon-button icon="fth-home" type="accent"></ui-icon-button>
<ui-dot-button></ui-dot-button>
<ui-select-button label="Choose" description="Select an item"></ui-select-button>
<ui-state-button :items="[{value: 'yes', label: 'Yes'}, {value: 'no', label: 'No'}]"></ui-state-button>
<br />
message: <ui-message text="Warning, this is not acceptable" />
<br />
pagination: <ui-pagination :pages="10" :page="2"></ui-pagination>
<br />
rte:
<br />
<ui-rte v-model="rte" />
<br />
<ui-toggle v-model="toggled" />
<br />
<ui-search v-if="toggled" v-model="search" @submit="searchSubmit($event)" />
<br />
countrypicker:
<ui-countrypicker v-if="!toggled" />
</div>
<div class="app" v-if="initialized">
<app-login v-if="!accountStore.isAuthenticated" />
<template v-else>
<app-navigation />
<div class="app-main">
<router-view></router-view>
</div>
</template>
</div>
</template>
<script lang="ts">
import './styles/styles';
import { selectorToArray } from './utils';
import { localize } from './services/localization';
import AppLogin from './account/login.vue';
import AppNavigation from './ui/app-navigation.vue';
import { useAccountStore } from './account/store';
import { useUiStore } from './ui/store';
import { useTranslationStore } from './stores/translations';
import accountApi from './account/api';
import { defineComponent } from 'vue';
export default defineComponent({
name: 'app',
components: { AppLogin, AppNavigation },
export default {
data: () => ({
input: null,
name: localize('@name'),
tabLabel: 'Settings',
rte: '<p><b>Hallo</b><br>Das ist <i>schön</i></p><p>Ciao Tobi</p>',
toggled: true,
search: null
initialized: false,
accountStore: null
}),
computed: {
output()
async mounted()
{
this.accountStore = useAccountStore();
try
{
return selectorToArray(this.input);
this.accountStore.user = await accountApi.getUser();
await useUiStore().setup();
}
catch { }
await useTranslationStore().setup();
this.initialized = true;
},
methods: {
searchSubmit(ev)
{
console.info('searched: ' + ev);
}
}
}
});
</script>
+11 -1
View File
@@ -11,6 +11,11 @@ import uiStateButton from './ui-state-button.vue';
import uiMessage from './ui-message.vue';
import uiPagination from './ui-pagination.vue';
import uiError from './ui-error.vue';
import uiLocalize from './ui-localize.vue';
import uiThumbnail from './ui-thumbnail.vue';
import uiDropdownSeparator from './ui-dropdown-separator.vue';
import uiDropdownButton from './ui-dropdown-button.vue';
import uiDropdown from './ui-dropdown.vue';
export {
uiIcon,
@@ -25,5 +30,10 @@ export {
uiStateButton,
uiMessage,
uiPagination,
uiError
uiError,
uiLocalize,
uiThumbnail,
uiDropdownSeparator,
uiDropdownButton,
uiDropdown
};
@@ -0,0 +1,277 @@
<template>
<button v-if="!confirming" :disabled="disabled" type="button" @click="onClick" class="ui-dropdown-button" :class="{ 'has-icon': icon, 'is-active': selected, 'is-multiline': multiline }">
<ui-icon v-if="icon" :symbol="icon" class="ui-dropdown-button-icon" />
<span><ui-localize :value="label" /><span v-if="false && confirm && !confirming"> &hellip;</span><span v-if="false && confirming">?</span></span>
<ui-icon v-if="selected" symbol="check" class="ui-dropdown-button-selected" />
<i v-if="loading" class="ui-dropdown-button-progress"></i>
</button>
<div v-if="confirming" class="ui-dropdown-button-confirmation">
<ui-icon v-if="icon" :symbol="icon" class="ui-dropdown-button-icon" />
<ui-localize :value="label" />
<ui-button type="small light" icon="fth-x" title="Cancel" @click="confirming=false" />
<ui-button :type="negative ? 'small danger' : 'small primary'" icon="fth-check" title="OK" @click="onClick($event, true)" />
</div>
</template>
<script>
export default {
name: 'uiDropdownButton',
props: {
value: {
default: null
},
multiline: {
type: Boolean,
default: false
},
label: {
type: String,
required: true
},
icon: {
type: String
},
selected: {
type: Boolean,
default: false
},
confirm: {
type: Boolean,
default: false
},
disabled: {
type: Boolean,
default: false
},
negative: {
type: Boolean,
default: false
},
prevent: {
type: Boolean,
default: false
}
},
data: () => ({
dropdown: null,
loading: false,
confirming: false
}),
mounted ()
{
// find parent dropdown so we can pass it on item click
let current = this;
do
{
if (current.$options.name === 'uiDropdown')
{
this.dropdown = current;
break;
}
}
while (current = current.$parent);
if (!this.dropdown)
{
console.warn('ui-dropdown-button: Could not find parent <ui-dropdown />');
}
},
methods: {
onClick(e, confirmed)
{
var instance = this;
if (!this.loading && !this.disabled)
{
if (this.confirm && !confirmed)
{
this.confirming = true;
//var confirmed = window.confirm('confirm?');
return;
}
else
{
this.confirming = false;
}
if (!this.prevent && this.dropdown)
{
this.dropdown.hide();
}
this.$emit('click', this.value, {
dropdown: this.dropdown,
hide()
{
if (this.dropdown)
{
this.dropdown.hide();
}
instance.$emit('hide');
},
loading(isLoading)
{
instance.loading = isLoading;
}
});
}
}
}
}
</script>
<style lang="scss">
button.ui-dropdown-button
{
display: grid;
width: 100%;
grid-template-columns: minmax(0, 1fr) auto;
gap: 6px;
align-items: center;
font-size: var(--font-size);
font-weight: 500;
padding: 0 16px;
height: 48px;
color: var(--color-text);
border-radius: var(--radius);
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
max-width: 100%;
&.has-icon
{
grid-template-columns: 30px minmax(0, 1fr) auto;
&:not([disabled]):hover .ui-dropdown-button-icon
{
color: var(--color-text);
}
}
&.has-icon.is-negative:not([disabled]):hover .ui-dropdown-button-icon
{
color: var(--color-accent-error);
}
&.is-multiline
{
white-space: normal;
overflow: visible;
}
&:not([disabled]):hover, &.is-active, &:focus
{
background: var(--color-dropdown-selected);
color: var(--color-text);
//font-weight: 700;
.ui-dropdown-list-item-icon
{
color: var(--color-text);
}
}
&.is-active
{
font-weight: 700;
}
&.is-active .ui-dropdown-button-icon
{
color: var(--color-text);
}
&[disabled]
{
color: var(--color-text-dim);
cursor: default;
pointer-events: none;
.ui-dropdown-list-item-icon,
.ui-dropdown-button-icon
{
color: var(--color-text-dim);
}
}
& + .ui-dropdown-button
{
margin-top: 4px;
}
}
.ui-dropdown-button-icon
{
font-size: 18px;
line-height: 1;
font-weight: 400;
position: relative;
top: -1px;
color: var(--color-text);
}
.ui-dropdown-button-progress
{
width: 16px;
height: 16px;
z-index: 2;
border-radius: 40px;
border: 2px solid transparent;
border-left-color: var(--color-text);
opacity: 1;
will-change: transform;
animation: rotating .5s linear infinite;
transition: opacity .25s ease;
}
.ui-dropdown-button-confirmation
{
display: grid;
flex-grow: 0;
width: 100%;
grid-template-columns: 30px minmax(0, 1fr) auto auto;
gap: 6px;
align-items: center;
font-size: var(--font-size);
padding: 0 6px 0 16px;
height: 48px;
color: var(--color-text);
border-radius: var(--radius);
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
max-width: 300px;
.ui-button + .ui-button
{
margin-left: 2px;
}
.ui-button
{
width: 40px;
}
}
@keyframes rotating
{
from
{
-webkit-transform: rotate(0);
transform: rotate(0)
}
to
{
-webkit-transform: rotate(1turn);
transform: rotate(1turn)
}
}
</style>
@@ -0,0 +1,20 @@
<template>
<hr class="ui-dropdown-separator">
</template>
<script>
export default {
name: 'uiDropdownSeparator'
}
</script>
<style lang="scss">
.ui-dropdown-separator
{
border: none;
border-bottom: 1px solid var(--color-dropdown-line);
margin: 5px 0;
}
</style>
@@ -0,0 +1,187 @@
<template>
<div class="ui-dropdown-container">
<div v-if="hasButton" ref="trigger" class="ui-dropdown-toggle" @click.prevent.stop="toggle">
<slot name="button"></slot>
</div>
<div class="ui-dropdown" ref="overlay" role="dialog" v-if="open" v-click-outside="hide" :class="dropdownClasses">
<slot></slot>
</div>
</div>
</template>
<script>
//import Overlay from 'zero/helpers/overlay.js';
import { defineComponent } from 'vue';
export default defineComponent({
name: 'uiDropdown',
props: {
align: {
type: String,
default: 'left'
},
theme: {
type: String,
default: 'dark'
},
locked: {
type: Boolean,
default: false
},
disabled: {
type: Boolean,
default: false
}
},
computed: {
hasButton()
{
return this.$scopedSlots.hasOwnProperty('button');
},
dropdownClasses()
{
let classes = 'align-' + this.align.split(' ').join(' align-');
if (!!this.theme)
{
classes += ' theme-' + this.theme;
}
return classes;
}
},
data: () => ({
open: false
}),
methods: {
toggle()
{
if (this.open)
{
this.hide();
}
else if (!this.disabled)
{
this.show();
}
},
show()
{
if (this.disabled)
{
return;
}
//Overlay.setDropdown(this);
this.open = true;
//this.position();
this.$emit('opened');
},
hide()
{
if (this.locked)
{
return;
}
this.open = false;
this.$emit('closed');
},
position()
{
this.$nextTick(() =>
{
// the trigger which is the relative to the overlay
const reference = this.$refs.trigger;
// get bounding boxes both for the trigger and the overlay
const triggerBoundingBox = reference.getBoundingClientRect();
const overlayBoundingBox = this.$refs.overlay.getBoundingClientRect();
const windowBox = { width: window.innerWidth, height: window.innerHeight };
const windowOffset = 32;
// calculate available space for the dropdown in all 4 directions areound the trigger element
let availableSpace = {
left: triggerBoundingBox.left + triggerBoundingBox.width - windowOffset,
right: windowBox.width - triggerBoundingBox.left - windowOffset,
top: triggerBoundingBox.top + triggerBoundingBox.height - windowOffset,
bottom: windowBox.height - triggerBoundingBox.top - windowOffset
};
console.table(availableSpace);
//console.info(triggerBoundingBox, overlayBoundingBox, windowBox);
//const resize_ob = new ResizeObserver(function (entries)
//{
// // since we are observing only a single element, so we access the first element in entries array
// let rect = entries[0].contentRect;
// // current width & height
// let width = rect.width;
// let height = rect.height;
// console.log('Current Width : ' + width);
// console.log('Current Height : ' + height);
//});
//// start observing for resize
//resize_ob.observe(document.querySelector("#demo-textarea"));
//resize_ob.unobserve(document.querySelector("#demo-textarea"));
});
}
}
});
</script>
<style lang="scss">
.ui-dropdown-container
{
position: relative;
}
.ui-dropdown
{
position: absolute;
min-width: 300px;
min-height: 20px;
background: var(--color-dropdown);
border-radius: var(--radius);
border: 1px solid var(--color-dropdown-border);
box-shadow: var(--shadow-dropdown);
z-index: 8;
top: calc(100% + 5px);
padding: 5px;
color: var(--color-text);
&.align-right
{
right: 0;
}
&.align-top
{
top: calc(100% + 5px);
bottom: auto;
}
&.align-bottom
{
bottom: calc(100% + 5px);
top: auto;
}
}
</style>
@@ -0,0 +1,38 @@
<template>
<span>{{output}}</span>
</template>
<script>
import { localize } from '../services/localization';
export default {
name: 'uiLocalize',
props: {
value: {
type: String,
default: null
},
force: {
type: Boolean,
default: false
},
tokens: {
type: Object,
default: null
},
hideEmpty: {
type: Boolean,
default: false
}
},
computed: {
output()
{
return localize(this.value, { tokens: this.tokens, force: this.force, hideEmpty: this.hideEmpty });
}
}
}
</script>
@@ -0,0 +1,30 @@
<template>
<img :src="src" />
</template>
<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({
name: 'uiThumbnail',
props: {
media: {
type: String,
required: true
},
size: {
type: String,
default: 'thumbnail'
}
},
computed: {
src()
{
return 'zero/api/media/getSource/?id=' + this.media + '&size=' + this.size;
}
}
});
</script>
+3 -2
View File
@@ -6,9 +6,9 @@ import { Zero } from './types/zero';
import { createRouter, RouteRecordRaw, RouterOptions } from 'vue-router';
import registerDirectives from '../directives/register';
import registerComponents from '../components/register';
import registerFormComponents from '../forms/components/register';
import registerFormComponents from '../forms/register';
import { getRouterConfig, appendRouterGuards } from './router/routerConfig';
import countryPlugin from '../modules/countries/plugin';
import { countryPlugin, applicationPlugin } from '../modules';
export class ZeroRuntime implements Zero
{
@@ -61,6 +61,7 @@ export class ZeroRuntime implements Zero
// install all plugins
countryPlugin.install(pluginOptions);
applicationPlugin.install(pluginOptions);
}
@@ -1,9 +1,4 @@
const isServer = vNode =>
{
return typeof vNode.componentInstance !== 'undefined' && vNode.componentInstance.$isServer;
};
const isPopup = (popupItem, elements) =>
{
if (!popupItem || !elements)
@@ -79,7 +74,7 @@ export default {
setTimeout(() =>
{
!isServer(vNode) && document.addEventListener('click', handler);
document.addEventListener('click', handler);
}, 200);
},
@@ -93,7 +88,7 @@ export default {
unbind(el, binding, vNode)
{
!isServer(vNode) && document.removeEventListener('click', el.__vueClickOutside__.handler);
document.removeEventListener('click', el.__vueClickOutside__.handler);
delete el.__vueClickOutside__;
}
};
+7
View File
@@ -5,4 +5,11 @@ declare module '*.vue' {
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types
const component: DefineComponent<{}, {}, any>
export default component
}
declare module 'vue' {
import { CompatVue } from '@vue/runtime-dom'
const Vue: CompatVue
export default Vue
export * from '@vue/runtime-dom'
}
@@ -2,10 +2,12 @@ import uiRte from './ui-rte/ui-rte.vue';
import uiCheckList from './ui-check-list.vue';
import uiSearch from './ui-search.vue';
import uiToggle from './ui-toggle.vue';
import uiProperty from './ui-property.vue';
export {
uiRte,
uiCheckList,
uiSearch,
uiToggle
uiToggle,
uiProperty
};
+5
View File
@@ -0,0 +1,5 @@
import uiForm from './ui-form.vue';
export {
uiForm
};
@@ -1,13 +1,19 @@
import { App } from 'vue';
import * as partials from './components/index';
import * as components from './index';
export default function (app: App)
{
for (var key in partials)
{
let component = partials[key];
app.component(key, component.default || component);
}
for (var key in components)
{
let component = components[key];
app.component(key, component.default || component);
}
};
@@ -4,27 +4,31 @@
<div v-if="loadingState == 'loading'" class="ui-form-loading">
<i class="ui-form-loading-progress"></i>
</div>
<ui-error-view v-if="loadingState === 'error'" :error="loadingError" />
<form-error-view v-if="loadingState === 'error'" :error="loadingError" />
</form>
</template>
<script>
import Overlay from 'zero/helpers/overlay.js'
import Notification from 'zero/helpers/notification.js'
import { isArray as _isArray, filter as _filter, groupBy as _groupBy, each as _each, difference as _difference } from 'underscore'
<script lang="ts">
import { defineComponent } from 'vue';
import FormErrorView from './form-error-view.vue';
//import Overlay from 'zero/helpers/overlay.js'
import * as notifications from '../services/notification';
import { arrayGroupBy } from '../utils/arrays';
export default {
export default defineComponent({
name: 'uiForm',
components: { FormErrorView },
props: {
errorComponents: {
type: Array,
default: () => [ 'uiError' ]
default: () => ['uiError']
},
inputComponents: {
type: Array,
default: () => [ 'uiProperty' ]
default: () => ['uiProperty']
},
route: {
type: [String, Object],
@@ -76,7 +80,7 @@
// shows a confirmation dialog for dirty forms when the route tries to change
// it only works when this method is attached to the route component
beforeRouteLeave(to, from, next)
{
{
if (this.dirty)
{
Overlay.confirm({
@@ -228,7 +232,7 @@
Overlay.confirmDelete().then(opts =>
{
opts.state('loading');
promise().then(response =>
{
if (response.success)
@@ -236,14 +240,14 @@
opts.state('success');
opts.hide();
this.$router.go(-1);
Notification.success('@deleteoverlay.success', '@deleteoverlay.success_text');
notifications.success('@deleteoverlay.success', '@deleteoverlay.success_text');
}
else
{
opts.errors(response.errors);
}
});
});
});
},
@@ -297,12 +301,12 @@
}
else
{
this.errors = !_isArray(errors) ? [errors] : errors;
this.errors = !Array.isArray(errors) ? [errors] : errors;
}
// get all components + grouped errors
let errorComponents = this.getErrorComponents();
let errorGroups = _groupBy(this.errors, 'property');
let errorGroups = arrayGroupBy(this.errors, 'property');
let handledGroups = [];
// set errors
@@ -322,9 +326,9 @@
}
});
// fill leftovers
_each(errorGroups, (errorGroup, field) =>
for (var field in errorGroups)
{
let errorGroup = errorGroups[field];
if (handledGroups.indexOf(field) < 0)
{
errorComponents.forEach(component =>
@@ -340,7 +344,7 @@
}
});
}
});
}
},
@@ -374,7 +378,7 @@
});
};
traverseChildren(this);
traverseChildren(this);
return errorComponents;
},
@@ -426,7 +430,7 @@
//console.info('done');
}
}
}
});
</script>
<style lang="scss">
@@ -0,0 +1,16 @@
import { get, post, put, del, ZeroRequestConfig, ZeroRequestQuery } from '../../services/request';
export default {
getEmpty: async (flavor?: string, config?: ZeroRequestConfig) => await get("applications/empty", { ...config, params: { flavor } }),
getById: async (id: string, changeVector?: string, config?: ZeroRequestConfig) => await get('applications/' + id, { ...config, params: { changeVector } }),
getByQuery: async (query: ZeroRequestQuery, config?: ZeroRequestConfig) => await get('applications', { ...config, params: { ...query } }),
create: async (model: any, config?: ZeroRequestConfig) => await post('applications', model, config),
update: async (model: any, config?: ZeroRequestConfig) => await put('applications/' + model.id, model, config),
delete: async (id: string, config?: ZeroRequestConfig) => await del('applications/' + id, config),
};
@@ -0,0 +1,11 @@
import api from './api';
//import editor from './editor';
//import list from './list';
import plugin from './plugin';
export {
api as applicationApi,
//editor as countrySchema,
//list as countryListSchema,
plugin as applicationPlugin
};
@@ -0,0 +1,18 @@
import { ZeroPlugin, ZeroPluginOptions } from '../../core';
import { defineAsyncComponent } from 'vue';
//const Picker = () => import('./ui-countrypicker.vue');
//const Page = () => import('./_page.vue');
export default {
name: "zero.applications",
install(app: ZeroPluginOptions)
{
//app.vue.component('ui-countrypicker', defineAsyncComponent(Picker));
//app.route({ path: '/countries', component: Page });
//app.editor('country', null);
//app.editorField('')
}
} as ZeroPlugin;
@@ -0,0 +1,11 @@
import api from './api';
import editor from './editor';
//import list from './list';
import plugin from './plugin';
export {
api as countryApi,
editor as countrySchema,
//list as countryListSchema,
plugin as countryPlugin
};
+3
View File
@@ -0,0 +1,3 @@
export * from './countries';
export * from './applications';
+12 -9
View File
@@ -68,16 +68,19 @@ export async function patch(url: string, data: any, config?: ZeroRequestConfig)
export async function send(config: ZeroRequestConfig)
{
config = getConfig(config);
const result = await axios(config);
return result.data;
try
{
const result = await axios(config);
return result.data;
}
catch (err)
{
// TODO handle errors
}
//try
//{
// const result = await axios(config);
// return result.data;
//}
//catch (err)
//{
// console.error('axios err: ' + err);
// // TODO handle errors
//}
}
@@ -1,5 +1,6 @@
import { defineStore } from 'pinia';
import uiApi from '../ui/api';
export type TranslationState = {
culture: string,
@@ -9,13 +10,18 @@ export type TranslationState = {
export const useTranslationStore = defineStore('zero.translations', {
state: () => ({
culture: null,
translations: { name: 'Mini' }
translations: { }
} as TranslationState),
actions: {
find(key: string): string
{
return this.translations[key.toLowerCase()];
},
async setup()
{
this.translations = await uiApi.getTranslations();
}
}
});
@@ -1,488 +0,0 @@
.app-nav-apps
{
position: absolute;
left: 100%;
top: 0;
bottom: 0;
width: 360px;
background: var(--color-bg-shade-3);
z-index: -1;
margin-left: 0;
display: flex;
flex-direction: column;
display: none; // TODO
.ui-header-bar
{
height: 92px;
}
}
.app-nav-app
{
display: grid;
grid-template-columns: auto minmax(auto, 1fr) auto;
grid-gap: var(--padding-s);
align-items: center;
margin: 0 var(--padding-m);
padding: var(--padding-s) var(--padding-s);
border-radius: var(--radius);
&.is-active
{
background: var(--color-bg-shade-4);
font-weight: 700;
}
}
.app-nav-app-icon
{
height: 32px;
}
.app-nav-app-selected
{
}
.app-nav
{
position: relative;
background: var(--color-box);
width: 260px;
color: var(--color-text);
height: 100vh;
display: grid;
grid-template-rows: auto auto 1fr auto;
box-shadow: var(--shadow-short);
margin-right: 1px;
z-index: 5;
.theme-rounded &
{
height: calc(100vh - 20px);
margin: 10px;
margin-right: 0;
border-radius: var(--radius);
box-shadow: var(--shadow-short);
}
&.theme-dark
{
//background-image: radial-gradient(rgba(255,255,255,.1) 1px,transparent 0),radial-gradient(rgba(255,255,255,.1) 1px,transparent 0);
//background-size: 40px 40px;
//background-position: 0 0,20px 20px;
}
}
.app-nav-boxed
{
height: 90px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 var(--padding-xs) 0 0;
}
.app-nav-inner
{
overflow-y: auto;
overflow-x: hidden;
}
.app-nav-headline
{
display: flex;
align-items: center;
padding: 0 var(--padding-m);
margin: 0;
.theme-rounded &
{
margin: 0;
}
img
{
//height: 24px;
height: 15px;
//margin-top: 9px;
}
}
.app-nav-logo-circle
{
display: inline-block;
width: 22px;
height: 22px;
border-radius: 20px;
border: 4px solid var(--color-accent);
margin-right: 12px;
}
.app-nav-search
{
}
.app-nav-switch
{
margin-bottom: var(--padding-s);
//border-bottom: 1px solid var(--color-line-onbg);
background: var(--color-bg-shade-3);
//background: var(--color-accent);
.ui-button.type-light
{
padding: 0 24px;
height: 70px;
background: transparent;
border-radius: 0;
font-size: var(--font-size-m);
}
.ui-dropdown-button-icon
{
max-height: 20px;
max-width: 20px;
}
&.is-fake
{
margin-bottom: 0;
height: 0;
background: none;
}
}
a.app-nav-item, button.app-nav-item
{
display: grid;
grid-template-columns: 28px 1fr auto;
gap: 6px;
align-items: center;
font-size: var(--font-size);
padding: 0 var(--padding-m);
height: 50px;
color: var(--color-text);
position: relative;
& + .app-nav-item
{
margin-top: 1px;
}
&:hover
{
color: var(--color-text);
background: var(--color-tree-selected);
.app-nav-item-icon
{
color: var(--color-text);
}
}
&.is-active:not([alias="dashboard"]), &.is-active-exact
{
color: var(--color-text);
background: var(--color-tree-selected);
font-weight: 700;
border-right: 3px solid var(--color-accent);
.app-nav-item-icon
{
color: var(--color-text);
}
.app-nav-item-arrow
{
transform: rotate(180deg);
}
.app-nav-item-text
{
color: var(--color-text);
}
&:before
{
content: '';
position: absolute;
left: 0;
top: 0;
bottom: 0;
//width: 3px;
display: inline-block;
background: var(--color-tree-selected-line);
}
}
}
.app-nav-item-text
{
transition: color 0.2s ease;
}
.app-nav-item-icon
{
font-size: 18px;
line-height: 1;
font-weight: 400;
position: relative;
top: -1px;
color: var(--color-text);
transition: color 0.2s ease;
}
.app-nav-item-arrow
{
color: var(--color-text-dim);
}
.app-nav-children
{
padding: 5px 0 10px;
}
a.app-nav-child
{
display: flex;
align-items: center;
font-size: var(--font-size);
padding: 0 var(--padding) 0 calc(var(--padding) + 26px);
height: 36px;
color: var(--color-text-dim);
position: relative;
&:hover, &.is-active
{
color: var(--color-text);
}
&.is-active
{
font-weight: 700;
}
&.is-active:before
{
content: '';
display: inline-block;
width: 4px;
height: 4px;
border-radius: 4px;
background: var(--color-accent);
position: absolute;
margin-left: -14px;
margin-top: -3px;
top: 50%;
}
}
.app-nav-children-enter-active
{
transition: all .3s ease;
}
.app-nav-children-leave-active
{
transition: all 0;
}
.app-nav-children-enter, .app-nav-children-leave-to
{
transform: translateX(-10px);
opacity: 0;
}
// account
.app-nav-account
{
border-top: 1px solid var(--color-line-onbg);
}
.app-nav-account-button
{
display: grid;
width: 100%;
grid-template-columns: auto minmax(auto, 1fr) auto;
gap: 16px;
color: var(--color-text-dim);
align-items: center;
padding: var(--padding-m);
border-bottom-left-radius: var(--radius);
border-bottom-right-radius: var(--radius);
&:hover
{
background: var(--color-bg-shade-2);
}
.-image
{
height: 32px;
width: 32px;
border-radius: 18px;
position: relative;
top: -1px;
background: var(--color-bg-shade-3);
text-align: center;
line-height: 33px;
font-size: 16px;
overflow: hidden;
color: transparent;
}
.-text
{
font-weight: 400;
margin: 0;
strong
{
font-weight: 700;
color: var(--color-text);
}
}
}
/* COMPACT MODE */
.app-nav.is-compact
{
width: 82px;
.app-nav-headline
{
width: 100%;
overflow: hidden;
padding-left: 0;
padding-right: 0;
img
{
margin-left: 29px;
clip-path: circle(23.78% at 13px 14px);
min-width: 118px;
}
}
.app-nav-boxed
{
width: 100%;
overflow: hidden;
}
.app-nav-switch
{
visibility: hidden;
pointer-events: none;
opacity: 0;
width: 100%;
overflow: hidden;
padding: 16px 0 0;
}
a.app-nav-item, button.app-nav-item
{
display: flex;
padding-left: var(--padding);
width: 100%;
//height: 60px;
&:hover + .app-nav-children
{
display: block;
}
&:before
{
display: none;
}
}
.app-nav-item-text, .app-nav-item-arrow
{
display: none;
}
.app-nav-children
{
display: none;
position: absolute;
z-index: 8;
min-width: 240px;
min-height: 20px;
background: var(--color-dropdown);
border-radius: var(--radius);
border-top-left-radius: 0;
border-bottom-left-radius: 0;
border: 1px solid var(--color-dropdown-border);
box-shadow: 6px 1px 8px rgba(0, 0, 0, 0.02);
padding: 5px;
color: var(--color-text);
margin-left: 82px;
margin-top: -55px;
&:hover
{
display: block;
}
}
a.app-nav-child
{
padding: 0 var(--padding);
display: grid;
width: 100%;
grid-template-columns: minmax(0, 1fr) auto;
gap: 6px;
align-items: center;
font-size: var(--font-size);
padding: 0 16px;
height: 48px;
color: var(--color-text-dim);
border-radius: var(--radius);
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
max-width: 100%;
&:not([disabled]):hover, &:focus
{
background: var(--color-dropdown-selected);
}
&.is-active
{
color: var(--color-text);
font-weight: 700;
}
}
.app-nav-account
{
padding: 0;
margin-bottom: var(--padding);
margin-left: 25px;
}
.app-nav-account-button
{
display: block;
width: 32px;
.-text, .-arrow
{
display: none;
}
}
}
-1
View File
@@ -13,7 +13,6 @@ import './settings/_font-sizes.scss';
import './settings/_shadows.scss';
import './canvas/_canvas.scss';
import './canvas/_navigation.scss';
import './modules/_button.scss';
import './modules/_headlines.scss';
+13
View File
@@ -0,0 +1,13 @@
import { UiIconSet, UiSection, UiSettingsGroup } from 'zero/ui';
import { get, ZeroRequestConfig } from '../services/request';
export default {
getSections: async (config?: ZeroRequestConfig): Promise<UiSection[]> => await get('backoffice/ui/sections', { ...config }),
getSettingGroups: async (config?: ZeroRequestConfig): Promise<UiSettingsGroup[]> => await get('backoffice/ui/settingareas', { ...config }),
getIconSets: async (config?: ZeroRequestConfig): Promise<UiIconSet[]> => await get('backoffice/ui/iconsets', { ...config }),
getTranslations: async (config?: ZeroRequestConfig): Promise<Record<string, string>> => await get('backoffice/ui/translations', { ...config }),
};
@@ -0,0 +1,666 @@
<template>
<div class="app-nav theme-dark" :class="{'is-compact': compact }">
<div class="app-nav-boxed">
<h1 class="app-nav-headline">
<span class="app-nav-logo-circle"></span>
<img src="/Assets/zero.svg" class="show-light" v-localize:alt="'@zero.name'" />
<img src="/Assets/zero-dark.svg" class="show-dark" v-localize:alt="'@zero.name'" />
</h1>
<ui-button icon="fth-search" :stroke="2.5" type="blank" class="app-nav-search" @click="openSearch" />
</div>
<ui-dropdown v-if="applications.length > 0" class="app-nav-switch">
<template v-slot:button>
<ui-button type="light block" :label="currentApplication.name" caret="right" />
</template>
<ui-dropdown-button v-for="app in applications" :value="app" :key="app.id" :label="app.name" :selected="app.id === appId" @click="applicationChanged" :prevent="true" />
<ui-dropdown-separator />
<ui-dropdown-button label="Add new application" icon="fth-plus" @click="addApplication" />
<ui-dropdown-button label="Manage apps" icon="fth-edit-2" @click="manageApplications" />
</ui-dropdown>
<div v-else class="app-nav-switch is-fake"></div>
<nav class="app-nav-inner">
<template v-for="section in ui.sections">
<router-link :to="section.url" class="app-nav-item" :alias="section.alias" :class="{ 'has-children': hasChildren(section) }">
<ui-icon :symbol="section.icon" class="app-nav-item-icon" :size="18" />
<span class="app-nav-item-text" v-localize="section.name"></span>
<ui-icon v-if="hasChildren(section)" symbol="fth-chevron-down" class="app-nav-item-arrow" />
</router-link>
<transition name="app-nav-children">
<div class="app-nav-children" v-if="hasChildren(section) && $route.path.indexOf('/' + section.alias) === 0">
<router-link v-for="child in section.children" v-bind:key="child.alias" :to="child.url" class="app-nav-child">
<span class="app-nav-child-text" v-localize="child.name"></span>
</router-link>
</div>
</transition>
</template>
</nav>
<ui-dropdown class="app-nav-account" v-if="account.user" align="left bottom">
<template v-slot:button>
<button type="button" class="app-nav-account-button">
<ui-thumbnail v-if="false && account.user.avatarId" class="-image" :media="account.user.avatarId" :alt="account.user.name" />
<span v-else class="-image"><ui-icon symbol="fth-user"></ui-icon></span>
<p class="-text"><strong>{{account.user.name}}</strong></p>
<ui-icon symbol="fth-more-horizontal" class="-arrow" />
</button>
</template>
<ui-dropdown-button label="Edit" icon="fth-edit-2" @click="editUser" />
<ui-dropdown-button label="Change password" icon="fth-lock" @click="changePassword" />
<!--<ui-dropdown-button label="Toggle sidebar" icon="fth-minimize-2" @click="toggleSidebar" />-->
<ui-dropdown-button label="Dark theme" v-if="!darkTheme" icon="fth-moon" @click="toggleDarkTheme" />
<ui-dropdown-button label="Light theme" v-if="darkTheme" icon="fth-sun" @click="toggleDarkTheme" />
<ui-dropdown-button label="Logout" icon="fth-log-out" @click="logout" />
</ui-dropdown>
</div>
</template>
<script>
import { defineComponent } from 'vue';
import { useAccountStore } from '../account/store';
import { useUiStore } from '../ui/store';
import { applicationApi } from '../modules/applications';
import EventHub from '../services/eventhub';
//const compactCacheKey = 'zero.navigation.compact';
const themeCacheKey = 'zero.theme';
export default defineComponent({
name: 'app-navigation',
data: () => ({
account: null,
ui: null,
appId: null,
applications: [],
sections: [],
compact: false,
darkTheme: false,
currentApplication: null,
themeSwitchTimeout: null
}),
async created()
{
this.ui = useUiStore();
this.account = useAccountStore();
this.applications = (await applicationApi.getByQuery({ pageSize: 100 })).items;
this.currentApplication = this.applications[0];
this.appId = this.currentApplication.id;
//this.compact = localStorage.getItem(compactCacheKey) === 'true';
//this.darkTheme = localStorage.getItem(themeCacheKey) === 'dark';
//this.buildUser(AuthApi.user);
},
methods: {
hasChildren(section)
{
return section.children && section.children.length > 0;
},
editUser(item, opts)
{
opts.hide();
this.$router.push({
name: 'users-edit',
params: { id: this.user.id }
});
},
changePassword(item, opts)
{
AuthApi.openPasswordOverlay();
opts.hide();
},
logout(item, opts)
{
AuthApi.logout();
opts.hide();
},
addApplication(item, opts)
{
opts.hide();
this.$router.push({
name: 'applications-create'
});
},
manageApplications(item, opts)
{
opts.hide();
this.$router.push({
name: 'applications'
});
},
applicationChanged(item, opts)
{
//opts.loading(true);
AuthApi.switchApp(item.id).then(success =>
{
//opts.loading(false);
//opts.hide();
});
},
toggleSidebar()
{
this.compact = !this.compact;
localStorage.setItem(compactCacheKey, this.compact.toString());
},
toggleDarkTheme()
{
this.darkTheme = !this.darkTheme;
EventHub.emit('app.theme', this.darkTheme ? 'dark' : 'light');
localStorage.setItem(themeCacheKey, this.darkTheme ? 'dark' : 'light');
document.body.classList.toggle('theme-light', !this.darkTheme);
document.body.classList.toggle('theme-dark', this.darkTheme);
},
openSearch()
{
//EventHub.$emit('app.search.open');
}
}
});
</script>
<style lang="scss">
.app-nav-apps
{
position: absolute;
left: 100%;
top: 0;
bottom: 0;
width: 360px;
background: var(--color-bg-shade-3);
z-index: -1;
margin-left: 0;
display: flex;
flex-direction: column;
display: none; // TODO
.ui-header-bar
{
height: 92px;
}
}
.app-nav-app
{
display: grid;
grid-template-columns: auto minmax(auto, 1fr) auto;
grid-gap: var(--padding-s);
align-items: center;
margin: 0 var(--padding-m);
padding: var(--padding-s) var(--padding-s);
border-radius: var(--radius);
&.is-active
{
background: var(--color-bg-shade-4);
font-weight: 700;
}
}
.app-nav-app-icon
{
height: 32px;
}
.app-nav-app-selected
{
}
.app-nav
{
position: relative;
background: var(--color-box);
width: 260px;
color: var(--color-text);
height: 100vh;
display: grid;
grid-template-rows: auto auto 1fr auto;
box-shadow: var(--shadow-short);
margin-right: 1px;
z-index: 5;
.theme-rounded &
{
height: calc(100vh - 20px);
margin: 10px;
margin-right: 0;
border-radius: var(--radius);
box-shadow: var(--shadow-short);
}
&.theme-dark
{
//background-image: radial-gradient(rgba(255,255,255,.1) 1px,transparent 0),radial-gradient(rgba(255,255,255,.1) 1px,transparent 0);
//background-size: 40px 40px;
//background-position: 0 0,20px 20px;
}
}
.app-nav-boxed
{
height: 90px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 var(--padding-xs) 0 0;
}
.app-nav-inner
{
overflow-y: auto;
overflow-x: hidden;
}
.app-nav-headline
{
display: flex;
align-items: center;
padding: 0 var(--padding-m);
margin: 0;
.theme-rounded &
{
margin: 0;
}
img
{
//height: 24px;
height: 15px;
//margin-top: 9px;
}
}
.app-nav-logo-circle
{
display: inline-block;
width: 22px;
height: 22px;
border-radius: 20px;
border: 4px solid var(--color-accent);
margin-right: 12px;
}
.app-nav-search
{
}
.app-nav-switch
{
margin-bottom: var(--padding-s);
//border-bottom: 1px solid var(--color-line-onbg);
background: var(--color-bg-shade-3);
//background: var(--color-accent);
.ui-button.type-light
{
padding: 0 24px;
height: 70px;
background: transparent;
border-radius: 0;
font-size: var(--font-size-m);
}
.ui-dropdown-button-icon
{
max-height: 20px;
max-width: 20px;
}
&.is-fake
{
margin-bottom: 0;
height: 0;
background: none;
}
}
a.app-nav-item, button.app-nav-item
{
display: grid;
grid-template-columns: 28px 1fr auto;
gap: 6px;
align-items: center;
font-size: var(--font-size);
padding: 0 var(--padding-m);
height: 50px;
color: var(--color-text);
position: relative;
& + .app-nav-item
{
margin-top: 1px;
}
&:hover
{
color: var(--color-text);
background: var(--color-tree-selected);
.app-nav-item-icon
{
color: var(--color-text);
}
}
&.is-active:not([alias="dashboard"]), &.is-active-exact
{
color: var(--color-text);
background: var(--color-tree-selected);
font-weight: 700;
border-right: 3px solid var(--color-accent);
.app-nav-item-icon
{
color: var(--color-text);
}
.app-nav-item-arrow
{
transform: rotate(180deg);
}
.app-nav-item-text
{
color: var(--color-text);
}
&:before
{
content: '';
position: absolute;
left: 0;
top: 0;
bottom: 0;
//width: 3px;
display: inline-block;
background: var(--color-tree-selected-line);
}
}
}
.app-nav-item-text
{
transition: color 0.2s ease;
}
.app-nav-item-icon
{
font-size: 18px;
line-height: 1;
font-weight: 400;
position: relative;
top: -1px;
color: var(--color-text);
transition: color 0.2s ease;
}
.app-nav-item-arrow
{
color: var(--color-text-dim);
}
.app-nav-children
{
padding: 5px 0 10px;
}
a.app-nav-child
{
display: flex;
align-items: center;
font-size: var(--font-size);
padding: 0 var(--padding) 0 calc(var(--padding) + 26px);
height: 36px;
color: var(--color-text-dim);
position: relative;
&:hover, &.is-active
{
color: var(--color-text);
}
&.is-active
{
font-weight: 700;
}
&.is-active:before
{
content: '';
display: inline-block;
width: 4px;
height: 4px;
border-radius: 4px;
background: var(--color-accent);
position: absolute;
margin-left: -14px;
margin-top: -3px;
top: 50%;
}
}
.app-nav-children-enter-active
{
transition: all .3s ease;
}
.app-nav-children-leave-active
{
transition: all 0;
}
.app-nav-children-enter, .app-nav-children-leave-to
{
transform: translateX(-10px);
opacity: 0;
}
// account
.app-nav-account
{
border-top: 1px solid var(--color-line-onbg);
}
.app-nav-account-button
{
display: grid;
width: 100%;
grid-template-columns: auto minmax(auto, 1fr) auto;
gap: 16px;
color: var(--color-text-dim);
align-items: center;
padding: var(--padding-m);
border-bottom-left-radius: var(--radius);
border-bottom-right-radius: var(--radius);
&:hover
{
background: var(--color-bg-shade-2);
}
.-image
{
display: flex;
align-items: center;
justify-content: center;
height: 32px;
width: 32px;
border-radius: 18px;
position: relative;
top: -1px;
background: var(--color-bg-shade-3);
text-align: center;
font-size: 16px;
overflow: hidden;
color: var(--color-text);
}
.-text
{
font-weight: 400;
margin: 0;
strong
{
font-weight: 700;
color: var(--color-text);
}
}
}
/* COMPACT MODE */
.app-nav.is-compact
{
width: 82px;
.app-nav-headline
{
width: 100%;
overflow: hidden;
padding-left: 0;
padding-right: 0;
img
{
margin-left: 29px;
clip-path: circle(23.78% at 13px 14px);
min-width: 118px;
}
}
.app-nav-boxed
{
width: 100%;
overflow: hidden;
}
.app-nav-switch
{
visibility: hidden;
pointer-events: none;
opacity: 0;
width: 100%;
overflow: hidden;
padding: 16px 0 0;
}
a.app-nav-item, button.app-nav-item
{
display: flex;
padding-left: var(--padding);
width: 100%;
//height: 60px;
&:hover + .app-nav-children
{
display: block;
}
&:before
{
display: none;
}
}
.app-nav-item-text, .app-nav-item-arrow
{
display: none;
}
.app-nav-children
{
display: none;
position: absolute;
z-index: 8;
min-width: 240px;
min-height: 20px;
background: var(--color-dropdown);
border-radius: var(--radius);
border-top-left-radius: 0;
border-bottom-left-radius: 0;
border: 1px solid var(--color-dropdown-border);
box-shadow: 6px 1px 8px rgba(0, 0, 0, 0.02);
padding: 5px;
color: var(--color-text);
margin-left: 82px;
margin-top: -55px;
&:hover
{
display: block;
}
}
a.app-nav-child
{
padding: 0 var(--padding);
display: grid;
width: 100%;
grid-template-columns: minmax(0, 1fr) auto;
gap: 6px;
align-items: center;
font-size: var(--font-size);
padding: 0 16px;
height: 48px;
color: var(--color-text-dim);
border-radius: var(--radius);
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
max-width: 100%;
&:not([disabled]):hover, &:focus
{
background: var(--color-dropdown-selected);
}
&.is-active
{
color: var(--color-text);
font-weight: 700;
}
}
.app-nav-account
{
padding: 0;
margin-bottom: var(--padding);
margin-left: 25px;
}
.app-nav-account-button
{
display: block;
width: 32px;
.-text, .-arrow
{
display: none;
}
}
}
</style>
+27
View File
@@ -0,0 +1,27 @@
import { defineStore } from 'pinia';
import { UiIconSet, UiSection, UiSettingsGroup, UiStoreState } from 'zero/ui';
import api from './api';
export const useUiStore = defineStore('zero.ui', {
state: () => ({
sections: [],
settingGroups: [],
iconSets: []
} as UiStoreState),
actions: {
async setup()
{
const values = await Promise.all([
api.getSections(),
api.getSettingGroups(),
api.getIconSets()
]);
this.sections = values[0] as UiSection[];
this.settingGroups = values[1] as UiSettingsGroup[];
this.iconSets = values[2] as UiIconSet[];
}
}
});
+44
View File
@@ -0,0 +1,44 @@
declare module 'zero/ui'
{
export interface UiStoreState
{
sections: UiSection[];
settingGroups: UiSettingsGroup[];
iconSets: UiIconSet[];
}
export interface UiSection
{
alias: string;
name: string;
icon: string;
url: string;
isExternal: boolean;
children: UiSection[];
}
export interface UiSettingsGroup
{
name: string;
items: UiSettingsItem[];
}
export interface UiSettingsItem
{
alias: string;
name: string;
description: string;
icon: string;
url: string;
isPlugin: boolean;
}
export interface UiIconSet
{
alias: string;
name: string;
prefix: string;
icons: string[];
}
}
+12
View File
@@ -1,4 +1,6 @@
import { groupBy as _groupBy } from 'underscore';
const arrayMoveMutate = (array: any[], from: number, to: number) =>
{
const startIndex = to < 0 ? array.length + to : to;
@@ -54,6 +56,16 @@ export function arrayRemove(array: any[], value: any): number
}
/**
* Groups an array by a property key
* @returns {any} Array groups
*/
export function arrayGroupBy(array: any[], key: string): any
{
return _groupBy(array, key);
}
/**
* Converts an object path to an array of path parts
* @returns {string[]} Object paths as array
File diff suppressed because one or more lines are too long
@@ -1,322 +0,0 @@
<template>
<div class="blueprint" v-if="value && (isParent || isChild)">
<div class="blueprint-box">
<template v-if="isChild">
<div class="blueprint-inner">
<ui-icon symbol="fth-cloud" :size="22" />
<p v-localize:html="'@blueprint.hint.childText'"></p>
</div>
<aside>
<ui-button class="blueprint-button-settings" type="blank small" icon="fth-settings"
:title="{ key: value.blueprint.desync.length > 0 ? '@blueprint.hint.xUnlocked' : '@blueprint.hint.settingsButton', tokens: { count: value.blueprint.desync.length }}"
@click="openSettings" />
<router-link replace :to="switchLink" class="ui-button type-light type-small" v-localize="'@blueprint.hint.goToBlueprint'"></router-link>
</aside>
</template>
<template v-if="isParent">
<div class="blueprint-inner">
<ui-icon symbol="fth-cloud" :size="22" />
<p v-localize:html="value.id ? '@blueprint.hint.parentText' : '@blueprint.hint.parentCreateText'"></p>
</div>
<aside v-if="value.id">
<router-link replace :to="switchLink" class="ui-button type-light type-small" v-localize="'@blueprint.hint.goToChild'"></router-link>
</aside>
</template>
</div>
</div>
</template>
<script>
import Overlay from 'zero/helpers/overlay.js';
import SettingsOverlay from './settings.vue';
import Localization from 'zero/helpers/localization.js';
export default {
props: {
value: {
type: Object
},
meta: {
type: Object,
default: () => { }
},
config: {
type: Object,
default: () => { }
},
disabled: {
type: Boolean,
default: false
}
},
inject: ['editor'],
watch: {
'$route': function ()
{
this.bind();
}
},
computed: {
isParent()
{
return this.config.isBlueprintParent(this.$route, this.value);
},
isChild()
{
return this.config.isBlueprintChild(this.$route, this.value);
},
switchLink()
{
return {
name: this.$route.name,
params: this.$route.params,
query: {
...(this.$route.query || {}),
scope: !this.isChild ? undefined : 'shared'
}
};
}
},
mounted()
{
this.bind();
},
methods: {
openSettings()
{
const editor = typeof this.editor === 'string' ? this.zero.getEditor(this.editor) : this.editor;
return Overlay.open({
component: SettingsOverlay,
display: 'editor',
model: this.value,
blueprintConfig: this.config
}).then(res =>
{
this.value.blueprint = res.blueprint;
this.$emit('input', res.blueprint);
if (typeof res.update === 'function')
{
res.update(this.value);
}
//EventHub.$emit('page.update');
});
},
bind()
{
const form = this.getForm();
const onLoaded = () =>
{
this.$nextTick(() =>
{
this.setupSync(form);
});
};
if (form.loadingState === 'default')
{
onLoaded();
}
else
{
form.$on('loaded', onLoaded);
}
},
setupSync(form)
{
const meta = form.$parent.meta;
if (meta)
{
meta.canDelete = this.isBlueprint;
meta.canEdit = this.isBlueprint;
}
if (this.hasBlueprint)
{
const properties = this.getProperties(form);
properties.forEach(property =>
{
if (property.config.path !== 'blueprint')
{
//property.setBlock(BlueprintBlockComponent);
//property.setDisabled(true);
}
//property.$el.classList.add('is-property-locked');
//property.setLocked(true);
});
}
},
getForm()
{
let component = this.$parent;
do
{
if (component.$options.name === 'uiForm')
{
return component;
}
}
while (component = component.$parent);
return null;
},
// find all form properties
getProperties(form)
{
let find = 'uiEditorComponent';
let components = [];
// find components which can output errors
let traverseChildren = (parent) =>
{
parent.$children.forEach(component =>
{
if (component.$options.name === find)
{
components.push(component);
}
else
{
traverseChildren(component);
}
});
};
traverseChildren(form);
return components;
}
}
}
</script>
<style lang="scss">
.ui-property.is-property-locked
{
pointer-events: none;
opacity: .8;
}
/*.language .ui-property.has-block:after,
.mails .ui-property.has-block:after
{
position: absolute;
left: -13px;
top: -7px;
display: inline-flex;
justify-content: center;
align-items: center;
width: 32px;
height: 32px;
border-radius: 16px;
background: var(--color-box);
content: "\e887";
font-family: 'Feather';
font-size: 15px;
font-weight: 400;
color: var(--color-text-dim);
}*/
.blueprint
{
position: relative;
margin: 0 -32px 0;
padding: 0 32px 0;
margin-bottom: 30px;
//background: var(--color-box-nested);
border-top-left-radius: var(--radius);
border-top-right-radius: var(--radius);
aside
{
display: flex;
}
&.is-shared
{
//border-bottom: 1px dotted var(--color-accent-error);
}
}
.blueprint-box
{
display: flex;
justify-content: space-between;
align-items: center;
padding: var(--padding-s);
padding-left: var(--padding-m);
border-radius: var(--radius);
border: 1px dashed var(--color-line-dashed);
//background: repeating-linear-gradient(-45deg, transparent, transparent 2px, var(--color-bg-shade-2) 2px, var(--color-bg-shade-2) 4px);
}
.blueprint-button-settings
{
margin-right: -5px;
}
.blueprint-inner
{
display: grid;
grid-template-columns: auto minmax(0, 1fr);
grid-gap: var(--padding-m);
align-items: center;
font-size: var(--font-size);
line-height: 1.4;
position: relative;
padding-right: var(--padding-s);
p
{
margin: 0;
color: var(--color-text);
}
.ui-icon
{
color: var(--color-text);
margin-top: -2px;
}
}
</style>
<!--
<button type="button" class="ui-property-lock" v-if="locked" > <i class="fth-lock" > </i > </button >
/*.ui-property.is-locked
{
}*/
.ui-property-lock
{
display: inline-flex;
width: 20px;
height: 20px;
border-radius: 10px;
background: var(--color-button-light);
color: var(--color-text);
justify-content: center;
align-items: center;
font-size: 10px;
position: relative;
top: -1px;
margin-right: 8px;
}
-->
@@ -1,223 +0,0 @@
<template>
<ui-overlay-editor class="blueprint-settings">
<template v-slot:header>
<ui-header-bar title="Synchronisation" :back-button="false" :close-button="true" />
</template>
<template v-slot:footer>
<ui-button type="light onbg" label="@ui.close" @click="config.hide" />
<ui-button type="primary" label="@ui.confirm" @click="onSave" :state="state" />
</template>
<p class="blueprint-settings-text">By default all properties of your entity are synced with its blueprint.<br />You can disable synchronisation per property so it won't be overridden on changes.</p>
<div class="ui-box" v-if="loaded">
<ui-property class="blueprint-settings-tableheader" :key="-1" label="Property" :vertical="false">
<b>Synchronized</b>
</ui-property>
<ui-property v-for="(field, index) in items" v-if="!field.disabled" :key="index" :label="field.label" :description="field.description" :vertical="false" :class="{'not-synced': !field.synced}">
<ui-toggle v-model="field.synced" />
</ui-property>
</div>
</ui-overlay-editor>
</template>
<script>
import Arrays from 'zero/helpers/arrays.js';
import Localization from 'zero/helpers/localization.js';
import BlueprintApi from 'zero/api/blueprint.js';
export default {
props: {
model: Object,
config: Object,
blueprintConfig: Object
},
data: () => ({
state: 'default',
loaded: false,
editor: null,
items: []
}),
mounted()
{
this.blueprintConfig.fields.forEach(field =>
{
let item = JSON.parse(JSON.stringify(field));
item.synced = field.synced(this.model);
this.items.push(item);
});
this.loaded = true;
},
methods: {
onSave()
{
this.state = 'loading';
let desync = JSON.parse(JSON.stringify(this.model.blueprint.desync));
let resync = [];
this.items.forEach(field =>
{
let desynced = this.model.blueprint.desync.indexOf(field.path) > -1;
if (field.synced && desynced)
{
Arrays.remove(desync, field.path);
resync.push(field.path);
}
else if (!field.synced && !desynced)
{
desync.push(field.path);
}
});
this.model.blueprint.desync = desync;
// we need to revert changed values which were switch backed to synchronised state
// to do this we load the blueprint entity and its copy properties
if (resync.length > 0)
{
BlueprintApi.getById(this.model.blueprint.id).then(blueprint =>
{
this.config.confirm({
blueprint: this.model.blueprint,
update: entity =>
{
resync.forEach(path =>
{
entity[path] = blueprint[path]; // TODO does not work for nested paths
});
}
});
});
}
else
{
//this.state = 'success';
this.config.confirm({
blueprint: this.model.blueprint
});
}
},
//rebuildModel()
//{
// this.selector = Strings.selectorToArray(this.config.path);
// let currentValue = this.value;
// let found = false;
// if (!this.selector || !this.selector.length || !currentValue)
// {
// found = true;
// this.model = null;
// }
// else
// {
// for (var key of this.selector)
// {
// if (key in currentValue)
// {
// found = true;
// currentValue = currentValue[key];
// }
// else
// {
// break;
// }
// }
// this.model = found ? currentValue : null;
// }
//},
}
}
</script>
<style lang="scss">
.blueprint-settings-text
{
margin: 0 0 var(--padding);
line-height: 1.5;
}
.blueprint-settings-headline
{
margin: 0 0 var(--padding) !important;
}
.blueprint-settings .ui-property
{
display: flex;
justify-content: space-between;
}
.blueprint-settings .blueprint-settings-tableheader
{
border-bottom: 1px dashed var(--color-line-dashed);
padding-bottom: 20px;
margin-bottom: 26px;
b
{
display: inline-block;
margin-top: 3px;
}
}
.blueprint-settings .ui-property + .ui-property
{
margin-top: var(--padding-s);
padding-top: 0;
border-top: none;
}
.blueprint-settings .ui-property-content
{
display: inline;
flex: 0 0 auto;
}
.blueprint-settings .ui-property-label
{
padding-top: 1px;
}
.blueprint-settings .ui-property.not-synced .ui-property-label
{
font-weight: 400;
color: var(--color-text-dim);
}
.blueprint-settings-lock
{
color: var(--color-text-dim);
}
/*.blueprint-settings .ui-property.not-synced .ui-property-label:before
{
content: "\e929";
font-family: 'Feather';
margin-right: 0.8em;
color: var(--color-text-dim);
font-weight: 400;
}
.blueprint-settings .ui-property:not(.not-synced) .ui-property-label:before
{
content: "\e8f8";
font-family: 'Feather';
margin-right: 0.8em;
color: var(--color-primary);
font-weight: 400;
}*/
</style>
@@ -1,72 +0,0 @@
<template>
<div>
<ui-property v-if="value.id && value.lastModifiedDate" field="lastModifiedDate" label="@ui.modifiedDate" :is-text="true" :vertical="true">
<ui-date v-model="value.lastModifiedDate" />
</ui-property>
<ui-property v-if="value.id" label="@ui.createdDate" field="createdDate" :is-text="true" :vertical="true">
<ui-date v-model="value.createdDate" />
</ui-property>
</div>
</template>
<script>
export default {
name: 'uiEditorAside',
inject: [ 'meta' ],
props: {
editor: {
type: Object,
required: true
},
value: {
type: Object,
required: true
},
infos: {
type: String,
default: 'aside'
},
activeToggle: {
type: Boolean,
default: true
},
nested: {
type: Boolean,
default: false
},
isPage: {
type: Boolean,
default: false
},
disabled: {
type: [Boolean, Function],
default: false
}
},
data: () => ({
component: null
}),
created()
{
this.component = zero.overrides['editor-aside'] || null;
}
}
</script>
<style lang="scss">
.editor-aside
{
padding-top: var(--padding);
padding-left: var(--padding-s);
}
.editor-aside .ui-property + .ui-property
{
margin-top: var(--padding-m);
}
</style>
@@ -1,192 +0,0 @@
<template>
<ui-property v-if="!isHidden"
:field="config.path"
:label="label"
:hide-label="config.options.hideLabel"
:description="description"
:required="isRequired"
:disabled="isDisabled"
:vertical="config.options.vertical"
:class="{'is-disabled': isDisabled }"
:locked="isLocked"
:can-unlock="canUnlock || false"
@unlock="unlock"
@lock="lock">
<component :is="config.component" v-bind="config.componentOptions" :value="model" :entity="value" :meta="meta" @input="onChange" :disabled="isDisabled" />
<p v-if="config.options.helpText" class="ui-property-help" v-localize="config.options.helpText"></p>
</ui-property>
</template>
<script>
import Strings from 'zero/helpers/strings.js';
import Objects from 'zero/helpers/objects.js';
import Editor from 'zero/core/editor.ts';
import EditorField from 'zero/core/editor-field.ts';
import Localization from 'zero/helpers/localization.js';
import Overlay from 'zero/helpers/overlay.js';
import Arrays from 'zero/helpers/arrays.js';
export default {
name: 'uiEditorComponent',
inject: [ 'meta' ],
props: {
config: {
type: Object,
required: true
},
editor: {
type: Object,
required: true
},
value: {
type: Object,
required: true
},
disabled: {
type: Boolean,
default: false
},
},
watch: {
value: {
deep: true,
handler: function ()
{
this.rebuildModel();
}
}
},
data: () => ({
model: null,
loaded: false,
manualDisabled: false,
selector: null
}),
mounted()
{
this.rebuildModel();
this.loaded = true;
},
computed: {
isHidden()
{
return this.loaded && typeof this.config.options.condition === 'function' && !this.config.options.condition(this.value, this);
},
isRequired()
{
return typeof this.config.isRequired === 'function' ? this.config.isRequired(this.value) : this.config.isRequired;
},
isDisabled()
{
return this.manualDisabled || this.disabled || (typeof this.config.options.disabled === 'boolean' && this.config.options.disabled) || (typeof this.config.options.disabled === 'function' && this.config.options.disabled(this.value, this.model));
},
label()
{
return this.config.options.label || this.editor.templateLabel(this.config.path);
},
description()
{
return Localization.localize(this.config.options.description || this.editor.templateDescription(this.config.path), { hideEmpty: true });
},
isLocked()
{
return !this.editor.blueprint.unlocked(this.value, this.config);
},
canUnlock()
{
return this.editor.blueprint.canUnlock(this.value, this.config);
}
},
methods: {
rebuildModel()
{
this.selector = Strings.selectorToArray(this.config.path);
let currentValue = this.value;
let found = false;
if (!this.selector || !this.selector.length || !currentValue)
{
found = true;
this.model = null;
}
else
{
for (var key of this.selector)
{
if (key in currentValue)
{
found = true;
currentValue = currentValue[key];
}
else
{
break;
}
}
this.model = found ? currentValue : null;
}
},
onChange(value)
{
let oldValue = JSON.parse(JSON.stringify(this.model));
if (typeof value === 'function')
{
value(this.value);
}
else
{
Objects.setValue(this.value, this.selector, value);
}
this.$emit('input', this.value);
if (typeof this.config.options.onChange === 'function')
{
this.config.options.onChange(value, {
oldValue,
model: this.value,
component: this
});
}
},
setDisabled(disabled)
{
this.manualDisabled = disabled;
},
async unlock()
{
Overlay.confirm({
title: 'Unlock property',
text: 'Unlock this property to override the value passed by the blueprint',
confirmLabel: 'Confirm',
closeLabel: 'Cancel'
}).then(
async () => await this.editor.blueprint.unlock(this.value, this.config),
() => {}
);
},
async lock()
{
let originalValue = await this.editor.blueprint.lock(this.value, this.config);
this.onChange(originalValue);
}
}
}
</script>
@@ -1,75 +0,0 @@
<template>
<ui-form ref="form" v-slot="form" @submit="onSubmit" class="editor-form">
<ui-form-header v-if="rendered" v-model="model" prefix="@country.list" title="@country.name" :disabled="disabled" :is-create="!$route.params.id" :state="form.state" :can-delete="meta.canDelete" @delete="onDelete" />
<ui-editor v-if="rendered" :config="config" v-model="model" :meta="meta" :disabled="disabled" :on-configure="onLoad">
<template v-slot:below>
<ui-editor-infos v-model="model" :disabled="disabled" />
</template>
</ui-editor>
</ui-form>
</template>
<script>
import './editor.scss';
export default {
name: 'uiEditorForm',
props: {
config: {
type: [String, Object],
required: true
}
},
data: () => ({
meta: {},
model: {},
route: __zero.alias.settings.countries + '-edit',
disabled: false,
collection: null,
rendered: false
}),
mounted()
{
this.rendered = true;
},
methods: {
onLoad(editor)
{
this.collection = editor.editorConfig.collection;
if (!this.collection)
{
// TODO error
}
//this.$refs.form.load(!this.$route.params.id ? this.collection.getEmpty() : this.collection.getById(this.$route.params.id)).then(response =>
//{
// this.disabled = !response.meta.canEdit;
// this.meta = response.meta;
// this.model = response.entity;
//});
},
onSubmit(form)
{
form.handle(this.collection.save(this.model));
},
onDelete(item, opts)
{
opts.hide();
this.$refs.form.onDelete(this.collection.delete.bind(this, this.$route.params.id));
}
}
}
</script>
@@ -1,222 +0,0 @@
<template>
<ui-header-bar class="ui-editor-header" :back-button="true">
<template v-slot:title>
<h2 class="ui-header-bar-title" :class="{'is-empty': title && !value.name && !titleDisabled}">
<template v-for="prefix in prefixes">
<span class="-minor -prefix" v-localize:html="prefix"></span>
<ui-icon class="-chevron" symbol="fth-chevron-right" :size="14" />
</template>
<ui-error field="name" />
<input v-if="!titleDisabled" class="ui-form-header-title-input" type="text" v-model="value.name" v-localize:placeholder="title" :readonly="titleDisabled || disabled" />
<!--<ui-alias class="ui-form-header-title-alias" v-if="hasAlias" v-model="value.alias" :name="value.name" :disabled="disabled" />-->
<span v-if="titleDisabled" v-localize="forceTitle ? title : (value.name || title)"></span>
</h2>
</template>
<div class="ui-form-header-aside">
<slot></slot>
<div v-if="!activeDisabled && typeof value.isActive !== 'undefined'" class="ui-form-header-toggle">
<ui-toggle v-model="value.isActive" class="is-accent" off-content="@ui.inactive" :off-warning="true" on-content="@ui.active" :content-left="true" :disabled="disabled" />
</div>
<slot name="buttons"></slot>
<ui-dropdown v-if="actionsDefined" align="right">
<template v-slot:button>
<ui-button type="light onbg" label="@ui.actions" caret="down" />
</template>
<slot name="actions"></slot>
<ui-dropdown-button v-if="canDelete" label="@ui.delete" icon="fth-trash" @click="onDelete" :disabled="disabled" />
</ui-dropdown>
<ui-button :submit="true" type="accent" :label="isCreate ? '@ui.create' :'@ui.update'" :state="state" v-if="!disabled" class="ui-form-header-primary-button" />
</div>
</ui-header-bar>
</template>
<script>
export default {
name: 'uiEditorHeader',
props: {
title: {
type: String,
default: null
},
forceTitle: {
type: Boolean,
default: false
},
prefix: {
type: [String, Array]
},
value: {
type: Object
},
canDelete: {
type: Boolean,
default: true
},
disabled: {
type: Boolean,
default: false
},
titleDisabled: {
type: Boolean,
default: false
},
activeDisabled: {
type: Boolean,
default: false
},
state: {
type: String
},
isCreate: {
type: Boolean,
default: false
},
hasAlias: {
type: Boolean,
default: true
}
},
computed: {
actionsDefined()
{
return !this.isCreate && (this.canDelete || this.$scopedSlots.hasOwnProperty('actions'));
},
prefixes()
{
let items = Array.isArray(this.prefix) ? this.prefix : [this.prefix];
return items.filter(x => !!x);
}
},
methods: {
onDelete(item, opts)
{
this.$emit('delete', item, opts);
}
}
}
</script>
<style lang="scss">
.ui-editor-header
{
/* width: 100%;
max-width: 1320px;
margin: 0 auto;*/
}
.ui-editor-header-aside
{
display: flex;
align-items: center;
justify-content: flex-end;
> * + *
{
margin-left: var(--padding-s);
}
}
.ui-editor-header-toggle
{
display: inline-flex;
justify-content: center;
align-items: center;
position: relative;
top: -1px;
margin-left: var(--padding-s);
.ui-toggle-off-warning
{
display: none;
color: var(--color-accent-red);
}
.ui-toggle-switch
{
background: var(--color-button-light-onbg);
box-shadow: var(--shadow-short) !important;
}
.ui-toggle-switch.is-active
{
background: var(--color-toggled);
box-shadow: none !important;
}
input:focus + .ui-toggle-switch
{
border-color: transparent;
}
& + .ui-button
{
margin-left: var(--padding);
}
}
.ui-header-bar-title
{
position: relative;
.ui-error
{
display: none;
}
.ui-error + input[type="text"].ui-editor-header-title-input:not(:focus)
{
border-color: var(--color-accent-error);
}
}
input[type="text"].ui-editor-header-title-input
{
font-family: var(--font);
color: var(--color-text);
font-size: var(--font-size-l);
font-weight: 700;
background: none;
border: 1px dashed var(--color-line-dashed-onbg);
/*&:hover, &:focus, .ui-header-bar-title.is-empty &
{
border: 1px dashed var(--color-text-dim-one);
}*/
.-prefix + &
{
margin-left: 5px;
}
}
.ui-editor-header-title-alias
{
position: absolute;
right: 10px;
top: 11px;
z-index: 2;
.ui-alias-lock
{
background: none;
}
}
.ui-editor-header-info-button
{
padding: 0;
justify-content: center;
width: 48px;
text-align: center;
}
.ui-editor-header-info-button .ui-button-icon
{
margin: 0 !important;
font-size: 18px;
}
</style>
@@ -1,47 +0,0 @@
<template>
<div v-if="visible" class="editor-infos">
<div class="ui-box is-light editor-infos-aside">
<slot name="before"></slot>
<template v-if="value && value.id">
<ui-property v-if="value.lastModifiedDate" field="lastModifiedDate" label="@ui.modifiedDate">
<ui-date v-model="value.lastModifiedDate" />
</ui-property>
<ui-property label="@ui.createdDate" field="createdDate">
<ui-date v-model="value.createdDate" />
</ui-property>
<ui-property label="@ui.entityfields.alias" field="alias">
{{value.alias}}
</ui-property>
<ui-property label="@ui.entityfields.sort" field="sort">
{{value.sort}}
</ui-property>
<ui-property v-if="value.key" label="@ui.entityfields.key" field="key">
{{value.key}}
</ui-property>
</template>
<slot name="after"></slot>
</div>
</div>
</template>
<script>
export default {
props: {
value: {
type: [Object, Array]
},
disabled: {
type: Boolean,
default: false
}
},
computed: {
visible()
{
return (this.value && this.value.id) || this.$scopedSlots.hasOwnProperty('before') || this.$scopedSlots.hasOwnProperty('after');
}
}
}
</script>
@@ -1,156 +0,0 @@
.editor > .ui-view-box
{
padding-top: 0;
}
.editor
{
.ui-tabs-list
{
padding-top: 0;
}
.ui-tab > h3
{
border-bottom: 1px solid var(--color-line);
padding-bottom: var(--padding-m);
margin-bottom: var(--padding);
}
}
.editor-outer
{
width: 100%;
max-width: 1320px;
margin: 0 auto;
&.-infos-aside:not(.is-page)
{
display: grid;
//grid-template-columns: 1fr 340px var(--padding);
align-items: flex-start;
}
&:not(.has-tabs)
{
.ui-tabs-list
{
display: none;
}
.ui-tab, .editor-infos
{
margin-top: 0;
}
}
}
.editor-infos
{
margin: 61px 0 0;
position: sticky;
top: var(--padding);
.ui-box
{
margin-left: 0;
margin-right: 0;
margin-bottom: 0;
}
.ui-box:first-child
{
margin-top: 0;
}
.ui-box + .ui-box
{
margin-top: 16px;
}
.ui-box + .ui-box.is-light
{
margin-top: 1px;
border-top-left-radius: 0;
border-top-right-radius: 0;
}
.ui-property + .ui-property
{
margin-top: 15px;
padding-top: 10px;
border-top: none;
}
.is-toggle
{
align-items: center;
}
.ui-property-label
{
width: auto;
padding-right: 20px;
}
.ui-property-content
{
flex: 0 1 auto;
}
.ui-property
{
display: flex;
justify-content: space-between;
}
}
.editor-active-toggle
{
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
/*.editor-active-toggle.is-active
{
background: var(--color-accent-info-bg);
.ui-property-label
{
color: var(--color-accent-info);
}
}*/
.editor-error
{
background: transparent;
min-height: calc(100vh - 100px);
}
/// tabs as boxes
.editor-outer.as-boxes
{
.ui-tabs-list
{
display: none !important;
}
.ui-tab
{
display: block !important;
}
.ui-tab:first-child
{
margin-top: 0;
}
.editor-infos
{
margin-top: 0;
}
}
@@ -1,82 +0,0 @@
<template>
<ui-form ref="form" v-slot="form" @submit="onSubmit" @load="onLoad">
<ui-overlay-editor class="ui-editor-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="@ui.close" @click="config.hide"></ui-button>
<ui-button v-if="!disabled" type="primary" :submit="true" :label="config.confirmButton || '@ui.confirm'" :state="form.state" :disabled="loading"></ui-button>
</template>
<ui-loading v-if="loading" :is-big="true" />
<div v-if="!loading" class="ui-editor-overlay-editor">
<ui-editor :config="editor" v-model="model" :meta="meta" :is-page="false" infos="none" :disabled="disabled" />
</div>
</ui-overlay-editor>
</ui-form>
</template>
<script>
import UiEditor from 'zero/editor/editor.vue';
export default {
props: {
config: Object
},
components: { UiEditor },
data: () => ({
isAdd: true,
disabled: false,
id: null,
loading: true,
state: 'default',
parent: null,
editor: null,
meta: {},
model: {}
}),
methods: {
onLoad(form)
{
this.isAdd = this.config.create === true;
this.meta = {
parentModel: this.config.parentModel
};
this.model = JSON.parse(JSON.stringify(this.config.model));
this.editor = this.config.editor;
this.loading = false;
},
onSubmit(form)
{
if (typeof this.config.confirmGuard === 'function')
{
form.setState('loading');
this.config.confirmGuard(this.model).then(res =>
{
form.setState('success');
form.setDirty(false);
this.config.confirm(this.model, res);
});
}
else
{
this.config.confirm(this.model);
}
}
}
}
</script>
@@ -1,156 +0,0 @@
.editor
{
width: 100%;
max-width: 960px;
margin: 0 auto;
padding: 0 var(--padding) var(--padding);
&:not(.display-tabs), &.hide-tabs
{
.ui-tabs-list
{
display: none;
}
.ui-tab, .editor-infos
{
margin-top: 0;
}
.editor-aside
{
margin-top: 0;
}
}
&.has-sidebar
{
max-width: 1580px;
display: grid;
grid-template-columns: minmax(auto, 1fr) 260px;
grid-gap: 0 var(--padding);
}
}
.editor.has-below .ui-tab.ui-box
{
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
.editor.display-boxes .ui-tab
{
display: inherit !important;
}
.editor.display-boxes .ui-tab + .ui-tab
{
margin-top: var(--padding-m);
}
.editor-tab-headline
{
font-size: var(--font-size-l) !important;
font-weight: 900 !important;
margin-bottom: var(--padding) !important;
}
.editor-tabs
{
.ui-tabs-list
{
padding: 0;
margin-bottom: 0;
}
.ui-tab
{
margin: 0;
}
}
.editor-aside
{
margin-top: 58px;
}
.editor-infos
{
display: block;
//border-top: 1px solid var(--color-line-onbg);
}
.theme-dark .editor-infos
{
border-top: none;
}
.editor-infos .ui-box
{
margin: 0;
border-top-left-radius: 0;
border-top-right-radius: 0;
display: flex;
gap: var(--padding-l);
}
.editor-infos .ui-property
{
flex-direction: column;
border-top: none;
margin: 0;
padding: 0;
grid-gap: 6px !important;
}
.editor-infos .ui-property-label
{
font-size: var(--font-size-s);
font-weight: 400;
color: var(--color-text-dim);
width: 100%;
padding-right: 0;
}
.editor-infos .ui-property-content
{
font-size: var(--font-size-s);
font-weight: 400;
color: var(--color-text-dim);
}
.editor-infos-aside .ui-property + .ui-property
{
margin-top: 0;
padding-top: 0;
border-top: none;
}
.editor-aside-links
{
display: flex;
width: 100%;
margin-bottom: -5px;
}
.editor-aside-links .ui-link
{
color: var(--color-text-dim);
font-size: var(--font-size-s);
text-decoration: none !important;
position: relative;
}
.editor-aside-links .ui-link + .ui-link
{
margin-left: 20px;
}
.editor-aside-links .ui-link + .ui-link:before
{
content: '·';
position: absolute;
left: -12px;
top: -1px;
}
@@ -1,129 +0,0 @@
<template>
<div class="editor-outer" v-if="loaded">
<header class="editor-above" v-if="aboveDefined">
<slot name="above" v-bind:config="editorConfig"></slot>
</header>
<div class="editor" :class="['display-' + display, { 'has-sidebar': asideDefined, 'hide-tabs': tabs.length < 2, 'has-below': belowDefined }]">
<ui-tabs class="editor-tabs">
<ui-tab v-if="!tab.disabled(value)" v-for="(tab, index) in tabs" class="ui-box" :class="tab.class" :label="tab.name" :count="tab.count(value)" :key="index">
<h3 v-if="display == 'boxes' && tab.name" class="ui-headline editor-tab-headline" v-localize="tab.name"></h3>
<slot name="blueprint">
<blueprint-property v-if="value && editorConfig.blueprint" :value="value" :meta="meta" :config="editorConfig.blueprint" />
</slot>
<div class="ui-property ui-property-parent" v-for="fieldset in tab.fieldsets">
<editor-component v-for="(field, fieldIndex) in fieldset.fields" :disabled="disabled" :key="fieldIndex" :config="field" @input="onChange" :editor="editorConfig" :value="value"
:class="field.options.class" :data-cols="!!field.options.fieldset" :style="{ 'grid-column': field.options.fieldset ? 'span ' + field.options.fieldsetColumns : null }" />
</div>
<component v-if="tab.component" :is="tab.component" v-model="value" />
</ui-tab>
</ui-tabs>
<aside class="editor-aside" v-if="asideDefined">
<slot name="aside"></slot>
</aside>
<aside class="editor-below" v-if="belowDefined">
<slot name="below"></slot>
</aside>
</div>
</div>
</template>
<script>
import './editor.scss';
import EditorComponent from 'zero/editor/editor-component.vue';
import EditorAside from './editor-aside.vue';
import { createBlueprintConfig } from 'zero/core/editor-blueprint.ts';
import BlueprintProperty from './blueprint/property.vue';
import Localization from 'zero/helpers/localization.js';
export default {
name: 'uiEditor',
provide: function ()
{
return {
meta: this.meta,
editor: this.config
};
},
props: {
config: {
type: [String, Object],
required: true
},
meta: {
type: Object,
default: () => { }
},
disabled: {
type: Boolean,
default: false
},
value: {
type: Object
},
onConfigure: {
type: Function,
default: () => { }
},
},
components: { EditorComponent, EditorAside, BlueprintProperty },
data: () => ({
editorConfig: {},
loaded: false,
tabs: [],
currentFieldset: null
}),
computed: {
aboveDefined()
{
return this.$scopedSlots.hasOwnProperty('above');
},
asideDefined()
{
return this.$scopedSlots.hasOwnProperty('aside');
},
belowDefined()
{
return this.$scopedSlots.hasOwnProperty('below');
}
},
created()
{
this.editorConfig = typeof this.config === 'string' ? this.zero.getEditor(this.config) : this.config;
this.editorConfig.blueprint = createBlueprintConfig(this.zero, this.editorConfig, this.value);
this.tabs = this.editorConfig.tabs.map(tab =>
{
let fieldsets = this.editorConfig.getFieldsets(tab);
return {
...tab,
count: value => typeof tab.count === 'number' ? tab.count : (typeof tab.count === 'function' ? tab.count(value) : 0),
disabled: value => typeof tab.disabled === 'boolean' ? tab.disabled : (typeof tab.disabled === 'function' ? tab.disabled(value) : false),
fieldsets
};
});
this.display = this.editorConfig.options.display || 'tabs';
this.onConfigure(this);
this.loaded = true;
},
methods: {
onChange()
{
this.$emit('input', this.value);
}
}
}
</script>
@@ -1,31 +0,0 @@
<template>
<ui-alias :value="value" :name="entity.name" @input="$emit('input', $event)" :max-length="maxLength" :disabled="disabled" />
</template>
<script>
export default {
props: {
value: {
type: String,
default: null
},
entity: {
type: Object,
required: true
},
config: Object,
disabled: {
type: Boolean,
default: false
}
},
computed: {
maxLength()
{
return this.config.maxLength > 0 ? this.config.maxLength : null;
}
}
}
</script>
@@ -1,41 +0,0 @@
<template>
<ui-check-list :value="value" @input="$emit('input', $event)"
:reverse="reverse"
:items="items"
:limit="limit"
:label-key="labelKey"
:id-key="idKey"
:disabled="disabled" />
</template>
<script>
export default {
props: {
value: {
type: Array,
default: () => []
},
disabled: {
type: Boolean,
default: false
},
items: {
type: [Array, Function, Promise],
required: true
},
limit: {
type: Number,
default: 100
},
reverse: Boolean,
labelKey: {
type: String,
default: 'value'
},
idKey: {
type: String,
default: 'key'
}
},
}
</script>
@@ -1,19 +0,0 @@
<template>
<ui-colorpicker :value="value" @input="$emit('input', $event)" :disabled="disabled" />
</template>
<script>
export default {
props: {
value: {
type: String
},
disabled: {
type: Boolean,
default: false
},
config: Object
}
}
</script>
@@ -1,17 +0,0 @@
<template>
<ui-countrypicker :value="value" @input="$emit('input', $event)" :limit="limit" :disabled="disabled" />
</template>
<script>
export default {
props: {
value: [String, Array],
disabled: Boolean,
limit: {
type: Number,
default: 1
}
}
}
</script>
@@ -1,39 +0,0 @@
<template>
<div v-if="!loading" class="ui-native-select" :disabled="disabled">
<select :value="value" @input="$emit('input', $event.target.value)" :disabled="disabled">
<option v-for="item in items" :value="item.code">{{item.name}}</option>
</select>
</div>
</template>
<script>
import LanguagesApi from 'zero/api/languages.js';
export default {
props: {
value: {
type: String
},
config: Object,
disabled: {
type: Boolean,
default: false
}
},
data: () => ({
loading: true,
items: []
}),
mounted()
{
LanguagesApi.getSupportedCultures().then(res =>
{
this.items = res;
this.loading = false;
});
}
}
</script>
@@ -1,23 +0,0 @@
<template>
<div class="ui-box">
<input :value="value" @input="$emit('input', +$event.target.value)" type="text" class="ui-input" v-placeholder="{ placeholder, model: entity }" :disabled="disabled" />
</div>
</template>
<script>
export default {
props: {
value: {
type: Number,
default: null
},
disabled: {
type: Boolean,
default: false
},
placeholder: String,
entity: Object
},
}
</script>
@@ -1,45 +0,0 @@
<template>
<div class="ui-box">
<ui-datepicker :value="value" @input="$emit('input', $event)" :disabled="disabled"
v-bind="{ format, time, maxDate, minDate, amPm }" />
</div>
</template>
<script>
export default {
props: {
value: {
type: [String, Date],
default: {
from: null,
to: null
}
},
format: {
type: String,
default: null
},
time: {
type: Boolean,
default: false,
},
maxDate: {
type: [String, Date],
default: null
},
minDate: {
type: [String, Date],
default: null
},
amPm: {
type: Boolean,
default: false
},
disabled: {
type: Boolean,
default: false
}
}
}
</script>
@@ -1,57 +0,0 @@
<template>
<div class="ui-box">
<ui-daterangepicker :value="value" @input="$emit('input', $event)" :disabled="disabled"
v-bind="{ format, time, maxDate, minDate, fromLabel, toLabel, amPm, inline }" />
</div>
</template>
<script>
export default {
props: {
value: {
type: Object,
default: {
from: null,
to: null
}
},
format: {
type: String,
default: null
},
time: {
type: Boolean,
default: false,
},
maxDate: {
type: [String, Date],
default: null
},
minDate: {
type: [String, Date],
default: null
},
fromLabel: {
type: String,
default: '@ui.date.range_from'
},
toLabel: {
type: String,
default: '@ui.date.range_to'
},
amPm: {
type: Boolean,
default: false
},
inline: {
type: Boolean,
default: false
},
disabled: {
type: Boolean,
default: false
}
}
}
</script>
@@ -1,23 +0,0 @@
<template>
<ui-iconpicker :value="value" @input="$emit('input', $event)" :disabled="disabled" :set="set" />
</template>
<script>
export default {
props: {
value: {
type: String
},
disabled: {
type: Boolean,
default: false
},
set: {
type: String,
default: 'feather'
},
config: Object
}
}
</script>
@@ -1,32 +0,0 @@
<template>
<ui-input-list :value="value" @input="$emit('input', $event)" :add-label="addLabel" :disabled="disabled"
:max-items="limit"
:inline="true" :max-length="maxItemLength" />
</template>
<script>
export default {
props: {
value: {
type: Array,
default: () => []
},
disabled: {
type: Boolean,
default: false
},
limit: {
type: Number,
default: 10
},
maxItemLength: {
type: Number,
default: 10
},
addLabel: {
type: String,
default: '@ui.add'
}
},
}
</script>
@@ -1,47 +0,0 @@
<template>
<div class="ui-native-select" :disabled="disabled">
<select :value="value" @input="onChange($event)" :disabled="disabled">
<option :value="null"></option>
<option v-for="item in items" :value="item.id">{{item.name}}</option>
</select>
</div>
</template>
<script>
import LanguagesApi from 'zero/api/languages.js';
export default {
props: {
value: {
type: String
},
config: Object,
disabled: {
type: Boolean,
default: false
}
},
data: () => ({
items: []
}),
mounted()
{
LanguagesApi.getForPicker().then(res =>
{
this.items = res;
});
},
methods: {
onChange(e)
{
this.$emit('input', e.target.value || null);
}
}
}
</script>
@@ -1,43 +0,0 @@
<!--<template>
<input :value="url" @input="onChange($event.target.value)" type="text" class="ui-input" :disabled="disabled" />
</template>
<script>
export default {
props: {
value: {
type: Object,
default: {
providerAlias: null,
target: 'default',
urlSuffix: null,
label: null,
title: null,
values: {
url: null
}
}
},
disabled: {
type: Boolean,
default: false
}
},
data: () => ({
url: null
}),
methods: {
onChange(value)
{
this.$emit('input', {
values: {
url: value
}
});
}
}
}
</script>-->
@@ -1,44 +0,0 @@
<template>
<ui-linkpicker :config="config" :value="value" @input="$emit('input', $event)" :disabled="disabled" v-bind="{ disabled, limit, title, label, target, suffix, areas }" />
</template>
<script>
export default {
props: {
value: {
type: [Object, Array],
default: null
},
disabled: {
type: Boolean,
default: false
},
limit: {
type: Number,
default: 1
},
title: {
type: Boolean,
default: true
},
label: {
type: Boolean,
default: false
},
target: {
type: Boolean,
default: true
},
suffix: {
type: Boolean,
default: false
},
areas: {
type: Array,
default: () => []
},
config: Object
},
}
</script>
@@ -1,17 +0,0 @@
<template>
<ui-mailtemplatepicker :value="value" @input="$emit('input', $event)" :limit="limit" :disabled="disabled" />
</template>
<script>
export default {
props: {
value: [String, Array],
disabled: Boolean,
limit: {
type: Number,
default: 1
}
}
}
</script>
@@ -1,43 +0,0 @@
<template>
<ui-mediapicker :config="{ limit, disallowSelect, disallowUpload, fileExtensions, maxFileSize, shared: meta.isShared }" :value="value" @input="$emit('input', $event)" :disabled="disabled" />
</template>
<script>
export default {
name: 'uiMediaField',
props: {
value: {
type: [String, Array],
default: null
},
disabled: {
type: Boolean,
default: false
},
limit: {
type: Number,
default: 1
},
disallowSelect: {
type: Boolean,
default: false
},
disallowUpload: {
type: Boolean,
default: false
},
fileExtensions: {
type: Array,
default: null
},
maxFileSize: {
type: Number,
default: 10
},
config: Object,
meta: Object
}
}
</script>
@@ -1,23 +0,0 @@
<template>
<ui-modules :value="value" @input="$emit('input', $event)" :config="config" :tags="tags" :disabled="disabled" />
</template>
<script>
export default {
props: {
value: {
type: [String, Object, Array]
},
disabled: {
type: Boolean,
default: false
},
tags: {
type: Array,
default: () => []
},
config: Object
}
}
</script>
@@ -1,187 +0,0 @@
<template>
<div class="editor-nested" :depth="depth">
<div class="ui-pick-previews" v-if="items.length" v-sortable="{ onUpdate: onSortingUpdated }">
<div v-for="(item, index) in items" :key="item.id" class="ui-pick-preview">
<ui-select-button :icon="getIcon(item)" :label="getName(item)" :description="getDescription(item)" :disabled="disabled" @click="editItem(item)" />
<ui-icon-button v-if="!disabled" icon="fth-x" title="@ui.close" @click="removeItem(index)" :disabled="disabled" :size="14" />
</div>
</div>
<ui-select-button v-if="limit > items.length" icon="fth-plus" :label="addLabel || '@ui.add'" @click="addItem" :disabled="disabled" />
</div>
</template>
<script>
import UiEditor from 'zero/editor/editor.vue';
import UiEditorOverlay from 'zero/editor/editor-overlay.vue';
import Overlay from 'zero/helpers/overlay.js';
import Editor from 'zero/core/editor.ts';
import Strings from 'zero/helpers/strings.js';
import Arrays from 'zero/helpers/arrays.js';
export default {
name: 'UiEditorFieldNested',
components: { UiEditor },
props: {
value: [Array, Object],
meta: Object,
depth: Number,
disabled: Boolean,
editor: {
type: Object,
required: true
},
entity: Object,
limit: {
type: Number,
default: 100
},
width: {
type: Number,
default: 820
},
title: String,
addLabel: String,
itemLabel: Function,
itemDescription: Function,
itemIcon: [String, Function],
template: {
type: Object,
required: true
}
},
watch: {
value: {
deep: true,
handler(val)
{
this.setup(val);
}
}
},
data: () => ({
items: [],
multiple: false
}),
mounted()
{
this.setup(this.value);
},
methods: {
setup(value)
{
this.items = JSON.parse(JSON.stringify(value)) || [];
this.multiple = this.limit > 1;
if (!this.multiple)
{
this.items = this.items ? [this.items] : [];
}
},
getNewItem()
{
return JSON.parse(JSON.stringify(this.template || {}));
// TODO we need to set a default ID here so we can sort based on this v-for key.
// the problem is we don't know if the object has an ID nor how long it should be.
// It is only generated on the server so we don't have access to it yet.
// the v-for key is necessary so the v-sortable works and correctly propagates changes, d'oh.
},
addItem()
{
if (this.limit <= this.items.length)
{
return;
}
this.editItem(this.getNewItem(), true);
this.onChange();
},
editItem(item, isAdd)
{
let parentModel = JSON.parse(JSON.stringify(this.entity));
if (this.meta && this.meta.parentModel)
{
parentModel.parentModel = this.meta.parentModel;
}
// open editing overlay
return Overlay.open({
component: UiEditorOverlay,
display: 'editor',
editor: this.editor,
title: this.title || '@ui.edit.title',
model: item,
width: this.width,
parentModel,
create: isAdd
}).then(value =>
{
if (isAdd)
{
this.items.push(value);
}
else
{
const index = this.items.indexOf(item);
this.removeItem(index);
this.items.splice(index, 0, value);
}
this.onChange();
});
},
removeItem(index)
{
this.items.splice(index, 1);
this.onChange();
},
onChange()
{
this.$emit('input', this.multiple ? this.items : (this.items.length > 0 ? this.items[0] : null));
},
getName(item)
{
let name = typeof this.itemLabel === 'function' ? this.itemLabel(item) : null;
return Strings.htmlToText(name ?? '@ui.item');
},
getDescription(item)
{
return Strings.htmlToText(typeof this.itemDescription === 'function' ? this.itemDescription(item) : '');
},
getIcon(item)
{
return typeof this.itemIcon === 'function' ? this.itemIcon(item) : this.itemIcon;
},
onSortingUpdated(ev)
{
this.items = Arrays.move(this.items, ev.oldIndex, ev.newIndex);
this.onChange();
//this.$emit('input', this.multiple ? result : (result.length > 0 ? result[0] : null));
},
}
}
</script>
@@ -1,30 +0,0 @@
<template>
<input :value="value" @input="onChange($event.target.value)" type="text" class="ui-input" v-placeholder="{ placeholder, model: entity }" :maxlength="maxLength" :disabled="disabled" />
</template>
<script>
export default {
props: {
value: Number,
maxLength: Number,
placeholder: [String, Function],
disabled: Boolean,
entity: Object
},
methods: {
onChange(value)
{
var parsedValue = parseFloat(value);
if (isNaN(parsedValue))
{
return;
}
this.$emit('input', parsedValue);
}
}
}
</script>
@@ -1,20 +0,0 @@
<template>
<span class="ui-property-output" v-localize="output"></span>
</template>
<script>
export default {
props: {
value: [Object, String, Array, Number, Boolean],
entity: Object,
render: Function
},
computed: {
output()
{
return typeof this.render === 'function' ? this.render(this.value, this.entity) : this.value;
}
}
}
</script>
@@ -1,24 +0,0 @@
<template>
<ui-pagepicker :config="config" :value="value" @input="$emit('input', $event)" :disabled="disabled" :limit="1" />
</template>
<script>
export default {
props: {
value: {
type: [String, Array],
default: null
},
disabled: {
type: Boolean,
default: false
},
limit: {
type: Number,
default: 1
},
config: Object
},
}
</script>
@@ -1,54 +0,0 @@
<template>
<div class="ui-password-hash">
<input value="******************" type="text" class="ui-input" readonly="readonly" disabled="disabled" />
<ui-button v-if="!disabled" label="change" type="light" @click="open" />
</div>
</template>
<script>
import Overlay from 'zero/helpers/overlay.js';
import PasswordChangeOverlay from 'zero/components/overlays/password-change.vue';
export default {
props: {
value: {
type: String,
default: null
},
disabled: {
type: Boolean,
default: false
},
placeholder: {
type: [String, Function],
default: null
},
entity: Object
},
methods: {
open()
{
return Overlay.open({
component: PasswordChangeOverlay
}).then(hash =>
{
this.$emit('input', hash);
});
}
}
}
</script>
<style lang="scss">
.ui-password-hash
{
display: grid;
grid-template-columns: 1fr auto;
grid-gap: var(--padding-xxs);
}
</style>
@@ -1,28 +0,0 @@
<template>
<input :value="value" @input="$emit('input', $event.target.value)" type="password" class="ui-input" v-placeholder="{ placeholder, model: entity }" :maxlength="maxLength" :disabled="disabled" />
</template>
<script>
export default {
props: {
value: {
type: String,
default: null
},
maxLength: {
type: Number,
default: null
},
disabled: {
type: Boolean,
default: false
},
placeholder: {
type: String,
default: null
},
entity: Object
}
}
</script>
@@ -1,32 +0,0 @@
<template>
<ui-rte :value="value" @input="$emit('input', $event)" :disabled="disabled" v-bind="{ maxLength, placeholder, setup }" />
</template>
<script>
export default {
props: {
value: {
type: String,
default: null
},
disabled: {
type: Boolean,
default: false
},
maxLength: {
type: Number,
default: null
},
placeholder: {
type: String,
default: null
},
setup: {
type: Function,
default: () => { }
},
config: Object
}
}
</script>
@@ -1,82 +0,0 @@
<template>
<div class="ui-native-select" :disabled="disabled">
<select :value="value" @input="onChange" :disabled="disabled">
<option v-if="emptyOption"></option>
<option v-for="option in options" :value="option.key" v-localize="option.value"></option>
</select>
</div>
</template>
<script>
export default {
props: {
value: [String, Number, Object],
items: [Array, Function],
entity: {
type: Object,
required: true
},
disabled: Boolean,
emptyOption: {
type: Boolean,
default: false
}
},
data: () => ({
options: []
}),
created()
{
this.rebuild();
},
watch: {
items: {
deep: true,
handler()
{
this.rebuild();
this.onChange({ target: { value: this.value } });
}
},
entity: {
deep: true,
handler()
{
this.rebuild();
this.onChange({ target: { value: this.value } });
}
}
},
methods: {
rebuild()
{
let items = [];
if (!this.entity || !this.items)
{
this.options = items;
return;
}
if (typeof this.items === 'function')
{
this.options = this.items(this.entity);
}
else
{
this.options = [...this.items];
}
},
onChange(ev)
{
this.$emit('input', ev.target.value ? ev.target.value : null);
}
}
}
</script>
@@ -1,17 +0,0 @@
<template>
<ui-spacepicker :value="value" @input="$emit('input', $event)" :limit="limit" :disabled="disabled" />
</template>
<script>
export default {
props: {
value: [String, Array],
disabled: Boolean,
limit: {
type: Number,
default: 1
}
}
}
</script>
@@ -1,19 +0,0 @@
<template>
<ui-state-button :items="items" :value="value" @input="$emit('input', $event)" :disabled="disabled" />
</template>
<script>
export default {
props: {
value: {
type: [ String, Number ]
},
disabled: {
type: Boolean,
default: false
},
items: [Array, Function]
}
}
</script>
@@ -1,11 +0,0 @@
<template>
<hr style="margin: 50px 0;" />
</template>
<script>
export default {
props: {
config: Object
}
}
</script>
@@ -1,23 +0,0 @@
<template>
<ui-tags :value="value" @input="$emit('input', $event)" :max-items="limit" :max-length="maxItemLength" :disabled="disabled" />
</template>
<script>
export default {
props: {
value: {
type: Array,
default: () => []
},
disabled: Boolean,
limit: {
type: Number,
default: 10
},
maxItemLength: {
type: Number,
default: 200
}
},
}
</script>
@@ -1,28 +0,0 @@
<template>
<input :value="value" @input="$emit('input', $event.target.value)" type="text" class="ui-input" v-placeholder="{ placeholder, model: entity }" :maxlength="maxLength" :disabled="disabled" />
</template>
<script>
export default {
props: {
value: {
type: String,
default: null
},
maxLength: {
type: Number,
default: null
},
disabled: {
type: Boolean,
default: false
},
placeholder: {
type: [String, Function],
default: null
},
entity: Object
}
}
</script>
@@ -1,19 +0,0 @@
<template>
<textarea class="ui-textarea" :value="value" @input="$emit('input', $event.target.value)" rows="3" :disabled="disabled"></textarea>
</template>
<script>
export default {
props: {
value: {
type: String
},
disabled: {
type: Boolean,
default: false
},
config: Object
},
}
</script>
@@ -1,16 +0,0 @@
<template>
<ui-toggle :value="value" @input="$emit('input', $event)" v-bind="{ disabled, negative, onContent, offContent }" />
</template>
<script>
export default {
props: {
value: Boolean,
disabled: Boolean,
negative: Boolean,
onContent: String,
offContent: String
}
}
</script>
@@ -1,28 +0,0 @@
<template>
<video-picker :value="value" @input="$emit('input', $event)" :disabled="disabled" v-bind="{ disabled, limit }" title="Select a video" />
</template>
<script>
import VideoPicker from 'zero/components/pickers/videoPicker/videopicker.vue';
export default {
props: {
value: {
type: [Object, Array],
default: null
},
limit: {
type: Number,
default: 1
},
disabled: {
type: Boolean,
default: false
},
config: Object
},
components: { VideoPicker }
}
</script>
+1
View File
@@ -17,6 +17,7 @@
"underscore": "^1.13.1",
"vue": "^3.2.24",
"vue-router": "^4.0.12",
"@vue/compat": "^3.2.24",
"mitt": "^3.0.0",
"pinia": "^2.0.6"
},
+14 -1
View File
@@ -67,7 +67,20 @@ let config = defineConfig({
}
}
},
plugins: [vue(), ...zeroPlugins],
resolve: {
alias: {
vue: '@vue/compat'
}
},
plugins: [vue({
template: {
compilerOptions: {
compatConfig: {
MODE: 2
}
}
}
}), ...zeroPlugins],
build: {
manifest: true,
outDir: 'dist/zero',
@@ -0,0 +1,19 @@
namespace zero.Backoffice.Endpoints.Applications;
public class ApplicationMapperProfile : ZeroMapperProfile
{
public override void Configure(IZeroMapper mapper)
{
mapper.Define<Application, ApplicationPresentation>((source, ctx) => new(), Map);
}
protected virtual void Map(Application source, ApplicationPresentation target, IZeroMapperContext ctx)
{
target.Id = source.Id;
target.Alias = source.Alias;
target.Name = source.Name;
target.ImageId = source.IconId.Or(source.ImageId);
target.IsActive = source.IsActive;
}
}
@@ -0,0 +1,12 @@
namespace zero.Backoffice.Endpoints.Applications;
public class ApplicationPresentation : ZeroIdEntity
{
public string Alias { get; set; }
public string Name { get; set; }
public bool IsActive { get; set; }
public string ImageId { get; set; }
}
@@ -0,0 +1,22 @@
using Microsoft.AspNetCore.Mvc;
using zero.Api.Filters;
namespace zero.Backoffice.Endpoints.Applications;
[ZeroSystemApi]
public class ApplicationsController : ZeroBackofficeController
{
readonly IApplicationStore Store;
public ApplicationsController(IApplicationStore store)
{
Store = store;
}
[HttpGet("")]
public virtual async Task<ActionResult<Paged>> Get()
{
Paged<Application> result = await Store.Load(1, 100);
return Mapper.Map<Application, ApplicationPresentation>(result);
}
}
@@ -1,6 +1,5 @@
using Microsoft.AspNetCore.Mvc;
using System.Collections;
using zero.Api.Endpoints.Applications;
using zero.Backoffice.Services;
namespace zero.Backoffice.Endpoints.UI;
+1 -1
View File
@@ -1,4 +1,4 @@
//using zero.Api.Endpoints.Applications;
//using zero.Backoffice.Endpoints.Applications;
//namespace zero.Backoffice.Endpoints.UI;
+2
View File
@@ -6,6 +6,7 @@ using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;
using System.IO;
using zero.Backoffice.Endpoints.Account;
using zero.Backoffice.Endpoints.Applications;
using zero.Backoffice.Services;
namespace zero.Backoffice;
@@ -29,6 +30,7 @@ public class ZeroBackofficePlugin : ZeroPlugin
services.AddHostedService<ZeroDevService>();
services.AddTransient<IZeroVue, ZeroVue>();
services.AddSingleton<IMapperProfile, AccountMapperProfile>();
services.AddSingleton<IMapperProfile, ApplicationMapperProfile>();
services.AddSingleton<IIconService, IconService>();
services.AddSingleton<IResourceService, ResourceService>();
@@ -19,13 +19,6 @@
<body class="theme-light theme-rounded">
<div id="app"></div>
<svg xmlns="http://www.w3.org/2000/svg" style="display: none;">{svg}</svg>
<script id="zeroconfig" type="application/json">{config}</script>
<script>
window.__zero = JSON.parse(document.getElementById('zeroconfig').innerHTML);
window.zero = window.__zero;
</script>
{js}
</body>
@@ -16,7 +16,7 @@ public class ZeroBackofficeControllerModelConvention : ZeroApiControllerModelCon
readonly bool RuntimeIsAppAware = false;
public ZeroBackofficeControllerModelConvention(string zeroPath, string backofficeApiPath = "backoffice", bool isAppAware = false) : base(zeroPath, backofficeApiPath, isAppAware)
public ZeroBackofficeControllerModelConvention(string zeroPath, string backofficeApiPath = "api/backoffice", bool isAppAware = false) : base(zeroPath, backofficeApiPath, isAppAware)
{
RuntimeIsAppAware = isAppAware;
AppAwareRouteModel = BuildRouteModel(zeroPath, backofficeApiPath, true);