New Pages section & sitemap editor (#192)
This adds a new Pages section as discussed in https://github.com/openhab/openhab-webui/issues/155#issuecomment-586711180 with an editor for "managed" sitemaps stored as UI components in the system:sitemap namespace. A code view supporting the current sitemap DSL is provided. Sitemaps made with the UI don't support visibility or color rules yet. Signed-off-by: Yannick Schaus <github@schaus.net>pull/194/head
parent
c332164944
commit
d7ae6036af
|
@ -6,19 +6,22 @@
|
|||
"things.text": "Things are the devices and services connected to openHAB, they are provided by binding add-ons.<br><br>Installed bindings which support auto-discovery will add thing candidates to your Inbox. You can also start a scan for a certain binding or add your first thing manually with the button below.",
|
||||
|
||||
"things.nobindings.title": "No bindings installed",
|
||||
"things.nobindings.text": "You need to install binding add-ons to add things they support to your system. Click the button below to install some.",
|
||||
"things.nobindings.text": "You need to install binding add-ons to be able to add things to your system.",
|
||||
|
||||
"model.title": "Start modelling your home",
|
||||
"model.text": "Build a model from your items to organize them and relate them to each other semantically.<br><br>Begin with a hierarchy of locations: buildings, outside areas, floors and rooms, as needed. Then, insert equipments and points from your things (or manually).",
|
||||
|
||||
"items.title": "No items yet",
|
||||
"items.text": "Items represent the functional side of your home - you can link them to the channels defined by your things. Start with the Model view to create a clean initial structure.<br><br>You can also define items with configuration files, or with the button below.",
|
||||
|
||||
"model.title": "Start modelling your home",
|
||||
"model.text": "Build a model from your items to organize them and relate them to each other semantically.<br><br>Begin with a hierarchy of locations: buildings, outside areas, floors and rooms, as needed. Then, insert equipments and points from your things (or manually).",
|
||||
"pages.title": "No pages yet",
|
||||
"pages.text": "Design pages to display information in various ways and interact with your items. You can create several kinds of pages: charts, sitemaps, floor plans...<br><br>Click the button below to create your first page.",
|
||||
|
||||
"rules.title": "No rules yet",
|
||||
"rules.text": "Rules are the basic building blocks to automate your home - they define which actions to perform when certain events occur.<br><br>Create your first rule with the button below; for more advanced scenarios, you can also write script files in your configuration folder.",
|
||||
|
||||
"schedule.title": "Nothing in the schedule",
|
||||
"schedule.text": "The schedule displays up to 30 days of times when rules specifically tagged \"Schedule\" are expected to run.<br><br>Click the button below to create your first scheduled rule.",
|
||||
"schedule.text": "The schedule displays when rules specifically tagged \"Schedule\" are expected to run, up to 30 days.<br><br>Click the button below to create your first scheduled rule.",
|
||||
|
||||
"rules.missingengine.title": "Rule engine not installed",
|
||||
"rules.missingengine.text": "The rule engine must be installed before rules can be created.",
|
||||
|
|
|
@ -0,0 +1,111 @@
|
|||
@{%
|
||||
const moo = require('moo')
|
||||
|
||||
let lexer = moo.compile({
|
||||
WS: /[ \t]+/,
|
||||
comment: /\/\/.*?$/,
|
||||
number: /\-?[0-9]+(?:\.[0-9]*)?/,
|
||||
string: { match: /"(?:\\["\\]|[^\n"\\])*"/, value: x => x.slice(1, -1) },
|
||||
sitemap: 'sitemap ',
|
||||
name: 'name=',
|
||||
label: 'label=',
|
||||
item: 'item=',
|
||||
icon: 'icon=',
|
||||
widgetattr: ['url=', 'refresh=', 'service=', 'refresh=', 'period=', 'legend=', 'height=', 'frequency=', 'sendFrequency=',
|
||||
'switchEnabled=', 'mappings=', 'minValue=', 'maxValue=', 'step=', 'separator=', 'encoding='],
|
||||
nlwidget: ['Switch ', 'Selection ', 'Slider ', 'List ', 'Setpoint ', 'Video ', 'Chart ', 'Webview ', 'Colorpicker ', 'Mapview ', 'Default '],
|
||||
lwidget: ['Text', 'Group', 'Image', 'Frame'],
|
||||
identifier: /[A-Za-z0-9_]+/,
|
||||
lparen: '(',
|
||||
rparen: ')',
|
||||
colon: ':',
|
||||
lbrace: '{',
|
||||
rbrace: '}',
|
||||
lbracket: '[',
|
||||
rbracket: ']',
|
||||
lt: '<',
|
||||
gt: '>',
|
||||
equals: '=',
|
||||
comma: ',',
|
||||
NL: { match: /\n/, lineBreaks: true },
|
||||
})
|
||||
|
||||
function getSitemap(d) {
|
||||
return {
|
||||
"uid": d[2][0].text,
|
||||
"component": "Sitemap",
|
||||
"config": d[4],
|
||||
"slots": {
|
||||
"widgets": d[8]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getWidget(d,l,r) {
|
||||
let widget = {
|
||||
component: d[0].text.trim(),
|
||||
config: {}
|
||||
}
|
||||
if (d[2] && d[2][0]) {
|
||||
for (let a of d[2][0]) {
|
||||
widget.config[a[0].replace('=', '')] = a[1]
|
||||
}
|
||||
}
|
||||
if (d[6]) {
|
||||
widget.slots = {
|
||||
widgets: d[6]
|
||||
}
|
||||
}
|
||||
|
||||
return widget
|
||||
}
|
||||
|
||||
%}
|
||||
|
||||
@lexer lexer
|
||||
|
||||
|
||||
Main -> _ Sitemap _ {% (d) => d[1] %}
|
||||
Sitemap -> %sitemap _ SitemapName __ SitemapLabel __ %lbrace _ Widgets _ %rbrace {% getSitemap %}
|
||||
|
||||
SitemapName -> %identifier
|
||||
SitemapLabel -> null {% (d) => { return {} } %}
|
||||
| %label %string {% (d) => { return { "label": d[1].value } } %}
|
||||
|
||||
Widgets -> Widget {% (d) => [d[0]] %}
|
||||
| Widgets _ Widget {% (d) => d[0].concat([d[2]]) %}
|
||||
|
||||
Widget -> %nlwidget _ WidgetAttrs:* {% getWidget %}
|
||||
| %lwidget _ WidgetAttrs:* {% getWidget %}
|
||||
| %lwidget _ WidgetAttrs:* __ %lbrace __ Widgets __ %rbrace {% getWidget %}
|
||||
|
||||
WidgetAttrs -> WidgetAttr {% (d) => [d[0]] %}
|
||||
| WidgetAttrs _ WidgetAttr {% (d) => d[0].concat([d[2]]) %}
|
||||
WidgetAttr -> WidgetAttrName WidgetAttrValue {% (d) => [d[0][0].value, d[1]] %}
|
||||
WidgetAttrName -> %item | %label | %icon | %widgetattr
|
||||
WidgetAttrValue -> %string {% (d) => d[0].value %}
|
||||
| %identifier {% (d) => d[0].value %}
|
||||
| %number {% (d) => { return parseFloat(d[0].value) } %}
|
||||
| %lbracket _ Mappings _ %rbracket {% (d) => d[2] %}
|
||||
|
||||
Mappings -> Mapping {% (d) => [d[0]] %}
|
||||
| Mappings _ %comma _ Mapping {% (d) => d[0].concat([d[4]]) %}
|
||||
Mapping -> MappingCommand %equals MappingLabel {% (d) => d[0][0].value.toString() + '=' + d[2][0].value.toString() %}
|
||||
MappingCommand -> %number | %identifier | %string
|
||||
MappingLabel -> %number | %identifier | %string
|
||||
|
||||
|
||||
_ -> null {% () => null %}
|
||||
| _ %WS {% () => null %}
|
||||
| _ %NL {% () => null %}
|
||||
# | _ %comment {% () => null %}
|
||||
|
||||
__ -> %WS {% () => null %}
|
||||
| %NL {% () => null %}
|
||||
| %comment {% () => null %}
|
||||
| __ %WS {% () => null %}
|
||||
| __ %NL {% () => null %}
|
||||
| __ %comment {% () => null %}
|
||||
|
||||
NL -> %NL {% () => null %}
|
||||
| _ %NL {% () => null %}
|
|
@ -28,11 +28,14 @@
|
|||
<f7-list-item inset link="/settings/things/" title="Things" view=".view-main" panel-close :animate="false">
|
||||
<f7-icon slot="media" f7="lightbulb" color="gray"></f7-icon>
|
||||
</f7-list-item>
|
||||
<f7-list-item inset link="/settings/model/" title="Model" view=".view-main" panel-close :animate="false">
|
||||
<f7-icon slot="media" f7="list_bullet_indent" color="gray"></f7-icon>
|
||||
</f7-list-item>
|
||||
<f7-list-item inset link="/settings/items/" title="Items" view=".view-main" panel-close :animate="false">
|
||||
<f7-icon slot="media" f7="square_on_circle" color="gray"></f7-icon>
|
||||
</f7-list-item>
|
||||
<f7-list-item inset link="/settings/model/" title="Model" view=".view-main" panel-close :animate="false">
|
||||
<f7-icon slot="media" f7="list_bullet_indent" color="gray"></f7-icon>
|
||||
<f7-list-item inset link="/settings/pages/" title="Pages" view=".view-main" panel-close :animate="false">
|
||||
<f7-icon slot="media" f7="tv" color="gray"></f7-icon>
|
||||
</f7-list-item>
|
||||
<f7-list-item inset link="/settings/rules/" title="Rules" view=".view-main" panel-close :animate="false">
|
||||
<f7-icon slot="media" f7="wand_rays" color="gray"></f7-icon>
|
||||
|
@ -217,7 +220,7 @@ export default {
|
|||
}
|
||||
},
|
||||
methods: {
|
||||
loadSitemaps () {
|
||||
loadSidebarPages () {
|
||||
this.$oh.api.get('/rest/sitemaps').then((data) => {
|
||||
this.sitemaps = data
|
||||
})
|
||||
|
@ -272,13 +275,17 @@ export default {
|
|||
this.loggedIn = true
|
||||
}
|
||||
|
||||
this.loadSitemaps()
|
||||
this.loadSidebarPages()
|
||||
|
||||
this.$f7.on('pageBeforeIn', (page) => {
|
||||
if (page.route && page.route.url) {
|
||||
this.showAdministrationMenu = page.route.url.indexOf('/settings/') === 0
|
||||
}
|
||||
})
|
||||
|
||||
this.$f7.on('sidebarRefresh', () => {
|
||||
this.loadSidebarPages()
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
function writeWidget (widget, indent) {
|
||||
let dsl = ' '.repeat(indent)
|
||||
dsl += widget.component
|
||||
if (widget.config) {
|
||||
for (let key in widget.config) {
|
||||
if (!widget.config[key]) continue
|
||||
dsl += ` ${key}=`
|
||||
if (key === 'item' || Number.isFinite(widget.config[key])) {
|
||||
dsl += widget.config[key]
|
||||
} else if (key === 'mappings') {
|
||||
dsl += '['
|
||||
const mappingsDsl = widget.config.mappings.map((m) =>
|
||||
`${m.split('=')[0]}="${m.substring(m.indexOf('=') + 1)}"`
|
||||
)
|
||||
dsl += mappingsDsl.join(',')
|
||||
dsl += ']'
|
||||
} else {
|
||||
dsl += '"' + widget.config[key] + '"'
|
||||
}
|
||||
}
|
||||
}
|
||||
if (widget.slots) {
|
||||
dsl += ' {\n'
|
||||
widget.slots.widgets.forEach((w) => {
|
||||
dsl += writeWidget(w, indent + 4)
|
||||
})
|
||||
dsl += ' '.repeat(indent) + '}'
|
||||
}
|
||||
dsl += '\n'
|
||||
|
||||
return dsl
|
||||
}
|
||||
|
||||
export default {
|
||||
toDsl (sitemap) {
|
||||
let dsl = 'sitemap ' + sitemap.uid
|
||||
if (sitemap.config && sitemap.config.label) {
|
||||
dsl += ` label="${sitemap.config.label}"`
|
||||
}
|
||||
dsl += ' {\n'
|
||||
if (sitemap.slots && sitemap.slots.widgets) {
|
||||
sitemap.slots.widgets.forEach((w) => {
|
||||
dsl += writeWidget(w, 4)
|
||||
})
|
||||
}
|
||||
dsl += '}\n'
|
||||
|
||||
return dsl
|
||||
}
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
<template>
|
||||
<f7-card v-if="widget">
|
||||
<f7-card-content v-if="mappings.length">
|
||||
<f7-list inline-labels sortable @sortable:sort="onSort">
|
||||
<f7-list-input v-for="(mapping, idx) in mappings" :key="idx"
|
||||
:label="`#${idx+1}`" type="text" placeholder="command=Label" :value="mapping" @input="updateMapping(idx, $event)" clear-button>
|
||||
</f7-list-input>
|
||||
</f7-list>
|
||||
</f7-card-content>
|
||||
<f7-card-footer key="item-card-buttons-edit-mode" v-if="widget.component !== 'Sitemap'">
|
||||
<f7-button color="blue" @click="addMapping">Add</f7-button>
|
||||
</f7-card-footer>
|
||||
|
||||
</f7-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: ['widget'],
|
||||
computed: {
|
||||
mappings () {
|
||||
if (this.widget && this.widget.config && this.widget.config.mappings) {
|
||||
return this.widget.config.mappings
|
||||
}
|
||||
return []
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateMapping (idx, $event) {
|
||||
const value = $event.target.value
|
||||
if (!value) {
|
||||
this.widget.config.mappings.splice(idx, 1)
|
||||
} else {
|
||||
this.$set(this.widget.config.mappings, idx, value)
|
||||
}
|
||||
},
|
||||
addMapping () {
|
||||
if (this.widget && this.widget.config && this.widget.config.mappings) {
|
||||
this.widget.config.mappings.push('')
|
||||
} else {
|
||||
this.$set(this.widget.config, 'mappings', [''])
|
||||
}
|
||||
},
|
||||
onSort (ev) {
|
||||
const element = this.widget.config.mappings[ev.from]
|
||||
this.widget.config.mappings.splice(ev.from, 1)
|
||||
this.widget.config.mappings.splice(ev.to, 0, element)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,76 @@
|
|||
<template>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<editor class="sitemap-parser" :value="sitemapDsl" @input="updateSitemap" />
|
||||
<div v-if="parsedSitemap.error" class="sitemap-results error">
|
||||
<pre><code class="text-color-red">{{parsedSitemap.error}}</code></pre>
|
||||
</div>
|
||||
<div v-else class="sitemap-results">
|
||||
<pre><code class="text-color-teal">Your sitemap definition looks valid.</code></pre>
|
||||
<pre><code>{{parsedSitemap}}</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="stylus">
|
||||
.sitemap-parser.vue-codemirror
|
||||
display block
|
||||
top calc(var(--f7-navbar-height) + var(--f7-tabbar-height))
|
||||
height calc(50% - 2*var(--f7-navbar-height))
|
||||
width 100%
|
||||
.sitemap-results
|
||||
position absolute
|
||||
top 50%
|
||||
height 50%
|
||||
overflow-y auto
|
||||
width 100%
|
||||
&.error
|
||||
pre
|
||||
padding 0 1rem
|
||||
pre
|
||||
padding 0 1rem
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import { Parser, Grammar } from 'nearley'
|
||||
import grammar from '@/assets/sitemap-lexer.nearley'
|
||||
import dslUtil from './dslUtil'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
'editor': () => import('@/components/config/controls/script-editor.vue')
|
||||
},
|
||||
props: ['sitemap'],
|
||||
data () {
|
||||
return {
|
||||
sitemapDsl: ''
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.sitemapDsl = dslUtil.toDsl(this.sitemap)
|
||||
},
|
||||
methods: {
|
||||
updateSitemap (value) {
|
||||
this.sitemapDsl = value
|
||||
const parsed = this.parsedSitemap
|
||||
if (!parsed.error) {
|
||||
this.$emit('updated', parsed)
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
parsedSitemap () {
|
||||
try {
|
||||
const parser = new Parser(Grammar.fromCompiled(grammar))
|
||||
parser.feed(this.sitemapDsl.trim())
|
||||
if (!parser.results.length) return { error: 'Unable to parse, check your input' }
|
||||
// return parser.results[0].map((i) => i.name).join('\n')
|
||||
return parser.results[0]
|
||||
} catch (e) {
|
||||
return { error: e }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,77 @@
|
|||
<template>
|
||||
<f7-treeview-item selectable :label="widget.config.label"
|
||||
:icon-ios="icon('ios')" :icon-aurora="icon('aurora')" :icon-md="icon('md')"
|
||||
:textColor="iconColor" :color="'blue'"
|
||||
:selected="selected && selected === widget"
|
||||
:opened="!widget.closed"
|
||||
@click="select">
|
||||
<sitemap-treeview-item v-for="(childwidget, idx) in children"
|
||||
:key="idx"
|
||||
:widget="childwidget" :parent-widget="widget"
|
||||
@selected="(event) => $emit('selected', event)"
|
||||
:selected="selected" />
|
||||
<div slot="label" class="subtitle"> {{subtitle()}}</div>
|
||||
</f7-treeview-item>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: ['widget', 'parentWidget', 'selected'],
|
||||
methods: {
|
||||
icon (theme) {
|
||||
switch (this.widget.component) {
|
||||
case 'Switch':
|
||||
return 'f7:power'
|
||||
case 'Selection':
|
||||
return 'f7:text_justify'
|
||||
case 'Slider':
|
||||
return 'f7:slider_horizontal_3'
|
||||
case 'List':
|
||||
return 'f7:square_list'
|
||||
case 'Setpoint':
|
||||
return 'f7:plus_slash_minus'
|
||||
case 'Video':
|
||||
return 'f7:videocam'
|
||||
case 'Chart':
|
||||
return 'f7:chart_bar_square'
|
||||
case 'Webview':
|
||||
return 'f7:globe'
|
||||
case 'Colorpicker':
|
||||
return 'f7:drop'
|
||||
case 'Mapview':
|
||||
return 'f7:map'
|
||||
case 'Default':
|
||||
return 'f7:rectangle'
|
||||
case 'Text':
|
||||
return 'f7:textformat'
|
||||
case 'Group':
|
||||
return 'f7:square_stack_3d_down_right'
|
||||
case 'Image':
|
||||
return 'f7:photo'
|
||||
case 'Frame':
|
||||
return 'f7:macwindow'
|
||||
default:
|
||||
return 'f7:slider_horizontal_below_rectangle'
|
||||
}
|
||||
},
|
||||
subtitle () {
|
||||
return this.widget.component + ((this.widget.config && this.widget.config.item) ? ': ' + this.widget.config.item : '')
|
||||
},
|
||||
select (event) {
|
||||
var self = this
|
||||
var $ = self.$$
|
||||
if ($(event.target).is('.treeview-toggle')) return
|
||||
this.$emit('selected', [this.widget, this.parentWidget])
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
iconColor () {
|
||||
return ''
|
||||
},
|
||||
children () {
|
||||
if (!this.widget.slots || !this.widget.slots.widgets) return []
|
||||
return this.widget.slots.widgets
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,125 @@
|
|||
<template>
|
||||
<f7-card v-if="widget">
|
||||
<f7-card-content>
|
||||
|
||||
<f7-list inline-labels>
|
||||
<f7-list-input v-if="widget.component === 'Sitemap'" label="ID" type="text" placeholder="ID" :value="widget.uid" @input="widget.uid = $event.target.value"
|
||||
required validate pattern="[A-Za-z0-9_]+" error-message="Required. Alphanumeric & underscores only" :disabled="!createMode">
|
||||
</f7-list-input>
|
||||
<f7-list-input label="Label" type="text" placeholder="Label" :value="widget.config.label" @input="updateParameter('label', $event)" clear-button>
|
||||
</f7-list-input>
|
||||
<item-picker v-if="widget.component !== 'Sitemap'" title="Item" :value="widget.config.item" @input="(value) => widget.config.item = value"></item-picker>
|
||||
<ul v-if="widget.component !== 'Sitemap'">
|
||||
<f7-list-input ref="icon" label="Icon" autocomplete="off" type="text" placeholder="temperature, firstfloor..." :value="widget.config.icon"
|
||||
@input="updateParameter('icon', $event)" clear-button>
|
||||
<div slot="root-end" style="margin-left: calc(35% + 8px)">
|
||||
<oh-icon :icon="widget.config.icon" height="32" width="32" />
|
||||
</div>
|
||||
</f7-list-input>
|
||||
</ul>
|
||||
<ul>
|
||||
<!-- additional controls -->
|
||||
<f7-list-input v-if="supports('url')" label="URL" type="text" :value="widget.config.url" @input="updateParameter('url', $event)" clear-button />
|
||||
<f7-list-input v-if="supports('refresh')" label="Refresh interval" type="text" :value="widget.config.refresh" @input="updateParameter('url', $event)" clear-button />
|
||||
<f7-list-input v-if="supports('encoding')" label="Encoding" type="text" :value="widget.config.encoding" @input="updateParameter('url', $event)" clear-button />
|
||||
<f7-list-input v-if="supports('service')" label="Service" type="text" :value="widget.config.service" @input="updateParameter('service', $event)" clear-button />
|
||||
<f7-list-input v-if="supports('period')" label="Period" type="text" :value="widget.config.period" @input="updateParameter('period', $event)" clear-button />
|
||||
<f7-list-input v-if="supports('height')" label="Height" type="text" :value="widget.config.height" @input="updateParameter('height', $event)" clear-button />
|
||||
<f7-list-input v-if="supports('sendFrequency')" label="Frequency" type="text" :value="widget.config.sendFrequency" @input="updateParameter('sendFrequency', $event)" clear-button />
|
||||
<f7-list-input v-if="supports('frequency')" label="Frequency" type="text" :value="widget.config.frequency" @input="updateParameter('frequency', $event)" clear-button />
|
||||
<f7-list-input v-if="supports('minValue')" label="Minimum" type="number" :value="widget.config.minValue" @input="updateParameter('minValue', $event)" clear-button />
|
||||
<f7-list-input v-if="supports('maxValue')" label="Maximum" type="number" :value="widget.config.maxValue" @input="updateParameter('maxValue', $event)" clear-button />
|
||||
<f7-list-input v-if="supports('step')" label="Step" type="number" :value="widget.config.step" @input="updateParameter('step', $event)" clear-button />
|
||||
<f7-list-input v-if="supports('separator')" label="Separator" type="number" :value="widget.config.separator" @input="updateParameter('separator', $event)" clear-button />
|
||||
<f7-list-item v-if="supports('switchEnabled')" title="Switch enabled">
|
||||
<f7-toggle slot="after" :checked="widget.config.switchEnabled" @toggle:change="widget.config.switchEnabled = $event"></f7-toggle>
|
||||
</f7-list-item>
|
||||
</ul>
|
||||
</f7-list>
|
||||
</f7-card-content>
|
||||
<f7-card-footer key="sitemap-widget-buttons-edit-mode" v-if="widget.component !== 'Sitemap'">
|
||||
<!-- <f7-button v-if="!editMode && !createMode" color="blue" @click="editMode = true" icon-ios="material:expand_more" icon-md="material:expand_more" icon-aurora="material:expand_more">Edit</f7-button> -->
|
||||
<f7-segmented>
|
||||
<f7-button color="blue" @click="$emit('moveup', widget)" icon-f7="chevron_up"></f7-button>
|
||||
<f7-button color="blue" @click="$emit('movedown', widget)" icon-f7="chevron_down"></f7-button>
|
||||
</f7-segmented>
|
||||
<f7-button v-if="widget.component !== 'Sitemap'" color="red" @click="$emit('remove', widget)">Remove</f7-button>
|
||||
</f7-card-footer>
|
||||
</f7-card>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import { Categories } from '@/assets/categories.js'
|
||||
import ItemPicker from '@/components/config/controls/item-picker.vue'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ItemPicker
|
||||
},
|
||||
props: ['widget', 'createMode'],
|
||||
data () {
|
||||
return {
|
||||
iconInputId: '',
|
||||
iconAutocomplete: null,
|
||||
additionalControls: {
|
||||
Image: ['url', 'refresh'],
|
||||
Video: ['url', 'encoding'],
|
||||
Chart: ['service', 'period', 'refresh', 'legend'],
|
||||
Webview: ['url', 'height'],
|
||||
Mapview: ['height'],
|
||||
Slider: ['sendFrequency', 'switchEnabled', 'minValue', 'maxValue', 'step'],
|
||||
List: ['separator'],
|
||||
Setpoint: ['minValue', 'maxValue', 'step'],
|
||||
Colorpicker: ['frequency'],
|
||||
Default: ['height']
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
initializeAutocomplete (inputElement) {
|
||||
this.iconAutocomplete = this.$f7.autocomplete.create({
|
||||
inputEl: inputElement,
|
||||
openIn: 'dropdown',
|
||||
source (query, render) {
|
||||
if (!query || !query.length) {
|
||||
render([])
|
||||
} else {
|
||||
render(Categories.filter((c) => c.toLowerCase().indexOf(query.toLowerCase()) >= 0))
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
supports (parameter) {
|
||||
if (!this.additionalControls[this.widget.component]) return false
|
||||
return (this.additionalControls[this.widget.component].indexOf(parameter) >= 0)
|
||||
},
|
||||
updateParameter (parameter, $event) {
|
||||
let value = $event.target.value
|
||||
if (value && !isNaN(value)) {
|
||||
value = parseFloat(value)
|
||||
}
|
||||
this.$set(this.widget.config, parameter, value)
|
||||
},
|
||||
remove () {
|
||||
this.$emit('remove')
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
if (!this.widget) return
|
||||
if (!this.widget.config.icon) this.$set(this.widget.config, 'icon', '')
|
||||
const iconControl = this.$refs.icon
|
||||
if (!iconControl || !iconControl.$el) return
|
||||
const inputElement = this.$$(iconControl.$el).find('input')
|
||||
this.initializeAutocomplete(inputElement)
|
||||
},
|
||||
beforeDestroy () {
|
||||
if (this.iconControl) {
|
||||
this.$f7.autocomplete.destroy(this.iconControl)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -3,7 +3,8 @@ import Vue from 'vue'
|
|||
|
||||
import SitemapWidgetGeneric from '../components/sitemap/widget-generic.vue'
|
||||
import OHIconComponent from '../components/oh-icon.vue'
|
||||
import TreeviewItem from '../components/model/treeview-item.vue'
|
||||
import ModelTreeviewItem from '../components/model/treeview-item.vue'
|
||||
import SitemapTreeviewItem from '../components/pagedesigner/sitemap/treeview-item.vue'
|
||||
import EmptyStatePlaceholder from '../components/empty-state-placeholder.vue'
|
||||
|
||||
// Import Framework7
|
||||
|
@ -47,5 +48,6 @@ const app = new Vue({
|
|||
|
||||
Vue.component('sitemap-widget-generic', SitemapWidgetGeneric)
|
||||
Vue.component('oh-icon', OHIconComponent)
|
||||
Vue.component('model-treeview-item', TreeviewItem)
|
||||
Vue.component('model-treeview-item', ModelTreeviewItem)
|
||||
Vue.component('sitemap-treeview-item', SitemapTreeviewItem)
|
||||
Vue.component('empty-state-placeholder', EmptyStatePlaceholder)
|
||||
|
|
|
@ -31,6 +31,9 @@ import RulesListPage from '../pages/settings/rules/rules-list.vue'
|
|||
import RuleEditPage from '../pages/settings/rules/rule-edit.vue'
|
||||
import RuleConfigureModulePage from '../pages/settings/rules/rule-configure-module.vue'
|
||||
|
||||
import PagesListPage from '../pages/settings/pages/pages-list.vue'
|
||||
import SitemapEditPage from '../pages/settings/pages/sitemap/sitemap-edit.vue'
|
||||
|
||||
// import SchedulePage from '../pages/settings/schedule/schedule.vue'
|
||||
|
||||
import Analyzer from '../pages/analyzer/analyzer.vue'
|
||||
|
@ -135,22 +138,25 @@ export default [
|
|||
}
|
||||
]
|
||||
},
|
||||
// {
|
||||
// path: 'items-virtual',
|
||||
// component: ItemsVirtualListPage,
|
||||
// routes: [
|
||||
// {
|
||||
// path: ':itemName',
|
||||
// component: ItemDetailsPage,
|
||||
// routes: [
|
||||
// {
|
||||
// path: 'edit',
|
||||
// component: ItemEditPage
|
||||
// }
|
||||
// ]
|
||||
// }
|
||||
// ]
|
||||
// },
|
||||
{
|
||||
path: 'pages',
|
||||
component: PagesListPage,
|
||||
routes: [
|
||||
{
|
||||
path: 'sitemap/add',
|
||||
component: SitemapEditPage,
|
||||
options: {
|
||||
props: {
|
||||
createMode: true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'sitemap/:uid',
|
||||
component: SitemapEditPage
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: 'things/',
|
||||
component: ThingsListPage,
|
||||
|
|
|
@ -0,0 +1,211 @@
|
|||
<template>
|
||||
<f7-page @page:afterin="onPageAfterIn" @page:afterout="onPageAfterOut">
|
||||
<f7-navbar title="Pages" back-link="Settings" back-link-url="/settings/" back-link-force>
|
||||
<f7-nav-right>
|
||||
<f7-link icon-md="material:done_all" @click="toggleCheck()"
|
||||
:text="(!$theme.md) ? ((showCheckboxes) ? 'Done' : 'Select') : ''"></f7-link>
|
||||
</f7-nav-right>
|
||||
<f7-subnavbar :inner="false" v-show="initSearchbar">
|
||||
<f7-searchbar
|
||||
v-if="initSearchbar"
|
||||
class="searchbar-pages"
|
||||
:init="initSearchbar"
|
||||
search-container=".pages-list"
|
||||
search-item=".pagelist-item"
|
||||
search-in=".item-title, .item-subtitle, .item-header, .item-footer"
|
||||
remove-diacritics
|
||||
:disable-button="!$theme.aurora"
|
||||
></f7-searchbar>
|
||||
</f7-subnavbar>
|
||||
</f7-navbar>
|
||||
<f7-toolbar class="contextual-toolbar" :class="{ 'navbar': $theme.md }" v-if="showCheckboxes" bottom-ios bottom-aurora>
|
||||
<f7-link color="red" v-show="selectedItems.length" v-if="!$theme.md" class="delete" icon-ios="f7:trash" icon-aurora="f7:trash" @click="removeSelected">Remove {{selectedItems.length}}</f7-link>
|
||||
<f7-link v-if="$theme.md" icon-md="material:close" icon-color="white" @click="showCheckboxes = false"></f7-link>
|
||||
<div class="title" v-if="$theme.md">
|
||||
{{selectedItems.length}} selected
|
||||
</div>
|
||||
<div class="right" v-if="$theme.md">
|
||||
<f7-link icon-md="material:delete" icon-color="white" @click="removeSelected"></f7-link>
|
||||
<f7-link icon-md="material:more_vert" icon-color="white" @click="removeSelected"></f7-link>
|
||||
</div>
|
||||
</f7-toolbar>
|
||||
|
||||
<f7-list class="searchbar-not-found">
|
||||
<f7-list-item title="Nothing found"></f7-list-item>
|
||||
</f7-list>
|
||||
|
||||
<!-- skeleton for not ready -->
|
||||
<f7-block class="block-narrow">
|
||||
<f7-col v-show="!ready">
|
||||
<f7-block-title> Loading...</f7-block-title>
|
||||
<f7-list media-list class="col wide">
|
||||
<f7-list-group>
|
||||
<f7-list-item
|
||||
media-item
|
||||
v-for="n in 20"
|
||||
:key="n"
|
||||
:class="`skeleton-text skeleton-effect-blink`"
|
||||
title="Title of the page"
|
||||
subtitle="Type of the page"
|
||||
after="status badge"
|
||||
>
|
||||
</f7-list-item>
|
||||
</f7-list-group>
|
||||
</f7-list>
|
||||
</f7-col>
|
||||
<f7-col v-if="ready">
|
||||
<f7-block-title class="searchbar-hide-on-search">{{pages.length}} pages</f7-block-title>
|
||||
<f7-list
|
||||
v-show="pages.length > 0"
|
||||
class="searchbar-found col pages-list"
|
||||
ref="pagesList"
|
||||
media-list>
|
||||
<f7-list-item
|
||||
v-for="(page, index) in pages"
|
||||
:key="index"
|
||||
media-item
|
||||
class="pagelist-item"
|
||||
:checkbox="showCheckboxes"
|
||||
:checked="isChecked(page.uid)"
|
||||
@change="(e) => toggleItemCheck(e, page.uid, page)"
|
||||
:link="showCheckboxes ? null : page.component.toLowerCase() + '/' + page.uid"
|
||||
:title="page.config.label"
|
||||
:subtitle="page.component"
|
||||
>
|
||||
<div slot="subtitle">
|
||||
<f7-chip v-for="tag in page.tags" :key="tag" :text="tag" media-bg-color="blue" style="margin-right: 6px">
|
||||
<f7-icon slot="media" ios="f7:tag_fill" md="material:label" aurora="f7:tag_fill" ></f7-icon>
|
||||
</f7-chip>
|
||||
</div>
|
||||
<!-- <span slot="media" class="item-initial">{{page.config.label[0].toUpperCase()}}</span> -->
|
||||
<f7-icon slot="media" color="gray" :f7="icon(page)" :size="32"></f7-icon>
|
||||
</f7-list-item>
|
||||
</f7-list>
|
||||
</f7-col>
|
||||
</f7-block>
|
||||
<f7-block v-if="ready && !pages.length" class="service-config block-narrow">
|
||||
<empty-state-placeholder icon="tv" title="pages.title" text="pages.text" />
|
||||
</f7-block>
|
||||
<f7-fab v-show="ready && !showCheckboxes" position="right-bottom" slot="fixed" color="blue">
|
||||
<f7-icon ios="f7:plus" md="material:add" aurora="f7:plus"></f7-icon>
|
||||
<f7-icon ios="f7:multiply" md="material:close" aurora="f7:multiply"></f7-icon>
|
||||
<f7-fab-buttons position="top">
|
||||
<f7-fab-button fab-close label="Create sitemap" href="sitemap/add"><f7-icon f7="menu"></f7-icon></f7-fab-button>
|
||||
<!-- <f7-fab-button fab-close label="Create layout" href="add"><f7-icon f7="rectangle_grid_2x2"></f7-icon></f7-fab-button>
|
||||
<f7-fab-button fab-close label="Create map view" href="add"><f7-icon f7="map"></f7-icon></f7-fab-button>
|
||||
<f7-fab-button fab-close label="Create chart" href="add"><f7-icon f7="graph_square"></f7-icon></f7-fab-button>
|
||||
<f7-fab-button fab-close label="Create floor plan" href="add"><f7-icon f7="layers"></f7-icon></f7-fab-button> -->
|
||||
</f7-fab-buttons>
|
||||
</f7-fab>
|
||||
</f7-page>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
ready: false,
|
||||
loading: false,
|
||||
pages: [],
|
||||
initSearchbar: false,
|
||||
selectedItems: [],
|
||||
showCheckboxes: false
|
||||
}
|
||||
},
|
||||
created () {
|
||||
|
||||
},
|
||||
methods: {
|
||||
onPageAfterIn () {
|
||||
this.load()
|
||||
},
|
||||
onPageAfterOut () {
|
||||
|
||||
},
|
||||
icon (page) {
|
||||
switch (page.component) {
|
||||
case 'Sitemap':
|
||||
return 'menu'
|
||||
default:
|
||||
return 'tv'
|
||||
}
|
||||
},
|
||||
load () {
|
||||
if (this.loading) return
|
||||
this.loading = true
|
||||
var promises = [
|
||||
this.$oh.api.get('/rest/ui/components/system:sitemap'),
|
||||
this.$oh.api.get('/rest/ui/components/ui:page')
|
||||
]
|
||||
Promise.all(promises).then(data => {
|
||||
const pagesAndSitemaps = data[0].concat(data[1])
|
||||
this.pages = pagesAndSitemaps.sort((a, b) => {
|
||||
return a.config.label.localeCompare(b.config.label)
|
||||
})
|
||||
this.loading = false
|
||||
this.ready = true
|
||||
setTimeout(() => { this.initSearchbar = true })
|
||||
})
|
||||
},
|
||||
toggleCheck () {
|
||||
this.showCheckboxes = !this.showCheckboxes
|
||||
},
|
||||
isChecked (item) {
|
||||
return this.selectedItems.indexOf(item) >= 0
|
||||
},
|
||||
toggleItemCheck (event, itemName, item) {
|
||||
console.log('toggle check')
|
||||
itemName = (item.component === 'Sitemap') ? 'system:sitemap:' + itemName : 'ui:page:' + itemName
|
||||
if (this.isChecked(itemName)) {
|
||||
this.selectedItems.splice(this.selectedItems.indexOf(itemName), 1)
|
||||
} else {
|
||||
this.selectedItems.push(itemName)
|
||||
}
|
||||
},
|
||||
removeSelected () {
|
||||
const vm = this
|
||||
|
||||
this.$f7.dialog.confirm(
|
||||
`Remove ${this.selectedItems.length} selected pages?`,
|
||||
'Remove Pages',
|
||||
() => {
|
||||
vm.doRemoveSelected()
|
||||
}
|
||||
)
|
||||
},
|
||||
doRemoveSelected () {
|
||||
let dialog = this.$f7.dialog.progress('Deleting Pages...')
|
||||
|
||||
const promises = this.selectedItems.map((p) => {
|
||||
if (p.startsWith('system:sitemap')) {
|
||||
return this.$oh.api.delete('/rest/ui/components/system:sitemap/' + p.replace('system:sitemap:', ''))
|
||||
} else {
|
||||
return this.$oh.api.delete('/rest/ui/components/ui:page/' + p.replace('ui:page:', ''))
|
||||
}
|
||||
})
|
||||
Promise.all(promises).then((data) => {
|
||||
this.$f7.toast.create({
|
||||
text: 'Pages removed',
|
||||
destroyOnClose: true,
|
||||
closeTimeout: 2000
|
||||
}).open()
|
||||
this.selectedItems = []
|
||||
dialog.close()
|
||||
this.load()
|
||||
this.$f7.emit('sidebarRefresh', null)
|
||||
}).catch((err) => {
|
||||
dialog.close()
|
||||
this.load()
|
||||
console.error(err)
|
||||
this.$f7.dialog.alert('An error occurred while deleting: ' + err)
|
||||
this.$f7.emit('sidebarRefresh', null)
|
||||
})
|
||||
}
|
||||
},
|
||||
asyncComputed: {
|
||||
iconUrl () {
|
||||
return icon => this.$oh.media.getIcon(icon)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,378 @@
|
|||
<template>
|
||||
<f7-page @page:afterin="onPageAfterIn" @page:beforeout="onPageBeforeOut">
|
||||
<f7-navbar :title="(!ready) ? '' : (createMode) ? 'Create sitemap' : 'Sitemap: ' + sitemap.config.label" back-link="Back" no-hairline>
|
||||
<f7-nav-right>
|
||||
<f7-link @click="save()" v-if="$theme.md" icon-md="material:save" icon-only></f7-link>
|
||||
<f7-link @click="save()" v-if="!$theme.md">Save</f7-link>
|
||||
</f7-nav-right>
|
||||
</f7-navbar>
|
||||
<f7-toolbar tabbar position="top">
|
||||
<f7-link @click="currentTab = 'tree'" :tab-link-active="currentTab === 'tree'" class="tab-link">Design</f7-link>
|
||||
<f7-link @click="currentTab = 'code'" :tab-link-active="currentTab === 'code'" class="tab-link">Code</f7-link>
|
||||
</f7-toolbar>
|
||||
<f7-toolbar bottom class="toolbar-details" v-show="currentTab === 'tree'">
|
||||
<f7-link class="left details-link" @click="detailsOpened = true">Details</f7-link>
|
||||
<f7-link :disabled="selectedWidget != null" class="right" @click="selectedWidget = null">Clear</f7-link>
|
||||
</f7-toolbar>
|
||||
<f7-tabs class="sitemap-editor-tabs">
|
||||
<f7-tab id="tree" @tab:show="() => this.currentTab = 'tree'" :tab-active="currentTab === 'tree'">
|
||||
<f7-block v-if="!ready" class="text-align-center">
|
||||
<f7-preloader></f7-preloader>
|
||||
<div>Loading...</div>
|
||||
</f7-block>
|
||||
<f7-block v-else class="sitemap-tree-wrapper" :class="{ 'sheet-opened' : detailsOpened }">
|
||||
<f7-row v-if="currentTab === 'tree'">
|
||||
<f7-col width="100" medium="50">
|
||||
<f7-block strong class="sitemap-tree" no-gap @click.native="clearSelection">
|
||||
<f7-treeview>
|
||||
<sitemap-treeview-item :widget="sitemap" @selected="selectWidget" :selected="selectedWidget">
|
||||
</sitemap-treeview-item>
|
||||
</f7-treeview>
|
||||
</f7-block>
|
||||
</f7-col>
|
||||
<f7-col width="100" medium="50" class="details-pane">
|
||||
<f7-block v-if="selectedWidget" no-gap>
|
||||
<widget-details :widget="selectedWidget" :createMode="createMode" @remove="removeWidget" @movedown="moveWidgetDown" @moveup="moveWidgetUp" />
|
||||
</f7-block>
|
||||
<f7-block v-else>
|
||||
<div class="padding text-align-center">Nothing selected</div>
|
||||
</f7-block>
|
||||
<f7-block v-if="selectedWidget && ['Switch', 'Selection'].indexOf(selectedWidget.component) >= 0">
|
||||
<div><f7-block-title>Mappings</f7-block-title></div>
|
||||
<mapping-details :widget="selectedWidget" />
|
||||
</f7-block>
|
||||
<f7-block v-if="selectedWidget && canAddChildren">
|
||||
<div><f7-block-title>Add Child Widget</f7-block-title></div>
|
||||
<f7-card>
|
||||
<f7-card-content>
|
||||
<f7-list>
|
||||
<f7-list-button color="blue" :title="`Insert Widget Inside ${selectedWidget.component}`" actions-open="#widget-type-selection"></f7-list-button>
|
||||
</f7-list>
|
||||
</f7-card-content>
|
||||
</f7-card>
|
||||
</f7-block>
|
||||
</f7-col>
|
||||
</f7-row>
|
||||
</f7-block>
|
||||
|
||||
<f7-actions ref="widgetTypeSelection" id="widget-type-selection" :grid="true">
|
||||
<f7-actions-group>
|
||||
<f7-actions-button v-for="widgetType in widgetTypes" :key="widgetType.type" @click="addWidget(widgetType.type)">
|
||||
<f7-icon :f7="widgetType.icon" slot="media" />
|
||||
<span>{{widgetType.type}}</span>
|
||||
</f7-actions-button>
|
||||
</f7-actions-group>
|
||||
</f7-actions>
|
||||
</f7-tab>
|
||||
<f7-tab id="code" @tab:show="() => { this.currentTab = 'code' }" :tab-active="currentTab === 'code'">
|
||||
<sitemap-code v-if="currentTab === 'code'" :sitemap="sitemap" @updated="(value) => update(value)" />
|
||||
</f7-tab>
|
||||
|
||||
</f7-tabs>
|
||||
|
||||
<f7-fab class="add-to-sitemap-fab" v-if="canAddChildren" position="right-bottom" slot="fixed" color="blue" @click="$refs.widgetTypeSelection.open()">
|
||||
<f7-icon ios="f7:plus" md="material:add" aurora="f7:plus"></f7-icon>
|
||||
<f7-icon ios="f7:multiply" md="material:close" aurora="f7:multiply"></f7-icon>
|
||||
</f7-fab>
|
||||
|
||||
<f7-sheet class="sitemap-details-sheet" :backdrop="false" :close-on-escape="true" :opened="detailsOpened" @sheet:closed="detailsOpened = false">
|
||||
<f7-page>
|
||||
<f7-toolbar tabbar>
|
||||
<f7-link class="padding-left padding-right" :tab-link-active="detailsTab === 'item'" @click="detailsTab = 'widget'">Widget</f7-link>
|
||||
<f7-link class="padding-left padding-right" :tab-link-active="detailsTab === 'mappings'" @click="detailsTab = 'mappings'">Mappings</f7-link>
|
||||
<div class="right">
|
||||
<f7-link sheet-close class="padding-right"><f7-icon f7="chevron_down"></f7-icon></f7-link>
|
||||
</div>
|
||||
</f7-toolbar>
|
||||
<f7-block style="margin-bottom: 6rem" v-if="selectedWidget && detailsTab === 'widget'">
|
||||
<widget-details :widget="selectedWidget" :createMode="createMode" @remove="removeWidget" @movedown="moveWidgetDown" @moveup="moveWidgetUp" />
|
||||
</f7-block>
|
||||
<f7-block style="margin-bottom: 6rem" v-if="selectedWidget && detailsTab === 'mappings' && ['Switch', 'Selection'].indexOf(selectedWidget.component) >= 0">
|
||||
<mapping-details :widget="selectedWidget" />
|
||||
</f7-block>
|
||||
</f7-page>
|
||||
</f7-sheet>
|
||||
|
||||
</f7-page>
|
||||
</template>
|
||||
|
||||
<style lang="stylus">
|
||||
.sitemap-editor-tabs
|
||||
--f7-grid-gap 0px
|
||||
height calc(100% - var(--f7-toolbar-height))
|
||||
.tab
|
||||
height 100%
|
||||
|
||||
.sitemap-tree-wrapper
|
||||
padding 0
|
||||
margin-bottom 0
|
||||
.sitemap-tree
|
||||
padding 0
|
||||
border-right 1px solid var(--f7-block-strong-border-color)
|
||||
.treeview
|
||||
--f7-treeview-item-height 40px
|
||||
.treeview-item-label
|
||||
font-size 10pt
|
||||
white-space nowrap
|
||||
overflow-x hidden
|
||||
.subtitle
|
||||
font-size 8pt
|
||||
color var(--f7-list-item-footer-text-color)
|
||||
.sitemap-details-sheet
|
||||
z-index 10900
|
||||
.md .sitemap-details-sheet .toolbar .link
|
||||
width 35%
|
||||
|
||||
@media (min-width: 768px)
|
||||
.sitemap-tree-wrapper
|
||||
height 100%
|
||||
.row
|
||||
height 100%
|
||||
.col-100
|
||||
height 100%
|
||||
overflow auto
|
||||
.sitemap-tree
|
||||
min-height 100%
|
||||
margin 0
|
||||
height auto
|
||||
.details-pane
|
||||
padding-top 0
|
||||
.block
|
||||
margin-top 0
|
||||
.toolbar-details
|
||||
.details-link
|
||||
visibility hidden !important
|
||||
.add-to-sitemap-fab
|
||||
visibility hidden !important
|
||||
|
||||
@media (max-width: 767px)
|
||||
.details-pane
|
||||
display none
|
||||
.sitemap-tree-wrapper.sheet-opened
|
||||
margin-bottom var(--f7-sheet-height)
|
||||
.details-sheet
|
||||
height calc(1.4*var(--f7-sheet-height))
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import SitemapCode from '@/components/pagedesigner/sitemap/sitemap-code.vue'
|
||||
import WidgetDetails from '@/components/pagedesigner/sitemap/widget-details.vue'
|
||||
import MappingDetails from '@/components/pagedesigner/sitemap/mapping-details.vue'
|
||||
|
||||
function uuidv4 () {
|
||||
return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, c =>
|
||||
(c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
|
||||
)
|
||||
}
|
||||
|
||||
export default {
|
||||
components: {
|
||||
SitemapCode,
|
||||
WidgetDetails,
|
||||
MappingDetails
|
||||
},
|
||||
props: ['createMode', 'uid'],
|
||||
data () {
|
||||
return {
|
||||
ready: false,
|
||||
loading: false,
|
||||
sitemap: {
|
||||
uid: 'page_' + uuidv4().split('-')[0],
|
||||
component: 'Sitemap',
|
||||
config: {
|
||||
label: 'New Sitemap'
|
||||
},
|
||||
tags: [],
|
||||
slots: { widgets: [] }
|
||||
},
|
||||
selectedWidget: null,
|
||||
selectedWidgetParent: null,
|
||||
previousSelection: null,
|
||||
detailsOpened: false,
|
||||
detailsTab: 'widget',
|
||||
currentTab: 'tree',
|
||||
eventSource: null,
|
||||
widgetTypes: [
|
||||
{ type: 'Text', icon: 'textformat' },
|
||||
{ type: 'Switch', icon: 'power' },
|
||||
{ type: 'Selection', icon: 'text_justify' },
|
||||
{ type: 'Slider', icon: 'slider_horizontal_3' },
|
||||
{ type: 'Frame', icon: 'macwindow' },
|
||||
{ type: 'Setpoint', icon: 'plus_slash_minus' },
|
||||
{ type: 'Default', icon: 'rectangle' },
|
||||
{ type: 'Group', icon: 'square_stack_3d_down_right' },
|
||||
{ type: 'Chart', icon: 'chart_bar_square' },
|
||||
{ type: 'Webview', icon: 'globe' },
|
||||
{ type: 'Colorpicker', icon: 'drop' },
|
||||
{ type: 'Mapview', icon: 'map' },
|
||||
{ type: 'List', icon: 'square_list' },
|
||||
{ type: 'Image', icon: 'photo' },
|
||||
{ type: 'Video', icon: 'videocam' }
|
||||
],
|
||||
linkableWidgetTypes: ['Sitemap', 'Text', 'Frame', 'Group', 'Image']
|
||||
}
|
||||
},
|
||||
created () {
|
||||
|
||||
},
|
||||
computed: {
|
||||
canAddChildren () {
|
||||
if (!this.selectedWidget) return false
|
||||
if (this.linkableWidgetTypes.indexOf(this.selectedWidget.component) < 0) return false
|
||||
return true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onPageAfterIn () {
|
||||
if (window) {
|
||||
window.addEventListener('keydown', this.keyDown)
|
||||
}
|
||||
this.load()
|
||||
},
|
||||
onPageBeforeOut () {
|
||||
if (window) {
|
||||
window.removeEventListener('keydown', this.keyDown)
|
||||
}
|
||||
this.detailsOpened = false
|
||||
},
|
||||
keyDown (ev) {
|
||||
if (ev.keyCode === 83 && (ev.ctrlKey || ev.metaKey)) {
|
||||
if (this.createMode) return // not supported!
|
||||
this.save(true)
|
||||
ev.stopPropagation()
|
||||
ev.preventDefault()
|
||||
}
|
||||
},
|
||||
load () {
|
||||
if (this.loading) return
|
||||
this.loading = true
|
||||
|
||||
if (this.createMode) {
|
||||
this.loading = false
|
||||
this.ready = true
|
||||
} else {
|
||||
this.$oh.api.get('/rest/ui/components/system:sitemap/' + this.uid).then((data) => {
|
||||
this.$set(this, 'sitemap', data)
|
||||
this.ready = true
|
||||
this.loading = false
|
||||
})
|
||||
}
|
||||
},
|
||||
save (stay) {
|
||||
if (!this.sitemap.uid) {
|
||||
this.$f7.dialog.alert('Please give an ID to the sitemap')
|
||||
return
|
||||
}
|
||||
if (!this.sitemap.config.label) {
|
||||
this.$f7.dialog.alert('Please give an label to the sitemap')
|
||||
return
|
||||
}
|
||||
if (!this.createMode && this.uid !== this.sitemap.uid) {
|
||||
this.$f7.dialog.alert('You cannot change the ID of an existing sitemap. Duplicate it with the new ID then delete this one.')
|
||||
return
|
||||
}
|
||||
|
||||
const promise = (this.createMode)
|
||||
? this.$oh.api.postPlain('/rest/ui/components/system:sitemap', JSON.stringify(this.sitemap), 'text/plain', 'application/json')
|
||||
: this.$oh.api.put('/rest/ui/components/system:sitemap/' + this.sitemap.uid, this.sitemap)
|
||||
promise.then((data) => {
|
||||
if (this.createMode) {
|
||||
this.$f7.toast.create({
|
||||
text: 'Sitemap created',
|
||||
destroyOnClose: true,
|
||||
closeTimeout: 2000
|
||||
}).open()
|
||||
this.load()
|
||||
} else {
|
||||
this.$f7.toast.create({
|
||||
text: 'Sitemap updated',
|
||||
destroyOnClose: true,
|
||||
closeTimeout: 2000
|
||||
}).open()
|
||||
}
|
||||
this.$f7.emit('sidebarRefresh', null)
|
||||
if (!stay) this.$f7router.back()
|
||||
}).catch((err) => {
|
||||
this.$f7.toast.create({
|
||||
text: 'Error while saving sitemap: ' + err,
|
||||
destroyOnClose: true,
|
||||
closeTimeout: 2000
|
||||
}).open()
|
||||
})
|
||||
},
|
||||
cleanConfig (widget) {
|
||||
if (widget.config) {
|
||||
for (let key in widget.config) {
|
||||
if (!widget.config[key]) {
|
||||
delete widget.config[key]
|
||||
}
|
||||
}
|
||||
}
|
||||
if (widget.slots && widget.slots.widgets) {
|
||||
widget.slots.widgets.forEach(this.cleanConfig)
|
||||
}
|
||||
},
|
||||
update (value) {
|
||||
this.selectedWidget = null
|
||||
this.selectedWidgetParent = null
|
||||
this.$set(this, 'sitemap', value)
|
||||
this.cleanConfig(this.sitemap)
|
||||
},
|
||||
startEventSource () {
|
||||
|
||||
},
|
||||
stopEventSource () {
|
||||
|
||||
},
|
||||
removeWidget () {
|
||||
this.selectedWidgetParent.slots.widgets.splice(this.selectedWidgetParent.slots.widgets.indexOf(this.selectedWidget), 1)
|
||||
if (!this.selectedWidgetParent.slots.widgets.length) {
|
||||
delete this.selectedWidgetParent.slots
|
||||
}
|
||||
this.selectedWidget = null
|
||||
this.selectedWidgetParent = null
|
||||
},
|
||||
moveWidgetDown () {
|
||||
let widgets = this.selectedWidgetParent.slots.widgets
|
||||
let pos = widgets.indexOf(this.selectedWidget)
|
||||
if (pos >= widgets.length - 1) return
|
||||
widgets.splice(pos, 1)
|
||||
widgets.splice(pos + 1, 0, this.selectedWidget)
|
||||
},
|
||||
moveWidgetUp () {
|
||||
let widgets = this.selectedWidgetParent.slots.widgets
|
||||
let pos = widgets.indexOf(this.selectedWidget)
|
||||
if (pos <= 0) return
|
||||
widgets.splice(pos, 1)
|
||||
widgets.splice(pos - 1, 0, this.selectedWidget)
|
||||
},
|
||||
selectWidget (widgets) {
|
||||
const widget = widgets[0]
|
||||
const parentWidget = widgets[1]
|
||||
this.selectedWidget = null
|
||||
this.selectedWidgetParent = null
|
||||
this.$nextTick(() => {
|
||||
this.selectedWidget = widget
|
||||
this.selectedWidgetParent = parentWidget
|
||||
})
|
||||
},
|
||||
clearSelection (ev) {
|
||||
if (ev.target && ev.currentTarget && ev.target === ev.currentTarget) {
|
||||
this.selectedWidget = null
|
||||
this.selectedWidgetParent = null
|
||||
}
|
||||
},
|
||||
addWidget (widgetType) {
|
||||
if (!this.selectedWidget.slots) {
|
||||
this.$set(this.selectedWidget, 'slots', { widgets: [] })
|
||||
}
|
||||
const widget = {
|
||||
component: widgetType,
|
||||
config: {}
|
||||
}
|
||||
this.selectedWidget.slots.widgets.push(widget)
|
||||
this.selectWidget([widget, this.selectedWidget])
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -57,7 +57,7 @@
|
|||
</f7-list>
|
||||
</f7-col>
|
||||
<f7-col v-if="ready">
|
||||
<f7-block-title v-show="rules.length" class="searchbar-hide-on-search">{{rules.length}} rules</f7-block-title>
|
||||
<f7-block-title class="searchbar-hide-on-search">{{rules.length}} rules</f7-block-title>
|
||||
<f7-list
|
||||
v-show="rules.length > 0"
|
||||
class="searchbar-found col rules-list"
|
||||
|
|
|
@ -33,6 +33,14 @@
|
|||
:footer="objectsSubtitles.things">
|
||||
<f7-icon slot="media" f7="lightbulb" color="gray"></f7-icon>
|
||||
</f7-list-item>
|
||||
<f7-list-item
|
||||
media-item
|
||||
link="model/"
|
||||
title="Model"
|
||||
badge-color="blue"
|
||||
:footer="objectsSubtitles.model">
|
||||
<f7-icon slot="media" f7="list_bullet_indent" color="gray"></f7-icon>
|
||||
</f7-list-item>
|
||||
<f7-list-item
|
||||
media-item
|
||||
link="items/"
|
||||
|
@ -43,20 +51,12 @@
|
|||
<f7-icon slot="media" f7="square_on_circle" color="gray"></f7-icon>
|
||||
</f7-list-item>
|
||||
<f7-list-item
|
||||
media-item
|
||||
link="model/"
|
||||
title="Model"
|
||||
link="pages/"
|
||||
title="Pages"
|
||||
badge-color="blue"
|
||||
:footer="objectsSubtitles.model">
|
||||
<f7-icon slot="media" f7="list_bullet_indent" color="gray"></f7-icon>
|
||||
:footer="objectsSubtitles.pages">
|
||||
<f7-icon slot="media" f7="tv" color="gray"></f7-icon>
|
||||
</f7-list-item>
|
||||
<!-- <f7-list-item
|
||||
link="items-virtual/"
|
||||
title="Items (virtual)"
|
||||
:after="itemsCount"
|
||||
badge-color="blue"
|
||||
:footer="objectsSubtitles.items"
|
||||
></f7-list-item> -->
|
||||
<f7-list-item
|
||||
media-item
|
||||
link="rules/"
|
||||
|
@ -123,8 +123,9 @@ export default {
|
|||
otherServices: [],
|
||||
objectsSubtitles: {
|
||||
things: 'Manage the physical layer',
|
||||
items: 'Manage the functional layer',
|
||||
model: 'The semantic model of your home',
|
||||
items: 'Manage the functional layer',
|
||||
pages: 'Design displays for user control & monitoring',
|
||||
rules: 'Automate with triggers and actions',
|
||||
schedule: 'View upcoming time-based rules'
|
||||
},
|
||||
|
|
|
@ -7,6 +7,9 @@
|
|||
<f7-nav-title-large v-if="isRoot">{{sitemap.title}}</f7-nav-title-large>
|
||||
<f7-nav-title>{{sitemap.title}}</f7-nav-title>
|
||||
</f7-navbar>
|
||||
<f7-toolbar position="bottom">
|
||||
<span class="text-color-red">Warning: sitemaps are not functional. Please use Basic UI.</span>
|
||||
</f7-toolbar>
|
||||
<f7-block class="block-narrow" v-if="sitemap.widgets && sitemap.widgets.length > 0">
|
||||
<f7-row>
|
||||
<f7-col>
|
||||
|
@ -30,12 +33,8 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
// import SitemapWidgetGeneric from '../components/sitemap/widget-generic.vue'
|
||||
|
||||
export default {
|
||||
// components: {
|
||||
// SitemapWidgetGeneric
|
||||
// },
|
||||
props: ['sitemapId', 'pageId'],
|
||||
data () {
|
||||
return {
|
||||
|
@ -46,11 +45,6 @@ export default {
|
|||
this.$oh.api.get('/rest/sitemaps/' + this.sitemapId + '/' + this.pageId).then(data => {
|
||||
this.sitemap = data
|
||||
})
|
||||
this.$f7.toast.create({
|
||||
text: 'The sitemap rendering is currently for demonstration purposes only. It is not functional nor updates in real time. Please use another app like Basic UI or HABPanel to interact with your items.',
|
||||
closeButton: true,
|
||||
destroyOnClose: true
|
||||
}).open()
|
||||
},
|
||||
computed: {
|
||||
isRoot () {
|
||||
|
|
Loading…
Reference in New Issue