mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-28 03:36:44 +00:00
Add Fields to Script UI (#18250)
This commit is contained in:
parent
7ce7cbb755
commit
80112bb662
@ -26,7 +26,7 @@ export class HaDateSelector extends LitElement {
|
|||||||
.label=${this.label}
|
.label=${this.label}
|
||||||
.locale=${this.hass.locale}
|
.locale=${this.hass.locale}
|
||||||
.disabled=${this.disabled}
|
.disabled=${this.disabled}
|
||||||
.value=${this.value}
|
.value=${typeof this.value === "string" ? this.value : undefined}
|
||||||
.required=${this.required}
|
.required=${this.required}
|
||||||
.helper=${this.helper}
|
.helper=${this.helper}
|
||||||
>
|
>
|
||||||
|
@ -30,7 +30,8 @@ export class HaDateTimeSelector extends LitElement {
|
|||||||
@query("ha-time-input") private _timeInput!: HaTimeInput;
|
@query("ha-time-input") private _timeInput!: HaTimeInput;
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
const values = this.value?.split(" ");
|
const values =
|
||||||
|
typeof this.value === "string" ? this.value.split(" ") : undefined;
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div class="input">
|
<div class="input">
|
||||||
|
@ -38,7 +38,10 @@ export class HaNumberSelector extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected render() {
|
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`
|
return html`
|
||||||
<div class="input">
|
<div class="input">
|
||||||
@ -67,11 +70,9 @@ export class HaNumberSelector extends LitElement {
|
|||||||
(this.selector.number?.step ?? 1) % 1 !== 0
|
(this.selector.number?.step ?? 1) % 1 !== 0
|
||||||
? "decimal"
|
? "decimal"
|
||||||
: "numeric"}
|
: "numeric"}
|
||||||
.label=${this.selector.number?.mode !== "box"
|
.label=${!isBox ? undefined : this.label}
|
||||||
? undefined
|
|
||||||
: this.label}
|
|
||||||
.placeholder=${this.placeholder}
|
.placeholder=${this.placeholder}
|
||||||
class=${classMap({ single: this.selector.number?.mode === "box" })}
|
class=${classMap({ single: isBox })}
|
||||||
.min=${this.selector.number?.min}
|
.min=${this.selector.number?.min}
|
||||||
.max=${this.selector.number?.max}
|
.max=${this.selector.number?.max}
|
||||||
.value=${this._valueStr ?? ""}
|
.value=${this._valueStr ?? ""}
|
||||||
@ -83,7 +84,7 @@ export class HaNumberSelector extends LitElement {
|
|||||||
.suffix=${this.selector.number?.unit_of_measurement}
|
.suffix=${this.selector.number?.unit_of_measurement}
|
||||||
type="number"
|
type="number"
|
||||||
autoValidate
|
autoValidate
|
||||||
?no-spinner=${this.selector.number?.mode !== "box"}
|
?no-spinner=${!isBox}
|
||||||
@input=${this._handleInputChange}
|
@input=${this._handleInputChange}
|
||||||
>
|
>
|
||||||
</ha-textfield>
|
</ha-textfield>
|
||||||
|
@ -23,7 +23,7 @@ export class HaTimeSelector extends LitElement {
|
|||||||
protected render() {
|
protected render() {
|
||||||
return html`
|
return html`
|
||||||
<ha-time-input
|
<ha-time-input
|
||||||
.value=${this.value}
|
.value=${typeof this.value === "string" ? this.value : undefined}
|
||||||
.locale=${this.hass.locale}
|
.locale=${this.hass.locale}
|
||||||
.disabled=${this.disabled}
|
.disabled=${this.disabled}
|
||||||
.required=${this.required}
|
.required=${this.required}
|
||||||
|
@ -93,12 +93,27 @@ export interface ManualScriptConfig {
|
|||||||
icon?: string;
|
icon?: string;
|
||||||
mode?: (typeof MODES)[number];
|
mode?: (typeof MODES)[number];
|
||||||
max?: number;
|
max?: number;
|
||||||
|
fields?: Fields;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BlueprintScriptConfig extends ManualScriptConfig {
|
export interface BlueprintScriptConfig extends ManualScriptConfig {
|
||||||
use_blueprint: { path: string; input?: BlueprintInput };
|
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 {
|
interface BaseAction {
|
||||||
alias?: string;
|
alias?: string;
|
||||||
continue_on_error?: boolean;
|
continue_on_error?: boolean;
|
||||||
|
@ -5,17 +5,18 @@ import {
|
|||||||
mdiContentSave,
|
mdiContentSave,
|
||||||
mdiDelete,
|
mdiDelete,
|
||||||
mdiDotsVertical,
|
mdiDotsVertical,
|
||||||
|
mdiFormTextbox,
|
||||||
mdiInformationOutline,
|
mdiInformationOutline,
|
||||||
mdiPlay,
|
mdiPlay,
|
||||||
mdiTransitConnection,
|
mdiTransitConnection,
|
||||||
} from "@mdi/js";
|
} from "@mdi/js";
|
||||||
import {
|
import {
|
||||||
css,
|
|
||||||
CSSResultGroup,
|
CSSResultGroup,
|
||||||
html,
|
|
||||||
LitElement,
|
LitElement,
|
||||||
PropertyValues,
|
PropertyValues,
|
||||||
TemplateResult,
|
TemplateResult,
|
||||||
|
css,
|
||||||
|
html,
|
||||||
nothing,
|
nothing,
|
||||||
} from "lit";
|
} from "lit";
|
||||||
import { property, query, state } from "lit/decorators";
|
import { property, query, state } from "lit/decorators";
|
||||||
@ -38,16 +39,18 @@ import "../../../components/ha-icon-button";
|
|||||||
import "../../../components/ha-svg-icon";
|
import "../../../components/ha-svg-icon";
|
||||||
import "../../../components/ha-yaml-editor";
|
import "../../../components/ha-yaml-editor";
|
||||||
import type { HaYamlEditor } from "../../../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 { EntityRegistryEntry } from "../../../data/entity_registry";
|
||||||
import {
|
import {
|
||||||
|
MODES,
|
||||||
|
MODES_MAX,
|
||||||
|
ScriptConfig,
|
||||||
deleteScript,
|
deleteScript,
|
||||||
fetchScriptFileConfig,
|
fetchScriptFileConfig,
|
||||||
getScriptEditorInitData,
|
getScriptEditorInitData,
|
||||||
getScriptStateConfig,
|
getScriptStateConfig,
|
||||||
isMaxMode,
|
isMaxMode,
|
||||||
MODES,
|
|
||||||
MODES_MAX,
|
|
||||||
ScriptConfig,
|
|
||||||
showScriptEditor,
|
showScriptEditor,
|
||||||
triggerScript,
|
triggerScript,
|
||||||
} from "../../../data/script";
|
} from "../../../data/script";
|
||||||
@ -60,8 +63,7 @@ import { documentationUrl } from "../../../util/documentation-url";
|
|||||||
import { showToast } from "../../../util/toast";
|
import { showToast } from "../../../util/toast";
|
||||||
import "./blueprint-script-editor";
|
import "./blueprint-script-editor";
|
||||||
import "./manual-script-editor";
|
import "./manual-script-editor";
|
||||||
import { UNAVAILABLE } from "../../../data/entity";
|
import type { HaManualScriptEditor } from "./manual-script-editor";
|
||||||
import { validateConfig } from "../../../data/config";
|
|
||||||
|
|
||||||
export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
|
export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
@ -92,7 +94,10 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
|
|||||||
|
|
||||||
@state() private _readOnly = false;
|
@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)[];
|
@state() private _validationErrors?: (string | TemplateResult)[];
|
||||||
|
|
||||||
@ -231,6 +236,19 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
|
|||||||
<ha-svg-icon slot="graphic" .path=${mdiPlay}></ha-svg-icon>
|
<ha-svg-icon slot="graphic" .path=${mdiPlay}></ha-svg-icon>
|
||||||
</mwc-list-item>
|
</mwc-list-item>
|
||||||
|
|
||||||
|
${!useBlueprint && !("fields" in this._config)
|
||||||
|
? html`
|
||||||
|
<mwc-list-item graphic="icon" @click=${this._addFields}>
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.script.editor.field.add_fields"
|
||||||
|
)}
|
||||||
|
<ha-svg-icon
|
||||||
|
slot="graphic"
|
||||||
|
.path=${mdiFormTextbox}
|
||||||
|
></ha-svg-icon>
|
||||||
|
</mwc-list-item>
|
||||||
|
`
|
||||||
|
: nothing}
|
||||||
${this.scriptId && this.narrow
|
${this.scriptId && this.narrow
|
||||||
? html`
|
? html`
|
||||||
<a href="/config/script/trace/${this.scriptId}">
|
<a href="/config/script/trace/${this.scriptId}">
|
||||||
@ -661,6 +679,14 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _addFields() {
|
||||||
|
if ("fields" in this._config!) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._manualEditor?.addFields();
|
||||||
|
this._dirty = true;
|
||||||
|
}
|
||||||
|
|
||||||
private _valueChanged(ev: CustomEvent) {
|
private _valueChanged(ev: CustomEvent) {
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
if (this._readOnly) {
|
if (this._readOnly) {
|
||||||
|
359
src/panels/config/script/ha-script-field-row.ts
Normal file
359
src/panels/config/script/ha-script-field-row.ts
Normal file
@ -0,0 +1,359 @@
|
|||||||
|
import { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
|
||||||
|
import "@material/mwc-list/mwc-list-item";
|
||||||
|
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";
|
||||||
|
import { fireEvent } from "../../../common/dom/fire_event";
|
||||||
|
import { slugify } from "../../../common/string/slugify";
|
||||||
|
import "../../../components/ha-alert";
|
||||||
|
import "../../../components/ha-button-menu";
|
||||||
|
import "../../../components/ha-card";
|
||||||
|
import "../../../components/ha-expansion-panel";
|
||||||
|
import type { SchemaUnion } from "../../../components/ha-form/types";
|
||||||
|
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";
|
||||||
|
|
||||||
|
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 excludeKeys: string[] = [];
|
||||||
|
|
||||||
|
@property() public field!: Field;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public disabled = false;
|
||||||
|
|
||||||
|
@state() private _uiError?: Record<string, string>;
|
||||||
|
|
||||||
|
@state() private _yamlError?: undefined | "yaml_error" | "key_not_unique";
|
||||||
|
|
||||||
|
@state() private _yamlMode: boolean = false;
|
||||||
|
|
||||||
|
private _errorKey?: string;
|
||||||
|
|
||||||
|
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 && typeof selector === "object" ? selector : {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "required",
|
||||||
|
selector: { boolean: {} },
|
||||||
|
},
|
||||||
|
] as const
|
||||||
|
);
|
||||||
|
|
||||||
|
protected render() {
|
||||||
|
const schema = this._schema(this.field.selector);
|
||||||
|
const data = { ...this.field, key: this._errorKey ?? this.key };
|
||||||
|
|
||||||
|
const yamlValue = { [this.key]: this.field };
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<ha-card outlined>
|
||||||
|
<ha-expansion-panel leftChevron>
|
||||||
|
<h3 slot="header">${this.key}</h3>
|
||||||
|
|
||||||
|
<slot name="icons" slot="icons"></slot>
|
||||||
|
<ha-button-menu
|
||||||
|
slot="icons"
|
||||||
|
@action=${this._handleAction}
|
||||||
|
@click=${preventDefault}
|
||||||
|
fixed
|
||||||
|
>
|
||||||
|
<ha-icon-button
|
||||||
|
slot="trigger"
|
||||||
|
.label=${this.hass.localize("ui.common.menu")}
|
||||||
|
.path=${mdiDotsVertical}
|
||||||
|
></ha-icon-button>
|
||||||
|
|
||||||
|
<mwc-list-item graphic="icon">
|
||||||
|
${this.hass.localize("ui.panel.config.automation.editor.edit_ui")}
|
||||||
|
${!this._yamlMode
|
||||||
|
? html` <ha-svg-icon
|
||||||
|
class="selected_menu_item"
|
||||||
|
slot="graphic"
|
||||||
|
.path=${mdiCheck}
|
||||||
|
></ha-svg-icon>`
|
||||||
|
: ``}
|
||||||
|
</mwc-list-item>
|
||||||
|
|
||||||
|
<mwc-list-item graphic="icon">
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.automation.editor.edit_yaml"
|
||||||
|
)}
|
||||||
|
${this._yamlMode
|
||||||
|
? html`<ha-svg-icon
|
||||||
|
class="selected_menu_item"
|
||||||
|
slot="graphic"
|
||||||
|
.path=${mdiCheck}
|
||||||
|
></ha-svg-icon>`
|
||||||
|
: ``}
|
||||||
|
</mwc-list-item>
|
||||||
|
|
||||||
|
<mwc-list-item
|
||||||
|
class="warning"
|
||||||
|
graphic="icon"
|
||||||
|
.disabled=${this.disabled}
|
||||||
|
>
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.automation.editor.actions.delete"
|
||||||
|
)}
|
||||||
|
<ha-svg-icon
|
||||||
|
class="warning"
|
||||||
|
slot="graphic"
|
||||||
|
.path=${mdiDelete}
|
||||||
|
></ha-svg-icon>
|
||||||
|
</mwc-list-item>
|
||||||
|
</ha-button-menu>
|
||||||
|
<div
|
||||||
|
class=${classMap({
|
||||||
|
"card-content": true,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
${this._yamlMode
|
||||||
|
? html` ${this._yamlError
|
||||||
|
? html`<ha-alert alert-type="error">
|
||||||
|
${this.hass.localize(
|
||||||
|
`ui.panel.config.script.editor.field.${this._yamlError}`
|
||||||
|
)}
|
||||||
|
</ha-alert>`
|
||||||
|
: nothing}
|
||||||
|
<ha-yaml-editor
|
||||||
|
.hass=${this.hass}
|
||||||
|
.defaultValue=${yamlValue}
|
||||||
|
@value-changed=${this._onYamlChange}
|
||||||
|
></ha-yaml-editor>`
|
||||||
|
: html`<ha-form
|
||||||
|
.schema=${schema}
|
||||||
|
.data=${data}
|
||||||
|
.error=${this._uiError}
|
||||||
|
.hass=${this.hass}
|
||||||
|
.disabled=${this.disabled}
|
||||||
|
.computeLabel=${this._computeLabelCallback}
|
||||||
|
.computeError=${this._computeError}
|
||||||
|
@value-changed=${this._valueChanged}
|
||||||
|
></ha-form>`}
|
||||||
|
</div>
|
||||||
|
</ha-expansion-panel>
|
||||||
|
</ha-card>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _handleAction(ev: CustomEvent<ActionDetail>) {
|
||||||
|
switch (ev.detail.index) {
|
||||||
|
case 0:
|
||||||
|
this._yamlMode = false;
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
this._yamlMode = true;
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
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 _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 _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)
|
||||||
|
: this.hass.localize("ui.panel.config.script.editor.field.field") ||
|
||||||
|
"field";
|
||||||
|
const regex = new RegExp(`^${slugifyName}(_\\d)?$`);
|
||||||
|
if (regex.test(this.key)) {
|
||||||
|
let key = !value.name
|
||||||
|
? this.hass.localize("ui.panel.config.script.editor.field.field") ||
|
||||||
|
"field"
|
||||||
|
: slugify(value.name);
|
||||||
|
if (this.excludeKeys.includes(key)) {
|
||||||
|
let uniqueKey = key;
|
||||||
|
let i = 2;
|
||||||
|
do {
|
||||||
|
uniqueKey = `${key}_${i}`;
|
||||||
|
i++;
|
||||||
|
} while (this.excludeKeys.includes(uniqueKey));
|
||||||
|
key = uniqueKey;
|
||||||
|
}
|
||||||
|
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
|
||||||
|
? {
|
||||||
|
key: "key_not_unique",
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
key: "key_not_null",
|
||||||
|
};
|
||||||
|
this._errorKey = value.key ?? "";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._errorKey = undefined;
|
||||||
|
this._uiError = undefined;
|
||||||
|
|
||||||
|
fireEvent(this, "value-changed", { value });
|
||||||
|
}
|
||||||
|
|
||||||
|
public expand() {
|
||||||
|
this.updateComplete.then(() => {
|
||||||
|
this.shadowRoot!.querySelector("ha-expansion-panel")!.expanded = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private _computeLabelCallback = (
|
||||||
|
schema: SchemaUnion<ReturnType<typeof this._schema>>
|
||||||
|
): 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;
|
||||||
|
}
|
||||||
|
}
|
165
src/panels/config/script/ha-script-fields.ts
Normal file
165
src/panels/config/script/ha-script-fields.ts
Normal file
@ -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`<div class="fields">
|
||||||
|
${Object.entries(this.fields).map(
|
||||||
|
([key, field]) => html`
|
||||||
|
<ha-script-field-row
|
||||||
|
.key=${key}
|
||||||
|
.excludeKeys=${Object.keys(this.fields).filter(
|
||||||
|
(k) => k !== key
|
||||||
|
)}
|
||||||
|
.field=${field}
|
||||||
|
.disabled=${this.disabled}
|
||||||
|
@value-changed=${this._fieldChanged}
|
||||||
|
.hass=${this.hass}
|
||||||
|
>
|
||||||
|
</ha-script-field-row>
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
</div> `
|
||||||
|
: nothing}
|
||||||
|
<ha-button
|
||||||
|
outlined
|
||||||
|
@click=${this._addField}
|
||||||
|
.disabled=${this.disabled}
|
||||||
|
.label=${this.hass.localize(
|
||||||
|
"ui.panel.config.script.editor.field.add_field"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<ha-svg-icon .path=${mdiPlus} slot="icon"></ha-svg-icon>
|
||||||
|
</ha-button>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
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<HaScriptFieldRow>(
|
||||||
|
"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;
|
||||||
|
}
|
||||||
|
}
|
@ -1,15 +1,17 @@
|
|||||||
import "@material/mwc-button/mwc-button";
|
import "@material/mwc-button/mwc-button";
|
||||||
import { mdiHelpCircle } from "@mdi/js";
|
import { mdiHelpCircle } from "@mdi/js";
|
||||||
import { css, CSSResultGroup, html, LitElement } from "lit";
|
import { CSSResultGroup, LitElement, css, html, nothing } from "lit";
|
||||||
import { customElement, property } from "lit/decorators";
|
import { customElement, property, query } from "lit/decorators";
|
||||||
import { fireEvent } from "../../../common/dom/fire_event";
|
import { fireEvent } from "../../../common/dom/fire_event";
|
||||||
import "../../../components/ha-card";
|
import "../../../components/ha-card";
|
||||||
import "../../../components/ha-icon-button";
|
import "../../../components/ha-icon-button";
|
||||||
import { Action, ScriptConfig } from "../../../data/script";
|
import { Action, Fields, ScriptConfig } from "../../../data/script";
|
||||||
import { haStyle } from "../../../resources/styles";
|
import { haStyle } from "../../../resources/styles";
|
||||||
import type { HomeAssistant } from "../../../types";
|
import type { HomeAssistant } from "../../../types";
|
||||||
import { documentationUrl } from "../../../util/documentation-url";
|
import { documentationUrl } from "../../../util/documentation-url";
|
||||||
import "../automation/action/ha-automation-action";
|
import "../automation/action/ha-automation-action";
|
||||||
|
import "./ha-script-fields";
|
||||||
|
import type HaScriptFields from "./ha-script-fields";
|
||||||
|
|
||||||
@customElement("manual-script-editor")
|
@customElement("manual-script-editor")
|
||||||
export class HaManualScriptEditor extends LitElement {
|
export class HaManualScriptEditor extends LitElement {
|
||||||
@ -23,6 +25,37 @@ export class HaManualScriptEditor extends LitElement {
|
|||||||
|
|
||||||
@property({ attribute: false }) public config!: ScriptConfig;
|
@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() {
|
protected render() {
|
||||||
return html`
|
return html`
|
||||||
${this.disabled
|
${this.disabled
|
||||||
@ -33,6 +66,40 @@ export class HaManualScriptEditor extends LitElement {
|
|||||||
</mwc-button>
|
</mwc-button>
|
||||||
</ha-alert>`
|
</ha-alert>`
|
||||||
: ""}
|
: ""}
|
||||||
|
${this.config.fields
|
||||||
|
? html`<div class="header">
|
||||||
|
<h2 id="fields-heading" class="name">
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.script.editor.field.fields"
|
||||||
|
)}
|
||||||
|
</h2>
|
||||||
|
<a
|
||||||
|
href=${documentationUrl(
|
||||||
|
this.hass,
|
||||||
|
"/integrations/script/#fields"
|
||||||
|
)}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
<ha-icon-button
|
||||||
|
.path=${mdiHelpCircle}
|
||||||
|
.label=${this.hass.localize(
|
||||||
|
"ui.panel.config.script.editor.field.link_help_fields"
|
||||||
|
)}
|
||||||
|
></ha-icon-button>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ha-script-fields
|
||||||
|
role="region"
|
||||||
|
aria-labelledby="fields-heading"
|
||||||
|
.fields=${this.config.fields}
|
||||||
|
@value-changed=${this._fieldsChanged}
|
||||||
|
.hass=${this.hass}
|
||||||
|
.disabled=${this.disabled}
|
||||||
|
></ha-script-fields>`
|
||||||
|
: nothing}
|
||||||
|
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<h2 id="sequence-heading" class="name">
|
<h2 id="sequence-heading" class="name">
|
||||||
${this.hass.localize("ui.panel.config.script.editor.sequence")}
|
${this.hass.localize("ui.panel.config.script.editor.sequence")}
|
||||||
@ -63,6 +130,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 {
|
private _sequenceChanged(ev: CustomEvent): void {
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
fireEvent(this, "value-changed", {
|
fireEvent(this, "value-changed", {
|
||||||
|
@ -311,7 +311,8 @@
|
|||||||
"successfully_deleted": "Successfully deleted",
|
"successfully_deleted": "Successfully deleted",
|
||||||
"error_required": "Required",
|
"error_required": "Required",
|
||||||
"copied": "Copied",
|
"copied": "Copied",
|
||||||
"copied_clipboard": "Copied to clipboard"
|
"copied_clipboard": "Copied to clipboard",
|
||||||
|
"name": "Name"
|
||||||
},
|
},
|
||||||
"components": {
|
"components": {
|
||||||
"selectors": {
|
"selectors": {
|
||||||
@ -3006,6 +3007,24 @@
|
|||||||
"unavailable": "Script is unavailable",
|
"unavailable": "Script is unavailable",
|
||||||
"migrate": "Migrate",
|
"migrate": "Migrate",
|
||||||
"duplicate": "[%key:ui::common::duplicate%]",
|
"duplicate": "[%key:ui::common::duplicate%]",
|
||||||
|
"field": {
|
||||||
|
"name": "[%key:ui::common::name%]",
|
||||||
|
"key": "Field variable key name",
|
||||||
|
"description": "Description",
|
||||||
|
"required": "Required",
|
||||||
|
"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.",
|
||||||
|
"fields": "Fields",
|
||||||
|
"link_help_fields": "Learn more about fields.",
|
||||||
|
"add_fields": "Add fields",
|
||||||
|
"add_field": "Add field",
|
||||||
|
"field": "field"
|
||||||
|
},
|
||||||
|
"field_delete_confirm_title": "Delete field?",
|
||||||
|
"field_delete_confirm_text": "[%key:ui::panel::config::automation::editor::triggers::delete_confirm_text%]",
|
||||||
"header": "Script: {name}",
|
"header": "Script: {name}",
|
||||||
"default_name": "New Script",
|
"default_name": "New Script",
|
||||||
"modes": {
|
"modes": {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user