Add UI for new selectors (#7822)

This commit is contained in:
Bram Kragten 2020-11-26 18:38:01 +01:00 committed by GitHub
parent e3293837a8
commit e1add14453
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 480 additions and 169 deletions

View File

@ -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;

View File

@ -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`<ha-area-picker
.hass=${this.hass}
.value=${this.value}
.label=${this.label}
no-add
></ha-area-picker>`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-selector-area": HaAreaSelector;
}
}

View File

@ -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` <ha-formfield alignEnd spaceBetween .label=${this.label}>
<ha-switch
.checked=${this.value}
@change=${this._handleChange}
></ha-switch>
</ha-formfield>`;
}
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;
}
}

View File

@ -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
></ha-device-picker>`;
}

View File

@ -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`<ha-slider
.min=${this.selector.number.min}
.max=${this.selector.number.max}
.value=${this._value}
.step=${this.selector.number.step}
pin
ignore-bar-touch
@change=${this._handleSliderChange}
>
</ha-slider>`
: ""}
<paper-input
pattern="[0-9]+([\\.][0-9]+)?"
.label=${this.selector.number.mode === "slider"
? undefined
: this.label}
.noLabelFloat=${this.selector.number.mode === "slider"}
class=${classMap({ single: this.selector.number.mode === "box" })}
.min=${this.selector.number.min}
.max=${this.selector.number.max}
.value=${this._value}
.step=${this.selector.number.step}
type="number"
auto-validate
@value-changed=${this._handleInputChange}
>
${this.selector.number.unit_of_measurement
? html`<div slot="suffix">
${this.selector.number.unit_of_measurement}
</div>`
: ""}
</paper-input>`;
}
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;
}
}

View File

@ -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`
<paper-time-input
.label=${this.label}
.hour=${useAMPM && Number(hours) > 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
></paper-time-input>
`;
}
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;
}
}

View File

@ -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")

View File

@ -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] {

View File

@ -11,18 +11,19 @@ export interface Blueprint {
export interface BlueprintMetaData {
domain: string;
name: string;
input?: Record<string, BlueprintInput | null>;
description?: string;
input: Record<string, BlueprintInput | null>;
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;

View File

@ -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;
}

View File

@ -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: {};
}

View File

@ -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,7 +126,6 @@ export class HaBlueprintAutomationEditor extends LitElement {
)}</span
>
<ha-card>
<div class="card-content">
<div class="blueprint-picker-container">
${this._blueprints
? Object.keys(this._blueprints).length
@ -153,7 +153,7 @@ export class HaBlueprintAutomationEditor extends LitElement {
${this.config.use_blueprint.path
? blueprint && "error" in blueprint
? html`<p class="warning">
? html`<p class="warning padding">
There is an error in this Blueprint: ${blueprint.error}
</p>`
: html`${blueprint?.metadata.description
@ -168,32 +168,36 @@ export class HaBlueprintAutomationEditor extends LitElement {
</h3>
${Object.entries(blueprint.metadata.input).map(
([key, value]) =>
html`<div>
${value?.description}
html`<ha-settings-row .narrow=${this.narrow}>
<span slot="heading">${value?.name || key}</span>
<span slot="description"
>${value?.description}</span
>
${value?.selector
? html`<ha-selector
.hass=${this.hass}
.selector=${value.selector}
.key=${key}
.label=${value?.name || key}
.value=${this.config.use_blueprint.input &&
this.config.use_blueprint.input[key]}
.value=${(this.config.use_blueprint.input &&
this.config.use_blueprint.input[key]) ||
value?.default}
@value-changed=${this._inputChanged}
></ha-selector>`
: html`<paper-input
.key=${key}
.label=${value?.name || key}
.value=${this.config.use_blueprint.input &&
this.config.use_blueprint.input[key]}
@value-changed=${this._inputChanged}
no-label-float
></paper-input>`}
</div>`
</ha-settings-row>`
)}`
: this.hass.localize(
: html`<p class="padding">
${this.hass.localize(
"ui.panel.config.automation.editor.blueprint.no_inputs"
)}`
)}
</p>`}`
: ""}
</div>
</ha-card>
</ha-config-section>`;
}
@ -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));

View File

@ -205,12 +205,14 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) {
${"use_blueprint" in this._config
? html`<blueprint-automation-editor
.hass=${this.hass}
.narrow=${this.narrow}
.stateObj=${stateObj}
.config=${this._config}
@value-changed=${this._valueChanged}
></blueprint-automation-editor>`
: html`<manual-automation-editor
.hass=${this.hass}
.narrow=${this.narrow}
.stateObj=${stateObj}
.config=${this._config}
@value-changed=${this._valueChanged}

View File

@ -23,6 +23,7 @@ import {
importBlueprint,
saveBlueprint,
} from "../../../data/blueprint";
import "../../../components/ha-expansion-panel";
@customElement("ha-dialog-import-blueprint")
class DialogImportBlueprint extends LitElement {
@ -71,7 +72,9 @@ class DialogImportBlueprint extends LitElement {
html`<b>${this._result.blueprint.metadata.name}</b>`,
"domain",
this._result.blueprint.metadata.domain
)} <br /><br />
)}
<br />
${this._result.blueprint.metadata.description}
${this._result.validation_errors
? html`
<p class="error">
@ -94,7 +97,14 @@ class DialogImportBlueprint extends LitElement {
)}
></paper-input>
`}
<pre>${this._result.raw_data}</pre>`
<ha-expansion-panel>
<span slot="title"
>${this.hass.localize(
"ui.panel.config.blueprint.add.raw_blueprint"
)}</span
>
<pre>${this._result.raw_data}</pre>
</ha-expansion-panel>`
: html`${this.hass.localize(
"ui.panel.config.blueprint.add.import_introduction"
)}<paper-input
@ -180,7 +190,7 @@ class DialogImportBlueprint extends LitElement {
this._result!.blueprint.metadata.domain,
filename,
this._result!.raw_data,
this._result!.url
this._result!.blueprint.metadata.source_url
);
this._params.importedCallback();
this.closeDialog();
@ -192,7 +202,14 @@ class DialogImportBlueprint extends LitElement {
}
static get styles(): CSSResult[] {
return [haStyleDialog, css``];
return [
haStyleDialog,
css`
ha-expansion-panel {
--expansion-panel-summary-padding: 0;
}
`,
];
}
}

View File

@ -80,8 +80,7 @@ class HaBlueprintOverview extends LitElement {
});
private _columns = memoizeOne(
(narrow, _language): DataTableColumnContainer => {
const columns: DataTableColumnContainer = {
(narrow, _language): DataTableColumnContainer => ({
name: {
title: this.hass.localize(
"ui.panel.config.blueprint.overview.headers.name"
@ -90,25 +89,35 @@ class HaBlueprintOverview extends LitElement {
filterable: true,
direction: "asc",
grows: true,
},
};
if (narrow) {
columns.name.template = (name, entity: any) => {
return html`
template: narrow
? (name, entity: any) =>
html`
${name}<br />
<div class="secondary">
${entity.path}
</div>
`;
};
columns.create = {
`
: 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: "icon-button",
type: narrow ? "icon-button" : undefined,
width: narrow ? undefined : "180px",
template: (_, blueprint: any) =>
blueprint.error
? ""
: html` <mwc-icon-button
: narrow
? html`<mwc-icon-button
.blueprint=${blueprint}
.label=${this.hass.localize(
"ui.panel.config.blueprint.overview.use_blueprint"
@ -118,24 +127,7 @@ class HaBlueprintOverview extends LitElement {
)}
@click=${(ev) => this._createNew(ev)}
><ha-svg-icon .path=${mdiRobot}></ha-svg-icon
></mwc-icon-button>`,
};
} 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
? ""
></mwc-icon-button>`
: html`<mwc-button
.blueprint=${blueprint}
@click=${(ev) => this._createNew(ev)}
@ -144,10 +136,8 @@ class HaBlueprintOverview extends LitElement {
"ui.panel.config.blueprint.overview.use_blueprint"
)}
</mwc-button>`,
};
}
columns.delete = {
},
delete: {
title: "",
type: "icon-button",
template: (_, blueprint: any) =>
@ -161,10 +151,8 @@ class HaBlueprintOverview extends LitElement {
@click=${(ev) => this._delete(ev)}
><ha-svg-icon .path=${mdiDelete}></ha-svg-icon
></mwc-icon-button>`,
};
return columns;
}
},
})
);
protected render(): TemplateResult {

View File

@ -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}
></paper-time-input>
`
: ``}
@ -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"
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);

View File

@ -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...",