Home page model cards rewrite (#562)
Refactored the experimental-grade code with some mixins, better separated code. Implements #555: Allow the home page cards to be customized - reordered, grouped, and edited, with config & slots and a dedicated editor. Implements #556: Add glance badges to the location cards (other types tbd.) See #556 for the specifications on how to organize your model to get the badges. Styling fixes and cleanups. Signed-off-by: Yannick Schaus <github@schaus.net>pull/566/head
parent
69eede2135
commit
a8fd16f8c9
|
@ -0,0 +1,77 @@
|
|||
import { WidgetDefinition, pt, pi, pg, pb, pn, po } from '../helpers'
|
||||
import { actionGroup, actionParams } from '../actions'
|
||||
|
||||
export const OhHomePageDefinition = () => new WidgetDefinition('oh-home-page', 'Home page')
|
||||
.params([
|
||||
pt('displayModelCardsTo', 'Display model cards to', 'Restrict who sees the Locations/Equipment/Properties tabs with the model cards')
|
||||
.o([
|
||||
{ value: 'role:administrator', label: 'Administrators' },
|
||||
{ value: 'role:user', label: 'Users' }
|
||||
]).m(),
|
||||
pt('allowChatInputTo', 'Allow chat input box to', 'Restrict who can interact with the chatbot input box on the top of the Overview tab (when available)')
|
||||
.o([
|
||||
{ value: 'role:administrator', label: 'Administrators' },
|
||||
{ value: 'role:user', label: 'Users' }
|
||||
]).m(),
|
||||
pt('hiddenModelTabs', 'Hidden Model Tabs', 'Hide individual model exploring tabs from view')
|
||||
.o([
|
||||
{ value: 'locations', label: 'Locations' },
|
||||
{ value: 'equipment', label: 'Equipment' },
|
||||
{ value: 'properties', label: 'Properties' }
|
||||
]).m()
|
||||
])
|
||||
|
||||
const ModelCardParameterGroup = () => pg('card', 'Model Card', 'General settings for this card')
|
||||
|
||||
const ModelCardParameters = () => [
|
||||
pt('title', 'Title', 'Title of the card'),
|
||||
pt('subtitle', 'Subtitle', 'Subtitle of the card'),
|
||||
pt('backgroundColor', 'Background Color', 'Color of the card\'s background; if unset, choose automatically from built-in defaults for certain semantic classes')
|
||||
.o([
|
||||
{ value: 'red', label: 'Red' },
|
||||
{ value: 'green', label: 'Green' },
|
||||
{ value: 'blue', label: 'Blue' },
|
||||
{ value: 'pink', label: 'Pink' },
|
||||
{ value: 'yellow', label: 'Yellow' },
|
||||
{ color: 'orange', label: 'Orange' },
|
||||
{ value: 'purple', label: 'Purple' },
|
||||
{ value: 'deeppurple', label: 'Deep Purple' },
|
||||
{ value: 'lightblue', label: 'Light Blue' },
|
||||
{ value: 'teal', label: 'Teal' },
|
||||
{ value: 'lime', label: 'Lime' },
|
||||
{ value: 'deeporange', label: 'Deep Orange' },
|
||||
{ value: 'gray', label: 'Gray' },
|
||||
{ value: 'black', label: 'Black' }
|
||||
]),
|
||||
pt('backgroundImage', 'Background Image', 'URL of an image to display in the background'),
|
||||
pb('invertText', 'Invert Text', 'Display the text in black (for light backgrounds)')
|
||||
]
|
||||
|
||||
export const OhLocationCardParameters = () => new WidgetDefinition('oh-location-card', 'Location Card', 'A card showing model items in a certain location')
|
||||
.paramGroup(ModelCardParameterGroup(), ModelCardParameters())
|
||||
.paramGroup(pg('glance', 'Card at-a-glance badges'), [
|
||||
pb('disableBadges', 'Disable badges', 'Do not examine items to display badges - can help with performance if you don\'t need them.'),
|
||||
pt('badges', 'Enabled badges', 'Select the badges you wish to show in the header of the card. Display all if none are selected.')
|
||||
.o([
|
||||
{ value: 'lights', label: 'Lights On' },
|
||||
{ value: 'windows', label: 'Open Windows' },
|
||||
{ value: 'doors', label: 'Open Doors' },
|
||||
{ value: 'garagedoors', label: 'Open Garage Doors' },
|
||||
{ value: 'blinds', label: 'Open Blinds' },
|
||||
{ value: 'presence', label: 'Presence Detected' },
|
||||
{ value: 'lock', label: 'Locks' },
|
||||
{ value: 'climate', label: 'Climate Control Powered On' },
|
||||
{ value: 'screens', label: 'Screens Powered On' },
|
||||
{ value: 'projectors', label: 'Projectors Powered On' },
|
||||
{ value: 'speakers', label: 'Speakers/AV Receivers Powered On' },
|
||||
{ value: 'temperature', label: 'Average Temperature (+ Setpoint)' },
|
||||
{ value: 'humidity', label: 'Average Humidity' },
|
||||
{ value: 'luminance', label: 'Average Luminance' }
|
||||
], true, true)
|
||||
])
|
||||
|
||||
export const OhEquipmentCardParameters = () => new WidgetDefinition('oh-equipment-card', 'Equipment Class Card', 'A card showing model items belonging to a certain equipment class')
|
||||
.paramGroup(ModelCardParameterGroup(), ModelCardParameters())
|
||||
|
||||
export const OhPropertyCardParameters = () => new WidgetDefinition('oh-property-card', 'Property Card', 'A card showing model items related to a certain property')
|
||||
.paramGroup(ModelCardParameterGroup(), ModelCardParameters())
|
|
@ -1,5 +1,8 @@
|
|||
import mixin from '@/components/widgets/widget-mixin'
|
||||
|
||||
export default {
|
||||
props: ['color', 'type', 'header', 'title', 'subtitle', 'items'],
|
||||
mixins: [mixin],
|
||||
props: ['type', 'element'],
|
||||
data () {
|
||||
return {
|
||||
opened: false,
|
||||
|
@ -12,9 +15,59 @@ export default {
|
|||
beforeDestroy () {
|
||||
window.removeEventListener('popstate', this.back)
|
||||
},
|
||||
computed: {
|
||||
title () {
|
||||
if (this.config.title) return this.config.title
|
||||
return (this.element) ? this.element.defaultTitle : '?'
|
||||
},
|
||||
subtitle () {
|
||||
if (this.config.subtitle) return this.config.subtitle
|
||||
return (this.element) ? this.element.defaultSubtitle : '?'
|
||||
},
|
||||
color () {
|
||||
if (this.config.backgroundColor) {
|
||||
return this.config.backgroundColor
|
||||
}
|
||||
switch (this.type) {
|
||||
case 'location':
|
||||
const item = this.element.item
|
||||
if (item.metadata.semantics.value.indexOf('LivingRoom') > 0) return 'orange'
|
||||
if (item.metadata.semantics.value.indexOf('Kitchen') > 0) return 'deeporange'
|
||||
if (item.metadata.semantics.value.indexOf('Bedroom') > 0) return 'pink'
|
||||
if (item.metadata.semantics.value.indexOf('Bathroom') > 0) return 'blue'
|
||||
if (item.metadata.semantics.value.indexOf('_Room') > 0) return 'lightblue'
|
||||
if (item.metadata.semantics.value.indexOf('_Floor') > 0) return 'deeppurple'
|
||||
if (item.metadata.semantics.value.indexOf('_Outdoor') > 0) return 'green'
|
||||
return 'gray'
|
||||
case 'equipment':
|
||||
const equipmentType = this.element.key
|
||||
if (equipmentType === 'HVAC') return 'red'
|
||||
if (equipmentType === 'Lightbulb') return 'yellow'
|
||||
if (equipmentType === 'Window') return 'blue'
|
||||
if (equipmentType === 'Door') return 'green'
|
||||
if (equipmentType === 'Camera') return 'pink'
|
||||
if (equipmentType === 'Blinds') return 'teal'
|
||||
if (equipmentType === 'SmokeDetector' || equipmentType === 'Siren') return 'deeppurple'
|
||||
// etc. - use a map
|
||||
return 'gray'
|
||||
case 'property':
|
||||
const property = this.element.key
|
||||
if (property === 'Temperature') return 'red'
|
||||
if (property === 'Light') return 'orange'
|
||||
if (property === 'Humidity') return 'blue'
|
||||
if (property === 'Presence') return 'teal'
|
||||
if (property === 'Pressure') return 'deeppurple'
|
||||
// etc. - use a map
|
||||
return 'gray'
|
||||
default:
|
||||
return 'gray'
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
cardOpening () {
|
||||
history.pushState({ cardId: this.cardId }, null, window.location.href.split('#card=')[0] + '#' + this.$f7.utils.serializeObject({ card: this.cardId }))
|
||||
this.cardId = this.title + '-' + this.$f7.utils.id()
|
||||
history.pushState({ cardId: this.cardId }, null, window.location.href.split('#card=')[0] + '#' + this.$f7.utils.serializeObject({ card: this.element.key }))
|
||||
setTimeout(() => { this.opened = true })
|
||||
},
|
||||
cardClosed () {
|
||||
|
@ -25,7 +78,7 @@ export default {
|
|||
setTimeout(() => { this.$f7.card.close(this.$refs.card.$el) }, 100)
|
||||
},
|
||||
back (evt) {
|
||||
console.log(evt.state)
|
||||
console.debug(evt.state)
|
||||
if (!evt.state || evt.state.cardId === this.cardId) return
|
||||
if (this.opened) this.closeCard()
|
||||
}
|
||||
|
|
|
@ -0,0 +1,67 @@
|
|||
<template>
|
||||
<model-card type="equipment" :context="context" :element="element" header-height="150px">
|
||||
<template v-slot:glance>
|
||||
<div v-if="context && context.component.slots && context.component.slots.glance" class="display-flex flex-direction-column align-items-flex-start">
|
||||
<generic-widget-component :context="childContext(slotComponent)" v-for="(slotComponent, idx) in context.component.slots.glance" :key="'glance-' + idx" @command="onCommand" />
|
||||
</div>
|
||||
<!-- <div class="equipment-stats" v-else><small v-if="element.equipment">{{element.equipment.length}}</small></div> -->
|
||||
</template>
|
||||
<div class="card-content-padding">
|
||||
<generic-widget-component :context="listContext" />
|
||||
<p>
|
||||
<f7-button fill round large card-close :color="color" class="margin-horizontal">Close</f7-button>
|
||||
</p>
|
||||
</div>
|
||||
</model-card>
|
||||
</template>
|
||||
|
||||
<style lang="stylus">
|
||||
.equipment-card
|
||||
height 150px
|
||||
.equipment-stats
|
||||
font-weight normal
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import mixin from '@/components/widgets/widget-mixin'
|
||||
import itemDefaultListComponent from '@/components/widgets/standard/list/default-list-item'
|
||||
import CardMixin from './card-mixin'
|
||||
import ModelCard from './model-card.vue'
|
||||
|
||||
export default {
|
||||
mixins: [mixin, CardMixin],
|
||||
components: {
|
||||
ModelCard
|
||||
},
|
||||
computed: {
|
||||
listContext () {
|
||||
const standaloneEquipment = this.element.equipment.filter((i) => i.points.length === 0).map((i) => itemDefaultListComponent(i.item, true))
|
||||
const equipmentWithPoints = this.element.equipment.filter((i) => i.points.length !== 0).map((i) => {
|
||||
return [
|
||||
{
|
||||
component: 'oh-list-item',
|
||||
config: {
|
||||
title: i.item.label || i.item.name,
|
||||
divider: true
|
||||
}
|
||||
},
|
||||
...i.points.map((p) => itemDefaultListComponent(p))
|
||||
]
|
||||
})
|
||||
|
||||
return {
|
||||
store: this.$store.getters.trackedItems,
|
||||
component: {
|
||||
component: 'oh-list',
|
||||
config: {
|
||||
mediaList: true
|
||||
},
|
||||
slots: {
|
||||
default: [...standaloneEquipment, ...equipmentWithPoints].flat()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -1,73 +0,0 @@
|
|||
<template>
|
||||
<f7-card expandable ref="card" class="equipments-card" :animate="$f7.data.themeOptions.expandableCardAnimation !== 'disabled'" card-tablet-fullscreen v-on:card:opened="cardOpening" v-on:card:closed="cardClosed">
|
||||
<f7-card-content :padding="false">
|
||||
<div :class="`bg-color-${color}`" :style="{height: '150px'}">
|
||||
<f7-card-header text-color="white" class="display-block">
|
||||
{{title || 'Something'}}
|
||||
<div class="equipment-stats"><small v-if="subtitle">{{subtitle}}</small></div>
|
||||
<br>
|
||||
<!-- <h1>State</h1> -->
|
||||
</f7-card-header>
|
||||
<f7-link
|
||||
card-close
|
||||
color="white"
|
||||
class="card-opened-fade-in"
|
||||
:style="{position: 'absolute', right: '15px', top: '15px'}"
|
||||
icon-f7="multiply_circle_fill"
|
||||
></f7-link>
|
||||
</div>
|
||||
<div v-if="opened">
|
||||
<generic-widget-component :context="listContext" />
|
||||
<p>
|
||||
<f7-button fill round large card-close :color="color" class="margin-horizontal">Close</f7-button>
|
||||
</p>
|
||||
</div>
|
||||
</f7-card-content>
|
||||
</f7-card>
|
||||
</template>
|
||||
|
||||
<style lang="stylus">
|
||||
.equipments-card
|
||||
height 150px
|
||||
.equipment-stats
|
||||
font-weight normal
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import itemDefaultListComponent from '@/components/widgets/standard/list/default-list-item'
|
||||
import CardMixin from './card-mixin'
|
||||
|
||||
export default {
|
||||
mixins: [CardMixin],
|
||||
computed: {
|
||||
listContext () {
|
||||
const standaloneEquipments = this.items.filter((i) => i.points.length === 0).map((i) => itemDefaultListComponent(i.item, true))
|
||||
const equipmentsWithPoints = this.items.filter((i) => i.points.length !== 0).map((i) => {
|
||||
return [
|
||||
{
|
||||
component: 'oh-list-item',
|
||||
config: {
|
||||
title: i.item.label || i.item.name,
|
||||
divider: true
|
||||
}
|
||||
},
|
||||
...i.points.map((p) => itemDefaultListComponent(p))
|
||||
]
|
||||
})
|
||||
|
||||
return {
|
||||
store: this.$store.getters.trackedItems,
|
||||
component: {
|
||||
component: 'oh-list',
|
||||
config: {
|
||||
mediaList: true
|
||||
},
|
||||
slots: {
|
||||
default: [...standaloneEquipments, ...equipmentsWithPoints].flat()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,32 @@
|
|||
/**
|
||||
* Retrieves equipment based on their semantic class
|
||||
* @param {Array} arr the array of equipment items & points to search
|
||||
* @param {String} value the semantic class (value) to find
|
||||
* @param {Boolean} partial match subclasses
|
||||
*/
|
||||
export function findEquipment (arr, value, partial) {
|
||||
return arr.filter((e) => (partial) ? e.item.metadata.semantics.value.indexOf(value) === 0 : e.item.metadata.semantics.value === value)
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the flatten list of points from the provided equipment collection
|
||||
* @param {Array} equipment the equipment collection
|
||||
*/
|
||||
export function allEquipmentPoints (equipment) {
|
||||
return equipment.map((e) => e.points || []).flat()
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves points based on their semantic class and eventual related property
|
||||
* @param {Array} arr the array of source points
|
||||
* @param {String} value the semantic class (value) to find
|
||||
* @param {Boolean} partial match subclasses
|
||||
* @param {String} property return only points also related to this property
|
||||
*/
|
||||
export function findPoints (arr, value, partial, property) {
|
||||
console.debug(arr)
|
||||
const points = arr.filter((p) => (partial) ? p.metadata.semantics.value.indexOf(value) === 0 : p.metadata.semantics.value === value)
|
||||
if (!property) return points
|
||||
console.debug('found1' + points)
|
||||
return points.filter((p) => p.metadata.semantics.config.relatesTo === property)
|
||||
}
|
|
@ -0,0 +1,104 @@
|
|||
<template>
|
||||
<span class="padding-right location-status-badge" v-show="reduce" :class="{ invert: invertColor }">
|
||||
<oh-icon v-if="config.icon.indexOf('oh:') === 0" :icon="config.icon.replace('oh:', '')" :state="config.state" class="oh-icon-badge" width="20" height="20" />
|
||||
<f7-icon v-else-if="config.icon.indexOf('f7:') === 0" :f7="config.icon.replace('f7:', '')" :color="config.invertText ? 'black' : 'white'" class="f7-icon-badge" size="20" />
|
||||
<!-- <oh-icon v-if="config.icon.indexOf('oh:') === 0 && config.stateOff" v-show="!reduce" icon="config.icon.replace('oh:', '')" :state="config.stateOff" class="oh-icon-badge" width="20" height="20" /> -->
|
||||
<span class="glance-label" v-show="reduce">{{reduce}} {{config.unit}}</span>
|
||||
<span class="glance-label" v-show="reduceAux"><small>({{reduceAux}} {{config.unit}})</small></span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<style lang="stylus">
|
||||
.location-status-badge
|
||||
.oh-icon-badge
|
||||
filter brightness(100)
|
||||
&.invert .oh-icon-badge
|
||||
filter brightness(0)
|
||||
.f7-icon-badge
|
||||
line-height 20px
|
||||
margin-top -7px
|
||||
.glance-label
|
||||
line-height 20px
|
||||
vertical-align top
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import { allEquipmentPoints, findPoints } from '../glance-helpers'
|
||||
|
||||
export default {
|
||||
props: ['element', 'type', 'customConfig', 'invertColor', 'store'],
|
||||
data () {
|
||||
return {
|
||||
badgeConfigs: {
|
||||
temperature: { icon: 'f7:thermometer', unit: '°' },
|
||||
humidity: { icon: 'f7:drop', unit: '%' },
|
||||
luminance: { icon: 'f7:sun_min', unit: 'lux' }
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
config () {
|
||||
return this.badgeConfigs[this.type]
|
||||
},
|
||||
query () {
|
||||
let direct
|
||||
switch (this.type) {
|
||||
case 'temperature':
|
||||
direct = findPoints(this.element.properties, 'Point_Measurement', true, 'Property_Temperature')
|
||||
if (direct.length) return direct
|
||||
return findPoints(allEquipmentPoints(this.element.equipment), 'Point_Measurement', true, 'Property_Temperature')
|
||||
case 'humidity':
|
||||
direct = findPoints(this.element.properties, 'Point_Measurement', true, 'Property_Humidity')
|
||||
if (direct.length) return direct
|
||||
return findPoints(allEquipmentPoints(this.element.equipment), 'Point_Measurement', true, 'Property_Humidity')
|
||||
case 'luminance':
|
||||
direct = findPoints(this.element.properties, 'Point_Measurement', true, 'Property_Light')
|
||||
if (direct.length) return direct
|
||||
return findPoints(allEquipmentPoints(this.element.equipment), 'Point_Measurement', true, 'Property_Light')
|
||||
default:
|
||||
return []
|
||||
}
|
||||
},
|
||||
queryAux () {
|
||||
let direct
|
||||
switch (this.type) {
|
||||
case 'temperature':
|
||||
direct = findPoints(this.element.properties, 'Point_Setpoint', true, 'Property_Temperature')
|
||||
if (direct.length) return direct
|
||||
return findPoints(allEquipmentPoints(this.element.equipment), 'Point_Setpoint', true, 'Property_Temperature')
|
||||
default:
|
||||
return []
|
||||
}
|
||||
},
|
||||
map () {
|
||||
return this.query.map((item) => this.store[item.name].state)
|
||||
},
|
||||
mapAux () {
|
||||
return this.queryAux.map((item) => this.store[item.name].state)
|
||||
},
|
||||
reduce () {
|
||||
const ret = this.map.reduce((avg, state, arr, { length }) => {
|
||||
const value = Number.parseFloat(state.split(' ')[0])
|
||||
if (Number.isFinite(value)) {
|
||||
return avg + value / length
|
||||
}
|
||||
return avg
|
||||
}, 0)
|
||||
|
||||
return (this.type === 'temperature') ? Math.round(ret * 10) / 10 : Math.round(ret)
|
||||
},
|
||||
reduceAux () {
|
||||
if (this.type !== 'temperature') return undefined
|
||||
const ret = this.mapAux.reduce((avg, state, arr, { length }) => {
|
||||
const value = Number.parseFloat(state.split(' ')[0])
|
||||
if (Number.isFinite(value)) {
|
||||
return avg + value / length
|
||||
}
|
||||
return avg
|
||||
}, 0)
|
||||
|
||||
return Math.round(ret * 10) / 10
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,165 @@
|
|||
<template>
|
||||
<span class="padding-right location-status-badge" v-show="reduce" :class="{ invert: invertColor }">
|
||||
<oh-icon v-if="config.icon.indexOf('oh:') === 0" :icon="config.icon.replace('oh:', '')" :state="config.state" class="oh-icon-badge" width="20" height="20" />
|
||||
<f7-icon v-else-if="config.icon.indexOf('f7:') === 0" :f7="config.icon.replace('f7:', '')" :color="config.invertText ? 'black' : 'white'" class="f7-icon-badge" size="20" />
|
||||
<!-- <oh-icon v-if="config.icon.indexOf('oh:') === 0 && config.stateOff" v-show="!reduce" icon="config.icon.replace('oh:', '')" :state="config.stateOff" class="oh-icon-badge" width="20" height="20" /> -->
|
||||
<span class="glance-label" v-show="reduce > 1">{{reduce}}</span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<style lang="stylus">
|
||||
.location-status-badge
|
||||
.oh-icon-badge
|
||||
filter brightness(100)
|
||||
&.invert .oh-icon-badge
|
||||
filter brightness(0)
|
||||
.f7-icon-badge
|
||||
line-height 20px
|
||||
margin-top -7px
|
||||
.glance-label
|
||||
line-height 20px
|
||||
vertical-align top
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import { findEquipment, allEquipmentPoints, findPoints } from '../glance-helpers'
|
||||
|
||||
export default {
|
||||
props: ['element', 'type', 'customConfig', 'invertColor', 'store'],
|
||||
data () {
|
||||
return {
|
||||
badgeConfigs: {
|
||||
lights: { icon: 'oh:lightbulb' },
|
||||
windows: { icon: 'oh:window', state: 'open' },
|
||||
doors: { icon: 'oh:door', state: 'open' },
|
||||
garagedoors: { icon: 'oh:garagedoor', state: 'open' },
|
||||
blinds: { icon: 'oh:cinemascreen', state: '100' },
|
||||
presence: { icon: 'oh:motion', state: 'on' },
|
||||
lock: { icon: 'oh:lock', state: 'open', stateOff: 'closed' },
|
||||
climate: { icon: 'oh:climate', state: 'on' },
|
||||
screens: { icon: 'f7:tv' },
|
||||
projectors: { icon: 'f7:videocam_fill' },
|
||||
speakers: { icon: 'f7:speaker_2_fill' }
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
config () {
|
||||
return this.badgeConfigs[this.type]
|
||||
},
|
||||
query () {
|
||||
let direct, equipment, allPoints, points
|
||||
switch (this.type) {
|
||||
case 'lights':
|
||||
direct = findPoints(this.element.properties, 'Point_Control', true, 'Property_Light')
|
||||
if (direct.length) return direct
|
||||
return findPoints(allEquipmentPoints(this.element.equipment), 'Point_Control', true, 'Property_Light')
|
||||
case 'windows':
|
||||
equipment = findEquipment(this.element.equipment, 'Equipment_Window', false)
|
||||
if (!equipment.length) return []
|
||||
allPoints = allEquipmentPoints(equipment)
|
||||
points = findPoints(allPoints, 'Point_Status_OpenState', false)
|
||||
if (points.length) return points
|
||||
return equipment.filter((e) => e.points.length === 0).map((e) => e.item)
|
||||
case 'doors':
|
||||
equipment = [
|
||||
...findEquipment(this.element.equipment, 'Equipment_Door', false),
|
||||
...findEquipment(this.element.equipment, 'Equipment_Door_FrontDoor', false)
|
||||
]
|
||||
if (!equipment.length) return []
|
||||
allPoints = allEquipmentPoints(equipment)
|
||||
points = findPoints(allPoints, 'Point_Status_OpenState', false)
|
||||
if (points.length) return points
|
||||
return equipment.filter((e) => e.points.length === 0).map((e) => e.item)
|
||||
case 'garagedoors':
|
||||
equipment = findEquipment(this.element.equipment, 'Equipment_Door_GarageDoor', false)
|
||||
if (!equipment.length) return []
|
||||
allPoints = allEquipmentPoints(equipment)
|
||||
points = findPoints(allPoints, 'Point_Status_OpenState', false)
|
||||
if (points.length) return points
|
||||
return equipment.filter((e) => e.points.length === 0).map((e) => e.item)
|
||||
case 'blinds':
|
||||
equipment = findEquipment(this.element.equipment, 'Equipment_Blinds', false)
|
||||
if (!equipment.length) return []
|
||||
allPoints = allEquipmentPoints(equipment)
|
||||
points = findPoints(allPoints, 'Point_Status_OpenState', false)
|
||||
if (points.length) return points
|
||||
return equipment.filter((e) => e.points.length === 0).map((e) => e.item)
|
||||
case 'presence':
|
||||
direct = findPoints(this.element.properties, 'Point_Status', false, 'Property_Presence')
|
||||
if (direct.length) return direct
|
||||
return findPoints(allEquipmentPoints(this.element.equipment), 'Point_Status', true, 'Property_Presence')
|
||||
case 'lock':
|
||||
equipment = findEquipment(this.element.equipment, 'Equipment_Lock', false)
|
||||
if (!equipment.length) return []
|
||||
allPoints = allEquipmentPoints(equipment)
|
||||
points = [
|
||||
...findPoints(allPoints, 'Point_Status_OpenState', false),
|
||||
...findPoints(allPoints, 'Point_Status', false),
|
||||
...findPoints(allPoints, 'Point_Control', true)
|
||||
]
|
||||
if (points.length) return points
|
||||
return equipment.filter((e) => e.points.length === 0).map((e) => e.item)
|
||||
case 'climate':
|
||||
equipment = findEquipment(this.element.equipment, 'Equipment_HVAC', true)
|
||||
if (!equipment.length) return []
|
||||
allPoints = allEquipmentPoints(equipment)
|
||||
points = [
|
||||
...findPoints(allPoints, 'Point_Status', false),
|
||||
...findPoints(allPoints, 'Point_Control', true)
|
||||
]
|
||||
if (points.length) return points
|
||||
return equipment.filter((e) => e.points.length === 0).map((e) => e.item)
|
||||
case 'screens':
|
||||
equipment = findEquipment(this.element.equipment, 'Equipment_Screen', true)
|
||||
if (!equipment.length) return []
|
||||
allPoints = allEquipmentPoints(equipment)
|
||||
points = [
|
||||
...findPoints(allPoints, 'Point_Status', false, 'Property_Power'),
|
||||
...findPoints(allPoints, 'Point_Control', true, 'Property_Power')
|
||||
]
|
||||
if (points.length) return points
|
||||
return equipment.filter((e) => e.points.length === 0).map((e) => e.item)
|
||||
case 'speakers':
|
||||
equipment = [
|
||||
...findEquipment(this.element.equipment, 'Equipment_Receiver', false),
|
||||
...findEquipment(this.element.equipment, 'Equipment_Speaker', false)
|
||||
]
|
||||
if (!equipment.length) return []
|
||||
allPoints = allEquipmentPoints(equipment)
|
||||
points = [
|
||||
...findPoints(allPoints, 'Point_Status', false, 'Property_Power'),
|
||||
...findPoints(allPoints, 'Point_Control', true, 'Property_Power')
|
||||
]
|
||||
if (points.length) return points
|
||||
return equipment.filter((e) => e.points.length === 0).map((e) => e.item)
|
||||
case 'projectors':
|
||||
equipment = findEquipment(this.element.equipment, 'Equipment_Projector', false)
|
||||
if (!equipment.length) return []
|
||||
allPoints = allEquipmentPoints(equipment)
|
||||
points = [
|
||||
...findPoints(allPoints, 'Point_Status', false, 'Property_Power'),
|
||||
...findPoints(allPoints, 'Point_Control', true, 'Property_Power')
|
||||
]
|
||||
if (points.length) return points
|
||||
return equipment.filter((e) => e.points.length === 0).map((e) => e.item)
|
||||
default:
|
||||
return []
|
||||
}
|
||||
},
|
||||
map () {
|
||||
return this.query.map((item) => this.store[item.name].state)
|
||||
},
|
||||
reduce () {
|
||||
switch (this.type) {
|
||||
case 'lights':
|
||||
return this.map.filter((state) => state === 'ON' || (state.split(',').length === 3 && state.split(',')[2] !== '0') || (state.indexOf(',') < 0 && Number.parseInt(state) > 0)).length
|
||||
case 'blinds':
|
||||
return this.map.filter((state) => state === 'OPEN' || Number.parseInt(state) > 0).length
|
||||
default:
|
||||
return this.map.filter((state) => state === 'ON' || state === 'OPEN').length
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -1,36 +1,35 @@
|
|||
<template>
|
||||
<f7-card expandable ref="card" class="location-card" :animate="$f7.data.themeOptions.expandableCardAnimation !== 'disabled'" card-tablet-fullscreen v-on:card:opened="cardOpening" v-on:card:closed="cardClosed">
|
||||
<f7-card-content :padding="false">
|
||||
<div :class="`bg-color-${color}`" :style="{height: '200px'}">
|
||||
<f7-card-header text-color="white" class="display-block">
|
||||
{{title || 'Something'}}
|
||||
<div><small>{{subtitle || ' '}}</small></div>
|
||||
<div class="location-stats" v-if="items.equipments.length > 0"><small><f7-icon ios="f7:cube_box" aurora="f7:cube_box" md="material:payments" /> {{items.equipments.length}}</small></div>
|
||||
<div class="location-stats" v-if="items.properties.length > 0"><small><f7-icon ios="f7:bolt" aurora="f7:bolt" md="material:flash_on" /> {{items.properties.length}}</small></div>
|
||||
</f7-card-header>
|
||||
<f7-link
|
||||
card-close
|
||||
color="white"
|
||||
class="card-opened-fade-in"
|
||||
:style="{position: 'absolute', right: '15px', top: '15px'}"
|
||||
icon-f7="multiply_circle_fill"
|
||||
></f7-link>
|
||||
<model-card type="location" :context="context" :element="element" header-height="200px">
|
||||
<template v-slot:glance>
|
||||
<div v-if="!subtitle && parentLocation" class="subtitle"><small>{{parentLocation}}</small></div>
|
||||
<div v-if="context && context.component.slots && context.component.slots.glance" class="display-flex flex-direction-column align-items-flex-start">
|
||||
<generic-widget-component :context="childContext(slotComponent)" v-for="(slotComponent, idx) in context.component.slots.glance" :key="'glance-' + idx" @command="onCommand" />
|
||||
</div>
|
||||
<div class="card-content-padding" v-if="opened && items.equipments.length > 0 && items.properties.length > 0">
|
||||
<f7-segmented round tag="p">
|
||||
<f7-button round outline :active="activeTab === 'equipments'" :color="color" @click="activeTab = 'equipments'">Equipment</f7-button>
|
||||
<f7-button round outline :active="activeTab === 'properties'" :color="color" @click="activeTab = 'properties'">Properties</f7-button>
|
||||
</f7-segmented>
|
||||
<div class="location-stats margin-top" :class="config.invertText ? 'invert-text' : ''" v-if="!config.disableBadges">
|
||||
<span v-for="badgeType in ['lights', 'windows', 'doors', 'garagedoors', 'blinds', 'presence', 'lock', 'climate', 'screens', 'projectors', 'speakers']" :key="badgeType">
|
||||
<status-badge v-if="!config.badges || !config.badges.length || config.badges.indexOf(badgeType) >= 0"
|
||||
:store="context.store" :element="element" :type="badgeType" :invert-color="config.invertText" />
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="opened">
|
||||
<generic-widget-component v-if="activeTab === 'equipments'" :context="equipmentsListContext" />
|
||||
<generic-widget-component v-if="activeTab === 'properties'" :context="propertiesListContext" />
|
||||
<p>
|
||||
<f7-button fill round large card-close :color="color" class="margin-horizontal">Close</f7-button>
|
||||
</p>
|
||||
<div class="location-stats margin-top-half" v-if="!config.disableBadges">
|
||||
<span v-for="badgeType in ['temperature', 'humidity', 'luminance']" :key="badgeType">
|
||||
<measurement-badge v-if="!config.badges || !config.badges.length || config.badges.indexOf(badgeType) >= 0"
|
||||
:store="context.store" :element="element" :type="badgeType" :invert-color="config.invertText" />
|
||||
</span>
|
||||
</div>
|
||||
</f7-card-content>
|
||||
</f7-card>
|
||||
</template>
|
||||
<div class="card-content-padding">
|
||||
<f7-segmented round tag="p" v-if="element.equipment.length > 0 && element.properties.length > 0">
|
||||
<f7-button round outline :active="activeTab === 'equipment'" :color="color" @click="activeTab = 'equipment'">Equipment</f7-button>
|
||||
<f7-button round outline :active="activeTab === 'properties'" :color="color" @click="activeTab = 'properties'">Properties</f7-button>
|
||||
</f7-segmented>
|
||||
<generic-widget-component v-if="activeTab === 'equipment'" :context="equipmentListContext" />
|
||||
<generic-widget-component v-if="activeTab === 'properties'" :context="propertiesListContext" />
|
||||
<p>
|
||||
<f7-button fill round large card-close :color="color" class="margin-horizontal">Close</f7-button>
|
||||
</p>
|
||||
</div>
|
||||
</model-card>
|
||||
</template>
|
||||
|
||||
<style lang="stylus">
|
||||
|
@ -38,18 +37,31 @@
|
|||
height 200px
|
||||
.location-stats
|
||||
font-weight normal
|
||||
font-size 16px
|
||||
max-width calc(340px - 2*var(--f7-card-header-padding-horizontal))
|
||||
display flex
|
||||
flex-wrap wrap
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import mixin from '@/components/widgets/widget-mixin'
|
||||
import itemDefaultListComponent from '@/components/widgets/standard/list/default-list-item'
|
||||
import CardMixin from './card-mixin'
|
||||
import ModelCard from './model-card.vue'
|
||||
import StatusBadge from './glance/location/status-badge.vue'
|
||||
import MeasurementBadge from './glance/location/measurement-badge.vue'
|
||||
|
||||
export default {
|
||||
mixins: [CardMixin],
|
||||
props: ['color', 'type', 'header', 'title', 'subtitle', 'items'],
|
||||
mixins: [mixin, CardMixin],
|
||||
props: ['parentLocation'],
|
||||
components: {
|
||||
ModelCard,
|
||||
StatusBadge,
|
||||
MeasurementBadge
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
activeTab: (this.items.equipments.length === 0 && this.items.properties.length > 0) ? 'properties' : 'equipments'
|
||||
activeTab: (this.element.equipment.length === 0 && this.element.properties.length > 0) ? 'properties' : 'equipment'
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
@ -64,14 +76,14 @@ export default {
|
|||
mediaList: true
|
||||
},
|
||||
slots: {
|
||||
default: this.items.properties.map((i) => itemDefaultListComponent(i))
|
||||
default: this.element.properties.map((i) => itemDefaultListComponent(i))
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
equipmentsListContext () {
|
||||
const standaloneEquipments = this.items.equipments.filter((i) => i.points.length === 0).map((i) => itemDefaultListComponent(i.item))
|
||||
const equipmentsWithPoints = this.items.equipments.filter((i) => i.points.length !== 0).map((i) => {
|
||||
equipmentListContext () {
|
||||
const standaloneEquipment = this.element.equipment.filter((i) => i.points.length === 0).map((i) => itemDefaultListComponent(i.item))
|
||||
const equipmentWithPoints = this.element.equipment.filter((i) => i.points.length !== 0).map((i) => {
|
||||
return [
|
||||
{
|
||||
component: 'oh-list-item',
|
||||
|
@ -92,7 +104,7 @@ export default {
|
|||
mediaList: true
|
||||
},
|
||||
slots: {
|
||||
default: [...standaloneEquipments, ...equipmentsWithPoints].flat()
|
||||
default: [...standaloneEquipment, ...equipmentWithPoints].flat()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,91 @@
|
|||
<template>
|
||||
<f7-card expandable ref="card" class="model-card" :class="type + '-card'" :animate="$f7.data.themeOptions.expandableCardAnimation !== 'disabled'" card-tablet-fullscreen v-on:card:opened="cardOpening" v-on:card:closed="cardClosed">
|
||||
<f7-card-content :padding="false">
|
||||
<div :class="(!config.backgroundImage) ? `bg-color-${color}` : undefined" :style="{ height: `calc(var(--f7-safe-area-top) + ${headerHeight})` }">
|
||||
<f7-card-header :text-color="config.invertText ? 'black' : 'white'" class="display-block card-header" :style="{ height: `calc(var(--f7-safe-area-top) + ${headerHeight})` }">
|
||||
<img v-if="config.backgroundImage" class="card-background" :src="config.backgroundImage" :style="config.backgroundImageStyle" />
|
||||
<slot name="header">
|
||||
<div v-if="context && context.component.slots && context.component.slots.header">
|
||||
<generic-widget-component :context="childContext(slotComponent)" v-for="(slotComponent, idx) in context.component.slots.header" :key="'header-' + idx" @command="onCommand" />
|
||||
</div>
|
||||
<div v-else>
|
||||
{{title}}
|
||||
<div v-if="subtitle" class="subtitle"><small>{{subtitle || ' '}}</small></div>
|
||||
</div>
|
||||
</slot>
|
||||
<slot name="glance">
|
||||
</slot>
|
||||
</f7-card-header>
|
||||
<f7-link
|
||||
card-close
|
||||
color="white"
|
||||
class="card-opened-fade-in card-close-button"
|
||||
icon-f7="multiply_circle_fill"
|
||||
></f7-link>
|
||||
</div>
|
||||
<div v-if="opened">
|
||||
<slot>
|
||||
</slot>
|
||||
</div>
|
||||
</f7-card-content>
|
||||
</f7-card>
|
||||
</template>
|
||||
|
||||
<style lang="stylus">
|
||||
.model-card
|
||||
--card-offset calc(40px + var(--f7-safe-area-left) + var(--f7-safe-area-right))
|
||||
--card-width calc(100% - var(--f7-safe-area-left) - var(--f7-safe-area-right) - 40px)
|
||||
.card-close-button
|
||||
position absolute
|
||||
top calc(16px + var(--f7-safe-area-top))
|
||||
right calc(var(--f7-card-content-padding-horizontal) + var(--f7-safe-area-right))
|
||||
&.invert-text
|
||||
.card-background
|
||||
position absolute
|
||||
left 0
|
||||
top 0
|
||||
width 100%
|
||||
height 100%
|
||||
object-fit cover
|
||||
object-position center
|
||||
z-index -1
|
||||
background-color #ccc
|
||||
transform translateX(calc(-1*var(--card-offset)/2))
|
||||
&.card-opening, &.card-opened
|
||||
.card-header
|
||||
padding-top calc(var(--f7-card-header-padding-vertical) + var(--f7-safe-area-top))
|
||||
&.card-opening, &.card-closing, &.card-transitioning
|
||||
transition-duration 300ms
|
||||
&.card-opening
|
||||
.card-content, .card-header
|
||||
transition-duration 350ms
|
||||
&.card-closing
|
||||
.card-content, .card-header
|
||||
transition-duration 400ms
|
||||
&.card-opening, &.card-opened, &.card-closing
|
||||
.card-background
|
||||
transition-duration 400ms
|
||||
&.card-opening, &.card-opened
|
||||
.card-background
|
||||
transform translateX(0)
|
||||
|
||||
.subtitle
|
||||
font-weight normal
|
||||
font-size normal
|
||||
line-height 0.7
|
||||
@media (min-width: 768px)
|
||||
.model-card
|
||||
--card-offset calc(675px - 200px + var(--f7-safe-area-left) + var(--f7-safe-area-right))
|
||||
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import CardMixin from './card-mixin'
|
||||
|
||||
export default {
|
||||
mixins: [CardMixin],
|
||||
props: ['headerHeight'],
|
||||
methods: {
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -1,32 +1,21 @@
|
|||
<template>
|
||||
<f7-card expandable ref="card" class="property-card" :animate="$f7.data.themeOptions.expandableCardAnimation !== 'disabled'" card-tablet-fullscreen v-on:card:opened="cardOpening" v-on:card:closed="cardClosed">
|
||||
<f7-card-content :padding="false">
|
||||
<div :class="`bg-color-${color}`" :style="{height: '150px'}">
|
||||
<f7-card-header text-color="white" class="display-block">
|
||||
{{title || 'Something'}}
|
||||
<div class="property-stats"><small v-if="subtitle">{{subtitle}}</small></div>
|
||||
<br>
|
||||
<!-- <h1>State</h1> -->
|
||||
</f7-card-header>
|
||||
<f7-link
|
||||
card-close
|
||||
color="white"
|
||||
class="card-opened-fade-in"
|
||||
:style="{position: 'absolute', right: '15px', top: '15px'}"
|
||||
icon-f7="multiply_circle_fill"
|
||||
></f7-link>
|
||||
<model-card type="property" :context="context" :element="element" header-height="150px">
|
||||
<template v-slot:glance>
|
||||
<div v-if="context && context.component.slots && context.component.slots.glance" class="display-flex flex-direction-column align-items-flex-start">
|
||||
<generic-widget-component :context="childContext(slotComponent)" v-for="(slotComponent, idx) in context.component.slots.glance" :key="'glance-' + idx" @command="onCommand" />
|
||||
</div>
|
||||
<div class="card-content-padding" v-if="opened">
|
||||
<generic-widget-component :context="listContext" />
|
||||
<p class="padding-top margin-horizontal">
|
||||
<f7-button outline round :color="color" :href="`/analyzer/?items=${items.map((m) => m.name).join(',')}`">Analyze{{items.length > 1 ? ' all' : ''}}</f7-button>
|
||||
</p>
|
||||
<p class="margin-horizontal">
|
||||
<f7-button fill round large card-close :color="color">Close</f7-button>
|
||||
</p>
|
||||
</div>
|
||||
</f7-card-content>
|
||||
</f7-card>
|
||||
<!-- <div class="property-stats" v-else><small v-if="element.points">{{element.points.length}}</small></div> -->
|
||||
</template>
|
||||
<div class="card-content-padding">
|
||||
<generic-widget-component :context="listContext" />
|
||||
<p class="padding-top margin-horizontal">
|
||||
<f7-button outline round :color="color" :href="`/analyzer/?items=${element.points.map((m) => m.name).join(',')}`">Analyze{{element.points.length > 1 ? ' all' : ''}}</f7-button>
|
||||
</p>
|
||||
<p class="margin-horizontal">
|
||||
<f7-button fill round large card-close :color="color">Close</f7-button>
|
||||
</p>
|
||||
</div>
|
||||
</model-card>
|
||||
</template>
|
||||
|
||||
<style lang="stylus">
|
||||
|
@ -37,11 +26,16 @@
|
|||
</style>
|
||||
|
||||
<script>
|
||||
import mixin from '@/components/widgets/widget-mixin'
|
||||
import itemDefaultListComponent from '@/components/widgets/standard/list/default-list-item'
|
||||
import CardMixin from './card-mixin'
|
||||
import ModelCard from './model-card.vue'
|
||||
|
||||
export default {
|
||||
mixins: [CardMixin],
|
||||
mixins: [mixin, CardMixin],
|
||||
components: {
|
||||
ModelCard
|
||||
},
|
||||
computed: {
|
||||
listContext () {
|
||||
let pointsByType = []
|
||||
|
@ -73,7 +67,7 @@ export default {
|
|||
},
|
||||
itemsByPointType () {
|
||||
const points = {}
|
||||
this.items.forEach((item) => {
|
||||
this.element.points.forEach((item) => {
|
||||
const pointType = item.metadata.semantics.value.replace('Point_', '')
|
||||
if (!points[pointType]) points[pointType] = []
|
||||
points[pointType].push(item)
|
||||
|
|
|
@ -13,6 +13,7 @@ import * as PlanWidgets from '@/components/widgets/plan'
|
|||
import * as MapWidgets from '@/components/widgets/map'
|
||||
import { OhChartPageDefinition } from '@/assets/definitions/widgets/chart/page'
|
||||
import ChartWidgetsDefinitions from '@/assets/definitions/widgets/chart/index'
|
||||
import { OhLocationCardParameters, OhEquipmentCardParameters, OhPropertyCardParameters } from '@/assets/definitions/widgets/home'
|
||||
|
||||
function getWidgetDefinitions (cm) {
|
||||
const mode = cm.state.originalMode
|
||||
|
@ -34,6 +35,7 @@ function getWidgetDefinitions (cm) {
|
|||
.filter((w) => w.widget && typeof w.widget === 'function')
|
||||
const f7Components = Object.values(f7vue).filter((m) => m.name && m.name.indexOf('f7-') === 0)
|
||||
return [
|
||||
...(componentType === 'home') ? [OhLocationCardParameters(), OhEquipmentCardParameters(), OhPropertyCardParameters()] : [],
|
||||
...ohComponents.map((c) => c.widget()).sort((c1, c2) => c1.name.localeCompare(c2.name)),
|
||||
...f7Components.sort((c1, c2) => c1.name.localeCompare(c2.name))
|
||||
]
|
||||
|
|
|
@ -268,6 +268,7 @@ export default {
|
|||
pageTypes: [
|
||||
{ type: 'sitemap', label: 'Sitemap', componentType: 'Sitemap', icon: 'menu' },
|
||||
{ type: 'layout', label: 'Layout', componentType: 'oh-layout-page', icon: 'rectangle_grid_2x2' },
|
||||
{ type: 'home', label: 'Home', componentType: 'oh-home-page', icon: 'house' },
|
||||
{ type: 'tabs', label: 'Tabbed', componentType: 'oh-tabs-page', icon: 'squares_below_rectangle' },
|
||||
{ type: 'map', label: 'Map', componentType: 'oh-map-page', icon: 'map' },
|
||||
{ type: 'plan', label: 'Floor plan', componentType: 'oh-plan-page', icon: 'square_stack_3d_up' },
|
||||
|
|
|
@ -74,6 +74,7 @@ export default {
|
|||
pageTypes: [
|
||||
{ type: 'sitemap', label: 'Sitemap', componentType: 'Sitemap', icon: 'menu' },
|
||||
{ type: 'layout', label: 'Layout', componentType: 'oh-layout-page', icon: 'rectangle_grid_2x2' },
|
||||
{ type: 'home', label: 'Home', componentType: 'oh-home-page', icon: 'house' },
|
||||
{ type: 'tabs', label: 'Tabbed', componentType: 'oh-tabs-page', icon: 'squares_below_rectangle' },
|
||||
{ type: 'map', label: 'Map', componentType: 'oh-map-page', icon: 'map' },
|
||||
{ type: 'plan', label: 'Floor plan', componentType: 'oh-plan-page', icon: 'square_stack_3d_up' },
|
||||
|
|
|
@ -222,61 +222,6 @@ html
|
|||
.aurora .after-big-title
|
||||
margin-top 5rem !important
|
||||
|
||||
.demo-expandable-cards
|
||||
justify-content center
|
||||
|
||||
.card-header
|
||||
padding-top calc(var(--f7-safe-area-top) + var(--f7-card-header-padding-vertical))
|
||||
padding-left calc(var(--f7-safe-area-left) + var(--f7-card-header-padding-horizontal))
|
||||
|
||||
/* Demo Expandable Cards */
|
||||
@media (min-width 768px)
|
||||
.demo-expandable-cards
|
||||
display flex
|
||||
flex-wrap wrap
|
||||
|
||||
.demo-expandable-cards .card
|
||||
flex-shrink 10
|
||||
min-width 0
|
||||
|
||||
@media (min-width 768px) and (max-width 1023px)
|
||||
.demo-expandable-cards .card
|
||||
width 340px
|
||||
|
||||
// .demo-expandable-cards .card
|
||||
// width calc((100% - var(--f7-card-expandable-margin-horizontal) * 3) / 2)
|
||||
//
|
||||
.demo-expandable-cards .card:nth-child(n),
|
||||
.demo-expandable-cards .card:nth-child(n + 1)
|
||||
margin-left 0
|
||||
|
||||
.demo-expandable-cards .card:nth-child(n + 3)
|
||||
margin-top 0
|
||||
|
||||
@media (min-width 1024px)
|
||||
.demo-expandable-cards
|
||||
margin-top 2rem
|
||||
.card
|
||||
width 340px
|
||||
margin-top 0
|
||||
|
||||
.demo-expandable-cards .card:nth-child(n),
|
||||
.demo-expandable-cards .card:nth-child(n + 1)
|
||||
margin-left 0
|
||||
|
||||
.demo-expandable-cards .card:nth-child(n + 3)
|
||||
margin-top 0
|
||||
|
||||
// hide scrollbar
|
||||
.card-expandable.card-opened .card-content
|
||||
overflow-y scroll
|
||||
scrollbar-width none /* Firefox */
|
||||
-ms-overflow-style none /* IE 10+ */
|
||||
|
||||
.card-expandable.card-opened .card-content::-webkit-scrollbar /* WebKit */
|
||||
width 0
|
||||
height 0
|
||||
|
||||
// Remove the borders and background for admin links in sidebar
|
||||
.list.admin-links
|
||||
ul
|
||||
|
|
|
@ -12,29 +12,29 @@
|
|||
{{title}}
|
||||
</f7-nav-title>
|
||||
<f7-nav-right>
|
||||
<f7-link v-if="this.$store.getters.isAdmin" icon-ios="f7:pencil" icon-aurora="f7:pencil" icon-md="material:edit" :href="(homePageComponent) ? '/settings/pages/home/home' : '/settings/pages/home/add'"></f7-link>
|
||||
<f7-link icon-ios="f7:sidebar_right" icon-aurora="f7:sidebar_right" icon-md="material:exit_to_app" panel-open="right"></f7-link>
|
||||
</f7-nav-right>
|
||||
</f7-navbar>
|
||||
|
||||
<f7-toolbar tabbar labels bottom>
|
||||
<f7-toolbar tabbar labels bottom v-if="tabsVisible">
|
||||
<f7-link tab-link @click="currentTab = 'overview'" :tab-link-active="currentTab === 'overview'" icon-ios="f7:house_fill" icon-aurora="f7:house_fill" icon-md="material:home" text="Overview"></f7-link>
|
||||
<f7-link tab-link @click="currentTab = 'locations'" :tab-link-active="currentTab === 'locations'" icon-ios="f7:placemark_fill" icon-aurora="f7:placemark_fill" icon-md="material:place" text="Locations"></f7-link>
|
||||
<f7-link tab-link @click="currentTab = 'equipments'" :tab-link-active="currentTab === 'equipments'" icon-ios="f7:cube_box_fill" icon-aurora="f7:cube_box_fill" icon-md="material:payments" text="Equipment"></f7-link>
|
||||
<f7-link tab-link @click="currentTab = 'properties'" :tab-link-active="currentTab === 'properties'" icon-ios="f7:bolt_fill" icon-aurora="f7:bolt_fill" icon-md="material:flash_on" text="Properties"></f7-link>
|
||||
<f7-link tab-link v-if="tabVisible('locations')" @click="currentTab = 'locations'" :tab-link-active="currentTab === 'locations'" icon-ios="f7:placemark_fill" icon-aurora="f7:placemark_fill" icon-md="material:place" text="Locations"></f7-link>
|
||||
<f7-link tab-link v-if="tabVisible('equipment')" @click="currentTab = 'equipment'" :tab-link-active="currentTab === 'equipment'" icon-ios="f7:cube_box_fill" icon-aurora="f7:cube_box_fill" icon-md="material:payments" text="Equipment"></f7-link>
|
||||
<f7-link tab-link v-if="tabVisible('properties')" @click="currentTab = 'properties'" :tab-link-active="currentTab === 'properties'" icon-ios="f7:bolt_fill" icon-aurora="f7:bolt_fill" icon-md="material:flash_on" text="Properties"></f7-link>
|
||||
</f7-toolbar>
|
||||
|
||||
<f7-tabs v-if="items">
|
||||
<f7-tabs>
|
||||
<f7-tab id="tab-overview" :tab-active="currentTab === 'overview'" @tab:show="() => this.currentTab = 'overview'">
|
||||
<overview-tab v-if="currentTab === 'overview'" :context="context" :items="items" />
|
||||
<overview-tab v-if="currentTab === 'overview'" :context="context" :allow-chat="allowChat" />
|
||||
</f7-tab>
|
||||
<f7-tab id="tab-locations" :tab-active="currentTab === 'locations'" @tab:show="() => this.currentTab = 'locations'">
|
||||
<locations-tab v-if="currentTab === 'locations'" :context="context" :semantic-items="semanticItems" />
|
||||
<model-tab v-if="currentTab === 'locations'" :context="context" type="locations" :model="model" :page="homePageComponent" />
|
||||
</f7-tab>
|
||||
<f7-tab id="tab-equipments" :tab-active="currentTab === 'equipments'" @tab:show="() => this.currentTab = 'equipments'">
|
||||
<equipments-tab v-if="currentTab === 'equipments'" :context="context" :semantic-items="semanticItems" />
|
||||
<f7-tab id="tab-equipment" :tab-active="currentTab === 'equipment'" @tab:show="() => this.currentTab = 'equipment'">
|
||||
<model-tab v-if="currentTab === 'equipment'" :context="context" type="equipment" :model="model" :page="homePageComponent" />
|
||||
</f7-tab>
|
||||
<f7-tab id="tab-properties" :tab-active="currentTab === 'properties'" @tab:show="() => this.currentTab = 'properties'">
|
||||
<properties-tab v-if="currentTab === 'properties'" :context="context" :semantic-items="semanticItems" />
|
||||
<model-tab v-if="currentTab === 'properties'" :context="context" type="properties" :model="model" :page="homePageComponent" />
|
||||
</f7-tab>
|
||||
</f7-tabs>
|
||||
</f7-page>
|
||||
|
@ -59,26 +59,23 @@
|
|||
top -6px
|
||||
letter-spacing 1px
|
||||
color var(--f7-list-item-footer-text-color)
|
||||
.edit-home-button
|
||||
float right
|
||||
display absolute
|
||||
z-index 9000
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import OverviewTab from './home/overview-tab.vue'
|
||||
import LocationsTab from './home/locations-tab.vue'
|
||||
import EquipmentsTab from './home/equipments-tab.vue'
|
||||
import PropertiesTab from './home/properties-tab.vue'
|
||||
import ModelTab from './home/model-tab.vue'
|
||||
|
||||
import { compareItems } from '@/components/widgets/widget-order'
|
||||
|
||||
function compareObjects (o1, o2) {
|
||||
return compareItems(o1.item || o1, o2.item || o2)
|
||||
}
|
||||
import HomeCards from './home/homecards-mixin'
|
||||
|
||||
export default {
|
||||
mixins: [HomeCards],
|
||||
components: {
|
||||
OverviewTab,
|
||||
LocationsTab,
|
||||
EquipmentsTab,
|
||||
PropertiesTab
|
||||
ModelTab
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
|
@ -86,8 +83,7 @@ export default {
|
|||
showTasks: true,
|
||||
showCards: false,
|
||||
currentTab: 'overview',
|
||||
items: [],
|
||||
semanticItems: {}
|
||||
items: []
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
@ -96,13 +92,39 @@ export default {
|
|||
store: this.$store.getters.trackedItems
|
||||
}
|
||||
},
|
||||
homePageComponent () {
|
||||
const page = this.$store.getters.page('home')
|
||||
if (!page) return null
|
||||
if (page.component !== 'oh-home-page') return null
|
||||
return page
|
||||
},
|
||||
tabsVisible () {
|
||||
if (!this.homePageComponent) return true
|
||||
const visibleTo = this.homePageComponent.config.displayModelCardsTo
|
||||
if (visibleTo === undefined || !visibleTo.length) return true
|
||||
const user = this.$store.getters.user
|
||||
if (!user) return false
|
||||
if (user.roles && user.roles.some(r => visibleTo.indexOf('role:' + r) >= 0)) return true
|
||||
if (visibleTo.indexOf('user:' + user.name) >= 0) return true
|
||||
return false
|
||||
},
|
||||
allowChat () {
|
||||
if (!this.homePageComponent) return true
|
||||
const visibleTo = this.homePageComponent.config.allowChatInputTo
|
||||
if (visibleTo === undefined || !visibleTo.length) return true
|
||||
const user = this.$store.getters.user
|
||||
if (!user) return false
|
||||
if (user.roles && user.roles.some(r => visibleTo.indexOf('role:' + r) >= 0)) return true
|
||||
if (visibleTo.indexOf('user:' + user.name) >= 0) return true
|
||||
return false
|
||||
},
|
||||
title () {
|
||||
switch (this.currentTab) {
|
||||
case 'overview':
|
||||
return 'Home'
|
||||
case 'locations':
|
||||
return 'Locations'
|
||||
case 'equipments':
|
||||
case 'equipment':
|
||||
return 'Equipment'
|
||||
case 'properties':
|
||||
return 'Properties'
|
||||
|
@ -114,7 +136,7 @@ export default {
|
|||
methods: {
|
||||
onPageAfterIn () {
|
||||
this.$store.dispatch('startTrackingStates')
|
||||
this.load()
|
||||
this.loadModel()
|
||||
},
|
||||
onPageBeforeOut () {
|
||||
this.$store.dispatch('stopTrackingStates')
|
||||
|
@ -122,72 +144,12 @@ export default {
|
|||
onPageInit () {
|
||||
this.$f7.panel.get('left').enableVisibleBreakpoint()
|
||||
},
|
||||
load () {
|
||||
this.$oh.api.get('/rest/items?metadata=semantics,listWidget,widgetOrder').then((data) => {
|
||||
this.items = data
|
||||
// get the location items
|
||||
this.semanticItems.locations = data.filter((item, index, items) => {
|
||||
return item.metadata && item.metadata.semantics &&
|
||||
item.metadata.semantics.value.indexOf('Location') === 0
|
||||
}).sort(compareObjects).map((l) => {
|
||||
return {
|
||||
item: l,
|
||||
properties: data.filter((item, index, items) => {
|
||||
return item.metadata && item.metadata.semantics &&
|
||||
item.metadata.semantics && item.metadata.semantics.config &&
|
||||
item.metadata.semantics.config.relatesTo &&
|
||||
item.metadata.semantics.config.hasLocation === l.name
|
||||
}).sort(compareObjects),
|
||||
equipments: data.filter((item, index, items) => {
|
||||
return item.metadata && item.metadata.semantics &&
|
||||
item.metadata.semantics && item.metadata.semantics.config &&
|
||||
item.metadata.semantics.value.indexOf('Equipment') === 0 &&
|
||||
item.metadata.semantics.config.hasLocation === l.name
|
||||
}).sort(compareObjects).map((item) => {
|
||||
return {
|
||||
item: item,
|
||||
points: data.filter((item2, index, items) => {
|
||||
return item2.metadata && item2.metadata.semantics &&
|
||||
item2.metadata.semantics && item2.metadata.semantics.config &&
|
||||
item2.metadata.semantics.config.isPointOf === item.name
|
||||
}).sort(compareObjects)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// get the equipment items
|
||||
this.semanticItems.equipments = data.filter((item, index, items) => {
|
||||
return item.metadata && item.metadata.semantics &&
|
||||
item.metadata.semantics &&
|
||||
item.metadata.semantics.value.indexOf('Equipment') === 0
|
||||
}).sort(compareObjects).reduce((prev, item, i, properties) => {
|
||||
const equipmentType = item.metadata.semantics.value.split('_')[1] || 'Equipment'
|
||||
if (!prev[equipmentType]) prev[equipmentType] = []
|
||||
const equipmentWithPoints = {
|
||||
item: item,
|
||||
points: data.filter((item2, index, items) => {
|
||||
return item2.metadata && item2.metadata.semantics &&
|
||||
item2.metadata.semantics && item2.metadata.semantics.config &&
|
||||
item2.metadata.semantics.config.isPointOf === item.name
|
||||
}).sort(compareObjects)
|
||||
}
|
||||
prev[equipmentType].push(equipmentWithPoints)
|
||||
return prev
|
||||
}, {})
|
||||
|
||||
// get the property items
|
||||
this.semanticItems.properties = data.filter((item, index, items) => {
|
||||
return item.metadata && item.metadata.semantics &&
|
||||
item.metadata.semantics && item.metadata.semantics.config &&
|
||||
item.metadata.semantics.config.relatesTo
|
||||
}).sort(compareObjects).reduce((prev, item, i, properties) => {
|
||||
const property = item.metadata.semantics.config.relatesTo.split('_')[1]
|
||||
if (!prev[property]) prev[property] = []
|
||||
prev[property].push(item)
|
||||
return prev
|
||||
}, {})
|
||||
})
|
||||
tabVisible (tab) {
|
||||
if (!this.tabsVisible) return false
|
||||
if (!this.homePageComponent) return true
|
||||
const hiddenTabs = this.homePageComponent.config.hiddenModelTabs
|
||||
if (hiddenTabs === undefined || !hiddenTabs.length) return true
|
||||
return hiddenTabs.indexOf(tab) < 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,47 +0,0 @@
|
|||
<template>
|
||||
<div v-if="showCards">
|
||||
<div class="demo-expandable-cards">
|
||||
<equipments-card v-for="equipmentType in Object.keys(semanticItems.equipments).sort()" :key="equipmentType"
|
||||
:title="equipmentType"
|
||||
:subtitle="`${semanticItems.equipments[equipmentType].length} item${semanticItems.equipments[equipmentType].length > 1 ? 's' : ''}`"
|
||||
:color="color(equipmentType)"
|
||||
:items="semanticItems.equipments[equipmentType]" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import EquipmentsCard from '../../components/cards/equipments-card.vue'
|
||||
|
||||
export default {
|
||||
props: ['semanticItems'],
|
||||
components: {
|
||||
EquipmentsCard
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
showCards: false,
|
||||
properties: []
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.showCards = true
|
||||
},
|
||||
methods: {
|
||||
hideCards () {
|
||||
this.showCards = false
|
||||
},
|
||||
color (equipmentType) {
|
||||
if (equipmentType === 'HVAC') return 'red'
|
||||
if (equipmentType === 'Lightbulb') return 'yellow'
|
||||
if (equipmentType === 'Window') return 'blue'
|
||||
if (equipmentType === 'Door') return 'green'
|
||||
if (equipmentType === 'Camera') return 'pink'
|
||||
if (equipmentType === 'Blinds') return 'teal'
|
||||
if (equipmentType === 'SmokeDetector' || equipmentType === 'Siren') return 'deeppurple'
|
||||
// etc. - use a map
|
||||
return 'gray'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,27 @@
|
|||
export default (model, type, page) => {
|
||||
const cardOrder = (page && page.slots && page.slots[type] && page.slots[type][0] && page.slots[type][0].config && page.slots[type][0].config.cardOrder) ? page.slots[type][0].config.cardOrder : []
|
||||
const elements = [...model[type]].map((e) => {
|
||||
if (e.separator) return e
|
||||
const card = (page && page.slots && page.slots[type] && page.slots[type][0] && page.slots[type][0].slots && page.slots[type][0].slots[e.key]) ? page.slots[type][0].slots[e.key][0] : null
|
||||
if (card) e.card = card
|
||||
return e
|
||||
})
|
||||
let groups = []
|
||||
let currentGroup = []
|
||||
for (const orderKey of cardOrder) {
|
||||
if (orderKey.separator) {
|
||||
if (currentGroup.length) groups.push(currentGroup)
|
||||
currentGroup = []
|
||||
currentGroup.push(orderKey)
|
||||
} else {
|
||||
const idx = elements.findIndex((c) => c.key === orderKey)
|
||||
if (idx >= 0) {
|
||||
currentGroup.push(elements[idx])
|
||||
elements.splice(idx, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (currentGroup.length) groups.push(currentGroup)
|
||||
groups.push([...elements])
|
||||
return groups
|
||||
}
|
|
@ -0,0 +1,121 @@
|
|||
import cardGroups from './homecards-grouping'
|
||||
import { compareItems } from '@/components/widgets/widget-order'
|
||||
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
model: {},
|
||||
modelReady: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
},
|
||||
methods: {
|
||||
cardGroups (type, page) {
|
||||
return cardGroups(this.model, type, page)
|
||||
},
|
||||
compareObjects (o1, o2) {
|
||||
return compareItems(o1.item || o1, o2.item || o2)
|
||||
},
|
||||
buildModelCard (type, source, key, page) {
|
||||
const card = (page && page.slots && page.slots[type] && page.slots[type][0] && page.slots[type][0].slots && page.slots[type][0].slots[key]) ? page.slots[type][0].slots[key][0] : null
|
||||
switch (type) {
|
||||
case 'location':
|
||||
let defaultLocationTitle = source.item.label || source.item.name
|
||||
return Object.assign(source, {
|
||||
key,
|
||||
card,
|
||||
defaultTitle: defaultLocationTitle
|
||||
})
|
||||
case 'equipment':
|
||||
let defaultEquipmentTitle = key
|
||||
return {
|
||||
key,
|
||||
card,
|
||||
defaultTitle: defaultEquipmentTitle,
|
||||
equipment: source
|
||||
}
|
||||
case 'property':
|
||||
let defaultPropertyTitle = key
|
||||
return {
|
||||
key,
|
||||
card,
|
||||
defaultTitle: defaultPropertyTitle,
|
||||
points: source
|
||||
}
|
||||
}
|
||||
},
|
||||
loadModel (page) {
|
||||
this.$oh.api.get('/rest/items?metadata=semantics,listWidget,widgetOrder').then((data) => {
|
||||
this.items = data
|
||||
// get the location items
|
||||
const locations = data.filter((item, index, items) => {
|
||||
return item.metadata && item.metadata.semantics &&
|
||||
item.metadata.semantics.value.indexOf('Location') === 0
|
||||
}).sort(this.compareObjects).map((l) => {
|
||||
return {
|
||||
item: l,
|
||||
properties: data.filter((item, index, items) => {
|
||||
return item.metadata && item.metadata.semantics &&
|
||||
item.metadata.semantics && item.metadata.semantics.config &&
|
||||
item.metadata.semantics.config.relatesTo &&
|
||||
item.metadata.semantics.config.hasLocation === l.name
|
||||
}).sort(this.compareObjects),
|
||||
equipment: data.filter((item, index, items) => {
|
||||
return item.metadata && item.metadata.semantics &&
|
||||
item.metadata.semantics && item.metadata.semantics.config &&
|
||||
item.metadata.semantics.value.indexOf('Equipment') === 0 &&
|
||||
item.metadata.semantics.config.hasLocation === l.name
|
||||
}).sort(this.compareObjects).map((item) => {
|
||||
return {
|
||||
item: item,
|
||||
points: data.filter((item2, index, items) => {
|
||||
return item2.metadata && item2.metadata.semantics &&
|
||||
item2.metadata.semantics && item2.metadata.semantics.config &&
|
||||
item2.metadata.semantics.config.isPointOf === item.name
|
||||
}).sort(this.compareObjects)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// get the equipment items
|
||||
const equipment = data.filter((item, index, items) => {
|
||||
return item.metadata && item.metadata.semantics &&
|
||||
item.metadata.semantics &&
|
||||
item.metadata.semantics.value.indexOf('Equipment') === 0
|
||||
}).sort(this.compareObjects).reduce((prev, item, i, properties) => {
|
||||
const equipmentType = item.metadata.semantics.value.split('_')[1] || 'Equipment'
|
||||
if (!prev[equipmentType]) prev[equipmentType] = []
|
||||
const equipmentWithPoints = {
|
||||
item: item,
|
||||
points: data.filter((item2, index, items) => {
|
||||
return item2.metadata && item2.metadata.semantics &&
|
||||
item2.metadata.semantics && item2.metadata.semantics.config &&
|
||||
item2.metadata.semantics.config.isPointOf === item.name
|
||||
}).sort(this.compareObjects)
|
||||
}
|
||||
prev[equipmentType].push(equipmentWithPoints)
|
||||
return prev
|
||||
}, {})
|
||||
|
||||
// get the property items
|
||||
const properties = data.filter((item, index, items) => {
|
||||
return item.metadata && item.metadata.semantics &&
|
||||
item.metadata.semantics && item.metadata.semantics.config &&
|
||||
item.metadata.semantics.config.relatesTo
|
||||
}).sort(this.compareObjects).reduce((prev, item, i, properties) => {
|
||||
const property = item.metadata.semantics.config.relatesTo.split('_')[1]
|
||||
if (!prev[property]) prev[property] = []
|
||||
prev[property].push(item)
|
||||
return prev
|
||||
}, {})
|
||||
|
||||
this.model.locations = locations.map(l => this.buildModelCard('location', l, l.item.name, page))
|
||||
this.model.equipment = Object.keys(equipment).sort().map(k => this.buildModelCard('equipment', equipment[k], k, page))
|
||||
this.model.properties = Object.keys(properties).sort().map(k => this.buildModelCard('property', properties[k], k, page))
|
||||
this.modelReady = true
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,50 +0,0 @@
|
|||
<template>
|
||||
<div v-if="showCards">
|
||||
<div class="demo-expandable-cards">
|
||||
<location-card v-for="location in semanticItems.locations.filter((l) => l.equipments.length > 0 || l.properties.length > 0)" :key="location.item.name"
|
||||
:title="location.item.label" :items="location" :subtitle="parentLocationName(location.item)" :color="color(location.item)" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import LocationCard from '../../components/cards/location-card.vue'
|
||||
|
||||
export default {
|
||||
props: ['semanticItems'],
|
||||
components: {
|
||||
LocationCard
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
showCards: false,
|
||||
locations: []
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.showCards = true
|
||||
},
|
||||
methods: {
|
||||
hideCards () {
|
||||
this.showCards = false
|
||||
},
|
||||
color (item) {
|
||||
if (item.metadata.semantics.value.indexOf('LivingRoom') > 0) return 'orange'
|
||||
if (item.metadata.semantics.value.indexOf('Kitchen') > 0) return 'deeporange'
|
||||
if (item.metadata.semantics.value.indexOf('Bedroom') > 0) return 'pink'
|
||||
if (item.metadata.semantics.value.indexOf('Bathroom') > 0) return 'blue'
|
||||
if (item.metadata.semantics.value.indexOf('_Room') > 0) return 'lightblue'
|
||||
if (item.metadata.semantics.value.indexOf('_Floor') > 0) return 'deeppurple'
|
||||
if (item.metadata.semantics.value.indexOf('_Outdoor') > 0) return 'green'
|
||||
return 'gray'
|
||||
},
|
||||
parentLocationName (item) {
|
||||
if (item.metadata.semantics.config && item.metadata.semantics.config.isPartOf) {
|
||||
const parent = (this.semanticItems.locations.find((i) => i.item.name === item.metadata.semantics.config.isPartOf))
|
||||
return parent.item.label || parent.item.name
|
||||
}
|
||||
return ''
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,131 @@
|
|||
<template>
|
||||
<div v-if="showCards && groups">
|
||||
<div v-for="(elements, idx) in groups" :key="idx">
|
||||
<f7-block-title medium v-if="elements.length > 0 && elements[0].separator">{{elements[0].separator}}</f7-block-title>
|
||||
<div class="model-cards-section" v-if="elements.length > 0">
|
||||
<div v-for="(element, idx) in elements.filter((e) => !isCardExcluded(e))" :key="idx">
|
||||
<location-card v-if="type === 'locations' && !element.separator && (element.equipment.length > 0 || element.properties.length > 0)" :key="element.key"
|
||||
type="location" :element="element" :context="cardContext(element)" :parent-location="parentLocationName(element.item)" />
|
||||
<equipment-card v-if="type === 'equipment' && !element.separator" :key="element.key"
|
||||
type="equipment" :element="element" :context="cardContext(element)" />
|
||||
<property-card v-if="type === 'properties' && !element.separator" :key="element.key"
|
||||
type="property" :element="element" :context="cardContext(element)" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="stylus">
|
||||
|
||||
.model-cards-section
|
||||
justify-content center
|
||||
|
||||
@media (min-width 768px)
|
||||
.model-cards-section
|
||||
display flex
|
||||
flex-wrap wrap
|
||||
|
||||
.model-cards-section .card
|
||||
flex-shrink 10
|
||||
min-width 0
|
||||
|
||||
@media (min-width 768px) and (max-width 1023px)
|
||||
.model-cards-section .card
|
||||
width 340px
|
||||
margin-top 0
|
||||
|
||||
// .model-cards-section .card
|
||||
// width calc((100% - var(--f7-card-expandable-margin-horizontal) * 3) / 2)
|
||||
//
|
||||
.model-cards-section .card:nth-child(n),
|
||||
.model-cards-section .card:nth-child(n + 1)
|
||||
margin-left 0
|
||||
|
||||
.model-cards-section .card:nth-child(n + 3)
|
||||
margin-top 0
|
||||
|
||||
@media (min-width 1024px)
|
||||
.model-cards-section
|
||||
margin-top 2rem
|
||||
.card
|
||||
width 340px
|
||||
margin-top 0
|
||||
|
||||
.model-cards-section .card:nth-child(n),
|
||||
.model-cards-section .card:nth-child(n + 1)
|
||||
margin-left 0
|
||||
|
||||
.model-cards-section .card:nth-child(n + 3)
|
||||
margin-top 0
|
||||
|
||||
// hide scrollbar
|
||||
.card-expandable.card-opened .card-content
|
||||
overflow-y scroll
|
||||
scrollbar-width none /* Firefox */
|
||||
-ms-overflow-style none /* IE 10+ */
|
||||
|
||||
.card-expandable.card-opened .card-content::-webkit-scrollbar /* WebKit */
|
||||
width 0
|
||||
height 0
|
||||
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import cardGroups from './homecards-grouping'
|
||||
|
||||
import LocationCard from '../../components/cards/location-card.vue'
|
||||
import EquipmentCard from '../../components/cards/equipment-card.vue'
|
||||
import PropertyCard from '../../components/cards/property-card.vue'
|
||||
|
||||
export default {
|
||||
props: ['type', 'model', 'page'],
|
||||
components: {
|
||||
LocationCard,
|
||||
EquipmentCard,
|
||||
PropertyCard
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
showCards: false
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.showCards = true
|
||||
},
|
||||
computed: {
|
||||
groups () {
|
||||
return cardGroups(this.model, this.type, this.page)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
hideCards () {
|
||||
this.showCards = false
|
||||
},
|
||||
isCardExcluded (card) {
|
||||
if (!card.key) return
|
||||
const page = this.page
|
||||
const type = this.type
|
||||
const excludedCards = (page && page.slots && page.slots[type] && page.slots[type][0] && page.slots[type][0].config && page.slots[type][0].config.excludedCards) ? page.slots[type][0].config.excludedCards : []
|
||||
const excludedIdx = excludedCards.indexOf(card.key)
|
||||
return excludedIdx >= 0
|
||||
},
|
||||
cardContext (element) {
|
||||
return {
|
||||
component: element.card || {
|
||||
component: (this.type === 'locations') ? 'oh-location-card' : (this.type === 'equipment') ? 'oh-equipment-card' : 'oh-property-card',
|
||||
config: {}
|
||||
},
|
||||
store: this.$store.getters.trackedItems
|
||||
}
|
||||
},
|
||||
parentLocationName (item) {
|
||||
if (item.metadata.semantics.config && item.metadata.semantics.config.isPartOf) {
|
||||
const parent = (this.model.locations.find((i) => i.item.name === item.metadata.semantics.config.isPartOf))
|
||||
return parent.item.label || parent.item.name
|
||||
}
|
||||
return ''
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -52,7 +52,7 @@ import OhLayoutPage from '@/components/widgets/layout/oh-layout-page.vue'
|
|||
import Habot from '../../components/home/habot.vue'
|
||||
|
||||
export default {
|
||||
props: ['context', 'items'],
|
||||
props: ['context', 'allowChat'],
|
||||
components: {
|
||||
OhLayoutPage,
|
||||
Habot
|
||||
|
@ -69,7 +69,7 @@ export default {
|
|||
},
|
||||
computed: {
|
||||
showHABot () {
|
||||
return this.$store.getters.apiEndpoint('habot') && localStorage.getItem('openhab.ui:theme.home.hidechatinput') !== 'true'
|
||||
return this.$store.getters.apiEndpoint('habot') && this.allowChat && localStorage.getItem('openhab.ui:theme.home.hidechatinput') !== 'true'
|
||||
},
|
||||
overviewPage () {
|
||||
const page = this.$store.getters.page('overview')
|
||||
|
|
|
@ -1,45 +0,0 @@
|
|||
<template>
|
||||
<div v-if="showCards">
|
||||
<div class="demo-expandable-cards">
|
||||
<property-card v-for="property in Object.keys(semanticItems.properties).sort()" :key="property"
|
||||
:title="property"
|
||||
:subtitle="`${semanticItems.properties[property].length} item${semanticItems.properties[property].length > 1 ? 's' : ''}`"
|
||||
:color="color(property)"
|
||||
:items="semanticItems.properties[property]" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import PropertyCard from '../../components/cards/property-card.vue'
|
||||
|
||||
export default {
|
||||
props: ['semanticItems'],
|
||||
components: {
|
||||
PropertyCard
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
showCards: false,
|
||||
properties: []
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.showCards = true
|
||||
},
|
||||
methods: {
|
||||
hideCards () {
|
||||
this.showCards = false
|
||||
},
|
||||
color (property) {
|
||||
if (property === 'Temperature') return 'red'
|
||||
if (property === 'Light') return 'orange'
|
||||
if (property === 'Humidity') return 'blue'
|
||||
if (property === 'Presence') return 'teal'
|
||||
if (property === 'Pressure') return 'deeppurple'
|
||||
// etc. - use a map
|
||||
return 'gray'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -39,7 +39,7 @@
|
|||
</f7-tab>
|
||||
|
||||
<f7-tab id="code" @tab:show="() => { this.currentTab = 'code' }" :tab-active="currentTab === 'code'">
|
||||
<editor v-if="currentTab === 'code'" :style="{ opacity: previewMode ? '0' : '' }" class="page-code-editor" mode="application/vnd.openhab.uicomponent;type=chart" :value="pageYaml" @input="(value) => pageYaml = value" />
|
||||
<editor v-if="currentTab === 'code'" :style="{ opacity: previewMode ? '0' : '' }" class="page-code-editor" mode="application/vnd.openhab.uicomponent+yaml;type=chart" :value="pageYaml" @input="(value) => pageYaml = value" />
|
||||
<!-- <pre v-show="!previewMode" class="yaml-message padding-horizontal" :class="[yamlError === 'OK' ? 'text-color-green' : 'text-color-red']">{{yamlError}}</pre> -->
|
||||
|
||||
<oh-chart-page class="chart-page" v-if="ready && previewMode" :context="context" :key="pageKey" />
|
||||
|
|
|
@ -0,0 +1,298 @@
|
|||
<template>
|
||||
<f7-page @page:afterin="onPageAfterIn" @page:beforeout="onPageBeforeOut" class="home-editor">
|
||||
<f7-navbar title="Edit Home Page" 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<span v-if="$device.desktop"> (Ctrl-S)</span></f7-link>
|
||||
</f7-nav-right>
|
||||
</f7-navbar>
|
||||
<f7-toolbar tabbar position="top" v-if="!previewMode">
|
||||
<f7-link @click="currentTab = 'design'; fromYaml()" :tab-link-active="currentTab === 'design'" class="tab-link">Design</f7-link>
|
||||
<f7-link @click="currentTab = 'code'; toYaml()" :tab-link-active="currentTab === 'code'" class="tab-link">Code</f7-link>
|
||||
</f7-toolbar>
|
||||
<f7-toolbar v-else tabbar position="top">
|
||||
<f7-link v-for="tab in modelTabs" :key="tab.value" @click="showCardControls = false; currentModelTab = tab.value" :tab-link-active="currentModelTab === tab.value" class="tab-link">{{tab.label}}</f7-link>
|
||||
</f7-toolbar>
|
||||
<f7-toolbar bottom class="toolbar-details">
|
||||
<div style="margin-left: auto">
|
||||
<f7-toggle :checked="previewMode" @toggle:change="(value) => togglePreviewMode(value)"></f7-toggle> Run mode<span v-if="$device.desktop"> (Ctrl-R)</span>
|
||||
</div>
|
||||
</f7-toolbar>
|
||||
<f7-tabs class="tabs-editor-tabs">
|
||||
<f7-tab id="design" class="tabs-editor-design-tab" @tab:show="() => this.currentTab = 'design'" :tab-active="currentTab === 'design'">
|
||||
<f7-block v-if="!ready" class="text-align-center">
|
||||
<f7-preloader></f7-preloader>
|
||||
<div>Loading...</div>
|
||||
</f7-block>
|
||||
<!-- <f7-block class="block-narrow" v-if="ready && !previewMode">
|
||||
<page-settings :page="page" :createMode="createMode" />
|
||||
</f7-block> -->
|
||||
|
||||
<f7-block class="block-narrow" style="padding-bottom: 8rem" v-if="ready && !previewMode">
|
||||
<f7-col>
|
||||
<f7-block-title>Page Configuration</f7-block-title>
|
||||
<config-sheet
|
||||
:parameterGroups="pageWidgetDefinition.props.parameterGroups || []"
|
||||
:parameters="pageWidgetDefinition.props.parameters || []"
|
||||
:configuration="page.config"
|
||||
@updated="dirty = true"
|
||||
/>
|
||||
</f7-col>
|
||||
|
||||
<f7-col v-if="modelReady && !previewMode">
|
||||
<f7-block-title>Cards</f7-block-title>
|
||||
<f7-segmented strong tag="p">
|
||||
<f7-button v-for="tab in modelTabs" :key="tab.value" @click="showCardControls = false; currentModelTab = tab.value" :active="currentModelTab === tab.value" :text="tab.label"></f7-button>
|
||||
</f7-segmented>
|
||||
|
||||
<div class="display-block padding">
|
||||
<div class="no-padding float-right">
|
||||
<f7-button @click="showCardControls = !showCardControls" small outline :fill="showCardControls" sortable-toggle=".sortable" style="margin-top: -3px; margin-right: 5px"
|
||||
color="gray" icon-size="12" icon-ios="material:wrap_text" icon-md="material:wrap_text" icon-aurora="material:wrap_text"> Reorder</f7-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<f7-list media-list class="homecards-list" sortable :key="'cards-' + currentModelTab + cardListId" @sortable:sort="reorderCard">
|
||||
<f7-list-item media-item :link="(showCardControls) ? undefined : ''"
|
||||
@click.native="(ev) => cardClicked(ev, card, idx)"
|
||||
v-for="(card, idx) in cardGroups(currentModelTab, page).flat()" :key="idx"
|
||||
:title="card.separator || card.defaultTitle" :footer="(card.separator) ? '(separator)' : card.key">
|
||||
<f7-menu slot="content-start" class="configure-layout-menu">
|
||||
<f7-menu-item icon-f7="list_bullet" dropdown>
|
||||
<f7-menu-dropdown>
|
||||
<f7-menu-dropdown-item v-if="!card.separator" @click="configureCard(card)" href="#" text="Configure Card"></f7-menu-dropdown-item>
|
||||
<f7-menu-dropdown-item v-if="!card.separator" @click="editCardCode(card)" href="#" text="Edit YAML"></f7-menu-dropdown-item>
|
||||
<f7-menu-dropdown-item v-if="card.separator" @click="renameCardSeparator(idx)" href="#" text="Rename"></f7-menu-dropdown-item>
|
||||
<f7-menu-dropdown-item divider></f7-menu-dropdown-item>
|
||||
<f7-menu-dropdown-item v-if="!card.separator" @click="addCardSeparator(idx)" href="#" text="Add Separator Before"></f7-menu-dropdown-item>
|
||||
<f7-menu-dropdown-item v-if="card.separator" @click="removeCardSeparator(idx)" href="#" text="Remove Separator"></f7-menu-dropdown-item>
|
||||
</f7-menu-dropdown>
|
||||
</f7-menu-item>
|
||||
</f7-menu>
|
||||
<f7-checkbox :checked="!isCardExcluded(card)" :disabled="card.separator !== undefined" slot="content-start" class="margin-right" />
|
||||
</f7-list-item>
|
||||
</f7-list>
|
||||
</f7-col>
|
||||
</f7-block>
|
||||
<div v-else-if="ready && previewMode && currentTab === 'design'" :context="context" :key="pageKey">
|
||||
<model-tab style="margin-bottom: 4rem" :context="context" :type="currentModelTab" :model="model" :page="page" />
|
||||
</div>
|
||||
|
||||
</f7-tab>
|
||||
|
||||
<f7-tab id="code" @tab:show="() => { this.currentTab = 'code' }" :tab-active="currentTab === 'code'">
|
||||
<editor v-if="currentTab === 'code'" :style="{ opacity: previewMode ? '0' : '' }" class="page-code-editor" mode="application/vnd.openhab.uicomponent+yaml;type=home" :value="pageYaml" @input="(value) => pageYaml = value" />
|
||||
<!-- <pre class="yaml-message padding-horizontal" :class="[yamlError === 'OK' ? 'text-color-green' : 'text-color-red']">{{yamlError}}</pre> -->
|
||||
<div v-if="ready && previewMode" :context="context" :key="pageKey">
|
||||
<model-tab style="margin-bottom: 4rem" :context="context" :type="currentModelTab" :model="model" :page="page" />
|
||||
</div>
|
||||
</f7-tab>
|
||||
</f7-tabs>
|
||||
</f7-page>
|
||||
</template>
|
||||
|
||||
<style lang="stylus">
|
||||
.page-code-editor.vue-codemirror
|
||||
display block
|
||||
top calc(var(--f7-navbar-height) + var(--f7-tabbar-height))
|
||||
height calc(100% - 3*var(--f7-navbar-height))
|
||||
width 100%
|
||||
.yaml-message
|
||||
display block
|
||||
position absolute
|
||||
top 80%
|
||||
white-space pre-wrap
|
||||
.homecards-list
|
||||
.item-link
|
||||
overflow inherit
|
||||
z-index inherit !important
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import PageDesigner from '../pagedesigner-mixin'
|
||||
import HomeCards from '../../../home/homecards-mixin'
|
||||
|
||||
import YAML from 'yaml'
|
||||
|
||||
import { OhHomePageDefinition, OhLocationCardParameters, OhEquipmentCardParameters, OhPropertyCardParameters } from '@/assets/definitions/widgets/home'
|
||||
|
||||
import ConfigSheet from '@/components/config/config-sheet.vue'
|
||||
import ModelTab from '@/pages/home/model-tab.vue'
|
||||
|
||||
const ConfigurableWidgets = {
|
||||
'oh-location-card': OhLocationCardParameters,
|
||||
'oh-equipment-card': OhEquipmentCardParameters,
|
||||
'oh-property-card': OhPropertyCardParameters
|
||||
}
|
||||
|
||||
export default {
|
||||
mixins: [PageDesigner, HomeCards],
|
||||
components: {
|
||||
'editor': () => import('@/components/config/controls/script-editor.vue'),
|
||||
ConfigSheet,
|
||||
ModelTab
|
||||
},
|
||||
props: ['createMode', 'uid'],
|
||||
data () {
|
||||
return {
|
||||
pageWidgetDefinition: OhHomePageDefinition(),
|
||||
modelTabs: [
|
||||
{ value: 'locations', label: 'Locations' },
|
||||
{ value: 'equipment', label: 'Equipment' },
|
||||
{ value: 'properties', label: 'Properties' }
|
||||
],
|
||||
currentModelTab: 'locations',
|
||||
showCardControls: false,
|
||||
cardListId: this.$f7.utils.id(),
|
||||
page: {
|
||||
uid: 'home',
|
||||
component: 'oh-home-page',
|
||||
config: {
|
||||
label: 'Home Page'
|
||||
},
|
||||
slots: {
|
||||
locations: [ { component: 'oh-locations-tab', config: {}, slots: {} } ],
|
||||
equipment: [ { component: 'oh-equipment-tab', config: {}, slots: {} } ],
|
||||
properties: [ { component: 'oh-properties-tab', config: {}, slots: {} } ]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
pageReady (val) {
|
||||
if (val) {
|
||||
this.loadModel(this.page)
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
addWidget (component, widgetType, parentContext, slot) {
|
||||
if (!slot) slot = 'default'
|
||||
if (!component.slots) component.slots = {}
|
||||
if (!component.slots[slot]) component.slots[slot] = []
|
||||
if (widgetType) {
|
||||
component.slots[slot].push({
|
||||
component: widgetType,
|
||||
config: {
|
||||
title: 'New Tab',
|
||||
icon: 'f7:squares_below_rectangle'
|
||||
},
|
||||
slots: { default: [] }
|
||||
})
|
||||
this.forceUpdate()
|
||||
}
|
||||
},
|
||||
getWidgetDefinition (componentType) {
|
||||
return ConfigurableWidgets[componentType] ? ConfigurableWidgets[componentType]() : null
|
||||
},
|
||||
ensureCardComponentExists (card) {
|
||||
if (!this.page.slots[this.currentModelTab][0].slots[card.key]) {
|
||||
this.page.slots[this.currentModelTab][0].slots[card.key] = [
|
||||
{
|
||||
component: (this.currentModelTab) === 'locations' ? 'oh-location-card' : (this.currentModelTab === 'equipment') ? 'oh-equipment-card' : 'oh-property-card',
|
||||
config: {}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
configureCard (card) {
|
||||
if (!card.key) return
|
||||
if (!this.page.slots[this.currentModelTab] || !this.page.slots[this.currentModelTab][0] || !this.page.slots[this.currentModelTab][0].slots) return
|
||||
this.ensureCardComponentExists(card)
|
||||
return this.configureWidget(this.page.slots[this.currentModelTab][0].slots[card.key][0])
|
||||
},
|
||||
editCardCode (card) {
|
||||
if (!card.key) return
|
||||
if (!this.page.slots[this.currentModelTab] || !this.page.slots[this.currentModelTab][0] || !this.page.slots[this.currentModelTab][0].slots) return
|
||||
this.ensureCardComponentExists(card)
|
||||
return this.editWidgetCode(this.page.slots[this.currentModelTab][0].slots[card.key][0])
|
||||
},
|
||||
addCardSeparator (idx) {
|
||||
const orderedCards = this.cardGroups(this.currentModelTab, this.page).flat().map((e) => (e.separator) ? e : e.key)
|
||||
orderedCards.splice(idx, 0, { separator: 'New Section' })
|
||||
this.$set(this.page.slots[this.currentModelTab][0].config, 'cardOrder', orderedCards)
|
||||
this.renameCardSeparator(idx)
|
||||
},
|
||||
renameCardSeparator (idx) {
|
||||
const orderedCards = this.cardGroups(this.currentModelTab, this.page).flat().map((e) => (e.separator) ? e : e.key)
|
||||
if (orderedCards[idx].separator) {
|
||||
this.$f7.dialog.prompt('Enter the title of the separator:', null,
|
||||
(title) => {
|
||||
orderedCards[idx].separator = title
|
||||
this.$set(this.page.slots[this.currentModelTab][0].config, 'cardOrder', orderedCards)
|
||||
},
|
||||
null,
|
||||
orderedCards[idx].separator)
|
||||
}
|
||||
},
|
||||
removeCardSeparator (idx) {
|
||||
const orderedCards = this.cardGroups(this.currentModelTab, this.page).flat().map((e) => (e.separator) ? e : e.key)
|
||||
orderedCards.splice(idx, 1)
|
||||
this.$set(this.page.slots[this.currentModelTab][0].config, 'cardOrder', orderedCards)
|
||||
},
|
||||
cardClicked (ev, card, idx) {
|
||||
ev.cancelBubble = true
|
||||
let el = ev.target
|
||||
if (el.classList.contains('icon-checkbox')) {
|
||||
this.toggleCardDisplay(card)
|
||||
return
|
||||
}
|
||||
while (!el.classList.contains('media-item')) {
|
||||
if (el && el.classList.contains('menu')) return
|
||||
el = el.parentElement
|
||||
}
|
||||
if (card.separator) {
|
||||
this.renameCardSeparator(idx)
|
||||
}
|
||||
this.configureCard(card)
|
||||
},
|
||||
reorderCard (ev) {
|
||||
const orderedCards = this.cardGroups(this.currentModelTab, this.page).flat().map((e) => (e.separator) ? e : e.key)
|
||||
const newOrder = [...orderedCards]
|
||||
newOrder.splice(ev.to, 0, newOrder.splice(ev.from, 1)[0])
|
||||
this.$set(this.page.slots[this.currentModelTab][0].config, 'cardOrder', newOrder)
|
||||
this.cardListId = null
|
||||
this.showCardControls = false
|
||||
this.$nextTick(() => {
|
||||
this.cardListId = this.$f7.utils.id()
|
||||
})
|
||||
},
|
||||
isCardExcluded (card) {
|
||||
if (!card.key) return
|
||||
const page = this.page
|
||||
const type = this.currentModelTab
|
||||
const excludedCards = (page && page.slots && page.slots[type] && page.slots[type][0] && page.slots[type][0].config && page.slots[type][0].config.excludedCards) ? page.slots[type][0].config.excludedCards : []
|
||||
const excludedIdx = excludedCards.indexOf(card.key)
|
||||
return excludedIdx >= 0
|
||||
},
|
||||
toggleCardDisplay (card) {
|
||||
if (!card.key) return
|
||||
const page = this.page
|
||||
const type = this.currentModelTab
|
||||
const excludedCards = (page && page.slots && page.slots[type] && page.slots[type][0] && page.slots[type][0].config && page.slots[type][0].config.excludedCards) ? page.slots[type][0].config.excludedCards : []
|
||||
const excludedIdx = excludedCards.indexOf(card.key)
|
||||
if (excludedIdx < 0) {
|
||||
this.$set(this.page.slots[type][0].config, 'excludedCards', [...excludedCards, card.key])
|
||||
} else {
|
||||
this.page.slots[type][0].config.excludedCards.splice(excludedIdx, 1)
|
||||
}
|
||||
},
|
||||
toYaml () {
|
||||
this.pageYaml = YAML.stringify(Object.assign({ config: this.page.config }, this.page.slots))
|
||||
},
|
||||
fromYaml () {
|
||||
try {
|
||||
const updatedTabs = YAML.parse(this.pageYaml)
|
||||
this.$set(this.page, 'slots', updatedTabs)
|
||||
this.$set(this.page, 'config', this.page.slots.config)
|
||||
delete this.page.slots.config
|
||||
this.forceUpdate()
|
||||
return true
|
||||
} catch (e) {
|
||||
this.$f7.dialog.alert(e).open()
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -199,7 +199,8 @@ export default {
|
|||
|
||||
this.currentComponent = component
|
||||
const popup = {
|
||||
component: WidgetConfigPopup
|
||||
component: WidgetConfigPopup,
|
||||
componentType: this.type
|
||||
}
|
||||
|
||||
this.$f7router.navigate({
|
||||
|
|
|
@ -139,6 +139,7 @@ export default {
|
|||
pageTypes: [
|
||||
{ type: 'sitemap', label: 'Sitemap', componentType: 'Sitemap', icon: 'menu' },
|
||||
{ type: 'layout', label: 'Layout', componentType: 'oh-layout-page', icon: 'rectangle_grid_2x2' },
|
||||
{ type: 'home', label: 'Home', componentType: 'oh-home-page', icon: 'house' },
|
||||
{ type: 'tabs', label: 'Tabbed', componentType: 'oh-tabs-page', icon: 'squares_below_rectangle' },
|
||||
{ type: 'map', label: 'Map', componentType: 'oh-map-page', icon: 'map' },
|
||||
{ type: 'plan', label: 'Floor plan', componentType: 'oh-plan-page', icon: 'square_stack_3d_up' },
|
||||
|
|
Loading…
Reference in New Issue