mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-17 22:36:35 +00:00
Notification drawer (#1536)
* Notification drawer MVP concept * Localization * Don't use events.
This commit is contained in:
parent
b9f84d012f
commit
02edbce460
12
src/panels/lovelace/common/compute-notifications.js
Normal file
12
src/panels/lovelace/common/compute-notifications.js
Normal file
@ -0,0 +1,12 @@
|
||||
import computeDomain from '../../../common/entity/compute_domain.js';
|
||||
|
||||
const NOTIFICATION_DOMAINS = [
|
||||
'configurator',
|
||||
'persistent_notification'
|
||||
];
|
||||
|
||||
export default function computeNotifications(states) {
|
||||
return Object.keys(states)
|
||||
.filter(entityId => NOTIFICATION_DOMAINS.includes(computeDomain(entityId)))
|
||||
.map(entityId => states[entityId]);
|
||||
}
|
@ -0,0 +1,53 @@
|
||||
import '@polymer/paper-button/paper-button.js';
|
||||
import '@polymer/paper-icon-button/paper-icon-button.js';
|
||||
|
||||
import { html } from '@polymer/polymer/lib/utils/html-tag.js';
|
||||
import { PolymerElement } from '@polymer/polymer/polymer-element.js';
|
||||
|
||||
import './hui-notification-item-template.js';
|
||||
|
||||
import EventsMixin from '../../../../mixins/events-mixin.js';
|
||||
import LocalizeMixin from '../../../../mixins/localize-mixin.js';
|
||||
|
||||
/*
|
||||
* @appliesMixin EventsMixin
|
||||
* @appliesMixin LocalizeMixin
|
||||
*/
|
||||
export class HuiConfiguratorNotificationItem extends EventsMixin(LocalizeMixin(PolymerElement)) {
|
||||
static get template() {
|
||||
return html`
|
||||
<hui-notification-item-template>
|
||||
<span slot="header">[[localize('domain.configurator')]]</span>
|
||||
|
||||
<div>[[_getMessage(stateObj)]]</div>
|
||||
|
||||
<paper-button
|
||||
slot="actions"
|
||||
class="primary"
|
||||
on-click="_handleClick"
|
||||
>[[_localizeState(stateObj.state)]]</paper-button>
|
||||
</hui-notification-item-template>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
stateObj: Object
|
||||
};
|
||||
}
|
||||
|
||||
_handleClick() {
|
||||
this.fire('hass-more-info', { entityId: this.stateObj.entity_id });
|
||||
}
|
||||
|
||||
_localizeState(state) {
|
||||
return this.localize(`state.configurator.${state}`);
|
||||
}
|
||||
|
||||
_getMessage(stateObj) {
|
||||
const friendlyName = stateObj.attributes.friendly_name;
|
||||
return this.localize('ui.notification_drawer.click_to_configure', 'entity', friendlyName);
|
||||
}
|
||||
}
|
||||
customElements.define('hui-configurator-notification-item', HuiConfiguratorNotificationItem);
|
@ -0,0 +1,179 @@
|
||||
import '@polymer/paper-button/paper-button.js';
|
||||
import '@polymer/paper-icon-button/paper-icon-button.js';
|
||||
import '@polymer/app-layout/app-toolbar/app-toolbar.js';
|
||||
|
||||
import { html } from '@polymer/polymer/lib/utils/html-tag.js';
|
||||
import { PolymerElement } from '@polymer/polymer/polymer-element.js';
|
||||
|
||||
import './hui-notification-item.js';
|
||||
|
||||
import computeNotifications from '../../common/compute-notifications.js';
|
||||
|
||||
import EventsMixin from '../../../../mixins/events-mixin.js';
|
||||
import LocalizeMixin from '../../../../mixins/localize-mixin.js';
|
||||
|
||||
/*
|
||||
* @appliesMixin EventsMixin
|
||||
* @appliesMixin LocalizeMixin
|
||||
*/
|
||||
export class HuiNotificationDrawer extends EventsMixin(LocalizeMixin(PolymerElement)) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="paper-material-styles">
|
||||
:host {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
:host([hidden]) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.container {
|
||||
align-items: stretch;
|
||||
background: var(--sidebar-background-color, var(--primary-background-color));
|
||||
bottom: 0;
|
||||
box-shadow: var(--paper-material-elevation-1_-_box-shadow);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-y: hidden;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
transition: right .2s ease-in;
|
||||
width: 500px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
:host(:not(narrow)) .container {
|
||||
right: -500px;
|
||||
}
|
||||
|
||||
:host([narrow]) .container {
|
||||
right: -100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:host(.open) .container,
|
||||
:host(.open[narrow]) .container {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
app-toolbar {
|
||||
color: var(--primary-text-color);
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
background-color: var(--primary-background-color);
|
||||
min-height: 64px;
|
||||
width: calc(100% - 32px);
|
||||
z-index: 11;
|
||||
}
|
||||
|
||||
.overlay {
|
||||
display: none;
|
||||
}
|
||||
|
||||
:host(.open) .overlay {
|
||||
bottom: 0;
|
||||
display: block;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.notifications {
|
||||
overflow-y: auto;
|
||||
padding-top: 16px;
|
||||
}
|
||||
|
||||
.notification {
|
||||
padding: 0 16px 16px;
|
||||
}
|
||||
|
||||
.empty {
|
||||
padding: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
<div class="overlay" on-click="_closeDrawer"></div>
|
||||
<div class="container">
|
||||
<app-toolbar>
|
||||
<div main-title>[[localize('ui.notification_drawer.title')]]</div>
|
||||
<paper-icon-button icon="hass:chevron-right" on-click="_closeDrawer"></paper-icon-button>
|
||||
</app-toolbar>
|
||||
<div class="notifications">
|
||||
<template is="dom-if" if="[[!_empty(_entities)]]">
|
||||
<dom-repeat items="[[_entities]]">
|
||||
<template>
|
||||
<div class="notification">
|
||||
<hui-notification-item hass="[[hass]]" state-obj="[[item]]"></hui-notification-item>
|
||||
</div>
|
||||
</template>
|
||||
</dom-repeat>
|
||||
</template>
|
||||
<template is="dom-if" if="[[_empty(_entities)]]">
|
||||
<div class="empty">[[localize('ui.notification_drawer.empty')]]<div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
_entities: {
|
||||
type: Array,
|
||||
computed: '_getEntities(hass.states, hidden)'
|
||||
},
|
||||
narrow: {
|
||||
type: Boolean,
|
||||
reflectToAttribute: true
|
||||
},
|
||||
open: {
|
||||
type: Boolean,
|
||||
notify: true,
|
||||
observer: '_openChanged'
|
||||
},
|
||||
hidden: {
|
||||
type: Boolean,
|
||||
value: true,
|
||||
reflectToAttribute: true
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
_getEntities(states, hidden) {
|
||||
return (states && !hidden) ? computeNotifications(states) : [];
|
||||
}
|
||||
|
||||
_closeDrawer(ev) {
|
||||
ev.stopPropagation();
|
||||
this.open = false;
|
||||
}
|
||||
|
||||
_empty(entities) {
|
||||
return entities.length === 0;
|
||||
}
|
||||
|
||||
_openChanged(open) {
|
||||
clearTimeout(this._openTimer);
|
||||
if (open) {
|
||||
// Render closed then animate open
|
||||
this.hidden = false;
|
||||
this._openTimer = setTimeout(() => {
|
||||
this.classList.add('open');
|
||||
}, 50);
|
||||
} else {
|
||||
// Animate closed then hide
|
||||
this.classList.remove('open');
|
||||
this._openTimer = setTimeout(() => {
|
||||
this.hidden = true;
|
||||
}, 250);
|
||||
}
|
||||
}
|
||||
}
|
||||
customElements.define('hui-notification-drawer', HuiNotificationDrawer);
|
@ -0,0 +1,46 @@
|
||||
import '@polymer/paper-button/paper-button.js';
|
||||
import '@polymer/paper-icon-button/paper-icon-button.js';
|
||||
|
||||
import { html } from '@polymer/polymer/lib/utils/html-tag.js';
|
||||
import { PolymerElement } from '@polymer/polymer/polymer-element.js';
|
||||
|
||||
import '../../../../components/ha-card.js';
|
||||
|
||||
export class HuiNotificationItemTemplate extends PolymerElement {
|
||||
static get template() {
|
||||
return html`
|
||||
<style>
|
||||
.contents {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
ha-card .header {
|
||||
@apply --paper-font-headline;
|
||||
color: var(--primary-text-color);
|
||||
padding: 16px 16px 0;
|
||||
}
|
||||
|
||||
.actions {
|
||||
border-top: 1px solid #e8e8e8;
|
||||
padding: 5px 16px;
|
||||
}
|
||||
|
||||
::slotted(.primary) {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
</style>
|
||||
<ha-card>
|
||||
<div class="header">
|
||||
<slot name="header"></slot>
|
||||
</div>
|
||||
<div class="contents">
|
||||
<slot></slot>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<slot name="actions"></slot>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
}
|
||||
customElements.define('hui-notification-item-template', HuiNotificationItemTemplate);
|
@ -0,0 +1,33 @@
|
||||
import { PolymerElement } from '@polymer/polymer/polymer-element.js';
|
||||
import computeDomain from '../../../../common/entity/compute_domain.js';
|
||||
|
||||
import './hui-configurator-notification-item.js';
|
||||
import './hui-persistent-notification-item.js';
|
||||
|
||||
export class HuiNotificationItem extends PolymerElement {
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
stateObj: {
|
||||
type: Object,
|
||||
observer: '_stateChanged'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
_stateChanged(stateObj) {
|
||||
if (this.lastChild) {
|
||||
this.removeChild(this.lastChild);
|
||||
}
|
||||
|
||||
if (!stateObj) return;
|
||||
|
||||
const domain = computeDomain(stateObj.entity_id);
|
||||
const tag = `hui-${domain}-notification-item`;
|
||||
const el = document.createElement(tag);
|
||||
el.hass = this.hass;
|
||||
el.stateObj = stateObj;
|
||||
this.appendChild(el);
|
||||
}
|
||||
}
|
||||
customElements.define('hui-notification-item', HuiNotificationItem);
|
@ -0,0 +1,61 @@
|
||||
import '@polymer/paper-button/paper-button.js';
|
||||
import '@polymer/paper-icon-button/paper-icon-button.js';
|
||||
import '@polymer/app-layout/app-toolbar/app-toolbar.js';
|
||||
|
||||
import { html } from '@polymer/polymer/lib/utils/html-tag.js';
|
||||
import { PolymerElement } from '@polymer/polymer/polymer-element.js';
|
||||
|
||||
import computeNotifications from '../../common/compute-notifications.js';
|
||||
|
||||
import EventsMixin from '../../../../mixins/events-mixin.js';
|
||||
|
||||
/*
|
||||
* @appliesMixin EventsMixin
|
||||
*/
|
||||
export class HuiNotificationsButton extends EventsMixin(PolymerElement) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style>
|
||||
:host {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.indicator {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
background: var(--accent-color);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.indicator[hidden] {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
<paper-icon-button icon="hass:bell" on-click="_clicked"></paper-icon-button>
|
||||
<span class="indicator" hidden$="[[!_hasNotifications(hass.states)]]"></span>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
notificationsOpen: {
|
||||
type: Boolean,
|
||||
notify: true
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
_clicked() {
|
||||
this.notificationsOpen = true;
|
||||
}
|
||||
|
||||
_hasNotifications(states) {
|
||||
return computeNotifications(states).length > 0;
|
||||
}
|
||||
}
|
||||
customElements.define('hui-notifications-button', HuiNotificationsButton);
|
@ -0,0 +1,52 @@
|
||||
import '@polymer/paper-button/paper-button.js';
|
||||
import '@polymer/paper-icon-button/paper-icon-button.js';
|
||||
|
||||
import { html } from '@polymer/polymer/lib/utils/html-tag.js';
|
||||
import { PolymerElement } from '@polymer/polymer/polymer-element.js';
|
||||
|
||||
import computeStateName from '../../../../common/entity/compute_state_name.js';
|
||||
|
||||
import '../../../../components/ha-markdown.js';
|
||||
import './hui-notification-item-template.js';
|
||||
|
||||
import LocalizeMixin from '../../../../mixins/localize-mixin.js';
|
||||
|
||||
/*
|
||||
* @appliesMixin LocalizeMixin
|
||||
*/
|
||||
export class HuiPersistentNotificationItem extends LocalizeMixin(PolymerElement) {
|
||||
static get template() {
|
||||
return html`
|
||||
<hui-notification-item-template>
|
||||
<span slot="header">[[_computeTitle(stateObj)]]</span>
|
||||
|
||||
<ha-markdown content="[[stateObj.attributes.message]]"></ha-markdown>
|
||||
|
||||
<paper-button
|
||||
slot="actions"
|
||||
class="primary"
|
||||
on-click="_handleDismiss"
|
||||
>[[localize('ui.card.persistent_notification.dismiss')]]</paper-button>
|
||||
</hui-notification-item-template>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
stateObj: Object
|
||||
};
|
||||
}
|
||||
|
||||
_handleDismiss() {
|
||||
this.hass.callApi('DELETE', `states/${this.stateObj.entity_id}`);
|
||||
}
|
||||
|
||||
_computeTitle(stateObj) {
|
||||
return (stateObj.attributes.title || computeStateName(stateObj));
|
||||
}
|
||||
}
|
||||
customElements.define(
|
||||
'hui-persistent_notification-notification-item',
|
||||
HuiPersistentNotificationItem
|
||||
);
|
@ -22,6 +22,8 @@ import '../../layouts/ha-app-layout.js';
|
||||
import '../../components/ha-start-voice-button.js';
|
||||
import '../../components/ha-icon.js';
|
||||
import { loadModule, loadCSS, loadJS } from '../../common/dom/load_resource.js';
|
||||
import './components/notifications/hui-notification-drawer.js';
|
||||
import './components/notifications/hui-notifications-button.js';
|
||||
import './hui-unused-entities.js';
|
||||
import './hui-view.js';
|
||||
import debounce from '../../common/util/debounce.js';
|
||||
@ -72,11 +74,20 @@ class HUIRoot extends NavigateMixin(EventsMixin(PolymerElement)) {
|
||||
}
|
||||
</style>
|
||||
<app-route route="[[route]]" pattern="/:view" data="{{routeData}}"></app-route>
|
||||
<hui-notification-drawer
|
||||
hass="[[hass]]"
|
||||
open="{{notificationsOpen}}"
|
||||
narrow="[[narrow]]"
|
||||
></hui-notification-drawer>
|
||||
<ha-app-layout id="layout">
|
||||
<app-header slot="header" effects="waterfall" fixed condenses>
|
||||
<app-toolbar>
|
||||
<ha-menu-button narrow='[[narrow]]' show-menu='[[showMenu]]'></ha-menu-button>
|
||||
<div main-title>[[_computeTitle(config)]]</div>
|
||||
<hui-notifications-button
|
||||
hass="[[hass]]"
|
||||
notifications-open="{{notificationsOpen}}"
|
||||
></hui-notifications-button>
|
||||
<ha-start-voice-button hass="[[hass]]"></ha-start-voice-button>
|
||||
<paper-menu-button
|
||||
no-animations
|
||||
@ -139,7 +150,13 @@ class HUIRoot extends NavigateMixin(EventsMixin(PolymerElement)) {
|
||||
type: Object,
|
||||
observer: '_routeChanged'
|
||||
},
|
||||
routeData: Object
|
||||
|
||||
notificationsOpen: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
routeData: Object,
|
||||
};
|
||||
}
|
||||
|
||||
@ -286,5 +303,4 @@ class HUIRoot extends NavigateMixin(EventsMixin(PolymerElement)) {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('hui-root', HUIRoot);
|
||||
|
@ -478,6 +478,11 @@
|
||||
"remember": "Remember",
|
||||
"log_in": "Log in"
|
||||
},
|
||||
"notification_drawer": {
|
||||
"click_to_configure": "Click button to configure {entity}",
|
||||
"empty": "No Notifications",
|
||||
"title": "Notifications"
|
||||
},
|
||||
"notification_toast": {
|
||||
"entity_turned_on": "Turned on {entity}.",
|
||||
"entity_turned_off": "Turned off {entity}.",
|
||||
|
Loading…
x
Reference in New Issue
Block a user