diff --git a/build-scripts/bundle.js b/build-scripts/bundle.js index 61dc06f74c..2205196266 100644 --- a/build-scripts/bundle.js +++ b/build-scripts/bundle.js @@ -33,6 +33,10 @@ module.exports.emptyPackages = ({ latestBuild, isHassioBuild }) => require.resolve( path.resolve(paths.polymer_dir, "src/components/ha-icon.ts") ), + isHassioBuild && + require.resolve( + path.resolve(paths.polymer_dir, "src/components/ha-icon-picker.ts") + ), ].filter(Boolean); module.exports.definedVars = ({ isProdBuild, latestBuild, defineOverlay }) => ({ diff --git a/gallery/src/pages/components/ha-form.ts b/gallery/src/pages/components/ha-form.ts index a49cf9f131..dac1320a37 100644 --- a/gallery/src/pages/components/ha-form.ts +++ b/gallery/src/pages/components/ha-form.ts @@ -61,6 +61,12 @@ const SCHEMAS: { select: { options: ["Everyone Home", "Some Home", "All gone"] }, }, }, + { + name: "icon", + selector: { + icon: {}, + }, + }, ], }, { diff --git a/gallery/src/pages/components/ha-selector.ts b/gallery/src/pages/components/ha-selector.ts index b4fc18f118..60890a3e4d 100644 --- a/gallery/src/pages/components/ha-selector.ts +++ b/gallery/src/pages/components/ha-selector.ts @@ -72,6 +72,7 @@ const SCHEMAS: { name: "Select", selector: { select: { options: ["Option 1", "Option 2"] } }, }, + icon: { name: "Icon", selector: { icon: {} } }, }, }, ]; diff --git a/src/components/ha-form/ha-form.ts b/src/components/ha-form/ha-form.ts index af97f2dffb..610d2d2de5 100644 --- a/src/components/ha-form/ha-form.ts +++ b/src/components/ha-form/ha-form.ts @@ -32,7 +32,12 @@ export class HaForm extends LitElement implements HaFormElement { @property() public computeError?: (schema: HaFormSchema, error) => string; - @property() public computeLabel?: (schema: HaFormSchema) => string; + @property() public computeLabel?: ( + schema: HaFormSchema, + data?: HaFormDataContainer + ) => string; + + @property() public computeHelper?: (schema: HaFormSchema) => string; public focus() { const root = this.shadowRoot?.querySelector(".root"); @@ -71,6 +76,7 @@ export class HaForm extends LitElement implements HaFormElement { : ""} ${this.schema.map((item) => { const error = getValue(this.error, item); + return html` ${error ? html` @@ -85,14 +91,15 @@ export class HaForm extends LitElement implements HaFormElement { .hass=${this.hass} .selector=${item.selector} .value=${getValue(this.data, item)} - .label=${this._computeLabel(item)} + .label=${this._computeLabel(item, this.data)} .disabled=${this.disabled} + .helper=${this._computeHelper(item)} .required=${item.required || false} >` : dynamicElement(`ha-form-${item.type}`, { schema: item, data: getValue(this.data, item), - label: this._computeLabel(item), + label: this._computeLabel(item, this.data), disabled: this.disabled, })} `; @@ -107,6 +114,7 @@ export class HaForm extends LitElement implements HaFormElement { root.addEventListener("value-changed", (ev) => { ev.stopPropagation(); const schema = (ev.target as HaFormElement).schema as HaFormSchema; + fireEvent(this, "value-changed", { value: { ...this.data, [schema.name]: ev.detail.value }, }); @@ -114,14 +122,18 @@ export class HaForm extends LitElement implements HaFormElement { return root; } - private _computeLabel(schema: HaFormSchema) { + private _computeLabel(schema: HaFormSchema, data: HaFormDataContainer) { return this.computeLabel - ? this.computeLabel(schema) + ? this.computeLabel(schema, data) : schema ? schema.name : ""; } + private _computeHelper(schema: HaFormSchema) { + return this.computeHelper ? this.computeHelper(schema) : ""; + } + private _computeError(error, schema: HaFormSchema | HaFormSchema[]) { return this.computeError ? this.computeError(error, schema) : error; } diff --git a/src/components/ha-selector/ha-selector-icon.ts b/src/components/ha-selector/ha-selector-icon.ts new file mode 100644 index 0000000000..046a612d5e --- /dev/null +++ b/src/components/ha-selector/ha-selector-icon.ts @@ -0,0 +1,39 @@ +import "../ha-icon-picker"; +import { html, LitElement } from "lit"; +import { customElement, property } from "lit/decorators"; +import { HomeAssistant } from "../../types"; +import { IconSelector } from "../../data/selector"; +import { fireEvent } from "../../common/dom/fire_event"; + +@customElement("ha-selector-icon") +export class HaIconSelector extends LitElement { + @property() public hass!: HomeAssistant; + + @property() public selector!: IconSelector; + + @property() public value?: string; + + @property() public label?: string; + + @property({ type: Boolean, reflect: true }) public disabled = false; + + protected render() { + return html` + + `; + } + + private _valueChanged(ev: CustomEvent) { + fireEvent(this, "value-changed", { value: ev.detail.value }); + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-selector-icon": HaIconSelector; + } +} diff --git a/src/components/ha-selector/ha-selector-select.ts b/src/components/ha-selector/ha-selector-select.ts index eaaca5a6ea..5c1cc3e91b 100644 --- a/src/components/ha-selector/ha-selector-select.ts +++ b/src/components/ha-selector/ha-selector-select.ts @@ -2,7 +2,7 @@ import { css, CSSResultGroup, html, LitElement } from "lit"; import { customElement, property } from "lit/decorators"; import { fireEvent } from "../../common/dom/fire_event"; import { stopPropagation } from "../../common/dom/stop_propagation"; -import { SelectSelector } from "../../data/selector"; +import { SelectOption, SelectSelector } from "../../data/selector"; import { HomeAssistant } from "../../types"; import "@material/mwc-select/mwc-select"; import "@material/mwc-list/mwc-list-item"; @@ -17,6 +17,8 @@ export class HaSelectSelector extends LitElement { @property() public label?: string; + @property() public helper?: string; + @property({ type: Boolean }) public disabled = false; protected render() { @@ -25,15 +27,17 @@ export class HaSelectSelector extends LitElement { naturalMenuWidth .label=${this.label} .value=${this.value} + .helper=${this.helper} .disabled=${this.disabled} @closed=${stopPropagation} @selected=${this._valueChanged} > - ${this.selector.select.options.map( - (item: string) => html` - ${item} - ` - )} + ${this.selector.select.options.map((item: string | SelectOption) => { + const value = typeof item === "object" ? item.value : item; + const label = typeof item === "object" ? item.label : item; + + return html`${label}`; + })} `; } diff --git a/src/components/ha-selector/ha-selector.ts b/src/components/ha-selector/ha-selector.ts index 145991228f..53aaa6a841 100644 --- a/src/components/ha-selector/ha-selector.ts +++ b/src/components/ha-selector/ha-selector.ts @@ -17,6 +17,7 @@ import "./ha-selector-select"; import "./ha-selector-target"; import "./ha-selector-text"; import "./ha-selector-time"; +import "./ha-selector-icon"; @customElement("ha-selector") export class HaSelector extends LitElement { @@ -28,6 +29,8 @@ export class HaSelector extends LitElement { @property() public label?: string; + @property() public helper?: string; + @property() public placeholder?: any; @property({ type: Boolean }) public disabled = false; @@ -52,6 +55,7 @@ export class HaSelector extends LitElement { placeholder: this.placeholder, disabled: this.disabled, required: this.required, + helper: this.helper, id: "selector", })} `; diff --git a/src/data/selector.ts b/src/data/selector.ts index 4ed8f9dd7b..458a84235a 100644 --- a/src/data/selector.ts +++ b/src/data/selector.ts @@ -12,7 +12,8 @@ export type Selector = | ActionSelector | StringSelector | ObjectSelector - | SelectSelector; + | SelectSelector + | IconSelector; export interface EntitySelector { entity: { @@ -133,8 +134,18 @@ export interface ObjectSelector { object: {}; } +export interface SelectOption { + value: string; + label: string; +} + export interface SelectSelector { select: { - options: string[]; + options: string[] | SelectOption[]; }; } + +export interface IconSelector { + // eslint-disable-next-line @typescript-eslint/ban-types + icon: {}; +} diff --git a/src/panels/config/script/ha-script-editor.ts b/src/panels/config/script/ha-script-editor.ts index 75f57c0c84..c4ce09a6b0 100644 --- a/src/panels/config/script/ha-script-editor.ts +++ b/src/panels/config/script/ha-script-editor.ts @@ -1,4 +1,4 @@ -import { ActionDetail } from "@material/mwc-list/mwc-list-foundation"; +import type { ActionDetail } from "@material/mwc-list/mwc-list-foundation"; import "@material/mwc-list/mwc-list-item"; import { mdiCheck, @@ -9,8 +9,6 @@ import { } from "@mdi/js"; import "@polymer/app-layout/app-header/app-header"; import "@polymer/app-layout/app-toolbar/app-toolbar"; -import "@polymer/paper-dropdown-menu/paper-dropdown-menu-light"; -import { PaperListboxElement } from "@polymer/paper-listbox"; import { css, CSSResultGroup, @@ -21,6 +19,7 @@ import { } from "lit"; import { property, state, query } from "lit/decorators"; import { classMap } from "lit/directives/class-map"; +import memoizeOne from "memoize-one"; import { computeObjectId } from "../../../common/entity/compute_object_id"; import { navigate } from "../../../common/navigate"; import { slugify } from "../../../common/string/slugify"; @@ -29,8 +28,12 @@ import { copyToClipboard } from "../../../common/util/copy-clipboard"; import "../../../components/ha-button-menu"; import "../../../components/ha-card"; import "../../../components/ha-fab"; +import type { + HaFormDataContainer, + HaFormSchema, + HaFormSelector, +} from "../../../components/ha-form/types"; import "../../../components/ha-icon-button"; -import "../../../components/ha-icon-picker"; import "../../../components/ha-svg-icon"; import "../../../components/ha-yaml-editor"; import type { HaYamlEditor } from "../../../components/ha-yaml-editor"; @@ -49,10 +52,9 @@ import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box import "../../../layouts/ha-app-layout"; import { KeyboardShortcutMixin } from "../../../mixins/keyboard-shortcut-mixin"; import { haStyle } from "../../../resources/styles"; -import { HomeAssistant, Route } from "../../../types"; +import type { HomeAssistant, Route } from "../../../types"; import { documentationUrl } from "../../../util/documentation-url"; import { showToast } from "../../../util/toast"; -import "../automation/action/ha-automation-action"; import { HaDeviceAction } from "../automation/action/types/ha-automation-action-device_id"; import "../ha-config-section"; import { configSections } from "../ha-panel-config"; @@ -83,7 +85,91 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) { @query("ha-yaml-editor", true) private _editor?: HaYamlEditor; + private _schema = memoizeOne( + (hasID: boolean, useBluePrint?: boolean, currentMode?: string) => { + const schema: HaFormSchema[] = [ + { + name: "alias", + selector: { + text: { + type: "text", + }, + }, + }, + { + name: "icon", + selector: { + icon: {}, + }, + }, + ]; + + if (!hasID) { + schema.push({ + name: "id", + selector: { + text: {}, + }, + }); + } + + if (!useBluePrint) { + schema.push({ + name: "mode", + selector: { + select: { + options: MODES.map((mode) => ({ + label: ` + ${ + this.hass.localize( + `ui.panel.config.script.editor.modes.${mode}` + ) || mode + } + `, + value: mode, + })), + }, + }, + }); + } + + if (currentMode && MODES_MAX.includes(currentMode)) { + schema.push({ + name: "max", + selector: { + text: { + type: "number", + }, + }, + }); + } + + return schema; + } + ); + protected render(): TemplateResult { + if (!this._config) { + return html``; + } + + const schema = this._schema( + !!this.scriptEntityId, + "use_blueprint" in this._config, + this._config.mode + ); + + const data = { + mode: MODES[0], + max: + this._config.mode && MODES_MAX.includes(this._config.mode) + ? 10 + : undefined, + icon: undefined, + ...this._config, + id: this._entityId, + }; + return html` ${this.hass.localize("ui.panel.config.automation.editor.edit_ui")} ${this._mode === "gui" - ? html` ` + ? html` + + ` : ``} ${this.hass.localize("ui.panel.config.automation.editor.edit_yaml")} ${this._mode === "yaml" - ? html` ` + ? html` + + ` : ``} @@ -173,16 +263,14 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) { ${this.narrow - ? html` ${this._config?.alias} ` + ? html`${this._config?.alias}` : ""}
- ${this._errors - ? html`
${this._errors}
` - : ""} + ${this._errors ? html`
${this._errors}
` : ""} ${this._mode === "gui" ? html`
- - - - - ${!this.scriptEntityId - ? html` - ` - : ""} - ${"use_blueprint" in this._config - ? "" - : html`

- ${this.hass.localize( - "ui.panel.config.script.editor.modes.description", - "documentation_link", - html`${this.hass.localize( - "ui.panel.config.script.editor.modes.documentation" - )}` - )} -

- - - ${MODES.map( - (mode) => html` - - ${this.hass.localize( - `ui.panel.config.script.editor.modes.${mode}` - ) || mode} - - ` - )} - - - ${this._config.mode && - MODES_MAX.includes(this._config.mode) - ? html` - ` - : html``} `} + >
${this.scriptEntityId ? html` @@ -328,47 +335,51 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) { ${"use_blueprint" in this._config - ? html`` - : html` - - ${this.hass.localize( - "ui.panel.config.script.editor.sequence" - )} - - -

- ${this.hass.localize( - "ui.panel.config.script.editor.sequence_sentence" - )} -

- - ${this.hass.localize( - "ui.panel.config.script.editor.link_available_actions" - )} - -
- -
`} + .narrow=${this.narrow} + .isWide=${this.isWide} + .config=${this._config} + @value-changed=${this._configChanged} + > + ` + : html` + + + ${this.hass.localize( + "ui.panel.config.script.editor.sequence" + )} + + +

+ ${this.hass.localize( + "ui.panel.config.script.editor.sequence_sentence" + )} +

+ + ${this.hass.localize( + "ui.panel.config.script.editor.link_available_actions" + )} + +
+ +
+ `} ` : ""}
@@ -495,7 +506,50 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) { } } - private async _runScript(ev) { + private _computeLabelCallback = ( + schema: HaFormSelector, + data: HaFormDataContainer + ): string => { + switch (schema.name) { + case "mode": + return this.hass.localize("ui.panel.config.script.editor.modes.label"); + case "max": + return this.hass.localize( + `ui.panel.config.script.editor.max.${data.mode}` + ); + default: + return this.hass.localize( + `ui.panel.config.script.editor.${schema.name}` + ); + } + }; + + private _computeHelperCallback = ( + schema: HaFormSelector + ): string | undefined => { + if (schema.name === "mode") { + return this.hass.localize( + "ui.panel.config.script.editor.modes.description", + "documentation_link", + html` + ${this.hass.localize( + "ui.panel.config.script.editor.modes.documentation" + )} + ` + ); + } + return undefined; + }; + + private async _runScript(ev: CustomEvent) { ev.stopPropagation(); await triggerScript(this.hass, this.scriptEntityId as string); showToast(this, { @@ -507,14 +561,7 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) { }); } - private _modeChanged(ev: CustomEvent) { - const mode = ((ev.target as PaperListboxElement)?.selectedItem as any) - ?.mode; - - if (mode === this._config!.mode) { - return; - } - + private _modeChanged(mode) { this._config = { ...this._config!, mode }; if (!MODES_MAX.includes(mode)) { delete this._config.max; @@ -522,23 +569,23 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) { this._dirty = true; } - private _aliasChanged(ev: CustomEvent) { + private _aliasChanged(alias: string) { if (this.scriptEntityId || this._entityId) { return; } - const aliasSlugify = slugify((ev.target as any).value); + const aliasSlugify = slugify(alias); let id = aliasSlugify; let i = 2; while (this.hass.states[`script.${id}`]) { id = `${aliasSlugify}_${i}`; i++; } + this._entityId = id; } - private _idChanged(ev: CustomEvent) { - ev.stopPropagation(); - this._entityId = (ev.target as any).value; + private _idChanged(id: string) { + this._entityId = id; if (this.hass.states[`script.${this._entityId}`]) { this._idError = true; } else { @@ -548,24 +595,39 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) { private _valueChanged(ev: CustomEvent) { ev.stopPropagation(); - const target = ev.target as any; - const name = target.name; - if (!name) { - return; - } - let newVal = ev.detail.value; - if (target.type === "number") { - newVal = Number(newVal); - } - if ((this._config![name] || "") === newVal) { - return; - } - if (!newVal) { - delete this._config![name]; - this._config = { ...this._config! }; - } else { - this._config = { ...this._config!, [name]: newVal }; + const values = ev.detail.value as any; + + for (const key of Object.keys(values)) { + if (key === "sequence") { + continue; + } + + const value = values[key]; + + if (value === this._config![key]) { + continue; + } + + switch (key) { + case "id": + this._idChanged(value); + return; + case "alias": + this._aliasChanged(value); + break; + case "mode": + this._modeChanged(value); + return; + } + + if (values[key] === undefined) { + delete this._config![key]; + this._config = { ...this._config! }; + } else { + this._config = { ...this._config!, [key]: value }; + } } + this._dirty = true; } @@ -575,7 +637,10 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) { } private _sequenceChanged(ev: CustomEvent): void { - this._config = { ...this._config!, sequence: ev.detail.value as Action[] }; + this._config = { + ...this._config!, + sequence: ev.detail.value as Action[], + }; this._errors = undefined; this._dirty = true; }