Compare commits

..

2 Commits

Author SHA1 Message Date
Paul Bottein
5d9044e07a Refactor summaries view code 2025-08-21 11:58:48 +02:00
Paul Bottein
e0a24ca641 Remove floors from the area overview dashboard 2025-08-20 18:50:11 +02:00
52 changed files with 1039 additions and 3031 deletions

View File

@@ -1,6 +1,6 @@
export default {
"*.?(c|m){js,ts}": [
"eslint --flag v10_config_lookup_from_file --cache --cache-strategy=content --cache-location=node_modules/.cache/eslint/.eslintcache --fix",
"eslint --flag unstable_config_lookup_from_file --cache --cache-strategy=content --cache-location=node_modules/.cache/eslint/.eslintcache --fix",
"prettier --cache --write",
"lit-analyzer --quiet",
],

View File

@@ -8,8 +8,8 @@
"version": "1.0.0",
"scripts": {
"build": "script/build_frontend",
"lint:eslint": "eslint --flag v10_config_lookup_from_file \"**/src/**/*.{js,ts,html}\" --cache --cache-strategy=content --cache-location=node_modules/.cache/eslint/.eslintcache --ignore-pattern=.gitignore --max-warnings=0",
"format:eslint": "eslint --flag v10_config_lookup_from_file \"**/src/**/*.{js,ts,html}\" --cache --cache-strategy=content --cache-location=node_modules/.cache/eslint/.eslintcache --ignore-pattern=.gitignore --fix",
"lint:eslint": "eslint --flag unstable_config_lookup_from_file \"**/src/**/*.{js,ts,html}\" --cache --cache-strategy=content --cache-location=node_modules/.cache/eslint/.eslintcache --ignore-pattern=.gitignore --max-warnings=0",
"format:eslint": "eslint --flag unstable_config_lookup_from_file \"**/src/**/*.{js,ts,html}\" --cache --cache-strategy=content --cache-location=node_modules/.cache/eslint/.eslintcache --ignore-pattern=.gitignore --fix",
"lint:prettier": "prettier . --cache --check",
"format:prettier": "prettier . --cache --write",
"lint:types": "tsc",
@@ -123,7 +123,7 @@
"lit": "3.3.1",
"lit-html": "3.3.1",
"luxon": "3.7.1",
"marked": "16.2.0",
"marked": "16.1.2",
"memoize-one": "6.0.0",
"node-vibrant": "4.0.3",
"object-hash": "3.0.0",

View File

@@ -31,7 +31,7 @@ export const computeEntityEntryName = (
entry.name ||
("original_name" in entry && entry.original_name != null
? String(entry.original_name)
: undefined);
: "");
const device = entry.device_id ? hass.devices[entry.device_id] : undefined;

View File

@@ -1,243 +0,0 @@
import { css, html, LitElement, type PropertyValues } from "lit";
import { customElement, property, query } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
const ANIMATION_DURATION_MS = 300;
@customElement("ha-bottom-sheet")
export class HaBottomSheet extends LitElement {
@query("dialog") private _dialog!: HTMLDialogElement;
@property({ attribute: false }) public contentHash?: string;
private _dragging = false;
private _dragStartY = 0;
private _initialSize = 0;
private _open = false;
render() {
return html`<dialog open>
<div class="handle-wrapper">
<div
@mousedown=${this._handleMouseDown}
@touchstart=${this._handleTouchStart}
class="handle"
></div>
</div>
<slot></slot>
</dialog>`;
}
protected firstUpdated(changedProperties) {
super.firstUpdated(changedProperties);
this._openSheet();
}
protected willUpdate(changedProperties: PropertyValues): void {
if (changedProperties.has("contentHash") && this._open) {
fireEvent(this, "bottom-sheet-opened");
}
}
private _openSheet() {
requestAnimationFrame(async () => {
this._dialog.classList.add("show");
setTimeout(() => {
this._dialog.style.setProperty(
"height",
`${(this._dialog.offsetHeight / window.innerHeight) * 100}vh`
);
this._dialog.style.setProperty("max-height", "90vh");
this._open = true;
fireEvent(this, "bottom-sheet-opened");
}, ANIMATION_DURATION_MS);
});
}
public closeSheet() {
requestAnimationFrame(() => {
this._dialog.classList.remove("show");
setTimeout(() => {
this._dialog.close();
fireEvent(this, "bottom-sheet-closed");
}, ANIMATION_DURATION_MS);
});
}
connectedCallback() {
super.connectedCallback();
document.addEventListener("mousemove", this._handleMouseMove);
document.addEventListener("mouseup", this._handleMouseUp);
// Use non-passive listeners so we can call preventDefault to block
// browser pull-to-refresh and page scrolling while dragging.
document.addEventListener("touchmove", this._handleTouchMove, {
passive: false,
});
document.addEventListener("touchend", this._handleTouchEnd);
document.addEventListener("touchcancel", this._handleTouchEnd);
}
disconnectedCallback() {
super.disconnectedCallback();
document.removeEventListener("mousemove", this._handleMouseMove);
document.removeEventListener("mouseup", this._handleMouseUp);
document.removeEventListener("touchmove", this._handleTouchMove);
document.removeEventListener("touchend", this._handleTouchEnd);
document.removeEventListener("touchcancel", this._handleTouchEnd);
}
private _handleMouseDown = (ev: MouseEvent) => {
this._startDrag(ev.clientY);
};
private _handleTouchStart = (ev: TouchEvent) => {
// Prevent the browser from interpreting this as a scroll/PTR gesture.
ev.preventDefault();
this._startDrag(ev.touches[0].clientY);
};
private _startDrag(clientY: number) {
this._dragging = true;
this._dragStartY = clientY;
this._initialSize = (this._dialog.offsetHeight / window.innerHeight) * 100;
document.body.style.cursor = "grabbing";
}
private _handleMouseMove = (ev: MouseEvent) => {
if (!this._dragging) return;
this._updateSize(ev.clientY);
};
private _handleTouchMove = (ev: TouchEvent) => {
if (!this._dragging) return;
ev.preventDefault(); // Prevent scrolling
this._updateSize(ev.touches[0].clientY);
};
private _updateSize(clientY: number) {
const deltaY = this._dragStartY - clientY;
const viewportHeight = window.innerHeight;
const deltaVh = (deltaY / viewportHeight) * 100;
// Calculate new size and clamp between 10vh and 90vh
let newSize = this._initialSize + deltaVh;
newSize = Math.max(10, Math.min(90, newSize));
// on drag down and below 20vh
if (newSize < 20 && deltaY < 0) {
this.closeSheet();
return;
}
this._dialog.style.setProperty("height", `${newSize}vh`);
}
private _handleMouseUp = () => {
this._endDrag();
};
private _handleTouchEnd = () => {
this._endDrag();
};
private _endDrag() {
if (!this._dragging) return;
this._dragging = false;
document.body.style.cursor = "";
}
static styles = css`
.handle-wrapper {
position: absolute;
top: 0;
width: 100%;
padding-bottom: 2px;
display: flex;
justify-content: center;
align-items: center;
cursor: grab;
touch-action: none;
}
.handle-wrapper .handle {
height: 20px;
width: 200px;
display: flex;
justify-content: center;
align-items: center;
z-index: 20;
}
.handle-wrapper .handle::after {
content: "";
border-radius: 8px;
height: 4px;
background: var(--divider-color, #e0e0e0);
width: 80px;
}
.handle-wrapper .handle:active::after {
cursor: grabbing;
}
dialog {
height: auto;
max-height: 70vh;
min-height: 30vh;
background-color: var(
--ha-dialog-surface-background,
var(--mdc-theme-surface, #fff)
);
display: flex;
flex-direction: column;
top: 0;
inset-inline-start: 0;
position: fixed;
width: calc(100% - 4px);
max-width: 100%;
overflow: hidden;
border: none;
box-shadow: var(--wa-shadow-l);
overflow: auto;
padding: 0;
margin: 0;
top: auto;
inset-inline-end: auto;
bottom: 0;
inset-inline-start: 0;
box-shadow: 0px -8px 16px rgba(0, 0, 0, 0.2);
border-top-left-radius: var(
--ha-dialog-border-radius,
var(--ha-border-radius-2xl)
);
border-top-right-radius: var(
--ha-dialog-border-radius,
var(--ha-border-radius-2xl)
);
transform: translateY(100%);
transition: transform ${ANIMATION_DURATION_MS}ms ease;
border-top-width: var(--ha-bottom-sheet-border-width);
border-right-width: var(--ha-bottom-sheet-border-width);
border-left-width: var(--ha-bottom-sheet-border-width);
border-bottom-width: 0;
border-style: var(--ha-bottom-sheet-border-style);
border-color: var(--ha-bottom-sheet-border-color);
}
dialog.show {
transform: translateY(0);
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-bottom-sheet": HaBottomSheet;
}
interface HASSDomEvents {
"bottom-sheet-closed": undefined;
"bottom-sheet-opened": undefined;
}
}

View File

@@ -8,6 +8,7 @@ import type {
LocalizeKeys,
} from "../../common/translations/localize";
import type { HomeAssistant } from "../../types";
import "../ha-alert";
import "../ha-form/ha-form";
const SELECTOR_DEFAULTS = {
@@ -155,8 +156,6 @@ export class HaSelectorSelector extends LitElement {
@property({ type: Boolean, reflect: true }) public disabled = false;
@property({ type: Boolean }) public narrow = false;
@property({ type: Boolean, reflect: true }) public required = true;
private _yamlMode = false;
@@ -173,10 +172,10 @@ export class HaSelectorSelector extends LitElement {
[
{
name: "type",
required: true,
selector: {
select: {
mode: "dropdown",
required: true,
options: Object.keys(SELECTOR_SCHEMAS)
.concat("manual")
.map((key) => ({
@@ -229,17 +228,17 @@ export class HaSelectorSelector extends LitElement {
const schema = this._schema(type, this.hass.localize);
return html`<div>
<p>${this.label ? this.label : ""}</p>
<ha-form
.hass=${this.hass}
.data=${data}
.schema=${schema}
.computeLabel=${this._computeLabelCallback}
@value-changed=${this._valueChanged}
.narrow=${this.narrow}
></ha-form>
</div>`;
return html`<ha-card>
<div class="card-content">
<p>${this.label ? this.label : ""}</p>
<ha-form
.hass=${this.hass}
.data=${data}
.schema=${schema}
.computeLabel=${this._computeLabelCallback}
@value-changed=${this._valueChanged}
></ha-form></div
></ha-card>`;
}
private _valueChanged(ev: CustomEvent) {
@@ -286,6 +285,23 @@ export class HaSelectorSelector extends LitElement {
) || schema.name;
static styles = css`
:host {
--expansion-panel-summary-padding: 0 16px;
}
ha-alert {
display: block;
margin-bottom: 16px;
}
ha-card {
margin: 0 0 16px 0;
}
ha-card.disabled {
pointer-events: none;
color: var(--disabled-text-color);
}
.card-content {
padding: 0px 16px 16px 16px;
}
.title {
font-size: var(--ha-font-size-l);
padding-top: 16px;

View File

@@ -7,24 +7,14 @@ import { navigate } from "../common/navigate";
import { createSearchParam } from "../common/url/search-params";
import type { Context, HomeAssistant } from "../types";
import type { BlueprintInput } from "./blueprint";
import { CONDITION_BUILDING_BLOCKS } from "./condition";
import type { DeviceCondition, DeviceTrigger } from "./device_automation";
import type { Action, Field, MODES } from "./script";
import type { Action, MODES } from "./script";
import { migrateAutomationAction } from "./script";
import { CONDITION_BUILDING_BLOCKS } from "./condition";
export const AUTOMATION_DEFAULT_MODE: (typeof MODES)[number] = "single";
export const AUTOMATION_DEFAULT_MAX = 10;
declare global {
interface HASSDomEvents {
/**
* Dispatched to open the automation editor.
* Used by custom cards/panels to trigger the editor view.
*/
"show-automation-editor": ShowAutomationEditorParams;
}
}
export interface AutomationEntity extends HassEntityBase {
attributes: HassEntityAttributeBase & {
id?: string;
@@ -523,14 +513,6 @@ export const isCondition = (config: unknown): boolean => {
return "condition" in condition && typeof condition.condition === "string";
};
export const isScriptField = (config: unknown): boolean => {
if (!config || typeof config !== "object") {
return false;
}
const field = config as Record<string, unknown>;
return "field" in field && typeof field.field === "object";
};
export const subscribeTrigger = (
hass: HomeAssistant,
onChange: (result: {
@@ -564,68 +546,3 @@ export interface AutomationClipboard {
condition?: Condition;
action?: Action;
}
export interface BaseSidebarConfig {
toggleYamlMode: () => boolean;
delete: () => void;
scrollIntoView: () => void;
}
export interface TriggerSidebarConfig extends BaseSidebarConfig {
save: (value: Trigger) => void;
close: () => void;
rename: () => void;
disable: () => void;
config: Trigger;
yamlMode: boolean;
uiSupported: boolean;
}
export interface ConditionSidebarConfig extends BaseSidebarConfig {
save: (value: Condition) => void;
close: () => void;
rename: () => void;
disable: () => void;
config: Condition;
yamlMode: boolean;
uiSupported: boolean;
}
export interface ActionSidebarConfig extends BaseSidebarConfig {
save: (value: Action) => void;
close: () => void;
rename: () => void;
disable: () => void;
config: Action;
yamlMode: boolean;
uiSupported: boolean;
}
export interface OptionSidebarConfig extends BaseSidebarConfig {
close: () => void;
rename: () => void;
}
export interface ScriptFieldSidebarConfig extends BaseSidebarConfig {
save: (value: Field) => void;
close: () => void;
config: {
field: Field;
selector: boolean;
key: string;
excludeKeys: string[];
};
yamlMode: boolean;
}
export type SidebarConfig =
| TriggerSidebarConfig
| ConditionSidebarConfig
| ActionSidebarConfig
| OptionSidebarConfig
| ScriptFieldSidebarConfig;
export interface ShowAutomationEditorParams {
data?: Partial<AutomationConfig>;
expanded?: boolean;
}

View File

@@ -1 +0,0 @@
export const SELECTOR_SELECTOR_BUILDING_BLOCKS = ["condition", "action"];

View File

@@ -41,7 +41,6 @@ import {
YAML_ONLY_ACTION_TYPES,
} from "../../../../data/action";
import type {
ActionSidebarConfig,
AutomationClipboard,
Condition,
} from "../../../../data/automation";
@@ -641,14 +640,10 @@ export default class HaAutomationActionRow extends LitElement {
this.openSidebar();
}
private _scrollIntoView = () => {
this.scrollIntoView({
block: "start",
behavior: "smooth",
});
};
public openSidebar(action?: Action): void {
if (this.narrow) {
this.scrollIntoView();
}
const sidebarAction = action ?? this.action;
const actionType = getAutomationActionType(sidebarAction);
@@ -670,10 +665,10 @@ export default class HaAutomationActionRow extends LitElement {
disable: this._onDisable,
delete: this._onDelete,
config: sidebarAction,
type: "action",
uiSupported: actionType ? this._uiSupported(actionType) : false,
yamlMode: this._yamlMode,
scrollIntoView: this._scrollIntoView,
} satisfies ActionSidebarConfig);
});
this._selected = true;
}

View File

@@ -167,6 +167,7 @@ export default class HaAutomationAction extends LitElement {
} else if (!this.optionsInSidebar) {
row.expand();
}
row.scrollIntoView();
row.focus();
});
}

View File

@@ -1,31 +1,24 @@
import { mdiClose, mdiPlus } from "@mdi/js";
import { dump } from "js-yaml";
import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { mdiClose, mdiPlus } from "@mdi/js";
import { dump } from "js-yaml";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/chips/ha-assist-chip";
import "../../../../components/chips/ha-chip-set";
import "../../../../components/ha-alert";
import "../../../../components/ha-area-picker";
import "../../../../components/ha-domain-icon";
import "../../../../components/ha-expansion-panel";
import "../../../../components/ha-icon-picker";
import "../../../../components/ha-labels-picker";
import "../../../../components/ha-suggest-with-ai-button";
import type { SuggestWithAIGenerateTask } from "../../../../components/ha-suggest-with-ai-button";
import "../../../../components/ha-svg-icon";
import "../../../../components/ha-textarea";
import "../../../../components/ha-textfield";
import "../../../../components/ha-labels-picker";
import "../../../../components/ha-suggest-with-ai-button";
import type { SuggestWithAIGenerateTask } from "../../../../components/ha-suggest-with-ai-button";
import "../../category/ha-category-picker";
import "../../../../components/ha-expansion-panel";
import "../../../../components/chips/ha-chip-set";
import "../../../../components/chips/ha-assist-chip";
import "../../../../components/ha-area-picker";
import { computeStateDomain } from "../../../../common/entity/compute_state_domain";
import { supportsMarkdownHelper } from "../../../../common/translations/markdown_support";
import { subscribeOne } from "../../../../common/util/subscribe-one";
import type { GenDataTaskResult } from "../../../../data/ai_task";
import { fetchCategoryRegistry } from "../../../../data/category_registry";
import { subscribeEntityRegistry } from "../../../../data/entity_registry";
import { subscribeLabelRegistry } from "../../../../data/label_registry";
import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
import { haStyle, haStyleDialog } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types";
@@ -33,6 +26,13 @@ import type {
EntityRegistryUpdate,
SaveDialogParams,
} from "./show-dialog-automation-save";
import { supportsMarkdownHelper } from "../../../../common/translations/markdown_support";
import type { GenDataTaskResult } from "../../../../data/ai_task";
import { computeStateDomain } from "../../../../common/entity/compute_state_domain";
import { subscribeOne } from "../../../../common/util/subscribe-one";
import { subscribeLabelRegistry } from "../../../../data/label_registry";
import { subscribeEntityRegistry } from "../../../../data/entity_registry";
import { fetchCategoryRegistry } from "../../../../data/category_registry";
@customElement("ha-dialog-automation-save")
class DialogAutomationSave extends LitElement implements HassDialog {
@@ -242,7 +242,7 @@ class DialogAutomationSave extends LitElement implements HassDialog {
const title = this.hass.localize(
this._params.config.alias
? "ui.panel.config.automation.editor.rename"
: "ui.common.save"
: "ui.panel.config.automation.editor.save"
);
return html`
@@ -289,7 +289,7 @@ class DialogAutomationSave extends LitElement implements HassDialog {
${this.hass.localize(
this._params.config.alias && !this._params.onDiscard
? "ui.panel.config.automation.editor.rename"
: "ui.common.save"
: "ui.panel.config.automation.editor.save"
)}
</ha-button>
</div>

View File

@@ -6,7 +6,6 @@ import { fireEvent } from "../../../common/dom/fire_event";
import "../../../components/ha-alert";
import "../../../components/ha-button";
import "../../../components/ha-markdown";
import "../../../components/ha-fab";
import type { BlueprintAutomationConfig } from "../../../data/automation";
import { fetchBlueprints } from "../../../data/blueprint";
import { HaBlueprintGenericEditor } from "../blueprint/blueprint-generic-editor";
@@ -59,7 +58,7 @@ export class HaBlueprintAutomationEditor extends HaBlueprintGenericEditor {
<ha-fab
slot="fab"
class=${this.dirty ? "dirty" : ""}
.label=${this.hass.localize("ui.common.save")}
.label=${this.hass.localize("ui.panel.config.automation.editor.save")}
.disabled=${this.saving}
extended
@click=${this._saveAutomation}

View File

@@ -35,7 +35,6 @@ import "../../../../components/ha-md-menu-item";
import type {
AutomationClipboard,
Condition,
ConditionSidebarConfig,
} from "../../../../data/automation";
import { testCondition } from "../../../../data/automation";
import { describeCondition } from "../../../../data/automation_i18n";
@@ -598,14 +597,11 @@ export default class HaAutomationConditionRow extends LitElement {
this.openSidebar();
}
private _scrollIntoView = () => {
this.scrollIntoView({
block: "start",
behavior: "smooth",
});
};
public openSidebar(condition?: Condition): void {
if (this.narrow) {
this.scrollIntoView();
}
const sidebarCondition = condition || this.condition;
fireEvent(this, "open-sidebar", {
save: (value) => {
@@ -625,10 +621,10 @@ export default class HaAutomationConditionRow extends LitElement {
disable: this._onDisable,
delete: this._onDelete,
config: sidebarCondition,
type: "condition",
uiSupported: this._uiSupported(sidebarCondition.condition),
yamlMode: this._yamlMode,
scrollIntoView: this._scrollIntoView,
} satisfies ConditionSidebarConfig);
});
this._selected = true;
}

View File

@@ -16,7 +16,6 @@ import type {
AutomationClipboard,
Condition,
} from "../../../../data/automation";
import { CONDITION_BUILDING_BLOCKS } from "../../../../data/condition";
import type { HomeAssistant } from "../../../../types";
import {
PASTE_VALUE,
@@ -24,6 +23,7 @@ import {
} from "../show-add-automation-element-dialog";
import "./ha-automation-condition-row";
import type HaAutomationConditionRow from "./ha-automation-condition-row";
import { CONDITION_BUILDING_BLOCKS } from "../../../../data/condition";
@customElement("ha-automation-condition")
export default class HaAutomationCondition extends LitElement {
@@ -111,6 +111,7 @@ export default class HaAutomationCondition extends LitElement {
} else if (!this.optionsInSidebar) {
row.expand();
}
row.scrollIntoView();
row.focus();
});
}

View File

@@ -535,10 +535,12 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
<ha-fab
slot="fab"
class=${this._dirty ? "dirty" : ""}
.label=${this.hass.localize("ui.common.save")}
.label=${this.hass.localize(
"ui.panel.config.automation.editor.save"
)}
.disabled=${this._saving}
extended
@click=${this._handleSaveAutomation}
@click=${this._saveAutomation}
>
<ha-svg-icon
slot="icon"

View File

@@ -1,131 +1,84 @@
import {
mdiClose,
mdiDelete,
mdiDotsVertical,
mdiIdentifier,
mdiPlayCircleOutline,
mdiPlaylistEdit,
mdiRenameBox,
mdiStopCircleOutline,
} from "@mdi/js";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { fireEvent } from "../../../common/dom/fire_event";
import "../../../components/ha-bottom-sheet";
import type { HaBottomSheet } from "../../../components/ha-bottom-sheet";
import {
isCondition,
isScriptField,
isTrigger,
type ActionSidebarConfig,
type ConditionSidebarConfig,
type ScriptFieldSidebarConfig,
type SidebarConfig,
type TriggerSidebarConfig,
} from "../../../data/automation";
import { stopPropagation } from "../../../common/dom/stop_propagation";
import { handleStructError } from "../../../common/structs/handle-errors";
import type { LocalizeKeys } from "../../../common/translations/localize";
import "../../../components/ha-card";
import "../../../components/ha-dialog-header";
import "../../../components/ha-icon-button";
import "../../../components/ha-md-button-menu";
import "../../../components/ha-md-divider";
import "../../../components/ha-md-menu-item";
import type { Condition, Trigger } from "../../../data/automation";
import type { Action, RepeatAction } from "../../../data/script";
import { isTriggerList } from "../../../data/trigger";
import type { HomeAssistant } from "../../../types";
import "./sidebar/ha-automation-sidebar-action";
import "./sidebar/ha-automation-sidebar-condition";
import "./sidebar/ha-automation-sidebar-option";
import "./sidebar/ha-automation-sidebar-script-field";
import "./sidebar/ha-automation-sidebar-script-field-selector";
import "./sidebar/ha-automation-sidebar-trigger";
import "./action/ha-automation-action-editor";
import { getAutomationActionType } from "./action/ha-automation-action-row";
import { getRepeatType } from "./action/types/ha-automation-action-repeat";
import "./condition/ha-automation-condition-editor";
import type HaAutomationConditionEditor from "./condition/ha-automation-condition-editor";
import "./ha-automation-editor-warning";
import "./trigger/ha-automation-trigger-editor";
import type HaAutomationTriggerEditor from "./trigger/ha-automation-trigger-editor";
import { ACTION_BUILDING_BLOCKS } from "../../../data/action";
import { CONDITION_BUILDING_BLOCKS } from "../../../data/condition";
export interface OpenSidebarConfig {
save: (config: Trigger | Condition | Action) => void;
close: () => void;
rename: () => void;
toggleYamlMode: () => boolean;
disable: () => void;
delete: () => void;
config: Trigger | Condition | Action;
type: "trigger" | "condition" | "action" | "option";
uiSupported: boolean;
yamlMode: boolean;
}
@customElement("ha-automation-sidebar")
export default class HaAutomationSidebar extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public config?: SidebarConfig;
@property({ attribute: false }) public config?: OpenSidebarConfig;
@property({ type: Boolean, attribute: "wide" }) public isWide = false;
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public narrow = false;
@state() private _yamlMode = false;
@query("ha-bottom-sheet") private _bottomSheetElement?: HaBottomSheet;
@state() private _requestShowId = false;
private _renderContent() {
// get config type
const type = this._getType();
@state() private _warnings?: string[];
if (type === "trigger") {
return html`
<ha-automation-sidebar-trigger
class="sidebar-content"
.hass=${this.hass}
.config=${this.config}
.isWide=${this.isWide}
.disabled=${this.disabled}
.yamlMode=${this._yamlMode}
@toggle-yaml-mode=${this._toggleYamlMode}
@close-sidebar=${this._handleCloseSidebar}
></ha-automation-sidebar-trigger>
`;
}
if (type === "condition") {
return html`
<ha-automation-sidebar-condition
class="sidebar-content"
.hass=${this.hass}
.config=${this.config}
.isWide=${this.isWide}
.disabled=${this.disabled}
.yamlMode=${this._yamlMode}
@toggle-yaml-mode=${this._toggleYamlMode}
@close-sidebar=${this._handleCloseSidebar}
></ha-automation-sidebar-condition>
`;
}
if (type === "action") {
return html`
<ha-automation-sidebar-action
class="sidebar-content"
.hass=${this.hass}
.config=${this.config}
.isWide=${this.isWide}
.disabled=${this.disabled}
.yamlMode=${this._yamlMode}
@toggle-yaml-mode=${this._toggleYamlMode}
@close-sidebar=${this._handleCloseSidebar}
></ha-automation-sidebar-action>
`;
}
if (type === "option") {
return html`
<ha-automation-sidebar-option
class="sidebar-content"
.hass=${this.hass}
.config=${this.config}
.isWide=${this.isWide}
.disabled=${this.disabled}
@close-sidebar=${this._handleCloseSidebar}
></ha-automation-sidebar-option>
`;
}
if (type === "script-field-selector") {
return html`
<ha-automation-sidebar-script-field-selector
class="sidebar-content"
.hass=${this.hass}
.config=${this.config}
.isWide=${this.isWide}
.disabled=${this.disabled}
.yamlMode=${this._yamlMode}
@toggle-yaml-mode=${this._toggleYamlMode}
@close-sidebar=${this._handleCloseSidebar}
></ha-automation-sidebar-script-field-selector>
`;
}
if (type === "script-field") {
return html`
<ha-automation-sidebar-script-field
class="sidebar-content"
.hass=${this.hass}
.config=${this.config}
.isWide=${this.isWide}
.disabled=${this.disabled}
.yamlMode=${this._yamlMode}
@toggle-yaml-mode=${this._toggleYamlMode}
@close-sidebar=${this._handleCloseSidebar}
></ha-automation-sidebar-script-field>
`;
}
@query(".sidebar-editor")
public editor?: HaAutomationTriggerEditor | HaAutomationConditionEditor;
return nothing;
protected willUpdate(changedProperties) {
if (changedProperties.has("config")) {
this._requestShowId = false;
this._warnings = undefined;
if (this.config) {
this._yamlMode = this.config.yamlMode;
if (this._yamlMode) {
this.editor?.yamlEditor?.setValue(this.config.config);
}
}
}
}
protected render() {
@@ -133,67 +86,245 @@ export default class HaAutomationSidebar extends LitElement {
return nothing;
}
if (this.narrow) {
return html`
<ha-bottom-sheet
.contentHash=${JSON.stringify(this.config)}
@bottom-sheet-closed=${this._closeSidebar}
@bottom-sheet-opened=${this._scrollRowIntoView}
>
${this._renderContent()}
</ha-bottom-sheet>
`;
const disabled =
this.disabled ||
("enabled" in this.config.config && this.config.config.enabled === false);
let type = isTriggerList(this.config.config as Trigger)
? "list"
: this.config.type === "action"
? getAutomationActionType(this.config.config as Action)
: this.config.config[this.config.type];
if (this.config.type === "action" && type === "repeat") {
type = `repeat_${getRepeatType((this.config.config as RepeatAction).repeat)}`;
}
return this._renderContent();
const isBuildingBlock = [
...CONDITION_BUILDING_BLOCKS,
...ACTION_BUILDING_BLOCKS,
].includes(type);
const subtitle = this.hass.localize(
(this.config.type === "option"
? "ui.panel.config.automation.editor.actions.type.choose.label"
: `ui.panel.config.automation.editor.${this.config.type}s.${this.config.type}`) as LocalizeKeys
);
const title =
this.hass.localize(
(this.config.type === "option"
? "ui.panel.config.automation.editor.actions.type.choose.option_label"
: `ui.panel.config.automation.editor.${this.config.type}s.type.${type}.label`) as LocalizeKeys
) || type;
const description =
isBuildingBlock || this.config.type === "option"
? this.hass.localize(
(this.config.type === "option"
? "ui.panel.config.automation.editor.actions.type.choose.option_description"
: `ui.panel.config.automation.editor.${this.config.type}s.type.${type}.description.picker`) as LocalizeKeys
)
: "";
return html`
<ha-card
outlined
class=${classMap({
mobile: !this.isWide,
yaml: this._yamlMode,
})}
>
<ha-dialog-header>
<ha-icon-button
slot="navigationIcon"
.label=${this.hass.localize("ui.common.close")}
.path=${mdiClose}
@click=${this._closeSidebar}
></ha-icon-button>
<span slot="title">${title}</span>
<span slot="subtitle">${subtitle}</span>
<ha-md-button-menu
slot="actionItems"
@click=${this._openOverflowMenu}
@keydown=${stopPropagation}
@closed=${stopPropagation}
positioning="fixed"
>
<ha-icon-button
slot="trigger"
.label=${this.hass.localize("ui.common.menu")}
.path=${mdiDotsVertical}
></ha-icon-button>
<ha-md-menu-item
.clickAction=${this.config.rename}
.disabled=${disabled || type === "list"}
>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.rename"
)}
<ha-svg-icon slot="start" .path=${mdiRenameBox}></ha-svg-icon>
</ha-md-menu-item>
${this.config.type === "trigger" &&
!this._yamlMode &&
!("id" in this.config.config) &&
!this._requestShowId
? html`<ha-md-menu-item
.clickAction=${this._showTriggerId}
.disabled=${disabled || type === "list"}
>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.edit_id"
)}
<ha-svg-icon
slot="start"
.path=${mdiIdentifier}
></ha-svg-icon>
</ha-md-menu-item>`
: nothing}
${this.config.type !== "option"
? html`
<ha-md-menu-item
.clickAction=${this._toggleYamlMode}
.disabled=${!this.config.uiSupported || !!this._warnings}
>
${this.hass.localize(
`ui.panel.config.automation.editor.edit_${!this._yamlMode ? "yaml" : "ui"}`
)}
<ha-svg-icon
slot="start"
.path=${mdiPlaylistEdit}
></ha-svg-icon>
</ha-md-menu-item>
`
: nothing}
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
${this.config.type !== "option"
? html`
<ha-md-menu-item
.clickAction=${this.config.disable}
.disabled=${this.disabled || type === "list"}
>
${disabled
? this.hass.localize(
"ui.panel.config.automation.editor.actions.enable"
)
: this.hass.localize(
"ui.panel.config.automation.editor.actions.disable"
)}
<ha-svg-icon
slot="start"
.path=${disabled
? mdiPlayCircleOutline
: mdiStopCircleOutline}
></ha-svg-icon>
</ha-md-menu-item>
`
: nothing}
<ha-md-menu-item
.clickAction=${this.config.delete}
class="warning"
.disabled=${this.disabled}
>
${this.hass.localize(
`ui.panel.config.automation.editor.actions.${this.config.type !== "option" ? "delete" : "type.choose.remove_option"}`
)}
<ha-svg-icon
class="warning"
slot="start"
.path=${mdiDelete}
></ha-svg-icon>
</ha-md-menu-item>
</ha-md-button-menu>
</ha-dialog-header>
${this._warnings
? html`<ha-automation-editor-warning
.localize=${this.hass.localize}
.warnings=${this._warnings}
>
</ha-automation-editor-warning>`
: nothing}
<div class="card-content">
${this.config.type === "trigger"
? html`<ha-automation-trigger-editor
class="sidebar-editor"
.hass=${this.hass}
.trigger=${this.config.config as Trigger}
@value-changed=${this._valueChangedSidebar}
.uiSupported=${this.config.uiSupported}
.showId=${this._requestShowId}
.yamlMode=${this._yamlMode}
.disabled=${this.disabled}
@ui-mode-not-available=${this._handleUiModeNotAvailable}
></ha-automation-trigger-editor>`
: this.config.type === "condition" &&
(this._yamlMode || !CONDITION_BUILDING_BLOCKS.includes(type))
? html`
<ha-automation-condition-editor
class="sidebar-editor"
.hass=${this.hass}
.condition=${this.config.config as Condition}
.yamlMode=${this._yamlMode}
.uiSupported=${this.config.uiSupported}
@value-changed=${this._valueChangedSidebar}
.disabled=${this.disabled}
@ui-mode-not-available=${this._handleUiModeNotAvailable}
></ha-automation-condition-editor>
`
: this.config.type === "action" &&
(this._yamlMode || !ACTION_BUILDING_BLOCKS.includes(type))
? html`
<ha-automation-action-editor
class="sidebar-editor"
.hass=${this.hass}
.action=${this.config.config as Action}
.yamlMode=${this._yamlMode}
.uiSupported=${this.config.uiSupported}
@value-changed=${this._valueChangedSidebar}
sidebar
narrow
.disabled=${this.disabled}
@ui-mode-not-available=${this._handleUiModeNotAvailable}
></ha-automation-action-editor>
`
: description || nothing}
</div>
</ha-card>
`;
}
private _getType() {
if (
(this.config as TriggerSidebarConfig)?.config &&
(isTrigger((this.config as TriggerSidebarConfig)?.config) ||
isTriggerList((this.config as TriggerSidebarConfig)?.config))
) {
return "trigger";
private _handleUiModeNotAvailable(ev: CustomEvent) {
this._warnings = handleStructError(this.hass, ev.detail).warnings;
if (!this._yamlMode) {
this._yamlMode = true;
}
if (isCondition((this.config as ConditionSidebarConfig)?.config)) {
return "condition";
}
if (
(this.config as ScriptFieldSidebarConfig)?.config &&
isScriptField((this.config as ScriptFieldSidebarConfig)?.config)
) {
return (this.config as ScriptFieldSidebarConfig)?.config.selector
? "script-field-selector"
: "script-field";
}
// option is always a building block and doesn't have a config
if (this.config && !(this.config as any)?.config) {
return "option";
}
if ((this.config as ActionSidebarConfig)?.config) {
return "action";
}
return undefined;
}
private _handleCloseSidebar(ev: CustomEvent) {
private _valueChangedSidebar(ev: CustomEvent) {
ev.stopPropagation();
if (this.narrow) {
this._bottomSheetElement?.closeSheet();
return;
}
this._closeSidebar();
this.config?.save(ev.detail.value);
if (this.config) {
fireEvent(this, "value-changed", {
value: {
...this.config,
config: ev.detail.value,
},
});
}
}
private _closeSidebar() {
this.config?.close();
}
private _openOverflowMenu(ev: MouseEvent) {
ev.stopPropagation();
ev.preventDefault();
}
private _toggleYamlMode = () => {
this._yamlMode = this.config!.toggleYamlMode();
fireEvent(this, "value-changed", {
@@ -204,8 +335,8 @@ export default class HaAutomationSidebar extends LitElement {
});
};
private _scrollRowIntoView = () => {
this.config?.scrollIntoView();
private _showTriggerId = () => {
this._requestShowId = true;
};
static styles = css`
@@ -216,14 +347,59 @@ export default class HaAutomationSidebar extends LitElement {
var(--ha-border-radius-2xl)
);
border-radius: var(--ha-card-border-radius);
--ha-bottom-sheet-border-width: 2px;
--ha-bottom-sheet-border-style: solid;
--ha-bottom-sheet-border-color: var(--primary-color);
}
ha-card {
height: 100%;
width: 100%;
border-color: var(--primary-color);
border-width: 2px;
display: block;
}
ha-card.mobile {
border-bottom-right-radius: var(--ha-border-radius-square);
border-bottom-left-radius: var(--ha-border-radius-square);
}
@media all and (max-width: 870px) {
.sidebar-content {
max-height: 100%;
ha-card.mobile {
max-height: 70vh;
max-height: 70dvh;
border-width: 2px 2px 0;
}
ha-card.mobile.yaml {
height: 70vh;
height: 70dvh;
}
}
ha-dialog-header {
border-radius: var(--ha-card-border-radius);
}
.sidebar-editor {
padding-top: 64px;
}
.card-content {
max-height: calc(100% - 80px);
overflow: auto;
}
@media (min-width: 450px) and (min-height: 500px) {
.card-content {
max-height: calc(100% - 104px);
overflow: auto;
}
}
@media all and (max-width: 870px) {
ha-card.mobile .card-content {
max-height: calc(
70vh - 88px - max(var(--safe-area-inset-bottom), 16px)
);
max-height: calc(
70dvh - 88px - max(var(--safe-area-inset-bottom), 16px)
);
}
}
`;
@@ -233,8 +409,4 @@ declare global {
interface HTMLElementTagNameMap {
"ha-automation-sidebar": HaAutomationSidebar;
}
interface HASSDomEvents {
"toggle-yaml-mode": undefined;
}
}

View File

@@ -1,7 +1,7 @@
import { mdiContentSave, mdiHelpCircle } from "@mdi/js";
import type { HassEntity } from "home-assistant-js-websocket";
import { load } from "js-yaml";
import type { CSSResultGroup } from "lit";
import type { CSSResultGroup, PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
@@ -18,6 +18,11 @@ import {
import { ensureArray } from "../../../common/array/ensure-array";
import { canOverrideAlphanumericInput } from "../../../common/dom/can-override-input";
import { fireEvent } from "../../../common/dom/fire_event";
import { constructUrlCurrentPath } from "../../../common/url/construct-url";
import {
extractSearchParam,
removeSearchParam,
} from "../../../common/url/search-params";
import "../../../components/ha-button";
import "../../../components/ha-fab";
import "../../../components/ha-icon-button";
@@ -26,7 +31,6 @@ import type {
AutomationConfig,
Condition,
ManualAutomationConfig,
SidebarConfig,
Trigger,
} from "../../../data/automation";
import {
@@ -39,11 +43,15 @@ import type { HomeAssistant } from "../../../types";
import { documentationUrl } from "../../../util/documentation-url";
import { showToast } from "../../../util/toast";
import "./action/ha-automation-action";
import type HaAutomationAction from "./action/ha-automation-action";
import "./condition/ha-automation-condition";
import type HaAutomationCondition from "./condition/ha-automation-condition";
import "./ha-automation-sidebar";
import type { OpenSidebarConfig } from "./ha-automation-sidebar";
import { showPasteReplaceDialog } from "./paste-replace-dialog/show-dialog-paste-replace";
import { saveFabStyles } from "./styles";
import "./trigger/ha-automation-trigger";
import type HaAutomationTrigger from "./trigger/ha-automation-trigger";
const baseConfigStruct = object({
alias: optional(string()),
@@ -82,7 +90,7 @@ export class HaManualAutomationEditor extends LitElement {
@state() private _pastedConfig?: ManualAutomationConfig;
@state() private _sidebarConfig?: SidebarConfig;
@state() private _sidebarConfig?: OpenSidebarConfig;
private _previousConfig?: ManualAutomationConfig;
@@ -96,6 +104,31 @@ export class HaManualAutomationEditor extends LitElement {
super.disconnectedCallback();
}
protected firstUpdated(changedProps: PropertyValues): void {
super.firstUpdated(changedProps);
const expanded = extractSearchParam("expanded");
if (expanded === "1") {
this._clearParam("expanded");
const items = this.shadowRoot!.querySelectorAll<
HaAutomationTrigger | HaAutomationCondition | HaAutomationAction
>("ha-automation-trigger, ha-automation-condition, ha-automation-action");
items.forEach((el) => {
el.updateComplete.then(() => {
el.expandAll();
});
});
}
}
private _clearParam(param: string) {
window.history.replaceState(
null,
"",
constructUrlCurrentPath(removeSearchParam(param))
);
}
private _renderContent() {
return html`
${this.stateObj?.state === "off"
@@ -261,7 +294,9 @@ export class HaManualAutomationEditor extends LitElement {
<ha-fab
slot="fab"
class=${this.dirty ? "dirty" : ""}
.label=${this.hass.localize("ui.common.save")}
.label=${this.hass.localize(
"ui.panel.config.automation.editor.save"
)}
.disabled=${this.saving}
extended
@click=${this._saveAutomation}
@@ -273,11 +308,10 @@ export class HaManualAutomationEditor extends LitElement {
class=${classMap({
sidebar: true,
hidden: !this._sidebarConfig,
overlay: !this.isWide && !this.narrow,
overlay: !this.isWide,
})}
.isWide=${this.isWide}
.hass=${this.hass}
.narrow=${this.narrow}
.config=${this._sidebarConfig}
@value-changed=${this._sidebarConfigChanged}
.disabled=${this.disabled}
@@ -286,13 +320,13 @@ export class HaManualAutomationEditor extends LitElement {
`;
}
private _openSidebar(ev: CustomEvent<SidebarConfig>) {
private _openSidebar(ev: CustomEvent<OpenSidebarConfig>) {
// deselect previous selected row
this._sidebarConfig?.close?.();
this._sidebarConfig = ev.detail;
}
private _sidebarConfigChanged(ev: CustomEvent<{ value: SidebarConfig }>) {
private _sidebarConfigChanged(ev: CustomEvent<{ value: OpenSidebarConfig }>) {
ev.stopPropagation();
if (!this._sidebarConfig) {
return;
@@ -613,23 +647,26 @@ export class HaManualAutomationEditor extends LitElement {
.sidebar.overlay {
position: fixed;
bottom: 8px;
right: 8px;
height: calc(100% - 70px);
bottom: 0;
right: 0;
height: calc(100% - 64px);
padding: 0;
z-index: 5;
box-shadow: -8px 0 16px rgba(0, 0, 0, 0.2);
}
@media all and (max-width: 870px) {
.split-view {
gap: 0;
margin-right: -8px;
.sidebar.overlay {
max-height: 70vh;
max-height: 70dvh;
height: auto;
width: 100%;
box-shadow: 0px -8px 16px rgba(0, 0, 0, 0.2);
}
.sidebar {
}
@media all and (max-width: 870px) {
.sidebar.overlay.hidden {
height: 0;
width: 0;
flex: 0;
}
}
@@ -681,7 +718,7 @@ declare global {
}
interface HASSDomEvents {
"open-sidebar": SidebarConfig;
"open-sidebar": OpenSidebarConfig;
"close-sidebar": undefined;
}
}

View File

@@ -23,10 +23,7 @@ import "../../../../components/ha-icon-button";
import "../../../../components/ha-md-button-menu";
import "../../../../components/ha-md-menu-item";
import "../../../../components/ha-svg-icon";
import type {
Condition,
OptionSidebarConfig,
} from "../../../../data/automation";
import type { Condition } from "../../../../data/automation";
import { describeCondition } from "../../../../data/automation_i18n";
import { fullEntitiesContext } from "../../../../data/context";
import type { EntityRegistryEntry } from "../../../../data/entity_registry";
@@ -342,15 +339,15 @@ export default class HaAutomationOptionRow extends LitElement {
this.openSidebar();
}
private _scrollIntoView = () => {
this.scrollIntoView({
block: "start",
behavior: "smooth",
});
};
public openSidebar(): void {
if (this.narrow) {
this.scrollIntoView();
}
fireEvent(this, "open-sidebar", {
save: () => {
// nothing to save for an option in the sidebar
},
close: () => {
this._selected = false;
fireEvent(this, "close-sidebar");
@@ -359,9 +356,15 @@ export default class HaAutomationOptionRow extends LitElement {
this._renameOption();
},
toggleYamlMode: () => false, // no yaml mode for options
disable: () => {
// option cannot be disabled
},
delete: this._removeOption,
scrollIntoView: this._scrollIntoView,
} satisfies OptionSidebarConfig);
config: {},
type: "option",
uiSupported: true,
yamlMode: false,
});
this._selected = true;
}

View File

@@ -133,6 +133,7 @@ export default class HaAutomationOption extends LitElement {
if (!this.optionsInSidebar) {
row.expand();
}
row.scrollIntoView();
row.focus();
});
}

View File

@@ -1,189 +0,0 @@
import {
mdiDelete,
mdiPlayCircleOutline,
mdiPlaylistEdit,
mdiRenameBox,
mdiStopCircleOutline,
} from "@mdi/js";
import { css, html, LitElement } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event";
import { handleStructError } from "../../../../common/structs/handle-errors";
import type { LocalizeKeys } from "../../../../common/translations/localize";
import "../../../../components/ha-md-divider";
import "../../../../components/ha-md-menu-item";
import { ACTION_BUILDING_BLOCKS } from "../../../../data/action";
import type { ActionSidebarConfig } from "../../../../data/automation";
import type { RepeatAction } from "../../../../data/script";
import type { HomeAssistant } from "../../../../types";
import type HaAutomationConditionEditor from "../action/ha-automation-action-editor";
import { getAutomationActionType } from "../action/ha-automation-action-row";
import { getRepeatType } from "../action/types/ha-automation-action-repeat";
import "../trigger/ha-automation-trigger-editor";
import "./ha-automation-sidebar-card";
@customElement("ha-automation-sidebar-action")
export default class HaAutomationSidebarAction extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public config!: ActionSidebarConfig;
@property({ type: Boolean, attribute: "wide" }) public isWide = false;
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean, attribute: "yaml-mode" }) public yamlMode = false;
@state() private _warnings?: string[];
@query(".sidebar-editor")
public editor?: HaAutomationConditionEditor;
protected willUpdate(changedProperties) {
if (changedProperties.has("config")) {
this._warnings = undefined;
if (this.config) {
this.yamlMode = this.config.yamlMode;
if (this.yamlMode) {
this.editor?.yamlEditor?.setValue(this.config.config);
}
}
}
}
protected render() {
const disabled =
this.disabled ||
("enabled" in this.config.config && this.config.config.enabled === false);
const actionType = getAutomationActionType(this.config.config);
const type =
actionType !== "repeat"
? actionType
: `repeat_${getRepeatType((this.config.config as RepeatAction).repeat)}`;
const isBuildingBlock = ACTION_BUILDING_BLOCKS.includes(type || "");
const subtitle = this.hass.localize(
"ui.panel.config.automation.editor.actions.action"
);
const title =
this.hass.localize(
`ui.panel.config.automation.editor.actions.type.${type}.label` as LocalizeKeys
) || type;
const description = isBuildingBlock
? this.hass.localize(
`ui.panel.config.automation.editor.actions.type.${type}.description.picker` as LocalizeKeys
)
: "";
return html`<ha-automation-sidebar-card
.hass=${this.hass}
.isWide=${this.isWide}
.yamlMode=${this.yamlMode}
.warnings=${this._warnings}
>
<span slot="title">${title}</span>
<span slot="subtitle">${subtitle}</span>
<ha-md-menu-item
slot="menu-items"
.clickAction=${this.config.rename}
.disabled=${!!disabled}
>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.rename"
)}
<ha-svg-icon slot="start" .path=${mdiRenameBox}></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu-item
slot="menu-items"
.clickAction=${this._toggleYamlMode}
.disabled=${!this.config.uiSupported || !!this._warnings}
>
${this.hass.localize(
`ui.panel.config.automation.editor.edit_${!this.yamlMode ? "yaml" : "ui"}`
)}
<ha-svg-icon slot="start" .path=${mdiPlaylistEdit}></ha-svg-icon>
</ha-md-menu-item>
<ha-md-divider
slot="menu-items"
role="separator"
tabindex="-1"
></ha-md-divider>
<ha-md-menu-item slot="menu-items" .clickAction=${this.config.disable}>
${this.hass.localize(
`ui.panel.config.automation.editor.actions.${disabled ? "enable" : "disable"}`
)}
<ha-svg-icon
slot="start"
.path=${this.disabled ? mdiPlayCircleOutline : mdiStopCircleOutline}
></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu-item
slot="menu-items"
.clickAction=${this.config.delete}
.disabled=${this.disabled}
class="warning"
>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.delete"
)}
<ha-svg-icon slot="start" .path=${mdiDelete}></ha-svg-icon>
</ha-md-menu-item>
${description ||
html`<ha-automation-action-editor
class="sidebar-editor"
.hass=${this.hass}
.action=${this.config.config}
.yamlMode=${this.yamlMode}
.uiSupported=${this.config.uiSupported}
@value-changed=${this._valueChangedSidebar}
sidebar
narrow
.disabled=${this.disabled}
@ui-mode-not-available=${this._handleUiModeNotAvailable}
></ha-automation-action-editor>`}
</ha-automation-sidebar-card>`;
}
private _handleUiModeNotAvailable(ev: CustomEvent) {
this._warnings = handleStructError(this.hass, ev.detail).warnings;
if (!this.yamlMode) {
this.yamlMode = true;
}
}
private _valueChangedSidebar(ev: CustomEvent) {
ev.stopPropagation();
this.config?.save?.(ev.detail.value);
if (this.config) {
fireEvent(this, "value-changed", {
value: {
...this.config,
config: ev.detail.value,
},
});
}
}
private _toggleYamlMode = () => {
fireEvent(this, "toggle-yaml-mode");
};
static styles = css`
.sidebar-editor {
padding-top: 64px;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-automation-sidebar-action": HaAutomationSidebarAction;
}
}

View File

@@ -1,220 +0,0 @@
import { css, html, LitElement } from "lit";
import { customElement, query, state } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event";
const ANIMATION_DURATION_MS = 300;
@customElement("ha-automation-sidebar-bottom-sheet")
export class HaAutomationSidebarBottomSheet extends LitElement {
@state() private _size = 30;
@query("dialog") private _dialog!: HTMLDialogElement;
private _dragging = false;
private _dragStartY = 0;
private _initialSize = 0;
public showDialog(): void {
this._openSheet();
}
public closeDialog(): void {
this.closeSheet();
}
render() {
return html`<dialog open style=${`--size: ${this._size}vh;`}>
<div
class="handle-wrapper"
@mousedown=${this._handleMouseDown}
@touchstart=${this._handleTouchStart}
>
<div class="handle"></div>
</div>
<slot></slot>
</dialog>`;
}
protected firstUpdated(changedProperties) {
super.firstUpdated(changedProperties);
this._openSheet();
}
private _openSheet() {
requestAnimationFrame(() => {
this._dialog.classList.add("show");
});
}
public closeSheet() {
requestAnimationFrame(() => {
this._dialog.classList.remove("show");
setTimeout(() => {
this._dialog.close();
fireEvent(this, "dialog-closed", { dialog: this.localName });
}, ANIMATION_DURATION_MS);
});
}
connectedCallback() {
super.connectedCallback();
document.addEventListener("mousemove", this._handleMouseMove);
document.addEventListener("mouseup", this._handleMouseUp);
// Use non-passive listeners so we can call preventDefault to block
// browser pull-to-refresh and page scrolling while dragging.
document.addEventListener("touchmove", this._handleTouchMove, {
passive: false,
});
document.addEventListener("touchend", this._handleTouchEnd);
document.addEventListener("touchcancel", this._handleTouchEnd);
}
disconnectedCallback() {
super.disconnectedCallback();
document.removeEventListener("mousemove", this._handleMouseMove);
document.removeEventListener("mouseup", this._handleMouseUp);
document.removeEventListener("touchmove", this._handleTouchMove);
document.removeEventListener("touchend", this._handleTouchEnd);
document.removeEventListener("touchcancel", this._handleTouchEnd);
}
private _handleMouseDown = (ev: MouseEvent) => {
this._startDrag(ev.clientY);
};
private _handleTouchStart = (ev: TouchEvent) => {
// Prevent the browser from interpreting this as a scroll/PTR gesture.
ev.preventDefault();
this._startDrag(ev.touches[0].clientY);
};
private _startDrag(clientY: number) {
this._dragging = true;
this._dragStartY = clientY;
this._initialSize = this._size;
document.body.style.cursor = "grabbing";
}
private _handleMouseMove = (ev: MouseEvent) => {
if (!this._dragging) return;
this._updateSize(ev.clientY);
};
private _handleTouchMove = (ev: TouchEvent) => {
if (!this._dragging) return;
ev.preventDefault(); // Prevent scrolling
this._updateSize(ev.touches[0].clientY);
};
private _updateSize(clientY: number) {
const deltaY = this._dragStartY - clientY;
const viewportHeight = window.innerHeight;
const deltaVh = (deltaY / viewportHeight) * 100;
// Calculate new size and clamp between 10vh and 90vh
let newSize = this._initialSize + deltaVh;
newSize = Math.max(10, Math.min(90, newSize));
this._size = newSize;
if (newSize < 20) {
this.closeSheet();
}
}
private _handleMouseUp = () => {
this._endDrag();
};
private _handleTouchEnd = () => {
this._endDrag();
};
private _endDrag() {
if (!this._dragging) return;
this._dragging = false;
document.body.style.cursor = "";
}
static styles = css`
:host {
--size: 30vh;
--size: 30dvh;
overscroll-behavior: contain;
}
.handle-wrapper {
width: 100%;
height: 32px;
padding-bottom: 2px;
display: flex;
justify-content: center;
align-items: center;
cursor: grab;
touch-action: none;
}
.handle-wrapper:active {
cursor: grabbing;
}
.handle-wrapper .handle {
border-radius: 8px;
height: 4px;
background: var(--divider-color, #e0e0e0);
width: 80px;
transition: background-color 0.2s ease;
pointer-events: none;
}
dialog {
background-color: var(
--ha-dialog-surface-background,
var(--mdc-theme-surface, #fff)
);
display: flex;
flex-direction: column;
top: 0;
inset-inline-start: 0;
position: fixed;
width: 100%;
max-width: 100%;
max-height: 100%;
overflow: hidden;
border: none;
box-shadow: var(--wa-shadow-l);
overflow: auto;
padding: 0;
margin: 0;
top: auto;
inset-inline-end: auto;
bottom: 0;
inset-inline-start: 0;
height: var(--size);
box-shadow: 0px -8px 16px rgba(0, 0, 0, 0.2);
border-top-left-radius: var(
--ha-dialog-border-radius,
var(--ha-border-radius-2xl)
);
border-top-right-radius: var(
--ha-dialog-border-radius,
var(--ha-border-radius-2xl)
);
transform: translateY(100%);
transition: transform ${ANIMATION_DURATION_MS}ms ease;
}
dialog.show {
transform: translateY(0);
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-automation-sidebar-bottom-sheet": HaAutomationSidebarBottomSheet;
}
interface HASSDomEvents {
"bottom-sheet-closed": undefined;
}
}

View File

@@ -1,175 +0,0 @@
import { mdiClose, mdiDotsVertical } from "@mdi/js";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { fireEvent } from "../../../../common/dom/fire_event";
import { stopPropagation } from "../../../../common/dom/stop_propagation";
import "../../../../components/ha-card";
import "../../../../components/ha-dialog-header";
import "../../../../components/ha-icon-button";
import "../../../../components/ha-md-button-menu";
import "../../../../components/ha-md-divider";
import "../../../../components/ha-md-menu-item";
import type { HomeAssistant } from "../../../../types";
import "../ha-automation-editor-warning";
export interface SidebarOverflowMenuEntry {
clickAction: () => void;
disabled?: boolean;
label: string;
icon?: string;
danger?: boolean;
}
export type SidebarOverflowMenu = (SidebarOverflowMenuEntry | "separator")[];
@customElement("ha-automation-sidebar-card")
export default class HaAutomationSidebarCard extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean, attribute: "wide" }) public isWide = false;
@property({ type: Boolean, attribute: "yaml-mode" }) public yamlMode = false;
@property({ attribute: false }) public warnings?: string[];
@property({ type: Boolean }) public narrow = false;
@state() private _contentScrolled = false;
@query(".card-content") private _contentElement?: HTMLDivElement;
protected render() {
return html`
<ha-card
outlined
class=${classMap({
mobile: !this.isWide,
yaml: this.yamlMode,
})}
>
<ha-dialog-header
class=${classMap({ scrolled: this._contentScrolled })}
>
<ha-icon-button
slot="navigationIcon"
.label=${this.hass.localize("ui.common.close")}
.path=${mdiClose}
@click=${this._closeSidebar}
></ha-icon-button>
<slot slot="title" name="title"></slot>
<slot slot="subtitle" name="subtitle"></slot>
<slot name="overflow-menu" slot="actionItems">
<ha-md-button-menu
@click=${this._openOverflowMenu}
@keydown=${stopPropagation}
@closed=${stopPropagation}
positioning="fixed"
>
<ha-icon-button
slot="trigger"
.label=${this.hass.localize("ui.common.menu")}
.path=${mdiDotsVertical}
></ha-icon-button>
<slot name="menu-items"></slot>
</ha-md-button-menu>
</slot>
</ha-dialog-header>
${this.warnings
? html`<ha-automation-editor-warning
.localize=${this.hass.localize}
.warnings=${this.warnings}
>
</ha-automation-editor-warning>`
: nothing}
<div class="card-content">
<slot></slot>
</div>
</ha-card>
`;
}
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
this._contentElement?.addEventListener("scroll", this._onScroll, {
passive: true,
});
this._onScroll();
}
disconnectedCallback(): void {
super.disconnectedCallback();
this._contentElement?.removeEventListener("scroll", this._onScroll);
}
private _onScroll = () => {
const top = this._contentElement?.scrollTop ?? 0;
this._contentScrolled = top > 0;
};
private _closeSidebar() {
fireEvent(this, "close-sidebar");
}
private _openOverflowMenu(ev: MouseEvent) {
ev.stopPropagation();
ev.preventDefault();
}
static styles = css`
ha-card {
height: 100%;
width: 100%;
border-color: var(--primary-color);
border-width: 2px;
display: block;
}
@media all and (max-width: 870px) {
ha-card.mobile {
border: none;
}
ha-card.mobile {
border-bottom-right-radius: var(--ha-border-radius-square);
border-bottom-left-radius: var(--ha-border-radius-square);
}
}
ha-dialog-header {
border-radius: var(--ha-card-border-radius);
box-shadow: none;
transition: box-shadow 180ms ease-in-out;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
z-index: 10;
position: relative;
background-color: var(
--ha-dialog-surface-background,
var(--mdc-theme-surface, #fff)
);
}
ha-dialog-header.scrolled {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
}
.card-content {
max-height: calc(100% - 80px);
overflow: auto;
}
@media (min-width: 450px) and (min-height: 500px) {
.card-content {
max-height: calc(100% - 104px);
overflow: auto;
}
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-automation-sidebar-card": HaAutomationSidebarCard;
}
}

View File

@@ -1,176 +0,0 @@
import {
mdiDelete,
mdiPlayCircleOutline,
mdiPlaylistEdit,
mdiRenameBox,
mdiStopCircleOutline,
} from "@mdi/js";
import { css, html, LitElement } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event";
import { handleStructError } from "../../../../common/structs/handle-errors";
import type { ConditionSidebarConfig } from "../../../../data/automation";
import { CONDITION_BUILDING_BLOCKS } from "../../../../data/condition";
import type { HomeAssistant } from "../../../../types";
import "../condition/ha-automation-condition-editor";
import type HaAutomationConditionEditor from "../condition/ha-automation-condition-editor";
import "./ha-automation-sidebar-card";
@customElement("ha-automation-sidebar-condition")
export default class HaAutomationSidebarCondition extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public config!: ConditionSidebarConfig;
@property({ type: Boolean, attribute: "wide" }) public isWide = false;
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean, attribute: "yaml-mode" }) public yamlMode = false;
@state() private _warnings?: string[];
@query(".sidebar-editor")
public editor?: HaAutomationConditionEditor;
protected willUpdate(changedProperties) {
if (changedProperties.has("config")) {
this._warnings = undefined;
if (this.config) {
this.yamlMode = this.config.yamlMode;
if (this.yamlMode) {
this.editor?.yamlEditor?.setValue(this.config.config);
}
}
}
}
protected render() {
const disabled =
this.disabled ||
("enabled" in this.config.config && this.config.config.enabled === false);
const type = this.config.config.condition;
const isBuildingBlock = CONDITION_BUILDING_BLOCKS.includes(type);
const subtitle = this.hass.localize(
"ui.panel.config.automation.editor.conditions.condition"
);
const title =
this.hass.localize(
`ui.panel.config.automation.editor.conditions.type.${type}.label`
) || type;
const description = isBuildingBlock
? this.hass.localize(
`ui.panel.config.automation.editor.conditions.type.${type}.description.picker`
)
: "";
return html`<ha-automation-sidebar-card
.hass=${this.hass}
.isWide=${this.isWide}
.yamlMode=${this.yamlMode}
.warnings=${this._warnings}
>
<span slot="title">${title}</span>
<span slot="subtitle">${subtitle}</span>
<ha-md-menu-item
slot="menu-items"
.clickAction=${this.config.rename}
.disabled=${!!disabled}
>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.rename"
)}
<ha-svg-icon slot="start" .path=${mdiRenameBox}></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu-item
slot="menu-items"
.clickAction=${this._toggleYamlMode}
.disabled=${!this.config.uiSupported || !!this._warnings}
>
${this.hass.localize(
`ui.panel.config.automation.editor.edit_${!this.yamlMode ? "yaml" : "ui"}`
)}
<ha-svg-icon slot="start" .path=${mdiPlaylistEdit}></ha-svg-icon>
</ha-md-menu-item>
<ha-md-divider
slot="menu-items"
role="separator"
tabindex="-1"
></ha-md-divider>
<ha-md-menu-item slot="menu-items" .clickAction=${this.config.disable}>
${this.hass.localize(
`ui.panel.config.automation.editor.actions.${disabled ? "enable" : "disable"}`
)}
<ha-svg-icon
slot="start"
.path=${this.disabled ? mdiPlayCircleOutline : mdiStopCircleOutline}
></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu-item
slot="menu-items"
.clickAction=${this.config.delete}
.disabled=${this.disabled}
class="warning"
>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.delete"
)}
<ha-svg-icon slot="start" .path=${mdiDelete}></ha-svg-icon>
</ha-md-menu-item>
${description ||
html`<ha-automation-condition-editor
class="sidebar-editor"
.hass=${this.hass}
.condition=${this.config.config}
.yamlMode=${this.yamlMode}
.uiSupported=${this.config.uiSupported}
@value-changed=${this._valueChangedSidebar}
.disabled=${this.disabled}
@ui-mode-not-available=${this._handleUiModeNotAvailable}
></ha-automation-condition-editor> `}
</ha-automation-sidebar-card>`;
}
private _handleUiModeNotAvailable(ev: CustomEvent) {
this._warnings = handleStructError(this.hass, ev.detail).warnings;
if (!this.yamlMode) {
this.yamlMode = true;
}
}
private _valueChangedSidebar(ev: CustomEvent) {
ev.stopPropagation();
this.config?.save?.(ev.detail.value);
if (this.config) {
fireEvent(this, "value-changed", {
value: {
...this.config,
config: ev.detail.value,
},
});
}
}
private _toggleYamlMode = () => {
fireEvent(this, "toggle-yaml-mode");
};
static styles = css`
.sidebar-editor {
padding-top: 64px;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-automation-sidebar-condition": HaAutomationSidebarCondition;
}
}

View File

@@ -1,84 +0,0 @@
import { mdiDelete, mdiRenameBox } from "@mdi/js";
import { css, html, LitElement } from "lit";
import { customElement, property, query } from "lit/decorators";
import type { OptionSidebarConfig } from "../../../../data/automation";
import type { HomeAssistant } from "../../../../types";
import type HaAutomationConditionEditor from "../action/ha-automation-action-editor";
import "./ha-automation-sidebar-card";
@customElement("ha-automation-sidebar-option")
export default class HaAutomationSidebarOption extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public config!: OptionSidebarConfig;
@property({ type: Boolean, attribute: "wide" }) public isWide = false;
@property({ type: Boolean }) public disabled = false;
@query(".sidebar-editor")
public editor?: HaAutomationConditionEditor;
protected render() {
const disabled = this.disabled;
const subtitle = this.hass.localize(
"ui.panel.config.automation.editor.actions.type.choose.label"
);
const title = this.hass.localize(
"ui.panel.config.automation.editor.actions.type.choose.option_label"
);
const description = this.hass.localize(
"ui.panel.config.automation.editor.actions.type.choose.option_description"
);
return html`<ha-automation-sidebar-card
.hass=${this.hass}
.isWide=${this.isWide}
>
<span slot="title">${title}</span>
<span slot="subtitle">${subtitle}</span>
<ha-md-menu-item
slot="menu-items"
.clickAction=${this.config.rename}
.disabled=${!!disabled}
>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.rename"
)}
<ha-svg-icon slot="start" .path=${mdiRenameBox}></ha-svg-icon>
</ha-md-menu-item>
<ha-md-divider
slot="menu-items"
role="separator"
tabindex="-1"
></ha-md-divider>
<ha-md-menu-item
slot="menu-items"
.clickAction=${this.config.delete}
.disabled=${this.disabled}
class="warning"
>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.type.choose.remove_option"
)}
<ha-svg-icon slot="start" .path=${mdiDelete}></ha-svg-icon>
</ha-md-menu-item>
${description}
</ha-automation-sidebar-card>`;
}
static styles = css`
.sidebar-editor {
padding-top: 64px;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-automation-sidebar-option": HaAutomationSidebarOption;
}
}

View File

@@ -1,130 +0,0 @@
import { mdiDelete, mdiPlaylistEdit } from "@mdi/js";
import { css, html, LitElement } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event";
import type { LocalizeKeys } from "../../../../common/translations/localize";
import type { ScriptFieldSidebarConfig } from "../../../../data/automation";
import type { HomeAssistant } from "../../../../types";
import "../../script/ha-script-field-selector-editor";
import type HaAutomationConditionEditor from "../action/ha-automation-action-editor";
import "./ha-automation-sidebar-card";
@customElement("ha-automation-sidebar-script-field-selector")
export default class HaAutomationSidebarScriptFieldSelector extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public config!: ScriptFieldSidebarConfig;
@property({ type: Boolean, attribute: "wide" }) public isWide = false;
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean, attribute: "yaml-mode" }) public yamlMode = false;
@state() private _warnings?: string[];
@query(".sidebar-editor")
public editor?: HaAutomationConditionEditor;
protected willUpdate(changedProperties) {
if (changedProperties.has("config")) {
this._warnings = undefined;
if (this.config) {
this.yamlMode = this.config.yamlMode;
if (this.yamlMode) {
this.editor?.yamlEditor?.setValue(this.config.config);
}
}
}
}
protected render() {
const subtitle = this.hass.localize(
"ui.panel.config.script.editor.field.field_selector"
);
const title =
this.hass.localize(
`ui.components.selectors.selector.types.${Object.keys(this.config.config.field.selector)[0]}` as LocalizeKeys
) || Object.keys(this.config.config.field.selector)[0];
return html`<ha-automation-sidebar-card
.hass=${this.hass}
.isWide=${this.isWide}
.yamlMode=${this.yamlMode}
.warnings=${this._warnings}
>
<span slot="title">${title}</span>
<span slot="subtitle">${subtitle}</span>
<ha-md-menu-item
slot="menu-items"
.clickAction=${this._toggleYamlMode}
.disabled=${!!this._warnings}
>
${this.hass.localize(
`ui.panel.config.automation.editor.edit_${!this.yamlMode ? "yaml" : "ui"}`
)}
<ha-svg-icon slot="start" .path=${mdiPlaylistEdit}></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu-item
slot="menu-items"
.clickAction=${this.config.delete}
.disabled=${this.disabled}
class="warning"
>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.delete"
)}
<ha-svg-icon slot="start" .path=${mdiDelete}></ha-svg-icon>
</ha-md-menu-item>
<ha-script-field-selector-editor
class="sidebar-editor"
.hass=${this.hass}
.field=${this.config.config.field}
.disabled=${this.disabled}
@value-changed=${this._valueChangedSidebar}
.yamlMode=${this.yamlMode}
></ha-script-field-selector-editor>
</ha-automation-sidebar-card>`;
}
private _valueChangedSidebar(ev: CustomEvent) {
ev.stopPropagation();
this.config?.save?.({
...this.config.config.field,
key: this.config.config.key,
...ev.detail.value,
});
if (this.config) {
fireEvent(this, "value-changed", {
value: {
...this.config,
config: {
field: ev.detail.value,
key: this.config.config.key,
excludeKeys: this.config.config.excludeKeys,
selector: true,
},
},
});
}
}
private _toggleYamlMode = () => {
fireEvent(this, "toggle-yaml-mode");
};
static styles = css`
.sidebar-editor {
padding-top: 64px;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-automation-sidebar-script-field-selector": HaAutomationSidebarScriptFieldSelector;
}
}

View File

@@ -1,124 +0,0 @@
import { mdiDelete, mdiPlaylistEdit } from "@mdi/js";
import { css, html, LitElement } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event";
import type { ScriptFieldSidebarConfig } from "../../../../data/automation";
import type { HomeAssistant } from "../../../../types";
import "../../script/ha-script-field-editor";
import type HaAutomationConditionEditor from "../action/ha-automation-action-editor";
import "./ha-automation-sidebar-card";
@customElement("ha-automation-sidebar-script-field")
export default class HaAutomationSidebarScriptField extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public config!: ScriptFieldSidebarConfig;
@property({ type: Boolean, attribute: "wide" }) public isWide = false;
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean, attribute: "yaml-mode" }) public yamlMode = false;
@state() private _warnings?: string[];
@query(".sidebar-editor")
public editor?: HaAutomationConditionEditor;
protected willUpdate(changedProperties) {
if (changedProperties.has("config")) {
this._warnings = undefined;
if (this.config) {
this.yamlMode = this.config.yamlMode;
if (this.yamlMode) {
this.editor?.yamlEditor?.setValue(this.config.config);
}
}
}
}
protected render() {
const title = this.hass.localize(
"ui.panel.config.script.editor.field.label"
);
return html`<ha-automation-sidebar-card
.hass=${this.hass}
.isWide=${this.isWide}
.yamlMode=${this.yamlMode}
.warnings=${this._warnings}
>
<span slot="title">${title}</span>
<ha-md-menu-item
slot="menu-items"
.clickAction=${this._toggleYamlMode}
.disabled=${!!this._warnings}
>
${this.hass.localize(
`ui.panel.config.automation.editor.edit_${!this.yamlMode ? "yaml" : "ui"}`
)}
<ha-svg-icon slot="start" .path=${mdiPlaylistEdit}></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu-item
slot="menu-items"
.clickAction=${this.config.delete}
.disabled=${this.disabled}
class="warning"
>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.delete"
)}
<ha-svg-icon slot="start" .path=${mdiDelete}></ha-svg-icon>
</ha-md-menu-item>
<ha-script-field-editor
class="sidebar-editor"
.hass=${this.hass}
.field=${this.config.config.field}
.key=${this.config.config.key}
.excludeKeys=${this.config.config.excludeKeys}
.disabled=${this.disabled}
.yamlMode=${this.yamlMode}
@value-changed=${this._valueChangedSidebar}
></ha-script-field-editor>
</ha-automation-sidebar-card>`;
}
private _valueChangedSidebar(ev: CustomEvent) {
ev.stopPropagation();
this.config?.save?.({
...this.config.config.field,
key: ev.detail.value.key ?? this.config.config.key,
...ev.detail.value,
});
if (this.config) {
fireEvent(this, "value-changed", {
value: {
...this.config,
config: {
field: ev.detail.value,
key: ev.detail.value.key ?? this.config.config.key,
excludeKeys: this.config.config.excludeKeys,
},
},
});
}
}
private _toggleYamlMode = () => {
fireEvent(this, "toggle-yaml-mode");
};
static styles = css`
.sidebar-editor {
padding-top: 64px;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-automation-sidebar-script-field": HaAutomationSidebarScriptField;
}
}

View File

@@ -1,196 +0,0 @@
import {
mdiDelete,
mdiIdentifier,
mdiPlayCircleOutline,
mdiPlaylistEdit,
mdiRenameBox,
mdiStopCircleOutline,
} from "@mdi/js";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event";
import { handleStructError } from "../../../../common/structs/handle-errors";
import type { TriggerSidebarConfig } from "../../../../data/automation";
import { isTriggerList } from "../../../../data/trigger";
import type { HomeAssistant } from "../../../../types";
import "../trigger/ha-automation-trigger-editor";
import type HaAutomationTriggerEditor from "../trigger/ha-automation-trigger-editor";
import "./ha-automation-sidebar-card";
@customElement("ha-automation-sidebar-trigger")
export default class HaAutomationSidebarTrigger extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public config!: TriggerSidebarConfig;
@property({ type: Boolean, attribute: "wide" }) public isWide = false;
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean, attribute: "yaml-mode" }) public yamlMode = false;
@state() private _requestShowId = false;
@state() private _warnings?: string[];
@query(".sidebar-editor")
public editor?: HaAutomationTriggerEditor;
protected willUpdate(changedProperties) {
if (changedProperties.has("config")) {
this._requestShowId = false;
this._warnings = undefined;
if (this.config) {
this.yamlMode = this.config.yamlMode;
if (this.yamlMode) {
this.editor?.yamlEditor?.setValue(this.config.config);
}
}
}
}
protected render() {
const disabled =
this.disabled ||
("enabled" in this.config.config && this.config.config.enabled === false);
const type = isTriggerList(this.config.config)
? "list"
: this.config.config.trigger;
const subtitle = this.hass.localize(
"ui.panel.config.automation.editor.triggers.trigger"
);
const title = this.hass.localize(
`ui.panel.config.automation.editor.triggers.type.${type}.label`
);
return html`
<ha-automation-sidebar-card
.hass=${this.hass}
.isWide=${this.isWide}
.yamlMode=${this.yamlMode}
.warnings=${this._warnings}
>
<span slot="title">${title}</span>
<span slot="subtitle">${subtitle}</span>
<ha-md-menu-item
slot="menu-items"
.clickAction=${this.config.rename}
.disabled=${disabled || type === "list"}
>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.rename"
)}
<ha-svg-icon slot="start" .path=${mdiRenameBox}></ha-svg-icon>
</ha-md-menu-item>
${!this.yamlMode &&
!("id" in this.config.config) &&
!this._requestShowId
? html`<ha-md-menu-item
slot="menu-items"
.clickAction=${this._showTriggerId}
.disabled=${disabled || type === "list"}
>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.edit_id"
)}
<ha-svg-icon slot="start" .path=${mdiIdentifier}></ha-svg-icon>
</ha-md-menu-item>`
: nothing}
<ha-md-menu-item
slot="menu-items"
.clickAction=${this._toggleYamlMode}
.disabled=${!this.config.uiSupported || !!this._warnings}
>
${this.hass.localize(
`ui.panel.config.automation.editor.edit_${!this.yamlMode ? "yaml" : "ui"}`
)}
<ha-svg-icon slot="start" .path=${mdiPlaylistEdit}></ha-svg-icon>
</ha-md-menu-item>
<ha-md-divider
slot="menu-items"
role="separator"
tabindex="-1"
></ha-md-divider>
<ha-md-menu-item
slot="menu-items"
.clickAction=${this.config.disable}
.disabled=${type === "list"}
>
${this.hass.localize(
`ui.panel.config.automation.editor.actions.${disabled ? "enable" : "disable"}`
)}
<ha-svg-icon
slot="start"
.path=${this.disabled ? mdiPlayCircleOutline : mdiStopCircleOutline}
></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu-item
slot="menu-items"
.clickAction=${this.config.delete}
.disabled=${this.disabled}
class="warning"
>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.delete"
)}
<ha-svg-icon slot="start" .path=${mdiDelete}></ha-svg-icon>
</ha-md-menu-item>
<ha-automation-trigger-editor
class="sidebar-editor"
.hass=${this.hass}
.trigger=${this.config.config}
@value-changed=${this._valueChangedSidebar}
.uiSupported=${this.config.uiSupported}
.showId=${this._requestShowId}
.yamlMode=${this.yamlMode}
.disabled=${this.disabled}
@ui-mode-not-available=${this._handleUiModeNotAvailable}
></ha-automation-trigger-editor>
</ha-automation-sidebar-card>
`;
}
private _handleUiModeNotAvailable(ev: CustomEvent) {
this._warnings = handleStructError(this.hass, ev.detail).warnings;
if (!this.yamlMode) {
this.yamlMode = true;
}
}
private _valueChangedSidebar(ev: CustomEvent) {
ev.stopPropagation();
this.config?.save?.(ev.detail.value);
if (this.config) {
fireEvent(this, "value-changed", {
value: {
...this.config,
config: ev.detail.value,
},
});
}
}
private _toggleYamlMode = () => {
fireEvent(this, "toggle-yaml-mode");
};
private _showTriggerId = () => {
this._requestShowId = true;
};
static styles = css`
.sidebar-editor {
padding-top: 64px;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-automation-sidebar-trigger": HaAutomationSidebarTrigger;
}
}

View File

@@ -1,12 +0,0 @@
import { fireEvent } from "../../../../common/dom/fire_event";
import "./ha-automation-sidebar-bottom-sheet";
export const showAutomationSidebarBottomSheet = (
element: HTMLElement
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "ha-automation-sidebar-bottom-sheet",
dialogImport: () => import("./ha-automation-sidebar-bottom-sheet"),
dialogParams: {},
});
};

View File

@@ -34,11 +34,7 @@ import "../../../../components/ha-icon-button";
import "../../../../components/ha-md-button-menu";
import "../../../../components/ha-md-divider";
import "../../../../components/ha-md-menu-item";
import type {
AutomationClipboard,
Trigger,
TriggerSidebarConfig,
} from "../../../../data/automation";
import type { AutomationClipboard, Trigger } from "../../../../data/automation";
import { subscribeTrigger } from "../../../../data/automation";
import { describeTrigger } from "../../../../data/automation_i18n";
import { validateConfig } from "../../../../data/config";
@@ -468,14 +464,10 @@ export default class HaAutomationTriggerRow extends LitElement {
this.openSidebar();
}
private _scrollIntoView = () => {
this.scrollIntoView({
block: "start",
behavior: "smooth",
});
};
public openSidebar(trigger?: Trigger): void {
if (this.narrow) {
this.scrollIntoView();
}
fireEvent(this, "open-sidebar", {
save: (value) => {
fireEvent(this, "value-changed", { value });
@@ -494,10 +486,10 @@ export default class HaAutomationTriggerRow extends LitElement {
disable: this._onDisable,
delete: this._onDelete,
config: trigger || this.trigger,
type: "trigger",
uiSupported: this._uiSupported(this._getType(trigger || this.trigger)),
yamlMode: this._yamlMode,
scrollIntoView: this._scrollIntoView,
} satisfies TriggerSidebarConfig);
});
this._selected = true;
}

View File

@@ -180,6 +180,7 @@ export default class HaAutomationTrigger extends LitElement {
} else {
row.expand();
}
row.scrollIntoView();
row.focus();
});
}

View File

@@ -1,22 +1,14 @@
import { mdiContentSave } from "@mdi/js";
import { css, html, nothing, type CSSResultGroup } from "lit";
import { html, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event";
import "../../../components/ha-fab";
import "../../../components/ha-markdown";
import { fetchBlueprints } from "../../../data/blueprint";
import type { BlueprintScriptConfig } from "../../../data/script";
import { saveFabStyles } from "../automation/styles";
import { HaBlueprintGenericEditor } from "../blueprint/blueprint-generic-editor";
@customElement("blueprint-script-editor")
export class HaBlueprintScriptEditor extends HaBlueprintGenericEditor {
@property({ attribute: false }) public config!: BlueprintScriptConfig;
@property({ type: Boolean }) public saving = false;
@property({ type: Boolean }) public dirty = false;
protected get _config(): BlueprintScriptConfig {
return this.config;
}
@@ -31,45 +23,12 @@ export class HaBlueprintScriptEditor extends HaBlueprintGenericEditor {
></ha-markdown>`
: nothing}
${this.renderCard()}
<ha-fab
slot="fab"
class=${this.dirty ? "dirty" : ""}
.label=${this.hass.localize("ui.common.save")}
.disabled=${this.saving}
extended
@click=${this._saveScript}
>
<ha-svg-icon slot="icon" .path=${mdiContentSave}></ha-svg-icon>
</ha-fab>
`;
}
private _saveScript() {
fireEvent(this, "save-script");
}
protected async _getBlueprints() {
this._blueprints = await fetchBlueprints(this.hass, "script");
}
static get styles(): CSSResultGroup {
return [
HaBlueprintGenericEditor.styles,
saveFabStyles,
css`
:host {
position: relative;
height: 100%;
min-height: calc(100vh - 85px);
min-height: calc(100dvh - 85px);
}
ha-fab {
position: fixed;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {

View File

@@ -358,61 +358,59 @@ export class HaScriptEditor extends SubscribeMixin(
</ha-svg-icon>
</ha-list-item>
</ha-button-menu>
<div class=${this._mode === "yaml" ? "yaml-mode" : ""}>
<div class="error-wrapper">
${this._errors || stateObj?.state === UNAVAILABLE
? html`<ha-alert
alert-type="error"
.title=${stateObj?.state === UNAVAILABLE
? this.hass.localize(
"ui.panel.config.script.editor.unavailable"
)
: undefined}
>
${this._errors || this._validationErrors}
${stateObj?.state === UNAVAILABLE
? html`<ha-svg-icon
slot="icon"
.path=${mdiRobotConfused}
></ha-svg-icon>`
: nothing}
<div
class="content ${classMap({
"yaml-mode": this._mode === "yaml",
})}"
>
${this._errors || stateObj?.state === UNAVAILABLE
? html`<ha-alert
alert-type="error"
.title=${stateObj?.state === UNAVAILABLE
? this.hass.localize(
"ui.panel.config.script.editor.unavailable"
)
: undefined}
>
${this._errors || this._validationErrors}
${stateObj?.state === UNAVAILABLE
? html`<ha-svg-icon
slot="icon"
.path=${mdiRobotConfused}
></ha-svg-icon>`
: nothing}
</ha-alert>`
: ""}
${this._blueprintConfig
? html`<ha-alert alert-type="info">
${this.hass.localize(
"ui.panel.config.script.editor.confirm_take_control"
)}
<div slot="action" style="display: flex;">
<ha-button appearance="plain" @click=${this._takeControlSave}
>${this.hass.localize("ui.common.yes")}</ha-button
>
<ha-button appearance="plain" @click=${this._revertBlueprint}
>${this.hass.localize("ui.common.no")}</ha-button
>
</div>
</ha-alert>`
: this._readOnly
? html`<ha-alert alert-type="warning" dismissable
>${this.hass.localize(
"ui.panel.config.script.editor.read_only"
)}
<ha-button
appearance="plain"
slot="action"
@click=${this._duplicate}
>
${this.hass.localize(
"ui.panel.config.script.editor.migrate"
)}
</ha-button>
</ha-alert>`
: nothing}
${this._blueprintConfig
? html`<ha-alert alert-type="info">
${this.hass.localize(
"ui.panel.config.script.editor.confirm_take_control"
)}
<div slot="action" style="display: flex;">
<ha-button
appearance="plain"
@click=${this._takeControlSave}
>${this.hass.localize("ui.common.yes")}</ha-button
>
<ha-button
appearance="plain"
@click=${this._revertBlueprint}
>${this.hass.localize("ui.common.no")}</ha-button
>
</div>
</ha-alert>`
: this._readOnly
? html`<ha-alert alert-type="warning" dismissable
>${this.hass.localize(
"ui.panel.config.script.editor.read_only"
)}
<ha-button
appearance="plain"
slot="action"
@click=${this._duplicate}
>
${this.hass.localize(
"ui.panel.config.script.editor.migrate"
)}
</ha-button>
</ha-alert>`
: nothing}
</div>
${this._mode === "gui"
? html`
<div
@@ -428,10 +426,7 @@ export class HaScriptEditor extends SubscribeMixin(
.isWide=${this.isWide}
.config=${this._config}
.disabled=${this._readOnly}
.saving=${this._saving}
.dirty=${this._dirty}
@value-changed=${this._valueChanged}
@save-script=${this._handleSaveScript}
></blueprint-script-editor>
`
: html`
@@ -442,40 +437,39 @@ export class HaScriptEditor extends SubscribeMixin(
.config=${this._config}
.disabled=${this._readOnly}
.dirty=${this._dirty}
.saving=${this._saving}
@value-changed=${this._valueChanged}
@editor-save=${this._handleSaveScript}
@save-script=${this._handleSaveScript}
@editor-save=${this._handleSave}
></manual-script-editor>
`}
</div>
`
: this._mode === "yaml"
? html`<ha-yaml-editor
copy-clipboard
.hass=${this.hass}
.defaultValue=${this._preprocessYaml()}
.readOnly=${this._readOnly}
disable-fullscreen
@value-changed=${this._yamlChanged}
@editor-save=${this._handleSaveScript}
.showErrors=${false}
></ha-yaml-editor>
<ha-fab
slot="fab"
class=${!this._readOnly && this._dirty ? "dirty" : ""}
.label=${this.hass.localize("ui.common.save")}
.disabled=${this._saving}
extended
@click=${this._handleSaveScript}
>
<ha-svg-icon
slot="icon"
.path=${mdiContentSave}
></ha-svg-icon>
</ha-fab>`
copy-clipboard
.hass=${this.hass}
.defaultValue=${this._preprocessYaml()}
.readOnly=${this._readOnly}
disable-fullscreen
@value-changed=${this._yamlChanged}
@editor-save=${this._handleSave}
.showErrors=${false}
></ha-yaml-editor>`
: nothing}
</div>
<ha-fab
slot="fab"
class=${classMap({
dirty: !this._readOnly && this._dirty,
})}
.label=${this.hass.localize(
"ui.panel.config.script.editor.save_script"
)}
.disabled=${this._saving}
extended
@click=${this._handleSave}
>
<ha-svg-icon slot="icon" .path=${mdiContentSave}></ha-svg-icon>
</ha-fab>
</hass-subpage>
`;
}
@@ -911,7 +905,7 @@ export class HaScriptEditor extends SubscribeMixin(
});
}
private async _handleSaveScript() {
private async _handleSave() {
if (this._yamlErrors) {
showToast(this, {
message: this._yamlErrors,
@@ -1018,7 +1012,7 @@ export class HaScriptEditor extends SubscribeMixin(
protected supportedShortcuts(): SupportedShortcuts {
return {
s: () => this._handleSaveScript(),
s: () => this._handleSave(),
};
}
@@ -1034,40 +1028,33 @@ export class HaScriptEditor extends SubscribeMixin(
return [
haStyle,
css`
p {
margin-bottom: 0;
}
.errors {
padding: 20px;
font-weight: var(--ha-font-weight-bold);
color: var(--error-color);
}
.yaml-mode {
height: 100%;
display: flex;
flex-direction: column;
padding-bottom: 0;
}
.config-container,
manual-script-editor,
blueprint-script-editor {
blueprint-script-editor,
:not(.yaml-mode) > ha-alert {
margin: 0 auto;
max-width: 1040px;
padding: 28px 20px 0;
display: block;
}
:not(.yaml-mode) > .error-wrapper {
position: absolute;
top: 4px;
z-index: 3;
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
.config-container ha-alert {
margin-bottom: 16px;
display: block;
}
:not(.yaml-mode) > .error-wrapper ha-alert {
background-color: var(--card-background-color);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
border-radius: var(--ha-border-radius-sm);
}
manual-script-editor {
max-width: 1540px;
padding: 0 12px;
}
ha-yaml-editor {
flex-grow: 1;
--actions-border-radius: 0;
@@ -1076,20 +1063,16 @@ export class HaScriptEditor extends SubscribeMixin(
display: flex;
flex-direction: column;
}
p {
margin-bottom: 0;
}
span[slot="introduction"] a {
color: var(--primary-color);
}
ha-fab {
position: fixed;
right: 16px;
position: relative;
bottom: calc(-80px - var(--safe-area-inset-bottom));
transition: bottom 0.3s;
}
ha-fab.dirty {
bottom: 16px;
bottom: 0;
}
li[role="separator"] {
border-bottom-color: var(--divider-color);
@@ -1122,8 +1105,4 @@ declare global {
interface HTMLElementTagNameMap {
"ha-script-editor": HaScriptEditor;
}
interface HASSDomEvents {
"save-script": undefined;
}
}

View File

@@ -1,187 +0,0 @@
import { LitElement, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../common/dom/fire_event";
import { slugify } from "../../../common/string/slugify";
import "../../../components/ha-alert";
import "../../../components/ha-form/ha-form";
import type { SchemaUnion } from "../../../components/ha-form/types";
import "../../../components/ha-yaml-editor";
import type { Field } from "../../../data/script";
import { haStyle } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types";
@customElement("ha-script-field-editor")
export default class HaScriptFieldEditor extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public key!: string;
@property({ attribute: false, type: Array }) public excludeKeys: string[] =
[];
@property({ attribute: false }) public field!: Field;
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean, attribute: "yaml-mode" }) public yamlMode = false;
@state() private _uiError?: Record<string, string>;
@state() private _yamlError?: undefined | "yaml_error" | "key_not_unique";
private _errorKey?: string;
private _schema = memoizeOne(
() =>
[
{
name: "name",
selector: { text: {} },
},
{
name: "key",
selector: { text: {} },
},
{
name: "description",
selector: { text: {} },
},
{
name: "required",
selector: { boolean: {} },
},
] as const
);
protected render() {
const schema = this._schema();
const data = { ...this.field, key: this._errorKey ?? this.key };
const yamlValue = { [this.key]: this.field };
return html`
${this.yamlMode
? html`${this._yamlError
? html`<ha-alert alert-type="error">
${this.hass.localize(
`ui.panel.config.script.editor.field.${this._yamlError}`
)}
</ha-alert>`
: nothing}
<ha-yaml-editor
.hass=${this.hass}
.defaultValue=${yamlValue}
@value-changed=${this._onYamlChange}
></ha-yaml-editor>`
: html`<ha-form
.schema=${schema}
.data=${data}
.error=${this._uiError}
.hass=${this.hass}
.disabled=${this.disabled}
.computeLabel=${this._computeLabelCallback}
.computeError=${this._computeError}
@value-changed=${this._valueChanged}
></ha-form>`}
`;
}
private _maybeSetKey(value): void {
const nameChanged = value.name !== this.field.name;
const keyChanged = value.key !== this.key;
if (!nameChanged || keyChanged) {
return;
}
const slugifyName = this.field.name
? slugify(this.field.name)
: this.hass.localize("ui.panel.config.script.editor.field.field") ||
"field";
const regex = new RegExp(`^${slugifyName}(_\\d)?$`);
if (regex.test(this.key)) {
let key = !value.name
? this.hass.localize("ui.panel.config.script.editor.field.field") ||
"field"
: slugify(value.name);
if (this.excludeKeys.includes(key)) {
let uniqueKey = key;
let i = 2;
do {
uniqueKey = `${key}_${i}`;
i++;
} while (this.excludeKeys.includes(uniqueKey));
key = uniqueKey;
}
value.key = key;
}
}
private _valueChanged(ev: CustomEvent) {
ev.stopPropagation();
const value = { ...ev.detail.value };
this._maybeSetKey(value);
// Don't allow to set an empty key, or duplicate an existing key.
if (!value.key || this.excludeKeys.includes(value.key)) {
this._uiError = value.key
? {
key: "key_not_unique",
}
: {
key: "key_not_null",
};
this._errorKey = value.key ?? "";
return;
}
this._errorKey = undefined;
this._uiError = undefined;
// If we render the default with an incompatible selector, it risks throwing an exception and not rendering.
// Clear the default when changing the selector type.
if (
Object.keys(this.field.selector)[0] !== Object.keys(value.selector)[0]
) {
delete value.default;
}
fireEvent(this, "value-changed", { value });
}
private _onYamlChange(ev: CustomEvent) {
ev.stopPropagation();
const value = { ...ev.detail.value };
if (typeof value !== "object" || Object.keys(value).length !== 1) {
this._yamlError = "yaml_error";
return;
}
const key = Object.keys(value)[0];
if (this.excludeKeys.includes(key)) {
this._yamlError = "key_not_unique";
return;
}
this._yamlError = undefined;
const newValue = { ...value[key], key };
fireEvent(this, "value-changed", { value: newValue });
}
private _computeLabelCallback = (
schema: SchemaUnion<ReturnType<typeof this._schema>>
): string =>
this.hass.localize(`ui.panel.config.script.editor.field.${schema.name}`);
private _computeError = (error: string) =>
this.hass.localize(`ui.panel.config.script.editor.field.${error}` as any) ||
error;
static styles = haStyle;
}
declare global {
interface HTMLElementTagNameMap {
"ha-script-field-editor": HaScriptFieldEditor;
}
}

View File

@@ -1,24 +1,27 @@
import { mdiDelete, mdiDotsVertical } from "@mdi/js";
import type { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
import { mdiDelete, mdiDotsVertical, mdiPlaylistEdit } from "@mdi/js";
import type { CSSResultGroup } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../common/dom/fire_event";
import { preventDefaultStopPropagation } from "../../../common/dom/prevent_default_stop_propagation";
import { stopPropagation } from "../../../common/dom/stop_propagation";
import type { LocalizeKeys } from "../../../common/translations/localize";
import "../../../components/ha-automation-row";
import { slugify } from "../../../common/string/slugify";
import "../../../components/ha-alert";
import "../../../components/ha-button-menu";
import "../../../components/ha-card";
import "../../../components/ha-form/ha-form";
import "../../../components/ha-expansion-panel";
import "../../../components/ha-list-item";
import type { SchemaUnion } from "../../../components/ha-form/types";
import "../../../components/ha-icon-button";
import "../../../components/ha-md-button-menu";
import "../../../components/ha-md-menu-item";
import type { ScriptFieldSidebarConfig } from "../../../data/automation";
import "../../../components/ha-yaml-editor";
import type { Field } from "../../../data/script";
import { SELECTOR_SELECTOR_BUILDING_BLOCKS } from "../../../data/selector/selector_selector";
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
import { haStyle } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types";
import "./ha-script-field-selector-editor";
const preventDefault = (ev) => ev.preventDefault();
@customElement("ha-script-field-row")
export default class HaScriptFieldRow extends LitElement {
@@ -33,38 +36,61 @@ export default class HaScriptFieldRow extends LitElement {
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public narrow = false;
@state() private _uiError?: Record<string, string>;
@state() private _yamlError?: undefined | "yaml_error" | "key_not_unique";
@state() private _yamlMode = false;
@state() private _selected = false;
private _errorKey?: string;
@state() private _collapsed = false;
@state() private _selectorRowSelected = false;
@state() private _selectorRowCollapsed = false;
private _schema = memoizeOne(
(selector: any) =>
[
{
name: "name",
selector: { text: {} },
},
{
name: "key",
selector: { text: {} },
},
{
name: "description",
selector: { text: {} },
},
{
name: "selector",
selector: { selector: {} },
},
{
name: "default",
selector: selector && typeof selector === "object" ? selector : {},
},
{
name: "required",
selector: { boolean: {} },
},
] as const
);
protected render() {
const schema = this._schema(this.field.selector);
const data = { ...this.field, key: this._errorKey ?? this.key };
const yamlValue = { [this.key]: this.field };
return html`
<ha-card outlined>
<ha-automation-row
.disabled=${this.disabled}
@click=${this._toggleSidebar}
.selected=${this._selected}
left-chevron
@toggle-collapsed=${this._toggleCollapse}
.collapsed=${this._collapsed}
>
<ha-expansion-panel left-chevron>
<h3 slot="header">${this.key}</h3>
<slot name="icons" slot="icons"></slot>
<ha-md-button-menu
<ha-button-menu
slot="icons"
@click=${preventDefaultStopPropagation}
@keydown=${stopPropagation}
@closed=${stopPropagation}
positioning="fixed"
@action=${this._handleAction}
@click=${preventDefault}
fixed
>
<ha-icon-button
slot="trigger"
@@ -72,9 +98,19 @@ export default class HaScriptFieldRow extends LitElement {
.path=${mdiDotsVertical}
></ha-icon-button>
<ha-md-menu-item
<ha-list-item graphic="icon">
${this.hass.localize(
`ui.panel.config.automation.editor.edit_${!this._yamlMode ? "yaml" : "ui"}`
)}
<ha-svg-icon
slot="graphic"
.path=${mdiPlaylistEdit}
></ha-svg-icon>
</ha-list-item>
<ha-list-item
class="warning"
.clickAction=${this._onDelete}
graphic="icon"
.disabled=${this.disabled}
>
${this.hass.localize(
@@ -85,149 +121,54 @@ export default class HaScriptFieldRow extends LitElement {
slot="graphic"
.path=${mdiDelete}
></ha-svg-icon>
</ha-md-menu-item>
</ha-md-button-menu>
</ha-automation-row>
</ha-card>
<div
class=${classMap({
"selector-row": true,
"parent-selected": this._selected,
hidden: this._collapsed,
})}
>
<ha-card>
<ha-automation-row
.selected=${this._selectorRowSelected}
@click=${this._toggleSelectorSidebar}
.collapsed=${this._selectorRowCollapsed}
@toggle-collapsed=${this._toggleSelectorRowCollapse}
.leftChevron=${SELECTOR_SELECTOR_BUILDING_BLOCKS.includes(
Object.keys(this.field.selector)[0]
)}
</ha-list-item>
</ha-button-menu>
<div
class=${classMap({
"card-content": true,
})}
>
<h3 slot="header">
${this.hass.localize(
`ui.components.selectors.selector.types.${Object.keys(this.field.selector)[0]}` as LocalizeKeys
)}
${this.hass.localize(
"ui.panel.config.script.editor.field.selector"
)}
</h3>
</ha-automation-row>
</ha-card>
${typeof this.field.selector === "object" &&
SELECTOR_SELECTOR_BUILDING_BLOCKS.includes(
Object.keys(this.field.selector)[0]
)
? html`
<ha-script-field-selector-editor
class=${this._selectorRowCollapsed ? "hidden" : ""}
.selected=${this._selectorRowSelected}
.hass=${this.hass}
.field=${this.field}
.disabled=${this.disabled}
indent
@value-changed=${this._selectorValueChanged}
.narrow=${this.narrow}
></ha-script-field-selector-editor>
`
: nothing}
</div>
${this._yamlMode
? html` ${this._yamlError
? html`<ha-alert alert-type="error">
${this.hass.localize(
`ui.panel.config.script.editor.field.${this._yamlError}`
)}
</ha-alert>`
: nothing}
<ha-yaml-editor
.hass=${this.hass}
.defaultValue=${yamlValue}
@value-changed=${this._onYamlChange}
></ha-yaml-editor>`
: html`<ha-form
.schema=${schema}
.data=${data}
.error=${this._uiError}
.hass=${this.hass}
.disabled=${this.disabled}
.computeLabel=${this._computeLabelCallback}
.computeError=${this._computeError}
@value-changed=${this._valueChanged}
></ha-form>`}
</div>
</ha-expansion-panel>
</ha-card>
`;
}
private _toggleCollapse() {
this._collapsed = !this._collapsed;
}
private _toggleSelectorRowCollapse() {
this._selectorRowCollapsed = !this._selectorRowCollapsed;
}
private _toggleSidebar(ev: Event) {
ev?.stopPropagation();
if (this._selected) {
this._selected = false;
fireEvent(this, "close-sidebar");
return;
private async _handleAction(ev: CustomEvent<ActionDetail>) {
switch (ev.detail.index) {
case 0:
this._yamlMode = !this._yamlMode;
break;
case 1:
this._onDelete();
break;
}
this._selected = true;
this.openSidebar();
}
private _toggleSelectorSidebar(ev: Event) {
ev?.stopPropagation();
if (this._selectorRowSelected) {
this._selectorRowSelected = false;
fireEvent(this, "close-sidebar");
return;
}
this._selectorRowSelected = true;
this.openSidebar(true);
}
private _selectorValueChanged(ev: CustomEvent) {
ev.stopPropagation();
fireEvent(this, "value-changed", {
value: {
...this.field,
key: this.key,
...ev.detail.value,
},
});
}
private _scrollIntoView = () => {
this.scrollIntoView({
block: "start",
behavior: "smooth",
});
};
public openSidebar(selectorEditor = false): void {
if (!selectorEditor) {
this._selected = true;
}
fireEvent(this, "open-sidebar", {
save: (value) => {
fireEvent(this, "value-changed", { value });
},
close: () => {
if (selectorEditor) {
this._selectorRowSelected = false;
} else {
this._selected = false;
}
fireEvent(this, "close-sidebar");
},
toggleYamlMode: () => {
this._toggleYamlMode();
return this._yamlMode;
},
delete: this._onDelete,
config: {
field: this.field,
selector: selectorEditor,
key: this.key,
excludeKeys: this.excludeKeys,
},
yamlMode: this._yamlMode,
scrollIntoView: this._scrollIntoView,
} satisfies ScriptFieldSidebarConfig);
}
private _toggleYamlMode = () => {
this._yamlMode = !this._yamlMode;
};
private _onDelete = () => {
private _onDelete() {
showConfirmationDialog(this, {
title: this.hass.localize(
"ui.panel.config.script.editor.field_delete_confirm_title"
@@ -240,13 +181,112 @@ export default class HaScriptFieldRow extends LitElement {
destructive: true,
confirm: () => {
fireEvent(this, "value-changed", { value: null });
if (this._selected || this._selectorRowSelected) {
fireEvent(this, "close-sidebar");
}
},
});
}
private _onYamlChange(ev: CustomEvent) {
ev.stopPropagation();
const value = { ...ev.detail.value };
if (typeof value !== "object" || Object.keys(value).length !== 1) {
this._yamlError = "yaml_error";
return;
}
const key = Object.keys(value)[0];
if (this.excludeKeys.includes(key)) {
this._yamlError = "key_not_unique";
return;
}
this._yamlError = undefined;
const newValue = { ...value[key], key };
fireEvent(this, "value-changed", { value: newValue });
}
private _maybeSetKey(value): void {
const nameChanged = value.name !== this.field.name;
const keyChanged = value.key !== this.key;
if (!nameChanged || keyChanged) {
return;
}
const slugifyName = this.field.name
? slugify(this.field.name)
: this.hass.localize("ui.panel.config.script.editor.field.field") ||
"field";
const regex = new RegExp(`^${slugifyName}(_\\d)?$`);
if (regex.test(this.key)) {
let key = !value.name
? this.hass.localize("ui.panel.config.script.editor.field.field") ||
"field"
: slugify(value.name);
if (this.excludeKeys.includes(key)) {
let uniqueKey = key;
let i = 2;
do {
uniqueKey = `${key}_${i}`;
i++;
} while (this.excludeKeys.includes(uniqueKey));
key = uniqueKey;
}
value.key = key;
}
}
private _valueChanged(ev: CustomEvent) {
ev.stopPropagation();
const value = { ...ev.detail.value };
this._maybeSetKey(value);
// Don't allow to set an empty key, or duplicate an existing key.
if (!value.key || this.excludeKeys.includes(value.key)) {
this._uiError = value.key
? {
key: "key_not_unique",
}
: {
key: "key_not_null",
};
this._errorKey = value.key ?? "";
return;
}
this._errorKey = undefined;
this._uiError = undefined;
// If we render the default with an incompatible selector, it risks throwing an exception and not rendering.
// Clear the default when changing the selector type.
if (
Object.keys(this.field.selector)[0] !== Object.keys(value.selector)[0]
) {
delete value.default;
}
fireEvent(this, "value-changed", { value });
}
public expand() {
this.updateComplete.then(() => {
this.shadowRoot!.querySelector("ha-expansion-panel")!.expanded = true;
});
}
private _computeLabelCallback = (
schema: SchemaUnion<ReturnType<typeof this._schema>>
): string => {
switch (schema.name) {
default:
return this.hass.localize(
`ui.panel.config.script.editor.field.${schema.name}`
);
}
};
private _computeError = (error: string) =>
this.hass.localize(`ui.panel.config.script.editor.field.${error}` as any) ||
error;
static get styles(): CSSResultGroup {
return [
haStyle,
@@ -259,8 +299,9 @@ export default class HaScriptFieldRow extends LitElement {
opacity: 0.5;
pointer-events: none;
}
.hidden {
display: none;
ha-expansion-panel {
--expansion-panel-summary-padding: 0 0 0 8px;
--expansion-panel-content-padding: 0;
}
h3 {
margin: 0;
@@ -300,7 +341,7 @@ export default class HaScriptFieldRow extends LitElement {
);
}
ha-md-menu-item[disabled] {
ha-list-item[disabled] {
--mdc-theme-text-primary-on-background: var(--disabled-text-color);
}
.warning ul {
@@ -318,18 +359,6 @@ export default class HaScriptFieldRow extends LitElement {
border-color: var(--state-inactive-color);
box-shadow: var(--shadow-default), var(--shadow-focus);
}
.selector-row {
margin-left: 12px;
padding: 12px 4px 16px 16px;
margin-right: -4px;
border-left: 2px solid var(--ha-color-border-neutral-quiet);
}
.selector-row.parent-selected {
border-color: var(--primary-color);
background-color: var(--ha-color-fill-primary-quiet-resting);
border-top-right-radius: var(--ha-border-radius-xl);
border-bottom-right-radius: var(--ha-border-radius-xl);
}
`,
];
}

View File

@@ -1,167 +0,0 @@
import type { CSSResultGroup } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../common/dom/fire_event";
import type { LocalizeKeys } from "../../../common/translations/localize";
import "../../../components/ha-alert";
import "../../../components/ha-form/ha-form";
import type { SchemaUnion } from "../../../components/ha-form/types";
import "../../../components/ha-yaml-editor";
import type { Field } from "../../../data/script";
import { SELECTOR_SELECTOR_BUILDING_BLOCKS } from "../../../data/selector/selector_selector";
import { haStyle } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types";
@customElement("ha-script-field-selector-editor")
export default class HaScriptFieldSelectorEditor extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public field!: Field;
@property({ type: Boolean }) public narrow = false;
@property({ type: Boolean, reflect: true }) public selected = false;
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public indent = false;
@property({ type: Boolean, attribute: "yaml-mode" }) public yamlMode = false;
@state() private _uiError?: Record<string, string>;
@state() private _yamlError?: undefined | "yaml_error" | "key_not_unique";
private _schema = memoizeOne(
(selector: any) =>
[
...(!this.indent
? [
{
name: "selector",
selector: { selector: {} },
},
]
: []),
...(selector &&
typeof selector === "object" &&
(this.indent ||
!SELECTOR_SELECTOR_BUILDING_BLOCKS.includes(Object.keys(selector)[0]))
? [
{
name: "default",
selector: !this.indent
? selector
: {
[Object.keys(selector)[0]]: {
...selector[Object.keys(selector)[0]],
optionsInSidebar: true,
},
},
},
]
: []),
] as const
);
protected render() {
const schema = this._schema(this.field.selector);
const data = { selector: this.field.selector, default: this.field.default };
return html`
${this.yamlMode
? html`${this._yamlError
? html`<ha-alert alert-type="error">
${this.hass.localize(
`ui.panel.config.script.editor.field.${this._yamlError}`
)}
</ha-alert>`
: nothing}
<ha-yaml-editor
.hass=${this.hass}
.defaultValue=${data}
@value-changed=${this._onYamlChange}
></ha-yaml-editor>`
: html`<ha-form
.schema=${schema}
.data=${data}
.error=${this._uiError}
.hass=${this.hass}
.disabled=${this.disabled}
.computeLabel=${this._computeLabelCallback}
.computeError=${this._computeError}
@value-changed=${this._valueChanged}
.narrow=${this.narrow}
></ha-form>`}
`;
}
private _valueChanged(ev: CustomEvent) {
ev.stopPropagation();
const value = { ...ev.detail.value };
this._uiError = undefined;
// If we render the default with an incompatible selector, it risks throwing an exception and not rendering.
// Clear the default when changing the selector type.
if (
!this.indent &&
Object.keys(this.field.selector)[0] !== Object.keys(value.selector)[0]
) {
value.default = undefined;
}
fireEvent(this, "value-changed", { value });
}
private _onYamlChange(ev: CustomEvent) {
ev.stopPropagation();
const value = { ...ev.detail.value };
if (typeof value !== "object" || Object.keys(value).length !== 2) {
this._yamlError = "yaml_error";
return;
}
fireEvent(this, "value-changed", { value });
}
private _computeLabelCallback = (
schema: SchemaUnion<ReturnType<typeof this._schema>>
): string =>
this.hass.localize(
`ui.panel.config.script.editor.field.${schema.name}` as LocalizeKeys
) ?? schema.name;
private _computeError = (error: string) =>
this.hass.localize(`ui.panel.config.script.editor.field.${error}` as any) ||
error;
static get styles(): CSSResultGroup {
return [
haStyle,
css`
:host([indent]) ha-form {
display: block;
margin-left: 12px;
padding: 12px 20px 16px 16px;
margin-right: -4px;
border-left: 2px solid var(--ha-color-border-neutral-quiet);
}
:host([selected]) ha-form {
border-color: var(--primary-color);
background-color: var(--ha-color-fill-primary-quiet-resting);
border-top-right-radius: var(--ha-border-radius-xl);
border-bottom-right-radius: var(--ha-border-radius-xl);
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-script-field-selector-editor": HaScriptFieldSelectorEditor;
}
}

View File

@@ -21,8 +21,6 @@ export default class HaScriptFields extends LitElement {
@property({ attribute: false }) public highlightedFields?: Fields;
@property({ type: Boolean }) public narrow = false;
private _focusLastActionOnChange = false;
protected render() {
@@ -41,7 +39,6 @@ export default class HaScriptFields extends LitElement {
@value-changed=${this._fieldChanged}
.hass=${this.hass}
?highlight=${this.highlightedFields?.[key] !== undefined}
.narrow=${this.narrow}
>
</ha-script-field-row>
`
@@ -74,7 +71,8 @@ export default class HaScriptFields extends LitElement {
"ha-script-field-row:last-of-type"
)!;
row.updateComplete.then(() => {
row.openSidebar();
row.expand();
row.scrollIntoView();
row.focus();
});
}

View File

@@ -1,9 +1,8 @@
import { mdiContentSave, mdiHelpCircle } from "@mdi/js";
import { load } from "js-yaml";
import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { mdiHelpCircle } from "@mdi/js";
import type { CSSResultGroup, PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { load } from "js-yaml";
import {
any,
array,
@@ -14,27 +13,32 @@ import {
optional,
string,
} from "superstruct";
import { ensureArray } from "../../../common/array/ensure-array";
import { canOverrideAlphanumericInput } from "../../../common/dom/can-override-input";
import { fireEvent } from "../../../common/dom/fire_event";
import { constructUrlCurrentPath } from "../../../common/url/construct-url";
import {
extractSearchParam,
removeSearchParam,
} from "../../../common/url/search-params";
import "../../../components/ha-card";
import "../../../components/ha-icon-button";
import "../../../components/ha-markdown";
import type { SidebarConfig } from "../../../data/automation";
import type { Action, Fields, ScriptConfig } from "../../../data/script";
import {
getActionType,
MODES,
normalizeScriptConfig,
} from "../../../data/script";
import { haStyle } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types";
import { documentationUrl } from "../../../util/documentation-url";
import { showToast } from "../../../util/toast";
import "../automation/action/ha-automation-action";
import "../automation/ha-automation-sidebar";
import { showPasteReplaceDialog } from "../automation/paste-replace-dialog/show-dialog-paste-replace";
import { saveFabStyles } from "../automation/styles";
import type HaAutomationAction from "../automation/action/ha-automation-action";
import "./ha-script-fields";
import type HaScriptFields from "./ha-script-fields";
import { canOverrideAlphanumericInput } from "../../../common/dom/can-override-input";
import { showToast } from "../../../util/toast";
import { showPasteReplaceDialog } from "../automation/paste-replace-dialog/show-dialog-paste-replace";
import { ensureArray } from "../../../common/array/ensure-array";
const scriptConfigStruct = object({
alias: optional(string()),
@@ -56,8 +60,6 @@ export class HaManualScriptEditor extends LitElement {
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public saving = false;
@property({ attribute: false }) public config!: ScriptConfig;
@property({ attribute: false }) public dirty = false;
@@ -69,8 +71,6 @@ export class HaManualScriptEditor extends LitElement {
@state() private _pastedConfig?: ScriptConfig;
@state() private _sidebarConfig?: SidebarConfig;
private _previousConfig?: ScriptConfig;
public addFields() {
@@ -99,19 +99,41 @@ export class HaManualScriptEditor extends LitElement {
}
}
private _renderContent() {
protected firstUpdated(changedProps: PropertyValues): void {
super.firstUpdated(changedProps);
const expanded = extractSearchParam("expanded");
if (expanded === "1") {
this._clearParam("expanded");
const items = this.shadowRoot!.querySelectorAll<HaAutomationAction>(
"ha-automation-action"
);
items.forEach((el) => {
el.updateComplete.then(() => {
el.expandAll();
});
});
}
}
private _clearParam(param: string) {
window.history.replaceState(
null,
"",
constructUrlCurrentPath(removeSearchParam(param))
);
}
protected render() {
return html`
${
this.config.description
? html`<ha-markdown
class="description"
breaks
.content=${this.config.description}
></ha-markdown>`
: nothing
}
${
this.config.fields
${this.config.description
? html`<ha-markdown
class="description"
breaks
.content=${this.config.description}
></ha-markdown>`
: nothing}
${this.config.fields
? html`<div class="header">
<h2 id="fields-heading" class="name">
${this.hass.localize(
@@ -143,77 +165,39 @@ export class HaManualScriptEditor extends LitElement {
@value-changed=${this._fieldsChanged}
.hass=${this.hass}
.disabled=${this.disabled}
.narrow=${this.narrow}
@open-sidebar=${this._openSidebar}
@close-sidebar=${this._handleCloseSidebar}
></ha-script-fields>`
: nothing
}
: nothing}
<div class="header">
<h2 id="sequence-heading" class="name">
${this.hass.localize("ui.panel.config.script.editor.sequence")}
</h2>
<a
href=${documentationUrl(this.hass, "/docs/scripts/")}
target="_blank"
rel="noreferrer"
>
<ha-icon-button
.path=${mdiHelpCircle}
.label=${this.hass.localize(
"ui.panel.config.script.editor.link_available_actions"
)}
></ha-icon-button>
</a>
</div>
<ha-automation-action
role="region"
aria-labelledby="sequence-heading"
.actions=${this.config.sequence || []}
.highlightedActions=${this._pastedConfig?.sequence || []}
@value-changed=${this._sequenceChanged}
@open-sidebar=${this._openSidebar}
@close-sidebar=${this._handleCloseSidebar}
.hass=${this.hass}
.narrow=${this.narrow}
.disabled=${this.disabled || this.saving}
root
sidebar
></ha-automation-action>
</div>`;
}
protected render() {
return html`
<div class="split-view">
<div class="content-wrapper">
<div class="content">${this._renderContent()}</div>
<ha-fab
slot="fab"
class=${this.dirty ? "dirty" : ""}
.label=${this.hass.localize("ui.common.save")}
.disabled=${this.saving}
extended
@click=${this._saveScript}
>
<ha-svg-icon slot="icon" .path=${mdiContentSave}></ha-svg-icon>
</ha-fab>
</div>
<ha-automation-sidebar
class=${classMap({
sidebar: true,
hidden: !this._sidebarConfig,
overlay: !this.isWide,
})}
.isWide=${this.isWide}
.hass=${this.hass}
.config=${this._sidebarConfig}
@value-changed=${this._sidebarConfigChanged}
.disabled=${this.disabled}
></ha-automation-sidebar>
<div class="header">
<h2 id="sequence-heading" class="name">
${this.hass.localize("ui.panel.config.script.editor.sequence")}
</h2>
<a
href=${documentationUrl(this.hass, "/docs/scripts/")}
target="_blank"
rel="noreferrer"
>
<ha-icon-button
.path=${mdiHelpCircle}
.label=${this.hass.localize(
"ui.panel.config.script.editor.link_available_actions"
)}
></ha-icon-button>
</a>
</div>
<ha-automation-action
role="region"
aria-labelledby="sequence-heading"
.actions=${this.config.sequence || []}
.highlightedActions=${this._pastedConfig?.sequence || []}
.path=${["sequence"]}
@value-changed=${this._sequenceChanged}
.hass=${this.hass}
.narrow=${this.narrow}
.disabled=${this.disabled}
root
></ha-automation-action>
`;
}
@@ -422,116 +406,22 @@ export class HaManualScriptEditor extends LitElement {
});
}
private _openSidebar(ev: CustomEvent<SidebarConfig>) {
// deselect previous selected row
this._sidebarConfig?.close?.();
this._sidebarConfig = ev.detail;
}
private _sidebarConfigChanged(ev: CustomEvent<{ value: SidebarConfig }>) {
ev.stopPropagation();
if (!this._sidebarConfig) {
return;
}
this._sidebarConfig = {
...this._sidebarConfig,
...ev.detail.value,
};
}
private _closeSidebar() {
if (this._sidebarConfig) {
const closeRow = this._sidebarConfig?.close;
this._sidebarConfig = undefined;
closeRow?.();
}
}
private _handleCloseSidebar() {
this._sidebarConfig = undefined;
}
private _saveScript() {
this._closeSidebar();
fireEvent(this, "save-script");
}
static get styles(): CSSResultGroup {
return [
saveFabStyles,
haStyle,
css`
:host {
display: block;
}
.split-view {
display: flex;
flex-direction: row;
height: 100%;
position: relative;
gap: 16px;
}
.content-wrapper {
position: relative;
flex: 6;
}
.content {
padding: 32px 16px 64px 0;
height: calc(100vh - 153px);
height: calc(100dvh - 153px);
overflow-y: auto;
overflow-x: hidden;
}
.sidebar {
padding: 12px 0;
flex: 4;
height: calc(100vh - 81px);
height: calc(100dvh - 81px);
width: 40%;
}
.sidebar.hidden {
border-color: transparent;
border-width: 0;
ha-card {
overflow: hidden;
flex: 0;
visibility: hidden;
}
.sidebar.overlay {
position: fixed;
bottom: 0;
right: 0;
height: calc(100% - 64px);
padding: 0;
z-index: 5;
}
@media all and (max-width: 870px) {
.sidebar.overlay {
max-height: 70vh;
max-height: 70dvh;
height: auto;
width: 100%;
box-shadow: 0px -8px 16px rgba(0, 0, 0, 0.2);
}
}
@media all and (max-width: 870px) {
.sidebar.overlay.hidden {
height: 0;
}
}
.sidebar.overlay.hidden {
width: 0;
}
.description {
margin: 0;
}
p {
margin-bottom: 0;
}
.header {
display: flex;
align-items: center;

View File

@@ -9,6 +9,7 @@ import {
type TemplateResult,
} from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { ifDefined } from "lit/directives/if-defined";
import { styleMap } from "lit/directives/style-map";
import memoizeOne from "memoize-one";
@@ -84,8 +85,10 @@ export class HuiAreaCard extends LitElement implements LovelaceCard {
const displayType =
config.display_type || (config.show_camera ? "camera" : "picture");
const vertical = displayType === "compact" ? config.vertical : false;
this._config = {
...config,
vertical,
display_type: displayType,
};
@@ -109,7 +112,7 @@ export class HuiAreaCard extends LitElement implements LovelaceCard {
const featuresCount = this._config?.features?.length || 0;
return (
1 +
(displayType === "compact" ? 0 : 2) +
(displayType === "compact" ? (this._config?.vertical ? 1 : 0) : 2) +
(featuresPosition === "inline" ? 0 : featuresCount)
);
}
@@ -133,6 +136,11 @@ export class HuiAreaCard extends LitElement implements LovelaceCard {
const displayType = this._config?.display_type || "picture";
if (this._config?.vertical) {
rows++;
min_columns = 3;
}
if (displayType !== "compact") {
if (featurePosition === "inline" && featuresCount > 0) {
rows += 3;
@@ -397,9 +405,12 @@ export class HuiAreaCard extends LitElement implements LovelaceCard {
return sensorStates;
}
private _featurePosition = memoizeOne(
(config: AreaCardConfig) => config.features_position || "bottom"
);
private _featurePosition = memoizeOne((config: AreaCardConfig) => {
if (config.vertical) {
return "bottom";
}
return config.features_position || "bottom";
});
private _displayedFeatures = memoizeOne((config: AreaCardConfig) => {
const features = config.features || [];
@@ -439,6 +450,8 @@ export class HuiAreaCard extends LitElement implements LovelaceCard {
`;
}
const contentClasses = { vertical: Boolean(this._config.vertical) };
const icon = area.icon;
const name = this._config.name || computeAreaName(area);
@@ -518,7 +531,7 @@ export class HuiAreaCard extends LitElement implements LovelaceCard {
</div>
`}
<div class="container ${containerOrientationClass}">
<div class="content">
<div class="content ${classMap(contentClasses)}">
<ha-tile-icon>
${displayType === "compact"
? this._renderAlertSensorBadge()
@@ -656,6 +669,16 @@ export class HuiAreaCard extends LitElement implements LovelaceCard {
gap: 10px;
}
.vertical {
flex-direction: column;
text-align: center;
justify-content: center;
}
.vertical ha-tile-info {
width: 100%;
flex: none;
}
ha-tile-icon {
--tile-icon-color: var(--tile-color);
position: relative;

View File

@@ -251,8 +251,6 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
const entityId = this._config.entity;
const stateObj = entityId ? this.hass.states[entityId] : undefined;
const contentClasses = { vertical: Boolean(this._config.vertical) };
if (!stateObj) {
return html`
<hui-warning .hass=${this.hass}>
@@ -261,6 +259,8 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
`;
}
const contentClasses = { vertical: Boolean(this._config.vertical) };
const name = this._config.name || computeStateName(stateObj);
const active = stateActive(stateObj);
const color = this._computeStateColor(stateObj, this._config.color);

View File

@@ -104,12 +104,13 @@ export interface EntitiesCardConfig extends LovelaceCardConfig {
state_color?: boolean;
}
export type AreaCardDisplayType = "compact" | "icon" | "picture" | "camera";
export interface AreaCardConfig extends LovelaceCardConfig {
area?: string;
name?: string;
color?: string;
navigation_path?: string;
display_type?: "compact" | "icon" | "picture" | "camera";
display_type?: AreaCardDisplayType;
/** @deprecated Use `display_type` instead */
show_camera?: boolean;
camera_view?: HuiImage["cameraView"];

View File

@@ -36,7 +36,7 @@ import {
DEVICE_CLASSES,
type AreaCardFeatureContext,
} from "../../cards/hui-area-card";
import type { AreaCardConfig } from "../../cards/types";
import type { AreaCardConfig, AreaCardDisplayType } from "../../cards/types";
import type { LovelaceCardEditor } from "../../types";
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
import type { EditDetailElementEvent, EditSubElementEvent } from "../types";
@@ -52,6 +52,7 @@ const cardConfigStruct = assign(
navigation_path: optional(string()),
show_camera: optional(boolean()),
display_type: optional(enums(["compact", "icon", "picture", "camera"])),
vertical: optional(boolean()),
camera_view: optional(string()),
alert_classes: optional(array(string())),
sensor_classes: optional(array(string())),
@@ -78,7 +79,7 @@ export class HuiAreaCardEditor
private _schema = memoizeOne(
(
localize: LocalizeFunc,
showCamera: boolean,
displayType: AreaCardDisplayType,
binaryClasses: SelectOption[],
sensorClasses: SelectOption[]
) =>
@@ -113,7 +114,28 @@ export class HuiAreaCardEditor
},
},
},
...(showCamera
...(displayType === "compact"
? ([
{
name: "content_layout",
required: true,
selector: {
select: {
mode: "dropdown",
options: ["horizontal", "vertical"].map(
(value) => ({
label: localize(
`ui.panel.lovelace.editor.card.area.content_layout_options.${value}`
),
value,
})
),
},
},
},
] as const satisfies readonly HaFormSchema[])
: []),
...(displayType === "camera"
? ([
{
name: "camera_view",
@@ -282,7 +304,7 @@ export class HuiAreaCardEditor
}
private _featuresSchema = memoizeOne(
(localize: LocalizeFunc) =>
(localize: LocalizeFunc, vertical: boolean) =>
[
{
name: "features_position",
@@ -303,6 +325,7 @@ export class HuiAreaCardEditor
src_dark: `/static/images/form/tile_features_position_${value}_dark.svg`,
flip_rtl: true,
},
disabled: vertical && value === "inline",
})),
},
},
@@ -338,31 +361,35 @@ export class HuiAreaCardEditor
this._config.sensor_classes || DEVICE_CLASSES.sensor
);
const showCamera = this._config.display_type === "camera";
const displayType =
this._config.display_type || this._config.show_camera
? "camera"
: "picture";
this._config.display_type ||
(this._config.show_camera ? "camera" : "picture");
const schema = this._schema(
this.hass.localize,
showCamera,
displayType,
binarySelectOptions,
sensorSelectOptions
);
const featuresSchema = this._featuresSchema(this.hass.localize);
const vertical = this._config.vertical && displayType === "compact";
const featuresSchema = this._featuresSchema(this.hass.localize, vertical);
const data = {
camera_view: "auto",
alert_classes: DEVICE_CLASSES.binary_sensor,
sensor_classes: DEVICE_CLASSES.sensor,
features_position: "bottom",
display_type: displayType,
content_layout: vertical ? "vertical" : "horizontal",
...this._config,
};
// Default features position to bottom and force it to bottom in vertical mode
if (!data.features_position || vertical) {
data.features_position = "bottom";
}
const hasCompatibleFeatures = this._hasCompatibleFeatures(
this._featureContext
);
@@ -420,6 +447,12 @@ export class HuiAreaCardEditor
delete config.camera_view;
}
// Convert content_layout to vertical
if (config.content_layout) {
config.vertical = config.content_layout === "vertical";
delete config.content_layout;
}
fireEvent(this, "config-changed", { config });
}

View File

@@ -261,18 +261,17 @@ export class HuiTileCardEditor
this._config.hide_state ?? false
);
const featuresSchema = this._featuresSchema(
this.hass.localize,
this._config.vertical ?? false
);
const vertical = this._config.vertical ?? false;
const featuresSchema = this._featuresSchema(this.hass.localize, vertical);
const data = {
...this._config,
content_layout: this._config.vertical ? "vertical" : "horizontal",
content_layout: vertical ? "vertical" : "horizontal",
};
// Default features position to bottom and force it to bottom in vertical mode
if (!data.features_position || data.vertical) {
if (!data.features_position || vertical) {
data.features_position = "bottom";
}

View File

@@ -3,6 +3,7 @@ import { customElement } from "lit/decorators";
import { generateEntityFilter } from "../../../../common/entity/entity_filter";
import { clamp } from "../../../../common/number/clamp";
import { floorDefaultIcon } from "../../../../components/ha-floor-icon";
import type { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
import type { LovelaceSectionRawConfig } from "../../../../data/lovelace/config/section";
import type { LovelaceViewConfig } from "../../../../data/lovelace/config/view";
import type { HomeAssistant } from "../../../../types";
@@ -11,11 +12,11 @@ import {
getAreas,
getFloors,
} from "../areas/helpers/areas-strategy-helper";
import { getHomeStructure } from "./helpers/overview-home-structure";
import {
findEntities,
OVERVIEW_SUMMARIES_FILTERS,
} from "./helpers/overview-summaries";
import { getHomeStructure } from "./helpers/overview-home-structure";
export interface OverviewClimateViewStrategyConfig {
type: "overview-climate";
@@ -24,10 +25,9 @@ export interface OverviewClimateViewStrategyConfig {
const processAreasForClimate = (
areaIds: string[],
hass: HomeAssistant,
entities: string[],
computeTileCard: (entityId: string) => any
): any[] => {
const cards: any[] = [];
entities: string[]
): LovelaceCardConfig[] => {
const cards: LovelaceCardConfig[] = [];
for (const areaId of areaIds) {
const area = hass.areas[areaId];
@@ -38,6 +38,8 @@ const processAreasForClimate = (
});
const areaEntities = entities.filter(areaFilter);
const computeTileCard = computeAreaTileCardConfig(hass, "", true);
if (areaEntities.length > 0) {
cards.push({
heading_style: "subtitle",
@@ -81,8 +83,6 @@ export class OverviewClimateViewStrategy extends ReactiveElement {
const floorCount = home.floors.length + (home.areas.length ? 1 : 0);
const computeTileCard = computeAreaTileCardConfig(hass, "", true);
// Process floors
for (const floorStructure of home.floors) {
const floorId = floorStructure.id;
@@ -100,12 +100,7 @@ export class OverviewClimateViewStrategy extends ReactiveElement {
],
};
const areaCards = processAreasForClimate(
areaIds,
hass,
entities,
computeTileCard
);
const areaCards = processAreasForClimate(areaIds, hass, entities);
if (areaCards.length > 0) {
section.cards!.push(...areaCards);
@@ -126,12 +121,7 @@ export class OverviewClimateViewStrategy extends ReactiveElement {
],
};
const areaCards = processAreasForClimate(
home.areas,
hass,
entities,
computeTileCard
);
const areaCards = processAreasForClimate(home.areas, hass, entities);
if (areaCards.length > 0) {
section.cards!.push(...areaCards);

View File

@@ -1,22 +1,16 @@
import { ReactiveElement } from "lit";
import { customElement } from "lit/decorators";
import { clamp } from "../../../../common/number/clamp";
import { floorDefaultIcon } from "../../../../components/ha-floor-icon";
import type { AreaRegistryEntry } from "../../../../data/area_registry";
import type { LovelaceSectionConfig } from "../../../../data/lovelace/config/section";
import type { LovelaceViewConfig } from "../../../../data/lovelace/config/view";
import type { HomeAssistant } from "../../../../types";
import { getAreaControlEntities } from "../../card-features/hui-area-controls-card-feature";
import { AREA_CONTROLS, type AreaControl } from "../../card-features/types";
import type {
AreaCardConfig,
ButtonCardConfig,
HeadingCardConfig,
MarkdownCardConfig,
TileCardConfig,
} from "../../cards/types";
import { getAreas, getFloors } from "../areas/helpers/areas-strategy-helper";
import { getHomeStructure } from "./helpers/overview-home-structure";
import { getAreas } from "../areas/helpers/areas-strategy-helper";
import { OVERVIEW_SUMMARIES_ICONS } from "./helpers/overview-summaries";
export interface OverviewHomeViewStrategyConfig {
@@ -31,42 +25,22 @@ const computeAreaCard = (
const area = hass.areas[areaId] as AreaRegistryEntry | undefined;
const path = `areas-${areaId}`;
const controls: AreaControl[] = AREA_CONTROLS.filter(
(a) => a !== "switch" // Exclude switches control for areas as we don't know what the switches control
);
const controlEntities = getAreaControlEntities(controls, areaId, [], hass);
const filteredControls = controls.filter(
(control) => controlEntities[control].length > 0
);
const sensorClasses: string[] = [];
if (area?.temperature_entity_id) {
sensorClasses.push("temperature");
}
if (area?.humidity_entity_id) {
sensorClasses.push("humidity");
}
return {
type: "area",
area: areaId,
display_type: "compact",
sensor_classes: sensorClasses,
features: filteredControls.length
? [
{
type: "area-controls",
controls: filteredControls,
},
]
: [],
grid_options: {
rows: 1,
columns: 12,
},
features_position: "inline",
navigation_path: path,
vertical: true,
grid_options: {
rows: 2,
columns: 4,
},
};
};
@@ -76,58 +50,25 @@ export class OverviewHomeViewStrategy extends ReactiveElement {
config: OverviewHomeViewStrategyConfig,
hass: HomeAssistant
): Promise<LovelaceViewConfig> {
const floors = getFloors(hass.floors);
const areas = getAreas(hass.areas);
const home = getHomeStructure(floors, areas);
const floorCount = home.floors.length + (home.areas.length ? 1 : 0);
const floorsSections: LovelaceSectionConfig[] = home.floors.map(
(floorStructure) => {
const floorId = floorStructure.id;
const areaIds = floorStructure.areas;
const floor = hass.floors[floorId];
const headingCard: HeadingCardConfig = {
const areasSection: LovelaceSectionConfig = {
type: "grid",
column_span: 2,
cards: [
{
type: "heading",
heading_style: "title",
heading: floorCount > 1 ? floor.name : "Areas",
icon: floor.icon || floorDefaultIcon(floor),
};
const areasCards = areaIds.map<AreaCardConfig>((areaId) =>
computeAreaCard(areaId, hass)
);
return {
max_columns: 3,
type: "grid",
cards: [headingCard, ...areasCards],
};
}
);
if (home.areas.length > 0) {
floorsSections.push({
type: "grid",
max_columns: 3,
cards: [
{
type: "heading",
heading_style: "title",
icon: "mdi:home",
heading: floorCount > 1 ? "Other areas" : "Areas",
},
...home.areas.map<AreaCardConfig>((areaId) =>
computeAreaCard(areaId, hass)
),
],
} as LovelaceSectionConfig);
}
heading: "Areas",
},
...areas.map<AreaCardConfig>((area) =>
computeAreaCard(area.area_id, hass)
),
],
};
// Allow between 2 and 3 columns (the max should be set to define the width of the header)
const maxColumns = clamp(floorsSections.length, 2, 3);
const maxColumns = 2;
const favoriteSection: LovelaceSectionConfig = {
type: "grid",
@@ -143,7 +84,8 @@ export class OverviewHomeViewStrategy extends ReactiveElement {
favoriteSection.cards!.push(
{
type: "heading",
heading: "Quick actions",
headiing: "",
heading_style: "subtitle",
},
...favoriteEntities.map(
(entityId) =>
@@ -167,6 +109,7 @@ export class OverviewHomeViewStrategy extends ReactiveElement {
type: "button",
icon: OVERVIEW_SUMMARIES_ICONS.lights,
name: "Lights",
icon_height: "24px",
grid_options: {
rows: 2,
columns: 4,
@@ -180,6 +123,7 @@ export class OverviewHomeViewStrategy extends ReactiveElement {
type: "button",
icon: OVERVIEW_SUMMARIES_ICONS.climate,
name: "Climate",
icon_height: "30px",
grid_options: {
rows: 2,
columns: 4,
@@ -193,6 +137,7 @@ export class OverviewHomeViewStrategy extends ReactiveElement {
type: "button",
icon: OVERVIEW_SUMMARIES_ICONS.security,
name: "Security",
icon_height: "30px",
grid_options: {
rows: 2,
columns: 4,
@@ -206,6 +151,7 @@ export class OverviewHomeViewStrategy extends ReactiveElement {
type: "button",
icon: OVERVIEW_SUMMARIES_ICONS.media_players,
name: "Media Players",
icon_height: "30px",
grid_options: {
rows: 2,
columns: 4,
@@ -219,6 +165,7 @@ export class OverviewHomeViewStrategy extends ReactiveElement {
type: "button",
icon: "mdi:lightning-bolt",
name: "Energy",
icon_height: "30px",
grid_options: {
rows: 2,
columns: 4,
@@ -234,7 +181,7 @@ export class OverviewHomeViewStrategy extends ReactiveElement {
const sections = [
...(favoriteSection.cards ? [favoriteSection] : []),
summarySection,
...floorsSections,
areasSection,
];
return {
type: "sections",

View File

@@ -3,6 +3,7 @@ import { customElement } from "lit/decorators";
import { generateEntityFilter } from "../../../../common/entity/entity_filter";
import { clamp } from "../../../../common/number/clamp";
import { floorDefaultIcon } from "../../../../components/ha-floor-icon";
import type { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
import type { LovelaceSectionRawConfig } from "../../../../data/lovelace/config/section";
import type { LovelaceViewConfig } from "../../../../data/lovelace/config/view";
import type { HomeAssistant } from "../../../../types";
@@ -11,11 +12,11 @@ import {
getAreas,
getFloors,
} from "../areas/helpers/areas-strategy-helper";
import { getHomeStructure } from "./helpers/overview-home-structure";
import {
findEntities,
OVERVIEW_SUMMARIES_FILTERS,
} from "./helpers/overview-summaries";
import { getHomeStructure } from "./helpers/overview-home-structure";
export interface OverviewLightsViewStrategyConfig {
type: "overview-lights";
@@ -24,10 +25,9 @@ export interface OverviewLightsViewStrategyConfig {
const processAreasForLights = (
areaIds: string[],
hass: HomeAssistant,
entities: string[],
computeTileCard: (entityId: string) => any
): any[] => {
const cards: any[] = [];
entities: string[]
): LovelaceCardConfig[] => {
const cards: LovelaceCardConfig[] = [];
for (const areaId of areaIds) {
const area = hass.areas[areaId];
@@ -38,6 +38,8 @@ const processAreasForLights = (
});
const areaLights = entities.filter(areaFilter);
const computeTileCard = computeAreaTileCardConfig(hass, "", false);
if (areaLights.length > 0) {
cards.push({
heading_style: "subtitle",
@@ -81,8 +83,6 @@ export class OverviewLightsViewStrategy extends ReactiveElement {
const floorCount = home.floors.length + (home.areas.length ? 1 : 0);
const computeTileCard = computeAreaTileCardConfig(hass, "", true);
// Process floors
for (const floorStructure of home.floors) {
const floorId = floorStructure.id;
@@ -100,12 +100,7 @@ export class OverviewLightsViewStrategy extends ReactiveElement {
],
};
const areaCards = processAreasForLights(
areaIds,
hass,
entities,
computeTileCard
);
const areaCards = processAreasForLights(areaIds, hass, entities);
if (areaCards.length > 0) {
section.cards!.push(...areaCards);
@@ -126,12 +121,7 @@ export class OverviewLightsViewStrategy extends ReactiveElement {
],
};
const areaCards = processAreasForLights(
home.areas,
hass,
entities,
computeTileCard
);
const areaCards = processAreasForLights(home.areas, hass, entities);
if (areaCards.length > 0) {
section.cards!.push(...areaCards);

View File

@@ -3,16 +3,17 @@ import { customElement } from "lit/decorators";
import { generateEntityFilter } from "../../../../common/entity/entity_filter";
import { clamp } from "../../../../common/number/clamp";
import { floorDefaultIcon } from "../../../../components/ha-floor-icon";
import type { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
import type { LovelaceSectionRawConfig } from "../../../../data/lovelace/config/section";
import type { LovelaceViewConfig } from "../../../../data/lovelace/config/view";
import type { HomeAssistant } from "../../../../types";
import type { MediaControlCardConfig } from "../../cards/types";
import { getAreas, getFloors } from "../areas/helpers/areas-strategy-helper";
import { getHomeStructure } from "./helpers/overview-home-structure";
import {
findEntities,
OVERVIEW_SUMMARIES_FILTERS,
} from "./helpers/overview-summaries";
import { getHomeStructure } from "./helpers/overview-home-structure";
export interface OvervieMediaPlayersViewStrategyConfig {
type: "overview-media-players";
@@ -22,8 +23,8 @@ const processAreasForMediaPlayers = (
areaIds: string[],
hass: HomeAssistant,
entities: string[]
): any[] => {
const cards: any[] = [];
): LovelaceCardConfig[] => {
const cards: LovelaceCardConfig[] = [];
for (const areaId of areaIds) {
const area = hass.areas[areaId];

View File

@@ -16,6 +16,7 @@ import {
OVERVIEW_SUMMARIES_FILTERS,
} from "./helpers/overview-summaries";
import { getHomeStructure } from "./helpers/overview-home-structure";
import type { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
export interface OverviewSecurityViewStrategyConfig {
type: "overview-security";
@@ -24,10 +25,9 @@ export interface OverviewSecurityViewStrategyConfig {
const processAreasForSecurity = (
areaIds: string[],
hass: HomeAssistant,
entities: string[],
computeTileCard: (entityId: string) => any
): any[] => {
const cards: any[] = [];
entities: string[]
): LovelaceCardConfig[] => {
const cards: LovelaceCardConfig[] = [];
for (const areaId of areaIds) {
const area = hass.areas[areaId];
@@ -38,6 +38,8 @@ const processAreasForSecurity = (
});
const areaEntities = entities.filter(areaFilter);
const computeTileCard = computeAreaTileCardConfig(hass, "", true);
if (areaEntities.length > 0) {
cards.push({
heading_style: "subtitle",
@@ -81,8 +83,6 @@ export class OverviewSecurityViewStrategy extends ReactiveElement {
const floorCount = home.floors.length + (home.areas.length ? 1 : 0);
const computeTileCard = computeAreaTileCardConfig(hass, "", true);
// Process floors
for (const floorStructure of home.floors) {
const floorId = floorStructure.id;
@@ -100,12 +100,7 @@ export class OverviewSecurityViewStrategy extends ReactiveElement {
],
};
const areaCards = processAreasForSecurity(
areaIds,
hass,
entities,
computeTileCard
);
const areaCards = processAreasForSecurity(areaIds, hass, entities);
if (areaCards.length > 0) {
section.cards!.push(...areaCards);
@@ -126,12 +121,7 @@ export class OverviewSecurityViewStrategy extends ReactiveElement {
],
};
const areaCards = processAreasForSecurity(
home.areas,
hass,
entities,
computeTileCard
);
const areaCards = processAreasForSecurity(home.areas, hass, entities);
if (areaCards.length > 0) {
section.cards!.push(...areaCards);

View File

@@ -8,10 +8,6 @@ export const waMainStyles = css`
--wa-focus-ring-offset: 2px;
--wa-focus-ring: var(--wa-focus-ring-style) var(--wa-focus-ring-width)
var(--wa-focus-ring-color);
--wa-space-l: 24px;
--wa-shadow-l: 0 8px 8px -4px rgba(0, 0, 0, 0.2);
--wa-form-control-padding-block: 0.75em;
}
`;

View File

@@ -1,26 +0,0 @@
import type { PropertyValues } from "lit";
import type { HASSDomEvent } from "../common/dom/fire_event";
import {
showAutomationEditor,
type ShowAutomationEditorParams,
} from "../data/automation";
import type { Constructor } from "../types";
import type { HassBaseEl } from "./hass-base-mixin";
export default <T extends Constructor<HassBaseEl>>(superClass: T) =>
class extends superClass {
protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps);
this.addEventListener("show-automation-editor", (ev) =>
this._handleShowAutomationEditor(
ev as HASSDomEvent<ShowAutomationEditorParams>
)
);
}
private _handleShowAutomationEditor(
ev: HASSDomEvent<ShowAutomationEditorParams>
) {
showAutomationEditor(ev.detail.data, ev.detail.expanded);
}
};

View File

@@ -8,7 +8,6 @@ import { HassBaseEl } from "./hass-base-mixin";
import { loggingMixin } from "./logging-mixin";
import { contextMixin } from "./context-mixin";
import MoreInfoMixin from "./more-info-mixin";
import AutomationEditorMixin from "./automation-editor-mixin";
import ActionMixin from "./action-mixin";
import NotificationMixin from "./notification-mixin";
import { panelTitleMixin } from "./panel-title-mixin";
@@ -27,7 +26,6 @@ export class HassElement extends ext(HassBaseEl, [
TranslationsMixin,
StateDisplayMixin,
MoreInfoMixin,
AutomationEditorMixin,
ActionMixin,
SidebarMixin,
DisconnectToastMixin,

View File

@@ -473,8 +473,6 @@
},
"selector": {
"options": "Selector options",
"type": "Type",
"multiple": "Multiple",
"types": {
"action": "Action",
"area": "Area",
@@ -3782,6 +3780,7 @@
"load_error_not_editable": "Only automations in automations.yaml are editable.",
"load_error_not_deletable": "Only automations in automations.yaml can be deleted.",
"load_error_unknown": "Error loading automation ({err_no}).",
"save": "Save",
"unsaved_confirm_title": "Leave editor?",
"unsaved_confirm_text": "Unsaved changes will be lost.",
"alias": "Name",
@@ -4742,9 +4741,7 @@
"link_help_fields": "Learn more about fields.",
"add_fields": "Add fields",
"add_field": "Add field",
"field": "field",
"label": "Field",
"field_selector": "Field selector"
"field": "field"
},
"field_delete_confirm_title": "Delete field?",
"field_delete_confirm_text": "[%key:ui::panel::config::automation::editor::triggers::delete_confirm_text%]",
@@ -4767,6 +4764,7 @@
"load_error_unknown": "Error loading script ({err_no}).",
"delete_confirm_title": "Delete script?",
"delete_confirm_text": "{name} will be permanently deleted.",
"save_script": "Save script",
"sequence": "Sequence",
"sequence_sentence": "The sequence of actions of this script.",
"link_available_actions": "Learn more about available actions.",
@@ -7363,6 +7361,11 @@
"sensor_classes": "Sensor classes",
"description": "The Area card automatically displays entities of a specific area.",
"display_type": "Display type",
"content_layout": "[%key:ui::panel::lovelace::editor::card::tile::content_layout%]",
"content_layout_options": {
"horizontal": "[%key:ui::panel::lovelace::editor::card::tile::content_layout_options::horizontal%]",
"vertical": "[%key:ui::panel::lovelace::editor::card::tile::content_layout_options::vertical%]"
},
"display_type_options": {
"compact": "Compact",
"icon": "Area icon",

View File

@@ -159,25 +159,4 @@ describe("computeEntityEntryName", () => {
expect(computeEntityEntryName(entry as any, hass as any)).toBe("2");
vi.restoreAllMocks();
});
it("returns undefined when entity has device but no name or original_name", () => {
vi.spyOn(computeDeviceNameModule, "computeDeviceName").mockReturnValue(
"Kitchen Device"
);
const entry = {
entity_id: "sensor.kitchen_sensor",
// No name property
// No original_name property
device_id: "dev1",
};
const hass = {
devices: { dev1: {} },
states: {},
};
// Should return undefined to maintain function contract
expect(computeEntityEntryName(entry as any, hass as any)).toBeUndefined();
vi.restoreAllMocks();
});
});

View File

@@ -9549,7 +9549,7 @@ __metadata:
lodash.template: "npm:4.5.0"
luxon: "npm:3.7.1"
map-stream: "npm:0.0.7"
marked: "npm:16.2.0"
marked: "npm:16.1.2"
memoize-one: "npm:6.0.0"
node-vibrant: "npm:4.0.3"
object-hash: "npm:3.0.0"
@@ -11220,12 +11220,12 @@ __metadata:
languageName: node
linkType: hard
"marked@npm:16.2.0":
version: 16.2.0
resolution: "marked@npm:16.2.0"
"marked@npm:16.1.2":
version: 16.1.2
resolution: "marked@npm:16.1.2"
bin:
marked: bin/marked.js
checksum: 10/0a73dcfbe500514d2f1106da99708beed8a31de586e2826e1aa47ca0e0a4850b1e9598569b09d5366d4f4dee2d279a13f32616ed1ee75c832068eb7dd660f66f
checksum: 10/190d9b206f05d87a7acac3b50ab19505878297971a0c5652a9d4fa2b022407f22d2b79e1aa1e9f23a32c0158b1f5852ad33da2e83cc12100116a8fc0afc2b17e
languageName: node
linkType: hard