diff --git a/src/components/ha-area-picker.ts b/src/components/ha-area-picker.ts new file mode 100644 index 0000000000..a20bcb06d4 --- /dev/null +++ b/src/components/ha-area-picker.ts @@ -0,0 +1,229 @@ +import "@polymer/paper-input/paper-input"; +import "@polymer/paper-item/paper-item"; +import "@polymer/paper-item/paper-item-body"; +import "@vaadin/vaadin-combo-box/theme/material/vaadin-combo-box-light"; +import "@polymer/paper-listbox/paper-listbox"; +import { + LitElement, + TemplateResult, + html, + css, + CSSResult, + customElement, + property, +} from "lit-element"; +import { UnsubscribeFunc } from "home-assistant-js-websocket"; +import { SubscribeMixin } from "../mixins/subscribe-mixin"; + +import { HomeAssistant } from "../types"; +import { fireEvent } from "../common/dom/fire_event"; +import { PolymerChangedEvent } from "../polymer-types"; +import { + AreaRegistryEntry, + subscribeAreaRegistry, + createAreaRegistryEntry, +} from "../data/area_registry"; +import { + showPromptDialog, + showAlertDialog, +} from "../dialogs/generic/show-dialog-box"; + +const rowRenderer = ( + root: HTMLElement, + _owner, + model: { item: AreaRegistryEntry } +) => { + if (!root.firstElementChild) { + root.innerHTML = ` + + + +
[[item.name]]
+
+
+ `; + } + root.querySelector(".name")!.textContent = model.item.name!; + if (model.item.area_id === "add_new") { + root.querySelector("paper-item")!.className = "add-new"; + } else { + root.querySelector("paper-item")!.classList.remove("add-new"); + } +}; + +@customElement("ha-area-picker") +export class HaAreaPicker extends SubscribeMixin(LitElement) { + @property() public hass!: HomeAssistant; + @property() public label?: string; + @property() public value?: string; + @property() public _areas?: AreaRegistryEntry[]; + @property({ type: Boolean, attribute: "no-add" }) + public noAdd?: boolean; + @property() private _opened?: boolean; + + public hassSubscribe(): UnsubscribeFunc[] { + return [ + subscribeAreaRegistry(this.hass.connection!, (areas) => { + this._areas = this.noAdd + ? areas + : [ + ...areas, + { + area_id: "add_new", + name: this.hass.localize("ui.components.area-picker.add_new"), + }, + ]; + }), + ]; + } + + protected render(): TemplateResult | void { + if (!this._areas) { + return; + } + return html` + + + ${this.value + ? html` + + ${this.hass.localize("ui.components.area-picker.clear")} + + ` + : ""} + ${this._areas.length > 0 + ? html` + + ${this.hass.localize("ui.components.area-picker.toggle")} + + ` + : ""} + + + `; + } + + private _clearValue(ev: Event) { + ev.stopPropagation(); + this._setValue(""); + } + + private get _value() { + return this.value || ""; + } + + private _openedChanged(ev: PolymerChangedEvent) { + this._opened = ev.detail.value; + } + + private _areaChanged(ev: PolymerChangedEvent) { + const newValue = ev.detail.value; + + if (newValue !== "add_new") { + if (newValue !== this._value) { + this._setValue(newValue); + } + return; + } + + (ev.target as any).value = this._value; + showPromptDialog(this, { + title: this.hass.localize("ui.components.area-picker.add_dialog.title"), + text: this.hass.localize("ui.components.area-picker.add_dialog.text"), + confirmText: this.hass.localize( + "ui.components.area-picker.add_dialog.add" + ), + inputLabel: this.hass.localize( + "ui.components.area-picker.add_dialog.name" + ), + confirm: async (name) => { + if (!name) { + return; + } + try { + const area = await createAreaRegistryEntry(this.hass, { + name, + }); + this._areas = [...this._areas!, area]; + this._setValue(area.area_id); + } catch (err) { + showAlertDialog(this, { + text: this.hass.localize( + "ui.components.area-picker.add_dialog.failed_create_area" + ), + }); + } + }, + }); + } + + private _setValue(value: string) { + this.value = value; + setTimeout(() => { + fireEvent(this, "value-changed", { 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; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-area-picker": HaAreaPicker; + } +} diff --git a/src/dialogs/config-flow/dialog-data-entry-flow.ts b/src/dialogs/config-flow/dialog-data-entry-flow.ts index c4c2972574..f2cfe54008 100644 --- a/src/dialogs/config-flow/dialog-data-entry-flow.ts +++ b/src/dialogs/config-flow/dialog-data-entry-flow.ts @@ -322,7 +322,7 @@ class DataEntryFlowDialog extends LitElement { haStyleDialog, css` ha-paper-dialog { - max-width: 500px; + max-width: 600px; } ha-paper-dialog > * { margin: 0; diff --git a/src/dialogs/config-flow/step-flow-create-entry.ts b/src/dialogs/config-flow/step-flow-create-entry.ts index 07cad8a94a..3666b82298 100644 --- a/src/dialogs/config-flow/step-flow-create-entry.ts +++ b/src/dialogs/config-flow/step-flow-create-entry.ts @@ -11,7 +11,7 @@ import "@material/mwc-button"; import "@polymer/paper-dropdown-menu/paper-dropdown-menu-light"; import "@polymer/paper-item/paper-item"; import "@polymer/paper-listbox/paper-listbox"; - +import "../../components/ha-area-picker"; import { HomeAssistant } from "../../types"; import { fireEvent } from "../../common/dom/fire_event"; import { configFlowContentStyles } from "./styles"; @@ -19,13 +19,9 @@ import { DeviceRegistryEntry, updateDeviceRegistryEntry, } from "../../data/device_registry"; -import { - AreaRegistryEntry, - createAreaRegistryEntry, -} from "../../data/area_registry"; import { DataEntryFlowStepCreateEntry } from "../../data/data_entry_flow"; import { FlowConfig } from "./show-dialog-data-entry-flow"; -import { showPromptDialog } from "../generic/show-dialog-box"; +import { showAlertDialog } from "../generic/show-dialog-box"; @customElement("step-flow-create-entry") class StepFlowCreateEntry extends LitElement { @@ -40,9 +36,6 @@ class StepFlowCreateEntry extends LitElement { @property() public devices!: DeviceRegistryEntry[]; - @property() - public areas!: AreaRegistryEntry[]; - protected render(): TemplateResult | void { const localize = this.hass.localize; @@ -63,28 +56,11 @@ class StepFlowCreateEntry extends LitElement { ${device.name}
${device.model} (${device.manufacturer}) - - - - ${localize( - "ui.panel.config.integrations.config_entry.no_area" - )} - - ${this.areas.map( - (area) => html` - - ${area.name} - - ` - )} - - + @value-changed=${this._areaPicked} + > ` )} @@ -92,16 +68,6 @@ class StepFlowCreateEntry extends LitElement { `}
- ${this.devices.length > 0 - ? html` - ${localize( - "ui.panel.config.integrations.config_flow.add_area" - )} - ` - : ""} - ${localize( "ui.panel.config.integrations.config_flow.finish" @@ -115,59 +81,24 @@ class StepFlowCreateEntry extends LitElement { fireEvent(this, "flow-update", { step: undefined }); } - private async _addArea(name?: string) { - if (!name) { - return; - } - try { - const area = await createAreaRegistryEntry(this.hass, { - name, - }); - this.areas = [...this.areas, area]; - } catch (err) { - alert( - this.hass.localize( - "ui.panel.config.integrations.config_flow.failed_create_area" - ) - ); - } - } + private async _areaPicked(ev: CustomEvent) { + const picker = ev.currentTarget as any; + const device = picker.device; - private async _promptAddArea() { - showPromptDialog(this, { - title: this.hass.localize( - "ui.panel.config.integrations.config_flow.name_new_area" - ), - inputLabel: this.hass.localize( - "ui.panel.config.integrations.config_flow.area_picker_label" - ), - confirm: (text) => this._addArea(text), - }); - } - - private async _handleAreaChanged(ev: Event) { - const dropdown = ev.currentTarget as any; - const device = dropdown.device; - - // Item first becomes null, then new item. - if (!dropdown.selectedItem) { - return; - } - - const area = dropdown.selectedItem.area; + const area = ev.detail.value; try { await updateDeviceRegistryEntry(this.hass, device, { area_id: area, }); } catch (err) { - alert( - this.hass.localize( + showAlertDialog(this, { + text: this.hass.localize( "ui.panel.config.integrations.config_flow.error_saving_area", "error", - "err.message" - ) - ); - dropdown.value = null; + err.message + ), + }); + picker.value = null; } } @@ -188,7 +119,7 @@ class StepFlowCreateEntry extends LitElement { border-radius: 4px; margin: 4px; display: inline-block; - width: 200px; + width: 250px; } .buttons > *:last-child { margin-left: auto; diff --git a/src/dialogs/device-registry-detail/dialog-device-registry-detail.ts b/src/dialogs/device-registry-detail/dialog-device-registry-detail.ts index 5f266f97e0..c01f684d5b 100644 --- a/src/dialogs/device-registry-detail/dialog-device-registry-detail.ts +++ b/src/dialogs/device-registry-detail/dialog-device-registry-detail.ts @@ -15,15 +15,12 @@ import "@polymer/paper-item/paper-item"; import "@material/mwc-button/mwc-button"; import "../../components/dialog/ha-paper-dialog"; +import "../../components/ha-area-picker"; import { DeviceRegistryDetailDialogParams } from "./show-dialog-device-registry-detail"; import { PolymerChangedEvent } from "../../polymer-types"; import { haStyleDialog } from "../../resources/styles"; import { HomeAssistant } from "../../types"; -import { - subscribeAreaRegistry, - AreaRegistryEntry, -} from "../../data/area_registry"; import { computeDeviceName } from "../../data/device_registry"; @customElement("dialog-device-registry-detail") @@ -33,11 +30,9 @@ class DialogDeviceRegistryDetail extends LitElement { @property() private _nameByUser!: string; @property() private _error?: string; @property() private _params?: DeviceRegistryDetailDialogParams; - @property() private _areas?: AreaRegistryEntry[]; @property() private _areaId?: string; private _submitting?: boolean; - private _unsubAreas?: any; public async showDialog( params: DeviceRegistryDetailDialogParams @@ -49,20 +44,6 @@ class DialogDeviceRegistryDetail extends LitElement { await this.updateComplete; } - public connectedCallback() { - super.connectedCallback(); - this._unsubAreas = subscribeAreaRegistry(this.hass.connection, (areas) => { - this._areas = areas; - }); - } - - public disconnectedCallback() { - super.disconnectedCallback(); - if (this._unsubAreas) { - this._unsubAreas(); - } - } - protected render(): TemplateResult | void { if (!this._params) { return html``; @@ -88,36 +69,20 @@ class DialogDeviceRegistryDetail extends LitElement { -
- - - - ${this.hass.localize( - "ui.panel.config.integrations.config_entry.no_area" - )} - - ${this._renderAreas()} - - -
+
- ${this.hass.localize("ui.panel.config.entities.editor.update")} + ${this.hass.localize("ui.panel.config.devices.update")}
@@ -129,35 +94,8 @@ class DialogDeviceRegistryDetail extends LitElement { this._nameByUser = ev.detail.value; } - private _renderAreas() { - if (!this._areas) { - return; - } - return this._areas!.map( - (area) => html` - ${area.name} - ` - ); - } - - private _computeSelectedArea() { - if (!this._params || !this._areas) { - return -1; - } - const device = this._params!.device; - if (!device.area_id) { - return 0; - } - // +1 because of "No Area" entry - return this._areas.findIndex((area) => area.area_id === device.area_id) + 1; - } - - private _areaIndexChanged(event): void { - const selectedAreaIdx = event.target!.selected; - this._areaId = - selectedAreaIdx < 1 - ? undefined - : this._areas![selectedAreaIdx - 1].area_id; + private _areaPicked(event: CustomEvent): void { + this._areaId = event.detail.value; } private async _updateEntry(): Promise { diff --git a/src/dialogs/generic/dialog-box.ts b/src/dialogs/generic/dialog-box.ts index 0738239257..227573ac3f 100644 --- a/src/dialogs/generic/dialog-box.ts +++ b/src/dialogs/generic/dialog-box.ts @@ -17,6 +17,7 @@ import { HomeAssistant } from "../../types"; import { DialogParams } from "./show-dialog-box"; import { PolymerChangedEvent } from "../../polymer-types"; import { haStyleDialog } from "../../resources/styles"; +import { classMap } from "lit-html/directives/class-map"; @customElement("dialog-box") class DialogBox extends LitElement { @@ -56,7 +57,13 @@ class DialogBox extends LitElement { ${this._params.text ? html` -

${this._params.text}

+

+ ${this._params.text} +

` : ""} ${this._params.prompt @@ -137,6 +144,9 @@ class DialogBox extends LitElement { padding-bottom: 24px; color: var(--primary-text-color); } + .no-bottom-padding { + padding-bottom: 0; + } .secondary { color: var(--secondary-text-color); } diff --git a/src/translations/en.json b/src/translations/en.json index a12d2c0944..9a3ca7ac1e 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -535,9 +535,23 @@ }, "device-picker": { "clear": "Clear", + "toggle": "Toggle", "show_devices": "Show devices", "device": "Device" }, + "area-picker": { + "clear": "Clear", + "show_areas": "Show areas", + "area": "Area", + "add_new": "Add new area…", + "add_dialog": { + "title": "Add new area", + "text": "Enter the name of the new area.", + "name": "Name", + "add": "Add", + "failed_create_area": "Failed to create area." + } + }, "relative_time": { "past": "{time} ago", "future": "In {time}", @@ -1254,7 +1268,8 @@ "description": "Manage connected devices", "unnamed_device": "Unnamed device", "unknown_error": "Unknown error", - "area_picker_label": "Area", + "name": "Name", + "update": "Update", "automation": { "triggers": { "caption": "Do something when..." @@ -1425,11 +1440,7 @@ "finish": "Finish", "submit": "Submit", "not_all_required_fields": "Not all required fields are filled in.", - "add_area": "Add Area", - "area_picker_label": "Area", - "failed_create_area": "Failed to create area.", "error_saving_area": "Error saving area: {error}", - "name_new_area": "Name of the new area?", "created_config": "Created config for {name}.", "external_step": { "description": "This step requires you to visit an external website to be completed.",