Script Editor to Ha Form (#11601)

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
pull/11688/head
Zack Barett 2022-02-14 11:27:29 -06:00 committed by GitHub
parent 2ac0ad1d98
commit 460b9003fc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 339 additions and 193 deletions

View File

@ -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 }) => ({

View File

@ -61,6 +61,12 @@ const SCHEMAS: {
select: { options: ["Everyone Home", "Some Home", "All gone"] },
},
},
{
name: "icon",
selector: {
icon: {},
},
},
],
},
{

View File

@ -72,6 +72,7 @@ const SCHEMAS: {
name: "Select",
selector: { select: { options: ["Option 1", "Option 2"] } },
},
icon: { name: "Icon", selector: { icon: {} } },
},
},
];

View File

@ -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}
></ha-selector>`
: 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;
}

View File

@ -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`
<ha-icon-picker
.label=${this.label}
.value=${this.value}
@value-changed=${this._valueChanged}
></ha-icon-picker>
`;
}
private _valueChanged(ev: CustomEvent) {
fireEvent(this, "value-changed", { value: ev.detail.value });
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-selector-icon": HaIconSelector;
}
}

View File

@ -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`
<mwc-list-item .value=${item}>${item}</mwc-list-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`<mwc-list-item .value=${value}>${label}</mwc-list-item>`;
})}
</mwc-select>`;
}

View File

@ -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",
})}
`;

View File

@ -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: {};
}

View File

@ -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`
<hass-tabs-subpage
.hass=${this.hass}
@ -113,11 +199,13 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
>
${this.hass.localize("ui.panel.config.automation.editor.edit_ui")}
${this._mode === "gui"
? html` <ha-svg-icon
? html`
<ha-svg-icon
class="selected_menu_item"
slot="graphic"
.path=${mdiCheck}
></ha-svg-icon>`
></ha-svg-icon>
`
: ``}
</mwc-list-item>
<mwc-list-item
@ -129,11 +217,13 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
>
${this.hass.localize("ui.panel.config.automation.editor.edit_yaml")}
${this._mode === "yaml"
? html` <ha-svg-icon
? html`
<ha-svg-icon
class="selected_menu_item"
slot="graphic"
.path=${mdiCheck}
></ha-svg-icon>`
></ha-svg-icon>
`
: ``}
</mwc-list-item>
@ -180,9 +270,7 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
"yaml-mode": this._mode === "yaml",
})}"
>
${this._errors
? html` <div class="errors">${this._errors}</div> `
: ""}
${this._errors ? html`<div class="errors">${this._errors}</div>` : ""}
${this._mode === "gui"
? html`
<div
@ -205,95 +293,14 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
</span>
<ha-card>
<div class="card-content">
<paper-input
.label=${this.hass.localize(
"ui.panel.config.script.editor.alias"
)}
name="alias"
.value=${this._config.alias}
<ha-form
.schema=${schema}
.data=${data}
.hass=${this.hass}
.computeLabel=${this._computeLabelCallback}
.computeHelper=${this._computeHelperCallback}
@value-changed=${this._valueChanged}
@change=${this._aliasChanged}
>
</paper-input>
<ha-icon-picker
.label=${this.hass.localize(
"ui.panel.config.script.editor.icon"
)}
.name=${"icon"}
.value=${this._config.icon}
@value-changed=${this._valueChanged}
>
</ha-icon-picker>
${!this.scriptEntityId
? html`<paper-input
.label=${this.hass.localize(
"ui.panel.config.script.editor.id"
)}
.errorMessage=${this.hass.localize(
"ui.panel.config.script.editor.id_already_exists"
)}
.invalid=${this._idError}
.value=${this._entityId}
@value-changed=${this._idChanged}
>
</paper-input>`
: ""}
${"use_blueprint" in this._config
? ""
: html`<p>
${this.hass.localize(
"ui.panel.config.script.editor.modes.description",
"documentation_link",
html`<a
href=${documentationUrl(
this.hass,
"/integrations/script/#script-modes"
)}
target="_blank"
rel="noreferrer"
>${this.hass.localize(
"ui.panel.config.script.editor.modes.documentation"
)}</a
>`
)}
</p>
<paper-dropdown-menu-light
.label=${this.hass.localize(
"ui.panel.config.script.editor.modes.label"
)}
no-animations
>
<paper-listbox
slot="dropdown-content"
.selected=${this._config.mode
? MODES.indexOf(this._config.mode)
: 0}
@iron-select=${this._modeChanged}
>
${MODES.map(
(mode) => html`
<paper-item .mode=${mode}>
${this.hass.localize(
`ui.panel.config.script.editor.modes.${mode}`
) || mode}
</paper-item>
`
)}
</paper-listbox>
</paper-dropdown-menu-light>
${this._config.mode &&
MODES_MAX.includes(this._config.mode)
? html`<paper-input
.label=${this.hass.localize(
`ui.panel.config.script.editor.max.${this._config.mode}`
)}
type="number"
name="max"
.value=${this._config.max || "10"}
@value-changed=${this._valueChanged}
>
</paper-input>`
: html``} `}
></ha-form>
</div>
${this.scriptEntityId
? html`
@ -328,14 +335,17 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
</ha-config-section>
${"use_blueprint" in this._config
? html`<blueprint-script-editor
? html`
<blueprint-script-editor
.hass=${this.hass}
.narrow=${this.narrow}
.isWide=${this.isWide}
.config=${this._config}
@value-changed=${this._configChanged}
></blueprint-script-editor>`
: html`<ha-config-section
></blueprint-script-editor>
`
: html`
<ha-config-section
vertical
.isWide=${this.isWide}
>
@ -368,7 +378,8 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
@value-changed=${this._sequenceChanged}
.hass=${this.hass}
></ha-automation-action>
</ha-config-section>`}
</ha-config-section>
`}
`
: ""}
</div>
@ -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`
<a
href=${documentationUrl(
this.hass,
"/integrations/script/#script-modes"
)}
target="_blank"
rel="noreferrer"
>${this.hass.localize(
"ui.panel.config.script.editor.modes.documentation"
)}</a
>
`
);
}
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) {
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;
}
let newVal = ev.detail.value;
if (target.type === "number") {
newVal = Number(newVal);
}
if ((this._config![name] || "") === newVal) {
return;
}
if (!newVal) {
delete this._config![name];
if (values[key] === undefined) {
delete this._config![key];
this._config = { ...this._config! };
} else {
this._config = { ...this._config!, [name]: newVal };
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;
}