mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-25 18:26:35 +00:00
ha-selector-selector implementation (#18421)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
This commit is contained in:
parent
a5a9bcafa7
commit
0c042079ed
@ -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") &&
|
||||
|
@ -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") &&
|
||||
|
@ -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;
|
||||
|
297
src/components/ha-selector/ha-selector-selector.ts
Normal file
297
src/components/ha-selector/ha-selector-selector.ts
Normal 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;
|
||||
}
|
||||
}
|
@ -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"),
|
||||
|
@ -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 }[];
|
||||
|
@ -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 });
|
||||
}
|
||||
|
||||
|
@ -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"
|
||||
|
Loading…
x
Reference in New Issue
Block a user