mirror of
https://github.com/home-assistant/frontend.git
synced 2026-04-30 22:43:04 +00:00
Compare commits
7 Commits
devtools-a
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3046f3e47d | ||
|
|
35601a0900 | ||
|
|
e7016c15af | ||
|
|
624521e30b | ||
|
|
4876bfa639 | ||
|
|
5dea0764b2 | ||
|
|
121ed7ac1f |
@@ -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"
|
||||
|
||||
42
src/components/ha-code-editor-jinja-arg-hover.ts
Normal file
42
src/components/ha-code-editor-jinja-arg-hover.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import type { CompletionItem } from "./ha-code-editor-completion-items";
|
||||
import "./ha-code-editor-completion-items";
|
||||
|
||||
@customElement("ha-code-editor-jinja-arg-hover")
|
||||
export class HaCodeEditorJinjaArgHover extends LitElement {
|
||||
/** Bold heading shown above the items grid (e.g. entity/device/area name). */
|
||||
@property({ attribute: false }) public heading?: string;
|
||||
|
||||
@property({ attribute: false }) public items: CompletionItem[] = [];
|
||||
|
||||
render() {
|
||||
return html`
|
||||
${this.heading
|
||||
? html`<div class="heading">${this.heading}</div>`
|
||||
: nothing}
|
||||
<ha-code-editor-completion-items
|
||||
.items=${this.items}
|
||||
></ha-code-editor-completion-items>
|
||||
`;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
padding: 6px 10px;
|
||||
max-width: 360px;
|
||||
}
|
||||
|
||||
.heading {
|
||||
font-weight: var(--ha-font-weight-bold);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-code-editor-jinja-arg-hover": HaCodeEditorJinjaArgHover;
|
||||
}
|
||||
}
|
||||
101
src/components/ha-code-editor-jinja-hover.ts
Normal file
101
src/components/ha-code-editor-jinja-hover.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import type { Completion } from "@codemirror/autocomplete";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { mdiHelpCircleOutline } from "@mdi/js";
|
||||
import "./ha-svg-icon";
|
||||
|
||||
@customElement("ha-code-editor-jinja-hover")
|
||||
export class HaCodeEditorJinjaHover extends LitElement {
|
||||
@property({ attribute: false }) public completion!: Completion;
|
||||
|
||||
@property({ attribute: false }) public docUrl?: string;
|
||||
|
||||
@property({ attribute: false }) public openDocumentation =
|
||||
"Open documentation";
|
||||
|
||||
render() {
|
||||
const info =
|
||||
typeof this.completion.info === "string"
|
||||
? this.completion.info
|
||||
: undefined;
|
||||
|
||||
return html`
|
||||
<div class="header">
|
||||
<div class="sig">
|
||||
<strong>${this.completion.label}</strong>
|
||||
${this.completion.detail
|
||||
? html`<span class="detail">(${this.completion.detail})</span>`
|
||||
: nothing}
|
||||
</div>
|
||||
${this.docUrl
|
||||
? html`<a
|
||||
class="doc-link"
|
||||
href=${this.docUrl}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
title=${this.openDocumentation}
|
||||
><ha-svg-icon .path=${mdiHelpCircleOutline}></ha-svg-icon
|
||||
></a>`
|
||||
: nothing}
|
||||
</div>
|
||||
${info ? html`<div class="desc">${info}</div>` : nothing}
|
||||
`;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
padding: 6px 10px;
|
||||
max-width: 360px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.sig {
|
||||
font-family: var(--ha-font-family-code);
|
||||
font-size: 0.9em;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.detail {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
.doc-link {
|
||||
flex-shrink: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
color: var(--secondary-text-color);
|
||||
opacity: 0.7;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.doc-link:hover {
|
||||
opacity: 1;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.doc-link ha-svg-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.desc {
|
||||
font-size: 0.9em;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-code-editor-jinja-hover": HaCodeEditorJinjaHover;
|
||||
}
|
||||
}
|
||||
@@ -36,9 +36,13 @@ import { computeAreaName } from "../common/entity/compute_area_name";
|
||||
import { computeFloorName } from "../common/entity/compute_floor_name";
|
||||
import { copyToClipboard } from "../common/util/copy-clipboard";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import type { JinjaArgType } from "../resources/jinja_ha_completions";
|
||||
import type {
|
||||
JinjaArgType,
|
||||
HassArgHoverContext,
|
||||
} from "../resources/jinja_ha_completions";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { showToast } from "../util/toast";
|
||||
import { documentationUrl } from "../util/documentation-url";
|
||||
import { labelsContext } from "../data/context";
|
||||
import type { LabelRegistryEntry } from "../data/label/label_registry";
|
||||
import "./ha-code-editor-completion-items";
|
||||
@@ -326,6 +330,16 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
this._loadedCodeMirror.tooltips({
|
||||
position: "absolute",
|
||||
}),
|
||||
this._loadedCodeMirror.hoverTooltip(
|
||||
(view, pos) =>
|
||||
this._loadedCodeMirror!.haJinjaHoverSource(
|
||||
view,
|
||||
pos,
|
||||
this.hass ? documentationUrl(this.hass, "") : undefined,
|
||||
this.hass ? this._hassArgHoverContext() : undefined
|
||||
),
|
||||
{ hoverTime: 300 }
|
||||
),
|
||||
...(this.placeholder ? [placeholder(this.placeholder)] : []),
|
||||
];
|
||||
|
||||
@@ -575,6 +589,48 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Builds a HassArgHoverContext from the current hass object so that
|
||||
* haJinjaHoverSource can resolve entity / device / area friendly names
|
||||
* without importing the full HomeAssistant type into the resource file.
|
||||
*/
|
||||
private _hassArgHoverContext(): HassArgHoverContext {
|
||||
const hass = this.hass!;
|
||||
const labelMap: Record<
|
||||
string,
|
||||
{ name: string; description?: string | null }
|
||||
> = {};
|
||||
for (const label of this._labels ?? []) {
|
||||
labelMap[label.label_id] = {
|
||||
name: label.name,
|
||||
description: label.description,
|
||||
};
|
||||
}
|
||||
return {
|
||||
states: hass.states as HassArgHoverContext["states"],
|
||||
devices: hass.devices as HassArgHoverContext["devices"],
|
||||
areas: hass.areas as HassArgHoverContext["areas"],
|
||||
floors: hass.floors as HassArgHoverContext["floors"],
|
||||
entities: hass.entities as HassArgHoverContext["entities"],
|
||||
labels: labelMap,
|
||||
formatEntityState: (entityId) =>
|
||||
hass.formatEntityState(hass.states[entityId]),
|
||||
formatEntityName: (entityId) => {
|
||||
const stateObj = hass.states[entityId];
|
||||
return (
|
||||
(stateObj?.attributes.friendly_name as string | undefined) ??
|
||||
hass.entities[entityId]?.name ??
|
||||
undefined
|
||||
);
|
||||
},
|
||||
formatAttributeName: (entityId, attribute) =>
|
||||
hass.formatEntityAttributeName(hass.states[entityId], attribute),
|
||||
formatAttributeValue: (entityId, attribute) =>
|
||||
hass.formatEntityAttributeValue(hass.states[entityId], attribute),
|
||||
localize: (key) => hass.localize(key as never),
|
||||
};
|
||||
}
|
||||
|
||||
private _renderInfo = (completion: Completion): CompletionInfo => {
|
||||
const key =
|
||||
typeof completion.apply === "string"
|
||||
|
||||
@@ -59,7 +59,7 @@ export class HaSelect extends LitElement {
|
||||
value: string | number | undefined
|
||||
) => {
|
||||
// just in case value is a number, convert it to string to avoid falsy value
|
||||
const valueStr = String(value);
|
||||
const valueStr = value !== undefined ? String(value) : undefined;
|
||||
if (!options || !valueStr) {
|
||||
return valueStr;
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ import { SubscribeMixin } from "../mixins/subscribe-mixin";
|
||||
import { actionHandler } from "../panels/lovelace/common/directives/action-handler-directive";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import type { HomeAssistant, PanelInfo, Route } from "../types";
|
||||
import { isMobileClient } from "../util/is_mobile";
|
||||
import "./animation/ha-fade-in";
|
||||
import "./ha-icon";
|
||||
import "./ha-icon-button";
|
||||
@@ -579,6 +580,10 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
|
||||
}
|
||||
|
||||
private _renderToolTip(id: string, text: string) {
|
||||
if (isMobileClient) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`<ha-tooltip
|
||||
for=${id}
|
||||
show-delay="0"
|
||||
|
||||
@@ -9,6 +9,7 @@ import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-card";
|
||||
import "../../../../components/ha-code-editor";
|
||||
import "../../../../components/ha-spinner";
|
||||
import "../../../../components/ha-tip";
|
||||
import type { RenderTemplateResult } from "../../../../data/ws-templates";
|
||||
import { subscribeRenderTemplate } from "../../../../data/ws-templates";
|
||||
import { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog-box";
|
||||
@@ -158,6 +159,14 @@ class HaPanelDevTemplate extends LitElement {
|
||||
${this.hass.localize("ui.common.clear")}
|
||||
</ha-button>
|
||||
</div>
|
||||
<ha-tip .hass=${this.hass}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.developer-tools.tabs.templates.keyboard_tip",
|
||||
{
|
||||
autocomplete: html`<kbd>Ctrl</kbd>+<kbd>Space</kbd>`,
|
||||
}
|
||||
)}
|
||||
</ha-tip>
|
||||
</ha-card>
|
||||
|
||||
<ha-card
|
||||
@@ -361,6 +370,22 @@ ${type === "object"
|
||||
color: var(--warning-color);
|
||||
}
|
||||
|
||||
ha-tip {
|
||||
padding: 0 var(--ha-space-4) var(--ha-space-4);
|
||||
display: block;
|
||||
}
|
||||
|
||||
kbd {
|
||||
display: inline-block;
|
||||
font-family: var(--ha-font-family-code);
|
||||
font-size: 0.85em;
|
||||
padding: 1px 5px;
|
||||
border: 1px solid var(--divider-color);
|
||||
border-radius: var(--ha-border-radius-xs);
|
||||
background-color: var(--secondary-background-color);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@media all and (max-width: 870px) {
|
||||
.content ha-card {
|
||||
max-width: 100%;
|
||||
|
||||
@@ -8,15 +8,18 @@ import "../../../../../components/ha-alert";
|
||||
import "../../../../../components/ha-button";
|
||||
import "../../../../../components/ha-icon-next";
|
||||
import "../../../../../components/ha-list-item";
|
||||
import "../../../../../components/ha-markdown";
|
||||
import "../../../../../components/ha-spinner";
|
||||
import "../../../../../components/ha-dialog-footer";
|
||||
import "../../../../../components/ha-dialog";
|
||||
import type { DeviceRegistryEntry } from "../../../../../data/device/device_registry";
|
||||
import {
|
||||
fetchZwaveNodeMetadata,
|
||||
fetchZwaveNodeStatus,
|
||||
NodeStatus,
|
||||
removeFailedZwaveNode,
|
||||
} from "../../../../../data/zwave_js";
|
||||
import type { ZwaveJSNodeMetadata } from "../../../../../data/zwave_js";
|
||||
import { haStyleDialog } from "../../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
import type { ZWaveJSRemoveNodeDialogParams } from "./show-dialog-zwave_js-remove-node";
|
||||
@@ -51,6 +54,8 @@ class DialogZWaveJSRemoveNode extends LitElement {
|
||||
|
||||
@state() private _node?: ZWaveJSRemovedNode;
|
||||
|
||||
@state() private _metadata?: ZwaveJSNodeMetadata;
|
||||
|
||||
@state() private _onClose?: () => void;
|
||||
|
||||
private _removeNodeTimeoutHandle?: number;
|
||||
@@ -72,12 +77,26 @@ class DialogZWaveJSRemoveNode extends LitElement {
|
||||
this._entryId = params.entryId;
|
||||
this._deviceId = params.deviceId;
|
||||
this._onClose = params.onClose;
|
||||
this._metadata = undefined;
|
||||
this._open = true;
|
||||
if (this._deviceId) {
|
||||
const nodeStatus = await fetchZwaveNodeStatus(this.hass, this._deviceId!);
|
||||
this._device = this.hass.devices[this._deviceId];
|
||||
this._step =
|
||||
nodeStatus.status === NodeStatus.Dead ? "start_removal" : "start";
|
||||
if (nodeStatus.status !== NodeStatus.Dead) {
|
||||
const requestedDeviceId = this._deviceId;
|
||||
fetchZwaveNodeMetadata(this.hass, requestedDeviceId).then(
|
||||
(metadata) => {
|
||||
if (this._deviceId === requestedDeviceId) {
|
||||
this._metadata = metadata;
|
||||
}
|
||||
},
|
||||
() => {
|
||||
// instructions are supplemental — ignore fetch errors
|
||||
}
|
||||
);
|
||||
}
|
||||
} else if (params.skipConfirmation) {
|
||||
this._startExclusion();
|
||||
} else {
|
||||
@@ -170,13 +189,39 @@ class DialogZWaveJSRemoveNode extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
if (["exclusion", "remove"].includes(this._step)) {
|
||||
if (this._step === "exclusion") {
|
||||
const exclusion = this._metadata?.exclusion?.trim();
|
||||
return html`
|
||||
<ha-spinner></ha-spinner>
|
||||
<div>
|
||||
<p>
|
||||
<b
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.remove_node.ready_to_remove"
|
||||
)}</b
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.remove_node.trigger_device_exclusion"
|
||||
)}
|
||||
</p>
|
||||
${exclusion
|
||||
? html`<ha-markdown breaks .content=${exclusion}></ha-markdown>`
|
||||
: html`<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.remove_node.follow_device_instructions"
|
||||
)}
|
||||
</p>`}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
if (this._step === "remove") {
|
||||
return html`
|
||||
<ha-spinner></ha-spinner>
|
||||
<div>
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.zwave_js.remove_node.${this._step === "exclusion" ? "follow_device_instructions" : "removing_device"}`
|
||||
"ui.panel.config.zwave_js.remove_node.removing_device"
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
@@ -343,6 +388,7 @@ class DialogZWaveJSRemoveNode extends LitElement {
|
||||
public handleDialogClosed(): void {
|
||||
this._unsubscribe();
|
||||
this._entryId = undefined;
|
||||
this._metadata = undefined;
|
||||
this._step = "start";
|
||||
this._open = false;
|
||||
if (this._onClose) {
|
||||
@@ -371,6 +417,10 @@ class DialogZWaveJSRemoveNode extends LitElement {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
.content ha-markdown {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
ha-alert {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@@ -43,6 +43,7 @@ export {
|
||||
drawSelection,
|
||||
EditorView,
|
||||
highlightActiveLine,
|
||||
hoverTooltip,
|
||||
keymap,
|
||||
lineNumbers,
|
||||
rectangularSelection,
|
||||
@@ -70,9 +71,10 @@ export const closeBracketsOverride = Prec.highest(
|
||||
|
||||
export {
|
||||
haJinjaCompletionSource,
|
||||
haJinjaHoverSource,
|
||||
JINJA_FUNCTION_ARG_TYPES,
|
||||
} from "./jinja_ha_completions";
|
||||
export type { JinjaArgType } from "./jinja_ha_completions";
|
||||
export type { HassArgHoverContext, JinjaArgType } from "./jinja_ha_completions";
|
||||
export { closePercentBrace };
|
||||
|
||||
export const langCompartment = new Compartment();
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1497,6 +1497,9 @@
|
||||
"area_label": "Area",
|
||||
"description": "Configure which areas correspond to each vacuum segment"
|
||||
},
|
||||
"codemirror": {
|
||||
"open_documentation": "Open documentation"
|
||||
},
|
||||
"safe_mode": {
|
||||
"title": "Safe mode",
|
||||
"text": "Home Assistant is running in safe mode, custom integrations and community frontend modules are not available. Restart Home Assistant to exit safe mode."
|
||||
@@ -3867,7 +3870,8 @@
|
||||
"no_listeners": "This template does not listen for any events and will not update automatically.",
|
||||
"listeners": "This template listens for the following state changed events:",
|
||||
"entity": "Entity",
|
||||
"domain": "Domain"
|
||||
"domain": "Domain",
|
||||
"keyboard_tip": "Press {autocomplete} to trigger autocomplete, when your cursor is inside a function that supports it."
|
||||
},
|
||||
"statistics": {
|
||||
"title": "Statistics",
|
||||
@@ -7702,7 +7706,9 @@
|
||||
"menu_remove_device": "Force-remove an unavailable device",
|
||||
"start_exclusion": "Start exclusion",
|
||||
"cancel_exclusion": "Cancel exclusion",
|
||||
"follow_device_instructions": "Follow the directions that came with your device to trigger exclusion on the device.",
|
||||
"ready_to_remove": "Ready to remove device.",
|
||||
"follow_device_instructions": "Follow the directions that came with your device.",
|
||||
"trigger_device_exclusion": "To trigger exclusion on the device:",
|
||||
"removing_device": "Removing device",
|
||||
"exclusion_failed": "An error occurred. Please check the logs for more information.",
|
||||
"exclusion_finished": "Device {id} has been removed from your Z-Wave network."
|
||||
|
||||
Reference in New Issue
Block a user