ha-selector-selector implementation (#18421)

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
This commit is contained in:
karwosts 2023-11-29 03:09:49 -08:00 committed by GitHub
parent a5a9bcafa7
commit 0c042079ed
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 384 additions and 3 deletions

View File

@ -5,6 +5,7 @@ import memoizeOne from "memoize-one";
import { ensureArray } from "../../common/array/ensure-array";
import type { DeviceRegistryEntry } from "../../data/device_registry";
import { getDeviceIntegrationLookup } from "../../data/device_registry";
import { fireEvent } from "../../common/dom/fire_event";
import {
EntitySources,
fetchEntitySourcesWithCache,
@ -49,6 +50,18 @@ export class HaAreaSelector extends LitElement {
);
}
protected willUpdate(changedProperties: PropertyValues): void {
if (changedProperties.has("selector") && this.value !== undefined) {
if (this.selector.area?.multiple && !Array.isArray(this.value)) {
this.value = [this.value];
fireEvent(this, "value-changed", { value: this.value });
} else if (!this.selector.area?.multiple && Array.isArray(this.value)) {
this.value = this.value[0];
fireEvent(this, "value-changed", { value: this.value });
}
}
}
protected updated(changedProperties: PropertyValues): void {
if (
changedProperties.has("selector") &&

View File

@ -1,8 +1,9 @@
import { HassEntity } from "home-assistant-js-websocket";
import { html, LitElement, nothing } from "lit";
import { html, LitElement, PropertyValues, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { ensureArray } from "../../common/array/ensure-array";
import { fireEvent } from "../../common/dom/fire_event";
import type { DeviceRegistryEntry } from "../../data/device_registry";
import { getDeviceIntegrationLookup } from "../../data/device_registry";
import {
@ -51,7 +52,19 @@ export class HaDeviceSelector extends LitElement {
);
}
protected updated(changedProperties): void {
protected willUpdate(changedProperties: PropertyValues): void {
if (changedProperties.has("selector") && this.value !== undefined) {
if (this.selector.device?.multiple && !Array.isArray(this.value)) {
this.value = [this.value];
fireEvent(this, "value-changed", { value: this.value });
} else if (!this.selector.device?.multiple && Array.isArray(this.value)) {
this.value = this.value[0];
fireEvent(this, "value-changed", { value: this.value });
}
}
}
protected updated(changedProperties: PropertyValues): void {
super.updated(changedProperties);
if (
changedProperties.has("selector") &&

View File

@ -2,6 +2,7 @@ import { HassEntity } from "home-assistant-js-websocket";
import { html, LitElement, PropertyValues, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { ensureArray } from "../../common/array/ensure-array";
import { fireEvent } from "../../common/dom/fire_event";
import {
EntitySources,
fetchEntitySourcesWithCache,
@ -37,6 +38,18 @@ export class HaEntitySelector extends LitElement {
);
}
protected willUpdate(changedProperties: PropertyValues): void {
if (changedProperties.has("selector") && this.value !== undefined) {
if (this.selector.entity?.multiple && !Array.isArray(this.value)) {
this.value = [this.value];
fireEvent(this, "value-changed", { value: this.value });
} else if (!this.selector.entity?.multiple && Array.isArray(this.value)) {
this.value = this.value[0];
fireEvent(this, "value-changed", { value: this.value });
}
}
}
protected render() {
if (this._hasIntegration(this.selector) && !this._entitySources) {
return nothing;

View File

@ -0,0 +1,297 @@
import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit";
import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../common/dom/fire_event";
import { LocalizeFunc, LocalizeKeys } from "../../common/translations/localize";
import type { HomeAssistant } from "../../types";
import "../ha-alert";
import "../ha-form/ha-form";
const SELECTOR_DEFAULTS = {
number: {
min: 1,
max: 100,
},
};
const SELECTOR_SCHEMAS = {
action: [] as const,
area: [
{
name: "multiple",
selector: { boolean: {} },
},
] as const,
attribute: [
{
name: "entity_id",
selector: { entity: {} },
},
] as const,
boolean: [] as const,
color_temp: [
{
name: "unit",
selector: { select: { options: ["kelvin", "mired"] } },
},
{
name: "min",
selector: { number: { mode: "box" } },
},
{
name: "max",
selector: { number: { mode: "box" } },
},
] as const,
condition: [] as const,
date: [] as const,
datetime: [] as const,
device: [
{
name: "multiple",
selector: { boolean: {} },
},
] as const,
duration: [
{
name: "enable_day",
selector: { boolean: {} },
},
] as const,
entity: [
{
name: "multiple",
selector: { boolean: {} },
},
] as const,
icon: [] as const,
location: [] as const,
media: [] as const,
number: [
{
name: "min",
selector: { number: { mode: "box" } },
},
{
name: "max",
selector: { number: { mode: "box" } },
},
{
name: "step",
selector: { number: { mode: "box" } },
},
] as const,
object: [] as const,
color_rgb: [] as const,
select: [
{
name: "options",
selector: { object: {} },
},
{
name: "multiple",
selector: { boolean: {} },
},
] as const,
state: [
{
name: "entity_id",
selector: { entity: {} },
},
] as const,
target: [] as const,
template: [] as const,
text: [
{
name: "multiple",
selector: { boolean: {} },
},
{
name: "multiline",
selector: { boolean: {} },
},
{ name: "prefix", selector: { text: {} } },
{ name: "suffix", selector: { text: {} } },
] as const,
theme: [] as const,
time: [] as const,
};
@customElement("ha-selector-selector")
export class HaSelectorSelector extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public value?: any;
@property() public label?: string;
@property() public helper?: string;
@property({ type: Boolean, reflect: true }) public disabled = false;
@property({ type: Boolean, reflect: true }) public required = true;
private _yamlMode = false;
protected shouldUpdate(changedProps: PropertyValues) {
if (changedProps.size === 1 && changedProps.has("hass")) {
return false;
}
return true;
}
private _schema = memoizeOne(
(choice: string, localize: LocalizeFunc) =>
[
{
name: "type",
selector: {
select: {
mode: "dropdown",
required: true,
options: Object.keys(SELECTOR_SCHEMAS)
.concat("manual")
.map((key) => ({
label:
localize(
`ui.components.selectors.selector.types.${key}` as LocalizeKeys
) || key,
value: key,
})),
},
},
},
...(choice === "manual"
? ([
{
name: "manual",
selector: { object: {} },
},
] as const)
: []),
...(SELECTOR_SCHEMAS[choice]
? SELECTOR_SCHEMAS[choice].length > 1
? [
{
name: "",
type: "expandable",
title: localize("ui.components.selectors.selector.options"),
schema: SELECTOR_SCHEMAS[choice],
},
]
: SELECTOR_SCHEMAS[choice]
: []),
] as const
);
protected render() {
let data;
let type;
if (this._yamlMode) {
type = "manual";
data = { type, manual: this.value };
} else {
type = Object.keys(this.value)[0];
const value0 = Object.values(this.value)[0];
data = {
type,
...(typeof value0 === "object" ? value0 : []),
};
}
const schema = this._schema(type, this.hass.localize);
return html`<ha-card>
<div class="card-content">
<p>${this.label ? this.label : ""}</p>
<ha-form
.hass=${this.hass}
.data=${data}
.schema=${schema}
.computeLabel=${this._computeLabelCallback}
@value-changed=${this._valueChanged}
></ha-form></div
></ha-card>`;
}
private _valueChanged(ev: CustomEvent) {
ev.stopPropagation();
const value = ev.detail.value;
const type = value.type;
if (!type || typeof value !== "object" || Object.keys(value).length === 0) {
// not sure how this happens, but reject it
return;
}
const oldType = Object.keys(this.value)[0];
if (type === "manual" && !this._yamlMode) {
this._yamlMode = true;
this.requestUpdate();
return;
}
if (type === "manual" && value.manual === undefined) {
return;
}
if (type !== "manual") {
this._yamlMode = false;
}
delete value.type;
let newValue;
if (type === "manual") {
newValue = value.manual;
} else if (type === oldType) {
newValue = {
[type]: { ...(value.manual ? value.manual[oldType] : value) },
};
} else {
newValue = { [type]: { ...SELECTOR_DEFAULTS[type] } };
}
fireEvent(this, "value-changed", { value: newValue });
}
private _computeLabelCallback = (schema: any): string =>
this.hass.localize(
`ui.components.selectors.selector.${schema.name}` as LocalizeKeys
) || schema.name;
static get styles(): CSSResultGroup {
return css`
:host {
--expansion-panel-summary-padding: 0 16px;
}
ha-alert {
display: block;
margin-bottom: 16px;
}
ha-card {
margin: 0 0 16px 0;
}
ha-card.disabled {
pointer-events: none;
color: var(--disabled-text-color);
}
.card-content {
padding: 0px 16px 16px 16px;
}
.title {
font-size: 16px;
padding-top: 16px;
overflow: hidden;
text-overflow: ellipsis;
margin-bottom: 16px;
padding-left: 16px;
padding-right: 4px;
white-space: nowrap;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-selector-selector": HaSelectorSelector;
}
}

View File

@ -35,6 +35,7 @@ const LOAD_ELEMENTS = {
number: () => import("./ha-selector-number"),
object: () => import("./ha-selector-object"),
select: () => import("./ha-selector-select"),
selector: () => import("./ha-selector-selector"),
state: () => import("./ha-selector-state"),
backup_location: () => import("./ha-selector-backup-location"),
stt: () => import("./ha-selector-stt"),

View File

@ -42,6 +42,7 @@ export type Selector =
| ObjectSelector
| AssistPipelineSelector
| SelectSelector
| SelectorSelector
| StateSelector
| StatisticSelector
| StringSelector
@ -323,6 +324,11 @@ export interface SelectSelector {
} | null;
}
export interface SelectorSelector {
// eslint-disable-next-line @typescript-eslint/ban-types
selector: {} | null;
}
export interface StateSelector {
state: {
extra_options?: { label: string; value: any }[];

View File

@ -58,7 +58,7 @@ export default class HaScriptFieldRow extends LitElement {
},
{
name: "selector",
selector: { object: {} },
selector: { selector: {} },
},
{
name: "default",
@ -269,6 +269,14 @@ export default class HaScriptFieldRow extends LitElement {
this._errorKey = undefined;
this._uiError = undefined;
// If we render the default with an incompatible selector, it risks throwing an exception and not rendering.
// Clear the default when changing the selector type.
if (
Object.keys(this.field.selector)[0] !== Object.keys(value.selector)[0]
) {
delete value.default;
}
fireEvent(this, "value-changed", { value });
}

View File

@ -361,6 +361,36 @@
"upload_failed": "Upload failed",
"unknown_file": "Unknown file"
},
"selector": {
"options": "Selector Options",
"types": {
"action": "Action",
"area": "Area",
"attribute": "Attribute",
"boolean": "Boolean",
"color_temp": "Color temperature",
"condition": "Condition",
"date": "Date",
"datetime": "Date and Time",
"device": "Device",
"duration": "Duration",
"entity": "Entity",
"icon": "Icon",
"location": "Location",
"media": "Media",
"number": "Number",
"object": "Object",
"color_rgb": "RGB Color",
"select": "Select",
"state": "State",
"target": "Target",
"template": "Template",
"text": "Text",
"theme": "Theme",
"time": "Time",
"manual": "Manual Entry"
}
},
"text": {
"show_password": "Show password",
"hide_password": "Hide password"