Skip to content

Main UI: Visualize semantic model tree in item details screen #3227

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 18 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@
</f7-list-input>
</f7-list-group>
</f7-list>
<semantics-picker v-if="!hideSemantics" :item="item" :same-class-only="true" :hide-type="true" :hide-none="forceSemantics" :createMode="createMode" :key="'semantics-' + item.tags.toString()" />
<semantics-picker v-if="!hideSemantics" :item="item" :createMode="createMode" :hide-none="forceSemantics" />
<f7-list inline-labels no-hairline-md>
<tag-input title="Non-Semantic Tags" :disabled="!editable" :item="item" />
</f7-list>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ export default {
this.$nextTick(() => {
this.initSearchbar = true
this.restoreExpanded()
this.expandSelected()
})
})
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
<template>
<f7-treeview class="model-treeview">
<draggable :disabled="!canDragDrop" :list="children" group="model-treeview" animation="150" forceFallBack="true" fallbackOnBody="true" fallbackThreshold="5"
scrollSensitivity="200" delay="400" delayOnTouchOnly="true" touchStartThreshold="10" invertSwap="true" sort="false"
<draggable :disabled="!canDragDrop" :list="children" :group="{ name: 'model-treeview', put: allowDrop }" animation="150" forceFallBack="true" fallbackOnBody="true" fallbackThreshold="5"
scrollSensitivity="200" delay="400" delayOnTouchOnly="true" touchStartThreshold="10" invertSwap="true" sort="false" ghost-class="model-sortable-ghost"
@start="onDragStart" @change="onDragChange" @end="onDragEnd" :move="onDragMove">
<model-treeview-item v-for="node in children"
:key="node.item.name" :model="node" :parentNode="model"
:key="node.item.name" :model="node" :parentNode="model" :rootNode="model"
:includeItemName="includeItemName" :includeItemTags="includeItemTags" :canDragDrop="canDragDrop" :moveState="moveState"
@selected="nodeSelected" :selected="selected"
@checked="(item, check) => $emit('checked', item, check)"
@reload="$emit('reload')" />
<!-- Drop zone for adding at root level -->
<div v-if="canDragDrop" class="root-drop-zone">
<!-- empty space to catch drops outside children -->
</div>
</draggable>
</f7-treeview>
</template>
Expand All @@ -23,6 +27,18 @@
.semantic-class
font-size 8pt
color var(--f7-list-item-footer-text-color)
.model-sortable-ghost
visibility hidden /* Don't show, but don't use display none as this will misalign the dragged item relative to the cursor, style to have 0 total height */
position absolute
z-index -1
pointer-events none
height 0 !important
margin 0 !important
padding 0 !important
border none !important
.root-drop-zone
height 40px
margin-top 4px
</style>

<script>
Expand All @@ -39,21 +55,22 @@ export default {
ModelTreeviewItem
},
computed: {
model: {
get: function () {
return {
class: '',
children: {
locations: this.rootNodes.filter(n => n.class.startsWith('Location')),
equipment: this.rootNodes.filter(n => n.class.startsWith('Equipment')),
points: this.rootNodes.filter(n => n.class.startsWith('Point')),
groups: this.rootNodes.filter(n => !n.class && n.item.type === 'Group'),
items: this.rootNodes.filter(n => !n.class && n.item.type !== 'Group')
},
opened: true,
item: null
}
model () {
return {
class: '',
children: {
locations: this.rootNodes.filter(n => n.class.startsWith('Location')),
equipment: this.rootNodes.filter(n => n.class.startsWith('Equipment')),
points: this.rootNodes.filter(n => n.class.startsWith('Point')),
groups: this.rootNodes.filter(n => !n.class && n.item.type === 'Group'),
items: this.rootNodes.filter(n => !n.class && n.item.type !== 'Group')
},
opened: true,
item: null
}
},
rootNode () {
return this.model
}
},
methods: {
Expand Down
59 changes: 35 additions & 24 deletions bundles/org.openhab.ui/web/src/components/model/treeview-item.vue
Original file line number Diff line number Diff line change
@@ -1,27 +1,30 @@
<template>
<f7-treeview-item selectable :label="(model.item.created === false) ? '(New Item)' : (model.item.label ? (includeItemName ? model.item.label + ' (' + model.item.name + ')' : model.item.label) : model.item.name)"
<f7-treeview-item selectable :label="label"
:icon-ios="icon('ios')" :icon-aurora="icon('aurora')" :icon-md="icon('md')"
:textColor="iconColor" :color="(model.item.created !== false) ? 'blue' :'orange'"
:selected="selected && selected.item.name === model.item.name"
:opened="model.opened" :toggle="canHaveChildren"
@treeview:open="model.opened = true" @treeview:close="model.opened = false" @click="select">
<draggable :disabled="!canDragDrop" :list="children" :group="{name: 'model-treeview', put: dropAllowed(model)}" animation="150" forceFallback="true" fallbackOnBody="true" fallbackThreshold="5"
scrollSensitivity="200" delay="400" delayOnTouchOnly="true" touchStartThreshold="10" invertSwap="true" sort="false"
<draggable :disabled="!canDragDrop" :list="children" :group="{ name: 'model-treeview', put: allowDrop }" animation="150" forceFallback="true" fallbackOnBody="true" fallbackThreshold="5"
scrollSensitivity="200" delay="400" delayOnTouchOnly="true" touchStartThreshold="10" invertSwap="true" sort="false" ghost-class="model-sortable-ghost"
@start="onDragStart" @change="onDragChange" @end="onDragEnd" :move="onDragMove">
<model-treeview-item v-for="node in children"
:key="node.item.name"
:model="node"
:parentNode="model"
@selected="(event) => $emit('selected', event)"
:selected="selected"
:includeItemName="includeItemName" :includeItemTags="includeItemTags"
:canDragDrop="canDragDrop"
:moveState="moveState"
@checked="(item, check) => $emit('checked', item, check)"
@reload="$emit('reload')" />
<template v-if="model.opened">
<model-treeview-item v-for="node in children"
:key="node.item.name"
:model="node"
:parentNode="model"
:rootNode="rootNode"
@selected="(event) => $emit('selected', event)"
:selected="selected"
:includeItemName="includeItemName" :includeItemTags="includeItemTags"
:canDragDrop="canDragDrop"
:moveState="moveState"
@checked="(item, check) => $emit('checked', item, check)"
@reload="$emit('reload')" />
</template>
</draggable>
<div slot="label" class="semantic-class">
{{ className() }}
{{ className }}
<template v-if="includeItemTags">
<div class="semantic-class chip" v-for="tag in getNonSemanticTags(model.item)" :key="tag" style="height: 16px; margin-left: 4px">
<div class="chip-media bg-color-blue" style="height: 16px; width: 16px">
Expand All @@ -46,12 +49,28 @@ import Draggable from 'vuedraggable'
export default {
name: 'model-treeview-item',
mixins: [ItemMixin, ModelDragDropMixin],
props: ['model', 'parentNode', 'selected', 'includeItemName', 'includeItemTags', 'canDragDrop'],
props: ['model', 'parentNode', 'rootNode', 'selected', 'includeItemName', 'includeItemTags', 'canDragDrop'],
emits: ['reload'],
components: {
Draggable,
ModelTreeviewItem: 'model-treeview-item'
},
computed: {
label () {
const item = this.model.item
if (item.created === false) return '(New Item)'
if (item.label) return this.includeItemName ? `${item.label} (${item.name})` : item.label
return item.name
},
className () {
if (!this.model.item.metadata || !this.model.item.metadata.semantics) return ''
const semantics = this.model.item.metadata.semantics
const property = (semantics.config && semantics.config.relatesTo)
? semantics.config.relatesTo : null
return this.model.class.substring(this.model.class.lastIndexOf('_') + 1) +
((property) ? ' (' + property.replace('Property_', '') + ')' : '')
}
},
methods: {
icon (theme) {
if (this.model.class?.indexOf('Location') === 0) {
Expand All @@ -66,14 +85,6 @@ export default {
return 'material:label_outline'
}
},
className () {
if (!this.model.item.metadata || !this.model.item.metadata.semantics) return
const semantics = this.model.item.metadata.semantics
const property = (semantics.config && semantics.config.relatesTo)
? semantics.config.relatesTo : null
return this.model.class.substring(this.model.class.lastIndexOf('_') + 1) +
((property) ? ' (' + property.replace('Property_', '') + ')' : '')
},
select (event) {
let self = this
if (self.dragDropActive) return // avoid opening item properties during drag drop
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
<template>
<f7-popup closeByBackdropClick closeOnEscape @popup:open="onOpen" @popup:close="onClose">
<f7-page>
<f7-navbar :title="propertyMode ? 'Semantic Property' : 'Semantic Class'">
<f7-nav-right>
<f7-link @click="onClose">
Close
</f7-link>
</f7-nav-right>
</f7-navbar>
<f7-subnavbar :inner="false">
<f7-searchbar
search-container=".semantics-treeview"
search-item=".treeview-item"
search-in=".treeview-item-label"
:disable-button="!$theme.aurora"
@input="showFiltered($event.target.value)" />
<div class="expand-button">
<f7-button v-if="!expanded" icon-size="24" tooltip="Expand" icon-f7="rectangle_expand_vertical" @click="toggleExpanded()" />
<f7-button v-else color="gray" icon-size="24" tooltip="Collapse" icon-f7="rectangle_compress_vertical" @click="toggleExpanded()" />
</div>
</f7-subnavbar>
<f7-toolbar bottom class="toolbar-details">
<span />
<div class="padding-left padding-right text-align-center" style="font-size: 12px">
<div v-if="classMode">
<f7-checkbox :checked="!limitToClass" @change="toggleLimitToClass" />
<label @click="toggleLimitToClass" class="advanced-label">Show all classes</label>
</div>
<f7-checkbox :checked="showNames" @change="toggleShowNames" />
<label @click="toggleShowNames" class="advanced-label">Show tag names</label>
<f7-checkbox style="margin-left: 5px" :checked="showSynonyms" @change="toggleShowSynonyms" />
<label @click="toggleShowSynonyms" class="advanced-label">Show synonyms</label>
</div>
<span />
</f7-toolbar>
<semantics-treeview class="semantic-classes" :semanticTags="semanticTags" :expandedTags="expandedTags"
@selected="tagSelected" :showNames="showNames" :showSynonyms="showSynonyms"
:selectedTag="selectedTag" :selectedClass="selectedClass" :hideNone="hideNone"
picker="true" :propertyMode="!!propertyMode" :classMode="!!classMode" :limitToClass="!!limitToClass" />
</f7-page>
</f7-popup>
</template>

<style lang="stylus">
.expand-button
margin-right 8px
text-overflow unset
align-self center
.icon
margin-bottom 2.75px !important
</style>

<script>
import SemanticsTreeview from '@/components/tags/semantics-treeview.vue'

export default {
components: {
SemanticsTreeview
},
props: ['item', 'propertyMode', 'classMode', 'hideNone', 'semanticClass', 'semanticProperty'],
data () {
return {
semanticClasses: this.$store.getters.semanticClasses,
expanded: false,
expandedTags: [],
showNames: false,
showSynonyms: false,
filtering: false,
expandedBeforeFiltering: false,
selectedTag: null,
limitToClass: true
}
},
computed: {
semanticTags () {
return this.semanticClasses.Tags.map((t) => {
const tag = {
uid: t.uid,
name: t.name,
label: this.semanticClasses.Labels[t.name],
description: t.description,
synonyms: this.semanticClasses.Synonyms[t.name],
parent: t.parent
}
return tag
})
},
selectedClass () {
const selectedTag = this.semanticTags.find((t) => t.name === (this.semanticClass || this.semanticProperty)) || { uid: 'None', label: 'None' }
const tagName = selectedTag?.name
if (this.semanticClasses.Locations.indexOf(tagName) >= 0) return 'Location'
if (this.semanticClasses.Equipment.indexOf(tagName) >= 0) return 'Equipment'
if (this.semanticClasses.Points.indexOf(tagName) >= 0) return 'Point'
return ''
}
},
methods: {
onOpen () {
this.selectedTag = this.semanticTags.find((t) => t.name === (this.semanticClass || this.semanticProperty)) || { uid: 'None', label: 'None' }
// expand tree down to current selection
this.expandToSelection()
},
toggleShowNames () {
this.showNames = !this.showNames
},
toggleShowSynonyms () {
this.showSynonyms = !this.showSynonyms
},
toggleLimitToClass () {
this.limitToClass = !this.limitToClass
},
toggleExpanded () {
this.expanded = !this.expanded
this.semanticTags.forEach((t) => {
this.$set(this.expandedTags, t.uid, this.expanded)
})
this.expandToSelection()
},
expandToSelection () {
this.selectedTag?.parent?.split('_').reduce((prev, p) => {
const parent = (prev ? (prev + '_') : '') + p
this.$set(this.expandedTags, parent, true)
return parent
}, '')
},
showFiltered (filter) {
if (filter) {
if (!this.filtering) {
this.filtering = true
this.expandedBeforeFiltering = this.expanded
if (!this.expanded) {
this.toggleExpanded()
}
}
} else if (this.filtering) {
this.filtering = false
if (this.expanded && !this.expandedBeforeFiltering) {
this.toggleExpanded()
}
}
},
tagSelected (tag) {
const previousTag = this.selectedTag
if (previousTag?.name) {
const prevIndex = this.item.tags.indexOf(previousTag.name)
this.item.tags.splice(prevIndex, 1)
}
// Only add tag if note 'None'
if (tag.name) {
if (this.item.tags) {
this.item.tags.push(tag.name)
} else {
this.$set(this.item, 'tags', [tag.name])
}
}
// If changing tag to 'None', a 'Location' tag or an 'Equipment' tag, remove 'Property' tags
if (this.classMode && this.item.tags && (!tag.name || tag.uid.split('_')[0] !== 'Point')) {
const tags = [...this.item.tags]
tags.forEach((t) => {
if (this.semanticClasses.Properties.indexOf(t) >= 0) {
const index = this.item.tags.indexOf(t)
this.item.tags.splice(index, 1)
}
})
}
this.selectedTag = tag
this.$emit('changed')
},
onClose () {
this.$f7.popup.close()
this.$emit('close')
}
}
}
</script>
Loading