Support for entities with children on the frontend

This commit is contained in:
Fabio Manganiello 2023-01-01 23:06:40 +01:00
parent 772ba6adb0
commit 80c2c77272
Signed by untrusted user: blacklight
GPG key ID: D90FBA7F76362774
5 changed files with 171 additions and 31 deletions

View file

@ -0,0 +1,37 @@
<template>
<div class="entity device-container">
<div class="head">
<div class="col-1 icon">
<EntityIcon
:icon="value.meta?.icon || {}"
:loading="loading"
:error="error" />
</div>
<div class="col-12 label">
<div class="name" v-text="value.name" />
</div>
</div>
</div>
</template>
<script>
import EntityMixin from "./EntityMixin"
import EntityIcon from "./EntityIcon"
export default {
name: 'Device',
components: {EntityIcon},
mixins: [EntityMixin],
}
</script>
<style lang="scss" scoped>
@import "common";
.device-container {
.icon {
margin-right: 1em;
}
}
</style>

View file

@ -1,12 +1,33 @@
<template>
<div class="row item entity-container" :class="{blink: justUpdated}">
<component :is="component"
:value="value"
:loading="loading"
:error="error || value?.reachable == false"
@input="$emit('input', $event)"
@loading="$emit('loading', $event)"
/>
<div class="entity-container-wrapper"
:class="{'with-children': hasChildren, collapsed: isCollapsed}">
<div class="row item entity-container"
:class="{blink: justUpdated, 'with-children': hasChildren, collapsed: isCollapsed}">
<div class="adjuster" :class="{'col-12': !hasChildren, 'col-11': hasChildren}">
<component :is="component"
:value="value"
:loading="loading"
:error="error || value?.reachable == false"
@input="$emit('input', $event)"
@loading="$emit('loading', $event)"
/>
</div>
<div class="col-1 collapse-toggler" @click.stop="collapsed = !collapsed" v-if="hasChildren">
<i class="fas"
:class="{'fa-chevron-down': isCollapsed, 'fa-chevron-up': !isCollapsed}" />
</div>
</div>
<div class="children" v-if="!isCollapsed">
<div class="child" v-for="entity in computedChildren" :key="entity.id">
<Entity
:value="entity"
:loading="loading"
:level="level + 1"
@input="$emit('input', entity)" />
</div>
</div>
</div>
</template>
@ -22,10 +43,28 @@ export default {
data() {
return {
component: null,
collapsed: true,
justUpdated: false,
}
},
computed: {
computedChildren() {
return Object.values(this.children || {}).filter((child) => child)
},
hasChildren() {
return !!this.computedChildren.length
},
isCollapsed() {
if (!this.hasChildren)
return true
return this.collapsed
},
},
methods: {
valuesEqual(a, b) {
a = {...a}
@ -68,10 +107,47 @@ export default {
<style lang="scss" scoped>
@import "common";
.entity-container-wrapper {
&.with-children:not(.collapsed) {
box-shadow: 0 3px 4px 0 $default-shadow-color;
}
}
.entity-container {
width: 100%;
display: flex;
align-items: center;
position: relative;
padding: 0 !important;
border-bottom: $default-border-3;
&.with-children:not(.collapsed) {
background: $selected-bg;
font-weight: bold;
box-shadow: 0 0 3px 2px $default-shadow-color;
}
&:hover {
background: $hover-bg;
}
.collapse-toggler {
display: flex;
justify-content: center;
align-items: center;
flex: 1;
min-height: 3em;
margin-left: 0;
cursor: pointer;
&:hover {
color: $default-hover-fg;
}
}
.adjuster {
cursor: pointer;
}
}
.blink {

View file

@ -20,6 +20,16 @@ export default {
type: Object,
required: true,
},
children: {
type: Object,
default: () => {},
},
level: {
type: Number,
default: 0,
},
},
data() {

View file

@ -49,10 +49,12 @@
v-for="entity in group.entities" :key="entity.id">
<Entity
:value="entity"
@input="onEntityInput"
:children="childrenByParentId(entity.id)"
@input="onEntityInput(entity)"
:error="!!errorEntities[entity.id]"
:loading="!!loadingEntities[entity.id]"
@loading="loadingEntities[entity.id] = $event"
v-if="!entity.parent_id"
/>
</div>
</div>
@ -135,35 +137,41 @@ export default {
},
displayGroups() {
return Object.entries(this.entityGroups[this.selector.grouping]).filter(
(entry) => entry[1].filter(
(e) => !!this.selector.selectedEntities[e.id]
).length > 0
).sort((a, b) => a[0].localeCompare(b[0])).map(
([grouping, entities]) => {
return {
name: grouping,
entities: entities.filter(
(e) => e.id in this.selector.selectedEntities
),
return Object.entries(this.entityGroups[this.selector.grouping]).
filter(
(entry) => entry[1].filter(
(e) =>
!!this.selector.selectedEntities[e.id] && e.parent_id == null
).length > 0
).
map(
([grouping, entities]) => {
return {
name: grouping,
entities: entities.filter(
(e) => e.id in this.selector.selectedEntities
),
}
}
}
)
).
sort((a, b) => a.name.localeCompare(b.name))
},
},
methods: {
groupEntities(attr) {
return Object.values(this.entities).reduce((obj, entity) => {
const entities = obj[entity[attr]] || {}
entities[entity.id] = entity
return Object.values(this.entities).
filter((entity) => entity.parent_id == null).
reduce((obj, entity) => {
const entities = obj[entity[attr]] || {}
entities[entity.id] = entity
obj[entity[attr]] = Object.values(entities).sort((a, b) => {
return a.name.localeCompare(b.name)
})
obj[entity[attr]] = Object.values(entities).sort((a, b) => {
return a.name.localeCompare(b.name)
})
return obj
}, {})
return obj
}, {})
},
async refresh(group) {
@ -223,6 +231,15 @@ export default {
}
},
childrenByParentId(parentId) {
return Object.values(this.entities).
filter((entity) => entity.parent_id === parentId).
reduce((obj, entity) => {
obj[entity.id] = entity
return obj
}, {})
},
clearEntityTimeouts(entityId) {
if (this.errorEntities[entityId])
delete this.errorEntities[entityId]

View file

@ -1,5 +1,5 @@
<template>
<div class="entity sensor-container" :class="{hidden: value.value == null}">
<div class="entity sensor-container">
<div class="head">
<div class="col-1 icon">
<EntityIcon