Compare commits

..

7 Commits

Author SHA1 Message Date
Paul Bottein
3046f3e47d 20260429.1 (#51817) 2026-04-30 20:33:33 +02:00
Paul Bottein
35601a0900 Bumped version to 20260429.1 2026-04-30 20:32:28 +02:00
Wendelin
e7016c15af Fix ha-select undefined value (#51800)
Fix ha-select undefined

Co-authored-by: Copilot <copilot@github.com>
2026-04-30 20:32:08 +02:00
Wendelin
624521e30b Hide tooltip on mobile clients in ha-sidebar component (#51799) 2026-04-30 20:32:08 +02:00
Bram Kragten
4876bfa639 Add tooltips for Jinja editors (#51792)
* Add descriptions to Jinja2 tags, filters, expressions, tests and variables

All standard Jinja2 tags, filters, and expression completions now carry
info and detail strings so the autocomplete info popover shows meaningful
documentation when users browse them — not just HA-specific functions.

* Add keyboard shortcut tip to the template developer tool

A ha-tip below the editor card now shows users that Ctrl+Space triggers
autocomplete, Ctrl+F opens the search panel, and F11 toggles fullscreen,
making the editor's built-in features more discoverable.

* Add hover tooltips for Jinja2 functions, filters and expressions

Hovering over a function, filter, tag, test, or variable name inside a
Jinja2 template shows a tooltip with its signature and description.
Non-tag completions also get a help-circle icon linking to the
corresponding Home Assistant template-functions documentation page.

The tooltip is rendered as a custom Lit element (ha-code-editor-jinja-hover)
that takes the Completion object and an optional docUrl as properties.

The tooltip source (haJinjaHoverSource) is wired into ha-code-editor
via CodeMirror's hoverTooltip extension. The documentationUrl() helper
is used so the link points to the correct subdomain (www / rc / next)
based on the running HA version.

* Add hover tooltips for Jinja2 hover + arg value tooltips for entity/device/area

Wire haJinjaHoverSource into ha-code-editor via CodeMirror hoverTooltip.
Two types of hover are now shown in jinja2/yaml mode:

- Hovering a function/filter/tag/expression name shows its signature,
  description, and a doc-link icon (non-tags only).
- Hovering a string-literal argument of a known HA Jinja function (e.g.
  states(), device_name(), area_entities()) shows the friendly name,
  current state, device, and area for entity_id arguments; the device
  name and area for device_id arguments; and the area name for area_id
  arguments. The same applies to states["entity_id"] subscripts.

The arg-value tooltip reuses CompletionItem / ha-code-editor-completion-items
(the same component used for autocomplete info popovers) via a new
ha-code-editor-jinja-arg-hover element. HA registry data is passed from
ha-code-editor via a HassArgHoverContext interface to keep jinja_ha_completions.ts
free of HomeAssistant type imports.

* only add tip for autocomplete

* review
2026-04-30 20:32:07 +02:00
AlCalzone
5dea0764b2 Expose Z-Wave exclusion instructions when removing device (#51788)
* Expose Z-Wave exclusion instructions when removing device

* text tweaks

* Apply suggestion from @MindFreeze

* Apply suggestions from code review

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>

* bring back comment

---------

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2026-04-30 20:32:05 +02:00
Paul Bottein
121ed7ac1f 20260429.0 (#51790) 2026-04-29 16:47:32 +02:00
22 changed files with 331 additions and 806 deletions

View File

@@ -33,7 +33,6 @@
"@codemirror/lang-jinja": "6.0.1",
"@codemirror/lang-yaml": "6.1.3",
"@codemirror/language": "6.12.3",
"@codemirror/lint": "6.9.5",
"@codemirror/search": "6.7.0",
"@codemirror/state": "6.6.0",
"@codemirror/view": "6.41.1",
@@ -54,7 +53,7 @@
"@fullcalendar/list": "6.1.20",
"@fullcalendar/luxon3": "6.1.20",
"@fullcalendar/timegrid": "6.1.20",
"@home-assistant/webawesome": "3.3.1-ha.2",
"@home-assistant/webawesome": "3.3.1-ha.1",
"@lezer/highlight": "1.2.3",
"@lit-labs/motion": "1.1.0",
"@lit-labs/observers": "2.1.0",
@@ -185,7 +184,7 @@
"gulp-rename": "2.1.0",
"html-minifier-terser": "7.2.0",
"husky": "9.1.7",
"jsdom": "29.1.0",
"jsdom": "29.0.2",
"jszip": "3.10.1",
"lint-staged": "16.4.0",
"lit-analyzer": "2.0.3",

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "home-assistant-frontend"
version = "20260429.0"
version = "20260429.1"
license = "Apache-2.0"
license-files = ["LICENSE*"]
description = "The Home Assistant frontend"

View File

@@ -95,8 +95,6 @@ export class HaCodeEditor extends ReactiveElement {
@property({ type: Boolean }) public error = false;
@property({ type: Boolean }) public lint = false;
@property({ type: Boolean, attribute: "disable-fullscreen" })
public disableFullscreen = false;
@@ -165,40 +163,6 @@ export class HaCodeEditor extends ReactiveElement {
return !!this.renderRoot.querySelector(`span.${className}`);
}
/**
* Push a YAML parse error (or null to clear) into the lint gutter as a
* diagnostic. Avoids re-parsing the document — the caller (ha-yaml-editor)
* already has the error from its own js-yaml load() call.
*/
public setYamlError(
err: {
mark?: { position: number; line: number; column: number };
reason?: string;
} | null
): void {
if (!this.codemirror || !this._loadedCodeMirror) return;
let diagnostics: {
from: number;
to: number;
severity: "error";
message: string;
}[] = [];
if (err) {
const doc = this.codemirror.state.doc;
const pos = err.mark ? Math.min(err.mark.position, doc.length) : 0;
const line = doc.lineAt(pos);
const message = `${
err.reason ||
this.hass?.localize("ui.components.yaml-editor.error") ||
"YAML syntax error"
}${err.mark ? ` (${this.hass?.localize("ui.components.yaml-editor.error_location", { line: err.mark.line + 1, column: err.mark.column + 1 })})` : ""}`;
diagnostics = [{ from: pos, to: line.to, severity: "error", message }];
}
this.codemirror.dispatch(
this._loadedCodeMirror.setDiagnostics(this.codemirror.state, diagnostics)
);
}
public connectedCallback() {
super.connectedCallback();
this.classList.toggle("in-dialog", this.inDialog);
@@ -256,37 +220,16 @@ export class HaCodeEditor extends ReactiveElement {
transactions.push({
effects: [
this._loadedCodeMirror!.langCompartment!.reconfigure(this._mode),
this._loadedCodeMirror!.yamlLintCompartment!.reconfigure(
this.lint && !this.readOnly
? [this._loadedCodeMirror!.lintGutter()]
: []
),
],
});
}
if (changedProps.has("readOnly")) {
transactions.push({
effects: [
this._loadedCodeMirror!.readonlyCompartment!.reconfigure(
this._loadedCodeMirror!.EditorView!.editable.of(!this.readOnly)
),
this._loadedCodeMirror!.yamlLintCompartment!.reconfigure(
this.lint && !this.readOnly
? [this._loadedCodeMirror!.lintGutter()]
: []
),
],
});
this._updateToolbarButtons();
}
if (changedProps.has("lint")) {
transactions.push({
effects: this._loadedCodeMirror!.yamlLintCompartment!.reconfigure(
this.lint && !this.readOnly
? [this._loadedCodeMirror!.lintGutter()]
: []
effects: this._loadedCodeMirror!.readonlyCompartment!.reconfigure(
this._loadedCodeMirror!.EditorView!.editable.of(!this.readOnly)
),
});
this._updateToolbarButtons();
}
if (changedProps.has("linewrap")) {
transactions.push({
@@ -369,7 +312,6 @@ export class HaCodeEditor extends ReactiveElement {
...this._loadedCodeMirror.searchKeymap,
...this._loadedCodeMirror.historyKeymap,
...this._loadedCodeMirror.tabKeyBindings,
...this._loadedCodeMirror.lintKeymap,
saveKeyBinding,
]),
this._loadedCodeMirror.search({ top: true }),
@@ -384,9 +326,6 @@ export class HaCodeEditor extends ReactiveElement {
this._loadedCodeMirror.linewrapCompartment.of(
this.linewrap ? this._loadedCodeMirror.EditorView.lineWrapping : []
),
this._loadedCodeMirror.yamlLintCompartment.of(
this.lint && !this.readOnly ? [this._loadedCodeMirror.lintGutter()] : []
),
this._loadedCodeMirror.EditorView.updateListener.of(this._onUpdate),
this._loadedCodeMirror.tooltips({
position: "absolute",

View File

@@ -53,10 +53,7 @@ export class HaIconButton extends LitElement {
.download=${this.download}
>
${this.path
? html`<ha-svg-icon
aria-hidden="true"
.path=${this.path}
></ha-svg-icon>`
? html`<ha-svg-icon .path=${this.path}></ha-svg-icon>`
: html`<span><slot></slot></span>`}
</ha-button>
`;

View File

@@ -8,6 +8,7 @@ import { copyToClipboard } from "../common/util/copy-clipboard";
import { haStyle } from "../resources/styles";
import type { HomeAssistant } from "../types";
import { showToast } from "../util/toast";
import "./ha-alert";
import "./ha-button";
import "./ha-code-editor";
import type { HaCodeEditor } from "./ha-code-editor";
@@ -57,8 +58,15 @@ export class HaYamlEditor extends LitElement {
@property({ attribute: "has-extra-actions", type: Boolean })
public hasExtraActions = false;
@property({ attribute: "show-errors", type: Boolean })
public showErrors = true;
@state() private _yaml = "";
@state() private _error = "";
@state() private _showingError = false;
@query("ha-code-editor") _codeEditor?: HaCodeEditor;
public setValue(value): void {
@@ -118,14 +126,16 @@ export class HaYamlEditor extends LitElement {
.disableFullscreen=${this.disableFullscreen}
.inDialog=${this.inDialog}
mode="yaml"
lint
autocomplete-entities
autocomplete-icons
.error=${this.isValid === false}
@value-changed=${this._onChange}
@editor-save=${this._onEditorSave}
@blur=${this._onBlur}
dir="ltr"
></ha-code-editor>
${this._showingError
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: nothing}
${this.copyClipboard || this.hasExtraActions
? html`
<div class="card-actions">
@@ -148,13 +158,9 @@ export class HaYamlEditor extends LitElement {
private _onChange(ev: CustomEvent): void {
ev.stopPropagation();
this._yaml = ev.detail.value;
let parsed: unknown;
let parsed;
let isValid = true;
let errorMsg: string | undefined;
let yamlError: {
mark?: { position: number; line: number; column: number };
message?: string;
} | null = null;
let errorMsg;
if (this._yaml) {
try {
@@ -162,13 +168,15 @@ export class HaYamlEditor extends LitElement {
} catch (err: any) {
// Invalid YAML
isValid = false;
yamlError = err;
errorMsg = `${this.hass.localize("ui.components.yaml-editor.error", { reason: err.reason })}${err.mark ? ` (${this.hass.localize("ui.components.yaml-editor.error_location", { line: err.mark.line + 1, column: err.mark.column + 1 })})` : ""}`;
}
} else {
parsed = {};
}
this._codeEditor?.setYamlError(yamlError);
this._error = errorMsg ?? "";
if (isValid) {
this._showingError = false;
}
this.value = parsed;
this.isValid = isValid;
@@ -180,23 +188,16 @@ export class HaYamlEditor extends LitElement {
} as any);
}
private _onBlur(): void {
if (this.showErrors && this._error) {
this._showingError = true;
}
}
get yaml() {
return this._yaml;
}
get codemirror() {
return this._codeEditor?.codemirror;
}
get hasComments(): boolean {
return this._codeEditor?.hasComments ?? false;
}
private _onEditorSave(ev: CustomEvent): void {
fireEvent(this, "editor-save");
ev.stopPropagation();
}
private async _copyYaml(): Promise<void> {
if (this.yaml) {
await copyToClipboard(this.yaml);

View File

@@ -1,6 +1,8 @@
import { consume, type ContextType } from "@lit/context";
import { mdiMagnify } from "@mdi/js";
import { css, html, type PropertyValues } from "lit";
import { customElement } from "lit/decorators";
import { customElement, state } from "lit/decorators";
import { internationalizationContext } from "../../data/context";
import { HaInput } from "./ha-input";
/**
@@ -15,6 +17,10 @@ import { HaInput } from "./ha-input";
*/
@customElement("ha-input-search")
export class HaInputSearch extends HaInput {
@state()
@consume({ context: internationalizationContext, subscribe: true })
private _i18n!: ContextType<typeof internationalizationContext>;
constructor() {
super();
this.withClear = true;
@@ -29,7 +35,7 @@ export class HaInputSearch extends HaInput {
!this.placeholder &&
(!this.hasUpdated || changedProps.has("_i18n"))
) {
this.placeholder = this.i18n?.localize?.("ui.common.search") || "Search";
this.placeholder = this._i18n.localize("ui.common.search");
}
}

View File

@@ -2,21 +2,19 @@ import "@home-assistant/webawesome/dist/components/animation/animation";
import "@home-assistant/webawesome/dist/components/input/input";
import type WaInput from "@home-assistant/webawesome/dist/components/input/input";
import { HasSlotController } from "@home-assistant/webawesome/dist/internal/slot";
import { consume, type ContextType } from "@lit/context";
import { mdiClose, mdiEye, mdiEyeOff } from "@mdi/js";
import {
css,
html,
LitElement,
nothing,
type PropertyValues,
type TemplateResult,
css,
html,
nothing,
} from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { customElement, property, query } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { ifDefined } from "lit/directives/if-defined";
import { stopPropagation } from "../../common/dom/stop_propagation";
import { internationalizationContext } from "../../data/context";
import "../ha-icon-button";
import "../ha-svg-icon";
import "../ha-tooltip";
@@ -127,10 +125,6 @@ export class HaInput extends WaInputMixin(LitElement) {
@query("wa-input")
private _input?: WaInput;
@state()
@consume({ context: internationalizationContext, subscribe: true })
protected i18n?: ContextType<typeof internationalizationContext>;
private readonly _hasSlotController = new HasSlotController(
this,
"label",
@@ -239,22 +233,19 @@ export class HaInput extends WaInputMixin(LitElement) {
${this.renderStartDefault()}
</slot>
<slot name="end" slot="end"> ${this.renderEndDefault()} </slot>
<slot name="clear-button" slot="clear-button">
<ha-icon-button
@click=${this._handleClearClick}
.label=${this.i18n?.localize?.("ui.components.input.clear") ||
"Clear"}
.path=${mdiClose}
></ha-icon-button>
<slot name="clear-icon" slot="clear-icon">
<ha-icon-button .path=${mdiClose}></ha-icon-button>
</slot>
<slot name="password-toggle-button" slot="password-toggle-button">
<slot name="show-password-icon" slot="show-password-icon">
<ha-icon-button
@keydown=${stopPropagation}
@click=${this._handlePasswordToggle}
.label=${this.i18n?.localize?.(
`ui.components.input.${this.passwordVisible ? "hide_password" : "show_password"}`
) || (this.passwordVisible ? "Hide password" : "Show password")}
.path=${this.passwordVisible ? mdiEyeOff : mdiEye}
.path=${mdiEye}
></ha-icon-button>
</slot>
<slot name="hide-password-icon" slot="hide-password-icon">
<ha-icon-button
@keydown=${stopPropagation}
.path=${mdiEyeOff}
></ha-icon-button>
</slot>
<div
@@ -302,14 +293,6 @@ export class HaInput extends WaInputMixin(LitElement) {
}
};
private _handleClearClick() {
this._input?.clear();
}
private _handlePasswordToggle() {
this.passwordVisible = !this.passwordVisible;
}
static styles = [
waInputStyles,
css`
@@ -431,12 +414,6 @@ export class HaInput extends WaInputMixin(LitElement) {
color: var(--ha-color-text-secondary);
}
ha-icon-button {
display: flex;
align-items: center;
color: var(--ha-color-text-secondary);
}
:host([appearance="outlined"]) wa-input.no-label {
--ha-icon-button-size: 24px;
--mdc-icon-size: 18px;

View File

@@ -1,110 +0,0 @@
import { ResizeController } from "@lit-labs/observers/resize-controller";
import type { LitElement, PropertyValues } from "lit";
import { state } from "lit/decorators";
import type { StyleInfo } from "lit/directives/style-map";
import type { Constructor } from "../types";
/**
* Public interface added by {@link MatchMinHeightMixin}.
*
* Declared separately so consumers can reference the mixin's contributed
* members in their own type annotations, per the Lit mixin authoring guide.
*/
export declare class MatchMinHeightMixinInterface {
/** Most recently observed height of `matchMinHeightTarget`, in pixels. */
protected _matchedMinHeight?: number;
/**
* `StyleInfo` exposing the matched height as a `min-height` declaration.
* Pass to `styleMap` to keep a layout at least as tall as the target
* element. Empty until a height has been observed.
*/
protected get _matchMinHeightStyle(): StyleInfo;
/**
* Element whose height should be matched as a `min-height` floor. Override
* with a getter that returns a `@query` result. Return `null` to pause
* observation (e.g. while the element is not rendered).
*/
protected get matchMinHeightTarget(): HTMLElement | null;
}
/**
* Mixin that observes a target element's height and exposes it as a
* `min-height` style. Useful for keeping a sibling layout (e.g. a YAML
* editor) at least as tall as another (e.g. a UI form) to avoid content
* shift when toggling between them.
*
* Subclasses override `matchMinHeightTarget` (typically returning a
* `@query`-decorated element) to specify which element to observe.
*/
export const MatchMinHeightMixin = <T extends Constructor<LitElement>>(
superClass: T
) => {
class MatchMinHeightMixinClass extends superClass {
@state() protected _matchedMinHeight?: number;
private _matchTarget: HTMLElement | null = null;
private _matchResize = new ResizeController(this, {
target: null,
callback: (entries) => {
const height = entries[0]?.contentRect.height;
if (height) {
this._matchedMinHeight = height;
}
},
});
private static readonly DEFAULT_MATCH_TARGET: HTMLElement | null = null;
protected get matchMinHeightTarget(): HTMLElement | null {
return MatchMinHeightMixinClass.DEFAULT_MATCH_TARGET;
}
protected get _matchMinHeightStyle(): StyleInfo {
return this._matchedMinHeight !== undefined
? { "min-height": `${this._matchedMinHeight}px` }
: {};
}
protected firstUpdated(changedProperties: PropertyValues<this>) {
super.firstUpdated?.(changedProperties);
this._attachMatchTarget();
}
protected updated(changedProperties: PropertyValues<this>) {
super.updated?.(changedProperties);
this._attachMatchTarget();
}
public disconnectedCallback() {
this._detachMatchTarget();
super.disconnectedCallback();
}
private _attachMatchTarget() {
const element = this.matchMinHeightTarget;
if (element === this._matchTarget) {
return;
}
this._detachMatchTarget();
if (!element) {
return;
}
this._matchTarget = element;
this._matchResize.observe(element);
}
private _detachMatchTarget() {
if (!this._matchTarget) {
return;
}
this._matchResize.unobserve?.(this._matchTarget);
this._matchTarget = null;
}
}
return MatchMinHeightMixinClass as unknown as Constructor<MatchMinHeightMixinInterface> &
T;
};

View File

@@ -546,6 +546,7 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
.readOnly=${this.readOnly}
@value-changed=${this._yamlChanged}
@editor-save=${this._handleSaveAutomation}
.showErrors=${false}
disable-fullscreen
></ha-yaml-editor>
<ha-button

View File

@@ -1,18 +1,14 @@
import { mdiDownload } from "@mdi/js";
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { extractSearchParam } from "../../../common/url/search-params";
import "../../../components/ha-analytics";
import "../../../components/ha-button";
import "../../../components/ha-card";
import "../../../components/ha-md-list";
import "../../../components/ha-md-list-item";
import "../../../components/ha-spinner";
import "../../../components/ha-svg-icon";
import "../../../components/ha-switch";
import { getSignedPath } from "../../../data/auth";
import type { HaSwitch } from "../../../components/ha-switch";
import type { Analytics } from "../../../data/analytics";
import {
@@ -30,7 +26,6 @@ import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
import { haStyle } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types";
import { documentationUrl } from "../../../util/documentation-url";
import { fileDownload } from "../../../util/file_download";
@customElement("ha-config-analytics")
class ConfigAnalytics extends SubscribeMixin(LitElement) {
@@ -124,18 +119,6 @@ class ConfigAnalytics extends SubscribeMixin(LitElement) {
</ha-md-list-item>
</ha-md-list>
</div>
<div class="card-actions">
<ha-button
size="small"
appearance="plain"
@click=${this._downloadDeviceInfo}
>
<ha-svg-icon slot="start" .path=${mdiDownload}></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.analytics.download_device_info"
)}
</ha-button>
</div>
</ha-card>`
: nothing}
${this._zwaveEntryId !== undefined
@@ -307,11 +290,6 @@ class ConfigAnalytics extends SubscribeMixin(LitElement) {
this._save();
}
private async _downloadDeviceInfo(): Promise<void> {
const signedPath = await getSignedPath(this.hass, "/api/analytics/devices");
fileDownload(signedPath.path);
}
static get styles(): CSSResultGroup {
return [
haStyle,

View File

@@ -1,9 +1,17 @@
import { mdiDotsVertical, mdiDownload } from "@mdi/js";
import type { TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import "../../../components/ha-dropdown";
import "../../../components/ha-dropdown-item";
import "../../../components/ha-icon-button";
import "../../../components/ha-svg-icon";
import { getSignedPath } from "../../../data/auth";
import "../../../layouts/hass-subpage";
import type { HomeAssistant, Route } from "../../../types";
import { fileDownload } from "../../../util/file_download";
import "./ha-config-analytics";
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
@customElement("ha-config-section-analytics")
class HaConfigSectionAnalytics extends LitElement {
@@ -21,6 +29,19 @@ class HaConfigSectionAnalytics extends LitElement {
.narrow=${this.narrow}
.header=${this.hass.localize("ui.panel.config.analytics.caption")}
>
<ha-dropdown
@wa-select=${this._handleOverflowAction}
slot="toolbar-icon"
>
<ha-icon-button slot="trigger" .path=${mdiDotsVertical}>
</ha-icon-button>
<ha-dropdown-item .value=${"download_device_info"}>
<ha-svg-icon slot="icon" .path=${mdiDownload}></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.analytics.download_device_info"
)}
</ha-dropdown-item>
</ha-dropdown>
<div class="content">
<ha-config-analytics .hass=${this.hass}></ha-config-analytics>
</div>
@@ -28,6 +49,18 @@ class HaConfigSectionAnalytics extends LitElement {
`;
}
private async _handleOverflowAction(
ev: HaDropdownSelectEvent
): Promise<void> {
if (ev.detail.item.value === "download_device_info") {
const signedPath = await getSignedPath(
this.hass,
"/api/analytics/devices"
);
fileDownload(signedPath.path);
}
}
static styles = css`
.content {
padding: 28px 20px 0;

View File

@@ -5,11 +5,9 @@ import { dump, JSON_SCHEMA, load } from "js-yaml";
import type { CSSResultGroup, TemplateResult, PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import { until } from "lit/directives/until";
import memoizeOne from "memoize-one";
import { storage } from "../../../../common/decorators/storage";
import type { HASSDomEvent } from "../../../../common/dom/fire_event";
import { computeDomain } from "../../../../common/entity/compute_domain";
import { computeObjectId } from "../../../../common/entity/compute_object_id";
import {
@@ -25,7 +23,6 @@ import { showToast } from "../../../../util/toast";
import "../../../../components/entity/ha-entity-picker";
import "../../../../components/ha-alert";
import "../../../../components/ha-button";
import "../../../../components/ha-button-toggle-group";
import "../../../../components/ha-card";
import "../../../../components/buttons/ha-progress-button";
import "../../../../components/ha-expansion-panel";
@@ -42,14 +39,12 @@ import {
serviceCallWillDisconnect,
} from "../../../../data/service";
import { haStyle } from "../../../../resources/styles";
import type { HomeAssistant, ToggleButton } from "../../../../types";
import type { HomeAssistant } from "../../../../types";
import { documentationUrl } from "../../../../util/documentation-url";
import { resolveMediaSource } from "../../../../data/media_source";
import { MatchMinHeightMixin } from "../../../../mixins/match-min-height-mixin";
import { withViewTransition } from "../../../../common/util/view-transition";
@customElement("developer-tools-action")
class HaPanelDevAction extends MatchMinHeightMixin(LitElement) {
class HaPanelDevAction extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean }) public narrow = false;
@@ -85,12 +80,6 @@ class HaPanelDevAction extends MatchMinHeightMixin(LitElement) {
@query("#yaml-editor") private _yamlEditor?: HaYamlEditor;
@query(".ui-mode-content") private _uiModeContent?: HTMLElement;
protected get matchMinHeightTarget(): HTMLElement | null {
return this._yamlMode ? null : (this._uiModeContent ?? null);
}
protected willUpdate() {
if (
!this.hasUpdated &&
@@ -128,21 +117,6 @@ class HaPanelDevAction extends MatchMinHeightMixin(LitElement) {
this._serviceData?.action
);
const modeButtons: ToggleButton[] = [
{
label: this.hass.localize(
"ui.panel.config.developer-tools.tabs.actions.ui_mode"
),
value: "ui",
},
{
label: this.hass.localize(
"ui.panel.config.developer-tools.tabs.actions.yaml_mode"
),
value: "yaml",
},
];
const domain = this._serviceData?.action
? computeDomain(this._serviceData?.action)
: undefined;
@@ -158,34 +132,14 @@ class HaPanelDevAction extends MatchMinHeightMixin(LitElement) {
return html`
<div class="content">
<p>
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.actions.description"
)}
</p>
<ha-card>
<div class="card-header">
<div class="header-row">
<div class="header-title">
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.actions.title"
)}
</div>
<ha-button-toggle-group
size="small"
class="yaml-mode-toggle"
.buttons=${modeButtons}
.active=${this._yamlMode ? "yaml" : "ui"}
.disabled=${!this._uiAvailable}
@value-changed=${this._modeChanged}
></ha-button-toggle-group>
</div>
<p class="secondary">
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.actions.description"
)}
</p>
</div>
${this._yamlMode
? html`<div
class="card-content"
style=${styleMap(this._matchMinHeightStyle)}
>
? html`<div class="card-content">
<ha-service-picker
.hass=${this.hass}
.value=${this._serviceData?.action}
@@ -207,27 +161,44 @@ class HaPanelDevAction extends MatchMinHeightMixin(LitElement) {
show-advanced
show-service-id
@value-changed=${this._serviceDataChanged}
class="card-content ui-mode-content"
class="card-content"
></ha-service-control>
`}
${this._error !== undefined
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: nothing}
<div class="card-actions">
</ha-card>
</div>
<div class="button-row">
<div class="buttons">
<div class="switch-mode-container">
<ha-button
appearance="plain"
@click=${this._toggleYaml}
.disabled=${!this._uiAvailable}
>
${this._yamlMode
? this.hass.localize(
"ui.panel.config.developer-tools.tabs.actions.ui_mode"
)
: this.hass.localize(
"ui.panel.config.developer-tools.tabs.actions.yaml_mode"
)}
</ha-button>
${!this._uiAvailable
? html`<span class="error"
>${this.hass.localize(
"ui.panel.config.developer-tools.tabs.actions.no_template_ui_support"
)}</span
>`
: nothing}
<ha-progress-button raised @click=${this._callService}>
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.actions.call_service"
)}
</ha-progress-button>
: ""}
</div>
</ha-card>
<ha-progress-button raised @click=${this._callService}>
${this.hass.localize(
"ui.panel.config.developer-tools.tabs.actions.call_service"
)}
</ha-progress-button>
</div>
</div>
${this._response?.result
? html`<div class="content response">
@@ -468,7 +439,7 @@ class HaPanelDevAction extends MatchMinHeightMixin(LitElement) {
}
);
private async _callService(ev: Event) {
private async _callService(ev) {
const button = ev.currentTarget as HaProgressButton;
if (this._yamlMode && !this._yamlValid) {
@@ -589,20 +560,13 @@ class HaPanelDevAction extends MatchMinHeightMixin(LitElement) {
button.actionSuccess();
}
private _modeChanged(ev: HASSDomEvent<{ value: string }>) {
ev.stopPropagation();
const yamlMode = ev.detail.value === "yaml";
if (yamlMode === this._yamlMode) {
return;
}
withViewTransition(() => {
this._yamlMode = yamlMode;
this._yamlValid = true;
this._error = undefined;
});
private _toggleYaml() {
this._yamlMode = !this._yamlMode;
this._yamlValid = true;
this._error = undefined;
}
private _yamlChanged(ev: HASSDomEvent<{ value: any; isValid: boolean }>) {
private _yamlChanged(ev) {
if (!ev.detail.isValid) {
this._yamlValid = false;
return;
@@ -638,7 +602,7 @@ class HaPanelDevAction extends MatchMinHeightMixin(LitElement) {
}
}
private _serviceDataChanged(ev: HASSDomEvent<{ value: any }>) {
private _serviceDataChanged(ev) {
if (this._serviceData?.action !== ev.detail.value.action) {
this._error = undefined;
}
@@ -646,7 +610,7 @@ class HaPanelDevAction extends MatchMinHeightMixin(LitElement) {
this._checkUiSupported();
}
private _serviceChanged(ev: HASSDomEvent<{ value: any }>) {
private _serviceChanged(ev) {
ev.stopPropagation();
if (ev.detail.value) {
this._serviceData = { action: ev.detail.value, data: {} };
@@ -703,55 +667,30 @@ class HaPanelDevAction extends MatchMinHeightMixin(LitElement) {
max-width: 1200px;
margin: auto;
}
.card-header {
display: flex;
flex-direction: column;
gap: var(--ha-space-1);
.button-row {
padding: var(--ha-space-2) var(--ha-space-4);
border-top: 1px solid var(--divider-color);
border-bottom: 1px solid var(--divider-color);
background: var(--card-background-color);
position: sticky;
bottom: 0;
box-sizing: border-box;
width: 100%;
}
.header-row {
.button-row .buttons {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--ha-space-2);
max-width: 1200px;
margin: auto;
}
.header-title {
flex: 1;
min-width: 0;
}
.secondary {
margin: 0;
font-size: var(--ha-font-size-m);
font-weight: var(--ha-font-weight-normal);
line-height: normal;
letter-spacing: normal;
color: var(--secondary-text-color);
}
.card-content {
.switch-mode-container {
display: flex;
align-items: stretch;
justify-content: flex-start;
flex-direction: column;
gap: var(--ha-space-4);
margin: var(--ha-space-2);
--service-control-padding: 0;
}
.card-content ha-yaml-editor {
flex: 1;
display: flex;
flex-direction: column;
}
.yaml-mode-toggle {
flex-shrink: 0;
}
.card-actions {
display: flex;
justify-content: flex-end;
align-items: center;
gap: var(--ha-space-2);
}
.card-actions .error {
flex: 1;
color: var(--error-color);
.switch-mode-container .error {
margin-left: var(--ha-space-2);
margin-inline-start: var(--ha-space-2);
margin-inline-end: initial;
}
.attributes {
width: 100%;

View File

@@ -18,7 +18,7 @@ import "./events-list";
class HaPanelDevEvent extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean, reflect: true }) public narrow = false;
@property({ type: Boolean }) public narrow = false;
@state() private _eventType = "";
@@ -94,7 +94,6 @@ class HaPanelDevEvent extends LitElement {
<event-subscribe-card
.hass=${this.hass}
.narrow=${this.narrow}
.selectedEventType=${this._selectedEventType}
></event-subscribe-card>
</div>
@@ -159,8 +158,6 @@ class HaPanelDevEvent extends LitElement {
padding: var(--ha-space-4);
max-width: 1200px;
margin: auto;
height: 100%;
box-sizing: border-box;
}
:host {
@@ -168,26 +165,10 @@ class HaPanelDevEvent extends LitElement {
-webkit-user-select: initial;
-moz-user-select: initial;
display: block;
height: 100%;
}
:host([narrow]) {
height: auto;
}
:host([narrow]) .content {
height: auto;
}
.flex {
min-width: 0;
min-height: 0;
display: flex;
flex-direction: column;
}
:host([narrow]) .flex {
min-height: auto;
}
.inputs {
@@ -199,19 +180,11 @@ class HaPanelDevEvent extends LitElement {
}
event-subscribe-card {
display: flex;
flex-direction: column;
min-height: 0;
flex: 1;
display: block;
margin-top: var(--ha-space-4);
direction: var(--direction);
}
:host([narrow]) event-subscribe-card {
flex: none;
min-height: auto;
}
a {
color: var(--primary-color);
}

View File

@@ -1,39 +1,21 @@
import {
mdiChevronDoubleLeft,
mdiChevronDoubleRight,
mdiChevronLeft,
mdiChevronRight,
mdiInformationOutline,
} from "@mdi/js";
import type { HassEvent } from "home-assistant-js-websocket";
import type { TemplateResult, PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import { formatTimeWithSeconds } from "../../../../common/datetime/format_time";
import { repeat } from "lit/directives/repeat";
import { formatTime } from "../../../../common/datetime/format_time";
import "../../../../components/ha-alert";
import "../../../../components/ha-button";
import "../../../../components/ha-card";
import "../../../../components/ha-icon-button";
import "../../../../components/ha-svg-icon";
import "../../../../components/ha-tooltip";
import "../../../../components/ha-yaml-editor";
import "../../../../components/input/ha-input";
import type { HaInput } from "../../../../components/input/ha-input";
import type { HomeAssistant } from "../../../../types";
const MAX_BUFFERED_EVENTS = 100;
interface SubscribedEvent {
id: number;
event: HassEvent;
}
@customElement("event-subscribe-card")
class EventSubscribeCard extends LitElement {
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ type: Boolean, reflect: true }) public narrow = false;
@property({ attribute: false }) public selectedEventType = "";
@state() private _eventType = "";
@@ -42,12 +24,13 @@ class EventSubscribeCard extends LitElement {
@state() private _eventFilter = "";
@state() private _events: SubscribedEvent[] = [];
@state() private _events: {
id: number;
event: HassEvent;
}[] = [];
@state() private _error?: string;
@state() private _viewedEventId?: number;
private _eventCount = 0;
@state() _ignoredEventsCount = 0;
@@ -130,161 +113,43 @@ class EventSubscribeCard extends LitElement {
</ha-button>
</div>
</ha-card>
${this._renderEventsCard()}
`;
}
private _renderEventsCard(): TemplateResult {
if (!this._events.length) {
const message = this._subscribed
? this.hass!.localize(
"ui.panel.config.developer-tools.tabs.events.waiting_for_events"
)
: this.hass!.localize(
"ui.panel.config.developer-tools.tabs.events.subscribe_prompt"
);
return html`
<ha-card class="events-card">
<div class="empty-state">${message}</div>
</ha-card>
`;
}
const bufferTotal = this._events.length;
const index = this._resolveViewedIndex();
const event = this._events[index];
const position = event.id + 1;
const bufferPosition = bufferTotal - index;
const atNewest = index === 0;
const hasRolledOver = this._events[bufferTotal - 1].id > 0;
return html`
<ha-card class="events-card">
<div class="events-toolbar">
<ha-icon-button
.path=${mdiChevronDoubleLeft}
.disabled=${index >= bufferTotal - 1}
.label=${this.hass!.localize(
"ui.panel.config.developer-tools.tabs.events.oldest_event"
<ha-card>
<div class="card-content">
<div class="events">
${repeat(
this._events,
(event) => event.id,
(event) => html`
<div class="event">
${this.hass!.localize(
"ui.panel.config.developer-tools.tabs.events.event_fired",
{ name: event.id }
)}
${formatTime(
new Date(event.event.time_fired),
this.hass!.locale,
this.hass!.config
)}:
<ha-yaml-editor
.hass=${this.hass}
.defaultValue=${event.event}
read-only
></ha-yaml-editor>
</div>
`
)}
@click=${this._showOldest}
></ha-icon-button>
<ha-icon-button
.path=${mdiChevronLeft}
.disabled=${index >= bufferTotal - 1}
.label=${this.hass!.localize(
"ui.panel.config.developer-tools.tabs.events.older_event"
)}
@click=${this._showOlder}
></ha-icon-button>
<div class="event-info">
${this.hass!.localize(
"ui.panel.config.developer-tools.tabs.events.event_fired",
{
name: position,
time: formatTimeWithSeconds(
new Date(event.event.time_fired),
this.hass!.locale,
this.hass!.config
),
}
)}
<span class="counter">(${bufferPosition} / ${bufferTotal})</span>
${hasRolledOver
? html`
<ha-svg-icon
id="buffer-info"
class="buffer-info"
.path=${mdiInformationOutline}
></ha-svg-icon>
<ha-tooltip for="buffer-info" placement="bottom">
<span class="buffer-tooltip">
${this.hass!.localize(
"ui.panel.config.developer-tools.tabs.events.buffer_disclaimer",
{ count: MAX_BUFFERED_EVENTS }
)}
</span>
</ha-tooltip>
`
: nothing}
</div>
<ha-icon-button
.path=${mdiChevronRight}
.disabled=${atNewest}
.label=${this.hass!.localize(
"ui.panel.config.developer-tools.tabs.events.newer_event"
)}
@click=${this._showNewer}
></ha-icon-button>
<ha-icon-button
.path=${mdiChevronDoubleRight}
.disabled=${atNewest}
.label=${this.hass!.localize(
"ui.panel.config.developer-tools.tabs.events.newest_event"
)}
@click=${this._showNewest}
></ha-icon-button>
</div>
<ha-yaml-editor
.hass=${this.hass}
.value=${event.event}
auto-update
read-only
></ha-yaml-editor>
</ha-card>
`;
}
private _resolveViewedIndex(): number {
if (this._viewedEventId === undefined) {
return 0;
}
const found = this._events.findIndex((e) => e.id === this._viewedEventId);
// Fall back to the oldest available event when the viewed one has aged out.
return found === -1 ? this._events.length - 1 : found;
}
private _showOldest() {
if (!this._events.length) {
return;
}
this._viewedEventId = this._events[this._events.length - 1].id;
}
private _showOlder() {
if (!this._events.length) {
return;
}
const next = Math.min(
this._resolveViewedIndex() + 1,
this._events.length - 1
);
this._viewedEventId = this._events[next].id;
}
private _showNewest() {
if (!this._events.length) {
return;
}
this._viewedEventId = this._events[0].id;
}
private _showNewer() {
if (!this._events.length) {
return;
}
const next = Math.max(this._resolveViewedIndex() - 1, 0);
this._viewedEventId = this._events[next].id;
}
private _valueChanged(ev: InputEvent) {
private _valueChanged(ev: InputEvent): void {
this._eventType = (ev.target as HaInput).value ?? "";
this._error = undefined;
}
private _filterChanged(ev: InputEvent) {
private _filterChanged(ev: InputEvent): void {
this._eventFilter = (ev.target as HaInput).value ?? "";
}
@@ -295,7 +160,7 @@ class EventSubscribeCard extends LitElement {
const searchStr = this._eventFilter;
function visit(node: unknown) {
function visit(node) {
// Handle primitives directly
if (node === null || typeof node !== "object") {
return String(node).includes(searchStr);
@@ -338,116 +203,55 @@ class EventSubscribeCard extends LitElement {
return;
}
const tail =
this._events.length >= MAX_BUFFERED_EVENTS
? this._events.slice(0, MAX_BUFFERED_EVENTS - 1)
this._events.length > 30
? this._events.slice(0, 29)
: this._events;
const id = this._eventCount++;
this._events = [
{
event,
id,
id: this._eventCount++,
},
...tail,
];
if (this._viewedEventId === undefined) {
this._viewedEventId = id;
}
}, this._eventType);
} catch (error) {
} catch (error: any) {
this._error = this.hass!.localize(
"ui.panel.config.developer-tools.tabs.events.subscribe_failed",
{
error:
error instanceof Error
? error.message
: this.hass!.localize(
"ui.panel.config.developer-tools.tabs.events.unknown_error"
),
}
{ error: error.message || "Unknown error" }
);
}
}
}
private _clearEvents() {
private _clearEvents(): void {
this._events = [];
this._eventCount = 0;
this._ignoredEventsCount = 0;
this._error = undefined;
this._viewedEventId = undefined;
}
static styles = css`
:host {
display: flex;
flex-direction: column;
min-height: 0;
}
ha-input {
margin-bottom: var(--ha-space-2);
}
.error-message {
margin-top: var(--ha-space-2);
}
.event {
border-top: 1px solid var(--divider-color);
padding-top: var(--ha-space-2);
padding-bottom: var(--ha-space-2);
margin: var(--ha-space-4) 0;
}
.event:last-child {
border-bottom: 0;
margin-bottom: 0;
}
pre {
font-family: var(--ha-font-family-code);
}
ha-card {
margin-bottom: var(--ha-space-2);
}
.events-card {
display: flex;
flex-direction: column;
height: 620px;
padding: var(--ha-space-2);
}
:host([narrow]) .events-card {
height: auto;
min-height: 360px;
}
.events-toolbar {
display: flex;
align-items: center;
gap: var(--ha-space-2);
}
.empty-state {
display: flex;
flex: 1;
align-items: center;
justify-content: center;
padding: var(--ha-space-8);
color: var(--primary-text-color);
text-align: center;
font-size: var(--ha-font-size-xl);
line-height: var(--ha-line-height-normal);
}
.event-info {
flex: 1;
text-align: center;
font-size: var(--ha-font-size-m);
}
.counter {
color: var(--secondary-text-color);
margin-left: var(--ha-space-2);
}
.buffer-info {
color: var(--secondary-text-color);
margin-left: var(--ha-space-1);
vertical-align: middle;
--mdc-icon-size: 16px;
}
.buffer-tooltip {
white-space: pre-line;
display: block;
max-width: 320px;
}
ha-yaml-editor {
display: flex;
flex-direction: column;
min-height: 0;
flex: 1;
margin-top: var(--ha-space-2);
--code-mirror-height: 100%;
margin-bottom: var(--ha-space-1);
}
`;
}

View File

@@ -329,6 +329,7 @@ export class HaSceneEditor extends PreventUnsavedMixin(
.defaultValue=${this._config}
@value-changed=${this._yamlChanged}
@editor-save=${this._saveScene}
.showErrors=${false}
disable-fullscreen
></ha-yaml-editor>`;
}

View File

@@ -464,6 +464,7 @@ export class HaScriptEditor extends SubscribeMixin(
disable-fullscreen
@value-changed=${this._yamlChanged}
@editor-save=${this._handleSaveScript}
.showErrors=${false}
></ha-yaml-editor>
<ha-button
slot="fab"

View File

@@ -5,6 +5,7 @@ import { cache } from "lit/directives/cache";
import type { HASSDomEvent } from "../../../common/dom/fire_event";
import { fireEvent } from "../../../common/dom/fire_event";
import { handleStructError } from "../../../common/structs/handle-errors";
import { debounce } from "../../../common/util/debounce";
import { deepEqual } from "../../../common/util/deep-equal";
import "../../../components/ha-alert";
import "../../../components/ha-spinner";
@@ -70,6 +71,11 @@ export abstract class HuiElementEditor<
// Error: Configuration broken - do not save
@state() private _errors?: string[];
// Error from unparseable YAML, but don't show it immediately to prevent showing immediately on every keystroke
@state() private _pendingYamlError?: string;
@state() private _yamlError = false;
// Warning: GUI editor can't handle configuration - ok to save
@state() private _warnings?: string[];
@@ -77,8 +83,6 @@ export abstract class HuiElementEditor<
@state() private _loading = false;
@state() private _yamlError = false;
@query("ha-yaml-editor") _yamlEditor?: HaYamlEditor;
private _loadCount = 0;
@@ -101,7 +105,7 @@ export abstract class HuiElementEditor<
}
private _setConfig(): void {
if (!this._errors && !this._yamlError) {
if (!this._errors) {
try {
this._updateConfigElement();
} catch (err: any) {
@@ -127,9 +131,7 @@ export abstract class HuiElementEditor<
}
public get hasError(): boolean {
return (
this._yamlError || (this._errors !== undefined && this._errors.length > 0)
);
return this._errors !== undefined && this._errors.length > 0;
}
public get GUImode(): boolean {
@@ -249,8 +251,10 @@ export abstract class HuiElementEditor<
.hass=${this.hass}
.inDialog=${this.inDialog}
@value-changed=${this._handleYAMLChanged}
@blur=${this._onBlurYaml}
@keydown=${this._ignoreKeydown}
dir="ltr"
.showErrors=${false}
></ha-yaml-editor>
</div>
`}
@@ -270,7 +274,7 @@ export abstract class HuiElementEditor<
</ha-alert>
`
: nothing}
${this._errors?.length
${this.hasError
? html`
<ha-alert
alert-type="error"
@@ -279,7 +283,7 @@ export abstract class HuiElementEditor<
)}
>
<ul>
${this._errors.map((error) => html`<li>${error}</li>`)}
${this._errors!.map((error) => html`<li>${error}</li>`)}
</ul>
</ha-alert>
`
@@ -335,14 +339,40 @@ export abstract class HuiElementEditor<
private _handleYAMLChanged(ev: CustomEvent) {
ev.stopPropagation();
const config = ev.detail.value;
if (ev.detail.isValid) {
this._config = ev.detail.value;
this._config = config;
this._errors = undefined;
this._pendingYamlError = undefined;
this._yamlError = false;
this._debounceYamlError.cancel();
this._setConfig();
} else if (this._yamlError) {
// If we're already showing a yaml error, don't bother to debounce, just update immediately.
this._errors = [ev.detail.errorMsg];
} else {
this._yamlError = true;
this._pendingYamlError = ev.detail.errorMsg;
this._debounceYamlError();
}
}
private _debounceYamlError = debounce(() => {
if (this._pendingYamlError) {
this._yamlError = true;
this._errors = [this._pendingYamlError];
this._pendingYamlError = undefined;
this._setConfig();
}
}, 2000);
private _onBlurYaml() {
this._debounceYamlError.cancel();
if (this._pendingYamlError) {
this._yamlError = true;
this._errors = [this._pendingYamlError];
this._pendingYamlError = undefined;
this._setConfig();
}
this._setConfig();
}
protected async unloadConfigElement(): Promise<void> {

View File

@@ -1,5 +1,6 @@
import { undoDepth } from "@codemirror/commands";
import { mdiClose } from "@mdi/js";
import { dump, load } from "js-yaml";
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
@@ -7,8 +8,8 @@ import { classMap } from "lit/directives/class-map";
import { array, assert, object, optional, string, type } from "superstruct";
import { deepEqual } from "../../common/util/deep-equal";
import "../../components/ha-button";
import "../../components/ha-yaml-editor";
import type { HaYamlEditor } from "../../components/ha-yaml-editor";
import "../../components/ha-code-editor";
import type { HaCodeEditor } from "../../components/ha-code-editor";
import "../../components/ha-icon-button";
import "../../components/ha-top-app-bar-fixed";
import type { LovelaceRawConfig } from "../../data/lovelace/config/types";
@@ -46,10 +47,6 @@ class LovelaceFullConfigEditor extends LitElement {
@state() private _changed?: boolean;
private _config?: LovelaceRawConfig;
private _yamlError?: string;
protected render(): TemplateResult | undefined {
return html`
<ha-top-app-bar-fixed .narrow=${this.narrow}>
@@ -84,14 +81,18 @@ class LovelaceFullConfigEditor extends LitElement {
)}</ha-button
>
<div class="content">
<ha-yaml-editor
<ha-code-editor
mode="yaml"
autofocus
autocomplete-entities
autocomplete-icons
.hass=${this.hass}
@value-changed=${this._yamlChanged}
@editor-save=${this._handleSave}
disable-fullscreen
dir="ltr"
>
</ha-yaml-editor>
</ha-code-editor>
</div>
</ha-top-app-bar-fixed>
`;
@@ -99,7 +100,7 @@ class LovelaceFullConfigEditor extends LitElement {
protected firstUpdated(changedProps: PropertyValues<this>) {
super.firstUpdated(changedProps);
this.yamlEditor.setValue(this.lovelace!.rawConfig);
this.yamlEditor.value = dump(this.lovelace!.rawConfig);
}
protected updated(changedProps: PropertyValues<this>) {
@@ -111,7 +112,7 @@ class LovelaceFullConfigEditor extends LitElement {
oldLovelace.rawConfig !== this.lovelace.rawConfig &&
!deepEqual(oldLovelace.rawConfig, this.lovelace.rawConfig)
) {
this.yamlEditor.setValue(this.lovelace!.rawConfig);
this.yamlEditor.value = dump(this.lovelace!.rawConfig);
}
}
@@ -136,7 +137,7 @@ class LovelaceFullConfigEditor extends LitElement {
font-size: var(--ha-font-size-l);
}
ha-yaml-editor {
ha-code-editor {
height: 100%;
}
@@ -153,9 +154,7 @@ class LovelaceFullConfigEditor extends LitElement {
];
}
private _yamlChanged(ev: CustomEvent) {
this._config = ev.detail.isValid ? ev.detail.value : undefined;
this._yamlError = ev.detail.errorMsg;
private _yamlChanged() {
this._changed = undoDepth(this.yamlEditor.codemirror!.state) > 0;
if (this._changed && !window.onbeforeunload) {
window.onbeforeunload = () => true;
@@ -205,7 +204,9 @@ class LovelaceFullConfigEditor extends LitElement {
private async _handleSave() {
this._saving = true;
if (!this.yamlEditor.yaml) {
const value = this.yamlEditor.value;
if (!value) {
showConfirmationDialog(this, {
title: this.hass.localize(
"ui.panel.lovelace.editor.raw_editor.confirm_reset_config_title"
@@ -221,14 +222,6 @@ class LovelaceFullConfigEditor extends LitElement {
return;
}
if (this._yamlError) {
showAlertDialog(this, {
text: this._yamlError,
});
this._saving = false;
return;
}
if (this.yamlEditor.hasComments) {
if (
!confirm(
@@ -241,8 +234,19 @@ class LovelaceFullConfigEditor extends LitElement {
}
}
const config: LovelaceRawConfig = this._config!;
let config: LovelaceRawConfig;
try {
config = load(value) as LovelaceRawConfig;
} catch (err: any) {
showAlertDialog(this, {
text: this.hass.localize(
"ui.panel.lovelace.editor.raw_editor.error_parse_yaml",
{ error: err }
),
});
this._saving = false;
return;
}
try {
if (isStrategyDashboard(config)) {
assert(config, strategyStruct);
@@ -281,8 +285,8 @@ class LovelaceFullConfigEditor extends LitElement {
this._saving = false;
}
private get yamlEditor(): HaYamlEditor {
return this.shadowRoot!.querySelector("ha-yaml-editor")! as HaYamlEditor;
private get yamlEditor(): HaCodeEditor {
return this.shadowRoot!.querySelector("ha-code-editor")! as HaCodeEditor;
}
}

View File

@@ -409,8 +409,6 @@ class HUIRoot extends LitElement {
slot="actionItems"
.id="button-${index}"
.path=${item.icon}
.label=${label}
hide-title
@click=${item.buttonAction}
></ha-icon-button>
<ha-tooltip placement="bottom" .for="button-${index}">

View File

@@ -37,7 +37,6 @@ export {
search,
searchKeymap,
} from "@codemirror/search";
export { lintGutter, lintKeymap, setDiagnostics } from "@codemirror/lint";
export { EditorState } from "@codemirror/state";
export {
crosshairCursor,
@@ -81,7 +80,6 @@ export { closePercentBrace };
export const langCompartment = new Compartment();
export const readonlyCompartment = new Compartment();
export const linewrapCompartment = new Compartment();
export const yamlLintCompartment = new Compartment();
// ---------------------------------------------------------------------------
// YAML scalar type highlighter
@@ -371,20 +369,6 @@ export const haTheme = EditorView.theme({
paddingRight: "0",
},
".cm-gutterElement.lineNumber": { color: "inherit" },
// Lint gutter
".cm-lint-marker-error": { color: "var(--error-color)" },
".cm-lint-marker-warning": { color: "var(--warning-color)" },
".cm-lint-marker-info": { color: "var(--info-color, var(--primary-color))" },
".cm-diagnostic": {
fontFamily: "var(--mdc-typography-font-family, var(--ha-font-family-body))",
},
".cm-diagnostic.cm-diagnostic-error": {
borderLeft: "3px solid var(--error-color)",
},
".cm-diagnostic.cm-diagnostic-warning": {
borderLeft: "3px solid var(--warning-color)",
},
});
const haHighlightStyle = HighlightStyle.define([

View File

@@ -491,11 +491,6 @@
"markdown": "Markdown"
},
"components": {
"input": {
"clear": "Clear",
"show_password": "Show password",
"hide_password": "Hide password"
},
"selectors": {
"serial_port": {
"enter_manually": "Enter manually",
@@ -1432,7 +1427,6 @@
},
"yaml-editor": {
"copy_to_clipboard": "[%key:ui::panel::config::automation::editor::copy_to_clipboard%]",
"syntax_error": "YAML syntax error",
"error": "Error in parsing YAML: {reason}",
"error_location": "line: {line}, column: {column}",
"enter_fullscreen": "Enter fullscreen",
@@ -3790,7 +3784,7 @@
"type": "Event type",
"data": "Event data (YAML, optional)",
"fire_event": "Fire event",
"event_fired": "Event {name} fired at {time}",
"event_fired": "Event {name} fired",
"active_listeners": "Active listeners",
"count_listeners": "({count} {count, plural,\n one {listener}\n other {listeners}\n})",
"listen_to_events": "Listen to events",
@@ -3804,15 +3798,7 @@
"clear_events": "Clear events",
"alert_event_type": "Event type is a mandatory field",
"notification_event_fired": "Event {type} successfully fired!",
"subscribe_failed": "Failed to subscribe to event: {error}",
"unknown_error": "Unknown error",
"oldest_event": "Oldest event",
"older_event": "Older event",
"newer_event": "Newer event",
"newest_event": "Newest event",
"waiting_for_events": "Waiting for events…",
"subscribe_prompt": "Subscribe to an event type above to see events here.",
"buffer_disclaimer": "Showing the latest {count} events. Older events are discarded as new ones arrive.\n\nTip: Subscribe to a more specific event type, or use the filter to narrow results."
"subscribe_failed": "Failed to subscribe to event: {error}"
},
"actions": {
"title": "Actions",
@@ -3823,8 +3809,8 @@
"column_description": "Description",
"column_example": "Example",
"fill_example_data": "Fill example data",
"yaml_mode": "YAML mode",
"ui_mode": "UI mode",
"yaml_mode": "Go to YAML mode",
"ui_mode": "Go to UI mode",
"yaml_parameters": "Parameters only available in YAML mode",
"all_parameters": "All available parameters",
"accepts_target": "This action accepts a target, for example: `entity_id: light.bed_light`",
@@ -8701,6 +8687,7 @@
"confirm_reset_config_text": "Your dashboard will be reset to an empty state. You can start fresh and build your dashboard from scratch.",
"confirm_unsaved_changes": "You have unsaved changes, are you sure you want to exit?",
"confirm_unsaved_comments": "Your configuration might contain comments, these will not be saved. Do you want to continue?",
"error_parse_yaml": "Unable to parse YAML: {error}",
"error_invalid_config": "Your configuration is not valid: {error}",
"error_save_yaml": "Unable to save YAML: {error}",
"resources_moved": "Resources should no longer be added to the dashboard configuration but can be added in the dashboard config panel."

123
yarn.lock
View File

@@ -18,36 +18,27 @@ __metadata:
languageName: node
linkType: hard
"@asamuzakjp/css-color@npm:^5.1.11":
version: 5.1.11
resolution: "@asamuzakjp/css-color@npm:5.1.11"
"@asamuzakjp/css-color@npm:^5.1.5":
version: 5.1.6
resolution: "@asamuzakjp/css-color@npm:5.1.6"
dependencies:
"@asamuzakjp/generational-cache": "npm:^1.0.1"
"@csstools/css-calc": "npm:^3.2.0"
"@csstools/css-color-parser": "npm:^4.1.0"
"@csstools/css-calc": "npm:^3.1.1"
"@csstools/css-color-parser": "npm:^4.0.2"
"@csstools/css-parser-algorithms": "npm:^4.0.0"
"@csstools/css-tokenizer": "npm:^4.0.0"
checksum: 10/2e337cc94b5a3f9741a27f92b4e4b7dc467a76b1dcf66c40e71808fed71695f10c8cf07c8b13313cbb637154314ca1d8626bb9a045fe94b404b242a390cf3bd3
checksum: 10/5151369d9369e478e03c0eee0f171b8f86306ebbdf5b352544cd745c360d97343f437bdd0690ff658e47d2876b466bffc8811fcef7f0347cb243c6483a7e95a0
languageName: node
linkType: hard
"@asamuzakjp/dom-selector@npm:^7.1.1":
version: 7.1.1
resolution: "@asamuzakjp/dom-selector@npm:7.1.1"
"@asamuzakjp/dom-selector@npm:^7.0.6":
version: 7.0.7
resolution: "@asamuzakjp/dom-selector@npm:7.0.7"
dependencies:
"@asamuzakjp/generational-cache": "npm:^1.0.1"
"@asamuzakjp/nwsapi": "npm:^2.3.9"
bidi-js: "npm:^1.0.3"
css-tree: "npm:^3.2.1"
is-potential-custom-element-name: "npm:^1.0.1"
checksum: 10/49a065a64db5f53a3008c231d09606e4b67f509fa20148a67419451c2dc91a421202ed17bfc4bc679ad2f0432d7260720d602c1d5c9c5e165931fff5199c3f12
languageName: node
linkType: hard
"@asamuzakjp/generational-cache@npm:^1.0.1":
version: 1.0.1
resolution: "@asamuzakjp/generational-cache@npm:1.0.1"
checksum: 10/e1cf3f1916a334c6153f624982f0eb3d50fa3048435ea5c5b0f441f8f1ab74a0fe992dac214b612d22c0acafad3cd1a1f6b45d99c7b6e3b63cfdf7f6ca5fc144
checksum: 10/18f40def8c775c6008c8fcd75d7d049ff92d99a494929ab2bf742341b348c78cbf4808d29c13b9cd87ca4fd272773cf5aa9e58fee48603c286df48148be8cb67
languageName: node
linkType: hard
@@ -1340,7 +1331,7 @@ __metadata:
languageName: node
linkType: hard
"@codemirror/lint@npm:6.9.5, @codemirror/lint@npm:^6.0.0":
"@codemirror/lint@npm:^6.0.0":
version: 6.9.5
resolution: "@codemirror/lint@npm:6.9.5"
dependencies:
@@ -1390,26 +1381,26 @@ __metadata:
languageName: node
linkType: hard
"@csstools/css-calc@npm:^3.2.0":
version: 3.2.0
resolution: "@csstools/css-calc@npm:3.2.0"
"@csstools/css-calc@npm:^3.1.1":
version: 3.1.1
resolution: "@csstools/css-calc@npm:3.1.1"
peerDependencies:
"@csstools/css-parser-algorithms": ^4.0.0
"@csstools/css-tokenizer": ^4.0.0
checksum: 10/7eec51a21945a74aa6a407d1e6290d0f4c5d01829a42c01a56ce2055216398540cc3120837b15a0db38601bcb40cf97f1d991fefb3ee9d00d9cec03d67beba4c
checksum: 10/faa3aa2736b20757ceafd76e3d2841e8726ec9e7ae78e387684eb462aba73d533ba384039338685c3a52196196300ccdfecb051e59864b1d3b457fe927b7f53b
languageName: node
linkType: hard
"@csstools/css-color-parser@npm:^4.1.0":
version: 4.1.0
resolution: "@csstools/css-color-parser@npm:4.1.0"
"@csstools/css-color-parser@npm:^4.0.2":
version: 4.0.2
resolution: "@csstools/css-color-parser@npm:4.0.2"
dependencies:
"@csstools/color-helpers": "npm:^6.0.2"
"@csstools/css-calc": "npm:^3.2.0"
"@csstools/css-calc": "npm:^3.1.1"
peerDependencies:
"@csstools/css-parser-algorithms": ^4.0.0
"@csstools/css-tokenizer": ^4.0.0
checksum: 10/794508011a95ebac3856e67e0333ca4174604d2dfddc101d991f2ebfd52b3c99cd36a08462675c2a07d057ca3787187fcd7eac98bced2eefdd9040b37853426d
checksum: 10/6418bfadc8c15d3a65c1e80278df383b542f0437446c0ba21d591dd564bcc19ab0b11243edf62672f4c62cc778f9b386fa4349e9a8d1de2b414148ea8a1ac775
languageName: node
linkType: hard
@@ -1422,15 +1413,15 @@ __metadata:
languageName: node
linkType: hard
"@csstools/css-syntax-patches-for-csstree@npm:^1.1.3":
version: 1.1.3
resolution: "@csstools/css-syntax-patches-for-csstree@npm:1.1.3"
"@csstools/css-syntax-patches-for-csstree@npm:^1.1.1":
version: 1.1.1
resolution: "@csstools/css-syntax-patches-for-csstree@npm:1.1.1"
peerDependencies:
css-tree: ^3.2.1
peerDependenciesMeta:
css-tree:
optional: true
checksum: 10/1c91dc03b64ca913eed5064ca0e434da1c0be8def6ce20f932d1db10f9b478ac3830c99a033b0edf75954cf9164c7c267b220ed9faffbc3342bf320870c3bb4b
checksum: 10/745ec0f6f7d1c3707af9661d5dcc7e29c12c0416da46e10dda7518c872fef38446d39e13557b3d134e16eb1c78899fa6a712a27fd8ab544813e25a4cd0913cdc
languageName: node
linkType: hard
@@ -1857,9 +1848,9 @@ __metadata:
languageName: node
linkType: hard
"@home-assistant/webawesome@npm:3.3.1-ha.2":
version: 3.3.1-ha.2
resolution: "@home-assistant/webawesome@npm:3.3.1-ha.2"
"@home-assistant/webawesome@npm:3.3.1-ha.1":
version: 3.3.1-ha.1
resolution: "@home-assistant/webawesome@npm:3.3.1-ha.1"
dependencies:
"@ctrl/tinycolor": "npm:4.1.0"
"@floating-ui/dom": "npm:^1.6.13"
@@ -1870,7 +1861,7 @@ __metadata:
lit: "npm:^3.2.1"
nanoid: "npm:^5.1.5"
qr-creator: "npm:^1.0.0"
checksum: 10/c66965544bb34eb9d6d131579c31e4c6d21c8c231bdf565db61195da03a995d17e25d6d997875c902d7d2f3020c70434088efb7299b2626d4a7d1343cb33d726
checksum: 10/903b7e5e44a9c4afa5120b5fbbf139880889798a82262b10b5c5fa51cd56cd344d8027d69658b15c85efc579688476bbd8e5eff73535f903959bbd9ffd00051f
languageName: node
linkType: hard
@@ -6721,13 +6712,6 @@ __metadata:
languageName: node
linkType: hard
"entities@npm:^8.0.0":
version: 8.0.0
resolution: "entities@npm:8.0.0"
checksum: 10/d6e2ba75e444fb101ee2fbb07c839e687306c8a509426b75186619c19196f97c1db9932ca083f823c03e4a20e7407b654aa34de8cbb7770468e20fb2d4573a0e
languageName: node
linkType: hard
"env-paths@npm:^2.2.0":
version: 2.2.1
resolution: "env-paths@npm:2.2.1"
@@ -8122,7 +8106,6 @@ __metadata:
"@codemirror/lang-jinja": "npm:6.0.1"
"@codemirror/lang-yaml": "npm:6.1.3"
"@codemirror/language": "npm:6.12.3"
"@codemirror/lint": "npm:6.9.5"
"@codemirror/search": "npm:6.7.0"
"@codemirror/state": "npm:6.6.0"
"@codemirror/view": "npm:6.41.1"
@@ -8144,7 +8127,7 @@ __metadata:
"@fullcalendar/list": "npm:6.1.20"
"@fullcalendar/luxon3": "npm:6.1.20"
"@fullcalendar/timegrid": "npm:6.1.20"
"@home-assistant/webawesome": "npm:3.3.1-ha.2"
"@home-assistant/webawesome": "npm:3.3.1-ha.1"
"@html-eslint/eslint-plugin": "npm:0.59.0"
"@lezer/highlight": "npm:1.2.3"
"@lit-labs/motion": "npm:1.1.0"
@@ -8240,7 +8223,7 @@ __metadata:
idb-keyval: "npm:6.2.2"
intl-messageformat: "npm:11.2.2"
js-yaml: "npm:4.1.1"
jsdom: "npm:29.1.0"
jsdom: "npm:29.0.2"
jszip: "npm:3.10.1"
leaflet: "npm:1.9.4"
leaflet-draw: "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch"
@@ -9115,26 +9098,26 @@ __metadata:
languageName: node
linkType: hard
"jsdom@npm:29.1.0":
version: 29.1.0
resolution: "jsdom@npm:29.1.0"
"jsdom@npm:29.0.2":
version: 29.0.2
resolution: "jsdom@npm:29.0.2"
dependencies:
"@asamuzakjp/css-color": "npm:^5.1.11"
"@asamuzakjp/dom-selector": "npm:^7.1.1"
"@asamuzakjp/css-color": "npm:^5.1.5"
"@asamuzakjp/dom-selector": "npm:^7.0.6"
"@bramus/specificity": "npm:^2.4.2"
"@csstools/css-syntax-patches-for-csstree": "npm:^1.1.3"
"@csstools/css-syntax-patches-for-csstree": "npm:^1.1.1"
"@exodus/bytes": "npm:^1.15.0"
css-tree: "npm:^3.2.1"
data-urls: "npm:^7.0.0"
decimal.js: "npm:^10.6.0"
html-encoding-sniffer: "npm:^6.0.0"
is-potential-custom-element-name: "npm:^1.0.1"
lru-cache: "npm:^11.3.5"
parse5: "npm:^8.0.1"
lru-cache: "npm:^11.2.7"
parse5: "npm:^8.0.0"
saxes: "npm:^6.0.0"
symbol-tree: "npm:^3.2.4"
tough-cookie: "npm:^6.0.1"
undici: "npm:^7.25.0"
undici: "npm:^7.24.5"
w3c-xmlserializer: "npm:^5.0.0"
webidl-conversions: "npm:^8.0.1"
whatwg-mimetype: "npm:^5.0.0"
@@ -9145,7 +9128,7 @@ __metadata:
peerDependenciesMeta:
canvas:
optional: true
checksum: 10/91194be30c10b518c8e21c3a15cf1ef0e6c74722fb8fe402f201efed3e5de55f13005f2a4108d02ccde75d1e16719ca898690be93cd22d498a4c062d035adae5
checksum: 10/3ad1d9a5b6aba067427bc43be98e1c51fab489bf689a6530e596278c6326fe053c94fc47a9c133f126fbe914f421283ae723fb92214dfe4959ca6cf2ee1666f6
languageName: node
linkType: hard
@@ -9669,10 +9652,10 @@ __metadata:
languageName: node
linkType: hard
"lru-cache@npm:^11.0.0, lru-cache@npm:^11.1.0, lru-cache@npm:^11.2.1, lru-cache@npm:^11.3.5":
version: 11.3.5
resolution: "lru-cache@npm:11.3.5"
checksum: 10/3701b77e87765a3aea453402a7850bdbf7e02445210f35bd5ba1561f601f605f488bf9932be4a3851a6664073924f671a1ec99c4a1a98c457e0d126872a3e04f
"lru-cache@npm:^11.0.0, lru-cache@npm:^11.1.0, lru-cache@npm:^11.2.1, lru-cache@npm:^11.2.7":
version: 11.2.7
resolution: "lru-cache@npm:11.2.7"
checksum: 10/fbff4b8dee8189dde9b52cdfb3ea89b4c9cec094c1538cd30d1f47299477ff312efdb35f7994477ec72328f8e754e232b26a143feda1bd1f79ff22da6664d2c5
languageName: node
linkType: hard
@@ -10462,12 +10445,12 @@ __metadata:
languageName: node
linkType: hard
"parse5@npm:^8.0.1":
version: 8.0.1
resolution: "parse5@npm:8.0.1"
"parse5@npm:^8.0.0":
version: 8.0.0
resolution: "parse5@npm:8.0.0"
dependencies:
entities: "npm:^8.0.0"
checksum: 10/671dedfe7cbf4714414317bc8c6b2a14c61ef44f8fd90c983b5b1870653af5aa2e3b4e25e38e9538a7120ea2b688c50908830da2bd0930d8fd4bce34aed024eb
entities: "npm:^6.0.0"
checksum: 10/1973850932bb1cbd52ab64502761489fbe1bb43a52dee7ce41aac0b6c33a51a92aaee04661590b0912b739ae9ee316bce4c78c8ea34af42a7e522c983c3c6cf5
languageName: node
linkType: hard
@@ -12682,10 +12665,10 @@ __metadata:
languageName: node
linkType: hard
"undici@npm:^7.25.0":
version: 7.25.0
resolution: "undici@npm:7.25.0"
checksum: 10/038d3568c72bb976e3cc389284f7f1cc64cd70d578300e4676a449fbcb624a35fe99ac127b5f3729f18b8246d6c090444ab61b1b67736bb88f52a3e913d76bf8
"undici@npm:^7.24.5":
version: 7.24.5
resolution: "undici@npm:7.24.5"
checksum: 10/1eef66817e5bd1ee6ba908553452e8e23c6b743221f8435e838a87fcec84db901ad9bca5853e7a6e123520abd8d3dd03215b19bf57c2ffb114bfeb0b7c206027
languageName: node
linkType: hard