From 1a91222b41dab871823be6e67dd7b856ccafb345 Mon Sep 17 00:00:00 2001 From: karwosts Date: Sun, 15 Oct 2023 21:25:31 -0700 Subject: [PATCH 1/9] Add Fields to Script UI --- src/data/script.ts | 15 ++ .../config/script/ha-script-field-row.ts | 221 ++++++++++++++++++ src/panels/config/script/ha-script-fields.ts | 153 ++++++++++++ .../config/script/manual-script-editor.ts | 38 ++- src/translations/en.json | 16 +- 5 files changed, 441 insertions(+), 2 deletions(-) create mode 100644 src/panels/config/script/ha-script-field-row.ts create mode 100644 src/panels/config/script/ha-script-fields.ts diff --git a/src/data/script.ts b/src/data/script.ts index 6fb4fbb596b3..f0b5e7151b51 100644 --- a/src/data/script.ts +++ b/src/data/script.ts @@ -93,12 +93,27 @@ export interface ManualScriptConfig { icon?: string; mode?: (typeof MODES)[number]; max?: number; + fields?: Fields; } export interface BlueprintScriptConfig extends ManualScriptConfig { use_blueprint: { path: string; input?: BlueprintInput }; } +export interface Fields { + [key: string]: Field; +} + +export interface Field { + name?: string; + description?: string; + advanced?: boolean; + required?: boolean; + example?: string; + default?: any; + selector?: any; +} + interface BaseAction { alias?: string; continue_on_error?: boolean; diff --git a/src/panels/config/script/ha-script-field-row.ts b/src/panels/config/script/ha-script-field-row.ts new file mode 100644 index 000000000000..0edee07cc64a --- /dev/null +++ b/src/panels/config/script/ha-script-field-row.ts @@ -0,0 +1,221 @@ +import { ActionDetail } from "@material/mwc-list/mwc-list-foundation"; +import "@material/mwc-list/mwc-list-item"; +import { mdiDelete, mdiDotsVertical } from "@mdi/js"; +import { CSSResultGroup, LitElement, css, html } from "lit"; +import { customElement, property } from "lit/decorators"; +import { classMap } from "lit/directives/class-map"; +import { fireEvent } from "../../../common/dom/fire_event"; +import "../../../components/ha-alert"; +import "../../../components/ha-button-menu"; +import "../../../components/ha-card"; +import "../../../components/ha-expansion-panel"; +import "../../../components/ha-icon-button"; +import { Field } from "../../../data/script"; +import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box"; +import { haStyle } from "../../../resources/styles"; +import type { HomeAssistant } from "../../../types"; +import type { SchemaUnion } from "../../../components/ha-form/types"; + +const SCHEMA = [ + { + name: "name", + selector: { text: {} }, + }, + { + name: "description", + selector: { text: {} }, + }, + { + name: "required", + selector: { boolean: {} }, + }, + { + name: "example", + selector: { text: {} }, + }, + { + name: "default", + selector: { object: {} }, + }, + { + name: "selector", + selector: { object: {} }, + }, +] as const; + +const preventDefault = (ev) => ev.preventDefault(); + +@customElement("ha-script-field-row") +export default class HaScriptFieldRow extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property() public key!: string; + + @property() public field!: Field; + + @property({ type: Boolean }) public disabled = false; + + protected render() { + return html` + + +

${this.key}

+ + + + + + ${this.hass.localize( + "ui.panel.config.automation.editor.actions.delete" + )} + + + +
+ +
+
+
+ `; + } + + private async _handleAction(ev: CustomEvent) { + switch (ev.detail.index) { + case 0: + this._onDelete(); + break; + } + } + + private _onDelete() { + showConfirmationDialog(this, { + title: this.hass.localize( + "ui.panel.config.script.editor.field_delete_confirm_title" + ), + text: this.hass.localize( + "ui.panel.config.script.editor.field_delete_confirm_text" + ), + dismissText: this.hass.localize("ui.common.cancel"), + confirmText: this.hass.localize("ui.common.delete"), + destructive: true, + confirm: () => { + fireEvent(this, "value-changed", { value: null }); + }, + }); + } + + private _valueChanged(ev: CustomEvent) { + ev.stopPropagation(); + const value = { ...ev.detail.value }; + fireEvent(this, "value-changed", { value }); + } + + public expand() { + this.updateComplete.then(() => { + this.shadowRoot!.querySelector("ha-expansion-panel")!.expanded = true; + }); + } + + private _computeLabelCallback = ( + schema: SchemaUnion + ): string => { + switch (schema.name) { + default: + return this.hass.localize( + `ui.panel.config.script.editor.field.${schema.name}` + ); + } + }; + + static get styles(): CSSResultGroup { + return [ + haStyle, + css` + ha-button-menu, + ha-icon-button { + --mdc-theme-text-primary-on-background: var(--primary-text-color); + } + .disabled { + opacity: 0.5; + pointer-events: none; + } + ha-expansion-panel { + --expansion-panel-summary-padding: 0 0 0 8px; + --expansion-panel-content-padding: 0; + } + h3 { + margin: 0; + font-size: inherit; + font-weight: inherit; + } + .action-icon { + display: none; + } + @media (min-width: 870px) { + .action-icon { + display: inline-block; + color: var(--secondary-text-color); + opacity: 0.9; + margin-right: 8px; + } + } + .card-content { + padding: 16px; + } + .disabled-bar { + background: var(--divider-color, #e0e0e0); + text-align: center; + border-top-right-radius: var(--ha-card-border-radius); + border-top-left-radius: var(--ha-card-border-radius); + } + + mwc-list-item[disabled] { + --mdc-theme-text-primary-on-background: var(--disabled-text-color); + } + .warning ul { + margin: 4px 0; + } + .selected_menu_item { + color: var(--primary-color); + } + li[role="separator"] { + border-bottom-color: var(--divider-color); + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-script-field-row": HaScriptFieldRow; + } +} diff --git a/src/panels/config/script/ha-script-fields.ts b/src/panels/config/script/ha-script-fields.ts new file mode 100644 index 000000000000..a26471cf1836 --- /dev/null +++ b/src/panels/config/script/ha-script-fields.ts @@ -0,0 +1,153 @@ +import "@material/mwc-button"; +import { mdiPlus } from "@mdi/js"; +import { + CSSResultGroup, + LitElement, + PropertyValues, + css, + html, + nothing, +} from "lit"; +import { customElement, property } from "lit/decorators"; +import { fireEvent } from "../../../common/dom/fire_event"; +import "../../../components/ha-button"; +import "../../../components/ha-button-menu"; +import "../../../components/ha-svg-icon"; +import { Fields } from "../../../data/script"; +import { sortableStyles } from "../../../resources/ha-sortable-style"; +import { HomeAssistant } from "../../../types"; +import { slugify } from "../../../common/string/slugify"; +import type HaScriptFieldRow from "./ha-script-field-row"; +import "./ha-script-field-row"; + +@customElement("ha-script-fields") +export default class HaScriptFields extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ type: Boolean }) public disabled = false; + + @property() public fields!: Fields; + + private _focusLastActionOnChange = false; + + protected render() { + return html` + ${this.fields + ? html`
+ ${Object.entries(this.fields).map( + ([key, field]) => html` + + + ` + )} +
` + : nothing} + + + + `; + } + + protected updated(changedProps: PropertyValues) { + super.updated(changedProps); + + if (changedProps.has("fields") && this._focusLastActionOnChange) { + this._focusLastActionOnChange = false; + + const row = this.shadowRoot!.querySelector( + "ha-script-field-row:last-of-type" + )!; + row.updateComplete.then(() => { + row.expand(); + row.scrollIntoView(); + row.focus(); + }); + } + } + + private _addField() { + const key = this._getUniqueKey("new_field", this.fields || {}); + const fields = { ...(this.fields || {}), [key]: {} }; + this._focusLastActionOnChange = true; + fireEvent(this, "value-changed", { value: fields }); + } + + private _fieldChanged(ev: CustomEvent) { + ev.stopPropagation(); + const newValue = ev.detail.value; + const key = (ev.target as any).key; + + const nameChanged = + newValue !== null && this.fields[key].name !== newValue.name; + let fields: Fields = {}; + + // If the field name is changed, change the key as well, but recreate the entire object + // to maintain the same insertion order. + if (nameChanged) { + const oldFields = { ...this.fields }; + delete oldFields[key]; + const newKey = this._getUniqueKey( + slugify(newValue.name || "unnamed_field"), + oldFields + ); + Object.entries(this.fields).forEach(([k, v]) => { + if (k === key) { + fields[newKey] = newValue; + } else fields[k] = v; + }); + } else { + fields = { ...this.fields }; + if (newValue === null) { + delete fields[key]; + } else { + fields[key] = newValue; + } + } + fireEvent(this, "value-changed", { value: fields }); + } + + private _getUniqueKey(base: string, fields: Fields): string { + let key = base; + if (base in fields) { + let i = 2; + do { + key = `${base}_${i}`; + i++; + } while (key in fields); + } + return key; + } + + static get styles(): CSSResultGroup { + return [ + sortableStyles, + css` + ha-script-field-row { + display: block; + margin-bottom: 16px; + scroll-margin-top: 48px; + } + ha-svg-icon { + height: 20px; + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-script-fields": HaScriptFields; + } +} diff --git a/src/panels/config/script/manual-script-editor.ts b/src/panels/config/script/manual-script-editor.ts index d7d7ced9cf17..eec94202c9ba 100644 --- a/src/panels/config/script/manual-script-editor.ts +++ b/src/panels/config/script/manual-script-editor.ts @@ -5,11 +5,12 @@ import { customElement, property } from "lit/decorators"; import { fireEvent } from "../../../common/dom/fire_event"; import "../../../components/ha-card"; import "../../../components/ha-icon-button"; -import { Action, ScriptConfig } from "../../../data/script"; +import { Action, Fields, ScriptConfig } from "../../../data/script"; import { haStyle } from "../../../resources/styles"; import type { HomeAssistant } from "../../../types"; import { documentationUrl } from "../../../util/documentation-url"; import "../automation/action/ha-automation-action"; +import "./ha-script-fields"; @customElement("manual-script-editor") export class HaManualScriptEditor extends LitElement { @@ -33,6 +34,34 @@ export class HaManualScriptEditor extends LitElement { ` : ""} + +
+

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

+ + + +
+ + +

${this.hass.localize("ui.panel.config.script.editor.sequence")} @@ -63,6 +92,13 @@ export class HaManualScriptEditor extends LitElement { `; } + private _fieldsChanged(ev: CustomEvent): void { + ev.stopPropagation(); + fireEvent(this, "value-changed", { + value: { ...this.config!, fields: ev.detail.value as Fields }, + }); + } + private _sequenceChanged(ev: CustomEvent): void { ev.stopPropagation(); fireEvent(this, "value-changed", { diff --git a/src/translations/en.json b/src/translations/en.json index cc1ef6e6b15f..02961a70e4e6 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -311,7 +311,8 @@ "successfully_deleted": "Successfully deleted", "error_required": "Required", "copied": "Copied", - "copied_clipboard": "Copied to clipboard" + "copied_clipboard": "Copied to clipboard", + "name": "Name" }, "components": { "selectors": { @@ -3006,6 +3007,19 @@ "unavailable": "Script is unavailable", "migrate": "Migrate", "duplicate": "[%key:ui::common::duplicate%]", + "fields": "Fields", + "link_help_fields": "Learn more about fields.", + "field": { + "name": "[%key:ui::common::name%]", + "description": "Description", + "required": "Required", + "example": "Example", + "default": "Default", + "selector": "Selector" + }, + "add_field": "Add field", + "field_delete_confirm_title": "Delete field?", + "field_delete_confirm_text": "[%key:ui::panel::config::automation::editor::triggers::delete_confirm_text%]", "header": "Script: {name}", "default_name": "New Script", "modes": { From daded7d6b0c1e85e58d9cfb8ef3b9a3e45f35318 Mon Sep 17 00:00:00 2001 From: karwosts Date: Tue, 17 Oct 2023 21:40:48 -0700 Subject: [PATCH 2/9] updates --- src/components/ha-selector/ha-selector.ts | 2 + .../config/script/ha-script-field-row.ts | 91 +++++++++++++------ src/panels/config/script/ha-script-fields.ts | 42 ++++----- src/translations/en.json | 1 + 4 files changed, 83 insertions(+), 53 deletions(-) diff --git a/src/components/ha-selector/ha-selector.ts b/src/components/ha-selector/ha-selector.ts index a69fab215789..40e7b3c12bb4 100644 --- a/src/components/ha-selector/ha-selector.ts +++ b/src/components/ha-selector/ha-selector.ts @@ -52,6 +52,8 @@ const LOAD_ELEMENTS = { ui_color: () => import("./ha-selector-ui-color"), }; +export const SelectorTypes = Object.keys(LOAD_ELEMENTS); + const LEGACY_UI_SELECTORS = new Set(["ui-action", "ui-color"]); @customElement("ha-selector") diff --git a/src/panels/config/script/ha-script-field-row.ts b/src/panels/config/script/ha-script-field-row.ts index 0edee07cc64a..bb5c8db07e34 100644 --- a/src/panels/config/script/ha-script-field-row.ts +++ b/src/panels/config/script/ha-script-field-row.ts @@ -4,6 +4,7 @@ import { mdiDelete, mdiDotsVertical } from "@mdi/js"; import { CSSResultGroup, LitElement, css, html } from "lit"; import { customElement, property } from "lit/decorators"; import { classMap } from "lit/directives/class-map"; +import memoizeOne from "memoize-one"; import { fireEvent } from "../../../common/dom/fire_event"; import "../../../components/ha-alert"; import "../../../components/ha-button-menu"; @@ -15,33 +16,7 @@ import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box import { haStyle } from "../../../resources/styles"; import type { HomeAssistant } from "../../../types"; import type { SchemaUnion } from "../../../components/ha-form/types"; - -const SCHEMA = [ - { - name: "name", - selector: { text: {} }, - }, - { - name: "description", - selector: { text: {} }, - }, - { - name: "required", - selector: { boolean: {} }, - }, - { - name: "example", - selector: { text: {} }, - }, - { - name: "default", - selector: { object: {} }, - }, - { - name: "selector", - selector: { object: {} }, - }, -] as const; +import { SelectorTypes } from "../../../components/ha-selector/ha-selector"; const preventDefault = (ev) => ev.preventDefault(); @@ -51,11 +26,55 @@ export default class HaScriptFieldRow extends LitElement { @property() public key!: string; + @property() public excludeKeys: string[] = []; + @property() public field!: Field; @property({ type: Boolean }) public disabled = false; + private _schema = memoizeOne( + (selector: any) => + [ + { + name: "name", + selector: { text: {} }, + }, + { + name: "key", + selector: { text: {} }, + }, + { + name: "description", + selector: { text: {} }, + }, + { + name: "selector", + selector: { object: {} }, + }, + { + name: "default", + selector: selector, + }, + { + name: "required", + selector: { boolean: {} }, + }, + ] as const + ); + protected render() { + let selector = { object: null }; + if (typeof this.field.selector === "object") { + const keys = Object.keys(this.field.selector); + if (keys.length === 1 && SelectorTypes.includes(keys[0])) { + selector = this.field.selector; + } + } + + const schema = this._schema(selector); + + const data = { ...this.field, key: this.key }; + return html` @@ -94,8 +113,8 @@ export default class HaScriptFieldRow extends LitElement { })} > + schema: SchemaUnion> ): string => { switch (schema.name) { default: diff --git a/src/panels/config/script/ha-script-fields.ts b/src/panels/config/script/ha-script-fields.ts index a26471cf1836..cebaa64e487a 100644 --- a/src/panels/config/script/ha-script-fields.ts +++ b/src/panels/config/script/ha-script-fields.ts @@ -16,7 +16,6 @@ import "../../../components/ha-svg-icon"; import { Fields } from "../../../data/script"; import { sortableStyles } from "../../../resources/ha-sortable-style"; import { HomeAssistant } from "../../../types"; -import { slugify } from "../../../common/string/slugify"; import type HaScriptFieldRow from "./ha-script-field-row"; import "./ha-script-field-row"; @@ -38,6 +37,9 @@ export default class HaScriptFields extends LitElement { ([key, field]) => html` k !== key + )} .field=${field} .disabled=${this.disabled} @value-changed=${this._fieldChanged} @@ -85,32 +87,26 @@ export default class HaScriptFields extends LitElement { private _fieldChanged(ev: CustomEvent) { ev.stopPropagation(); - const newValue = ev.detail.value; const key = (ev.target as any).key; - - const nameChanged = - newValue !== null && this.fields[key].name !== newValue.name; let fields: Fields = {}; - - // If the field name is changed, change the key as well, but recreate the entire object - // to maintain the same insertion order. - if (nameChanged) { - const oldFields = { ...this.fields }; - delete oldFields[key]; - const newKey = this._getUniqueKey( - slugify(newValue.name || "unnamed_field"), - oldFields - ); - Object.entries(this.fields).forEach(([k, v]) => { - if (k === key) { - fields[newKey] = newValue; - } else fields[k] = v; - }); - } else { + if (ev.detail.value === null) { fields = { ...this.fields }; - if (newValue === null) { - delete fields[key]; + delete fields[key]; + } else { + const newValue = { ...ev.detail.value }; + const newKey = newValue.key; + delete newValue.key; + const keyChanged = key !== newKey; + + // If key is changed, recreate the object to maintain the same insertion order. + if (keyChanged) { + Object.entries(this.fields).forEach(([k, v]) => { + if (k === key) { + fields[newKey] = newValue; + } else fields[k] = v; + }); } else { + fields = { ...this.fields }; fields[key] = newValue; } } diff --git a/src/translations/en.json b/src/translations/en.json index 02961a70e4e6..c8d9941b6749 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -3011,6 +3011,7 @@ "link_help_fields": "Learn more about fields.", "field": { "name": "[%key:ui::common::name%]", + "key": "Field variable key name", "description": "Description", "required": "Required", "example": "Example", From e90d5650ebda22f9ab9306d505766048a70fc1a4 Mon Sep 17 00:00:00 2001 From: karwosts Date: Wed, 18 Oct 2023 08:23:32 -0700 Subject: [PATCH 3/9] key error handling --- .../config/script/ha-script-field-row.ts | 24 +++++++++++++++++-- src/translations/en.json | 4 +++- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/src/panels/config/script/ha-script-field-row.ts b/src/panels/config/script/ha-script-field-row.ts index bb5c8db07e34..1d0a810eaeb2 100644 --- a/src/panels/config/script/ha-script-field-row.ts +++ b/src/panels/config/script/ha-script-field-row.ts @@ -2,7 +2,7 @@ import { ActionDetail } from "@material/mwc-list/mwc-list-foundation"; import "@material/mwc-list/mwc-list-item"; import { mdiDelete, mdiDotsVertical } from "@mdi/js"; import { CSSResultGroup, LitElement, css, html } from "lit"; -import { customElement, property } from "lit/decorators"; +import { customElement, property, state } from "lit/decorators"; import { classMap } from "lit/directives/class-map"; import memoizeOne from "memoize-one"; import { fireEvent } from "../../../common/dom/fire_event"; @@ -32,6 +32,10 @@ export default class HaScriptFieldRow extends LitElement { @property({ type: Boolean }) public disabled = false; + @state() private _error?: Record; + + private _errorKey?: string; + private _schema = memoizeOne( (selector: any) => [ @@ -73,7 +77,7 @@ export default class HaScriptFieldRow extends LitElement { const schema = this._schema(selector); - const data = { ...this.field, key: this.key }; + const data = { ...this.field, key: this._errorKey ?? this.key }; return html` @@ -115,9 +119,11 @@ export default class HaScriptFieldRow extends LitElement {

@@ -157,8 +163,18 @@ export default class HaScriptFieldRow extends LitElement { // Don't allow to set an empty key, or duplicate an existing key. if (!value.key || this.excludeKeys.includes(value.key)) { + this._error = value.key + ? { + key: "key_not_unique", + } + : { + key: "key_not_null", + }; + this._errorKey = value.key ?? ""; return; } + this._errorKey = undefined; + this._error = 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. @@ -186,6 +202,10 @@ export default class HaScriptFieldRow extends LitElement { } }; + private _computeError = (error: string) => + this.hass.localize(`ui.panel.config.script.editor.field.${error}` as any) || + error; + static get styles(): CSSResultGroup { return [ haStyle, diff --git a/src/translations/en.json b/src/translations/en.json index c8d9941b6749..dbfb6f81c0b3 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -3016,7 +3016,9 @@ "required": "Required", "example": "Example", "default": "Default", - "selector": "Selector" + "selector": "Selector", + "key_not_null": "The field key must not be empty.", + "key_not_unique": "The field key must not be the same value as another field." }, "add_field": "Add field", "field_delete_confirm_title": "Delete field?", From 5a3be74850cd62dd79fd40594f8b1f77ea15d4ed Mon Sep 17 00:00:00 2001 From: karwosts Date: Wed, 18 Oct 2023 13:50:15 -0700 Subject: [PATCH 4/9] Hide by default in overflow menu, yaml editor, error handling --- src/components/ha-selector/ha-selector.ts | 2 - src/panels/config/script/ha-script-editor.ts | 37 ++++++ .../config/script/ha-script-field-row.ts | 113 ++++++++++++++---- src/panels/config/script/ha-script-fields.ts | 9 +- .../config/script/manual-script-editor.ts | 58 ++++----- src/translations/en.json | 1 + 6 files changed, 164 insertions(+), 56 deletions(-) diff --git a/src/components/ha-selector/ha-selector.ts b/src/components/ha-selector/ha-selector.ts index 40e7b3c12bb4..a69fab215789 100644 --- a/src/components/ha-selector/ha-selector.ts +++ b/src/components/ha-selector/ha-selector.ts @@ -52,8 +52,6 @@ const LOAD_ELEMENTS = { ui_color: () => import("./ha-selector-ui-color"), }; -export const SelectorTypes = Object.keys(LOAD_ELEMENTS); - const LEGACY_UI_SELECTORS = new Set(["ui-action", "ui-color"]); @customElement("ha-selector") diff --git a/src/panels/config/script/ha-script-editor.ts b/src/panels/config/script/ha-script-editor.ts index 6d43660242b2..dd6f7a0592d9 100644 --- a/src/panels/config/script/ha-script-editor.ts +++ b/src/panels/config/script/ha-script-editor.ts @@ -8,6 +8,7 @@ import { mdiInformationOutline, mdiPlay, mdiTransitConnection, + mdiFormTextbox, } from "@mdi/js"; import { css, @@ -45,9 +46,11 @@ import { getScriptEditorInitData, getScriptStateConfig, isMaxMode, + Fields, MODES, MODES_MAX, ScriptConfig, + ManualScriptConfig, showScriptEditor, triggerScript, } from "../../../data/script"; @@ -231,6 +234,23 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) { + ${!useBlueprint && !("fields" in this._config) + ? html` + + ${this.hass.localize( + "ui.panel.config.script.editor.add_field" + )} + + + ` + : nothing} ${this.scriptId && this.narrow ? html` @@ -661,6 +681,23 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) { } } + private _addFields() { + if ("fields" in this._config!) { + return; + } + this._config = { + ...this._config, + fields: { + new_field: { + selector: { + text: null, + }, + }, + } as Fields, + } as ManualScriptConfig; + this._dirty = true; + } + private _valueChanged(ev: CustomEvent) { ev.stopPropagation(); if (this._readOnly) { diff --git a/src/panels/config/script/ha-script-field-row.ts b/src/panels/config/script/ha-script-field-row.ts index 1d0a810eaeb2..6fc29af67bb1 100644 --- a/src/panels/config/script/ha-script-field-row.ts +++ b/src/panels/config/script/ha-script-field-row.ts @@ -1,7 +1,7 @@ import { ActionDetail } from "@material/mwc-list/mwc-list-foundation"; import "@material/mwc-list/mwc-list-item"; -import { mdiDelete, mdiDotsVertical } from "@mdi/js"; -import { CSSResultGroup, LitElement, css, html } from "lit"; +import { mdiCheck, mdiDelete, mdiDotsVertical } from "@mdi/js"; +import { CSSResultGroup, LitElement, css, html, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; import { classMap } from "lit/directives/class-map"; import memoizeOne from "memoize-one"; @@ -11,12 +11,12 @@ import "../../../components/ha-button-menu"; import "../../../components/ha-card"; import "../../../components/ha-expansion-panel"; import "../../../components/ha-icon-button"; +import "../../../components/ha-yaml-editor"; import { Field } from "../../../data/script"; import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box"; import { haStyle } from "../../../resources/styles"; import type { HomeAssistant } from "../../../types"; import type { SchemaUnion } from "../../../components/ha-form/types"; -import { SelectorTypes } from "../../../components/ha-selector/ha-selector"; const preventDefault = (ev) => ev.preventDefault(); @@ -32,7 +32,11 @@ export default class HaScriptFieldRow extends LitElement { @property({ type: Boolean }) public disabled = false; - @state() private _error?: Record; + @state() private _uiError?: Record; + + @state() private _yamlError?: undefined | "yaml_error" | "key_not_unique"; + + @state() private _yamlMode: boolean = false; private _errorKey?: string; @@ -67,18 +71,11 @@ export default class HaScriptFieldRow extends LitElement { ); protected render() { - let selector = { object: null }; - if (typeof this.field.selector === "object") { - const keys = Object.keys(this.field.selector); - if (keys.length === 1 && SelectorTypes.includes(keys[0])) { - selector = this.field.selector; - } - } - - const schema = this._schema(selector); - + const schema = this._schema(this.field.selector); const data = { ...this.field, key: this._errorKey ?? this.key }; + const yamlValue = { [this.key]: this.field }; + return html` @@ -96,6 +93,31 @@ export default class HaScriptFieldRow extends LitElement { .label=${this.hass.localize("ui.common.menu")} .path=${mdiDotsVertical} > + + + ${this.hass.localize("ui.panel.config.automation.editor.edit_ui")} + ${!this._yamlMode + ? html` ` + : ``} + + + + ${this.hass.localize( + "ui.panel.config.automation.editor.edit_yaml" + )} + ${this._yamlMode + ? html`` + : ``} + + - + ${this._yamlMode + ? html` ${this._yamlError + ? html` + ${this.hass.localize( + `ui.panel.config.script.editor.field.${this._yamlError}` + )} + ` + : nothing} + ` + : html``} @@ -135,6 +170,12 @@ export default class HaScriptFieldRow extends LitElement { private async _handleAction(ev: CustomEvent) { switch (ev.detail.index) { case 0: + this._yamlMode = false; + break; + case 1: + this._yamlMode = true; + break; + case 2: this._onDelete(); break; } @@ -157,13 +198,33 @@ export default class HaScriptFieldRow extends LitElement { }); } + private _onYamlChange(ev: CustomEvent) { + ev.stopPropagation(); + const value = { ...ev.detail.value }; + + if (typeof value !== "object" || Object.keys(value).length !== 1) { + this._yamlError = "yaml_error"; + return; + } + const key = Object.keys(value)[0]; + if (this.excludeKeys.includes(key)) { + this._yamlError = "key_not_unique"; + return; + } + this._yamlError = undefined; + + const newValue = { ...value[key], key }; + + fireEvent(this, "value-changed", { value: newValue }); + } + private _valueChanged(ev: CustomEvent) { ev.stopPropagation(); const value = { ...ev.detail.value }; // Don't allow to set an empty key, or duplicate an existing key. if (!value.key || this.excludeKeys.includes(value.key)) { - this._error = value.key + this._uiError = value.key ? { key: "key_not_unique", } @@ -174,7 +235,7 @@ export default class HaScriptFieldRow extends LitElement { return; } this._errorKey = undefined; - this._error = 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. diff --git a/src/panels/config/script/ha-script-fields.ts b/src/panels/config/script/ha-script-fields.ts index cebaa64e487a..6289a9fe795c 100644 --- a/src/panels/config/script/ha-script-fields.ts +++ b/src/panels/config/script/ha-script-fields.ts @@ -80,7 +80,14 @@ export default class HaScriptFields extends LitElement { private _addField() { const key = this._getUniqueKey("new_field", this.fields || {}); - const fields = { ...(this.fields || {}), [key]: {} }; + const fields = { + ...(this.fields || {}), + [key]: { + selector: { + text: null, + }, + }, + }; this._focusLastActionOnChange = true; fireEvent(this, "value-changed", { value: fields }); } diff --git a/src/panels/config/script/manual-script-editor.ts b/src/panels/config/script/manual-script-editor.ts index eec94202c9ba..da9e101c73c9 100644 --- a/src/panels/config/script/manual-script-editor.ts +++ b/src/panels/config/script/manual-script-editor.ts @@ -1,6 +1,6 @@ import "@material/mwc-button/mwc-button"; import { mdiHelpCircle } from "@mdi/js"; -import { css, CSSResultGroup, html, LitElement } from "lit"; +import { css, CSSResultGroup, html, nothing, LitElement } from "lit"; import { customElement, property } from "lit/decorators"; import { fireEvent } from "../../../common/dom/fire_event"; import "../../../components/ha-card"; @@ -34,33 +34,37 @@ export class HaManualScriptEditor extends LitElement { ` : ""} + ${this.config.fields + ? html` -
-

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

- - - -
- - + ` + : nothing}

diff --git a/src/translations/en.json b/src/translations/en.json index dbfb6f81c0b3..a87b4d8c62a0 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -3017,6 +3017,7 @@ "example": "Example", "default": "Default", "selector": "Selector", + "yaml_error": "Field yaml has invalid format.", "key_not_null": "The field key must not be empty.", "key_not_unique": "The field key must not be the same value as another field." }, From 017408c81c5171ec8296e7351a1c9cac72663ce4 Mon Sep 17 00:00:00 2001 From: karwosts Date: Wed, 18 Oct 2023 13:52:38 -0700 Subject: [PATCH 5/9] remove a translation key --- src/translations/en.json | 1 - 1 file changed, 1 deletion(-) diff --git a/src/translations/en.json b/src/translations/en.json index a87b4d8c62a0..a05e3664a5f8 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -3014,7 +3014,6 @@ "key": "Field variable key name", "description": "Description", "required": "Required", - "example": "Example", "default": "Default", "selector": "Selector", "yaml_error": "Field yaml has invalid format.", From 271284ca58a6ce5a2172b365df57b14e9909c3e2 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 24 Oct 2023 22:59:35 +0200 Subject: [PATCH 6/9] Open field when adding, calculate key name from name --- .../ha-selector/ha-selector-number.ts | 5 ++- src/panels/config/script/ha-script-editor.ts | 43 +++++++------------ .../config/script/ha-script-field-row.ts | 26 ++++++++++- src/panels/config/script/ha-script-fields.ts | 25 ++++++----- .../config/script/manual-script-editor.ts | 37 ++++++++++++++-- src/translations/en.json | 1 + 6 files changed, 94 insertions(+), 43 deletions(-) diff --git a/src/components/ha-selector/ha-selector-number.ts b/src/components/ha-selector/ha-selector-number.ts index d7242e70759e..f5103713713d 100644 --- a/src/components/ha-selector/ha-selector-number.ts +++ b/src/components/ha-selector/ha-selector-number.ts @@ -38,7 +38,10 @@ export class HaNumberSelector extends LitElement { } protected render() { - const isBox = this.selector.number?.mode === "box"; + const isBox = + this.selector.number?.mode === "box" || + this.selector.number?.min === undefined || + this.selector.number?.max === undefined; return html`
diff --git a/src/panels/config/script/ha-script-editor.ts b/src/panels/config/script/ha-script-editor.ts index dd6f7a0592d9..697a5826c3fe 100644 --- a/src/panels/config/script/ha-script-editor.ts +++ b/src/panels/config/script/ha-script-editor.ts @@ -5,18 +5,18 @@ import { mdiContentSave, mdiDelete, mdiDotsVertical, + mdiFormTextbox, mdiInformationOutline, mdiPlay, mdiTransitConnection, - mdiFormTextbox, } from "@mdi/js"; import { - css, CSSResultGroup, - html, LitElement, PropertyValues, TemplateResult, + css, + html, nothing, } from "lit"; import { property, query, state } from "lit/decorators"; @@ -39,18 +39,18 @@ import "../../../components/ha-icon-button"; import "../../../components/ha-svg-icon"; import "../../../components/ha-yaml-editor"; import type { HaYamlEditor } from "../../../components/ha-yaml-editor"; +import { validateConfig } from "../../../data/config"; +import { UNAVAILABLE } from "../../../data/entity"; import { EntityRegistryEntry } from "../../../data/entity_registry"; import { + MODES, + MODES_MAX, + ScriptConfig, deleteScript, fetchScriptFileConfig, getScriptEditorInitData, getScriptStateConfig, isMaxMode, - Fields, - MODES, - MODES_MAX, - ScriptConfig, - ManualScriptConfig, showScriptEditor, triggerScript, } from "../../../data/script"; @@ -63,8 +63,7 @@ import { documentationUrl } from "../../../util/documentation-url"; import { showToast } from "../../../util/toast"; import "./blueprint-script-editor"; import "./manual-script-editor"; -import { UNAVAILABLE } from "../../../data/entity"; -import { validateConfig } from "../../../data/config"; +import type { HaManualScriptEditor } from "./manual-script-editor"; export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) { @property({ attribute: false }) public hass!: HomeAssistant; @@ -95,7 +94,10 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) { @state() private _readOnly = false; - @query("ha-yaml-editor", true) private _yamlEditor?: HaYamlEditor; + @query("ha-yaml-editor") private _yamlEditor?: HaYamlEditor; + + @query("manual-script-editor") + private _manualEditor?: HaManualScriptEditor; @state() private _validationErrors?: (string | TemplateResult)[]; @@ -236,13 +238,9 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) { ${!useBlueprint && !("fields" in this._config) ? html` - + ${this.hass.localize( - "ui.panel.config.script.editor.add_field" + "ui.panel.config.script.editor.add_fields" )} ev.preventDefault(); @@ -218,10 +219,33 @@ export default class HaScriptFieldRow extends LitElement { fireEvent(this, "value-changed", { value: newValue }); } + private _maybeSetKey(value): void { + const nameChanged = value.name !== this.field.name; + const keyChanged = value.key !== this.key; + if (!nameChanged || keyChanged) { + return; + } + const slugifyName = this.field.name ? slugify(this.field.name) : "field"; + const regex = new RegExp(`^${slugifyName}(_\\d)?$`); + if (regex.test(this.key)) { + let key = !value.name ? "field" : slugify(value.name); + if (this.excludeKeys.includes(key)) { + let i = 2; + do { + key = `${key}_${i}`; + i++; + } while (this.excludeKeys.includes(key)); + } + value.key = key; + } + } + private _valueChanged(ev: CustomEvent) { ev.stopPropagation(); const value = { ...ev.detail.value }; + this._maybeSetKey(value); + // Don't allow to set an empty key, or duplicate an existing key. if (!value.key || this.excludeKeys.includes(value.key)) { this._uiError = value.key diff --git a/src/panels/config/script/ha-script-fields.ts b/src/panels/config/script/ha-script-fields.ts index 6289a9fe795c..f3c6a1e3ffd4 100644 --- a/src/panels/config/script/ha-script-fields.ts +++ b/src/panels/config/script/ha-script-fields.ts @@ -16,8 +16,8 @@ import "../../../components/ha-svg-icon"; import { Fields } from "../../../data/script"; import { sortableStyles } from "../../../resources/ha-sortable-style"; import { HomeAssistant } from "../../../types"; -import type HaScriptFieldRow from "./ha-script-field-row"; import "./ha-script-field-row"; +import type HaScriptFieldRow from "./ha-script-field-row"; @customElement("ha-script-fields") export default class HaScriptFields extends LitElement { @@ -66,20 +66,23 @@ export default class HaScriptFields extends LitElement { if (changedProps.has("fields") && this._focusLastActionOnChange) { this._focusLastActionOnChange = false; - - const row = this.shadowRoot!.querySelector( - "ha-script-field-row:last-of-type" - )!; - row.updateComplete.then(() => { - row.expand(); - row.scrollIntoView(); - row.focus(); - }); + this.focusLastField(); } } + public focusLastField() { + const row = this.shadowRoot!.querySelector( + "ha-script-field-row:last-of-type" + )!; + row.updateComplete.then(() => { + row.expand(); + row.scrollIntoView(); + row.focus(); + }); + } + private _addField() { - const key = this._getUniqueKey("new_field", this.fields || {}); + const key = this._getUniqueKey("field", this.fields || {}); const fields = { ...(this.fields || {}), [key]: { diff --git a/src/panels/config/script/manual-script-editor.ts b/src/panels/config/script/manual-script-editor.ts index da9e101c73c9..2d8d59a5f49b 100644 --- a/src/panels/config/script/manual-script-editor.ts +++ b/src/panels/config/script/manual-script-editor.ts @@ -1,7 +1,7 @@ import "@material/mwc-button/mwc-button"; import { mdiHelpCircle } from "@mdi/js"; -import { css, CSSResultGroup, html, nothing, LitElement } from "lit"; -import { customElement, property } from "lit/decorators"; +import { CSSResultGroup, LitElement, css, html, nothing } from "lit"; +import { customElement, property, query } from "lit/decorators"; import { fireEvent } from "../../../common/dom/fire_event"; import "../../../components/ha-card"; import "../../../components/ha-icon-button"; @@ -11,6 +11,7 @@ import type { HomeAssistant } from "../../../types"; import { documentationUrl } from "../../../util/documentation-url"; import "../automation/action/ha-automation-action"; import "./ha-script-fields"; +import type HaScriptFields from "./ha-script-fields"; @customElement("manual-script-editor") export class HaManualScriptEditor extends LitElement { @@ -24,6 +25,36 @@ export class HaManualScriptEditor extends LitElement { @property({ attribute: false }) public config!: ScriptConfig; + @query("ha-script-fields") + private _scriptFields?: HaScriptFields; + + private _openFields = false; + + public addFields() { + this._openFields = true; + fireEvent(this, "value-changed", { + value: { + ...this.config, + fields: { + field: { + selector: { + text: null, + }, + }, + }, + }, + }); + } + + protected updated(changedProps) { + if (this._openFields && changedProps.has("config")) { + this._openFields = false; + this._scriptFields?.updateComplete.then( + () => this._scriptFields?.focusLastField() + ); + } + } + protected render() { return html` ${this.disabled @@ -35,7 +66,7 @@ export class HaManualScriptEditor extends LitElement { ` : ""} ${this.config.fields - ? html`
+ ? html`

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

diff --git a/src/translations/en.json b/src/translations/en.json index a05e3664a5f8..5d5426415101 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -3020,6 +3020,7 @@ "key_not_null": "The field key must not be empty.", "key_not_unique": "The field key must not be the same value as another field." }, + "add_fields": "Add fields", "add_field": "Add field", "field_delete_confirm_title": "Delete field?", "field_delete_confirm_text": "[%key:ui::panel::config::automation::editor::triggers::delete_confirm_text%]", From c5132f1cf9fc69e8d023d0c493cfe6f5b0d60716 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 24 Oct 2023 23:09:12 +0200 Subject: [PATCH 7/9] translate default key name, fix --- src/panels/config/script/ha-script-editor.ts | 2 +- src/panels/config/script/ha-script-field-row.ts | 16 ++++++++++++---- src/panels/config/script/ha-script-fields.ts | 10 ++++++++-- src/panels/config/script/manual-script-editor.ts | 9 ++++++--- src/translations/en.json | 11 ++++++----- 5 files changed, 33 insertions(+), 15 deletions(-) diff --git a/src/panels/config/script/ha-script-editor.ts b/src/panels/config/script/ha-script-editor.ts index 697a5826c3fe..38e866fb2b19 100644 --- a/src/panels/config/script/ha-script-editor.ts +++ b/src/panels/config/script/ha-script-editor.ts @@ -240,7 +240,7 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) { ? html` ${this.hass.localize( - "ui.panel.config.script.editor.add_fields" + "ui.panel.config.script.editor.field.add_fields" )} @@ -82,7 +84,11 @@ export default class HaScriptFields extends LitElement { } private _addField() { - const key = this._getUniqueKey("field", this.fields || {}); + const key = this._getUniqueKey( + this.hass.localize("ui.panel.config.script.editor.field.field") || + "field", + this.fields || {} + ); const fields = { ...(this.fields || {}), [key]: { diff --git a/src/panels/config/script/manual-script-editor.ts b/src/panels/config/script/manual-script-editor.ts index 2d8d59a5f49b..a648ec8bbcbc 100644 --- a/src/panels/config/script/manual-script-editor.ts +++ b/src/panels/config/script/manual-script-editor.ts @@ -36,7 +36,8 @@ export class HaManualScriptEditor extends LitElement { value: { ...this.config, fields: { - field: { + [this.hass.localize("ui.panel.config.script.editor.field.field") || + "field"]: { selector: { text: null, }, @@ -68,7 +69,9 @@ export class HaManualScriptEditor extends LitElement { ${this.config.fields ? html`

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

diff --git a/src/translations/en.json b/src/translations/en.json index 5d5426415101..007519048050 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -3007,8 +3007,6 @@ "unavailable": "Script is unavailable", "migrate": "Migrate", "duplicate": "[%key:ui::common::duplicate%]", - "fields": "Fields", - "link_help_fields": "Learn more about fields.", "field": { "name": "[%key:ui::common::name%]", "key": "Field variable key name", @@ -3018,10 +3016,13 @@ "selector": "Selector", "yaml_error": "Field yaml has invalid format.", "key_not_null": "The field key must not be empty.", - "key_not_unique": "The field key must not be the same value as another field." + "key_not_unique": "The field key must not be the same value as another field.", + "fields": "Fields", + "link_help_fields": "Learn more about fields.", + "add_fields": "Add fields", + "add_field": "Add field", + "field": "field" }, - "add_fields": "Add fields", - "add_field": "Add field", "field_delete_confirm_title": "Delete field?", "field_delete_confirm_text": "[%key:ui::panel::config::automation::editor::triggers::delete_confirm_text%]", "header": "Script: {name}", From 3e70c355f4607054d2d44ea4a22548610a2b5735 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 24 Oct 2023 23:55:11 +0200 Subject: [PATCH 8/9] have selectors check value types --- src/components/ha-selector/ha-selector-date.ts | 2 +- src/components/ha-selector/ha-selector-datetime.ts | 3 ++- src/components/ha-selector/ha-selector-time.ts | 2 +- src/panels/config/script/ha-script-field-row.ts | 8 +------- 4 files changed, 5 insertions(+), 10 deletions(-) diff --git a/src/components/ha-selector/ha-selector-date.ts b/src/components/ha-selector/ha-selector-date.ts index cfde72537e1d..8484e29a76ed 100644 --- a/src/components/ha-selector/ha-selector-date.ts +++ b/src/components/ha-selector/ha-selector-date.ts @@ -26,7 +26,7 @@ export class HaDateSelector extends LitElement { .label=${this.label} .locale=${this.hass.locale} .disabled=${this.disabled} - .value=${this.value} + .value=${typeof this.value === "string" ? this.value : undefined} .required=${this.required} .helper=${this.helper} > diff --git a/src/components/ha-selector/ha-selector-datetime.ts b/src/components/ha-selector/ha-selector-datetime.ts index 94d4ad9e041e..0ad02213f975 100644 --- a/src/components/ha-selector/ha-selector-datetime.ts +++ b/src/components/ha-selector/ha-selector-datetime.ts @@ -30,7 +30,8 @@ export class HaDateTimeSelector extends LitElement { @query("ha-time-input") private _timeInput!: HaTimeInput; protected render() { - const values = this.value?.split(" "); + const values = + typeof this.value === "string" ? this.value.split(" ") : undefined; return html`
diff --git a/src/components/ha-selector/ha-selector-time.ts b/src/components/ha-selector/ha-selector-time.ts index 40982435a2b8..ceb8d9fe93eb 100644 --- a/src/components/ha-selector/ha-selector-time.ts +++ b/src/components/ha-selector/ha-selector-time.ts @@ -23,7 +23,7 @@ export class HaTimeSelector extends LitElement { protected render() { return html` Date: Tue, 24 Oct 2023 17:26:14 -0700 Subject: [PATCH 9/9] fix some number selector rendering --- src/components/ha-selector/ha-selector-number.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/components/ha-selector/ha-selector-number.ts b/src/components/ha-selector/ha-selector-number.ts index f5103713713d..1e913e09b416 100644 --- a/src/components/ha-selector/ha-selector-number.ts +++ b/src/components/ha-selector/ha-selector-number.ts @@ -70,11 +70,9 @@ export class HaNumberSelector extends LitElement { (this.selector.number?.step ?? 1) % 1 !== 0 ? "decimal" : "numeric"} - .label=${this.selector.number?.mode !== "box" - ? undefined - : this.label} + .label=${!isBox ? undefined : this.label} .placeholder=${this.placeholder} - class=${classMap({ single: this.selector.number?.mode === "box" })} + class=${classMap({ single: isBox })} .min=${this.selector.number?.min} .max=${this.selector.number?.max} .value=${this._valueStr ?? ""} @@ -86,7 +84,7 @@ export class HaNumberSelector extends LitElement { .suffix=${this.selector.number?.unit_of_measurement} type="number" autoValidate - ?no-spinner=${this.selector.number?.mode !== "box"} + ?no-spinner=${!isBox} @input=${this._handleInputChange} >