Compare commits

...

6 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
f8d8bbf5f9 Remove underscore prefix from protected members per style guide
Co-authored-by: wendevlin <12148533+wendevlin@users.noreply.github.com>
2026-02-27 09:41:44 +00:00
copilot-swe-agent[bot]
be810d1f09 Add shared styles and loading animation to automation/script editor mixin
Co-authored-by: wendevlin <12148533+wendevlin@users.noreply.github.com>
2026-02-26 12:45:35 +00:00
Wendelin
8e6f693c55 Simplify automation/script editor mixin signature 2026-02-26 11:15:09 +01:00
copilot-swe-agent[bot]
c9d35c0500 Fix mixin: use function-body syntax for decorators, curried generics for type safety
Co-authored-by: wendevlin <12148533+wendevlin@users.noreply.github.com>
2026-02-26 08:17:45 +00:00
copilot-swe-agent[bot]
3b2a1ed5be Changes before error encountered
Co-authored-by: wendevlin <12148533+wendevlin@users.noreply.github.com>
2026-02-26 07:43:55 +00:00
copilot-swe-agent[bot]
1c50432dd4 Initial plan 2026-02-26 07:12:09 +00:00
3 changed files with 496 additions and 607 deletions

View File

@@ -27,19 +27,15 @@ import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { UndoRedoController } from "../../../common/controllers/undo-redo-controller";
import { transform } from "../../../common/decorators/transform";
import { fireEvent } from "../../../common/dom/fire_event";
import { goBack, navigate } from "../../../common/navigate";
import { promiseTimeout } from "../../../common/util/promise-timeout";
import { afterNextRender } from "../../../common/util/render-status";
import "../../../components/ha-button";
import "../../../components/ha-dropdown";
import "../../../components/ha-dropdown-item";
import "../../../components/ha-fab";
import "../../../components/ha-fade-in";
import "../../../components/ha-icon";
import "../../../components/ha-icon-button";
import "../../../components/ha-spinner";
import "../../../components/ha-svg-icon";
import "../../../components/ha-yaml-editor";
import type {
@@ -72,27 +68,22 @@ import {
showAlertDialog,
showConfirmationDialog,
} from "../../../dialogs/generic/show-dialog-box";
import { showMoreInfoDialog } from "../../../dialogs/more-info/show-ha-more-info-dialog";
import "../../../layouts/hass-subpage";
import { KeyboardShortcutMixin } from "../../../mixins/keyboard-shortcut-mixin";
import { PreventUnsavedMixin } from "../../../mixins/prevent-unsaved-mixin";
import { haStyle } from "../../../resources/styles";
import type {
Entries,
HomeAssistant,
Route,
ValueChangedEvent,
} from "../../../types";
import type { Entries, ValueChangedEvent } from "../../../types";
import { isMac } from "../../../util/is_mac";
import { showToast } from "../../../util/toast";
import { showAssignCategoryDialog } from "../category/show-dialog-assign-category";
import { showAutomationModeDialog } from "./automation-mode-dialog/show-dialog-automation-mode";
import {
type EntityRegistryUpdate,
showAutomationSaveDialog,
} from "./automation-save-dialog/show-dialog-automation-save";
import { showAutomationSaveDialog } from "./automation-save-dialog/show-dialog-automation-save";
import { showAutomationSaveTimeoutDialog } from "./automation-save-timeout-dialog/show-dialog-automation-save-timeout";
import "./blueprint-automation-editor";
import {
AutomationScriptEditorMixin,
automationScriptEditorStyles,
} from "./ha-automation-script-editor-mixin";
import "./manual-automation-editor";
import type { HaManualAutomationEditor } from "./manual-automation-editor";
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
@@ -119,53 +110,13 @@ declare global {
}
@customElement("ha-automation-editor")
export class HaAutomationEditor extends PreventUnsavedMixin(
KeyboardShortcutMixin(LitElement)
export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationConfig>(
PreventUnsavedMixin(KeyboardShortcutMixin(LitElement))
) {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public automationId: string | null = null;
@property({ attribute: false }) public entityId: string | null = null;
@property({ attribute: false }) public automations!: AutomationEntity[];
@property({ attribute: "is-wide", type: Boolean }) public isWide = false;
@property({ type: Boolean }) public narrow = false;
@property({ attribute: false }) public route!: Route;
@state() private _config?: AutomationConfig;
@state() private _dirty = false;
@state() private _errors?: string;
@state() private _yamlErrors?: string;
@state() private _entityId?: string;
@state() private _mode: "gui" | "yaml" = "gui";
@state() private _readOnly = false;
@state() private _validationErrors?: (string | TemplateResult)[];
@state() private _blueprintConfig?: BlueprintAutomationConfig;
@state()
@consume({ context: fullEntitiesContext, subscribe: true })
@transform<EntityRegistryEntry[], EntityRegistryEntry>({
transformer: function (this: HaAutomationEditor, value) {
return value.find(({ entity_id }) => entity_id === this._entityId);
},
watch: ["_entityId"],
})
private _registryEntry?: EntityRegistryEntry;
@state() private _saving = false;
@state()
@consume({ context: fullEntitiesContext, subscribe: true })
_entityRegistry!: EntityRegistryEntry[];
@@ -180,24 +131,18 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
private _configSubscriptionsId = 1;
private _entityRegistryUpdate?: EntityRegistryUpdate;
private _newAutomationId?: string;
private _entityRegCreated?: (
value: PromiseLike<EntityRegistryEntry> | EntityRegistryEntry
) => void;
private _undoRedoController = new UndoRedoController<AutomationConfig>(this, {
apply: (config) => this._applyUndoRedo(config),
currentConfig: () => this._config!,
currentConfig: () => this.config!,
});
protected willUpdate(changedProps) {
super.willUpdate(changedProps);
if (
this._entityRegCreated &&
this.entityRegCreated &&
this._newAutomationId &&
changedProps.has("_entityRegistry")
) {
@@ -207,26 +152,22 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
entity.unique_id === this._newAutomationId
);
if (automation) {
this._entityRegCreated(automation);
this._entityRegCreated = undefined;
this.entityRegCreated(automation);
this.entityRegCreated = undefined;
}
}
}
protected render(): TemplateResult | typeof nothing {
if (!this._config) {
return html`
<ha-fade-in .delay=${500}>
<ha-spinner size="large"></ha-spinner>
</ha-fade-in>
`;
if (!this.config) {
return this.renderLoading();
}
const stateObj = this._entityId
? this.hass.states[this._entityId]
const stateObj = this.currentEntityId
? this.hass.states[this.currentEntityId]
: undefined;
const useBlueprint = "use_blueprint" in this._config;
const useBlueprint = "use_blueprint" in this.config;
const shortcutIcon = isMac
? html`<ha-svg-icon .path=${mdiAppleKeyboardCommand}></ha-svg-icon>`
: this.hass.localize("ui.panel.config.automation.editor.ctrl");
@@ -236,11 +177,11 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
.hass=${this.hass}
.narrow=${this.narrow}
.route=${this.route}
.backCallback=${this._backTapped}
.header=${this._config.alias ||
.backCallback=${this.backTapped}
.header=${this.config.alias ||
this.hass.localize("ui.panel.config.automation.editor.default_name")}
>
${this._mode === "gui" && !this.narrow
${this.mode === "gui" && !this.narrow
? html`<ha-icon-button
slot="toolbar-icon"
.label=${this.hass.localize("ui.common.undo")}
@@ -284,7 +225,7 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
</span>
</ha-tooltip>`
: nothing}
${this._config?.id && !this.narrow
${this.config?.id && !this.narrow
? html`
<ha-button
appearance="plain"
@@ -308,7 +249,7 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
.path=${mdiDotsVertical}
></ha-icon-button>
${this._mode === "gui" && this.narrow
${this.mode === "gui" && this.narrow
? html`<ha-dropdown-item
value="undo"
.disabled=${!this._undoRedoController.canUndo}
@@ -342,7 +283,7 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
<ha-dropdown-item .disabled=${!stateObj} value="category">
${this.hass.localize(
`ui.panel.config.scene.picker.${this._registryEntry?.categories?.automation ? "edit_category" : "assign_category"}`
`ui.panel.config.scene.picker.${this.registryEntry?.categories?.automation ? "edit_category" : "assign_category"}`
)}
<ha-svg-icon slot="icon" .path=${mdiTag}></ha-svg-icon>
</ha-dropdown-item>
@@ -366,9 +307,9 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
<ha-dropdown-item
value="rename"
.disabled=${this._readOnly ||
.disabled=${this.readOnly ||
!this.automationId ||
this._mode === "yaml"}
this.mode === "yaml"}
>
${this.hass.localize("ui.panel.config.automation.editor.rename")}
<ha-svg-icon slot="icon" .path=${mdiRenameBox}></ha-svg-icon>
@@ -377,7 +318,7 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
? html`
<ha-dropdown-item
@click=${this._promptAutomationMode}
.disabled=${this._readOnly || this._mode === "yaml"}
.disabled=${this.readOnly || this.mode === "yaml"}
>
${this.hass.localize(
"ui.panel.config.automation.editor.change_mode"
@@ -391,12 +332,12 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
: nothing}
<ha-dropdown-item
.disabled=${!!this._blueprintConfig ||
(!this._readOnly && !this.automationId)}
.disabled=${!!this.blueprintConfig ||
(!this.readOnly && !this.automationId)}
value="duplicate"
>
${this.hass.localize(
this._readOnly
this.readOnly
? "ui.panel.config.automation.editor.migrate"
: "ui.panel.config.automation.editor.duplicate"
)}
@@ -410,7 +351,7 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
? html`
<ha-dropdown-item
value="take_control"
.disabled=${this._readOnly}
.disabled=${this.readOnly}
>
${this.hass.localize(
"ui.panel.config.automation.editor.take_control"
@@ -422,7 +363,7 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
<ha-dropdown-item value="toggle_yaml_mode">
${this.hass.localize(
`ui.panel.config.automation.editor.edit_${this._mode === "gui" ? "yaml" : "ui"}`
`ui.panel.config.automation.editor.edit_${this.mode === "gui" ? "yaml" : "ui"}`
)}
<ha-svg-icon slot="icon" .path=${mdiPlaylistEdit}></ha-svg-icon>
</ha-dropdown-item>
@@ -456,10 +397,10 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
</ha-dropdown-item>
</ha-dropdown>
<div
class=${this._mode === "yaml" ? "yaml-mode" : ""}
class=${this.mode === "yaml" ? "yaml-mode" : ""}
@subscribe-automation-config=${this._subscribeAutomationConfig}
>
${this._mode === "gui"
${this.mode === "gui"
? html`
<div>
${useBlueprint
@@ -469,10 +410,10 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
.narrow=${this.narrow}
.isWide=${this.isWide}
.stateObj=${stateObj}
.config=${this._config}
.disabled=${this._readOnly}
.saving=${this._saving}
.dirty=${this._dirty}
.config=${this.config}
.disabled=${this.readOnly}
.saving=${this.saving}
.dirty=${this.dirty}
@value-changed=${this._valueChanged}
@save-automation=${this._handleSaveAutomation}
></blueprint-automation-editor>
@@ -483,16 +424,16 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
.narrow=${this.narrow}
.isWide=${this.isWide}
.stateObj=${stateObj}
.config=${this._config}
.disabled=${this._readOnly}
.dirty=${this._dirty}
.saving=${this._saving}
.config=${this.config}
.disabled=${this.readOnly}
.dirty=${this.dirty}
.saving=${this.saving}
@value-changed=${this._valueChanged}
@save-automation=${this._handleSaveAutomation}
@editor-save=${this._handleSaveAutomation}
>
<div class="alert-wrapper" slot="alerts">
${this._errors || stateObj?.state === UNAVAILABLE
${this.errors || stateObj?.state === UNAVAILABLE
? html`<ha-alert
alert-type="error"
.title=${stateObj?.state === UNAVAILABLE
@@ -501,7 +442,7 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
)
: undefined}
>
${this._errors || this._validationErrors}
${this.errors || this.validationErrors}
${stateObj?.state === UNAVAILABLE
? html`<ha-svg-icon
slot="icon"
@@ -510,7 +451,7 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
: nothing}
</ha-alert>`
: nothing}
${this._blueprintConfig
${this.blueprintConfig
? html`<ha-alert alert-type="info">
${this.hass.localize(
"ui.panel.config.automation.editor.confirm_take_control"
@@ -518,21 +459,21 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
<div slot="action" style="display: flex;">
<ha-button
appearance="plain"
@click=${this._takeControlSave}
@click=${this.takeControlSave}
>${this.hass.localize(
"ui.common.yes"
)}</ha-button
>
<ha-button
appearance="plain"
@click=${this._revertBlueprint}
@click=${this.revertBlueprint}
>${this.hass.localize(
"ui.common.no"
)}</ha-button
>
</div>
</ha-alert>`
: this._readOnly
: this.readOnly
? html`<ha-alert
alert-type="warning"
dismissable
@@ -575,7 +516,7 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
`}
</div>
`
: this._mode === "yaml"
: this.mode === "yaml"
? html`${stateObj?.state === "off"
? html`
<ha-alert alert-type="info">
@@ -598,7 +539,7 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
<ha-yaml-editor
.hass=${this.hass}
.defaultValue=${this._preprocessYaml()}
.readOnly=${this._readOnly}
.readOnly=${this.readOnly}
@value-changed=${this._yamlChanged}
@editor-save=${this._handleSaveAutomation}
.showErrors=${false}
@@ -606,9 +547,9 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
></ha-yaml-editor>
<ha-fab
slot="fab"
class=${this._dirty ? "dirty" : ""}
class=${this.dirty ? "dirty" : ""}
.label=${this.hass.localize("ui.common.save")}
.disabled=${this._saving}
.disabled=${this.saving}
extended
@click=${this._handleSaveAutomation}
>
@@ -645,7 +586,7 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
this.hass
) {
const initData = getAutomationEditorInitData();
this._dirty = !!initData;
this.dirty = !!initData;
let baseConfig: Partial<AutomationConfig> = { description: "" };
if (!initData || !("use_blueprint" in initData)) {
baseConfig = {
@@ -656,35 +597,35 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
actions: [],
};
}
this._config = {
this.config = {
...baseConfig,
...(initData ? normalizeAutomationConfig(initData) : initData),
} as AutomationConfig;
this._entityId = undefined;
this._readOnly = false;
this.currentEntityId = undefined;
this.readOnly = false;
}
if (changedProps.has("entityId") && this.entityId) {
getAutomationStateConfig(this.hass, this.entityId).then((c) => {
this._config = normalizeAutomationConfig(c.config);
this.config = normalizeAutomationConfig(c.config);
this._checkValidation();
});
this._entityId = this.entityId;
this._dirty = false;
this._readOnly = true;
this.currentEntityId = this.entityId;
this.dirty = false;
this.readOnly = true;
}
if (
changedProps.has("automations") &&
this.automationId &&
!this._entityId
!this.currentEntityId
) {
this._setEntityId();
}
if (changedProps.has("_config")) {
if (changedProps.has("config")) {
Object.values(this._configSubscriptions).forEach((sub) =>
sub(this._config)
sub(this.config)
);
}
}
@@ -693,24 +634,24 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
const automation = this.automations.find(
(entity: AutomationEntity) => entity.attributes.id === this.automationId
);
this._entityId = automation?.entity_id;
this.currentEntityId = automation?.entity_id;
}
private async _checkValidation() {
this._validationErrors = undefined;
if (!this._entityId || !this._config) {
this.validationErrors = undefined;
if (!this.currentEntityId || !this.config) {
return;
}
const stateObj = this.hass.states[this._entityId];
const stateObj = this.hass.states[this.currentEntityId];
if (stateObj?.state !== UNAVAILABLE) {
return;
}
const validation = await validateConfig(this.hass, {
triggers: this._config.triggers,
conditions: this._config.conditions,
actions: this._config.actions,
triggers: this.config.triggers,
conditions: this.config.conditions,
actions: this.config.actions,
});
this._validationErrors = (
this.validationErrors = (
Object.entries(validation) as Entries<typeof validation>
).map(([key, value]) =>
value.valid
@@ -728,9 +669,9 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
this.hass,
this.automationId as string
);
this._dirty = false;
this._readOnly = false;
this._config = normalizeAutomationConfig(config);
this.dirty = false;
this.readOnly = false;
this.config = normalizeAutomationConfig(config);
this._checkValidation();
} catch (err: any) {
const entity = this._entityRegistry.find(
@@ -761,34 +702,27 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
private _valueChanged(ev: ValueChangedEvent<AutomationConfig>) {
ev.stopPropagation();
if (this._config) {
this._undoRedoController.commit(this._config);
if (this.config) {
this._undoRedoController.commit(this.config);
}
this._config = ev.detail.value;
if (this._readOnly) {
this.config = ev.detail.value;
if (this.readOnly) {
return;
}
this._dirty = true;
this._errors = undefined;
this.dirty = true;
this.errors = undefined;
}
private _showInfo() {
if (!this.hass || !this._entityId) {
if (!this.hass || !this.currentEntityId) {
return;
}
fireEvent(this, "hass-more-info", { entityId: this._entityId });
}
private _showSettings() {
showMoreInfoDialog(this, {
entityId: this._entityId!,
view: "settings",
});
fireEvent(this, "hass-more-info", { entityId: this.currentEntityId });
}
private _editCategory() {
if (!this._registryEntry) {
if (!this.registryEntry) {
showAlertDialog(this, {
title: this.hass.localize(
"ui.panel.config.scene.picker.no_category_support"
@@ -801,36 +735,36 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
}
showAssignCategoryDialog(this, {
scope: "automation",
entityReg: this._registryEntry,
entityReg: this.registryEntry,
});
}
private async _showTrace() {
if (this._config?.id) {
const result = await this._confirmUnsavedChanged();
if (this.config?.id) {
const result = await this.confirmUnsavedChanged();
if (result) {
navigate(
`/config/automation/trace/${encodeURIComponent(this._config.id)}`
`/config/automation/trace/${encodeURIComponent(this.config.id)}`
);
}
}
}
private _runActions() {
if (!this.hass || !this._entityId) {
if (!this.hass || !this.currentEntityId) {
return;
}
triggerAutomationActions(
this.hass,
this.hass.states[this._entityId].entity_id
this.hass.states[this.currentEntityId].entity_id
);
}
private async _toggle(): Promise<void> {
if (!this.hass || !this._entityId) {
if (!this.hass || !this.currentEntityId) {
return;
}
const stateObj = this.hass.states[this._entityId];
const stateObj = this.hass.states[this.currentEntityId];
const service = stateObj.state === "off" ? "turn_on" : "turn_off";
await this.hass.callService("automation", service, {
entity_id: stateObj.entity_id,
@@ -838,42 +772,42 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
}
private _preprocessYaml() {
if (!this._config) {
if (!this.config) {
return {};
}
const cleanConfig: AutomationConfig = { ...this._config };
const cleanConfig: AutomationConfig = { ...this.config };
delete cleanConfig.id;
return cleanConfig;
}
private _yamlChanged(ev: CustomEvent) {
ev.stopPropagation();
this._dirty = true;
this.dirty = true;
if (!ev.detail.isValid) {
this._yamlErrors = ev.detail.errorMsg;
this.yamlErrors = ev.detail.errorMsg;
return;
}
this._yamlErrors = undefined;
this._config = {
id: this._config?.id,
this.yamlErrors = undefined;
this.config = {
id: this.config?.id,
...normalizeAutomationConfig(ev.detail.value),
};
this._errors = undefined;
this.errors = undefined;
}
private async _confirmUnsavedChanged(): Promise<boolean> {
if (!this._dirty) {
protected async confirmUnsavedChanged(): Promise<boolean> {
if (!this.dirty) {
return true;
}
return new Promise<boolean>((resolve) => {
showAutomationSaveDialog(this, {
config: this._config!,
config: this.config!,
domain: "automation",
updateConfig: async (config, entityRegistryUpdate) => {
this._config = config;
this._entityRegistryUpdate = entityRegistryUpdate;
this._dirty = true;
this.config = config;
this.entityRegistryUpdate = entityRegistryUpdate;
this.dirty = true;
this.requestUpdate();
const id = this.automationId || String(Date.now());
@@ -889,8 +823,8 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
},
onClose: () => resolve(false),
onDiscard: () => resolve(true),
entityRegistryUpdate: this._entityRegistryUpdate,
entityRegistryEntry: this._registryEntry,
entityRegistryUpdate: this.entityRegistryUpdate,
entityRegistryEntry: this.registryEntry,
title: this.hass.localize(
this.automationId
? "ui.panel.config.automation.editor.leave.unsaved_confirm_title"
@@ -906,15 +840,8 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
});
}
private _backTapped = async () => {
const result = await this._confirmUnsavedChanged();
if (result) {
afterNextRender(() => goBack("/config"));
}
};
private async _takeControl() {
const config = this._config as BlueprintAutomationConfig;
const config = this.config as BlueprintAutomationConfig;
try {
const result = await substituteBlueprint(
@@ -931,35 +858,20 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
description: config.description,
};
this._blueprintConfig = config;
this._config = newConfig;
if (this._mode === "yaml") {
this.renderRoot.querySelector("ha-yaml-editor")?.setValue(this._config);
this.blueprintConfig = config;
this.config = newConfig;
if (this.mode === "yaml") {
this.renderRoot.querySelector("ha-yaml-editor")?.setValue(this.config);
}
this._readOnly = true;
this._errors = undefined;
this.readOnly = true;
this.errors = undefined;
} catch (err: any) {
this._errors = err.message;
this.errors = err.message;
}
}
private _revertBlueprint() {
this._config = this._blueprintConfig;
if (this._mode === "yaml") {
this.renderRoot.querySelector("ha-yaml-editor")?.setValue(this._config);
}
this._blueprintConfig = undefined;
this._readOnly = false;
}
private _takeControlSave() {
this._readOnly = false;
this._dirty = true;
this._blueprintConfig = undefined;
}
private async _duplicate() {
const result = this._readOnly
const result = this.readOnly
? await showConfirmationDialog(this, {
title: this.hass.localize(
"ui.panel.config.automation.picker.migrate_automation"
@@ -968,12 +880,12 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
"ui.panel.config.automation.picker.migrate_automation_description"
),
})
: await this._confirmUnsavedChanged();
: await this.confirmUnsavedChanged();
if (result) {
showAutomationEditor({
...this._config,
...this.config,
id: undefined,
alias: this._readOnly ? this._config?.alias : undefined,
alias: this.readOnly ? this.config?.alias : undefined,
});
}
}
@@ -985,7 +897,7 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
),
text: this.hass.localize(
"ui.panel.config.automation.picker.delete_confirm_text",
{ name: this._config?.alias }
{ name: this.config?.alias }
),
confirmText: this.hass!.localize("ui.common.delete"),
destructive: true,
@@ -1001,43 +913,21 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
}
}
private async _switchUiMode() {
if (this._yamlErrors) {
const result = await showConfirmationDialog(this, {
text: html`${this.hass.localize(
"ui.panel.config.automation.editor.switch_ui_yaml_error"
)}<br /><br />${this._yamlErrors}`,
confirmText: this.hass!.localize("ui.common.continue"),
destructive: true,
dismissText: this.hass!.localize("ui.common.cancel"),
});
if (!result) {
return;
}
}
this._yamlErrors = undefined;
this._mode = "gui";
}
private _switchYamlMode() {
this._mode = "yaml";
}
private async _promptAutomationAlias(): Promise<boolean> {
return new Promise((resolve) => {
showAutomationSaveDialog(this, {
config: this._config!,
config: this.config!,
domain: "automation",
updateConfig: async (config, entityRegistryUpdate) => {
this._config = config;
this._entityRegistryUpdate = entityRegistryUpdate;
this._dirty = true;
this.config = config;
this.entityRegistryUpdate = entityRegistryUpdate;
this.dirty = true;
this.requestUpdate();
resolve(true);
},
onClose: () => resolve(false),
entityRegistryUpdate: this._entityRegistryUpdate,
entityRegistryEntry: this._registryEntry,
entityRegistryUpdate: this.entityRegistryUpdate,
entityRegistryEntry: this.registryEntry,
});
});
}
@@ -1045,10 +935,10 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
private async _promptAutomationMode(): Promise<void> {
return new Promise((resolve) => {
showAutomationModeDialog(this, {
config: this._config!,
config: this.config!,
updateConfig: (config) => {
this._config = config;
this._dirty = true;
this.config = config;
this.dirty = true;
this.requestUpdate();
resolve();
},
@@ -1058,9 +948,9 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
}
private async _handleSaveAutomation(): Promise<void> {
if (this._yamlErrors) {
if (this.yamlErrors) {
showToast(this, {
message: this._yamlErrors,
message: this.yamlErrors,
});
return;
}
@@ -1082,22 +972,22 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
}
private async _saveAutomation(id): Promise<void> {
this._saving = true;
this._validationErrors = undefined;
this.saving = true;
this.validationErrors = undefined;
let entityRegPromise: Promise<EntityRegistryEntry> | undefined;
if (this._entityRegistryUpdate !== undefined && !this._entityId) {
if (this.entityRegistryUpdate !== undefined && !this.currentEntityId) {
this._newAutomationId = id;
entityRegPromise = new Promise<EntityRegistryEntry>((resolve) => {
this._entityRegCreated = resolve;
this.entityRegCreated = resolve;
});
}
try {
await saveAutomationConfig(this.hass, id, this._config!);
await saveAutomationConfig(this.hass, id, this.config!);
if (this._entityRegistryUpdate !== undefined) {
let entityId = this._entityId;
if (this.entityRegistryUpdate !== undefined) {
let entityId = this.currentEntityId;
// wait for automation to appear in entity registry when creating a new automation
if (entityRegPromise) {
@@ -1131,23 +1021,23 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
if (entityId) {
await updateEntityRegistryEntry(this.hass, entityId, {
categories: {
automation: this._entityRegistryUpdate.category || null,
automation: this.entityRegistryUpdate.category || null,
},
labels: this._entityRegistryUpdate.labels || [],
area_id: this._entityRegistryUpdate.area || null,
labels: this.entityRegistryUpdate.labels || [],
area_id: this.entityRegistryUpdate.area || null,
});
}
}
this._dirty = false;
this.dirty = false;
} catch (errors: any) {
this._errors = errors.body?.message || errors.error || errors.body;
this.errors = errors.body?.message || errors.error || errors.body;
showToast(this, {
message: errors.body?.message || errors.error || errors.body,
});
throw errors;
} finally {
this._saving = false;
this.saving = false;
}
}
@@ -1157,7 +1047,7 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
ev.detail.unsub = () => {
delete this._configSubscriptions[id];
};
ev.detail.callback(this._config);
ev.detail.callback(this.config);
}
protected supportedShortcuts(): SupportedShortcuts {
@@ -1173,14 +1063,6 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
};
}
protected get isDirty() {
return this._dirty;
}
protected async promptDiscardChanges() {
return this._confirmUnsavedChanged();
}
// @ts-ignore
private _collapseAll() {
this._manualEditor?.collapseAll();
@@ -1205,8 +1087,8 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
private _applyUndoRedo(config: AutomationConfig) {
this._manualEditor?.triggerCloseSidebar();
this._config = config;
this._dirty = true;
this.config = config;
this.dirty = true;
}
private _undo() {
@@ -1235,7 +1117,7 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
this._showInfo();
break;
case "settings":
this._showSettings();
this.showSettings();
break;
case "category":
this._editCategory();
@@ -1256,11 +1138,11 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
this._takeControl();
break;
case "toggle_yaml_mode":
if (this._mode === "gui") {
this._switchYamlMode();
if (this.mode === "gui") {
this.switchYamlMode();
break;
}
this._switchUiMode();
this.switchUiMode();
break;
case "disable":
this._toggle();
@@ -1277,25 +1159,8 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
static get styles(): CSSResultGroup {
return [
haStyle,
automationScriptEditorStyles,
css`
:host {
--ha-automation-editor-max-width: var(
--ha-automation-editor-width,
1540px
);
}
ha-fade-in {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
}
.yaml-mode {
height: 100%;
display: flex;
flex-direction: column;
padding-bottom: 0;
}
manual-automation-editor,
blueprint-automation-editor {
margin: 0 auto;
@@ -1309,17 +1174,6 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
padding: 0 12px;
}
ha-yaml-editor {
flex-grow: 1;
--actions-border-radius: var(--ha-border-radius-square);
--code-mirror-height: 100%;
min-height: 0;
display: flex;
flex-direction: column;
}
p {
margin-bottom: 0;
}
ha-entity-toggle {
margin-right: 8px;
margin-inline-end: 8px;
@@ -1335,24 +1189,6 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
max-width: 1040px;
padding: 28px 20px 0;
}
ha-fab {
position: fixed;
right: calc(16px + var(--safe-area-inset-right, 0px));
bottom: calc(-80px - var(--safe-area-inset-bottom));
transition: bottom 0.3s;
}
ha-fab.dirty {
bottom: calc(16px + var(--safe-area-inset-bottom, 0px));
}
ha-tooltip ha-svg-icon {
width: 12px;
}
ha-tooltip .shortcut {
display: inline-flex;
flex-direction: row;
align-items: center;
gap: 2px;
}
`,
];
}

View File

@@ -0,0 +1,199 @@
import { consume } from "@lit/context";
import type { CSSResult, TemplateResult, LitElement } from "lit";
import { css, html } from "lit";
import { property, state } from "lit/decorators";
import { transform } from "../../../common/decorators/transform";
import { goBack } from "../../../common/navigate";
import { afterNextRender } from "../../../common/util/render-status";
import { fullEntitiesContext } from "../../../data/context";
import type { EntityRegistryEntry } from "../../../data/entity/entity_registry";
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
import { showMoreInfoDialog } from "../../../dialogs/more-info/show-ha-more-info-dialog";
import type { Constructor, HomeAssistant, Route } from "../../../types";
import type { EntityRegistryUpdate } from "./automation-save-dialog/show-dialog-automation-save";
import "../../../components/ha-fade-in";
import "../../../components/ha-spinner"; // used by renderLoading() provided to both editors
/** Minimum config shape shared by both AutomationConfig and ScriptConfig. */
interface BaseEditorConfig {
alias?: string;
}
/** Shared CSS styles for both automation and script editors. */
export const automationScriptEditorStyles: CSSResult = css`
:host {
--ha-automation-editor-max-width: var(--ha-automation-editor-width, 1540px);
}
ha-fade-in {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
}
.yaml-mode {
height: 100%;
display: flex;
flex-direction: column;
padding-bottom: 0;
}
ha-yaml-editor {
flex-grow: 1;
--actions-border-radius: var(--ha-border-radius-square);
--code-mirror-height: 100%;
min-height: 0;
display: flex;
flex-direction: column;
}
p {
margin-bottom: 0;
}
ha-fab {
position: fixed;
right: calc(16px + var(--safe-area-inset-right, 0px));
bottom: calc(-80px - var(--safe-area-inset-bottom));
transition: bottom 0.3s;
}
ha-fab.dirty {
bottom: calc(16px + var(--safe-area-inset-bottom, 0px));
}
ha-tooltip ha-svg-icon {
width: 12px;
}
ha-tooltip .shortcut {
display: inline-flex;
flex-direction: row;
align-items: center;
gap: 2px;
}
`;
export const AutomationScriptEditorMixin = <TConfig extends BaseEditorConfig>(
superClass: Constructor<LitElement>
) => {
class AutomationScriptEditorClass extends superClass {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: "is-wide", type: Boolean }) public isWide = false;
@property({ type: Boolean }) public narrow = false;
@property({ attribute: false }) public route!: Route;
@property({ attribute: false }) public entityId: string | null = null;
@state() protected dirty = false;
@state() protected errors?: string;
@state() protected yamlErrors?: string;
@state() protected currentEntityId?: string;
@state() protected mode: "gui" | "yaml" = "gui";
@state() protected readOnly = false;
@state() protected saving = false;
@state() protected validationErrors?: (string | TemplateResult)[];
@state() protected config?: TConfig;
@state() protected blueprintConfig?: TConfig;
@state()
@consume({ context: fullEntitiesContext, subscribe: true })
@transform<EntityRegistryEntry[], EntityRegistryEntry>({
transformer: function (this: { currentEntityId?: string }, value) {
return value.find(
({ entity_id }) => entity_id === this.currentEntityId
);
},
watch: ["currentEntityId"],
})
protected registryEntry?: EntityRegistryEntry;
protected entityRegistryUpdate?: EntityRegistryUpdate;
protected entityRegCreated?: (
value: PromiseLike<EntityRegistryEntry> | EntityRegistryEntry
) => void;
protected renderLoading(): TemplateResult {
return html`
<ha-fade-in .delay=${500}>
<ha-spinner size="large"></ha-spinner>
</ha-fade-in>
`;
}
protected showSettings() {
showMoreInfoDialog(this, {
entityId: this.currentEntityId!,
view: "settings",
});
}
protected async switchUiMode() {
if (this.yamlErrors) {
const result = await showConfirmationDialog(this, {
text: html`${this.hass.localize(
"ui.panel.config.automation.editor.switch_ui_yaml_error"
)}<br /><br />${this.yamlErrors}`,
confirmText: this.hass!.localize("ui.common.continue"),
destructive: true,
dismissText: this.hass!.localize("ui.common.cancel"),
});
if (!result) {
return;
}
}
this.yamlErrors = undefined;
this.mode = "gui";
}
protected switchYamlMode() {
this.mode = "yaml";
}
protected takeControlSave() {
this.readOnly = false;
this.dirty = true;
this.blueprintConfig = undefined;
}
protected revertBlueprint() {
this.config = this.blueprintConfig;
if (this.mode === "yaml") {
this.renderRoot.querySelector("ha-yaml-editor")?.setValue(this.config);
}
this.blueprintConfig = undefined;
this.readOnly = false;
}
protected backTapped = async () => {
const result = await this.confirmUnsavedChanged();
if (result) {
afterNextRender(() => goBack("/config"));
}
};
protected get isDirty() {
return this.dirty;
}
protected async promptDiscardChanges() {
return this.confirmUnsavedChanged();
}
/**
* Asks whether unsaved changes should be discarded.
* Subclasses must override this to show a confirmation dialog.
* @returns true to proceed (discard/save changes), false to cancel.
*/
protected confirmUnsavedChanged(): Promise<boolean> {
return Promise.resolve(true);
}
}
return AutomationScriptEditorClass;
};

View File

@@ -1,5 +1,4 @@
import "@home-assistant/webawesome/dist/components/divider/divider";
import { consume } from "@lit/context";
import {
mdiAppleKeyboardCommand,
mdiCog,
@@ -22,15 +21,13 @@ import {
} from "@mdi/js";
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { customElement, property, query } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { UndoRedoController } from "../../../common/controllers/undo-redo-controller";
import { transform } from "../../../common/decorators/transform";
import { fireEvent } from "../../../common/dom/fire_event";
import { goBack, navigate } from "../../../common/navigate";
import { slugify } from "../../../common/string/slugify";
import { promiseTimeout } from "../../../common/util/promise-timeout";
import { afterNextRender } from "../../../common/util/render-status";
import "../../../components/ha-button";
import "../../../components/ha-dropdown";
import "../../../components/ha-dropdown-item";
@@ -40,7 +37,6 @@ import "../../../components/ha-svg-icon";
import "../../../components/ha-yaml-editor";
import { substituteBlueprint } from "../../../data/blueprint";
import { validateConfig } from "../../../data/config";
import { fullEntitiesContext } from "../../../data/context";
import { UNAVAILABLE } from "../../../data/entity/entity";
import {
type EntityRegistryEntry,
@@ -67,88 +63,47 @@ 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 type { Entries, HomeAssistant, Route } from "../../../types";
import type { Entries } from "../../../types";
import { isMac } from "../../../util/is_mac";
import { showToast } from "../../../util/toast";
import { showAutomationModeDialog } from "../automation/automation-mode-dialog/show-dialog-automation-mode";
import type { EntityRegistryUpdate } from "../automation/automation-save-dialog/show-dialog-automation-save";
import { showAutomationSaveDialog } from "../automation/automation-save-dialog/show-dialog-automation-save";
import { showAutomationSaveTimeoutDialog } from "../automation/automation-save-timeout-dialog/show-dialog-automation-save-timeout";
import { showAssignCategoryDialog } from "../category/show-dialog-assign-category";
import "./blueprint-script-editor";
import {
AutomationScriptEditorMixin,
automationScriptEditorStyles,
} from "../automation/ha-automation-script-editor-mixin";
import "./manual-script-editor";
import type { HaManualScriptEditor } from "./manual-script-editor";
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
@customElement("ha-script-editor")
export class HaScriptEditor extends SubscribeMixin(
PreventUnsavedMixin(KeyboardShortcutMixin(LitElement))
AutomationScriptEditorMixin<ScriptConfig>(
PreventUnsavedMixin(KeyboardShortcutMixin(LitElement))
)
) {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public scriptId: string | null = null;
@property({ attribute: false }) public entityId: string | null = null;
@property({ attribute: false }) public entityRegistry!: EntityRegistryEntry[];
@property({ attribute: "is-wide", type: Boolean }) public isWide = false;
@property({ type: Boolean }) public narrow = false;
@property({ attribute: false }) public route!: Route;
@state() private _config?: ScriptConfig;
@state() private _dirty = false;
@state() private _errors?: string;
@state() private _yamlErrors?: string;
@state() private _entityId?: string;
@state() private _mode: "gui" | "yaml" = "gui";
@state() private _readOnly = false;
@state()
@consume({ context: fullEntitiesContext, subscribe: true })
@transform<EntityRegistryEntry[], EntityRegistryEntry>({
transformer: function (this: HaScriptEditor, value) {
return value.find(({ entity_id }) => entity_id === this._entityId);
},
watch: ["_entityId"],
})
private _registryEntry?: EntityRegistryEntry;
@query("manual-script-editor")
private _manualEditor?: HaManualScriptEditor;
@state() private _validationErrors?: (string | TemplateResult)[];
@state() private _blueprintConfig?: BlueprintScriptConfig;
@state() private _saving = false;
private _entityRegistryUpdate?: EntityRegistryUpdate;
private _newScriptId?: string;
private _entityRegCreated?: (
value: PromiseLike<EntityRegistryEntry> | EntityRegistryEntry
) => void;
private _undoRedoController = new UndoRedoController<ScriptConfig>(this, {
apply: (config) => this._applyUndoRedo(config),
currentConfig: () => this._config!,
currentConfig: () => this.config!,
});
protected willUpdate(changedProps) {
super.willUpdate(changedProps);
if (
this._entityRegCreated &&
this.entityRegCreated &&
this._newScriptId &&
changedProps.has("entityRegistry")
) {
@@ -157,22 +112,22 @@ export class HaScriptEditor extends SubscribeMixin(
entity.platform === "script" && entity.unique_id === this._newScriptId
);
if (script) {
this._entityRegCreated(script);
this._entityRegCreated = undefined;
this.entityRegCreated(script);
this.entityRegCreated = undefined;
}
}
}
protected render(): TemplateResult | typeof nothing {
if (!this._config) {
return nothing;
if (!this.config) {
return this.renderLoading();
}
const stateObj = this._entityId
? this.hass.states[this._entityId]
const stateObj = this.currentEntityId
? this.hass.states[this.currentEntityId]
: undefined;
const useBlueprint = "use_blueprint" in this._config;
const useBlueprint = "use_blueprint" in this.config;
const shortcutIcon = isMac
? html`<ha-svg-icon .path=${mdiAppleKeyboardCommand}></ha-svg-icon>`
: this.hass.localize("ui.panel.config.automation.editor.ctrl");
@@ -182,11 +137,11 @@ export class HaScriptEditor extends SubscribeMixin(
.hass=${this.hass}
.narrow=${this.narrow}
.route=${this.route}
.backCallback=${this._backTapped}
.header=${this._config.alias ||
.backCallback=${this.backTapped}
.header=${this.config.alias ||
this.hass.localize("ui.panel.config.script.editor.default_name")}
>
${this._mode === "gui" && !this.narrow
${this.mode === "gui" && !this.narrow
? html`<ha-icon-button
slot="toolbar-icon"
.label=${this.hass.localize("ui.common.undo")}
@@ -252,7 +207,7 @@ export class HaScriptEditor extends SubscribeMixin(
.path=${mdiDotsVertical}
></ha-icon-button>
${this._mode === "gui" && this.narrow
${this.mode === "gui" && this.narrow
? html`<ha-dropdown-item
value="undo"
.disabled=${!this._undoRedoController.canUndo}
@@ -286,7 +241,7 @@ export class HaScriptEditor extends SubscribeMixin(
<ha-dropdown-item .disabled=${!stateObj} value="category">
${this.hass.localize(
`ui.panel.config.scene.picker.${this._registryEntry?.categories?.script ? "edit_category" : "assign_category"}`
`ui.panel.config.scene.picker.${this.registryEntry?.categories?.script ? "edit_category" : "assign_category"}`
)}
<ha-svg-icon slot="icon" .path=${mdiTag}></ha-svg-icon>
</ha-dropdown-item>
@@ -307,10 +262,10 @@ export class HaScriptEditor extends SubscribeMixin(
></ha-svg-icon>
</ha-dropdown-item>`
: nothing}
${!useBlueprint && !("fields" in this._config)
${!useBlueprint && !("fields" in this.config)
? html`
<ha-dropdown-item
.disabled=${this._readOnly || this._mode === "yaml"}
.disabled=${this.readOnly || this.mode === "yaml"}
value="add_fields"
>
${this.hass.localize(
@@ -326,9 +281,7 @@ export class HaScriptEditor extends SubscribeMixin(
<ha-dropdown-item
value="rename"
.disabled=${!this.scriptId ||
this._readOnly ||
this._mode === "yaml"}
.disabled=${!this.scriptId || this.readOnly || this.mode === "yaml"}
>
${this.hass.localize("ui.panel.config.script.editor.rename")}
<ha-svg-icon slot="icon" .path=${mdiRenameBox}></ha-svg-icon>
@@ -337,7 +290,7 @@ export class HaScriptEditor extends SubscribeMixin(
? html`
<ha-dropdown-item
value="change_mode"
.disabled=${this._readOnly || this._mode === "yaml"}
.disabled=${this.readOnly || this.mode === "yaml"}
>
${this.hass.localize(
"ui.panel.config.script.editor.change_mode"
@@ -351,12 +304,12 @@ export class HaScriptEditor extends SubscribeMixin(
: nothing}
<ha-dropdown-item
.disabled=${!!this._blueprintConfig ||
(!this._readOnly && !this.scriptId)}
.disabled=${!!this.blueprintConfig ||
(!this.readOnly && !this.scriptId)}
value="duplicate"
>
${this.hass.localize(
this._readOnly
this.readOnly
? "ui.panel.config.script.editor.migrate"
: "ui.panel.config.script.editor.duplicate"
)}
@@ -370,7 +323,7 @@ export class HaScriptEditor extends SubscribeMixin(
? html`
<ha-dropdown-item
value="take_control"
.disabled=${this._readOnly}
.disabled=${this.readOnly}
>
${this.hass.localize(
"ui.panel.config.script.editor.take_control"
@@ -382,7 +335,7 @@ export class HaScriptEditor extends SubscribeMixin(
<ha-dropdown-item value="toggle_yaml_mode">
${this.hass.localize(
`ui.panel.config.automation.editor.edit_${this._mode === "gui" ? "yaml" : "ui"}`
`ui.panel.config.automation.editor.edit_${this.mode === "gui" ? "yaml" : "ui"}`
)}
<ha-svg-icon slot="icon" .path=${mdiPlaylistEdit}></ha-svg-icon>
</ha-dropdown-item>
@@ -390,7 +343,7 @@ export class HaScriptEditor extends SubscribeMixin(
<wa-divider></wa-divider>
<ha-dropdown-item
.disabled=${this._readOnly || !this.scriptId}
.disabled=${this.readOnly || !this.scriptId}
value="delete"
.variant=${this.scriptId ? "danger" : "default"}
>
@@ -403,8 +356,8 @@ export class HaScriptEditor extends SubscribeMixin(
</ha-svg-icon>
</ha-dropdown-item>
</ha-dropdown>
<div class=${this._mode === "yaml" ? "yaml-mode" : ""}>
${this._mode === "gui"
<div class=${this.mode === "yaml" ? "yaml-mode" : ""}>
${this.mode === "gui"
? html`
<div>
${useBlueprint
@@ -413,10 +366,10 @@ export class HaScriptEditor extends SubscribeMixin(
.hass=${this.hass}
.narrow=${this.narrow}
.isWide=${this.isWide}
.config=${this._config}
.disabled=${this._readOnly}
.saving=${this._saving}
.dirty=${this._dirty}
.config=${this.config}
.disabled=${this.readOnly}
.saving=${this.saving}
.dirty=${this.dirty}
@value-changed=${this._valueChanged}
@save-script=${this._handleSaveScript}
></blueprint-script-editor>
@@ -426,16 +379,16 @@ export class HaScriptEditor extends SubscribeMixin(
.hass=${this.hass}
.narrow=${this.narrow}
.isWide=${this.isWide}
.config=${this._config}
.disabled=${this._readOnly}
.dirty=${this._dirty}
.saving=${this._saving}
.config=${this.config}
.disabled=${this.readOnly}
.dirty=${this.dirty}
.saving=${this.saving}
@value-changed=${this._valueChanged}
@editor-save=${this._handleSaveScript}
@save-script=${this._handleSaveScript}
>
<div class="alert-wrapper" slot="alerts">
${this._errors || stateObj?.state === UNAVAILABLE
${this.errors || stateObj?.state === UNAVAILABLE
? html`<ha-alert
alert-type="error"
.title=${stateObj?.state === UNAVAILABLE
@@ -444,7 +397,7 @@ export class HaScriptEditor extends SubscribeMixin(
)
: undefined}
>
${this._errors || this._validationErrors}
${this.errors || this.validationErrors}
${stateObj?.state === UNAVAILABLE
? html`<ha-svg-icon
slot="icon"
@@ -453,7 +406,7 @@ export class HaScriptEditor extends SubscribeMixin(
: nothing}
</ha-alert>`
: nothing}
${this._blueprintConfig
${this.blueprintConfig
? html`<ha-alert alert-type="info">
${this.hass.localize(
"ui.panel.config.script.editor.confirm_take_control"
@@ -461,21 +414,21 @@ export class HaScriptEditor extends SubscribeMixin(
<div slot="action" style="display: flex;">
<ha-button
appearance="plain"
@click=${this._takeControlSave}
@click=${this.takeControlSave}
>${this.hass.localize(
"ui.common.yes"
)}</ha-button
>
<ha-button
appearance="plain"
@click=${this._revertBlueprint}
@click=${this.revertBlueprint}
>${this.hass.localize(
"ui.common.no"
)}</ha-button
>
</div>
</ha-alert>`
: this._readOnly
: this.readOnly
? html`<ha-alert
alert-type="warning"
dismissable
@@ -498,11 +451,11 @@ export class HaScriptEditor extends SubscribeMixin(
`}
</div>
`
: this._mode === "yaml"
: this.mode === "yaml"
? html`<ha-yaml-editor
.hass=${this.hass}
.defaultValue=${this._preprocessYaml()}
.readOnly=${this._readOnly}
.readOnly=${this.readOnly}
disable-fullscreen
@value-changed=${this._yamlChanged}
@editor-save=${this._handleSaveScript}
@@ -510,9 +463,9 @@ export class HaScriptEditor extends SubscribeMixin(
></ha-yaml-editor>
<ha-fab
slot="fab"
class=${!this._readOnly && this._dirty ? "dirty" : ""}
class=${!this.readOnly && this.dirty ? "dirty" : ""}
.label=${this.hass.localize("ui.common.save")}
.disabled=${this._saving}
.disabled=${this.saving}
extended
@click=${this._handleSaveScript}
>
@@ -551,26 +504,26 @@ export class HaScriptEditor extends SubscribeMixin(
const entity = this.entityRegistry.find(
(ent) => ent.platform === "script" && ent.unique_id === this.scriptId
);
this._entityId = entity?.entity_id;
this.currentEntityId = entity?.entity_id;
}
if (changedProps.has("scriptId") && !this.scriptId && this.hass) {
const initData = getScriptEditorInitData();
this._dirty = !!initData;
this.dirty = !!initData;
const baseConfig: Partial<ScriptConfig> = {};
if (!initData || !("use_blueprint" in initData)) {
baseConfig.sequence = [];
}
this._config = {
this.config = {
...baseConfig,
...initData,
} as ScriptConfig;
this._readOnly = false;
this.readOnly = false;
}
if (changedProps.has("entityId") && this.entityId) {
getScriptStateConfig(this.hass, this.entityId).then((c) => {
this._config = normalizeScriptConfig(c.config);
this.config = normalizeScriptConfig(c.config);
this._checkValidation();
});
const regEntry = this.entityRegistry.find(
@@ -579,25 +532,25 @@ export class HaScriptEditor extends SubscribeMixin(
if (regEntry?.unique_id) {
this.scriptId = regEntry.unique_id;
}
this._entityId = this.entityId;
this._dirty = false;
this._readOnly = true;
this.currentEntityId = this.entityId;
this.dirty = false;
this.readOnly = true;
}
}
private async _checkValidation() {
this._validationErrors = undefined;
if (!this._entityId || !this._config) {
this.validationErrors = undefined;
if (!this.currentEntityId || !this.config) {
return;
}
const stateObj = this.hass.states[this._entityId];
const stateObj = this.hass.states[this.currentEntityId];
if (stateObj?.state !== UNAVAILABLE) {
return;
}
const validation = await validateConfig(this.hass, {
actions: this._config.sequence,
actions: this.config.sequence,
});
this._validationErrors = (
this.validationErrors = (
Object.entries(validation) as Entries<typeof validation>
).map(([key, value]) =>
value.valid
@@ -612,13 +565,13 @@ export class HaScriptEditor extends SubscribeMixin(
private async _loadConfig() {
fetchScriptFileConfig(this.hass, this.scriptId!).then(
(config) => {
this._dirty = false;
this._readOnly = false;
this._config = normalizeScriptConfig(config);
this.dirty = false;
this.readOnly = false;
this.config = normalizeScriptConfig(config);
const entity = this.entityRegistry.find(
(ent) => ent.platform === "script" && ent.unique_id === this.scriptId
);
this._entityId = entity?.entity_id;
this.currentEntityId = entity?.entity_id;
this._checkValidation();
},
(resp) => {
@@ -647,19 +600,19 @@ export class HaScriptEditor extends SubscribeMixin(
}
private _valueChanged(ev) {
if (this._config) {
this._undoRedoController.commit(this._config);
if (this.config) {
this._undoRedoController.commit(this.config);
}
this._config = ev.detail.value;
this._errors = undefined;
this._dirty = true;
this.config = ev.detail.value;
this.errors = undefined;
this.dirty = true;
}
private async _runScript() {
if (hasScriptFields(this.hass, this._entityId!)) {
if (hasScriptFields(this.hass, this.currentEntityId!)) {
showMoreInfoDialog(this, {
entityId: this._entityId!,
entityId: this.currentEntityId!,
});
return;
}
@@ -667,20 +620,13 @@ export class HaScriptEditor extends SubscribeMixin(
await triggerScript(this.hass, this.scriptId!);
showToast(this, {
message: this.hass.localize("ui.notification_toast.triggered", {
name: this._config!.alias,
name: this.config!.alias,
}),
});
}
private _showSettings() {
showMoreInfoDialog(this, {
entityId: this._entityId!,
view: "settings",
});
}
private _editCategory() {
if (!this._registryEntry) {
if (!this.registryEntry) {
showAlertDialog(this, {
title: this.hass.localize(
"ui.panel.config.scene.picker.no_category_support"
@@ -693,7 +639,7 @@ export class HaScriptEditor extends SubscribeMixin(
}
showAssignCategoryDialog(this, {
scope: "script",
entityReg: this._registryEntry,
entityReg: this.registryEntry,
});
}
@@ -730,7 +676,7 @@ export class HaScriptEditor extends SubscribeMixin(
private async _showTrace() {
if (this.scriptId) {
const result = await this._confirmUnsavedChanged();
const result = await this.confirmUnsavedChanged();
if (result) {
navigate(`/config/script/trace/${this.scriptId}`);
}
@@ -738,47 +684,47 @@ export class HaScriptEditor extends SubscribeMixin(
}
private _addFields() {
if ("fields" in this._config!) {
if ("fields" in this.config!) {
return;
}
if (this._config) {
this._undoRedoController.commit(this._config);
if (this.config) {
this._undoRedoController.commit(this.config);
}
this._manualEditor?.addFields();
this._dirty = true;
this.dirty = true;
}
private _preprocessYaml() {
return this._config;
return this.config;
}
private _yamlChanged(ev: CustomEvent) {
ev.stopPropagation();
this._dirty = true;
this.dirty = true;
if (!ev.detail.isValid) {
this._yamlErrors = ev.detail.errorMsg;
this.yamlErrors = ev.detail.errorMsg;
return;
}
this._yamlErrors = undefined;
this._config = ev.detail.value;
this._errors = undefined;
this.yamlErrors = undefined;
this.config = ev.detail.value;
this.errors = undefined;
}
private async _confirmUnsavedChanged(): Promise<boolean> {
if (!this._dirty) {
protected async confirmUnsavedChanged(): Promise<boolean> {
if (!this.dirty) {
return true;
}
return new Promise<boolean>((resolve) => {
showAutomationSaveDialog(this, {
config: this._config!,
config: this.config!,
domain: "script",
updateConfig: async (config, entityRegistryUpdate) => {
this._config = config;
this._entityRegistryUpdate = entityRegistryUpdate;
this._dirty = true;
this.config = config;
this.entityRegistryUpdate = entityRegistryUpdate;
this.dirty = true;
this.requestUpdate();
const id = this.scriptId || String(Date.now());
@@ -794,8 +740,8 @@ export class HaScriptEditor extends SubscribeMixin(
},
onClose: () => resolve(false),
onDiscard: () => resolve(true),
entityRegistryUpdate: this._entityRegistryUpdate,
entityRegistryEntry: this._registryEntry,
entityRegistryUpdate: this.entityRegistryUpdate,
entityRegistryEntry: this.registryEntry,
title: this.hass.localize(
this.scriptId
? "ui.panel.config.script.editor.leave.unsaved_confirm_title"
@@ -811,15 +757,8 @@ export class HaScriptEditor extends SubscribeMixin(
});
}
private _backTapped = async () => {
const result = await this._confirmUnsavedChanged();
if (result) {
afterNextRender(() => goBack("/config"));
}
};
private async _takeControl() {
const config = this._config as BlueprintScriptConfig;
const config = this.config as BlueprintScriptConfig;
try {
const result = await substituteBlueprint(
@@ -835,35 +774,20 @@ export class HaScriptEditor extends SubscribeMixin(
description: config.description,
};
this._blueprintConfig = config;
this._config = newConfig;
if (this._mode === "yaml") {
this.renderRoot.querySelector("ha-yaml-editor")?.setValue(this._config);
this.blueprintConfig = config;
this.config = newConfig;
if (this.mode === "yaml") {
this.renderRoot.querySelector("ha-yaml-editor")?.setValue(this.config);
}
this._readOnly = true;
this._errors = undefined;
this.readOnly = true;
this.errors = undefined;
} catch (err: any) {
this._errors = err.message;
this.errors = err.message;
}
}
private _revertBlueprint() {
this._config = this._blueprintConfig;
if (this._mode === "yaml") {
this.renderRoot.querySelector("ha-yaml-editor")?.setValue(this._config);
}
this._blueprintConfig = undefined;
this._readOnly = false;
}
private _takeControlSave() {
this._readOnly = false;
this._dirty = true;
this._blueprintConfig = undefined;
}
private async _duplicate() {
const result = this._readOnly
const result = this.readOnly
? await showConfirmationDialog(this, {
title: this.hass.localize(
"ui.panel.config.script.picker.migrate_script"
@@ -872,14 +796,14 @@ export class HaScriptEditor extends SubscribeMixin(
"ui.panel.config.script.picker.migrate_script_description"
),
})
: await this._confirmUnsavedChanged();
: await this.confirmUnsavedChanged();
if (result) {
this._entityId = undefined;
this.currentEntityId = undefined;
showScriptEditor({
...this._config,
alias: this._readOnly
? this._config?.alias
: `${this._config?.alias} (${this.hass.localize(
...this.config,
alias: this.readOnly
? this.config?.alias
: `${this.config?.alias} (${this.hass.localize(
"ui.panel.config.script.picker.duplicate"
)})`,
});
@@ -893,7 +817,7 @@ export class HaScriptEditor extends SubscribeMixin(
),
text: this.hass.localize(
"ui.panel.config.script.editor.delete_confirm_text",
{ name: this._config?.alias }
{ name: this.config?.alias }
),
confirmText: this.hass!.localize("ui.common.delete"),
destructive: true,
@@ -907,42 +831,20 @@ export class HaScriptEditor extends SubscribeMixin(
goBack("/config");
}
private async _switchUiMode() {
if (this._yamlErrors) {
const result = await showConfirmationDialog(this, {
text: html`${this.hass.localize(
"ui.panel.config.automation.editor.switch_ui_yaml_error"
)}<br /><br />${this._yamlErrors}`,
confirmText: this.hass!.localize("ui.common.continue"),
destructive: true,
dismissText: this.hass!.localize("ui.common.cancel"),
});
if (!result) {
return;
}
}
this._yamlErrors = undefined;
this._mode = "gui";
}
private _switchYamlMode() {
this._mode = "yaml";
}
private async _promptScriptAlias(): Promise<boolean> {
return new Promise((resolve) => {
showAutomationSaveDialog(this, {
config: this._config!,
config: this.config!,
domain: "script",
updateConfig: async (config, entityRegistryUpdate) => {
this._config = config;
this._entityRegistryUpdate = entityRegistryUpdate;
this._dirty = true;
this.config = config;
this.entityRegistryUpdate = entityRegistryUpdate;
this.dirty = true;
this.requestUpdate();
resolve(true);
},
onClose: () => resolve(false),
entityRegistryUpdate: this._entityRegistryUpdate,
entityRegistryUpdate: this.entityRegistryUpdate,
entityRegistryEntry: this.entityRegistry.find(
(entry) => entry.unique_id === this.scriptId
),
@@ -953,10 +855,10 @@ export class HaScriptEditor extends SubscribeMixin(
private async _promptScriptMode(): Promise<void> {
return new Promise((resolve) => {
showAutomationModeDialog(this, {
config: this._config!,
config: this.config!,
updateConfig: (config) => {
this._config = config;
this._dirty = true;
this.config = config;
this.dirty = true;
this.requestUpdate();
resolve();
},
@@ -966,9 +868,9 @@ export class HaScriptEditor extends SubscribeMixin(
}
private async _handleSaveScript() {
if (this._yamlErrors) {
if (this.yamlErrors) {
showToast(this, {
message: this._yamlErrors,
message: this.yamlErrors,
});
return;
}
@@ -980,9 +882,9 @@ export class HaScriptEditor extends SubscribeMixin(
if (!saved) {
return;
}
this._entityId = this._computeEntityIdFromAlias(this._config!.alias);
this.currentEntityId = this._computeEntityIdFromAlias(this.config!.alias);
}
const id = this.scriptId || this._entityId || Date.now();
const id = this.scriptId || this.currentEntityId || Date.now();
await this._saveScript(id);
if (!this.scriptId) {
@@ -991,13 +893,13 @@ export class HaScriptEditor extends SubscribeMixin(
}
private async _saveScript(id): Promise<void> {
this._saving = true;
this.saving = true;
let entityRegPromise: Promise<EntityRegistryEntry> | undefined;
if (this._entityRegistryUpdate !== undefined && !this.scriptId) {
if (this.entityRegistryUpdate !== undefined && !this.scriptId) {
this._newScriptId = id.toString();
entityRegPromise = new Promise<EntityRegistryEntry>((resolve) => {
this._entityRegCreated = resolve;
this.entityRegCreated = resolve;
});
}
@@ -1005,11 +907,11 @@ export class HaScriptEditor extends SubscribeMixin(
await this.hass!.callApi(
"POST",
"config/script/config/" + id,
this._config
this.config
);
if (this._entityRegistryUpdate !== undefined) {
let entityId = this._entityId;
if (this.entityRegistryUpdate !== undefined) {
let entityId = this.currentEntityId;
// wait for new script to appear in entity registry
if (entityRegPromise) {
@@ -1044,23 +946,23 @@ export class HaScriptEditor extends SubscribeMixin(
if (entityId) {
await updateEntityRegistryEntry(this.hass, entityId, {
categories: {
script: this._entityRegistryUpdate.category || null,
script: this.entityRegistryUpdate.category || null,
},
labels: this._entityRegistryUpdate.labels || [],
area_id: this._entityRegistryUpdate.area || null,
labels: this.entityRegistryUpdate.labels || [],
area_id: this.entityRegistryUpdate.area || null,
});
}
}
this._dirty = false;
this.dirty = false;
} catch (errors: any) {
this._errors = errors.body?.message || errors.error || errors.body;
this.errors = errors.body?.message || errors.error || errors.body;
showToast(this, {
message: errors.body?.message || errors.error || errors.body,
});
throw errors;
} finally {
this._saving = false;
this.saving = false;
}
}
@@ -1077,14 +979,6 @@ export class HaScriptEditor extends SubscribeMixin(
};
}
protected get isDirty() {
return this._dirty;
}
protected async promptDiscardChanges() {
return this._confirmUnsavedChanged();
}
// @ts-ignore
private _collapseAll() {
this._manualEditor?.collapseAll();
@@ -1109,8 +1003,8 @@ export class HaScriptEditor extends SubscribeMixin(
private _applyUndoRedo(config: ScriptConfig) {
this._manualEditor?.triggerCloseSidebar();
this._config = config;
this._dirty = true;
this.config = config;
this.dirty = true;
}
private _undo() {
@@ -1139,7 +1033,7 @@ export class HaScriptEditor extends SubscribeMixin(
this._showInfo();
break;
case "settings":
this._showSettings();
this.showSettings();
break;
case "category":
this._editCategory();
@@ -1163,11 +1057,11 @@ export class HaScriptEditor extends SubscribeMixin(
this._takeControl();
break;
case "toggle_yaml_mode":
if (this._mode === "gui") {
this._switchYamlMode();
if (this.mode === "gui") {
this.switchYamlMode();
break;
}
this._switchUiMode();
this.switchUiMode();
break;
case "delete":
this._deleteConfirm();
@@ -1181,19 +1075,8 @@ export class HaScriptEditor extends SubscribeMixin(
static get styles(): CSSResultGroup {
return [
haStyle,
automationScriptEditorStyles,
css`
:host {
--ha-automation-editor-max-width: var(
--ha-automation-editor-width,
1540px
);
}
.yaml-mode {
height: 100%;
display: flex;
flex-direction: column;
padding-bottom: 0;
}
manual-script-editor,
blueprint-script-editor {
margin: 0 auto;
@@ -1244,29 +1127,9 @@ export class HaScriptEditor extends SubscribeMixin(
padding: 0 12px;
}
ha-yaml-editor {
flex-grow: 1;
--actions-border-radius: var(--ha-border-radius-square);
--code-mirror-height: 100%;
min-height: 0;
display: flex;
flex-direction: column;
}
p {
margin-bottom: 0;
}
span[slot="introduction"] a {
color: var(--primary-color);
}
ha-fab {
position: fixed;
right: 16px;
bottom: calc(-80px - var(--safe-area-inset-bottom));
transition: bottom 0.3s;
}
ha-fab.dirty {
bottom: calc(16px + var(--safe-area-inset-bottom, 0px));
}
.header {
display: flex;
margin: 16px 0;
@@ -1280,15 +1143,6 @@ export class HaScriptEditor extends SubscribeMixin(
.header a {
color: var(--secondary-text-color);
}
ha-tooltip ha-svg-icon {
width: 12px;
}
ha-tooltip .shortcut {
display: inline-flex;
flex-direction: row;
align-items: center;
gap: 2px;
}
`,
];
}