Empty state placeholders (#188)

Add information on how to start when there's nothing
to display on a page - a common UX pattern which helps
users figure out what they need to do.
https://uxdesign.cc/writing-empty-states-3e0279f39066
https://material.io/design/communication/empty-states.html

Fix search in rules screens.
Adjust positions of lists across screens.
Detect when the rules engine is not installed and display
a message accordingly.

Signed-off-by: Yannick Schaus <github@schaus.net>
pull/190/head
Yannick Schaus 2020-01-31 12:42:11 +01:00 committed by GitHub
parent 2e2356922a
commit ba7bc37051
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 191 additions and 108 deletions

View File

@ -0,0 +1,28 @@
{
"inbox.title": "The Inbox is empty",
"inbox.text": "Discovery results from your bindings that can be added as things will appear here.<br><br>You can also start a scan for a certain binding or add things manually with the button below.",
"things.nobindings.title": "No bindings installed",
"things.nobindings.text": "You need to install binding add-ons to add things they support to your system. Click the button below to install some.",
"things.title": "No things yet",
"things.text": "Things are the devices and services connected to openHAB, they are provided by binding add-ons.<br><br>Check the Inbox to add auto-discovered things. You can also start a scan for a certain binding or add your first thing manually with the button below.",
"items.title": "No items yet",
"items.text": "Items represent the functional side of your home - you can link them to the channels defined by your things. Start with the Model view to create a clean initial structure.<br><br>You can also define items with configuration files, or with the button below.",
"model.title": "Start modelling your home",
"model.text": "Build a model from your items to organize them and relate them to each other semantically.<br><br>Begin with a hierarchy of locations: buildings, outside areas, floors and rooms, as needed. Then, insert equipments and points from your things (or manually).",
"rules.title": "No rules yet",
"rules.text": "Rules are the basic building blocks to automate your home - they define which actions to perform when certain events occur.<br><br>Create your first rule with the button below; for more advanced scenarios, you can also write script files in your configuration folder.",
"schedule.title": "Nothing in the schedule",
"schedule.text": "The schedule displays up to 30 days of times when rules specifically tagged \"Schedule\" are expected to run.<br><br>Click the button below to create your first scheduled rule.",
"rules.missingengine.title": "Rule engine not installed",
"rules.missingengine.text": "The rule engine must be installed before rules can be created.",
"addons.title": "No add-ons installed",
"addons.text": "Add-ons add functionality to your openHAB system.<br><br>Install them with the button below."
}

View File

@ -0,0 +1,31 @@
<template>
<f7-block class="empty-state-placeholder text-color-gray">
<f7-row>
<f7-col>
<f7-icon :f7="icon" size="64" color="gray" />
<h1>{{texts[title] || title}}</h1>
<p v-html="texts[text] || text"></p>
</f7-col>
</f7-row>
</f7-block>
</template>
<style lang="stylus">
.empty-state-placeholder
margin-top 10vh !important
text-align center
</style>
<script>
// TODO: i18n
import texts from '@/assets/i18n/en/empty-states.json'
export default {
props: ['icon', 'title', 'text'],
data () {
return {
texts
}
}
}
</script>

View File

@ -4,6 +4,7 @@ import Vue from 'vue'
import SitemapWidgetGeneric from '../components/sitemap/widget-generic.vue'
import OHIconComponent from '../components/oh-icon.vue'
import TreeviewItem from '../components/model/treeview-item.vue'
import EmptyStatePlaceholder from '../components/empty-state-placeholder.vue'
// Import Framework7
import Framework7 from 'framework7/framework7-lite.esm.bundle.js'
@ -47,3 +48,4 @@ const app = new Vue({
Vue.component('sitemap-widget-generic', SitemapWidgetGeneric)
Vue.component('oh-icon', OHIconComponent)
Vue.component('model-treeview-item', TreeviewItem)
Vue.component('empty-state-placeholder', EmptyStatePlaceholder)

View File

@ -105,7 +105,7 @@ export default {
break
case 'failed':
this.$f7.toast.create({
text: `Installation of addon ${topicParts[2]} failed`,
text: `Installation of add-on ${topicParts[2]} failed`,
closeButton: true,
destroyOnClose: true
}).open()

View File

@ -1,6 +1,6 @@
<template>
<f7-page @page:afterin="onPageAfterIn" @page:beforeout="addonPopupOpened = false" @page:afterout="stopEventSource">
<f7-navbar :title="'Add-ons: ' + addonType" back-link="Settings" back-link-url="/settings/" back-link-force>
<f7-navbar :title="'Add-ons: ' + addonsLabels[addonType]" back-link="Settings" back-link-url="/settings/" back-link-force>
<!-- <f7-nav-right>
<f7-link href="add">Add</f7-link>
</f7-nav-right>-->
@ -44,11 +44,7 @@
</f7-col>
</f7-block>
<f7-block form v-if="ready && !addons.length" class="service-config block-narrow">
<f7-col>
<f7-block strong>
<p>No add-ons of type {{addonType}} installed yet. Click the + button to add one!</p>
</f7-block>
</f7-col>
<empty-state-placeholder :icon="addonsIcons[addonType]" :title="'No ' + addonsLabels[addonType] + ' installed yet'" text="addons.text" />
</f7-block>
<f7-fab position="right-bottom" slot="fixed" color="blue" href="add">
<f7-icon ios="f7:plus" md="material:add" aurora="f7:plus"></f7-icon>
@ -84,7 +80,25 @@ export default {
ready: false,
currentAddonId: null,
addonPopupOpened: false,
currentlyUninstalling: []
currentlyUninstalling: [],
addonsLabels: {
binding: 'bindings',
action: 'actions',
persistence: 'persistence services',
transformation: 'transformations',
misc: 'miscellaneous add-ons',
ui: 'user interfaces',
voice: 'voice services'
},
addonsIcons: {
binding: 'circle_grid_hex',
action: 'bolt_horizontal',
persistence: 'download_circle',
transformation: 'function',
misc: 'rectangle_3_offgrid',
ui: 'play_rectangle',
voice: 'chat_bubble_2'
}
}
},
methods: {
@ -123,7 +137,7 @@ export default {
break
case 'failed':
this.$f7.toast.create({
text: `Installation of addon ${topicParts[2]} failed`,
text: `Uninstallation of add-on ${topicParts[2]} failed`,
closeButton: true,
destroyOnClose: true
}).open()

View File

@ -47,7 +47,7 @@
<label @click="toggleIgnored" style="cursor:pointer">Show ignored</label> <f7-checkbox :checked="showIgnored" @change="toggleIgnored"></f7-checkbox>
</div>
</f7-block-title>
<div class="padding-left padding-right">
<div class="padding-left padding-right" v-show="!ready || inboxCount > 0">
<f7-segmented strong tag="p">
<f7-button :active="groupBy === 'alphabetical'" @click="groupBy = 'alphabetical'; $nextTick(() => $refs.listIndex.update())">Alphabetical</f7-button>
<f7-button :active="groupBy === 'binding'" @click="groupBy = 'binding'">By binding</f7-button>
@ -95,12 +95,8 @@
</f7-col>
</f7-block>
<f7-block v-if="ready && !inbox.length" class="block-narrow">
<f7-col>
<f7-block strong>
<p>Inbox is empty.</p>
</f7-block>
</f7-col>
<f7-block v-if="ready && inboxCount === 0" class="block-narrow">
<empty-state-placeholder icon="tray" title="inbox.title" text="inbox.text" />
</f7-block>
<f7-fab v-show="!showCheckboxes" position="right-bottom" slot="fixed" color="blue" href="/settings/things/add">
<f7-icon ios="f7:plus" md="material:add" aurora="f7:plus"></f7-icon>
@ -209,7 +205,7 @@ export default {
bold: true,
onClick: () => {
console.log(`Add ${entry.thingUID} as thing`)
this.$f7.dialog.prompt(`This will create a new Thing ${entry.thingUID} with the following name:`,
this.$f7.dialog.prompt(`This will create a new Thing of type ${entry.thingTypeUID} with the following name:`,
'Add as Thing',
(name) => {
this.$oh.api.postPlain(`/rest/inbox/${entry.thingUID}/approve`, name).then((res) => {
@ -259,7 +255,7 @@ export default {
text: 'Remove',
color: 'red',
onClick: () => {
this.$f7.dialog.confirm(`Remove ${entry.label} from Inbox?`, 'Remove Entry', () => {
this.$f7.dialog.confirm(`Remove ${entry.label} from the Inbox?`, 'Remove Entry', () => {
this.$oh.api.delete('/rest/inbox/' + entry.thingUID).then((res) => {
this.$f7.toast.create({
text: 'Entry removed',

View File

@ -32,30 +32,31 @@
<f7-list-item title="Nothing found"></f7-list-item>
</f7-list>
<!-- skeleton for not ready -->
<f7-block class="block-narrow" v-if="!ready">
<f7-block-title class="col wide padding-left">Loading...</f7-block-title>
<f7-list media-list class="col wide">
<f7-list-group>
<f7-list-item
media-item
v-for="n in 20"
:key="n"
:class="`skeleton-text skeleton-effect-blink`"
title="Label of the item"
subtitle="type, semantic metadata"
after="The item state"
footer="This contains the type of the item"
>
<f7-skeleton-block style="width: 32px; height: 32px; border-radius: 50%" slot="media"></f7-skeleton-block>
</f7-list-item>
</f7-list-group>
</f7-list>
</f7-block>
<f7-block class="block-narrow" v-else>
<f7-block-title class="col wide padding-left searchbar-hide-on-search">{{items.length}} items</f7-block-title>
<f7-col>
<f7-block class="block-narrow">
<f7-col v-show="!ready">
<f7-block-title>&nbsp;Loading...</f7-block-title>
<f7-list media-list class="col wide">
<f7-list-group>
<f7-list-item
media-item
v-for="n in 20"
:key="n"
:class="`skeleton-text skeleton-effect-blink`"
title="Label of the item"
subtitle="type, semantic metadata"
after="The item state"
footer="This contains the type of the item"
>
<f7-skeleton-block style="width: 32px; height: 32px; border-radius: 50%" slot="media"></f7-skeleton-block>
</f7-list-item>
</f7-list-group>
</f7-list>
</f7-col>
<f7-col v-if="ready">
<f7-block-title class="searchbar-hide-on-search">{{items.length}} items</f7-block-title>
<f7-list
class="searchbar-found col wide"
v-show="items.length > 0"
class="searchbar-found col"
ref="itemsList"
media-list
virtual-list
@ -86,13 +87,9 @@
</f7-list>
</f7-col>
</f7-block>
<!-- <f7-block v-if="!items.length" class="service-config block-narrow">
<f7-col>
<f7-block strong>
<p>No items.</p>
</f7-block>
</f7-col>
</f7-block>-->
<f7-block v-if="ready && !items.length" class="service-config block-narrow">
<empty-state-placeholder icon="square_on_circle" title="items.title" text="items.text" />
</f7-block>
<f7-fab v-show="!showCheckboxes" position="right-bottom" slot="fixed" color="blue">
<f7-icon ios="f7:plus" md="material:add" aurora="f7:plus"></f7-icon>
<f7-icon ios="f7:multiply" md="material:close" aurora="f7:multiply"></f7-icon>

View File

@ -24,7 +24,6 @@
</div>
<f7-link :disabled="selectedItem != null" class="right" @click="selectedItem = null">Clear</f7-link>
</f7-toolbar>
<f7-block v-if="!ready" class="text-align-center">
<f7-preloader></f7-preloader>
<div>Loading...</div>
@ -32,7 +31,9 @@
<f7-block v-else class="semantic-tree-wrapper" :class="{ 'sheet-opened' : detailsOpened }">
<f7-row>
<f7-col width="100" medium="50">
<f7-block 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" />
<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" /> -->
<f7-treeview>
<model-treeview-item v-for="node in [rootLocations, rootEquipments, rootPoints, rootGroups, rootItems].flat()"
:key="node.item.name" :model="node"
@ -103,10 +104,9 @@
.semantic-tree-wrapper
padding 0
margin-bottom 0
.block
padding 0
border-right 1px solid var(--f7-block-strong-border-color)
.semantic-tree
padding 0
border-right 1px solid var(--f7-block-strong-border-color)
.treeview
--f7-treeview-item-height 40px
.treeview-item-label
@ -191,6 +191,12 @@ export default {
},
created () {
},
computed: {
empty () {
let emptySemantic = !this.rootLocations.length && !this.rootEquipments.length && !this.rootPoints.length
return (this.includeNonSemantic) ? emptySemantic && !this.rootGroups.length && !this.rootItems.length : emptySemantic
}
},
methods: {
onPageAfterIn () {

View File

@ -12,14 +12,14 @@
:init="initSearchbar"
search-container=".rules-list"
search-item=".rulelist-item"
search-in=".item-title, .item-header, .item-footer"
search-in=".item-title, .item-subtitle, .item-header, .item-footer"
remove-diacritics
:disable-button="!$theme.aurora"
></f7-searchbar>
</f7-subnavbar>
</f7-navbar>
<f7-toolbar class="contextual-toolbar" :class="{ 'navbar': $theme.md }" v-if="showCheckboxes" bottom-ios bottom-aurora>
<f7-link v-show="selectedItems.length" v-if="!$theme.md" class="delete" icon-ios="f7:trash" icon-aurora="f7:trash" @click="removeSelected">Remove {{selectedItems.length}}</f7-link>
<f7-link color="red" v-show="selectedItems.length" v-if="!$theme.md" class="delete" icon-ios="f7:trash" icon-aurora="f7:trash" @click="removeSelected">Remove {{selectedItems.length}}</f7-link>
<f7-link v-if="$theme.md" icon-md="material:close" icon-color="white" @click="showCheckboxes = false"></f7-link>
<div class="title" v-if="$theme.md">
{{selectedItems.length}} selected
@ -33,30 +33,34 @@
<f7-list class="searchbar-not-found">
<f7-list-item title="Nothing found"></f7-list-item>
</f7-list>
<empty-state-placeholder v-if="noRuleEngine" icon="exclamationmark_triangle" title="rules.missingengine.title" text="rules.missingengine.text" />
<!-- skeleton for not ready -->
<f7-block class="block-narrow" v-if="!ready">
<f7-block-title class="col wide padding-left">Loading...</f7-block-title>
<f7-list media-list class="col wide">
<f7-list-group>
<f7-list-item
media-item
v-for="n in 20"
:key="n"
:class="`skeleton-text skeleton-effect-blink`"
title="Title of the rule"
subtitle="Tags, Schedule, Scene..."
after="status badge"
footer="Description of the rule"
>
</f7-list-item>
</f7-list-group>
</f7-list>
</f7-block>
<f7-block class="block-narrow" v-else>
<f7-block-title class="col wide padding-left searchbar-hide-on-search">{{rules.length}} rules</f7-block-title>
<f7-col>
<f7-block class="block-narrow" v-show="!noRuleEngine">
<f7-col v-show="!ready">
<f7-block-title>&nbsp;Loading...</f7-block-title>
<f7-list media-list class="col wide">
<f7-list-group>
<f7-list-item
media-item
v-for="n in 20"
:key="n"
:class="`skeleton-text skeleton-effect-blink`"
title="Title of the rule"
subtitle="Tags, Schedule, Scene..."
after="status badge"
footer="Description of the rule"
>
</f7-list-item>
</f7-list-group>
</f7-list>
</f7-col>
<f7-col v-if="ready">
<f7-block-title v-show="rules.length" class="searchbar-hide-on-search">{{rules.length}} rules</f7-block-title>
<f7-list
class="searchbar-found col wide rules-list"
v-show="rules.length > 0"
class="searchbar-found col rules-list"
ref="rulesList"
media-list>
<f7-list-item
@ -83,7 +87,10 @@
</f7-list>
</f7-col>
</f7-block>
<f7-fab v-show="!showCheckboxes" position="right-bottom" slot="fixed" color="blue" href="add">
<f7-block v-if="ready && !noRuleEngine && !rules.length" class="service-config block-narrow">
<empty-state-placeholder icon="wand_rays" title="rules.title" text="rules.text" />
</f7-block>
<f7-fab v-show="ready && !showCheckboxes" position="right-bottom" slot="fixed" color="blue" href="add">
<f7-icon ios="f7:plus" md="material:add" aurora="f7:plus"></f7-icon>
<f7-icon ios="f7:close" md="material:close" aurora="f7:close"></f7-icon>
</f7-fab>
@ -96,6 +103,7 @@ export default {
return {
ready: false,
loading: false,
noRuleEngine: false,
rules: [],
initSearchbar: false,
selectedItems: [],
@ -117,11 +125,15 @@ export default {
this.rules = data.sort((a, b) => {
return a.name.localeCompare(b.name)
})
this.initSearchbar = true
this.loading = false
this.ready = true
setTimeout(() => { this.initSearchbar = true })
if (!this.eventSource) this.startEventSource()
}).catch((err, status) => {
if (err === 'Not Found' || status === 404) {
this.noRuleEngine = true
}
})
},
startEventSource () {

View File

@ -30,7 +30,9 @@
</div>
</f7-toolbar>
<div class="timeline timeline-horizontal col-33 tablet-15">
<empty-state-placeholder v-if="noRuleEngine" icon="exclamationmark_triangle" title="rules.missingengine.title" text="rules.missingengine.text" />
<empty-state-placeholder v-else-if="ready && !rules.length" icon="calendar" title="schedule.title" text="schedule.text" />
<div v-else class="timeline timeline-horizontal col-33 tablet-15">
<div class="timeline-year" v-for="(yearObj, year) in calendar" :key="year">
<div class="timeline-year-title"><span>{{year}}</span></div>
<div class="timeline-month" v-for="(monthObj, month) in yearObj" :key="month">
@ -49,7 +51,7 @@
</div>
</div>
</div>
<f7-fab position="right-bottom" slot="fixed" color="blue" href="add">
<f7-fab v-if="ready" position="right-bottom" slot="fixed" color="blue" href="add">
<f7-icon ios="f7:plus" md="material:add" aurora="f7:plus"></f7-icon>
<f7-icon ios="f7:close" md="material:close" aurora="f7:close"></f7-icon>
</f7-fab>
@ -75,6 +77,7 @@ export default {
ready: false,
loading: false,
rules: [],
noRuleEngine: false,
calendar: {},
initSearchbar: false,
selectedItems: [],
@ -163,6 +166,10 @@ export default {
this.ready = true
if (!this.eventSource) this.startEventSource()
}).catch((err, status) => {
if (err === 'Not Found' || status === 404) {
this.noRuleEngine = true
}
})
},
startEventSource () {

View File

@ -20,43 +20,41 @@
:scroll-list="true"
:label="true"
></f7-list-index>
<empty-state-placeholder v-if="ready && !bindings.length" icon="circle_grid_hex" title="things.nobindings.title" text="things.nobindings.text" />
<f7-block class="block-narrow">
<f7-col>
<f7-list v-if="!ready" class="col binding-list">
<f7-list-group>
<f7-list-item
v-for="n in 10"
media-item
:key="n"
:class="`skeleton-text skeleton-effect-blink`"
title="Label of the binding"
header="BindingID"
footer="This contains the description of the binding"
media-item
>
footer="This contains the description of the binding">
</f7-list-item>
</f7-list-group>
</f7-list>
<f7-list v-else class="col">
<f7-list-item v-for="binding in bindings"
<f7-list-item
v-for="binding in bindings"
media-item
:key="binding.id"
:link="binding.id"
:title="binding.name"
:header="binding.id"
:footer="binding.description"
media-item
>
:footer="(binding.description && binding.description.indexOf('<br>') >= 0) ?
binding.description.split('<br>')[0] : binding.description">
</f7-list-item>
</f7-list>
</f7-col>
<f7-col v-if="ready && !bindings.length">
<f7-block strong>
<p>No bindings available.</p>
</f7-block>
</f7-col>
<f7-col>
<f7-list>
<f7-list-button color="blue" title="Install New Bindings" href="/settings/addons/binding/add" />
<f7-list-button color="blue" title="Install Bindings" href="/settings/addons/binding/add" />
</f7-list>
</f7-col>
</f7-block>
@ -72,13 +70,9 @@ export default {
initSearchbar: false,
bindings: []
}
},
created () {
},
methods: {
onPageAfterIn () {
// this.$f7.preloader.show()
this.loading = true
this.$oh.api.get('/rest/bindings').then((data) => {
this.bindings = data.sort((a, b) => a.name.localeCompare(b.name))

View File

@ -17,7 +17,7 @@
<f7-col>
<div v-if="discoverySupported" class="display-flex justify-content-center">
<div class="flex-shrink-0">
<f7-button class="padding-left padding-right" style="width: 150px" color="blue" large raised fill :disabled="scanning" @click="scan">{{(scanning) ? 'Scanning...' : 'Rescan'}}</f7-button>
<f7-button class="padding-left padding-right" style="width: 150px" color="blue" large raised fill :disabled="scanning" @click="scan">{{(scanning) ? 'Scanning...' : 'Scan Again'}}</f7-button>
</div>
</div>
<p class="margin-left margin-right" style="height: 30px" id="scan-progress"></p>
@ -157,7 +157,7 @@ export default {
},
approve (entry) {
console.log(`Add ${entry.thingUID} as thing`)
this.$f7.dialog.prompt(`This will create a new Thing ${entry.thingUID} with the following name:`,
this.$f7.dialog.prompt(`This will create a new Thing of type ${entry.thingTypeUID} with the following name:`,
'Add as Thing',
(name) => {
this.$oh.api.postPlain(`/rest/inbox/${entry.thingUID}/approve`, name).then((res) => {

View File

@ -28,7 +28,7 @@
<f7-block class="block-narrow">
<f7-col>
<f7-block-title class="searchbar-hide-on-search"><span v-if="ready">{{things.length}} things</span></f7-block-title>
<div class="padding-left padding-right">
<div class="padding-left padding-right" v-show="!ready || things.length > 0">
<f7-segmented strong tag="p">
<f7-button :active="groupBy === 'alphabetical'" @click="groupBy = 'alphabetical'; $nextTick(() => $refs.listIndex.update())">Alphabetical</f7-button>
<f7-button :active="groupBy === 'binding'" @click="groupBy = 'binding'">By binding</f7-button>
@ -67,13 +67,9 @@
</f7-col>
</f7-block>
<!-- <f7-block v-if="!things.length" class="block-narrow">
<f7-col>
<f7-block strong>
<p>No things.</p>
</f7-block>
</f7-col>
</f7-block>-->
<f7-block v-if="ready && !things.length" class="block-narrow">
<empty-state-placeholder icon="lightbulb" title="things.title" text="things.text" />
</f7-block>
<f7-fab position="right-bottom" slot="fixed" color="blue" href="add">
<f7-icon ios="f7:plus" md="material:add" aurora="f7:plus"></f7-icon>
<f7-icon ios="f7:close" md="material:close" aurora="f7:close"></f7-icon>