login works + nav bar output
This commit is contained in:
+28
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 })
|
||||
};
|
||||
+33
-31
@@ -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;
|
||||
@@ -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
|
||||
}
|
||||
});
|
||||
@@ -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,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"> …</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>
|
||||
@@ -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__;
|
||||
}
|
||||
};
|
||||
Vendored
+7
@@ -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
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
import uiForm from './ui-form.vue';
|
||||
|
||||
export {
|
||||
uiForm
|
||||
};
|
||||
+7
-1
@@ -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);
|
||||
}
|
||||
};
|
||||
+23
-19
@@ -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
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
|
||||
export * from './countries';
|
||||
export * from './applications';
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -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>
|
||||
@@ -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[];
|
||||
}
|
||||
}
|
||||
});
|
||||
Vendored
+44
@@ -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[];
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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,4 +1,4 @@
|
||||
//using zero.Api.Endpoints.Applications;
|
||||
//using zero.Backoffice.Endpoints.Applications;
|
||||
|
||||
//namespace zero.Backoffice.Endpoints.UI;
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user