output blueprint info

This commit is contained in:
2021-12-22 10:55:40 +01:00
parent 3051d86a70
commit 790791d919
15 changed files with 614 additions and 245 deletions
@@ -0,0 +1,325 @@
<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 * as overlays from '../../../services/overlay';
export default {
name: 'uiEditorBlueprintProperty',
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 || {}),
'zero.scope': !this.isChild ? undefined : 'system'
}
};
}
},
mounted()
{
this.bind();
},
methods: {
async openSettings()
{
const editor = typeof this.editor === 'string' ? this.zero.getSchema(this.editor) : this.editor;
const result = await overlays.open({
component: () => import('./settings.vue'),
display: 'editor',
model: {
value: this.value,
blueprintConfig: this.config
}
});
console.info(result);
return;
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;
}-->
@@ -0,0 +1,225 @@
<template>
<ui-trinity 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.close" />
<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>
<template v-for="(field, index) in items">
<ui-property v-if="!field.disabled" :key="index" :label="field.label" :description="field.description" :vertical="false" :class="{'not-synced': !field.synced}">
<ui-toggle v-model:on="field.synced" />
</ui-property>
</template>
</div>
</ui-trinity>
</template>
<script>
import { arrayRemove } from '../../../utils/arrays';
//import Localization from 'zero/helpers/localization.js';
//import BlueprintApi from 'zero/api/blueprint.js';
export default {
name: 'uiEditorBlueprintSettings',
props: {
model: Object,
config: Object
},
data: () => ({
state: 'default',
loaded: false,
editor: null,
items: []
}),
mounted()
{
this.model.blueprintConfig.fields.forEach(field =>
{
let item = JSON.parse(JSON.stringify(field));
item.synced = field.synced(this.model.value);
this.items.push(item);
});
this.loaded = true;
},
methods: {
onSave()
{
this.state = 'loading';
let desync = JSON.parse(JSON.stringify(this.model.value.blueprint.desync));
let resync = [];
this.items.forEach(field =>
{
let desynced = this.model.value.blueprint.desync.indexOf(field.path) > -1;
if (field.synced && desynced)
{
arrayRemove(desync, field.path);
resync.push(field.path);
}
else if (!field.synced && !desynced)
{
desync.push(field.path);
}
});
this.model.value.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.value.blueprint.id).then(blueprint =>
{
this.config.confirm({
blueprint: this.model.value.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.value.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,189 +0,0 @@
-<template>
<ui-property v-if="loaded && !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 { selectorToArray } from '../../utils/arrays';
import { setObjectValue } from '../../utils/objects';
import { localize } from '../../services/localization';
//import Overlay from 'zero/helpers/overlay.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 localize(this.config.options.description || this.editor.templateDescription(this.config.path), { hideEmpty: true });
},
isLocked()
{
return this.editor.blueprint && !this.editor.blueprint.unlocked(this.value, this.config);
},
canUnlock()
{
return this.editor.blueprint && this.editor.blueprint.canUnlock(this.value, this.config);
}
},
methods: {
rebuildModel()
{
this.selector = 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 (currentValue && 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
{
setObjectValue(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>
@@ -7,6 +7,11 @@
:required="!field.optional(value)"
:disabled="isDisabled"
:vertical="!field.horizontal"
:class="{'is-disabled': isDisabled }"
:locked="isLocked"
:can-unlock="canUnlock || false"
@unlock="unlock"
@lock="lock"
>
<component :is="field.component" v-bind="field.options"
:value="model"
@@ -14,17 +19,6 @@
@input="onChange" />
<p v-if="field.helpText" class="ui-property-help" v-localize="field.helpText"></p>
</ui-property>
<!--<ui-property v-if="loaded && !isHidden"
:vertical="field.configuration.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="field.configuration.helpText" class="ui-property-help" v-localize="field.configuration.helpText"></p>
</ui-property>-->
</template>
@@ -90,15 +84,15 @@
isDisabled()
{
return this.manualDisabled || this.disabled || this.field.readonly(this.model);
},
isLocked()
{
return false; //this.editor.blueprint && !this.editor.blueprint.unlocked(this.value, this.field);
},
canUnlock()
{
return false; // this.editor.blueprint && this.editor.blueprint.canUnlock(this.value, this.field);
}
//isLocked()
//{
// return this.editor.blueprint && !this.editor.blueprint.unlocked(this.value, this.field);
//},
//canUnlock()
//{
// return this.editor.blueprint && this.editor.blueprint.canUnlock(this.value, this.field);
//}
},
methods: {
@@ -14,7 +14,7 @@
:disabled="tab.disabled(value)">
<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" />-->
<blueprint-property v-if="value && editorConfig.blueprint.isEnabled" :value="value" :meta="meta" :config="editorConfig.blueprint" />
</slot>
<div v-if="!tab.component" class="ui-property ui-property-parent" v-for="(fieldset, fieldsetIndex) in tab.fieldsets" :key="fieldsetIndex">
@@ -47,13 +47,10 @@
import './ui-editor.scss';
import EditorComponent from './ui-editor-component.vue';
//import { createBlueprintConfig } from './editor-blueprint';
//import BlueprintProperty from './blueprint/property.vue';
import BlueprintProperty from './blueprint/property.vue';
import { defineComponent } from 'vue';
import { compileEditor } from '../../editor/compile';
let createBlueprintConfig = () => null;
export default defineComponent({
name: 'uiEditor',
@@ -91,7 +88,7 @@
},
},
components: { EditorComponent },
components: { EditorComponent, BlueprintProperty },
data: () => ({
display: 'tabs',
@@ -122,9 +119,7 @@
this.system = this.$route.query['zero.scope'] == 'system';
const schema = typeof this.config === 'string' ? await this.zero.getSchema(this.config) : this.config;
const editor = compileEditor(this.zero, schema);
this.editorConfig = editor;
//this.editorConfig.blueprint = createBlueprintConfig(this.zero, this.editorConfig, this.value);
this.onConfigure(this);
@@ -144,7 +144,7 @@
}
if (shared)
{
routeObj.query['zero.scope'] = 'shared';
routeObj.query['zero.scope'] = 'system';
}
this.$router.push(routeObj);
}
+10 -2
View File
@@ -2,12 +2,16 @@ import { Component } from "vue";
import { ZeroEditorField } from "zero/schemas";
import { Zero } from "../core";
import { ZeroEditor } from "./editor";
import { createBlueprintConfig } from "./editor-blueprint";
import { ZeroEditorCanvasBase, ZeroEditorTab } from "./editor-canvas";
import { ZeroEditorFieldConfiguration } from "./editor-field";
import { localize } from '../services/localization';
export interface ZeroCompiledEditor
{
alias: string;
blueprint: any;
tabs: ZeroCompiledEditorTab[];
}
@@ -108,8 +112,8 @@ export function compileField(zero: Zero, editor: ZeroEditor, field: ZeroEditorFi
return false;
},
label: editor.onLabelCreate(field),
description: editor.onDescriptionCreate(field),
label: editor.onLabelCreate(field),
description: localize(editor.onDescriptionCreate(field), { hideEmpty: true }),
hideLabel: field.configuration.hideLabel,
helpText: field.configuration.helpText,
classes: field.configuration.classes,
@@ -125,6 +129,8 @@ export function compileField(zero: Zero, editor: ZeroEditor, field: ZeroEditorFi
export function compileEditor(zero: Zero, editor: ZeroEditor): ZeroCompiledEditor
{
let model = {
alias: editor.alias,
blueprint: null,
tabs: []
} as ZeroCompiledEditor;
@@ -220,5 +226,7 @@ export function compileEditor(zero: Zero, editor: ZeroEditor): ZeroCompiledEdito
}
});
model.blueprint = createBlueprintConfig(model.alias, model);
return model;
}
@@ -2,6 +2,7 @@
import { localize } from '../services/localization';
import { arrayRemove } from '../utils/arrays';
import { useUiStore } from '../ui/store';
import { ZeroCompiledEditor } from './compile';
function unlocked(config, model, field)
{
@@ -77,16 +78,17 @@ function unlock(config, model, field)
* @param {string} alias - Alias for the tab
* @returns {EditorBlueprint}
*/
export function createBlueprintConfig(zero, editor, model)
export function createBlueprintConfig(alias: string, editor: ZeroCompiledEditor)
{
const store = useUiStore();
let blueprintAlias = editor.blueprintAlias;
let blueprintAlias = editor.alias;
let canBeBlueprinted = store.blueprints.indexOf(blueprintAlias) > -1;
if (!blueprintAlias || !canBeBlueprinted)
{
return {
alias: blueprintAlias,
isEnabled: false,
unlocked: () => true,
canUnlock: () => false,
isBlueprintParent: () => false,
@@ -128,28 +130,35 @@ export function createBlueprintConfig(zero, editor, model)
});
// add blueprint setting for custom fields
editor.fields.forEach(field =>
editor.tabs.forEach(tab =>
{
let alias = field.path;
if (processed.indexOf(alias) < 0)
tab.fieldsets.forEach(set =>
{
processed.push(alias);
if (config.unlocked.indexOf(alias) > -1 || config.unlocked.find(x => alias.indexOf(x + '.') === 0))
set.fields.forEach(field =>
{
fields.push({
path: alias,
label: field.options.label || editor.templateLabel(alias),
description: localize(field.options.description || editor.templateDescription(alias), { hideEmpty: true }),
synced: model => !model.blueprint || model.blueprint.desync.indexOf(alias) < 0
});
}
}
let alias = field.path;
if (processed.indexOf(alias) < 0)
{
processed.push(alias);
if (config.unlocked.indexOf(alias) > -1 || config.unlocked.find(x => alias.indexOf(x + '.') === 0))
{
fields.push({
path: alias,
label: field.label,
description: field.description,
synced: model => !model.blueprint || model.blueprint.desync.indexOf(alias) < 0
});
}
}
});
});
});
return {
alias: blueprintAlias,
isEnabled: true,
unlocked: (model, field) => unlocked(config, model, field),
canUnlock: (model, field) => canUnlock(config, model, field),
unlock: async (model, field) => await unlock(config, model, field),
+5 -3
View File
@@ -1,9 +1,10 @@
import { Component, ComponentPropsOptions } from "vue";
import { EditorSchema, ZeroEditorField } from "zero/schemas";
import { ZeroEditorField } from "zero/schemas";
import { ZeroEditorCanvas } from "./editor-canvas";
export class ZeroEditor extends ZeroEditorCanvas
{
alias: string;
resourcePrefix?: string;
system: boolean = false;
@@ -19,8 +20,9 @@ export class ZeroEditor extends ZeroEditorCanvas
};
constructor()
constructor(alias: string)
{
super();
this.alias = alias;
}
}
@@ -2,7 +2,7 @@
import { ZeroEditor } from "../../../editor/editor";
import { formatDate } from '../../../utils/dates';
const editor = new ZeroEditor();
const editor = new ZeroEditor('countries');
editor.resourcePrefix = '@country.fields.';
@@ -1,7 +1,7 @@
import { ZeroEditor } from '../../../editor/editor';
const editor = new ZeroEditor();
const editor = new ZeroEditor('integrations');
editor.resourcePrefix = '@translation.fields.';
@@ -1,7 +1,7 @@
import { ZeroEditor } from '../../../editor/editor';
const editor = new ZeroEditor();
const editor = new ZeroEditor('languages');
editor.resourcePrefix = '@language.fields.';
@@ -1,7 +1,7 @@
import { ZeroEditor } from "../../../editor/editor";
const editor = new ZeroEditor();
const editor = new ZeroEditor('mailtemplates');
editor.resourcePrefix = '@mailTemplate.fields.';
@@ -1,7 +1,7 @@
import { ZeroEditor } from '../../../editor/editor';
const editor = new ZeroEditor();
const editor = new ZeroEditor('translations');
editor.resourcePrefix = '@translation.fields.';
@@ -10,7 +10,7 @@ export interface LocalizeOptions
}
export const localize = (key: string, options?: LocalizeOptions): string =>
export const localize = (key: string | null, options?: LocalizeOptions): string =>
{
let params = extendObject({
force: false,