Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/data/automation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -616,6 +616,7 @@ export interface ActionSidebarConfig extends BaseSidebarConfig {
save: (value: Action) => void;
rename: () => void;
disable: () => void;
continueOnError: () => void;
duplicate: () => void;
cut: () => void;
copy: () => void;
Expand Down
45 changes: 45 additions & 0 deletions src/panels/config/automation/action/ha-automation-action-row.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { consume } from "@lit/context";
import {
mdiAlertCircleCheck,
mdiAlertCircleCheckOutline,
mdiAppleKeyboardCommand,
mdiArrowDown,
mdiArrowUp,
Expand Down Expand Up @@ -444,6 +445,28 @@ export default class HaAutomationActionRow extends LitElement {
)
)}
</ha-md-menu-item>
${type !== "condition"
? html`
<ha-md-menu-item
.clickAction=${this._onContinueOnError}
.disabled=${this.disabled}
>
<ha-svg-icon
slot="start"
.path=${(this.action as NonConditionAction)
.continue_on_error === true
? mdiAlertCircleCheck
: mdiAlertCircleCheckOutline}
></ha-svg-icon>

${this._renderOverflowLabel(
this.hass.localize(
"ui.panel.config.automation.editor.actions.continue_on_error"
)
)}
Comment on lines +454 to +466
Copy link
Member

@karwosts karwosts Nov 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't feel like with just the icon changing between two very similar variants, and the label staying static, that I would really be able to tell what state this was in.

A checked/unchecked checkbox would be more intuitive maybe?

Other toggles also change their verbiage between state e.g. Enable / Disable, but I don't know if "Enable Continue on Error / Disable Continue on Error" is uncomfortably long for the dropdown.

I would maybe wait for UX review before making any changes though.

</ha-md-menu-item>
`
: nothing}
<ha-md-menu-item
class="warning"
.clickAction=${this._onDelete}
Expand Down Expand Up @@ -609,6 +632,27 @@ export default class HaAutomationActionRow extends LitElement {
}
};

private _onContinueOnError = () => {
const continueOnError = !(
(this.action as NonConditionAction).continue_on_error ?? false
);
const value = continueOnError
? { ...this.action, continue_on_error: true }
: { ...this.action };
if (!continueOnError) {
delete (value as NonConditionAction).continue_on_error;
}
fireEvent(this, "value-changed", { value });

if (this._selected && this.optionsInSidebar) {
this.openSidebar(value);
}

if (this._yamlMode && !this.optionsInSidebar) {
this._actionEditor?.yamlEditor?.setValue(value);
}
};

private _runAction = async () => {
requestAnimationFrame(() => {
// @ts-ignore is supported in all browsers except firefox
Expand Down Expand Up @@ -818,6 +862,7 @@ export default class HaAutomationActionRow extends LitElement {
this.openSidebar();
},
disable: this._onDisable,
continueOnError: this._onContinueOnError,
delete: this._onDelete,
copy: this._copyAction,
cut: this._cutAction,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import {
mdiAlertCircleCheck,
mdiAlertCircleCheckOutline,
mdiAppleKeyboardCommand,
mdiContentCopy,
mdiContentCut,
Expand All @@ -21,7 +23,11 @@ import "../../../../components/ha-md-menu-item";
import { ACTION_BUILDING_BLOCKS } from "../../../../data/action";
import type { ActionSidebarConfig } from "../../../../data/automation";
import { domainToName } from "../../../../data/integration";
import type { RepeatAction, ServiceAction } from "../../../../data/script";
import type {
NonConditionAction,
RepeatAction,
ServiceAction,
} from "../../../../data/script";
import type { HomeAssistant } from "../../../../types";
import { isMac } from "../../../../util/is_mac";
import type HaAutomationConditionEditor from "../action/ha-automation-action-editor";
Expand Down Expand Up @@ -249,6 +255,29 @@ export default class HaAutomationSidebarAction extends LitElement {
<span class="shortcut-placeholder ${isMac ? "mac" : ""}"></span>
</div>
</ha-md-menu-item>
${actionType !== "condition"
? html`
<ha-md-menu-item
slot="menu-items"
.clickAction=${this.config.continueOnError}
.disabled=${this.disabled}
>
<ha-svg-icon
slot="start"
.path=${(actionConfig as NonConditionAction)
.continue_on_error === true
? mdiAlertCircleCheck
: mdiAlertCircleCheckOutline}
></ha-svg-icon>
<div class="overflow-label">
${this.hass.localize(
"ui.panel.config.automation.editor.actions.continue_on_error"
)}
<span class="shortcut-placeholder ${isMac ? "mac" : ""}"></span>
</div>
</ha-md-menu-item>
`
: nothing}
<ha-md-menu-item
slot="menu-items"
.clickAction=${this.config.delete}
Expand Down
113 changes: 113 additions & 0 deletions test/panels/config/automation/action/continue-on-error-toggle.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { describe, it, expect } from "vitest";
import type { NonConditionAction } from "../../../../../src/data/script";

/**
* Helper function that mirrors the toggle logic from ha-automation-action-row.ts
* This tests the core logic without needing to instantiate the full component.
*/
Comment on lines +4 to +7
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like this test is 100% synthetic, it doesn't test any real code from the design? I wouldn't think we would want that, doesn't seem useful to me. Otherwise it provides no benefit going forward.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I'll drop that commit

function toggleContinueOnError(action: NonConditionAction): NonConditionAction {
const continueOnError = !(action.continue_on_error ?? false);
if (continueOnError) {
return { ...action, continue_on_error: true };
}
const result = { ...action };
delete result.continue_on_error;
return result;
}

describe("continue_on_error toggle", () => {
it("should enable continue_on_error when currently undefined", () => {
const action: NonConditionAction = {
action: "light.turn_on",
target: { entity_id: "light.bedroom" },
};

const result = toggleContinueOnError(action);

expect(result.continue_on_error).toBe(true);
expect(result.action).toBe("light.turn_on");

Check failure on line 28 in test/panels/config/automation/action/continue-on-error-toggle.test.ts

View workflow job for this annotation

GitHub Actions / Lint and check format

Property 'action' does not exist on type 'NonConditionAction'.
});

it("should enable continue_on_error when currently false", () => {
const action: NonConditionAction = {
action: "light.turn_on",
continue_on_error: false,
};

const result = toggleContinueOnError(action);

expect(result.continue_on_error).toBe(true);
});

it("should remove continue_on_error when currently true", () => {
const action: NonConditionAction = {
action: "light.turn_on",
target: { entity_id: "light.bedroom" },
continue_on_error: true,
};

const result = toggleContinueOnError(action);

expect(result.continue_on_error).toBeUndefined();
expect(result.action).toBe("light.turn_on");

Check failure on line 52 in test/panels/config/automation/action/continue-on-error-toggle.test.ts

View workflow job for this annotation

GitHub Actions / Lint and check format

Property 'action' does not exist on type 'NonConditionAction'.
expect(result.target).toEqual({ entity_id: "light.bedroom" });

Check failure on line 53 in test/panels/config/automation/action/continue-on-error-toggle.test.ts

View workflow job for this annotation

GitHub Actions / Lint and check format

Property 'target' does not exist on type 'NonConditionAction'.
});

it("should preserve other action properties when toggling on", () => {
const action: NonConditionAction = {
alias: "Turn on bedroom light",
action: "light.turn_on",
target: { entity_id: "light.bedroom" },
data: { brightness: 255 },
enabled: true,
};

const result = toggleContinueOnError(action);

expect(result.continue_on_error).toBe(true);
expect(result.alias).toBe("Turn on bedroom light");
expect(result.action).toBe("light.turn_on");

Check failure on line 69 in test/panels/config/automation/action/continue-on-error-toggle.test.ts

View workflow job for this annotation

GitHub Actions / Lint and check format

Property 'action' does not exist on type 'NonConditionAction'.
expect(result.target).toEqual({ entity_id: "light.bedroom" });

Check failure on line 70 in test/panels/config/automation/action/continue-on-error-toggle.test.ts

View workflow job for this annotation

GitHub Actions / Lint and check format

Property 'target' does not exist on type 'NonConditionAction'.
expect(result.data).toEqual({ brightness: 255 });

Check failure on line 71 in test/panels/config/automation/action/continue-on-error-toggle.test.ts

View workflow job for this annotation

GitHub Actions / Lint and check format

Property 'data' does not exist on type 'NonConditionAction'.
expect(result.enabled).toBe(true);
});

it("should preserve other action properties when toggling off", () => {
const action: NonConditionAction = {
alias: "Turn on bedroom light",
action: "light.turn_on",
target: { entity_id: "light.bedroom" },
continue_on_error: true,
enabled: false,
};

const result = toggleContinueOnError(action);

expect(result.continue_on_error).toBeUndefined();
expect(result.alias).toBe("Turn on bedroom light");
expect(result.enabled).toBe(false);
});

it("should work with delay action", () => {
const action: NonConditionAction = {
delay: "00:00:05",
};

const result = toggleContinueOnError(action);

expect(result.continue_on_error).toBe(true);
expect((result as any).delay).toBe("00:00:05");
});

it("should work with event action", () => {
const action: NonConditionAction = {
event: "custom_event",
event_data: { key: "value" },
};

const result = toggleContinueOnError(action);

expect(result.continue_on_error).toBe(true);
expect((result as any).event).toBe("custom_event");
});
});
Loading