mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-28 11:46:42 +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:
parent
29c11978b3
commit
ab415188ba
30
src/common/dom/can-override-input.ts
Normal file
30
src/common/dom/can-override-input.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
export const canOverrideAlphanumericInput = (composedPath: EventTarget[]) => {
|
||||||
|
if (composedPath.some((el) => "tagName" in el && el.tagName === "HA-MENU")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const el = composedPath[0] as Element;
|
||||||
|
|
||||||
|
if (el.tagName === "TEXTAREA") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (el.parentElement?.tagName === "HA-SELECT") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (el.tagName !== "INPUT") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ((el as HTMLInputElement).type) {
|
||||||
|
case "button":
|
||||||
|
case "checkbox":
|
||||||
|
case "hidden":
|
||||||
|
case "radio":
|
||||||
|
case "range":
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
@ -474,3 +474,16 @@ export const migrateAutomationAction = (
|
|||||||
|
|
||||||
return action;
|
return action;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const normalizeScriptConfig = (config: ScriptConfig): ScriptConfig => {
|
||||||
|
// Normalize data: ensure sequence is a list
|
||||||
|
// Happens when people copy paste their scripts into the config
|
||||||
|
const value = config.sequence;
|
||||||
|
if (value && !Array.isArray(value)) {
|
||||||
|
config.sequence = [value];
|
||||||
|
}
|
||||||
|
if (config.sequence) {
|
||||||
|
config.sequence = migrateAutomationAction(config.sequence);
|
||||||
|
}
|
||||||
|
return config;
|
||||||
|
};
|
||||||
|
@ -68,6 +68,16 @@ const _SHORTCUTS: Section[] = [
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: "ui.dialogs.shortcuts.automations.title",
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
type: "shortcut",
|
||||||
|
shortcut: [{ key: "ui.dialogs.shortcuts.shortcuts.ctrl_cmd" }, "V"],
|
||||||
|
key: "ui.dialogs.shortcuts.automations.paste",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: "ui.dialogs.shortcuts.charts.title",
|
key: "ui.dialogs.shortcuts.charts.title",
|
||||||
items: [
|
items: [
|
||||||
|
@ -674,6 +674,12 @@ export default class HaAutomationActionRow extends LitElement {
|
|||||||
ha-tooltip {
|
ha-tooltip {
|
||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
:host([highlight]) ha-card {
|
||||||
|
--shadow-default: var(--ha-card-box-shadow, 0 0 0 0 transparent);
|
||||||
|
--shadow-focus: 0 0 0 1px var(--state-inactive-color);
|
||||||
|
border-color: var(--state-inactive-color);
|
||||||
|
box-shadow: var(--shadow-default), var(--shadow-focus);
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@ -32,6 +32,8 @@ export default class HaAutomationAction extends LitElement {
|
|||||||
|
|
||||||
@property({ attribute: false }) public actions!: Action[];
|
@property({ attribute: false }) public actions!: Action[];
|
||||||
|
|
||||||
|
@property({ attribute: false }) public highlightedActions?: Action[];
|
||||||
|
|
||||||
@state() private _showReorder = false;
|
@state() private _showReorder = false;
|
||||||
|
|
||||||
@storage({
|
@storage({
|
||||||
@ -91,6 +93,7 @@ export default class HaAutomationAction extends LitElement {
|
|||||||
@move-up=${this._moveUp}
|
@move-up=${this._moveUp}
|
||||||
@value-changed=${this._actionChanged}
|
@value-changed=${this._actionChanged}
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
|
?highlight=${this.highlightedActions?.includes(action)}
|
||||||
>
|
>
|
||||||
${this._showReorder && !this.disabled
|
${this._showReorder && !this.disabled
|
||||||
? html`
|
? html`
|
||||||
|
@ -587,6 +587,12 @@ export default class HaAutomationConditionRow extends LitElement {
|
|||||||
ha-md-menu-item > ha-svg-icon {
|
ha-md-menu-item > ha-svg-icon {
|
||||||
--mdc-icon-size: 24px;
|
--mdc-icon-size: 24px;
|
||||||
}
|
}
|
||||||
|
:host([highlight]) ha-card {
|
||||||
|
--shadow-default: var(--ha-card-box-shadow, 0 0 0 0 transparent);
|
||||||
|
--shadow-focus: 0 0 0 1px var(--state-inactive-color);
|
||||||
|
border-color: var(--state-inactive-color);
|
||||||
|
box-shadow: var(--shadow-default), var(--shadow-focus);
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@ -30,6 +30,8 @@ export default class HaAutomationCondition extends LitElement {
|
|||||||
|
|
||||||
@property({ attribute: false }) public conditions!: Condition[];
|
@property({ attribute: false }) public conditions!: Condition[];
|
||||||
|
|
||||||
|
@property({ attribute: false }) public highlightedConditions?: Condition[];
|
||||||
|
|
||||||
@property({ type: Boolean }) public disabled = false;
|
@property({ type: Boolean }) public disabled = false;
|
||||||
|
|
||||||
@state() private _showReorder = false;
|
@state() private _showReorder = false;
|
||||||
@ -140,6 +142,7 @@ export default class HaAutomationCondition extends LitElement {
|
|||||||
@move-up=${this._moveUp}
|
@move-up=${this._moveUp}
|
||||||
@value-changed=${this._conditionChanged}
|
@value-changed=${this._conditionChanged}
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
|
?highlight=${this.highlightedConditions?.includes(cond)}
|
||||||
>
|
>
|
||||||
${this._showReorder && !this.disabled
|
${this._showReorder && !this.disabled
|
||||||
? html`
|
? html`
|
||||||
|
@ -21,7 +21,7 @@ import {
|
|||||||
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||||
import { css, html, LitElement, nothing } from "lit";
|
import { css, html, LitElement, nothing } from "lit";
|
||||||
import { property, state } from "lit/decorators";
|
import { property, query, state } from "lit/decorators";
|
||||||
import { classMap } from "lit/directives/class-map";
|
import { classMap } from "lit/directives/class-map";
|
||||||
import { transform } from "../../../common/decorators/transform";
|
import { transform } from "../../../common/decorators/transform";
|
||||||
import { fireEvent } from "../../../common/dom/fire_event";
|
import { fireEvent } from "../../../common/dom/fire_event";
|
||||||
@ -81,6 +81,7 @@ import {
|
|||||||
} from "./automation-save-dialog/show-dialog-automation-save";
|
} from "./automation-save-dialog/show-dialog-automation-save";
|
||||||
import "./blueprint-automation-editor";
|
import "./blueprint-automation-editor";
|
||||||
import "./manual-automation-editor";
|
import "./manual-automation-editor";
|
||||||
|
import type { HaManualAutomationEditor } from "./manual-automation-editor";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface HTMLElementTagNameMap {
|
interface HTMLElementTagNameMap {
|
||||||
@ -149,6 +150,9 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
|||||||
@consume({ context: fullEntitiesContext, subscribe: true })
|
@consume({ context: fullEntitiesContext, subscribe: true })
|
||||||
_entityRegistry!: EntityRegistryEntry[];
|
_entityRegistry!: EntityRegistryEntry[];
|
||||||
|
|
||||||
|
@query("manual-automation-editor")
|
||||||
|
private _manualEditor?: HaManualAutomationEditor;
|
||||||
|
|
||||||
private _configSubscriptions: Record<
|
private _configSubscriptions: Record<
|
||||||
string,
|
string,
|
||||||
(config?: AutomationConfig) => void
|
(config?: AutomationConfig) => void
|
||||||
@ -469,6 +473,7 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
|||||||
.stateObj=${stateObj}
|
.stateObj=${stateObj}
|
||||||
.config=${this._config}
|
.config=${this._config}
|
||||||
.disabled=${Boolean(this._readOnly)}
|
.disabled=${Boolean(this._readOnly)}
|
||||||
|
.dirty=${this._dirty}
|
||||||
@value-changed=${this._valueChanged}
|
@value-changed=${this._valueChanged}
|
||||||
></manual-automation-editor>
|
></manual-automation-editor>
|
||||||
`}
|
`}
|
||||||
@ -552,7 +557,6 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
|||||||
} as AutomationConfig;
|
} as AutomationConfig;
|
||||||
this._entityId = undefined;
|
this._entityId = undefined;
|
||||||
this._readOnly = false;
|
this._readOnly = false;
|
||||||
this._dirty = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (changedProps.has("entityId") && this.entityId) {
|
if (changedProps.has("entityId") && this.entityId) {
|
||||||
@ -952,6 +956,8 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this._manualEditor?.resetPastedConfig();
|
||||||
|
|
||||||
const id = this.automationId || String(Date.now());
|
const id = this.automationId || String(Date.now());
|
||||||
if (!this.automationId) {
|
if (!this.automationId) {
|
||||||
const saved = await this._promptAutomationAlias();
|
const saved = await this._promptAutomationAlias();
|
||||||
|
@ -3,17 +3,30 @@ import { mdiHelpCircle } from "@mdi/js";
|
|||||||
import type { HassEntity } from "home-assistant-js-websocket";
|
import type { HassEntity } from "home-assistant-js-websocket";
|
||||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||||
import { css, html, LitElement, nothing } from "lit";
|
import { css, html, LitElement, nothing } from "lit";
|
||||||
import { customElement, property } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
|
import { load } from "js-yaml";
|
||||||
|
import {
|
||||||
|
any,
|
||||||
|
array,
|
||||||
|
assert,
|
||||||
|
assign,
|
||||||
|
object,
|
||||||
|
optional,
|
||||||
|
string,
|
||||||
|
union,
|
||||||
|
} from "superstruct";
|
||||||
import { ensureArray } from "../../../common/array/ensure-array";
|
import { ensureArray } from "../../../common/array/ensure-array";
|
||||||
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 "../../../components/ha-markdown";
|
import "../../../components/ha-markdown";
|
||||||
import type {
|
import type {
|
||||||
|
AutomationConfig,
|
||||||
Condition,
|
Condition,
|
||||||
ManualAutomationConfig,
|
ManualAutomationConfig,
|
||||||
Trigger,
|
Trigger,
|
||||||
} from "../../../data/automation";
|
} from "../../../data/automation";
|
||||||
|
import { normalizeAutomationConfig } from "../../../data/automation";
|
||||||
import type { Action } from "../../../data/script";
|
import type { Action } from "../../../data/script";
|
||||||
import { haStyle } from "../../../resources/styles";
|
import { haStyle } from "../../../resources/styles";
|
||||||
import type { HomeAssistant } from "../../../types";
|
import type { HomeAssistant } from "../../../types";
|
||||||
@ -29,6 +42,26 @@ import {
|
|||||||
removeSearchParam,
|
removeSearchParam,
|
||||||
} from "../../../common/url/search-params";
|
} from "../../../common/url/search-params";
|
||||||
import { constructUrlCurrentPath } from "../../../common/url/construct-url";
|
import { constructUrlCurrentPath } from "../../../common/url/construct-url";
|
||||||
|
import { canOverrideAlphanumericInput } from "../../../common/dom/can-override-input";
|
||||||
|
import { showToast } from "../../../util/toast";
|
||||||
|
import { showPasteReplaceDialog } from "./paste-replace-dialog/show-dialog-paste-replace";
|
||||||
|
|
||||||
|
const baseConfigStruct = object({
|
||||||
|
alias: optional(string()),
|
||||||
|
description: optional(string()),
|
||||||
|
triggers: optional(array(any())),
|
||||||
|
conditions: optional(array(any())),
|
||||||
|
actions: optional(array(any())),
|
||||||
|
mode: optional(string()),
|
||||||
|
max_exceeded: optional(string()),
|
||||||
|
id: optional(string()),
|
||||||
|
});
|
||||||
|
|
||||||
|
const automationConfigStruct = union([
|
||||||
|
assign(baseConfigStruct, object({ triggers: array(any()) })),
|
||||||
|
assign(baseConfigStruct, object({ conditions: array(any()) })),
|
||||||
|
assign(baseConfigStruct, object({ actions: array(any()) })),
|
||||||
|
]);
|
||||||
|
|
||||||
@customElement("manual-automation-editor")
|
@customElement("manual-automation-editor")
|
||||||
export class HaManualAutomationEditor extends LitElement {
|
export class HaManualAutomationEditor extends LitElement {
|
||||||
@ -44,6 +77,22 @@ export class HaManualAutomationEditor extends LitElement {
|
|||||||
|
|
||||||
@property({ attribute: false }) public stateObj?: HassEntity;
|
@property({ attribute: false }) public stateObj?: HassEntity;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public dirty = false;
|
||||||
|
|
||||||
|
@state() private _pastedConfig?: ManualAutomationConfig;
|
||||||
|
|
||||||
|
private _previousConfig?: ManualAutomationConfig;
|
||||||
|
|
||||||
|
public connectedCallback() {
|
||||||
|
super.connectedCallback();
|
||||||
|
window.addEventListener("paste", this._handlePaste);
|
||||||
|
}
|
||||||
|
|
||||||
|
public disconnectedCallback() {
|
||||||
|
window.removeEventListener("paste", this._handlePaste);
|
||||||
|
super.disconnectedCallback();
|
||||||
|
}
|
||||||
|
|
||||||
protected firstUpdated(changedProps: PropertyValues): void {
|
protected firstUpdated(changedProps: PropertyValues): void {
|
||||||
super.firstUpdated(changedProps);
|
super.firstUpdated(changedProps);
|
||||||
const expanded = extractSearchParam("expanded");
|
const expanded = extractSearchParam("expanded");
|
||||||
@ -123,6 +172,7 @@ export class HaManualAutomationEditor extends LitElement {
|
|||||||
role="region"
|
role="region"
|
||||||
aria-labelledby="triggers-heading"
|
aria-labelledby="triggers-heading"
|
||||||
.triggers=${this.config.triggers || []}
|
.triggers=${this.config.triggers || []}
|
||||||
|
.highlightedTriggers=${this._pastedConfig?.triggers || []}
|
||||||
.path=${["triggers"]}
|
.path=${["triggers"]}
|
||||||
@value-changed=${this._triggerChanged}
|
@value-changed=${this._triggerChanged}
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
@ -164,6 +214,7 @@ export class HaManualAutomationEditor extends LitElement {
|
|||||||
role="region"
|
role="region"
|
||||||
aria-labelledby="conditions-heading"
|
aria-labelledby="conditions-heading"
|
||||||
.conditions=${this.config.conditions || []}
|
.conditions=${this.config.conditions || []}
|
||||||
|
.highlightedConditions=${this._pastedConfig?.conditions || []}
|
||||||
.path=${["conditions"]}
|
.path=${["conditions"]}
|
||||||
@value-changed=${this._conditionChanged}
|
@value-changed=${this._conditionChanged}
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
@ -203,6 +254,7 @@ export class HaManualAutomationEditor extends LitElement {
|
|||||||
role="region"
|
role="region"
|
||||||
aria-labelledby="actions-heading"
|
aria-labelledby="actions-heading"
|
||||||
.actions=${this.config.actions || []}
|
.actions=${this.config.actions || []}
|
||||||
|
.highlightedActions=${this._pastedConfig?.actions || []}
|
||||||
.path=${["actions"]}
|
.path=${["actions"]}
|
||||||
@value-changed=${this._actionChanged}
|
@value-changed=${this._actionChanged}
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
@ -214,6 +266,7 @@ export class HaManualAutomationEditor extends LitElement {
|
|||||||
|
|
||||||
private _triggerChanged(ev: CustomEvent): void {
|
private _triggerChanged(ev: CustomEvent): void {
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
|
this.resetPastedConfig();
|
||||||
fireEvent(this, "value-changed", {
|
fireEvent(this, "value-changed", {
|
||||||
value: { ...this.config!, triggers: ev.detail.value as Trigger[] },
|
value: { ...this.config!, triggers: ev.detail.value as Trigger[] },
|
||||||
});
|
});
|
||||||
@ -221,6 +274,7 @@ export class HaManualAutomationEditor extends LitElement {
|
|||||||
|
|
||||||
private _conditionChanged(ev: CustomEvent): void {
|
private _conditionChanged(ev: CustomEvent): void {
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
|
this.resetPastedConfig();
|
||||||
fireEvent(this, "value-changed", {
|
fireEvent(this, "value-changed", {
|
||||||
value: {
|
value: {
|
||||||
...this.config!,
|
...this.config!,
|
||||||
@ -231,6 +285,7 @@ export class HaManualAutomationEditor extends LitElement {
|
|||||||
|
|
||||||
private _actionChanged(ev: CustomEvent): void {
|
private _actionChanged(ev: CustomEvent): void {
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
|
this.resetPastedConfig();
|
||||||
fireEvent(this, "value-changed", {
|
fireEvent(this, "value-changed", {
|
||||||
value: { ...this.config!, actions: ev.detail.value as Action[] },
|
value: { ...this.config!, actions: ev.detail.value as Action[] },
|
||||||
});
|
});
|
||||||
@ -245,6 +300,152 @@ export class HaManualAutomationEditor extends LitElement {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _handlePaste = async (ev: ClipboardEvent) => {
|
||||||
|
if (!canOverrideAlphanumericInput(ev.composedPath())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const paste = ev.clipboardData?.getData("text");
|
||||||
|
if (!paste) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const loaded: any = load(paste);
|
||||||
|
if (loaded) {
|
||||||
|
let normalized: AutomationConfig | undefined;
|
||||||
|
|
||||||
|
try {
|
||||||
|
normalized = normalizeAutomationConfig(loaded);
|
||||||
|
} catch (_err: any) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
assert(normalized, automationConfigStruct);
|
||||||
|
} catch (_err: any) {
|
||||||
|
showToast(this, {
|
||||||
|
message: this.hass.localize(
|
||||||
|
"ui.panel.config.automation.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: "automation",
|
||||||
|
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: ManualAutomationConfig) {
|
||||||
|
// make a copy otherwise we will reference the original config
|
||||||
|
this._previousConfig = { ...this.config } as ManualAutomationConfig;
|
||||||
|
this._pastedConfig = config;
|
||||||
|
|
||||||
|
if (!this.config) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("triggers" in config) {
|
||||||
|
this.config.triggers = ensureArray(this.config.triggers || []).concat(
|
||||||
|
ensureArray(config.triggers)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if ("conditions" in config) {
|
||||||
|
this.config.conditions = ensureArray(this.config.conditions || []).concat(
|
||||||
|
ensureArray(config.conditions)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if ("actions" in config) {
|
||||||
|
this.config.actions = ensureArray(this.config.actions || []).concat(
|
||||||
|
ensureArray(config.actions)
|
||||||
|
) as Action[];
|
||||||
|
}
|
||||||
|
|
||||||
|
this._showPastedToastWithUndo();
|
||||||
|
|
||||||
|
fireEvent(this, "value-changed", {
|
||||||
|
value: {
|
||||||
|
...this.config!,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private _replaceExistingConfig(config: ManualAutomationConfig) {
|
||||||
|
// make a copy otherwise we will reference the original config
|
||||||
|
this._previousConfig = { ...this.config } as ManualAutomationConfig;
|
||||||
|
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.automation.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 {
|
static get styles(): CSSResultGroup {
|
||||||
return [
|
return [
|
||||||
haStyle,
|
haStyle,
|
||||||
|
@ -0,0 +1,101 @@
|
|||||||
|
import { customElement, property, state } from "lit/decorators";
|
||||||
|
import { css, type CSSResultGroup, html, LitElement, nothing } from "lit";
|
||||||
|
import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
|
||||||
|
import type { HomeAssistant } from "../../../../types";
|
||||||
|
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||||
|
import { haStyle, haStyleDialog } from "../../../../resources/styles";
|
||||||
|
import { createCloseHeading } from "../../../../components/ha-dialog";
|
||||||
|
import "../trigger/ha-automation-trigger-row";
|
||||||
|
import type { PasteReplaceDialogParams } from "./show-dialog-paste-replace";
|
||||||
|
|
||||||
|
@customElement("ha-dialog-paste-replace")
|
||||||
|
class DialogPasteReplace extends LitElement implements HassDialog {
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@state() private _opened = false;
|
||||||
|
|
||||||
|
@state() private _params!: PasteReplaceDialogParams;
|
||||||
|
|
||||||
|
public showDialog(params: PasteReplaceDialogParams): void {
|
||||||
|
this._opened = true;
|
||||||
|
this._params = params;
|
||||||
|
}
|
||||||
|
|
||||||
|
public closeDialog() {
|
||||||
|
if (this._opened) {
|
||||||
|
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||||
|
}
|
||||||
|
this._opened = false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
if (!this._opened) {
|
||||||
|
return nothing;
|
||||||
|
}
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<ha-dialog
|
||||||
|
open
|
||||||
|
@closed=${this.closeDialog}
|
||||||
|
.heading=${createCloseHeading(
|
||||||
|
this.hass,
|
||||||
|
this.hass.localize(
|
||||||
|
`ui.panel.config.${this._params.domain}.editor.paste_confirm.title`
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
${this.hass.localize(
|
||||||
|
`ui.panel.config.${this._params.domain}.editor.paste_confirm.text`
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<ha-yaml-editor
|
||||||
|
.hass=${this.hass}
|
||||||
|
.defaultValue=${this._params?.pastedConfig}
|
||||||
|
read-only
|
||||||
|
></ha-yaml-editor>
|
||||||
|
|
||||||
|
<div slot="primaryAction">
|
||||||
|
<ha-button @click=${this._handleAppend}>
|
||||||
|
${this.hass.localize("ui.common.append")}
|
||||||
|
</ha-button>
|
||||||
|
<ha-button @click=${this._handleReplace}>
|
||||||
|
${this.hass.localize("ui.common.replace")}
|
||||||
|
</ha-button>
|
||||||
|
</div>
|
||||||
|
</ha-dialog>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _handleReplace() {
|
||||||
|
this._params?.onReplace();
|
||||||
|
this.closeDialog();
|
||||||
|
}
|
||||||
|
|
||||||
|
private _handleAppend() {
|
||||||
|
this._params?.onAppend();
|
||||||
|
this.closeDialog();
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResultGroup {
|
||||||
|
return [
|
||||||
|
haStyle,
|
||||||
|
haStyleDialog,
|
||||||
|
css`
|
||||||
|
h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: inherit;
|
||||||
|
font-weight: inherit;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-dialog-paste-replace": DialogPasteReplace;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,28 @@
|
|||||||
|
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||||
|
import type { AutomationConfig } from "../../../../data/automation";
|
||||||
|
import type { ScriptConfig } from "../../../../data/script";
|
||||||
|
|
||||||
|
export const loadPasteReplaceDialog = () => import("./dialog-paste-replace");
|
||||||
|
|
||||||
|
interface BasePasteReplaceDialogParams<D, T> {
|
||||||
|
domain: D;
|
||||||
|
pastedConfig: T;
|
||||||
|
onClose: () => void;
|
||||||
|
onAppend: () => void;
|
||||||
|
onReplace: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PasteReplaceDialogParams =
|
||||||
|
| BasePasteReplaceDialogParams<"automation", AutomationConfig>
|
||||||
|
| BasePasteReplaceDialogParams<"script", ScriptConfig>;
|
||||||
|
|
||||||
|
export const showPasteReplaceDialog = (
|
||||||
|
element: HTMLElement,
|
||||||
|
params: PasteReplaceDialogParams
|
||||||
|
): void => {
|
||||||
|
fireEvent(element, "show-dialog", {
|
||||||
|
dialogTag: "ha-dialog-paste-replace",
|
||||||
|
dialogImport: loadPasteReplaceDialog,
|
||||||
|
dialogParams: params,
|
||||||
|
});
|
||||||
|
};
|
@ -738,6 +738,12 @@ export default class HaAutomationTriggerRow extends LitElement {
|
|||||||
ha-md-menu-item > ha-svg-icon {
|
ha-md-menu-item > ha-svg-icon {
|
||||||
--mdc-icon-size: 24px;
|
--mdc-icon-size: 24px;
|
||||||
}
|
}
|
||||||
|
:host([highlight]) ha-card {
|
||||||
|
--shadow-default: var(--ha-card-box-shadow, 0 0 0 0 transparent);
|
||||||
|
--shadow-focus: 0 0 0 1px var(--state-inactive-color);
|
||||||
|
border-color: var(--state-inactive-color);
|
||||||
|
box-shadow: var(--shadow-default), var(--shadow-focus);
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@ -32,6 +32,8 @@ export default class HaAutomationTrigger extends LitElement {
|
|||||||
|
|
||||||
@property({ attribute: false }) public triggers!: Trigger[];
|
@property({ attribute: false }) public triggers!: Trigger[];
|
||||||
|
|
||||||
|
@property({ attribute: false }) public highlightedTriggers?: Trigger[];
|
||||||
|
|
||||||
@property({ type: Boolean }) public disabled = false;
|
@property({ type: Boolean }) public disabled = false;
|
||||||
|
|
||||||
@state() private _showReorder = false;
|
@state() private _showReorder = false;
|
||||||
@ -92,6 +94,7 @@ export default class HaAutomationTrigger extends LitElement {
|
|||||||
@value-changed=${this._triggerChanged}
|
@value-changed=${this._triggerChanged}
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.disabled=${this.disabled}
|
.disabled=${this.disabled}
|
||||||
|
?highlight=${this.highlightedTriggers?.includes(trg)}
|
||||||
>
|
>
|
||||||
${this._showReorder && !this.disabled
|
${this._showReorder && !this.disabled
|
||||||
? html`
|
? html`
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { consume } from "@lit/context";
|
||||||
import "@material/mwc-button";
|
import "@material/mwc-button";
|
||||||
import {
|
import {
|
||||||
mdiCog,
|
mdiCog,
|
||||||
@ -20,21 +21,23 @@ import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
|||||||
import { LitElement, css, html, nothing } from "lit";
|
import { LitElement, css, html, nothing } from "lit";
|
||||||
import { property, query, state } from "lit/decorators";
|
import { property, query, state } from "lit/decorators";
|
||||||
import { classMap } from "lit/directives/class-map";
|
import { classMap } from "lit/directives/class-map";
|
||||||
import { consume } from "@lit/context";
|
|
||||||
import { fireEvent } from "../../../common/dom/fire_event";
|
import { fireEvent } from "../../../common/dom/fire_event";
|
||||||
import { navigate } from "../../../common/navigate";
|
import { navigate } from "../../../common/navigate";
|
||||||
import { slugify } from "../../../common/string/slugify";
|
import { slugify } from "../../../common/string/slugify";
|
||||||
import { computeRTL } from "../../../common/util/compute_rtl";
|
import { computeRTL } from "../../../common/util/compute_rtl";
|
||||||
import { afterNextRender } from "../../../common/util/render-status";
|
|
||||||
import { promiseTimeout } from "../../../common/util/promise-timeout";
|
import { promiseTimeout } from "../../../common/util/promise-timeout";
|
||||||
|
import { afterNextRender } from "../../../common/util/render-status";
|
||||||
import "../../../components/ha-button-menu";
|
import "../../../components/ha-button-menu";
|
||||||
import "../../../components/ha-fab";
|
import "../../../components/ha-fab";
|
||||||
|
|
||||||
|
import { transform } from "../../../common/decorators/transform";
|
||||||
import "../../../components/ha-icon-button";
|
import "../../../components/ha-icon-button";
|
||||||
import "../../../components/ha-list-item";
|
import "../../../components/ha-list-item";
|
||||||
import "../../../components/ha-svg-icon";
|
import "../../../components/ha-svg-icon";
|
||||||
import "../../../components/ha-yaml-editor";
|
import "../../../components/ha-yaml-editor";
|
||||||
|
import { substituteBlueprint } from "../../../data/blueprint";
|
||||||
import { validateConfig } from "../../../data/config";
|
import { validateConfig } from "../../../data/config";
|
||||||
|
import { fullEntitiesContext } from "../../../data/context";
|
||||||
import { UNAVAILABLE } from "../../../data/entity";
|
import { UNAVAILABLE } from "../../../data/entity";
|
||||||
import {
|
import {
|
||||||
type EntityRegistryEntry,
|
type EntityRegistryEntry,
|
||||||
@ -42,12 +45,12 @@ import {
|
|||||||
} from "../../../data/entity_registry";
|
} from "../../../data/entity_registry";
|
||||||
import type { BlueprintScriptConfig, ScriptConfig } from "../../../data/script";
|
import type { BlueprintScriptConfig, ScriptConfig } from "../../../data/script";
|
||||||
import {
|
import {
|
||||||
|
normalizeScriptConfig,
|
||||||
deleteScript,
|
deleteScript,
|
||||||
fetchScriptFileConfig,
|
fetchScriptFileConfig,
|
||||||
getScriptEditorInitData,
|
getScriptEditorInitData,
|
||||||
getScriptStateConfig,
|
getScriptStateConfig,
|
||||||
hasScriptFields,
|
hasScriptFields,
|
||||||
migrateAutomationAction,
|
|
||||||
showScriptEditor,
|
showScriptEditor,
|
||||||
triggerScript,
|
triggerScript,
|
||||||
} from "../../../data/script";
|
} from "../../../data/script";
|
||||||
@ -58,21 +61,18 @@ import {
|
|||||||
import { showMoreInfoDialog } from "../../../dialogs/more-info/show-ha-more-info-dialog";
|
import { showMoreInfoDialog } from "../../../dialogs/more-info/show-ha-more-info-dialog";
|
||||||
import "../../../layouts/hass-subpage";
|
import "../../../layouts/hass-subpage";
|
||||||
import { KeyboardShortcutMixin } from "../../../mixins/keyboard-shortcut-mixin";
|
import { KeyboardShortcutMixin } from "../../../mixins/keyboard-shortcut-mixin";
|
||||||
|
import { PreventUnsavedMixin } from "../../../mixins/prevent-unsaved-mixin";
|
||||||
|
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
|
||||||
import { haStyle } from "../../../resources/styles";
|
import { haStyle } from "../../../resources/styles";
|
||||||
import type { Entries, HomeAssistant, Route } from "../../../types";
|
import type { Entries, HomeAssistant, Route } from "../../../types";
|
||||||
import { showToast } from "../../../util/toast";
|
import { showToast } from "../../../util/toast";
|
||||||
import { showAutomationModeDialog } from "../automation/automation-mode-dialog/show-dialog-automation-mode";
|
import { showAutomationModeDialog } from "../automation/automation-mode-dialog/show-dialog-automation-mode";
|
||||||
import type { EntityRegistryUpdate } from "../automation/automation-save-dialog/show-dialog-automation-save";
|
import type { EntityRegistryUpdate } from "../automation/automation-save-dialog/show-dialog-automation-save";
|
||||||
import { showAutomationSaveDialog } from "../automation/automation-save-dialog/show-dialog-automation-save";
|
import { showAutomationSaveDialog } from "../automation/automation-save-dialog/show-dialog-automation-save";
|
||||||
|
import { showAssignCategoryDialog } from "../category/show-dialog-assign-category";
|
||||||
import "./blueprint-script-editor";
|
import "./blueprint-script-editor";
|
||||||
import "./manual-script-editor";
|
import "./manual-script-editor";
|
||||||
import type { HaManualScriptEditor } from "./manual-script-editor";
|
import type { HaManualScriptEditor } from "./manual-script-editor";
|
||||||
import { substituteBlueprint } from "../../../data/blueprint";
|
|
||||||
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
|
|
||||||
import { showAssignCategoryDialog } from "../category/show-dialog-assign-category";
|
|
||||||
import { PreventUnsavedMixin } from "../../../mixins/prevent-unsaved-mixin";
|
|
||||||
import { fullEntitiesContext } from "../../../data/context";
|
|
||||||
import { transform } from "../../../common/decorators/transform";
|
|
||||||
|
|
||||||
export class HaScriptEditor extends SubscribeMixin(
|
export class HaScriptEditor extends SubscribeMixin(
|
||||||
PreventUnsavedMixin(KeyboardShortcutMixin(LitElement))
|
PreventUnsavedMixin(KeyboardShortcutMixin(LitElement))
|
||||||
@ -427,6 +427,7 @@ export class HaScriptEditor extends SubscribeMixin(
|
|||||||
.isWide=${this.isWide}
|
.isWide=${this.isWide}
|
||||||
.config=${this._config}
|
.config=${this._config}
|
||||||
.disabled=${this._readOnly}
|
.disabled=${this._readOnly}
|
||||||
|
.dirty=${this._dirty}
|
||||||
@value-changed=${this._valueChanged}
|
@value-changed=${this._valueChanged}
|
||||||
></manual-script-editor>
|
></manual-script-editor>
|
||||||
`}
|
`}
|
||||||
@ -499,12 +500,11 @@ export class HaScriptEditor extends SubscribeMixin(
|
|||||||
...initData,
|
...initData,
|
||||||
} as ScriptConfig;
|
} as ScriptConfig;
|
||||||
this._readOnly = false;
|
this._readOnly = false;
|
||||||
this._dirty = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (changedProps.has("entityId") && this.entityId) {
|
if (changedProps.has("entityId") && this.entityId) {
|
||||||
getScriptStateConfig(this.hass, this.entityId).then((c) => {
|
getScriptStateConfig(this.hass, this.entityId).then((c) => {
|
||||||
this._config = this._normalizeConfig(c.config);
|
this._config = normalizeScriptConfig(c.config);
|
||||||
this._checkValidation();
|
this._checkValidation();
|
||||||
});
|
});
|
||||||
const regEntry = this.entityRegistry.find(
|
const regEntry = this.entityRegistry.find(
|
||||||
@ -543,25 +543,12 @@ export class HaScriptEditor extends SubscribeMixin(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _normalizeConfig(config: ScriptConfig): ScriptConfig {
|
|
||||||
// Normalize data: ensure sequence is a list
|
|
||||||
// Happens when people copy paste their scripts into the config
|
|
||||||
const value = config.sequence;
|
|
||||||
if (value && !Array.isArray(value)) {
|
|
||||||
config.sequence = [value];
|
|
||||||
}
|
|
||||||
if (config.sequence) {
|
|
||||||
config.sequence = migrateAutomationAction(config.sequence);
|
|
||||||
}
|
|
||||||
return config;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _loadConfig() {
|
private async _loadConfig() {
|
||||||
fetchScriptFileConfig(this.hass, this.scriptId!).then(
|
fetchScriptFileConfig(this.hass, this.scriptId!).then(
|
||||||
(config) => {
|
(config) => {
|
||||||
this._dirty = false;
|
this._dirty = false;
|
||||||
this._readOnly = false;
|
this._readOnly = false;
|
||||||
this._config = this._normalizeConfig(config);
|
this._config = normalizeScriptConfig(config);
|
||||||
const entity = this.entityRegistry.find(
|
const entity = this.entityRegistry.find(
|
||||||
(ent) => ent.platform === "script" && ent.unique_id === this.scriptId
|
(ent) => ent.platform === "script" && ent.unique_id === this.scriptId
|
||||||
);
|
);
|
||||||
@ -770,7 +757,7 @@ export class HaScriptEditor extends SubscribeMixin(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const newConfig = {
|
const newConfig = {
|
||||||
...this._normalizeConfig(result.substituted_config),
|
...normalizeScriptConfig(result.substituted_config),
|
||||||
alias: config.alias,
|
alias: config.alias,
|
||||||
description: config.description,
|
description: config.description,
|
||||||
};
|
};
|
||||||
@ -913,6 +900,8 @@ export class HaScriptEditor extends SubscribeMixin(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this._manualEditor?.resetPastedConfig();
|
||||||
|
|
||||||
if (!this.scriptId) {
|
if (!this.scriptId) {
|
||||||
const saved = await this._promptScriptAlias();
|
const saved = await this._promptScriptAlias();
|
||||||
if (!saved) {
|
if (!saved) {
|
||||||
|
@ -353,6 +353,12 @@ export default class HaScriptFieldRow extends LitElement {
|
|||||||
li[role="separator"] {
|
li[role="separator"] {
|
||||||
border-bottom-color: var(--divider-color);
|
border-bottom-color: var(--divider-color);
|
||||||
}
|
}
|
||||||
|
:host([highlight]) ha-card {
|
||||||
|
--shadow-default: var(--ha-card-box-shadow, 0 0 0 0 transparent);
|
||||||
|
--shadow-focus: 0 0 0 1px var(--state-inactive-color);
|
||||||
|
border-color: var(--state-inactive-color);
|
||||||
|
box-shadow: var(--shadow-default), var(--shadow-focus);
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@ -20,6 +20,8 @@ export default class HaScriptFields extends LitElement {
|
|||||||
|
|
||||||
@property({ attribute: false }) public fields!: Fields;
|
@property({ attribute: false }) public fields!: Fields;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public highlightedFields?: Fields;
|
||||||
|
|
||||||
private _focusLastActionOnChange = false;
|
private _focusLastActionOnChange = false;
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
@ -37,6 +39,7 @@ export default class HaScriptFields extends LitElement {
|
|||||||
.disabled=${this.disabled}
|
.disabled=${this.disabled}
|
||||||
@value-changed=${this._fieldChanged}
|
@value-changed=${this._fieldChanged}
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
|
?highlight=${this.highlightedFields?.[key] !== undefined}
|
||||||
>
|
>
|
||||||
</ha-script-field-row>
|
</ha-script-field-row>
|
||||||
`
|
`
|
||||||
|
@ -2,7 +2,18 @@ import "@material/mwc-button/mwc-button";
|
|||||||
import { mdiHelpCircle } from "@mdi/js";
|
import { mdiHelpCircle } from "@mdi/js";
|
||||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||||
import { LitElement, css, html, nothing } 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 { fireEvent } from "../../../common/dom/fire_event";
|
||||||
import { constructUrlCurrentPath } from "../../../common/url/construct-url";
|
import { constructUrlCurrentPath } from "../../../common/url/construct-url";
|
||||||
import {
|
import {
|
||||||
@ -13,6 +24,7 @@ import "../../../components/ha-card";
|
|||||||
import "../../../components/ha-icon-button";
|
import "../../../components/ha-icon-button";
|
||||||
import "../../../components/ha-markdown";
|
import "../../../components/ha-markdown";
|
||||||
import type { Action, Fields, ScriptConfig } from "../../../data/script";
|
import type { Action, Fields, ScriptConfig } from "../../../data/script";
|
||||||
|
import { MODES, normalizeScriptConfig } 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";
|
||||||
@ -20,6 +32,20 @@ import "../automation/action/ha-automation-action";
|
|||||||
import type HaAutomationAction from "../automation/action/ha-automation-action";
|
import type HaAutomationAction from "../automation/action/ha-automation-action";
|
||||||
import "./ha-script-fields";
|
import "./ha-script-fields";
|
||||||
import type HaScriptFields from "./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")
|
@customElement("manual-script-editor")
|
||||||
export class HaManualScriptEditor extends LitElement {
|
export class HaManualScriptEditor extends LitElement {
|
||||||
@ -33,11 +59,17 @@ export class HaManualScriptEditor extends LitElement {
|
|||||||
|
|
||||||
@property({ attribute: false }) public config!: ScriptConfig;
|
@property({ attribute: false }) public config!: ScriptConfig;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public dirty = false;
|
||||||
|
|
||||||
@query("ha-script-fields")
|
@query("ha-script-fields")
|
||||||
private _scriptFields?: HaScriptFields;
|
private _scriptFields?: HaScriptFields;
|
||||||
|
|
||||||
private _openFields = false;
|
private _openFields = false;
|
||||||
|
|
||||||
|
@state() private _pastedConfig?: ScriptConfig;
|
||||||
|
|
||||||
|
private _previousConfig?: ScriptConfig;
|
||||||
|
|
||||||
public addFields() {
|
public addFields() {
|
||||||
this._openFields = true;
|
this._openFields = true;
|
||||||
fireEvent(this, "value-changed", {
|
fireEvent(this, "value-changed", {
|
||||||
@ -126,6 +158,7 @@ export class HaManualScriptEditor extends LitElement {
|
|||||||
role="region"
|
role="region"
|
||||||
aria-labelledby="fields-heading"
|
aria-labelledby="fields-heading"
|
||||||
.fields=${this.config.fields}
|
.fields=${this.config.fields}
|
||||||
|
.highlightedFields=${this._pastedConfig?.fields}
|
||||||
@value-changed=${this._fieldsChanged}
|
@value-changed=${this._fieldsChanged}
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.disabled=${this.disabled}
|
.disabled=${this.disabled}
|
||||||
@ -154,6 +187,7 @@ export class HaManualScriptEditor extends LitElement {
|
|||||||
role="region"
|
role="region"
|
||||||
aria-labelledby="sequence-heading"
|
aria-labelledby="sequence-heading"
|
||||||
.actions=${this.config.sequence || []}
|
.actions=${this.config.sequence || []}
|
||||||
|
.highlightedActions=${this._pastedConfig?.sequence || []}
|
||||||
.path=${["sequence"]}
|
.path=${["sequence"]}
|
||||||
@value-changed=${this._sequenceChanged}
|
@value-changed=${this._sequenceChanged}
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
@ -165,6 +199,7 @@ export class HaManualScriptEditor extends LitElement {
|
|||||||
|
|
||||||
private _fieldsChanged(ev: CustomEvent): void {
|
private _fieldsChanged(ev: CustomEvent): void {
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
|
this.resetPastedConfig();
|
||||||
fireEvent(this, "value-changed", {
|
fireEvent(this, "value-changed", {
|
||||||
value: { ...this.config!, fields: ev.detail.value as Fields },
|
value: { ...this.config!, fields: ev.detail.value as Fields },
|
||||||
});
|
});
|
||||||
@ -172,11 +207,165 @@ export class HaManualScriptEditor extends LitElement {
|
|||||||
|
|
||||||
private _sequenceChanged(ev: CustomEvent): void {
|
private _sequenceChanged(ev: CustomEvent): void {
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
|
this.resetPastedConfig();
|
||||||
fireEvent(this, "value-changed", {
|
fireEvent(this, "value-changed", {
|
||||||
value: { ...this.config!, sequence: ev.detail.value as Action[] },
|
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 {
|
static get styles(): CSSResultGroup {
|
||||||
return [
|
return [
|
||||||
haStyle,
|
haStyle,
|
||||||
|
@ -14,6 +14,7 @@ import { showToast } from "../util/toast";
|
|||||||
import type { HassElement } from "./hass-element";
|
import type { HassElement } from "./hass-element";
|
||||||
import { extractSearchParamsObject } from "../common/url/search-params";
|
import { extractSearchParamsObject } from "../common/url/search-params";
|
||||||
import { showVoiceCommandDialog } from "../dialogs/voice-command-dialog/show-ha-voice-command-dialog";
|
import { showVoiceCommandDialog } from "../dialogs/voice-command-dialog/show-ha-voice-command-dialog";
|
||||||
|
import { canOverrideAlphanumericInput } from "../common/dom/can-override-input";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface HASSDomEvents {
|
interface HASSDomEvents {
|
||||||
@ -80,7 +81,7 @@ export default <T extends Constructor<HassElement>>(superClass: T) =>
|
|||||||
private _showVoiceCommandDialog(e: KeyboardEvent) {
|
private _showVoiceCommandDialog(e: KeyboardEvent) {
|
||||||
if (
|
if (
|
||||||
!this.hass?.enableShortcuts ||
|
!this.hass?.enableShortcuts ||
|
||||||
!this._canOverrideAlphanumericInput(e) ||
|
!canOverrideAlphanumericInput(e.composedPath()) ||
|
||||||
!this._conversation(this.hass.config.components)
|
!this._conversation(this.hass.config.components)
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
@ -113,7 +114,7 @@ export default <T extends Constructor<HassElement>>(superClass: T) =>
|
|||||||
private async _createMyLink(e: KeyboardEvent) {
|
private async _createMyLink(e: KeyboardEvent) {
|
||||||
if (
|
if (
|
||||||
!this.hass?.enableShortcuts ||
|
!this.hass?.enableShortcuts ||
|
||||||
!this._canOverrideAlphanumericInput(e)
|
!canOverrideAlphanumericInput(e.composedPath())
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -182,42 +183,7 @@ export default <T extends Constructor<HassElement>>(superClass: T) =>
|
|||||||
return (
|
return (
|
||||||
this.hass?.user?.is_admin &&
|
this.hass?.user?.is_admin &&
|
||||||
this.hass.enableShortcuts &&
|
this.hass.enableShortcuts &&
|
||||||
this._canOverrideAlphanumericInput(e)
|
canOverrideAlphanumericInput(e.composedPath())
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _canOverrideAlphanumericInput(e: KeyboardEvent) {
|
|
||||||
const composedPath = e.composedPath();
|
|
||||||
|
|
||||||
if (
|
|
||||||
composedPath.some((el) => "tagName" in el && el.tagName === "HA-MENU")
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const el = composedPath[0] as Element;
|
|
||||||
|
|
||||||
if (el.tagName === "TEXTAREA") {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (el.parentElement?.tagName === "HA-SELECT") {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (el.tagName !== "INPUT") {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch ((el as HTMLInputElement).type) {
|
|
||||||
case "button":
|
|
||||||
case "checkbox":
|
|
||||||
case "hidden":
|
|
||||||
case "radio":
|
|
||||||
case "range":
|
|
||||||
return true;
|
|
||||||
default:
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
@ -372,7 +372,9 @@
|
|||||||
"default": "Default",
|
"default": "Default",
|
||||||
"dont_save": "Don't save",
|
"dont_save": "Don't save",
|
||||||
"copy": "Copy",
|
"copy": "Copy",
|
||||||
"show": "Show"
|
"show": "Show",
|
||||||
|
"replace": "Replace",
|
||||||
|
"append": "Append"
|
||||||
},
|
},
|
||||||
"components": {
|
"components": {
|
||||||
"selectors": {
|
"selectors": {
|
||||||
@ -1935,6 +1937,10 @@
|
|||||||
"title": "Assist",
|
"title": "Assist",
|
||||||
"open_assist": "open Assist dialog"
|
"open_assist": "open Assist dialog"
|
||||||
},
|
},
|
||||||
|
"automations": {
|
||||||
|
"title": "Automations",
|
||||||
|
"paste": "to paste automation YAML from clipboard to automation editor"
|
||||||
|
},
|
||||||
"charts": {
|
"charts": {
|
||||||
"title": "Charts",
|
"title": "Charts",
|
||||||
"drag_to_zoom": "to zoom in part of a chart",
|
"drag_to_zoom": "to zoom in part of a chart",
|
||||||
@ -4343,7 +4349,13 @@
|
|||||||
"add_category": "Add category",
|
"add_category": "Add category",
|
||||||
"add_labels": "Add labels",
|
"add_labels": "Add labels",
|
||||||
"add_area": "Add area"
|
"add_area": "Add area"
|
||||||
}
|
},
|
||||||
|
"paste_confirm": {
|
||||||
|
"title": "Pasted automation",
|
||||||
|
"text": "How do you want to paste your automation?"
|
||||||
|
},
|
||||||
|
"paste_toast_message": "Pasted automation from clipboard",
|
||||||
|
"paste_invalid_config": "Pasted automation is not editable in the visual editor"
|
||||||
},
|
},
|
||||||
"trace": {
|
"trace": {
|
||||||
"refresh": "[%key:ui::common::refresh%]",
|
"refresh": "[%key:ui::common::refresh%]",
|
||||||
@ -4576,7 +4588,13 @@
|
|||||||
"unsaved_new_text": "You can save your changes, or delete this script. You can't undo this action.",
|
"unsaved_new_text": "You can save your changes, or delete this script. You can't undo this action.",
|
||||||
"unsaved_confirm_title": "Save changes?",
|
"unsaved_confirm_title": "Save changes?",
|
||||||
"unsaved_confirm_text": "You have made some changes in this script. You can save these changes, or discard them and leave. You can't undo this action."
|
"unsaved_confirm_text": "You have made some changes in this script. You can save these changes, or discard them and leave. You can't undo this action."
|
||||||
}
|
},
|
||||||
|
"paste_confirm": {
|
||||||
|
"title": "Pasted script",
|
||||||
|
"text": "How do you want to paste your script?"
|
||||||
|
},
|
||||||
|
"paste_toast_message": "Pasted script from clipboard",
|
||||||
|
"paste_invalid_config": "Pasted script is not editable in the visual editor"
|
||||||
},
|
},
|
||||||
"trace": {
|
"trace": {
|
||||||
"edit_script": "Edit script"
|
"edit_script": "Edit script"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user