Model editor: Add drag & drop (#2970)
Closes #2728. Closes #969. Allow drag and drop in the model view: 1. Drag and drop inside the semantic model 2. Move items into the semantic model 3. Move between non-semantic groups (and duplicate) 4. Ask for creation of location, equipment or point if item does not have a semantic class yet when moving into the semantic model --------- Signed-off-by: Mark Herwege <mark.herwege@telenet.be>pull/3195/head
parent
d17072ae31
commit
6bdfe408e6
|
@ -8,7 +8,7 @@
|
||||||
:subtitle="noType ? '' : getItemTypeAndMetaLabel(item)"
|
:subtitle="noType ? '' : getItemTypeAndMetaLabel(item)"
|
||||||
:after="state"
|
:after="state"
|
||||||
v-on="$listeners">
|
v-on="$listeners">
|
||||||
<oh-icon v-if="!noIcon && item.category" slot="media" :icon="item.category" :state="(noState || item.type === 'Image') ? null : (context.store[item.name].state || item.state)" height="32" width="32" />
|
<oh-icon v-if="!noIcon && item.category" slot="media" :icon="item.category" :state="(noState || item.type === 'Image') ? null : (context?.store[item.name]?.state || item.state)" height="32" width="32" />
|
||||||
<span v-else-if="!noIcon" slot="media" class="item-initial">{{ item.name[0] }}</span>
|
<span v-else-if="!noIcon" slot="media" class="item-initial">{{ item.name[0] }}</span>
|
||||||
<f7-icon v-if="!item.editable" slot="after-title" f7="lock_fill" size="1rem" color="gray" />
|
<f7-icon v-if="!item.editable" slot="after-title" f7="lock_fill" size="1rem" color="gray" />
|
||||||
<slot name="footer" #footer />
|
<slot name="footer" #footer />
|
||||||
|
|
|
@ -66,7 +66,7 @@
|
||||||
<f7-block strong class="no-padding" v-if="ready">
|
<f7-block strong class="no-padding" v-if="ready">
|
||||||
<model-treeview class="model-picker-treeview" :root-nodes="rootNodes"
|
<model-treeview class="model-picker-treeview" :root-nodes="rootNodes"
|
||||||
:includeItemName="includeItemName" :includeItemTags="includeItemTags"
|
:includeItemName="includeItemName" :includeItemTags="includeItemTags"
|
||||||
:selected-item="selectedItem" @selected="selectItem" @checked="checkItem" />
|
:selected="selectedItem" @selected="selectItem" @checked="checkItem" />
|
||||||
</f7-block>
|
</f7-block>
|
||||||
<f7-block v-else-if="!ready" class="text-align-center">
|
<f7-block v-else-if="!ready" class="text-align-center">
|
||||||
<f7-preloader />
|
<f7-preloader />
|
||||||
|
@ -178,7 +178,7 @@ export default {
|
||||||
this.loadModel().then(() => {
|
this.loadModel().then(() => {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.initSearchbar = true
|
this.initSearchbar = true
|
||||||
this.applyExpandedOption()
|
this.restoreExpanded()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,10 +1,15 @@
|
||||||
<template>
|
<template>
|
||||||
<f7-treeview class="model-treeview">
|
<f7-treeview class="model-treeview">
|
||||||
<model-treeview-item v-for="node in rootNodes"
|
<draggable :disabled="!canDragDrop" :list="children" group="model-treeview" animation="150" fallbackOnBody="true" fallbackThreshold="5"
|
||||||
:key="node.item.name" :model="node"
|
scrollSensitivity="200" delay="400" delayOnTouchOnly="true" invertSwap="true"
|
||||||
:includeItemName="includeItemName" :includeItemTags="includeItemTags"
|
@start="onDragStart" @change="onDragChange" @end="onDragEnd" :move="onDragMove">
|
||||||
@selected="nodeSelected" :selected="selectedItem"
|
<model-treeview-item v-for="(node, index) in children"
|
||||||
@checked="(item, check) => $emit('checked', item, check)" />
|
:key="node.item.name + '_' + index" :model="node" :parentNode="model"
|
||||||
|
:includeItemName="includeItemName" :includeItemTags="includeItemTags" :canDragDrop="canDragDrop" :moveState="moveState"
|
||||||
|
@selected="nodeSelected" :selected="selected"
|
||||||
|
@checked="(item, check) => $emit('checked', item, check)"
|
||||||
|
@reload="$emit('reload')" />
|
||||||
|
</draggable>
|
||||||
</f7-treeview>
|
</f7-treeview>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -22,12 +27,35 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import ModelTreeviewItem from '@/components/model/treeview-item.vue'
|
import ModelTreeviewItem from '@/components/model/treeview-item.vue'
|
||||||
|
import ModelDragDropMixin from '@/pages/settings/model/model-dragdrop-mixin'
|
||||||
|
import Draggable from 'vuedraggable'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: ['rootNodes', 'selectedItem', 'includeItemName', 'includeItemTags'],
|
mixins: [ModelDragDropMixin],
|
||||||
|
props: ['rootNodes', 'selected', 'includeItemName', 'includeItemTags', 'canDragDrop'],
|
||||||
|
emits: ['reload'],
|
||||||
components: {
|
components: {
|
||||||
|
Draggable,
|
||||||
ModelTreeviewItem
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
nodeSelected (node) {
|
nodeSelected (node) {
|
||||||
this.$emit('selected', node)
|
this.$emit('selected', node)
|
||||||
|
|
|
@ -3,17 +3,23 @@
|
||||||
:icon-ios="icon('ios')" :icon-aurora="icon('aurora')" :icon-md="icon('md')"
|
:icon-ios="icon('ios')" :icon-aurora="icon('aurora')" :icon-md="icon('md')"
|
||||||
:textColor="iconColor" :color="(model.item.created !== false) ? 'blue' :'orange'"
|
:textColor="iconColor" :color="(model.item.created !== false) ? 'blue' :'orange'"
|
||||||
:selected="selected && selected.item.name === model.item.name"
|
:selected="selected && selected.item.name === model.item.name"
|
||||||
:opened="model.opened"
|
:opened="model.opened" :toggle="canHaveChildren"
|
||||||
@click="select">
|
@treeview:open="model.opened = true" @treeview:close="model.opened = false" @click="select">
|
||||||
<model-treeview-item v-for="node in [model.children.locations,
|
<draggable :disabled="!canDragDrop && !dropAllowed(model)" :list="children" group="model-treeview" animation="150" fallbackOnBody="true" fallbackThreshold="5"
|
||||||
model.children.equipment, model.children.points,
|
scrollSensitivity="200" delay="400" delayOnTouchOnly="true" invertSwap="true"
|
||||||
model.children.groups, model.children.items].flat()"
|
@start="onDragStart" @change="onDragChange" @end="onDragEnd" :move="onDragMove">
|
||||||
:key="node.item.name"
|
<model-treeview-item v-for="(node, index) in children"
|
||||||
|
:key="node.item.name + '_' + index"
|
||||||
:model="node"
|
:model="node"
|
||||||
|
:parentNode="model"
|
||||||
@selected="(event) => $emit('selected', event)"
|
@selected="(event) => $emit('selected', event)"
|
||||||
:selected="selected"
|
:selected="selected"
|
||||||
:includeItemName="includeItemName" :includeItemTags="includeItemTags"
|
:includeItemName="includeItemName" :includeItemTags="includeItemTags"
|
||||||
@checked="(item, check) => $emit('checked', item, check)" />
|
:canDragDrop="canDragDrop"
|
||||||
|
:moveState="moveState"
|
||||||
|
@checked="(item, check) => $emit('checked', item, check)"
|
||||||
|
@reload="$emit('reload')" />
|
||||||
|
</draggable>
|
||||||
<div slot="label" class="semantic-class">
|
<div slot="label" class="semantic-class">
|
||||||
{{ className() }}
|
{{ className() }}
|
||||||
<template v-if="includeItemTags">
|
<template v-if="includeItemTags">
|
||||||
|
@ -34,31 +40,25 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import ItemMixin from '@/components/item/item-mixin'
|
import ItemMixin from '@/components/item/item-mixin'
|
||||||
|
import ModelDragDropMixin from '@/pages/settings/model/model-dragdrop-mixin'
|
||||||
|
import Draggable from 'vuedraggable'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'model-treeview-item',
|
name: 'model-treeview-item',
|
||||||
mixins: [ItemMixin],
|
mixins: [ItemMixin, ModelDragDropMixin],
|
||||||
props: ['model', 'selected', 'includeItemName', 'includeItemTags'],
|
props: ['model', 'parentNode', 'selected', 'includeItemName', 'includeItemTags', 'canDragDrop'],
|
||||||
|
emits: ['reload'],
|
||||||
components: {
|
components: {
|
||||||
|
Draggable,
|
||||||
ModelTreeviewItem: 'model-treeview-item'
|
ModelTreeviewItem: 'model-treeview-item'
|
||||||
},
|
},
|
||||||
computed: {
|
|
||||||
children () {
|
|
||||||
return [this.model.children.locations,
|
|
||||||
this.model.children.equipment, this.model.children.points,
|
|
||||||
this.model.children.groups, this.model.children.items].flat()
|
|
||||||
},
|
|
||||||
iconColor () {
|
|
||||||
return (this.model.item.metadata && this.model.item.metadata.semantics) ? '' : 'gray'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
methods: {
|
||||||
icon (theme) {
|
icon (theme) {
|
||||||
if (this.model.class.indexOf('Location') === 0) {
|
if (this.model.class?.indexOf('Location') === 0) {
|
||||||
return (theme === 'md') ? 'material:place' : 'f7:placemark'
|
return (theme === 'md') ? 'material:place' : 'f7:placemark'
|
||||||
} else if (this.model.class.indexOf('Equipment') === 0) {
|
} else if (this.model.class?.indexOf('Equipment') === 0) {
|
||||||
return (theme === 'md') ? 'material:payments' : 'f7:cube_box'
|
return (theme === 'md') ? 'material:payments' : 'f7:cube_box'
|
||||||
} else if (this.model.class.indexOf('Point') === 0) {
|
} else if (this.model.class?.indexOf('Point') === 0) {
|
||||||
return (theme === 'md') ? 'material:flash_on' : 'f7:bolt_fill'
|
return (theme === 'md') ? 'material:flash_on' : 'f7:bolt_fill'
|
||||||
} else if (this.model.item.type === 'Group') {
|
} else if (this.model.item.type === 'Group') {
|
||||||
return (theme === 'md') ? 'material:folder' : 'f7:folder'
|
return (theme === 'md') ? 'material:folder' : 'f7:folder'
|
||||||
|
@ -76,6 +76,7 @@ export default {
|
||||||
},
|
},
|
||||||
select (event) {
|
select (event) {
|
||||||
let self = this
|
let self = this
|
||||||
|
if (self.dragDropActive) return // avoid opening item properties during drag drop
|
||||||
let $ = self.$$
|
let $ = self.$$
|
||||||
if ($(event.target).is('.treeview-toggle')) return
|
if ($(event.target).is('.treeview-toggle')) return
|
||||||
if ($(event.target).is('.checkbox') || $(event.target).is('.icon-checkbox') || $(event.target).is('input')) return
|
if ($(event.target).is('.checkbox') || $(event.target).is('.icon-checkbox') || $(event.target).is('input')) return
|
||||||
|
|
|
@ -43,7 +43,7 @@
|
||||||
</f7-block-footer>
|
</f7-block-footer>
|
||||||
<f7-block class="semantic-tree">
|
<f7-block class="semantic-tree">
|
||||||
<model-treeview class="model-picker-treeview" :rootNodes="rootLocations"
|
<model-treeview class="model-picker-treeview" :rootNodes="rootLocations"
|
||||||
:selected-item="selectedItem" @selected="selectItem" @checked="checkItem" />
|
:selected="selectedItem" @selected="selectItem" @checked="checkItem" />
|
||||||
</f7-block>
|
</f7-block>
|
||||||
</f7-block>
|
</f7-block>
|
||||||
</f7-col>
|
</f7-col>
|
||||||
|
|
|
@ -0,0 +1,683 @@
|
||||||
|
import cloneDeep from 'lodash/cloneDeep'
|
||||||
|
import * as types from '@/assets/item-types.js'
|
||||||
|
import ItemMixin from '@/components/item/item-mixin'
|
||||||
|
import TagMixin from '@/components/tags/tag-mixin'
|
||||||
|
import fastDeepEqual from 'fast-deep-equal/es6'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
mixins: [ItemMixin, TagMixin],
|
||||||
|
props: {
|
||||||
|
moveState: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({
|
||||||
|
moving: false,
|
||||||
|
canAdd: false,
|
||||||
|
canRemove: false,
|
||||||
|
dragEnd: true,
|
||||||
|
dragFinished: false,
|
||||||
|
saving: false,
|
||||||
|
cancelled: false,
|
||||||
|
moveConfirmed: false,
|
||||||
|
node: null,
|
||||||
|
newParent: null,
|
||||||
|
oldParent: null,
|
||||||
|
oldIndex: null,
|
||||||
|
dragStartTimestamp: null,
|
||||||
|
nodesToUpdate: [],
|
||||||
|
moveDelayedOpen: null,
|
||||||
|
moveTarget: null
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
moveState: {
|
||||||
|
handler: function () {
|
||||||
|
if (this.canSave) {
|
||||||
|
this.saveUpdate()
|
||||||
|
} else if (this.canRemove) {
|
||||||
|
this.validateRemove()
|
||||||
|
} else if (this.canAdd) {
|
||||||
|
this.validateAdd()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
deep: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
children: {
|
||||||
|
get: function () {
|
||||||
|
if (!this.model.children) return []
|
||||||
|
return [this.model.children.locations, this.model.children.equipment, this.model.children.points, this.model.children.groups, this.model.children.items].flat()
|
||||||
|
},
|
||||||
|
set: function (nodeList) {
|
||||||
|
const newChildren = {}
|
||||||
|
newChildren.locations = nodeList.filter(n => n.item.metadata?.semantics?.value?.startsWith('Location'))
|
||||||
|
newChildren.equipment = nodeList.filter(n => n.item.metadata?.semantics?.value?.startsWith('Equipment'))
|
||||||
|
newChildren.points = nodeList.filter(n => n.item.metadata?.semantics?.value?.startsWith('Point'))
|
||||||
|
newChildren.groups = nodeList.filter(n => !n.item.metadata?.semantics && n.item.type === 'Group')
|
||||||
|
newChildren.items = nodeList.filter(n => !n.item.metadata?.semantics && n.item.type !== 'Group')
|
||||||
|
this.$set(this.model, 'children', newChildren)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
iconColor () {
|
||||||
|
return (this.model.item.metadata && this.model.item.metadata.semantics) ? '' : 'gray'
|
||||||
|
},
|
||||||
|
dragDropActive () {
|
||||||
|
return !this.moveState.dragEnd
|
||||||
|
},
|
||||||
|
canAdd () {
|
||||||
|
return !this.moveState.cancelled && this.moveState.newParent && this.moveState.dragEnd &&
|
||||||
|
!this.moveState.dragFinished && this.moveState.canAdd && !this.moveState.adding
|
||||||
|
},
|
||||||
|
canRemove () {
|
||||||
|
return !this.moveState.cancelled && this.moveState.newParent && this.moveState.oldParent &&
|
||||||
|
this.moveState.dragEnd && !this.moveState.dragFinished && !this.moveState.canAdd && this.moveState.canRemove && !this.moveState.removing
|
||||||
|
},
|
||||||
|
canSave () {
|
||||||
|
return !this.moveState.cancelled && this.moveState.dragEnd && this.moveState.dragFinished && !this.moveState.canAdd && !this.moveState.canRemove && !this.moveState.saving
|
||||||
|
},
|
||||||
|
canHaveChildren () {
|
||||||
|
return ((this.model.item.type === 'Group') && (this.children.length > 0 || this.moveState.moving)) === true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onDragStart (event) {
|
||||||
|
this.moveState.node = this.children[event.oldIndex]
|
||||||
|
if (!this.moveState.node.item.editable) return
|
||||||
|
console.debug('Drag start - event:', event)
|
||||||
|
window.addEventListener('keydown', this.keyDownHandler)
|
||||||
|
this.moveState.moving = true
|
||||||
|
this.moveState.canAdd = false
|
||||||
|
this.moveState.canRemove = false
|
||||||
|
this.moveState.dragEnd = false
|
||||||
|
this.moveState.dragFinished = false
|
||||||
|
this.moveState.saving = false
|
||||||
|
this.moveState.cancelled = false
|
||||||
|
this.moveState.moveConfirmed = false
|
||||||
|
this.moveState.dragStartTimestamp = Date.now()
|
||||||
|
this.moveState.nodesToUpdate.splice(0)
|
||||||
|
this.moveState.moveDelayedOpen = null
|
||||||
|
this.moveState.moveTarget = null
|
||||||
|
console.debug('Drag start - moveState:', cloneDeep(this.moveState))
|
||||||
|
console.debug('runtime onDragStart', Date.now() - this.moveState.dragStartTimestamp)
|
||||||
|
},
|
||||||
|
onDragChange (event) {
|
||||||
|
const dropAllowed = this.moveState.moveTarget ? this.dropAllowed(this.moveState.moveTarget) : true
|
||||||
|
if (this.moveState.cancelled || !this.moveState.node.item.editable || !dropAllowed) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
console.debug('runtime onDragChange', Date.now() - this.moveState.dragStartTimestamp)
|
||||||
|
console.debug('Drag change - event:', event)
|
||||||
|
if (this.moveState.cancelled) {
|
||||||
|
console.debug('Drag change - cancelled')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (event.added) {
|
||||||
|
this.moveState.newParent = this.model
|
||||||
|
this.moveState.canAdd = true
|
||||||
|
}
|
||||||
|
if (event.removed) {
|
||||||
|
this.moveState.oldParent = this.model
|
||||||
|
this.moveState.oldIndex = event.removed.oldIndex
|
||||||
|
this.moveState.canRemove = true
|
||||||
|
}
|
||||||
|
console.debug('Drag change - moveState:', cloneDeep(this.moveState))
|
||||||
|
},
|
||||||
|
onDragMove (event) {
|
||||||
|
console.debug('Drag move - event:', event)
|
||||||
|
// cancel opening previous group we moved over as we moved away from it
|
||||||
|
const movedToSamePlace = event.relatedContext?.element?.item?.name === this.moveState.moveTarget?.item?.name
|
||||||
|
if (!movedToSamePlace) {
|
||||||
|
clearTimeout(this.moveState.moveDelayedOpen)
|
||||||
|
this.moveState.moveDelayedOpen = null
|
||||||
|
}
|
||||||
|
this.moveState.moveTarget = event.relatedContext?.element
|
||||||
|
// return if we cannot drop here
|
||||||
|
if (this.moveState.cancelled || !this.moveState.node.item.editable || !this.dropAllowed(this.moveState.moveTarget)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// Open group if not open yet, with a delay so you don't open it if you just drag over it
|
||||||
|
if (!movedToSamePlace && this.moveState.moveTarget?.item?.type === 'Group' && !this.moveState.moveTarget?.opened) {
|
||||||
|
const element = event.relatedContext.element
|
||||||
|
this.moveState.moveDelayedOpen = setTimeout(() => {
|
||||||
|
// this.$set(element, "opened", true)
|
||||||
|
element.opened = true
|
||||||
|
}, 1000, element)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
onDragEnd (event) {
|
||||||
|
if (!this.moveState.node.item.editable) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
console.debug('runtime onDragEnd', Date.now() - this.moveState.dragStartTimestamp)
|
||||||
|
console.debug('Drag end - event:', event)
|
||||||
|
window.removeEventListener('keydown', this.keyDownHandler)
|
||||||
|
if (this.moveState.cancelled) {
|
||||||
|
console.debug('Drag end - cancelled')
|
||||||
|
this.restoreModelUpdate()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.moveState.moving = false
|
||||||
|
this.moveState.dragEnd = true
|
||||||
|
console.debug('Drag end - moveState:', cloneDeep(this.moveState))
|
||||||
|
},
|
||||||
|
dropAllowed (node) {
|
||||||
|
if (node?.class?.startsWith('Point')) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (this.moveState.node?.class?.startsWith('Location') && node?.class?.startsWith('Equipment')) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
nestedSemanticNode (node) {
|
||||||
|
const children = [...node.children.locations, ...node.children.equipment, ...node.children.points, ...node.children.groups, ...node.children.items]
|
||||||
|
const semanticNode = children.find((c) => c.class !== '')
|
||||||
|
if (semanticNode) return semanticNode
|
||||||
|
return children.find((c) => {
|
||||||
|
return this.nestedSemanticNode(c)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
validateAdd () {
|
||||||
|
console.debug('runtime validateAdd start', Date.now() - this.moveState.dragStartTimestamp)
|
||||||
|
this.moveState.adding = true
|
||||||
|
const node = this.moveState.node
|
||||||
|
const parentNode = this.moveState.newParent
|
||||||
|
const oldParentNode = this.moveState.oldParent
|
||||||
|
if (node.item.name === parentNode?.item?.name) {
|
||||||
|
// This should not be possible, but just to make sure to avoid infinite loop
|
||||||
|
this.restoreModelUpdate()
|
||||||
|
console.debug('runtime validateAdd end', Date.now() - this.moveState.dragStartTimestamp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (parentNode.item && node.item.groupNames?.includes(parentNode.item.name)) {
|
||||||
|
const message = 'Group "' + this.itemLabel(parentNode.item) +
|
||||||
|
'" already contains item "' + this.itemLabel(node.item) + '"'
|
||||||
|
console.debug('Add rejected: ' + message)
|
||||||
|
console.debug('runtime dialog open', Date.now() - this.moveState.dragStartTimestamp)
|
||||||
|
this.$f7.dialog.alert(message).open()
|
||||||
|
this.restoreModelUpdate()
|
||||||
|
console.debug('runtime validateAdd end', Date.now() - this.moveState.dragStartTimestamp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (node.item.type === 'Group' && node.class === '') {
|
||||||
|
const semanticNode = this.nestedSemanticNode(node)
|
||||||
|
if (semanticNode) {
|
||||||
|
const message = 'Cannot insert non-semantic group "' + this.itemLabel(node.item) +
|
||||||
|
'" with semantic child "' + this.itemLabel(semanticNode.item) +
|
||||||
|
'" into semantic group "' + this.itemLabel(parentNode.item) + '"'
|
||||||
|
console.debug('Add rejected: ' + message)
|
||||||
|
console.debug('runtime dialog open', Date.now() - this.moveState.dragStartTimestamp)
|
||||||
|
this.$f7.dialog.alert(message).open()
|
||||||
|
this.restoreModelUpdate()
|
||||||
|
console.debug('runtime validateAdd end', Date.now() - this.moveState.dragStartTimestamp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (node.class !== '' && parentNode.class !== '' && (oldParentNode.item && oldParentNode?.class === '')) {
|
||||||
|
const message = 'Cannot move semantic item "' + this.itemLabel(node.item) +
|
||||||
|
'" from non-semantic group "' + this.itemLabel(oldParentNode.item) +
|
||||||
|
'" into semantic group "' + this.itemLabel(parentNode.item) + '"'
|
||||||
|
console.debug('Add rejected:' + message)
|
||||||
|
console.debug('runtime dialog open', Date.now() - this.moveState.dragStartTimestamp)
|
||||||
|
this.$f7.dialog.alert(message).open()
|
||||||
|
this.restoreModelUpdate()
|
||||||
|
console.debug('runtime validateAdd end', Date.now() - this.moveState.dragStartTimestamp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (node.class.startsWith('Point') && parentNode.class !== '') {
|
||||||
|
const groups = node.item.groupNames
|
||||||
|
if (oldParentNode.class.startsWith('Equipment') && parentNode.class.startsWith('Location')) {
|
||||||
|
const oldLocation = node.item.metadata.semantics.config.hasLocation
|
||||||
|
if (oldLocation) {
|
||||||
|
const message = 'Cannot move Point "' + this.itemLabel(node.item) +
|
||||||
|
'" from Equipment "' + this.itemLabel(oldParentNode.item) +
|
||||||
|
'" to Location "' + this.itemLabel(parentNode.item) +
|
||||||
|
'" as it is already in Location "' + oldLocation + '"'
|
||||||
|
console.debug('Add rejected:' + message)
|
||||||
|
console.debug('runtime dialog open', Date.now() - this.moveState.dragStartTimestamp)
|
||||||
|
this.$f7.dialog.alert(message).open()
|
||||||
|
this.restoreModelUpdate()
|
||||||
|
console.debug('runtime validateAdd end', Date.now() - this.moveState.dragStartTimestamp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else if (oldParentNode.class.startsWith('Location') && parentNode.class.startsWith('Equipment')) {
|
||||||
|
const oldEquipment = node.item.metadata.semantics.config.isPointOf
|
||||||
|
if (oldEquipment) {
|
||||||
|
const message = 'Cannot move Point "' + this.itemLabel(node.item) +
|
||||||
|
'" from Location "' + this.itemLabel(oldParentNode.item) +
|
||||||
|
'" to Equipment "' + this.itemLabel(parentNode.item) +
|
||||||
|
'" as it is already part of Equipment "' + oldEquipment + '"'
|
||||||
|
console.debug('Add rejected:' + message)
|
||||||
|
console.debug('runtime dialog open', Date.now() - this.moveState.dragStartTimestamp)
|
||||||
|
this.$f7.dialog.alert(message).open()
|
||||||
|
this.restoreModelUpdate()
|
||||||
|
console.debug('runtime validateAdd end', Date.now() - this.moveState.dragStartTimestamp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!this.isValidGroupType(node, parentNode)) {
|
||||||
|
this.restoreModelUpdate()
|
||||||
|
console.debug('runtime validateAdd end', Date.now() - this.moveState.dragStartTimestamp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (parentNode.class.startsWith('Location')) {
|
||||||
|
this.addIntoLocation(node, parentNode)
|
||||||
|
} else if (parentNode.class.startsWith('Equipment')) {
|
||||||
|
this.addIntoEquipment(node, parentNode)
|
||||||
|
} else if (parentNode.item) {
|
||||||
|
this.addIntoGroup(node, parentNode)
|
||||||
|
} else {
|
||||||
|
this.addIntoRoot(node, parentNode)
|
||||||
|
}
|
||||||
|
this.moveState.canAdd = false
|
||||||
|
this.moveState.adding = false
|
||||||
|
console.debug('runtime validateAdd end', Date.now() - this.moveState.dragStartTimestamp)
|
||||||
|
},
|
||||||
|
isValidGroupType (node, parentNode) {
|
||||||
|
console.debug('runtime isValidGroupType start', Date.now() - this.moveState.dragStartTimestamp)
|
||||||
|
const groupTypeDef = parentNode.item?.groupType?.split(':')
|
||||||
|
const baseType = groupTypeDef ? groupTypeDef[0] : 'None'
|
||||||
|
if (baseType === 'None') return true
|
||||||
|
const baseDimension = groupTypeDef && groupTypeDef.length > 1 ? groupTypeDef[1] : null
|
||||||
|
|
||||||
|
const typeDef = node.item.type !== 'Group' ? node.item.type?.split(':') : node.item.groupType?.split(':')
|
||||||
|
const type = typeDef ? typeDef[0] : 'None'
|
||||||
|
const dimension = typeDef.length > 1 ? typeDef[1] : null
|
||||||
|
if ((type === 'Number' || type === 'None') && baseType === 'Number') {
|
||||||
|
if (baseDimension && dimension && baseDimension !== dimension) {
|
||||||
|
const message = 'Group dimension "' + baseDimension +
|
||||||
|
'" of group "' + this.itemLabel(parentNode.item) +
|
||||||
|
'" not compatible with "' + (node.item.type === 'Group' ? 'group ' : '') + 'item dimension "' + dimension +
|
||||||
|
'" of "' + (node.item.type === 'Group' ? 'group ' : '') + '" item "' + this.itemLabel(node.item) + '"'
|
||||||
|
console.debug('Add rejected: ' + message)
|
||||||
|
console.debug('runtime dialog open', Date.now() - this.moveState.dragStartTimestamp)
|
||||||
|
this.$f7.dialog.alert(message).open()
|
||||||
|
console.debug('runtime isValidGroupType end', Date.now() - this.moveState.dragStartTimestamp)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (dimension) {
|
||||||
|
const childWithDifferentDimension = parentNode.children.map((child) => {
|
||||||
|
const childTypeDef = child.item.type !== 'Group' ? child.item.type.split(':') : child.item.groupType?.split(':')
|
||||||
|
return childTypeDef.length > 1 ? { item: child.item, dimension: childTypeDef[1] } : null
|
||||||
|
}).find((child) => { return dimension !== child?.dimension })
|
||||||
|
if (childWithDifferentDimension) {
|
||||||
|
const message = 'Group "' + this.itemLabel(parentNode.item) +
|
||||||
|
'" already contains item "' + this.itemLabel(childWithDifferentDimension.item) +
|
||||||
|
'" with dimension "' + childWithDifferentDimension.dimension +
|
||||||
|
'" different from group dimension "' + dimension + '"'
|
||||||
|
console.debug('Add rejected: ' + message)
|
||||||
|
console.debug('runtime dialog open', Date.now() - this.moveState.dragStartTimestamp)
|
||||||
|
this.$f7.dialog.alert(message).open()
|
||||||
|
console.debug('runtime isValidGroupType end', Date.now() - this.moveState.dragStartTimestamp)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const aggregationFunction = parentNode.item?.function?.name
|
||||||
|
if (aggregationFunction && !this.aggregationFunctions(type).includes(aggregationFunction)) {
|
||||||
|
const message = 'Group aggreggation function "' + aggregationFunction +
|
||||||
|
'" for group "' + this.itemLabel(parentNode.item) +
|
||||||
|
'" not compatible with type "' + type +
|
||||||
|
'" of item "' + this.itemLabel(node.item) + '"'
|
||||||
|
console.debug('Add rejected: ' + message)
|
||||||
|
console.debug('runtime dialog open', Date.now() - this.moveState.dragStartTimestamp)
|
||||||
|
this.$f7.dialog.alert(message).open()
|
||||||
|
console.debug('runtime isValidGroupType end', Date.now() - this.moveState.dragStartTimestamp)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
console.debug('runtime isValidGroupType end', Date.now() - this.moveState.dragStartTimestamp)
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
aggregationFunctions (type) {
|
||||||
|
const specificAggregationFunctions = (type) => {
|
||||||
|
switch (type) {
|
||||||
|
case 'Dimmer':
|
||||||
|
case 'Rollershutter':
|
||||||
|
case 'Number':
|
||||||
|
return types.ArithmeticFunctions
|
||||||
|
case 'Contact':
|
||||||
|
return types.LogicalOpenClosedFunctions
|
||||||
|
case 'Player':
|
||||||
|
return types.LogicalPlayPauseFunctions
|
||||||
|
case 'DateTime':
|
||||||
|
return types.DateTimeFunctions
|
||||||
|
case 'Switch':
|
||||||
|
return types.LogicalOnOffFunctions
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
return [...types.CommonFunctions, ...specificAggregationFunctions(type)]
|
||||||
|
},
|
||||||
|
addIntoLocation (node, parentNode) {
|
||||||
|
console.debug('runtime addIntoLocation start', Date.now() - this.moveState.dragStartTimestamp)
|
||||||
|
if (node.class.startsWith('Location')) {
|
||||||
|
this.addLocation(node, parentNode)
|
||||||
|
} else if (node.class.startsWith('Equipment')) {
|
||||||
|
this.addEquipment(node, parentNode)
|
||||||
|
} else if (node.class.startsWith('Point')) {
|
||||||
|
this.addPoint(node, parentNode)
|
||||||
|
} else if (node.item.type === 'Group') {
|
||||||
|
this.moveState.moveConfirmed = true
|
||||||
|
console.debug('runtime dialog open', Date.now() - this.moveState.dragStartTimestamp)
|
||||||
|
this.$f7.dialog.create({
|
||||||
|
text: 'Insert "' + this.itemLabel(node.item) +
|
||||||
|
'" into "' + this.itemLabel(parentNode.item) +
|
||||||
|
'" as',
|
||||||
|
verticalButtons: true,
|
||||||
|
buttons: [
|
||||||
|
{ text: 'Cancel', color: 'gray', keycodes: [27], onClick: () => this.restoreModelUpdate() },
|
||||||
|
{ text: 'Location', strong: true, keycodes: [13], onClick: () => this.addLocation(node, parentNode) },
|
||||||
|
{ text: 'Equipment', onClick: () => this.addEquipment(node, parentNode) }
|
||||||
|
]
|
||||||
|
}).open()
|
||||||
|
} else {
|
||||||
|
this.moveState.moveConfirmed = true
|
||||||
|
console.debug('runtime dialog open', Date.now() - this.moveState.dragStartTimestamp)
|
||||||
|
this.$f7.dialog.create({
|
||||||
|
text: 'Insert "' + this.itemLabel(node.item) +
|
||||||
|
'" into "' + this.itemLabel(parentNode.item) +
|
||||||
|
'" as',
|
||||||
|
verticalButtons: true,
|
||||||
|
buttons: [
|
||||||
|
{ text: 'Cancel', color: 'gray', keycodes: [27], onClick: () => this.restoreModelUpdate() },
|
||||||
|
{ text: 'Equipment', onClick: () => this.addEquipment(node, parentNode) },
|
||||||
|
{ text: 'Point', strong: true, keycodes: [13], onClick: () => this.addPoint(node, parentNode) }
|
||||||
|
]
|
||||||
|
}).open()
|
||||||
|
}
|
||||||
|
console.debug('runtime addIntoLocation end', Date.now() - this.moveState.dragStartTimestamp)
|
||||||
|
},
|
||||||
|
addIntoEquipment (node, parentNode) {
|
||||||
|
console.debug('runtime addIntoEquipment start', Date.now() - this.moveState.dragStartTimestamp)
|
||||||
|
if (node.class.startsWith('Location')) {
|
||||||
|
console.debug('runtime dialog open', Date.now() - this.moveState.dragStartTimestamp)
|
||||||
|
this.$f7.dialog.alert(
|
||||||
|
'Cannot move Location "' + this.itemLabel(node.item) +
|
||||||
|
'" into Equipment "' + this.itemLabel(parentNode.item) + '"'
|
||||||
|
).open()
|
||||||
|
this.restoreModelUpdate()
|
||||||
|
} else if (node.class.startsWith('Equipment')) {
|
||||||
|
this.addEquipment(node, parentNode)
|
||||||
|
} else if (node.class.startsWith('Point')) {
|
||||||
|
this.addPoint(node, parentNode)
|
||||||
|
} else if (node.item.type === 'Group') {
|
||||||
|
this.addEquipment(node, parentNode)
|
||||||
|
} else {
|
||||||
|
this.moveState.moveConfirmed = true
|
||||||
|
console.debug('runtime dialog open', Date.now() - this.moveState.dragStartTimestamp)
|
||||||
|
const dialog = this.$f7.dialog.create({
|
||||||
|
text: 'Insert "' + this.itemLabel(node.item) +
|
||||||
|
'" into "' + this.itemLabel(parentNode.item) +
|
||||||
|
'" as',
|
||||||
|
verticalButtons: true,
|
||||||
|
buttons: [
|
||||||
|
{ text: 'Cancel', color: 'gray', keycodes: [27], onClick: () => this.restoreModelUpdate() },
|
||||||
|
{ text: 'Equipment', onClick: () => this.addEquipment(node, parentNode) },
|
||||||
|
{ text: 'Point', strong: true, keycodes: [13], onClick: () => this.addPoint(node, parentNode) }
|
||||||
|
]
|
||||||
|
}).open()
|
||||||
|
}
|
||||||
|
console.debug('runtime addIntoEquipment end', Date.now() - this.moveState.dragStartTimestamp)
|
||||||
|
},
|
||||||
|
addIntoGroup (node, parentNode) {
|
||||||
|
console.debug('runtime addIntoGroup start', Date.now() - this.moveState.dragStartTimestamp)
|
||||||
|
if (node.class.startsWith('Location')) {
|
||||||
|
this.addLocation(node, parentNode)
|
||||||
|
} else if (node.class.startsWith('Equipment')) {
|
||||||
|
this.addEquipment(node, parentNode)
|
||||||
|
} else if (node.class.startsWith('Point')) {
|
||||||
|
this.addPoint(node, parentNode)
|
||||||
|
} else {
|
||||||
|
this.addNonSemantic(node, parentNode)
|
||||||
|
}
|
||||||
|
console.debug('runtime addIntoGroup end', Date.now() - this.moveState.dragStartTimestamp)
|
||||||
|
},
|
||||||
|
addIntoRoot (node, parentNode) {
|
||||||
|
console.debug('runtime addIntoRoot start', Date.now() - this.moveState.dragStartTimestamp)
|
||||||
|
if (node.class.startsWith('Location')) {
|
||||||
|
this.addLocation(node, parentNode)
|
||||||
|
} else if (node.class.startsWith('Equipment')) {
|
||||||
|
this.addEquipment(node, parentNode)
|
||||||
|
} else if (node.class.startsWith('Point')) {
|
||||||
|
this.addPoint(node, parentNode)
|
||||||
|
} else if (node.item.type === 'Group') {
|
||||||
|
this.moveState.moveConfirmed = true
|
||||||
|
console.debug('runtime dialog open', Date.now() - this.moveState.dragStartTimestamp)
|
||||||
|
this.$f7.dialog.create({
|
||||||
|
text: 'Insert "' + this.itemLabel(node.item) +
|
||||||
|
'" into "' + this.itemLabel(parentNode.item) +
|
||||||
|
'" as',
|
||||||
|
verticalButtons: true,
|
||||||
|
buttons: [
|
||||||
|
{ text: 'Cancel', color: 'gray', keycodes: [27], onClick: () => this.restoreModelUpdate() },
|
||||||
|
{ text: 'Location', onClick: () => this.addLocation(node, parentNode) },
|
||||||
|
{ text: 'Equipment', onClick: () => this.addEquipment(node, parentNode) },
|
||||||
|
{ text: 'Non Semantic', strong: true, keycodes: [13], onClick: () => this.addNonSemantic(node, parentNode) }
|
||||||
|
]
|
||||||
|
}).open()
|
||||||
|
} else {
|
||||||
|
this.moveState.moveConfirmed = true
|
||||||
|
console.debug('runtime dialog open', Date.now() - this.moveState.dragStartTimestamp)
|
||||||
|
this.$f7.dialog.create({
|
||||||
|
text: 'Insert "' + this.itemLabel(node.item) +
|
||||||
|
'" into "' + this.itemLabel(parentNode.item) +
|
||||||
|
'" as',
|
||||||
|
verticalButtons: true,
|
||||||
|
buttons: [
|
||||||
|
{ text: 'Cancel', color: 'gray', keycodes: [27], onClick: () => this.restoreModelUpdate() },
|
||||||
|
{ text: 'Equipment', onClick: () => this.addEquipment(node, parentNode) },
|
||||||
|
{ text: 'Point', onClick: () => this.addPoint(node, parentNode) },
|
||||||
|
{ text: 'Non Semantic', strong: true, keycodes: [13], onClick: () => this.addNonSemantic(node, parentNode) }
|
||||||
|
]
|
||||||
|
}).open()
|
||||||
|
}
|
||||||
|
console.debug('runtime addIntoRoot end', Date.now() - this.moveState.dragStartTimestamp)
|
||||||
|
},
|
||||||
|
addLocation (node, parentNode) {
|
||||||
|
console.debug('runtime addLocation start', Date.now() - this.moveState.dragStartTimestamp)
|
||||||
|
const semantics = { config: {} }
|
||||||
|
semantics.value = node.item?.metadata?.semantics?.value || 'Location'
|
||||||
|
if (parentNode.class.startsWith('Location')) {
|
||||||
|
semantics.config.isPartOf = parentNode.item.name
|
||||||
|
}
|
||||||
|
if (!node.item.tags.includes(semantics.value)) node.item.tags.push(semantics.value)
|
||||||
|
node.class = semantics.value
|
||||||
|
const nodeChildren = this.nodeChildren(node)
|
||||||
|
nodeChildren.filter((n) => !n.class).forEach((n) => this.addIntoLocation(n, node))
|
||||||
|
this.updateAfterAdd(node, parentNode, semantics)
|
||||||
|
console.debug('runtime addLocation end', Date.now() - this.moveState.dragStartTimestamp)
|
||||||
|
},
|
||||||
|
addEquipment (node, parentNode) {
|
||||||
|
console.debug('runtime addEquipment start', Date.now() - this.moveState.dragStartTimestamp)
|
||||||
|
const semantics = { config: {} }
|
||||||
|
semantics.value = node.item?.metadata?.semantics?.value || 'Equipment'
|
||||||
|
if (parentNode.class.startsWith('Location')) {
|
||||||
|
semantics.config.hasLocation = parentNode.item.name
|
||||||
|
} else if (parentNode.class.startsWith('Equipment')) {
|
||||||
|
semantics.config.isPartOf = parentNode.item.name
|
||||||
|
}
|
||||||
|
if (!node.item.tags.includes(semantics.value)) node.item.tags.push(semantics.value)
|
||||||
|
node.class = semantics.value
|
||||||
|
const nodeChildren = this.nodeChildren(node)
|
||||||
|
nodeChildren.filter((n) => !n.class).forEach((n) => this.addIntoEquipment(n, node))
|
||||||
|
this.updateAfterAdd(node, parentNode, semantics)
|
||||||
|
console.debug('runtime addEquipment end', Date.now() - this.moveState.dragStartTimestamp)
|
||||||
|
},
|
||||||
|
addPoint (node, parentNode) {
|
||||||
|
console.debug('runtime addPoint start', Date.now() - this.moveState.dragStartTimestamp)
|
||||||
|
const semantics = { config: {} }
|
||||||
|
semantics.value = node.item?.metadata?.semantics?.value || 'Point'
|
||||||
|
if (parentNode.class.startsWith('Location')) {
|
||||||
|
semantics.config.hasLocation = parentNode.item.name
|
||||||
|
} else if (parentNode.class.startsWith('Equipment')) {
|
||||||
|
semantics.config.isPointOf = parentNode.item.name
|
||||||
|
}
|
||||||
|
if (!node.item.tags.includes(semantics.value)) node.item.tags.push(semantics.value)
|
||||||
|
node.class = semantics.value
|
||||||
|
this.updateAfterAdd(node, parentNode, semantics)
|
||||||
|
console.debug('runtime addPoint end', Date.now() - this.moveState.dragStartTimestamp)
|
||||||
|
},
|
||||||
|
addNonSemantic (node, parentNode) {
|
||||||
|
console.debug('runtime addNonSemantic start', Date.now() - this.moveState.dragStartTimestamp)
|
||||||
|
node.class = ''
|
||||||
|
this.updateAfterAdd(node, parentNode, null)
|
||||||
|
console.debug('runtime addNonSemantic end', Date.now() - this.moveState.dragStartTimestamp)
|
||||||
|
},
|
||||||
|
updateAfterAdd (node, parentNode, semantics) {
|
||||||
|
console.debug('runtime updateAfterAdd start', Date.now() - this.moveState.dragStartTimestamp)
|
||||||
|
let updateRequired = false
|
||||||
|
if (semantics === null) {
|
||||||
|
if (node.item.metadata?.semantics) {
|
||||||
|
node.item.metadata.semantics = null
|
||||||
|
updateRequired = true
|
||||||
|
}
|
||||||
|
} else if (node.item.metadata) {
|
||||||
|
if (!fastDeepEqual(node.item.metadata.semantics, semantics)) {
|
||||||
|
node.item.metadata.semantics = semantics
|
||||||
|
updateRequired = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
node.item.metadata = { semantics }
|
||||||
|
updateRequired = true
|
||||||
|
}
|
||||||
|
if (parentNode.item?.type === 'Group' && !node.item.groupNames.includes(parentNode.item.name)) {
|
||||||
|
node.item.groupNames.push(parentNode.item.name)
|
||||||
|
updateRequired = true
|
||||||
|
}
|
||||||
|
console.debug('Add - new moveState:', cloneDeep(this.moveState))
|
||||||
|
if (!this.children.some(n => n.item.name === node.item.name)) {
|
||||||
|
// sometimes the list gets updates when dragging, sometimes it is missed so we have to add here
|
||||||
|
this.children.push(node)
|
||||||
|
}
|
||||||
|
const newChildren = this.children
|
||||||
|
this.children = newChildren // force setters to update model
|
||||||
|
if (updateRequired) {
|
||||||
|
this.moveState.nodesToUpdate.push(node)
|
||||||
|
}
|
||||||
|
console.debug('Add - finished, new moveState:', cloneDeep(this.moveState))
|
||||||
|
console.debug('runtime updateAfterAdd end', Date.now() - this.moveState.dragStartTimestamp)
|
||||||
|
},
|
||||||
|
validateRemove () {
|
||||||
|
console.debug('runtime validateRemove start', Date.now() - this.moveState.dragStartTimestamp)
|
||||||
|
this.moveState.removing = true
|
||||||
|
const node = this.moveState.node
|
||||||
|
const newParentNode = this.moveState.newParent
|
||||||
|
const parentNode = this.moveState.oldParent
|
||||||
|
const oldIndex = this.moveState.oldIndex
|
||||||
|
console.debug('Remove - new moveState:', cloneDeep(this.moveState))
|
||||||
|
if (!newParentNode.item) {
|
||||||
|
// moving into root, so remove from source
|
||||||
|
// issue: it will only remove from the current parent, not all
|
||||||
|
this.remove(node, parentNode, oldIndex)
|
||||||
|
} else if (parentNode.class !== '' && newParentNode.class !== '') {
|
||||||
|
// in general remove from semantic model group, unless moving into non-semantic group
|
||||||
|
this.remove(node, parentNode, oldIndex)
|
||||||
|
} else if (!parentNode.item && node.class !== '') {
|
||||||
|
// always remove semantic item from root level when moving into another group
|
||||||
|
this.remove(node, parentNode, oldIndex)
|
||||||
|
} else if (parentNode.item?.type === 'Group') {
|
||||||
|
this.moveState.moveConfirmed = true
|
||||||
|
console.debug('runtime dialog open', Date.now() - this.moveState.dragStartTimestamp)
|
||||||
|
this.$f7.dialog.create({
|
||||||
|
text: 'Item "' + this.itemLabel(node.item) +
|
||||||
|
'" dragged from group "' + this.itemLabel(parentNode.item) +
|
||||||
|
'" into "' + this.itemLabel(newParentNode.item) +
|
||||||
|
'", keep original?',
|
||||||
|
buttons: [
|
||||||
|
{ text: 'Cancel', color: 'gray', keycodes: [27], onClick: () => this.restoreModelUpdate() },
|
||||||
|
{ text: 'Yes', strong: true, keycodes: [13], onClick: () => this.updateAfterRemove() },
|
||||||
|
{ text: 'No', onClick: () => this.remove(node, parentNode, oldIndex) }
|
||||||
|
]
|
||||||
|
}).open()
|
||||||
|
} else {
|
||||||
|
this.updateAfterRemove()
|
||||||
|
}
|
||||||
|
console.debug('runtime validateRemove end', Date.now() - this.moveState.dragStartTimestamp)
|
||||||
|
},
|
||||||
|
remove (node, parentNode, oldIndex) {
|
||||||
|
console.debug('runtime remove start', Date.now() - this.moveState.dragStartTimestamp)
|
||||||
|
const groupNameIndex = node.item.groupNames.findIndex(g => g === parentNode.item?.name)
|
||||||
|
if (groupNameIndex >= 0) {
|
||||||
|
node.item.groupNames.splice(groupNameIndex, 1)
|
||||||
|
}
|
||||||
|
const newChildren = this.nodeChildren(parentNode)
|
||||||
|
newChildren.splice(oldIndex, 1)
|
||||||
|
this.children = newChildren
|
||||||
|
if (parentNode.class === '' && parentNode.item?.type === 'Group') {
|
||||||
|
// Moving a semantic item to a non-semantic group, remove semantics
|
||||||
|
if (node.item.metadata) {
|
||||||
|
node.item.metadata.semantics = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.updateAfterRemove()
|
||||||
|
console.debug('Remove - finished, new moveState:', cloneDeep(this.moveState))
|
||||||
|
console.debug('runtime remove end', Date.now() - this.moveState.dragStartTimestamp)
|
||||||
|
},
|
||||||
|
updateAfterRemove () {
|
||||||
|
console.debug('runtime updateAfterRemove start', Date.now() - this.moveState.dragStartTimestamp)
|
||||||
|
this.moveState.canRemove = false
|
||||||
|
this.moveState.removing = false
|
||||||
|
this.moveState.dragFinished = true
|
||||||
|
console.debug('runtime updateAfterRemove end', Date.now() - this.moveState.dragStartTimestamp)
|
||||||
|
},
|
||||||
|
saveUpdate () {
|
||||||
|
console.debug('runtime saveUpdate start', Date.now() - this.moveState.dragStartTimestamp)
|
||||||
|
this.moveState.saving = true
|
||||||
|
const node = this.moveState.node
|
||||||
|
const parentNode = this.moveState.newParent
|
||||||
|
if (!this.moveState.moveConfirmed) {
|
||||||
|
console.debug('runtime dialog open', Date.now() - this.moveState.dragStartTimestamp)
|
||||||
|
this.$f7.dialog.confirm(
|
||||||
|
'Move "' + this.itemLabel(node.item) + '" into "' + this.itemLabel(parentNode.item) + '"?',
|
||||||
|
() => this.saveModelUpdate(),
|
||||||
|
() => this.restoreModelUpdate()
|
||||||
|
).open()
|
||||||
|
} else {
|
||||||
|
this.saveModelUpdate()
|
||||||
|
}
|
||||||
|
console.debug('runtime saveUpdate end', Date.now() - this.moveState.dragStartTimestamp)
|
||||||
|
},
|
||||||
|
saveModelUpdate () {
|
||||||
|
console.debug('runtime saveModelUpdate start', Date.now() - this.moveState.dragStartTimestamp)
|
||||||
|
this.moveState.dragFinished = false
|
||||||
|
this.moveState.nodesToUpdate.forEach((n) => {
|
||||||
|
const updatedItem = n.item
|
||||||
|
console.debug('Save - updatedItem: ', cloneDeep(updatedItem))
|
||||||
|
this.saveItem(updatedItem)
|
||||||
|
})
|
||||||
|
this.moveState.saving = false
|
||||||
|
console.debug('runtime saveModelUpdate end', Date.now() - this.moveState.dragStartTimestamp)
|
||||||
|
},
|
||||||
|
restoreModelUpdate () {
|
||||||
|
console.debug('Restore model')
|
||||||
|
console.debug('runtime restoreModelUpdate start', Date.now() - this.moveState.dragStartTimestamp)
|
||||||
|
this.moveState.cancelled = true
|
||||||
|
this.moveState.canRemove = false
|
||||||
|
this.moveState.canAdd = false
|
||||||
|
this.moveState.adding = false
|
||||||
|
this.moveState.removing = false
|
||||||
|
this.moveState.saving = false
|
||||||
|
this.$emit('reload')
|
||||||
|
console.debug('runtime restoreModelUpdate end', Date.now() - this.moveState.dragStartTimestamp)
|
||||||
|
},
|
||||||
|
itemLabel (item) {
|
||||||
|
if (!item) return 'model root'
|
||||||
|
return (item.label ? (this.includeItemName ? item.label + ' (' + item.name + ')' : item.label) : item.name)
|
||||||
|
},
|
||||||
|
nodeChildren (node) {
|
||||||
|
if (!node) return this.children
|
||||||
|
if (!node.children) return []
|
||||||
|
return [node.children.locations, node.children.equipment, node.children.points, node.children.groups, node.children.items].flat()
|
||||||
|
},
|
||||||
|
keyDownHandler (event) {
|
||||||
|
if (!event.repeat && event.keyCode === 27) {
|
||||||
|
console.debug('escape pressed')
|
||||||
|
console.debug('runtime escape', Date.now() - this.moveState.dragStartTimestamp)
|
||||||
|
this.moveState.cancelled = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,7 +5,7 @@ function compareModelItems (o1, o2) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mixin for model page and model picker popup.
|
* Mixin for the model page and model picker popup.
|
||||||
*
|
*
|
||||||
* The component using this mixin has to provide the following methods:
|
* The component using this mixin has to provide the following methods:
|
||||||
* - `selectItem(item)`: Called when an item is selected.
|
* - `selectItem(item)`: Called when an item is selected.
|
||||||
|
@ -22,12 +22,14 @@ export default {
|
||||||
links: [],
|
links: [],
|
||||||
locations: [],
|
locations: [],
|
||||||
rootLocations: [],
|
rootLocations: [],
|
||||||
equipment: {},
|
equipment: [],
|
||||||
rootEquipment: [],
|
rootEquipment: [],
|
||||||
rootPoints: [],
|
rootPoints: [],
|
||||||
rootGroups: [],
|
rootGroups: [],
|
||||||
rootItems: [],
|
rootItems: [],
|
||||||
|
|
||||||
|
expandedTreeviewItems: [],
|
||||||
|
|
||||||
previousSelection: null,
|
previousSelection: null,
|
||||||
selectedItem: null
|
selectedItem: null
|
||||||
}
|
}
|
||||||
|
@ -39,9 +41,11 @@ export default {
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
loadModel () {
|
loadModel () {
|
||||||
if (this.loading) return
|
if (this.loading) return Promise.resolve()
|
||||||
this.loading = true
|
this.loading = true
|
||||||
|
|
||||||
|
this.saveExpanded()
|
||||||
|
|
||||||
const items = this.$oh.api.get('/rest/items?staticDataOnly=true&metadata=.+')
|
const items = this.$oh.api.get('/rest/items?staticDataOnly=true&metadata=.+')
|
||||||
const links = this.$oh.api.get('/rest/links')
|
const links = this.$oh.api.get('/rest/links')
|
||||||
return Promise.all([items, links]).then((data) => {
|
return Promise.all([items, links]).then((data) => {
|
||||||
|
@ -125,7 +129,7 @@ export default {
|
||||||
|
|
||||||
if (this.includeNonSemantic) {
|
if (this.includeNonSemantic) {
|
||||||
parent.children.groups = this.items
|
parent.children.groups = this.items
|
||||||
.filter((i) => i.type === 'Group' && (!i.metadata || (i.metadata && !i.metadata.semantics)) && i.groupNames.indexOf(parent.item.name) >= 0)
|
.filter((i) => i.type === 'Group' && !(parent.item.metadata && parent.item.metadata.semantics) && i.groupNames.indexOf(parent.item.name) >= 0)
|
||||||
.map(this.modelItem).sort(compareModelItems)
|
.map(this.modelItem).sort(compareModelItems)
|
||||||
parent.children.groups.forEach(this.getChildren)
|
parent.children.groups.forEach(this.getChildren)
|
||||||
if (parent.item.metadata && parent.item.metadata.semantics) {
|
if (parent.item.metadata && parent.item.metadata.semantics) {
|
||||||
|
@ -155,6 +159,20 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
},
|
||||||
|
saveExpanded () {
|
||||||
|
this.expandedTreeviewItems = [...document.querySelectorAll('.treeview-item-opened')]
|
||||||
|
},
|
||||||
|
restoreExpanded () {
|
||||||
|
const treeviewItems = document.querySelectorAll('.treeview-item')
|
||||||
|
|
||||||
|
treeviewItems.forEach(item => {
|
||||||
|
if (item.classList.contains('treeview-item')) {
|
||||||
|
if (this.expanded || this.expandedTreeviewItems.includes(item)) {
|
||||||
|
item.classList.add('treeview-item-opened')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,7 +35,7 @@
|
||||||
</div>
|
</div>
|
||||||
<f7-link class="right details-link padding-right" ref="detailsLink" @click="detailsOpened = true" icon-f7="chevron_up" />
|
<f7-link class="right details-link padding-right" ref="detailsLink" @click="detailsOpened = true" icon-f7="chevron_up" />
|
||||||
</f7-toolbar>
|
</f7-toolbar>
|
||||||
<f7-toolbar v-else bottom class="toolbar-details" style="height: calc(50px + var(--f7-safe-area-bottom))">
|
<f7-toolbar v-else bottom class="toolbar-details">
|
||||||
<f7-link :disabled="selectedItem != null" class="left" @click="selectedItem = null">
|
<f7-link :disabled="selectedItem != null" class="left" @click="selectedItem = null">
|
||||||
Clear
|
Clear
|
||||||
</f7-link>
|
</f7-link>
|
||||||
|
@ -71,13 +71,9 @@
|
||||||
</f7-block>
|
</f7-block>
|
||||||
|
|
||||||
<f7-block v-show="!empty" strong class="semantic-tree" no-gap @click.native="clearSelection">
|
<f7-block v-show="!empty" strong class="semantic-tree" no-gap @click.native="clearSelection">
|
||||||
<!-- <empty-state-placeholder v-if="empty" icon="list_bullet_indent" title="model.title" text="model.text" /> -->
|
<model-treeview :rootNodes="[rootLocations, rootEquipment, rootPoints, rootGroups, rootItems].flat()" :items="items"
|
||||||
<f7-treeview>
|
:includeItemName="includeItemName" :includeItemTags="includeItemTags" :canDragDrop="true"
|
||||||
<model-treeview-item v-for="node in [rootLocations, rootEquipment, rootPoints, rootGroups, rootItems].flat()"
|
@selected="selectItem" :selected="selectedItem" @reload="load" />
|
||||||
:key="node.item.name" :model="node"
|
|
||||||
:includeItemName="includeItemName" :includeItemTags="includeItemTags"
|
|
||||||
@selected="selectItem" :selected="selectedItem" />
|
|
||||||
</f7-treeview>
|
|
||||||
</f7-block>
|
</f7-block>
|
||||||
</f7-col>
|
</f7-col>
|
||||||
<f7-col width="100" medium="50" class="details-pane">
|
<f7-col width="100" medium="50" class="details-pane">
|
||||||
|
@ -172,20 +168,21 @@
|
||||||
|
|
||||||
<style lang="stylus">
|
<style lang="stylus">
|
||||||
.semantic-tree-wrapper
|
.semantic-tree-wrapper
|
||||||
|
user-select: none
|
||||||
padding 0
|
padding 0
|
||||||
margin-bottom 0
|
.row
|
||||||
|
height 100%
|
||||||
|
.col-100
|
||||||
|
height 100%
|
||||||
|
overflow auto
|
||||||
.semantic-tree
|
.semantic-tree
|
||||||
padding 0
|
min-height 100%
|
||||||
|
margin 0
|
||||||
|
height auto
|
||||||
|
.semantic-tree
|
||||||
|
user-select: none
|
||||||
|
margin 0 !important
|
||||||
border-right 1px solid var(--f7-block-strong-border-color)
|
border-right 1px solid var(--f7-block-strong-border-color)
|
||||||
.treeview
|
|
||||||
--f7-treeview-item-height 40px
|
|
||||||
.treeview-item-label
|
|
||||||
font-size 10pt
|
|
||||||
white-space nowrap
|
|
||||||
overflow-x hidden
|
|
||||||
.semantic-class
|
|
||||||
font-size 8pt
|
|
||||||
color var(--f7-list-item-footer-text-color)
|
|
||||||
.model-details-sheet
|
.model-details-sheet
|
||||||
.toolbar
|
.toolbar
|
||||||
--f7-theme-color var(--f7-color-blue)
|
--f7-theme-color var(--f7-color-blue)
|
||||||
|
@ -196,16 +193,8 @@
|
||||||
|
|
||||||
@media (min-width: 768px)
|
@media (min-width: 768px)
|
||||||
.semantic-tree-wrapper
|
.semantic-tree-wrapper
|
||||||
height calc(100% - var(--f7-navbar-height))
|
height calc(100% - var(--f7-toolbar-height))
|
||||||
.row
|
.row
|
||||||
height 100%
|
|
||||||
.col-100
|
|
||||||
height 100%
|
|
||||||
overflow auto
|
|
||||||
.semantic-tree
|
|
||||||
min-height 100%
|
|
||||||
margin 0
|
|
||||||
height auto
|
|
||||||
.details-pane
|
.details-pane
|
||||||
padding-top 0
|
padding-top 0
|
||||||
.block
|
.block
|
||||||
|
@ -217,12 +206,22 @@
|
||||||
visibility hidden !important
|
visibility hidden !important
|
||||||
|
|
||||||
@media (max-width: 767px)
|
@media (max-width: 767px)
|
||||||
|
.semantic-tree-wrapper.block:first-child
|
||||||
|
margin-top 5px
|
||||||
|
.semantic-tree-wrapper
|
||||||
|
height calc(100% - 20px)
|
||||||
|
margin-bottom 5px
|
||||||
.details-pane
|
.details-pane
|
||||||
display none
|
display none
|
||||||
.semantic-tree-wrapper.sheet-opened
|
.semantic-tree-wrapper.sheet-opened
|
||||||
margin-bottom var(--f7-sheet-height)
|
height calc(100% - 5px - var(--f7-sheet-height) + var(--f7-page-toolbar-bottom-offset, 0px) + var(--f7-page-content-extra-padding-bottom, 0px))
|
||||||
.details-sheet
|
margin-bottom calc(var(--f7-sheet-height) - var(--f7-page-toolbar-bottom-offset, 0px) - var(--f7-page-content-extra-padding-bottom, 0px))
|
||||||
height calc(1.4*var(--f7-sheet-height))
|
.toolbar-details.toolbar.toolbar-bottom
|
||||||
|
height calc( 50px + var(--f7-safe-area-bottom))
|
||||||
|
.model-details-sheet.sheet-modal.sheet-modal-bottom .block
|
||||||
|
margin-top 0px
|
||||||
|
padding-left 0px
|
||||||
|
padding-right 0px
|
||||||
|
|
||||||
.expand-button
|
.expand-button
|
||||||
margin-right 8px
|
margin-right 8px
|
||||||
|
@ -234,6 +233,7 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import ModelDetailsPane from '@/components/model/details-pane.vue'
|
import ModelDetailsPane from '@/components/model/details-pane.vue'
|
||||||
|
import ModelTreeview from '@/components/model/model-treeview.vue'
|
||||||
import AddFromThing from './add-from-thing.vue'
|
import AddFromThing from './add-from-thing.vue'
|
||||||
import AddFromTemplate from './add-from-template.vue'
|
import AddFromTemplate from './add-from-template.vue'
|
||||||
|
|
||||||
|
@ -241,20 +241,20 @@ import ItemStatePreview from '@/components/item/item-state-preview.vue'
|
||||||
import ItemDetails from '@/components/model/item-details.vue'
|
import ItemDetails from '@/components/model/item-details.vue'
|
||||||
import MetadataMenu from '@/components/item/metadata/item-metadata-menu.vue'
|
import MetadataMenu from '@/components/item/metadata/item-metadata-menu.vue'
|
||||||
import LinkDetails from '@/components/model/link-details.vue'
|
import LinkDetails from '@/components/model/link-details.vue'
|
||||||
import ModelTreeviewItem from '@/components/model/treeview-item.vue'
|
|
||||||
|
|
||||||
import ModelMixin from '@/pages/settings/model/model-mixin'
|
import ModelMixin from '@/pages/settings/model/model-mixin'
|
||||||
|
import ItemsAddFromTextualDefinition from '../items/parser/items-add-from-textual-definition.vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
mixins: [ModelMixin],
|
mixins: [ModelMixin],
|
||||||
components: {
|
components: {
|
||||||
'empty-state-placeholder': () => import('@/components/empty-state-placeholder.vue'),
|
'empty-state-placeholder': () => import('@/components/empty-state-placeholder.vue'),
|
||||||
ModelDetailsPane,
|
ModelDetailsPane,
|
||||||
|
ModelTreeview,
|
||||||
ItemStatePreview,
|
ItemStatePreview,
|
||||||
ItemDetails,
|
ItemDetails,
|
||||||
MetadataMenu,
|
MetadataMenu,
|
||||||
LinkDetails,
|
LinkDetails
|
||||||
ModelTreeviewItem
|
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
if (!this.$f7.data.model) this.$f7.data.model = {}
|
if (!this.$f7.data.model) this.$f7.data.model = {}
|
||||||
|
@ -336,7 +336,7 @@ export default {
|
||||||
this.$refs.searchbar.f7Searchbar.$inputEl[0].focus()
|
this.$refs.searchbar.f7Searchbar.$inputEl[0].focus()
|
||||||
}
|
}
|
||||||
this.$refs.searchbar?.f7Searchbar.search(this.$f7.data.lastModelSearchQuery || '')
|
this.$refs.searchbar?.f7Searchbar.search(this.$f7.data.lastModelSearchQuery || '')
|
||||||
this.applyExpandedOption()
|
this.restoreExpanded()
|
||||||
})
|
})
|
||||||
if (!this.eventSource) this.startEventSource()
|
if (!this.eventSource) this.startEventSource()
|
||||||
})
|
})
|
||||||
|
@ -381,6 +381,7 @@ export default {
|
||||||
clearSelection (ev) {
|
clearSelection (ev) {
|
||||||
if (ev.target && ev.currentTarget && ev.target === ev.currentTarget) {
|
if (ev.target && ev.currentTarget && ev.target === ev.currentTarget) {
|
||||||
this.selectedItem = null
|
this.selectedItem = null
|
||||||
|
this.detailsOpened = false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
toggleNonSemantic () {
|
toggleNonSemantic () {
|
||||||
|
|
Loading…
Reference in New Issue