diff --git a/package.json b/package.json
index d704eb4b9b..111db689f4 100644
--- a/package.json
+++ b/package.json
@@ -81,6 +81,7 @@
"lit-html": "^1.0.0",
"marked": "^0.6.0",
"mdn-polyfills": "^5.12.0",
+ "memoize-one": "^5.0.0",
"moment": "^2.22.2",
"preact": "^8.3.1",
"preact-compat": "^3.18.4",
@@ -105,6 +106,7 @@
"@gfx/zopfli": "^1.0.9",
"@types/chai": "^4.1.7",
"@types/codemirror": "^0.0.71",
+ "@types/memoize-one": "^4.1.0",
"@types/mocha": "^5.2.5",
"babel-eslint": "^10",
"babel-loader": "^8.0.4",
diff --git a/src/components/entity/ha-entities-picker.ts b/src/components/entity/ha-entities-picker.ts
new file mode 100644
index 0000000000..5fe0720c95
--- /dev/null
+++ b/src/components/entity/ha-entities-picker.ts
@@ -0,0 +1,120 @@
+import {
+ LitElement,
+ TemplateResult,
+ property,
+ html,
+ customElement,
+} from "lit-element";
+import "@polymer/paper-icon-button/paper-icon-button-light";
+
+import { HomeAssistant } from "../../types";
+import { PolymerChangedEvent } from "../../polymer-types";
+import { fireEvent } from "../../common/dom/fire_event";
+import isValidEntityId from "../../common/entity/valid_entity_id";
+
+import "./ha-entity-picker";
+// Not a duplicate, type import
+// tslint:disable-next-line
+import { HaEntityPickerEntityFilterFunc } from "./ha-entity-picker";
+import { HassEntity } from "home-assistant-js-websocket";
+
+@customElement("ha-entities-picker")
+class HaEntitiesPickerLight extends LitElement {
+ @property() public hass?: HomeAssistant;
+ @property() public value?: string[];
+ @property() public domainFilter?: string;
+ @property() public pickedEntityLabel?: string;
+ @property() public pickEntityLabel?: string;
+
+ protected render(): TemplateResult | void {
+ if (!this.hass) {
+ return;
+ }
+ const currentEntities = this._currentEntities;
+ return html`
+ ${currentEntities.map(
+ (entityId) => html`
+
+
+
+ `
+ )}
+
+
+
+ `;
+ }
+
+ private _entityFilter: HaEntityPickerEntityFilterFunc = (
+ stateObj: HassEntity
+ ) => !this.value || !this.value.includes(stateObj.entity_id);
+
+ private get _currentEntities() {
+ return this.value || [];
+ }
+
+ private async _updateEntities(entities) {
+ fireEvent(this, "value-changed", {
+ value: entities,
+ });
+
+ this.value = entities;
+ }
+
+ private _entityChanged(event: PolymerChangedEvent) {
+ event.stopPropagation();
+ const curValue = (event.currentTarget as any).curValue;
+ const newValue = event.detail.value;
+ if (
+ newValue === curValue ||
+ (newValue !== "" && !isValidEntityId(newValue))
+ ) {
+ return;
+ }
+ if (newValue === "") {
+ this._updateEntities(
+ this._currentEntities.filter((ent) => ent !== curValue)
+ );
+ } else {
+ this._updateEntities(
+ this._currentEntities.map((ent) => (ent === curValue ? newValue : ent))
+ );
+ }
+ }
+
+ private async _addEntity(event: PolymerChangedEvent) {
+ event.stopPropagation();
+ const toAdd = event.detail.value;
+ (event.currentTarget as any).value = "";
+ if (!toAdd) {
+ return;
+ }
+ const currentEntities = this._currentEntities;
+ if (currentEntities.includes(toAdd)) {
+ return;
+ }
+
+ this._updateEntities([...currentEntities, toAdd]);
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "ha-entities-picker": HaEntitiesPickerLight;
+ }
+}
diff --git a/src/components/entity/ha-entity-picker.js b/src/components/entity/ha-entity-picker.js
deleted file mode 100644
index bc4765d8d7..0000000000
--- a/src/components/entity/ha-entity-picker.js
+++ /dev/null
@@ -1,179 +0,0 @@
-import "@polymer/paper-icon-button/paper-icon-button";
-import "@polymer/paper-input/paper-input";
-import "@polymer/paper-item/paper-icon-item";
-import "@polymer/paper-item/paper-item-body";
-import { html } from "@polymer/polymer/lib/utils/html-tag";
-import { PolymerElement } from "@polymer/polymer/polymer-element";
-import "@vaadin/vaadin-combo-box/vaadin-combo-box-light";
-
-import "./state-badge";
-
-import computeStateName from "../../common/entity/compute_state_name";
-import LocalizeMixin from "../../mixins/localize-mixin";
-import EventsMixin from "../../mixins/events-mixin";
-
-/*
- * @appliesMixin LocalizeMixin
- */
-class HaEntityPicker extends EventsMixin(LocalizeMixin(PolymerElement)) {
- static get template() {
- return html`
-
-
-
- Clear
- Toggle
-
-
-
-
-
-
- [[_computeStateName(item)]]
- [[item.entity_id]]
-
-
-
-
- `;
- }
-
- static get properties() {
- return {
- allowCustomEntity: {
- type: Boolean,
- value: false,
- },
- hass: {
- type: Object,
- observer: "_hassChanged",
- },
- _hass: Object,
- _states: {
- type: Array,
- computed: "_computeStates(_hass, domainFilter, entityFilter)",
- },
- autofocus: Boolean,
- label: {
- type: String,
- },
- value: {
- type: String,
- notify: true,
- },
- opened: {
- type: Boolean,
- value: false,
- observer: "_openedChanged",
- },
- domainFilter: {
- type: String,
- value: null,
- },
- entityFilter: {
- type: Function,
- value: null,
- },
- disabled: Boolean,
- };
- }
-
- _computeLabel(label, localize) {
- return label === undefined
- ? localize("ui.components.entity.entity-picker.entity")
- : label;
- }
-
- _computeStates(hass, domainFilter, entityFilter) {
- if (!hass) return [];
-
- let entityIds = Object.keys(hass.states);
-
- if (domainFilter) {
- entityIds = entityIds.filter(
- (eid) => eid.substr(0, eid.indexOf(".")) === domainFilter
- );
- }
-
- let entities = entityIds.sort().map((key) => hass.states[key]);
-
- if (entityFilter) {
- entities = entities.filter(entityFilter);
- }
-
- return entities;
- }
-
- _computeStateName(state) {
- return computeStateName(state);
- }
-
- _openedChanged(newVal) {
- if (!newVal) {
- this._hass = this.hass;
- }
- }
-
- _hassChanged(newVal) {
- if (!this.opened) {
- this._hass = newVal;
- }
- }
-
- _computeToggleIcon(opened) {
- return opened ? "hass:menu-up" : "hass:menu-down";
- }
-
- _fireChanged(ev) {
- ev.stopPropagation();
- this.fire("change");
- }
-}
-
-customElements.define("ha-entity-picker", HaEntityPicker);
diff --git a/src/components/entity/ha-entity-picker.ts b/src/components/entity/ha-entity-picker.ts
new file mode 100644
index 0000000000..3b5bf7ed88
--- /dev/null
+++ b/src/components/entity/ha-entity-picker.ts
@@ -0,0 +1,200 @@
+import "@polymer/paper-icon-button/paper-icon-button";
+import "@polymer/paper-input/paper-input";
+import "@polymer/paper-item/paper-icon-item";
+import "@polymer/paper-item/paper-item-body";
+import "@vaadin/vaadin-combo-box/vaadin-combo-box-light";
+import memoizeOne from "memoize-one";
+
+import "./state-badge";
+
+import computeStateName from "../../common/entity/compute_state_name";
+import {
+ LitElement,
+ TemplateResult,
+ html,
+ css,
+ CSSResult,
+ property,
+ PropertyValues,
+} from "lit-element";
+import { HomeAssistant } from "../../types";
+import { HassEntity } from "home-assistant-js-websocket";
+import { PolymerChangedEvent } from "../../polymer-types";
+import { fireEvent } from "../../common/dom/fire_event";
+
+export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean;
+
+const rowRenderer = (
+ root: HTMLElement,
+ _owner,
+ model: { item: HassEntity }
+) => {
+ if (!root.firstElementChild) {
+ root.innerHTML = `
+
+
+
+
+ [[_computeStateName(item)]]
+ [[item.entity_id]]
+
+
+ `;
+ }
+
+ root.querySelector("state-badge")!.stateObj = model.item;
+ root.querySelector(".name")!.textContent = computeStateName(model.item);
+ root.querySelector("[secondary]")!.textContent = model.item.entity_id;
+};
+
+class HaEntityPicker extends LitElement {
+ @property({ type: Boolean }) public autofocus?: boolean;
+ @property({ type: Boolean }) public disabled?: boolean;
+ @property({ type: Boolean }) public allowCustomEntity;
+ @property() public hass?: HomeAssistant;
+ @property() public label?: string;
+ @property() public value?: string;
+ @property() public domainFilter?: string;
+ @property() public entityFilter?: HaEntityPickerEntityFilterFunc;
+ @property({ type: Boolean }) private _opened?: boolean;
+ @property() private _hass?: HomeAssistant;
+
+ private _getStates = memoizeOne(
+ (
+ hass: this["hass"],
+ domainFilter: this["domainFilter"],
+ entityFilter: this["entityFilter"]
+ ) => {
+ let states: HassEntity[] = [];
+
+ if (!hass) {
+ return [];
+ }
+ let entityIds = Object.keys(hass.states);
+
+ if (domainFilter) {
+ entityIds = entityIds.filter(
+ (eid) => eid.substr(0, eid.indexOf(".")) === domainFilter
+ );
+ }
+
+ states = entityIds.sort().map((key) => hass!.states[key]);
+
+ if (entityFilter) {
+ states = states.filter(
+ (stateObj) =>
+ // We always want to include the entity of the current value
+ stateObj.entity_id === this.value || entityFilter!(stateObj)
+ );
+ }
+ return states;
+ }
+ );
+
+ protected updated(changedProps: PropertyValues) {
+ super.updated(changedProps);
+
+ if (changedProps.has("hass") && !this._opened) {
+ this._hass = this.hass;
+ }
+ }
+
+ protected render(): TemplateResult | void {
+ const states = this._getStates(
+ this._hass,
+ this.domainFilter,
+ this.entityFilter
+ );
+
+ return html`
+
+
+ ${this.value
+ ? html`
+
+ Clear
+
+ `
+ : ""}
+ ${states.length > 0
+ ? html`
+
+ Toggle
+
+ `
+ : ""}
+
+
+ `;
+ }
+
+ private get _value() {
+ return this.value || "";
+ }
+
+ private _openedChanged(ev: PolymerChangedEvent) {
+ this._opened = ev.detail.value;
+ }
+
+ private _valueChanged(ev: PolymerChangedEvent) {
+ const newValue = ev.detail.value;
+ if (newValue !== this._value) {
+ this.value = ev.detail.value;
+ setTimeout(() => {
+ fireEvent(this, "value-changed", { value: this.value });
+ fireEvent(this, "change");
+ }, 0);
+ }
+ }
+
+ static get styles(): CSSResult {
+ return css`
+ paper-input > paper-icon-button {
+ width: 24px;
+ height: 24px;
+ padding: 2px;
+ color: var(--secondary-text-color);
+ }
+ [hidden] {
+ display: none;
+ }
+ `;
+ }
+}
+
+customElements.define("ha-entity-picker", HaEntityPicker);
diff --git a/src/panels/config/person/dialog-person-detail.ts b/src/panels/config/person/dialog-person-detail.ts
index 2e863e7c80..4497f6204d 100644
--- a/src/panels/config/person/dialog-person-detail.ts
+++ b/src/panels/config/person/dialog-person-detail.ts
@@ -2,14 +2,15 @@ import {
LitElement,
html,
css,
- PropertyDeclarations,
CSSResult,
TemplateResult,
+ property,
} from "lit-element";
import "@polymer/paper-dialog/paper-dialog";
import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable";
import "@polymer/paper-input/paper-input";
+import "../../../components/entity/ha-entities-picker";
import { PersonDetailDialogParams } from "./show-dialog-person-detail";
import { PolymerChangedEvent } from "../../../polymer-types";
import { haStyleDialog } from "../../../resources/ha-style";
@@ -17,24 +18,20 @@ import { HomeAssistant } from "../../../types";
import { PersonMutableParams } from "../../../data/person";
class DialogPersonDetail extends LitElement {
- public hass!: HomeAssistant;
- private _name!: string;
- private _error?: string;
- private _params?: PersonDetailDialogParams;
- private _submitting?: boolean;
-
- static get properties(): PropertyDeclarations {
- return {
- _error: {},
- _name: {},
- _params: {},
- };
- }
+ @property() public hass!: HomeAssistant;
+ @property() private _name!: string;
+ @property() private _deviceTrackers!: string[];
+ @property() private _error?: string;
+ @property() private _params?: PersonDetailDialogParams;
+ @property() private _submitting?: boolean;
public async showDialog(params: PersonDetailDialogParams): Promise {
this._params = params;
this._error = undefined;
this._name = this._params.entry ? this._params.entry.name : "";
+ this._deviceTrackers = this._params.entry
+ ? this._params.entry.device_trackers || []
+ : [];
await this.updateComplete;
}
@@ -64,6 +61,23 @@ class DialogPersonDetail extends LitElement {
error-message="Name is required"
.invalid=${nameInvalid}
>
+
+ ${this.hass.localize(
+ "ui.panel.config.person.detail.device_tracker_intro"
+ )}
+
+
@@ -94,14 +108,19 @@ class DialogPersonDetail extends LitElement {
this._name = ev.detail.value;
}
+ private _deviceTrackersChanged(ev: PolymerChangedEvent) {
+ this._error = undefined;
+ this._deviceTrackers = ev.detail.value;
+ }
+
private async _updateEntry() {
this._submitting = true;
try {
const values: PersonMutableParams = {
name: this._name.trim(),
+ device_trackers: this._deviceTrackers,
// Temp, we will add this in a future PR.
user_id: null,
- device_trackers: [],
};
if (this._params!.entry) {
await this._params!.updateEntry(values);
diff --git a/src/translations/en.json b/src/translations/en.json
index 01aca9f1e3..d20f31f8cf 100644
--- a/src/translations/en.json
+++ b/src/translations/en.json
@@ -783,8 +783,14 @@
}
},
"person": {
- "caption": "People",
- "description": "Manage the people in Home Assistant."
+ "caption": "Persons",
+ "description": "Manage the persons that Home Assistant tracks.",
+ "detail": {
+ "name": "Name",
+ "device_tracker_intro": "Select the devices that belong to this person.",
+ "device_tracker_picked": "Track Device",
+ "device_tracker_pick": "Pick device to track"
+ }
},
"integrations": {
"caption": "Integrations",
diff --git a/src/types.ts b/src/types.ts
index e72f40b124..50964a2640 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -29,6 +29,13 @@ declare global {
getComputedStyleValue(element, propertyName);
};
}
+ // for fire event
+ interface HASSDomEvents {
+ "value-changed": {
+ value: unknown;
+ };
+ change: undefined;
+ }
}
export interface WebhookError {
diff --git a/yarn.lock b/yarn.lock
index 800cf3275f..17185f5b93 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1642,6 +1642,11 @@
resolved "https://registry.yarnpkg.com/@types/launchpad/-/launchpad-0.6.0.tgz#37296109b7f277f6e6c5fd7e0c0706bc918fbb51"
integrity sha1-NylhCbfyd/bmxf1+DAcGvJGPu1E=
+"@types/memoize-one@^4.1.0":
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/@types/memoize-one/-/memoize-one-4.1.0.tgz#62119f26055b3193ae43ca1882c5b29b88b71ece"
+ integrity sha512-cmSgi6JMX/yBwgpVm4GooNWIH+vEeJoa8FAa6ExOhpJbC0Juq32/uYKiKb3VPSqrEA0aOnjvwZanla3O1WZMbw==
+
"@types/merge-stream@^1.0.28":
version "1.1.2"
resolved "https://registry.yarnpkg.com/@types/merge-stream/-/merge-stream-1.1.2.tgz#a880ff66b1fbbb5eef4958d015c5947a9334dbb1"
@@ -9466,6 +9471,11 @@ mem@^4.0.0:
mimic-fn "^1.0.0"
p-is-promise "^1.1.0"
+memoize-one@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.0.0.tgz#d55007dffefb8de7546659a1722a5d42e128286e"
+ integrity sha512-7g0+ejkOaI9w5x6LvQwmj68kUj6rxROywPSCqmclG/HBacmFnZqhVscQ8kovkn9FBCNJmOz6SY42+jnvZzDWdw==
+
memory-fs@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.2.0.tgz#f2bb25368bc121e391c2520de92969caee0a0290"