mirror of
https://github.com/home-assistant/frontend.git
synced 2025-11-06 17:40:27 +00:00
Allow pasting YAML in automation/script editor directly (#24838)
* Allow pasting YAML in automation/script editor directly * Highlight pasted items * Change highlighting * Also reset in script editor * Show dialog when pasting to changed automation/script * Add shortcuts to shortcuts dialog * Use translated shortcut * Change timeout and clear on save * Fix imports * Process code review * Move paste logic to manual-*-editor
This commit is contained in:
committed by
GitHub
parent
29c11978b3
commit
ab415188ba
@@ -2,7 +2,18 @@ import "@material/mwc-button/mwc-button";
|
||||
import { mdiHelpCircle } from "@mdi/js";
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { load } from "js-yaml";
|
||||
import {
|
||||
any,
|
||||
array,
|
||||
assert,
|
||||
enums,
|
||||
number,
|
||||
object,
|
||||
optional,
|
||||
string,
|
||||
} from "superstruct";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { constructUrlCurrentPath } from "../../../common/url/construct-url";
|
||||
import {
|
||||
@@ -13,6 +24,7 @@ import "../../../components/ha-card";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-markdown";
|
||||
import type { Action, Fields, ScriptConfig } from "../../../data/script";
|
||||
import { MODES, normalizeScriptConfig } from "../../../data/script";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { documentationUrl } from "../../../util/documentation-url";
|
||||
@@ -20,6 +32,20 @@ import "../automation/action/ha-automation-action";
|
||||
import type HaAutomationAction from "../automation/action/ha-automation-action";
|
||||
import "./ha-script-fields";
|
||||
import type HaScriptFields from "./ha-script-fields";
|
||||
import { canOverrideAlphanumericInput } from "../../../common/dom/can-override-input";
|
||||
import { showToast } from "../../../util/toast";
|
||||
import { showPasteReplaceDialog } from "../automation/paste-replace-dialog/show-dialog-paste-replace";
|
||||
import { ensureArray } from "../../../common/array/ensure-array";
|
||||
|
||||
const scriptConfigStruct = object({
|
||||
alias: optional(string()),
|
||||
description: optional(string()),
|
||||
sequence: optional(array(any())),
|
||||
icon: optional(string()),
|
||||
mode: optional(enums([typeof MODES])),
|
||||
max: optional(number()),
|
||||
fields: optional(object()),
|
||||
});
|
||||
|
||||
@customElement("manual-script-editor")
|
||||
export class HaManualScriptEditor extends LitElement {
|
||||
@@ -33,11 +59,17 @@ export class HaManualScriptEditor extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public config!: ScriptConfig;
|
||||
|
||||
@property({ attribute: false }) public dirty = false;
|
||||
|
||||
@query("ha-script-fields")
|
||||
private _scriptFields?: HaScriptFields;
|
||||
|
||||
private _openFields = false;
|
||||
|
||||
@state() private _pastedConfig?: ScriptConfig;
|
||||
|
||||
private _previousConfig?: ScriptConfig;
|
||||
|
||||
public addFields() {
|
||||
this._openFields = true;
|
||||
fireEvent(this, "value-changed", {
|
||||
@@ -126,6 +158,7 @@ export class HaManualScriptEditor extends LitElement {
|
||||
role="region"
|
||||
aria-labelledby="fields-heading"
|
||||
.fields=${this.config.fields}
|
||||
.highlightedFields=${this._pastedConfig?.fields}
|
||||
@value-changed=${this._fieldsChanged}
|
||||
.hass=${this.hass}
|
||||
.disabled=${this.disabled}
|
||||
@@ -154,6 +187,7 @@ export class HaManualScriptEditor extends LitElement {
|
||||
role="region"
|
||||
aria-labelledby="sequence-heading"
|
||||
.actions=${this.config.sequence || []}
|
||||
.highlightedActions=${this._pastedConfig?.sequence || []}
|
||||
.path=${["sequence"]}
|
||||
@value-changed=${this._sequenceChanged}
|
||||
.hass=${this.hass}
|
||||
@@ -165,6 +199,7 @@ export class HaManualScriptEditor extends LitElement {
|
||||
|
||||
private _fieldsChanged(ev: CustomEvent): void {
|
||||
ev.stopPropagation();
|
||||
this.resetPastedConfig();
|
||||
fireEvent(this, "value-changed", {
|
||||
value: { ...this.config!, fields: ev.detail.value as Fields },
|
||||
});
|
||||
@@ -172,11 +207,165 @@ export class HaManualScriptEditor extends LitElement {
|
||||
|
||||
private _sequenceChanged(ev: CustomEvent): void {
|
||||
ev.stopPropagation();
|
||||
this.resetPastedConfig();
|
||||
fireEvent(this, "value-changed", {
|
||||
value: { ...this.config!, sequence: ev.detail.value as Action[] },
|
||||
});
|
||||
}
|
||||
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
window.addEventListener("paste", this._handlePaste);
|
||||
}
|
||||
|
||||
public disconnectedCallback() {
|
||||
window.removeEventListener("paste", this._handlePaste);
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
|
||||
private _handlePaste = async (ev: ClipboardEvent) => {
|
||||
// Ignore events on inputs/textareas
|
||||
if (!canOverrideAlphanumericInput(ev.composedPath())) {
|
||||
return;
|
||||
}
|
||||
|
||||
const paste = ev.clipboardData?.getData("text");
|
||||
if (!paste) {
|
||||
return;
|
||||
}
|
||||
|
||||
const loaded: any = load(paste);
|
||||
if (loaded) {
|
||||
let normalized: ScriptConfig | undefined;
|
||||
|
||||
try {
|
||||
normalized = normalizeScriptConfig(loaded);
|
||||
} catch (_err: any) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
assert(normalized, scriptConfigStruct);
|
||||
} catch (_err: any) {
|
||||
showToast(this, {
|
||||
message: this.hass.localize(
|
||||
"ui.panel.config.script.editor.paste_invalid_config"
|
||||
),
|
||||
duration: 4000,
|
||||
dismissable: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (normalized) {
|
||||
ev.preventDefault();
|
||||
|
||||
if (this.dirty) {
|
||||
const result = await new Promise<boolean>((resolve) => {
|
||||
showPasteReplaceDialog(this, {
|
||||
domain: "script",
|
||||
pastedConfig: normalized,
|
||||
onClose: () => resolve(false),
|
||||
onAppend: () => {
|
||||
this._appendToExistingConfig(normalized);
|
||||
resolve(false);
|
||||
},
|
||||
onReplace: () => resolve(true),
|
||||
});
|
||||
});
|
||||
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// replace the config completely
|
||||
this._replaceExistingConfig(normalized);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private _appendToExistingConfig(config: ScriptConfig) {
|
||||
// make a copy otherwise we will reference the original config
|
||||
this._previousConfig = { ...this.config } as ScriptConfig;
|
||||
this._pastedConfig = config;
|
||||
|
||||
if (!this.config) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ("fields" in config) {
|
||||
this.config.fields = {
|
||||
...this.config.fields,
|
||||
...config.fields,
|
||||
};
|
||||
}
|
||||
if ("sequence" in config) {
|
||||
this.config.sequence = ensureArray(this.config.sequence || []).concat(
|
||||
ensureArray(config.sequence)
|
||||
) as Action[];
|
||||
}
|
||||
|
||||
this._showPastedToastWithUndo();
|
||||
|
||||
fireEvent(this, "value-changed", {
|
||||
value: {
|
||||
...this.config,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private _replaceExistingConfig(config: ScriptConfig) {
|
||||
// make a copy otherwise we will reference the original config
|
||||
this._previousConfig = { ...this.config } as ScriptConfig;
|
||||
this._pastedConfig = config;
|
||||
this.config = config;
|
||||
|
||||
this._showPastedToastWithUndo();
|
||||
|
||||
fireEvent(this, "value-changed", {
|
||||
value: {
|
||||
...this.config,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private _showPastedToastWithUndo() {
|
||||
showToast(this, {
|
||||
message: this.hass.localize(
|
||||
"ui.panel.config.script.editor.paste_toast_message"
|
||||
),
|
||||
duration: 4000,
|
||||
action: {
|
||||
text: this.hass.localize("ui.common.undo"),
|
||||
action: () => {
|
||||
fireEvent(this, "value-changed", {
|
||||
value: {
|
||||
...this._previousConfig!,
|
||||
},
|
||||
});
|
||||
|
||||
this._previousConfig = undefined;
|
||||
this._pastedConfig = undefined;
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public resetPastedConfig() {
|
||||
if (!this._previousConfig) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._pastedConfig = undefined;
|
||||
this._previousConfig = undefined;
|
||||
|
||||
showToast(this, {
|
||||
message: "",
|
||||
duration: 0,
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
|
||||
Reference in New Issue
Block a user