>
+ ): string => {
+ switch (schema.name) {
+ default:
+ return this.hass.localize(
+ `ui.panel.config.script.editor.field.${schema.name}`
+ );
+ }
+ };
+
+ private _computeError = (error: string) =>
+ this.hass.localize(`ui.panel.config.script.editor.field.${error}` as any) ||
+ error;
+
+ 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..b50a28a4b23b
--- /dev/null
+++ b/src/panels/config/script/ha-script-fields.ts
@@ -0,0 +1,165 @@
+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 "./ha-script-field-row";
+import type HaScriptFieldRow from "./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`
+ k !== key
+ )}
+ .field=${field}
+ .disabled=${this.disabled}
+ @value-changed=${this._fieldChanged}
+ .hass=${this.hass}
+ >
+
+ `
+ )}
+
`
+ : nothing}
+
+
+
+ `;
+ }
+
+ protected updated(changedProps: PropertyValues) {
+ super.updated(changedProps);
+
+ if (changedProps.has("fields") && this._focusLastActionOnChange) {
+ this._focusLastActionOnChange = false;
+ 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(
+ this.hass.localize("ui.panel.config.script.editor.field.field") ||
+ "field",
+ this.fields || {}
+ );
+ const fields = {
+ ...(this.fields || {}),
+ [key]: {
+ selector: {
+ text: null,
+ },
+ },
+ };
+ this._focusLastActionOnChange = true;
+ fireEvent(this, "value-changed", { value: fields });
+ }
+
+ private _fieldChanged(ev: CustomEvent) {
+ ev.stopPropagation();
+ const key = (ev.target as any).key;
+ let fields: Fields = {};
+ if (ev.detail.value === null) {
+ fields = { ...this.fields };
+ 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;
+ }
+ }
+ 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..a648ec8bbcbc 100644
--- a/src/panels/config/script/manual-script-editor.ts
+++ b/src/panels/config/script/manual-script-editor.ts
@@ -1,15 +1,17 @@
import "@material/mwc-button/mwc-button";
import { mdiHelpCircle } from "@mdi/js";
-import { css, CSSResultGroup, html, 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";
-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";
+import type HaScriptFields from "./ha-script-fields";
@customElement("manual-script-editor")
export class HaManualScriptEditor extends LitElement {
@@ -23,6 +25,37 @@ 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: {
+ [this.hass.localize("ui.panel.config.script.editor.field.field") ||
+ "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
@@ -33,6 +66,40 @@ export class HaManualScriptEditor extends LitElement {
`
: ""}
+ ${this.config.fields
+ ? html`
+
+ `
+ : nothing}
+