From e1add14453d3924ba2988bd80f9271cf88a3a93d Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 26 Nov 2020 18:38:01 +0100 Subject: [PATCH] Add UI for new selectors (#7822) --- src/components/ha-expansion-panel.ts | 2 +- .../ha-selector/ha-selector-area.ts | 30 ++++ .../ha-selector/ha-selector-boolean.ts | 54 ++++++ .../ha-selector/ha-selector-device.ts | 6 + .../ha-selector/ha-selector-number.ts | 104 ++++++++++++ .../ha-selector/ha-selector-time.ts | 59 +++++++ src/components/ha-selector/ha-selector.ts | 4 + src/components/paper-time-input.js | 1 + src/data/blueprint.ts | 5 +- src/data/input_number.ts | 4 +- src/data/selector.ts | 34 +++- .../automation/blueprint-automation-editor.ts | 158 ++++++++++-------- .../config/automation/ha-automation-editor.ts | 2 + .../blueprint/dialog-import-blueprint.ts | 25 ++- .../config/blueprint/ha-blueprint-overview.ts | 132 +++++++-------- .../hui-input-datetime-entity-row.ts | 26 ++- src/translations/en.json | 3 +- 17 files changed, 480 insertions(+), 169 deletions(-) create mode 100644 src/components/ha-selector/ha-selector-area.ts create mode 100644 src/components/ha-selector/ha-selector-boolean.ts create mode 100644 src/components/ha-selector/ha-selector-number.ts create mode 100644 src/components/ha-selector/ha-selector-time.ts diff --git a/src/components/ha-expansion-panel.ts b/src/components/ha-expansion-panel.ts index 6805ecfa82..b74022032d 100644 --- a/src/components/ha-expansion-panel.ts +++ b/src/components/ha-expansion-panel.ts @@ -76,7 +76,7 @@ class HaExpansionPanel extends LitElement { .summary { display: flex; - padding: 0px 16px; + padding: var(--expansion-panel-summary-padding, 0px 16px); min-height: 48px; align-items: center; cursor: pointer; diff --git a/src/components/ha-selector/ha-selector-area.ts b/src/components/ha-selector/ha-selector-area.ts new file mode 100644 index 0000000000..fb3a051b69 --- /dev/null +++ b/src/components/ha-selector/ha-selector-area.ts @@ -0,0 +1,30 @@ +import { customElement, html, LitElement, property } from "lit-element"; +import { HomeAssistant } from "../../types"; +import { AreaSelector } from "../../data/selector"; +import "../ha-area-picker"; + +@customElement("ha-selector-area") +export class HaAreaSelector extends LitElement { + @property() public hass!: HomeAssistant; + + @property() public selector!: AreaSelector; + + @property() public value?: any; + + @property() public label?: string; + + protected render() { + return html``; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-selector-area": HaAreaSelector; + } +} diff --git a/src/components/ha-selector/ha-selector-boolean.ts b/src/components/ha-selector/ha-selector-boolean.ts new file mode 100644 index 0000000000..8339763f81 --- /dev/null +++ b/src/components/ha-selector/ha-selector-boolean.ts @@ -0,0 +1,54 @@ +import { + css, + CSSResult, + customElement, + html, + LitElement, + property, +} from "lit-element"; +import { fireEvent } from "../../common/dom/fire_event"; +import { HomeAssistant } from "../../types"; +import "../ha-formfield"; +import "../ha-switch"; + +@customElement("ha-selector-boolean") +export class HaBooleanSelector extends LitElement { + @property() public hass!: HomeAssistant; + + @property() public value?: number; + + @property() public label?: string; + + protected render() { + return html` + + `; + } + + private _handleChange(ev) { + const value = ev.target.checked; + if (this.value === value) { + return; + } + fireEvent(this, "value-changed", { value }); + } + + static get styles(): CSSResult { + return css` + ha-formfield { + width: 100%; + margin: 16px 0; + --mdc-typography-body2-font-size: 1em; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-selector-boolean": HaBooleanSelector; + } +} diff --git a/src/components/ha-selector/ha-selector-device.ts b/src/components/ha-selector/ha-selector-device.ts index dbad6a4171..98a83ba562 100644 --- a/src/components/ha-selector/ha-selector-device.ts +++ b/src/components/ha-selector/ha-selector-device.ts @@ -38,6 +38,12 @@ export class HaDeviceSelector extends LitElement { .value=${this.value} .label=${this.label} .deviceFilter=${(device) => this._filterDevices(device)} + .includeDeviceClasses=${this.selector.device.entity?.device_class + ? [this.selector.device.entity.device_class] + : undefined} + .includeDomains=${this.selector.device.entity?.domain + ? [this.selector.device.entity.domain] + : undefined} allow-custom-entity >`; } diff --git a/src/components/ha-selector/ha-selector-number.ts b/src/components/ha-selector/ha-selector-number.ts new file mode 100644 index 0000000000..15d1ff0e23 --- /dev/null +++ b/src/components/ha-selector/ha-selector-number.ts @@ -0,0 +1,104 @@ +import { + css, + CSSResult, + customElement, + html, + LitElement, + property, +} from "lit-element"; +import { HomeAssistant } from "../../types"; +import { NumberSelector } from "../../data/selector"; +import "@polymer/paper-input/paper-input"; +import "../ha-slider"; +import { fireEvent } from "../../common/dom/fire_event"; +import { classMap } from "lit-html/directives/class-map"; + +@customElement("ha-selector-number") +export class HaNumberSelector extends LitElement { + @property() public hass!: HomeAssistant; + + @property() public selector!: NumberSelector; + + @property() public value?: number; + + @property() public label?: string; + + protected render() { + return html`${this.label} + ${this.selector.number.mode === "slider" + ? html` + ` + : ""} + + ${this.selector.number.unit_of_measurement + ? html`
+ ${this.selector.number.unit_of_measurement} +
` + : ""} +
`; + } + + private get _value() { + return this.value || 0; + } + + private _handleInputChange(ev) { + const value = ev.detail.value; + if (this._value === value) { + return; + } + fireEvent(this, "value-changed", { value }); + } + + private _handleSliderChange(ev) { + const value = ev.target.value; + if (this._value === value) { + return; + } + fireEvent(this, "value-changed", { value }); + } + + static get styles(): CSSResult { + return css` + :host { + display: flex; + justify-content: space-between; + align-items: center; + } + ha-slider { + flex: 1; + } + .single { + flex: 1; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-selector-number": HaNumberSelector; + } +} diff --git a/src/components/ha-selector/ha-selector-time.ts b/src/components/ha-selector/ha-selector-time.ts new file mode 100644 index 0000000000..8bfa3d1ec1 --- /dev/null +++ b/src/components/ha-selector/ha-selector-time.ts @@ -0,0 +1,59 @@ +import { customElement, html, LitElement, property } from "lit-element"; +import { HomeAssistant } from "../../types"; +import { TimeSelector } from "../../data/selector"; +import { fireEvent } from "../../common/dom/fire_event"; +import "../paper-time-input"; + +const test = new Date().toLocaleString(); +const useAMPM = test.includes("AM") || test.includes("PM"); + +@customElement("ha-selector-time") +export class HaTimeSelector extends LitElement { + @property() public hass!: HomeAssistant; + + @property() public selector!: TimeSelector; + + @property() public value?: string; + + @property() public label?: string; + + protected render() { + const parts = this.value?.split(":") || []; + const hours = useAMPM ? parts[0] ?? "12" : parts[0] ?? "0"; + + return html` + 12 ? Number(hours) - 12 : hours} + .min=${parts[1] ?? "00"} + .sec=${parts[2] ?? "00"} + .format=${useAMPM ? 12 : 24} + .amPm=${useAMPM && (Number(hours) > 12 ? "PM" : "AM")} + @change=${this._timeChanged} + @am-pm-changed=${this._timeChanged} + hide-label + enable-second + > + `; + } + + private _timeChanged(ev) { + let value = ev.target.value; + if (useAMPM) { + let hours = Number(ev.target.hour); + if (ev.target.amPm === "PM") { + hours += 12; + } + value = `${hours}:${ev.target.min}:${ev.target.sec}`; + } + fireEvent(this, "value-changed", { + value, + }); + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-selector-time": HaTimeSelector; + } +} diff --git a/src/components/ha-selector/ha-selector.ts b/src/components/ha-selector/ha-selector.ts index 9e80292de2..5e607d61fd 100644 --- a/src/components/ha-selector/ha-selector.ts +++ b/src/components/ha-selector/ha-selector.ts @@ -4,6 +4,10 @@ import { HomeAssistant } from "../../types"; import "./ha-selector-entity"; import "./ha-selector-device"; +import "./ha-selector-area"; +import "./ha-selector-number"; +import "./ha-selector-boolean"; +import "./ha-selector-time"; import { Selector } from "../../data/selector"; @customElement("ha-selector") diff --git a/src/components/paper-time-input.js b/src/components/paper-time-input.js index a1fd0799f1..7ec7cc64ad 100644 --- a/src/components/paper-time-input.js +++ b/src/components/paper-time-input.js @@ -97,6 +97,7 @@ export class PaperTimeInput extends PolymerElement { .time-input-wrap { @apply --layout-horizontal; @apply --layout-no-wrap; + justify-content: var(--paper-time-input-justify-content, normal); } [hidden] { diff --git a/src/data/blueprint.ts b/src/data/blueprint.ts index 4a3283b565..2d98dc8c80 100644 --- a/src/data/blueprint.ts +++ b/src/data/blueprint.ts @@ -11,18 +11,19 @@ export interface Blueprint { export interface BlueprintMetaData { domain: string; name: string; + input?: Record; description?: string; - input: Record; + source_url?: string; } export interface BlueprintInput { name?: string; description?: string; selector?: Selector; + default?: any; } export interface BlueprintImportResult { - url: string; suggested_filename: string; raw_data: string; blueprint: Blueprint; diff --git a/src/data/input_number.ts b/src/data/input_number.ts index 2a1d9c46ad..7efd128c01 100644 --- a/src/data/input_number.ts +++ b/src/data/input_number.ts @@ -5,10 +5,10 @@ export interface InputNumber { name: string; min: number; max: number; + step: number; + mode: "box" | "slider"; icon?: string; initial?: number; - step?: number; - mode?: "box" | "slider"; unit_of_measurement?: string; } diff --git a/src/data/selector.ts b/src/data/selector.ts index fd20dbce95..ab56f9f016 100644 --- a/src/data/selector.ts +++ b/src/data/selector.ts @@ -1,4 +1,10 @@ -export type Selector = EntitySelector | DeviceSelector; +export type Selector = + | EntitySelector + | DeviceSelector + | AreaSelector + | NumberSelector + | BooleanSelector + | TimeSelector; export interface EntitySelector { entity: { @@ -13,5 +19,31 @@ export interface DeviceSelector { integration?: string; manufacturer?: string; model?: string; + entity?: EntitySelector["entity"]; }; } + +export interface AreaSelector { + // eslint-disable-next-line @typescript-eslint/ban-types + area: {}; +} + +export interface NumberSelector { + number: { + min: number; + max: number; + step: number; + mode: "box" | "slider"; + unit_of_measurement?: string; + }; +} + +export interface BooleanSelector { + // eslint-disable-next-line @typescript-eslint/ban-types + boolean: {}; +} + +export interface TimeSelector { + // eslint-disable-next-line @typescript-eslint/ban-types + time: {}; +} diff --git a/src/panels/config/automation/blueprint-automation-editor.ts b/src/panels/config/automation/blueprint-automation-editor.ts index 7153cee0ee..2a966254b0 100644 --- a/src/panels/config/automation/blueprint-automation-editor.ts +++ b/src/panels/config/automation/blueprint-automation-editor.ts @@ -33,6 +33,7 @@ import { import "../../../components/ha-blueprint-picker"; import "../../../components/ha-circular-progress"; import "../../../components/ha-selector/ha-selector"; +import "../../../components/ha-settings-row"; @customElement("blueprint-automation-editor") export class HaBlueprintAutomationEditor extends LitElement { @@ -40,7 +41,7 @@ export class HaBlueprintAutomationEditor extends LitElement { @property() public isWide!: boolean; - @property() public narrow!: boolean; + @property({ reflect: true, type: Boolean }) public narrow!: boolean; @property() public config!: BlueprintAutomationConfig; @@ -125,75 +126,78 @@ export class HaBlueprintAutomationEditor extends LitElement { )} -
-
- ${this._blueprints - ? Object.keys(this._blueprints).length - ? html` - - ` - : this.hass.localize( - "ui.panel.config.automation.editor.blueprint.no_blueprints" - ) - : html``} - - ${this.hass.localize( - "ui.panel.config.automation.editor.blueprint.manage_blueprints" - )} - -
- - ${this.config.use_blueprint.path - ? blueprint && "error" in blueprint - ? html`

- There is an error in this Blueprint: ${blueprint.error} -

` - : html`${blueprint?.metadata.description - ? html`

${blueprint.metadata.description}

` - : ""} - ${blueprint?.metadata?.input && - Object.keys(blueprint.metadata.input).length - ? html`

- ${this.hass.localize( - "ui.panel.config.automation.editor.blueprint.inputs" - )} -

- ${Object.entries(blueprint.metadata.input).map( - ([key, value]) => - html`
- ${value?.description} - ${value?.selector - ? html`` - : html``} -
` - )}` - : this.hass.localize( - "ui.panel.config.automation.editor.blueprint.no_inputs" - )}` - : ""} +
+ ${this._blueprints + ? Object.keys(this._blueprints).length + ? html` + + ` + : this.hass.localize( + "ui.panel.config.automation.editor.blueprint.no_blueprints" + ) + : html``} + + ${this.hass.localize( + "ui.panel.config.automation.editor.blueprint.manage_blueprints" + )} +
+ + ${this.config.use_blueprint.path + ? blueprint && "error" in blueprint + ? html`

+ There is an error in this Blueprint: ${blueprint.error} +

` + : html`${blueprint?.metadata.description + ? html`

${blueprint.metadata.description}

` + : ""} + ${blueprint?.metadata?.input && + Object.keys(blueprint.metadata.input).length + ? html`

+ ${this.hass.localize( + "ui.panel.config.automation.editor.blueprint.inputs" + )} +

+ ${Object.entries(blueprint.metadata.input).map( + ([key, value]) => + html` + ${value?.name || key} + ${value?.description} + ${value?.selector + ? html`` + : html``} + ` + )}` + : html`

+ ${this.hass.localize( + "ui.panel.config.automation.editor.blueprint.no_inputs" + )} +

`}` + : ""} `; } @@ -279,16 +283,20 @@ export class HaBlueprintAutomationEditor extends LitElement { font-weight: bold; color: var(--error-color); } + .padding { + padding: 16px; + } .content { padding-bottom: 20px; } .blueprint-picker-container { + padding: 16px; display: flex; align-items: center; justify-content: space-between; } h3 { - margin-top: 16px; + margin: 16px; } span[slot="introduction"] a { color: var(--primary-color); @@ -299,6 +307,16 @@ export class HaBlueprintAutomationEditor extends LitElement { ha-entity-toggle { margin-right: 8px; } + ha-settings-row { + --paper-time-input-justify-content: flex-end; + border-top: 1px solid var(--divider-color); + } + :host(:not([narrow])) ha-settings-row paper-input { + width: 50%; + } + :host(:not([narrow])) ha-settings-row ha-selector { + width: 50%; + } mwc-fab { position: relative; bottom: calc(-80px - env(safe-area-inset-bottom)); diff --git a/src/panels/config/automation/ha-automation-editor.ts b/src/panels/config/automation/ha-automation-editor.ts index e6b785e01f..a544fc3215 100644 --- a/src/panels/config/automation/ha-automation-editor.ts +++ b/src/panels/config/automation/ha-automation-editor.ts @@ -205,12 +205,14 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) { ${"use_blueprint" in this._config ? html`` : html`${this._result.blueprint.metadata.name}`, "domain", this._result.blueprint.metadata.domain - )}

+ )} +
+ ${this._result.blueprint.metadata.description} ${this._result.validation_errors ? html`

@@ -94,7 +97,14 @@ class DialogImportBlueprint extends LitElement { )} > `} -

${this._result.raw_data}
` + + ${this.hass.localize( + "ui.panel.config.blueprint.add.raw_blueprint" + )} +
${this._result.raw_data}
+
` : html`${this.hass.localize( "ui.panel.config.blueprint.add.import_introduction" )} { - const columns: DataTableColumnContainer = { - name: { - title: this.hass.localize( - "ui.panel.config.blueprint.overview.headers.name" - ), - sortable: true, - filterable: true, - direction: "asc", - grows: true, - }, - }; - - if (narrow) { - columns.name.template = (name, entity: any) => { - return html` - ${name}
-
- ${entity.path} -
- `; - }; - columns.create = { - title: "", - type: "icon-button", - template: (_, blueprint: any) => - blueprint.error - ? "" - : html` this._createNew(ev)} - >`, - }; - } else { - columns.path = { - title: this.hass.localize( - "ui.panel.config.blueprint.overview.headers.file_name" - ), - sortable: true, - filterable: true, - direction: "asc", - width: "25%", - }; - columns.create = { - title: "", - width: "180px", - template: (_, blueprint: any) => - blueprint.error - ? "" - : html` this._createNew(ev)} - > - ${this.hass.localize( - "ui.panel.config.blueprint.overview.use_blueprint" - )} - `, - }; - } - - columns.delete = { + (narrow, _language): DataTableColumnContainer => ({ + name: { + title: this.hass.localize( + "ui.panel.config.blueprint.overview.headers.name" + ), + sortable: true, + filterable: true, + direction: "asc", + grows: true, + template: narrow + ? (name, entity: any) => + html` + ${name}
+
+ ${entity.path} +
+ ` + : undefined, + }, + path: { + title: this.hass.localize( + "ui.panel.config.blueprint.overview.headers.file_name" + ), + sortable: true, + filterable: true, + hidden: narrow, + direction: "asc", + width: "25%", + }, + create: { + title: "", + type: narrow ? "icon-button" : undefined, + width: narrow ? undefined : "180px", + template: (_, blueprint: any) => + blueprint.error + ? "" + : narrow + ? html` this._createNew(ev)} + >` + : html` this._createNew(ev)} + > + ${this.hass.localize( + "ui.panel.config.blueprint.overview.use_blueprint" + )} + `, + }, + delete: { title: "", type: "icon-button", template: (_, blueprint: any) => @@ -161,10 +151,8 @@ class HaBlueprintOverview extends LitElement { @click=${(ev) => this._delete(ev)} >`, - }; - - return columns; - } + }, + }) ); protected render(): TemplateResult { diff --git a/src/panels/lovelace/entity-rows/hui-input-datetime-entity-row.ts b/src/panels/lovelace/entity-rows/hui-input-datetime-entity-row.ts index 77f971e6c8..e36441c6e2 100644 --- a/src/panels/lovelace/entity-rows/hui-input-datetime-entity-row.ts +++ b/src/panels/lovelace/entity-rows/hui-input-datetime-entity-row.ts @@ -6,6 +6,7 @@ import { internalProperty, PropertyValues, TemplateResult, + query, } from "lit-element"; import "../../../components/ha-date-input"; import type { HaDateInput } from "../../../components/ha-date-input"; @@ -25,6 +26,10 @@ class HuiInputDatetimeEntityRow extends LitElement implements LovelaceRow { @internalProperty() private _config?: EntityConfig; + @query("paper-time-input") private _timeInputEl?: PaperTimeInput; + + @query("ha-date-input") private _dateInputEl?: HaDateInput; + public setConfig(config: EntityConfig): void { if (!config) { throw new Error("Invalid configuration"); @@ -74,11 +79,10 @@ class HuiInputDatetimeEntityRow extends LitElement implements LovelaceRow { .min=${stateObj.state === UNKNOWN ? "" : ("0" + stateObj.attributes.minute).slice(-2)} - .amPm=${false} @change=${this._selectedValueChanged} @click=${this._stopEventPropagation} hide-label - format="24" + .format=${24} > ` : ``} @@ -90,24 +94,14 @@ class HuiInputDatetimeEntityRow extends LitElement implements LovelaceRow { ev.stopPropagation(); } - private get _timeInputEl(): PaperTimeInput { - return this.shadowRoot!.querySelector("paper-time-input")!; - } - - private get _dateInputEl(): HaDateInput { - return this.shadowRoot!.querySelector("ha-date-input")!; - } - private _selectedValueChanged(ev): void { const stateObj = this.hass!.states[this._config!.entity]; - const time = - this._timeInputEl !== null - ? this._timeInputEl.value.trim() + ":00" - : undefined; + const time = this._timeInputEl + ? this._timeInputEl.value?.trim() + : undefined; - const date = - this._dateInputEl !== null ? this._dateInputEl.value : undefined; + const date = this._dateInputEl ? this._dateInputEl.value : undefined; if (time !== stateObj.state) { setInputDateTimeValue(this.hass!, stateObj.entity_id, time, date); diff --git a/src/translations/en.json b/src/translations/en.json index 86b06bb91b..d00b1e1933 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -1453,7 +1453,7 @@ }, "confirm_delete_header": "Delete this blueprint?", "confirm_delete_text": "Are you sure you want to delete this blueprint?", - "add_blueprint": "Add blueprint", + "add_blueprint": "Import blueprint", "use_blueprint": "Create automation", "delete_blueprint": "Delete blueprint" }, @@ -1462,6 +1462,7 @@ "import_header": "Import \"{name}\" (type: {domain})", "import_introduction": "You can import blueprints of other users from Github and the community forums. Enter the URL of the blueprint below.", "url": "URL of the blueprint", + "raw_blueprint": "Blueprint content", "importing": "Importing blueprint...", "import_btn": "Import blueprint", "saving": "Saving blueprint...",