mirror of
https://github.com/home-assistant/frontend.git
synced 2026-02-15 07:25:54 +00:00
Live inline template previews (#27557)
* Live inline template previews * Opt out for markdown, no fullscreen * flask/flaskOff * styling * mdiBug * Update src/components/ha-selector/ha-selector-template.ts Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com> * resub on connect --------- Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
This commit is contained in:
@@ -13,6 +13,8 @@ import {
|
||||
mdiArrowCollapse,
|
||||
mdiArrowExpand,
|
||||
mdiContentCopy,
|
||||
mdiBug,
|
||||
mdiBugOutline,
|
||||
mdiFindReplace,
|
||||
mdiRedo,
|
||||
mdiUndo,
|
||||
@@ -37,6 +39,7 @@ import type { HaIconButtonToolbar } from "./ha-icon-button-toolbar";
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
"editor-save": undefined;
|
||||
"test-toggle": undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,6 +86,11 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
@property({ type: Boolean, attribute: "has-toolbar" })
|
||||
public hasToolbar = true;
|
||||
|
||||
@property({ type: Boolean, attribute: "has-test" })
|
||||
public hasTest = false;
|
||||
|
||||
@property({ attribute: false }) public testing = false;
|
||||
|
||||
@property({ type: String }) public placeholder?: string;
|
||||
|
||||
@state() private _value = "";
|
||||
@@ -214,7 +222,8 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
if (
|
||||
changedProps.has("_canCopy") ||
|
||||
changedProps.has("_canUndo") ||
|
||||
changedProps.has("_canRedo")
|
||||
changedProps.has("_canRedo") ||
|
||||
changedProps.has("testing")
|
||||
) {
|
||||
this._updateToolbarButtons();
|
||||
}
|
||||
@@ -362,6 +371,19 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
}
|
||||
|
||||
this._editorToolbar.items = [
|
||||
...(this.hasTest && !this._isFullscreen
|
||||
? [
|
||||
{
|
||||
id: "test",
|
||||
label:
|
||||
this.hass?.localize(
|
||||
`ui.components.yaml-editor.test_${this.testing ? "off" : "on"}`
|
||||
) || "Test",
|
||||
path: this.testing ? mdiBugOutline : mdiBug,
|
||||
action: (e: Event) => this._handleTestClick(e),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
id: "undo",
|
||||
disabled: !this._canUndo,
|
||||
@@ -427,6 +449,15 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
}
|
||||
};
|
||||
|
||||
private _handleTestClick = (e: Event) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (!this.codemirror) {
|
||||
return;
|
||||
}
|
||||
fireEvent(this, "test-toggle");
|
||||
};
|
||||
|
||||
private _handleUndoClick = (e: Event) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, html, nothing, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { documentationUrl } from "../../util/documentation-url";
|
||||
import "../ha-code-editor";
|
||||
import "../ha-input-helper-text";
|
||||
import "../ha-alert";
|
||||
import type { RenderTemplateResult } from "../../data/ws-templates";
|
||||
import { subscribeRenderTemplate } from "../../data/ws-templates";
|
||||
import { debounce } from "../../common/util/debounce";
|
||||
import type { TemplateSelector } from "../../data/selector";
|
||||
|
||||
const WARNING_STRINGS = ["template:", "sensor:", "state:", "trigger: template"];
|
||||
|
||||
@@ -13,6 +19,8 @@ const WARNING_STRINGS = ["template:", "sensor:", "state:", "trigger: template"];
|
||||
export class HaTemplateSelector extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public selector!: TemplateSelector;
|
||||
|
||||
@property() public value?: string;
|
||||
|
||||
@property() public label?: string;
|
||||
@@ -27,6 +35,45 @@ export class HaTemplateSelector extends LitElement {
|
||||
|
||||
@state() private warn: string | undefined = undefined;
|
||||
|
||||
@state() private _test = false;
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
@state() private _errorLevel?: "ERROR" | "WARNING";
|
||||
|
||||
@state() private _templateResult?: RenderTemplateResult;
|
||||
|
||||
@state() private _unsubRenderTemplate?: Promise<UnsubscribeFunc>;
|
||||
|
||||
private _debounceError = debounce(
|
||||
(error, level) => {
|
||||
this._error = error;
|
||||
this._errorLevel = level;
|
||||
this._templateResult = undefined;
|
||||
},
|
||||
500,
|
||||
false
|
||||
);
|
||||
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this._debounceError.cancel();
|
||||
this._unsubscribeTemplate();
|
||||
}
|
||||
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
if (this._test) {
|
||||
this._subscribeTemplate();
|
||||
}
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
if (changedProps.has("value") && this._test) {
|
||||
this._subscribeTemplate();
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
${this.warn
|
||||
@@ -61,10 +108,22 @@ export class HaTemplateSelector extends LitElement {
|
||||
autofocus
|
||||
autocomplete-entities
|
||||
autocomplete-icons
|
||||
.hasTest=${this.selector.template?.preview !== false}
|
||||
.testing=${this._test}
|
||||
@value-changed=${this._handleChange}
|
||||
@test-toggle=${this._testToggle}
|
||||
dir="ltr"
|
||||
linewrap
|
||||
></ha-code-editor>
|
||||
${this._test && this._error
|
||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||
: this._test && this._templateResult
|
||||
? html`<pre class="rendered">
|
||||
${typeof this._templateResult.result === "object"
|
||||
? JSON.stringify(this._templateResult.result, null, 2)
|
||||
: this._templateResult.result}</pre
|
||||
>`
|
||||
: nothing}
|
||||
${this.helper
|
||||
? html`<ha-input-helper-text .disabled=${this.disabled}
|
||||
>${this.helper}</ha-input-helper-text
|
||||
@@ -73,6 +132,69 @@ export class HaTemplateSelector extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _testToggle() {
|
||||
this._test = !this._test;
|
||||
if (this._test) {
|
||||
this._subscribeTemplate();
|
||||
} else {
|
||||
this._unsubscribeTemplate();
|
||||
}
|
||||
}
|
||||
|
||||
private async _subscribeTemplate() {
|
||||
await this._unsubscribeTemplate();
|
||||
|
||||
const template = this.value || "";
|
||||
|
||||
try {
|
||||
this._unsubRenderTemplate = subscribeRenderTemplate(
|
||||
this.hass.connection,
|
||||
(result) => {
|
||||
if ("error" in result) {
|
||||
// We show the latest error, or a warning if there are no errors
|
||||
if (result.level === "ERROR" || this._errorLevel !== "ERROR") {
|
||||
this._debounceError(result.error, result.level);
|
||||
}
|
||||
} else {
|
||||
this._debounceError.cancel();
|
||||
this._error = undefined;
|
||||
this._errorLevel = undefined;
|
||||
this._templateResult = result;
|
||||
}
|
||||
},
|
||||
{
|
||||
template,
|
||||
timeout: 3,
|
||||
report_errors: true,
|
||||
}
|
||||
);
|
||||
await this._unsubRenderTemplate;
|
||||
} catch (err: any) {
|
||||
this._error = err.message || "Unknown error";
|
||||
this._errorLevel = undefined;
|
||||
this._templateResult = undefined;
|
||||
this._unsubRenderTemplate = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private async _unsubscribeTemplate(): Promise<void> {
|
||||
if (!this._unsubRenderTemplate) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const unsub = await this._unsubRenderTemplate;
|
||||
unsub();
|
||||
this._unsubRenderTemplate = undefined;
|
||||
} catch (err: any) {
|
||||
if (err.code === "not_found") {
|
||||
// If we get here, the connection was probably already closed. Ignore.
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _handleChange(ev) {
|
||||
ev.stopPropagation();
|
||||
let value = ev.target.value;
|
||||
@@ -90,6 +212,19 @@ export class HaTemplateSelector extends LitElement {
|
||||
p {
|
||||
margin-top: 0;
|
||||
}
|
||||
.rendered {
|
||||
font-family: var(--ha-font-family-code);
|
||||
-webkit-font-smoothing: var(--ha-font-smoothing);
|
||||
-moz-osx-font-smoothing: var(--ha-moz-osx-font-smoothing);
|
||||
clear: both;
|
||||
white-space: pre-wrap;
|
||||
background-color: var(--secondary-background-color);
|
||||
padding: var(--ha-space-2);
|
||||
margin-top: var(--ha-space-3);
|
||||
margin-bottom: 0;
|
||||
direction: ltr;
|
||||
border-radius: var(--ha-border-radius-sm);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -476,7 +476,9 @@ export interface TargetSelector {
|
||||
}
|
||||
|
||||
export interface TemplateSelector {
|
||||
template: {} | null;
|
||||
template: {
|
||||
preview?: boolean;
|
||||
} | null;
|
||||
}
|
||||
|
||||
export interface ThemeSelector {
|
||||
|
||||
@@ -63,7 +63,11 @@ export class HuiMarkdownCardEditor
|
||||
...(!text_only
|
||||
? ([{ name: "title", selector: { text: {} } }] as const)
|
||||
: []),
|
||||
{ name: "content", required: true, selector: { template: {} } },
|
||||
{
|
||||
name: "content",
|
||||
required: true,
|
||||
selector: { template: { preview: false } },
|
||||
},
|
||||
] as const satisfies HaFormSchema[]
|
||||
);
|
||||
|
||||
|
||||
@@ -1310,7 +1310,9 @@
|
||||
"error_location": "line: {line}, column: {column}",
|
||||
"enter_fullscreen": "Enter fullscreen",
|
||||
"exit_fullscreen": "Exit fullscreen",
|
||||
"find_and_replace": "Find and replace"
|
||||
"find_and_replace": "Find and replace",
|
||||
"test_on": "Turn on testing",
|
||||
"test_off": "Turn off testing"
|
||||
},
|
||||
"state-content-picker": {
|
||||
"state": "State",
|
||||
|
||||
Reference in New Issue
Block a user