Improve warning messages in conditional card (#18272)

* Improve warning messages in conditional card

* Update src/panels/lovelace/editor/conditions/ha-card-condition-editor.ts

Co-authored-by: Bram Kragten <mail@bramkragten.nl>

---------

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
This commit is contained in:
Paul Bottein 2023-10-23 14:50:45 +02:00 committed by GitHub
parent c48b620e03
commit c6be4d6f4d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 133 additions and 77 deletions

View File

@ -13,45 +13,33 @@ export const handleStructError = (
for (const failure of err.failures()) { for (const failure of err.failures()) {
if (failure.value === undefined) { if (failure.value === undefined) {
errors.push( errors.push(
hass.localize( hass.localize("ui.errors.config.key_missing", {
"ui.errors.config.key_missing", key: failure.path.join("."),
"key", })
failure.path.join(".")
)
); );
} else if (failure.type === "never") { } else if (failure.type === "never") {
warnings.push( warnings.push(
hass.localize( hass.localize("ui.errors.config.key_not_expected", {
"ui.errors.config.key_not_expected", key: failure.path.join("."),
"key", })
failure.path.join(".")
)
); );
} else if (failure.type === "union") { } else if (failure.type === "union") {
continue; continue;
} else if (failure.type === "enums") { } else if (failure.type === "enums") {
warnings.push( warnings.push(
hass.localize( hass.localize("ui.errors.config.key_wrong_type", {
"ui.errors.config.key_wrong_type", key: failure.path.join("."),
"key", type_correct: failure.message.replace("Expected ", "").split(", ")[0],
failure.path.join("."), type_wrong: JSON.stringify(failure.value),
"type_correct", })
failure.message.replace("Expected ", "").split(", ")[0],
"type_wrong",
JSON.stringify(failure.value)
)
); );
} else { } else {
warnings.push( warnings.push(
hass.localize( hass.localize("ui.errors.config.key_wrong_type", {
"ui.errors.config.key_wrong_type", key: failure.path.join("."),
"key", type_correct: failure.refinement || failure.type,
failure.path.join("."), type_wrong: JSON.stringify(failure.value),
"type_correct", })
failure.refinement || failure.type,
"type_wrong",
JSON.stringify(failure.value)
)
); );
} }
} }

View File

@ -1,7 +1,7 @@
import { Statistic, StatisticType } from "../../../data/recorder"; import { Statistic, StatisticType } from "../../../data/recorder";
import { ActionConfig, LovelaceCardConfig } from "../../../data/lovelace"; import { ActionConfig, LovelaceCardConfig } from "../../../data/lovelace";
import { FullCalendarView, TranslationDict } from "../../../types"; import { FullCalendarView, TranslationDict } from "../../../types";
import { Condition } from "../common/validate-condition"; import { Condition, LegacyCondition } from "../common/validate-condition";
import { HuiImage } from "../components/hui-image"; import { HuiImage } from "../components/hui-image";
import { LovelaceElementConfig } from "../elements/types"; import { LovelaceElementConfig } from "../elements/types";
import { import {
@ -37,7 +37,7 @@ export interface CalendarCardConfig extends LovelaceCardConfig {
export interface ConditionalCardConfig extends LovelaceCardConfig { export interface ConditionalCardConfig extends LovelaceCardConfig {
card: LovelaceCardConfig; card: LovelaceCardConfig;
conditions: Condition[]; conditions: (Condition | LegacyCondition)[];
} }
export interface EmptyStateCardConfig extends LovelaceCardConfig { export interface EmptyStateCardConfig extends LovelaceCardConfig {

View File

@ -21,7 +21,10 @@ export type ScreenCondition = {
media_query?: string; media_query?: string;
}; };
function checkStateCondition(condition: StateCondition, hass: HomeAssistant) { function checkStateCondition(
condition: StateCondition | LegacyCondition,
hass: HomeAssistant
) {
const state = const state =
condition.entity && hass.states[condition.entity] condition.entity && hass.states[condition.entity]
? hass.states[condition.entity].state ? hass.states[condition.entity].state
@ -42,19 +45,20 @@ function checkScreenCondition(
} }
export function checkConditionsMet( export function checkConditionsMet(
conditions: Condition[], conditions: (Condition | LegacyCondition)[],
hass: HomeAssistant hass: HomeAssistant
): boolean { ): boolean {
return conditions.every((c) => { return conditions.every((c) => {
if (c.condition === "screen") { if ("condition" in c) {
return checkScreenCondition(c, hass); if (c.condition === "screen") {
return checkScreenCondition(c, hass);
}
} }
return checkStateCondition(c, hass); return checkStateCondition(c, hass);
}); });
} }
function valideStateCondition(condition: StateCondition) { function valideStateCondition(condition: StateCondition | LegacyCondition) {
return ( return (
condition.entity != null && condition.entity != null &&
(condition.state != null || condition.state_not != null) (condition.state != null || condition.state_not != null)
@ -65,10 +69,14 @@ function validateScreenCondition(condition: ScreenCondition) {
return condition.media_query != null; return condition.media_query != null;
} }
export function validateConditionalConfig(conditions: Condition[]): boolean { export function validateConditionalConfig(
conditions: (Condition | LegacyCondition)[]
): boolean {
return conditions.every((c) => { return conditions.every((c) => {
if (c.condition === "screen") { if ("condition" in c) {
return validateScreenCondition(c); if (c.condition === "screen") {
return validateScreenCondition(c);
}
} }
return valideStateCondition(c); return valideStateCondition(c);
}); });

View File

@ -78,7 +78,7 @@ export class HuiConditionalBase extends ReactiveElement {
} }
const conditions = this._config.conditions.filter( const conditions = this._config.conditions.filter(
(c) => c.condition === "screen" (c) => "condition" in c && c.condition === "screen"
) as ScreenCondition[]; ) as ScreenCondition[];
const mediaQueries = conditions const mediaQueries = conditions

View File

@ -1,7 +1,7 @@
import { preventDefault } from "@fullcalendar/core/internal"; import { preventDefault } from "@fullcalendar/core/internal";
import { ActionDetail } from "@material/mwc-list"; import { ActionDetail } from "@material/mwc-list";
import { mdiCheck, mdiDelete, mdiDotsVertical } from "@mdi/js"; import { mdiCheck, mdiDelete, mdiDotsVertical } from "@mdi/js";
import { LitElement, css, html, nothing } from "lit"; import { LitElement, PropertyValues, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { dynamicElement } from "../../../../common/dom/dynamic-element-directive"; import { dynamicElement } from "../../../../common/dom/dynamic-element-directive";
import { fireEvent } from "../../../../common/dom/fire_event"; import { fireEvent } from "../../../../common/dom/fire_event";
@ -13,33 +13,59 @@ import "../../../../components/ha-svg-icon";
import "../../../../components/ha-yaml-editor"; import "../../../../components/ha-yaml-editor";
import { haStyle } from "../../../../resources/styles"; import { haStyle } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types"; import type { HomeAssistant } from "../../../../types";
import { Condition, LegacyCondition } from "../../common/validate-condition";
import type { LovelaceConditionEditorConstructor } from "./types";
import { ICON_CONDITION } from "../../common/icon-condition"; import { ICON_CONDITION } from "../../common/icon-condition";
import { Condition } from "../../common/validate-condition";
import type { LovelaceConditionEditorConstructor } from "./types";
import { handleStructError } from "../../../../common/structs/handle-errors";
@customElement("ha-card-condition-editor") @customElement("ha-card-condition-editor")
export default class HaCardConditionEditor extends LitElement { export default class HaCardConditionEditor extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) condition!: Condition | LegacyCondition; @property({ attribute: false }) condition!: Condition;
@state() public _yamlMode = false; @state() public _yamlMode = false;
protected render() { @state() public _uiAvailable = false;
const condition: Condition = {
condition: "state", @state() public _uiWarnings: string[] = [];
...this.condition,
}; private get _editor() {
const element = customElements.get( const element = customElements.get(
`ha-card-condition-${condition.condition}` `ha-card-condition-${this.condition.condition}`
) as LovelaceConditionEditorConstructor | undefined; ) as LovelaceConditionEditorConstructor | undefined;
const supported = element !== undefined;
const valid = return element;
element && }
(!element.validateUIConfig || element.validateUIConfig(condition));
const yamlMode = this._yamlMode || !supported || !valid; protected willUpdate(changedProperties: PropertyValues): void {
if (changedProperties.has("condition")) {
const validator = this._editor?.validateUIConfig;
if (validator) {
try {
validator(this.condition, this.hass);
this._uiAvailable = true;
this._uiWarnings = [];
} catch (err) {
this._uiWarnings = handleStructError(
this.hass,
err as Error
).warnings;
this._uiAvailable = false;
}
} else {
this._uiAvailable = false;
this._uiWarnings = [];
}
if (!this._uiAvailable && !this._yamlMode) {
this._yamlMode = true;
}
}
}
protected render() {
const condition = this.condition;
return html` return html`
<div class="header"> <div class="header">
@ -68,9 +94,9 @@ export default class HaCardConditionEditor extends LitElement {
> >
</ha-icon-button> </ha-icon-button>
<ha-list-item graphic="icon" .disabled=${!supported || !valid}> <ha-list-item graphic="icon" .disabled=${!this._uiAvailable}>
${this.hass.localize("ui.panel.lovelace.editor.edit_card.edit_ui")} ${this.hass.localize("ui.panel.lovelace.editor.edit_card.edit_ui")}
${!yamlMode ${!this._yamlMode
? html` ? html`
<ha-svg-icon <ha-svg-icon
class="selected_menu_item" class="selected_menu_item"
@ -85,7 +111,7 @@ export default class HaCardConditionEditor extends LitElement {
${this.hass.localize( ${this.hass.localize(
"ui.panel.lovelace.editor.edit_card.edit_yaml" "ui.panel.lovelace.editor.edit_card.edit_yaml"
)} )}
${yamlMode ${this._yamlMode
? html` ? html`
<ha-svg-icon <ha-svg-icon
class="selected_menu_item" class="selected_menu_item"
@ -108,15 +134,30 @@ export default class HaCardConditionEditor extends LitElement {
</ha-list-item> </ha-list-item>
</ha-button-menu> </ha-button-menu>
</div> </div>
${!valid ${!this._uiAvailable
? html` ? html`
<ha-alert alert-type="warning"> <ha-alert
${this.hass.localize("ui.errors.config.editor_not_supported")} alert-type="warning"
.title=${this.hass.localize(
"ui.errors.config.editor_not_supported"
)}
>
${this._uiWarnings!.length > 0 &&
this._uiWarnings![0] !== undefined
? html`
<ul>
${this._uiWarnings!.map(
(warning) => html`<li>${warning}</li>`
)}
</ul>
`
: nothing}
${this.hass.localize("ui.errors.config.edit_in_yaml_supported")}
</ha-alert> </ha-alert>
` `
: nothing} : nothing}
<div class="content"> <div class="content">
${yamlMode ${this._yamlMode
? html` ? html`
<ha-yaml-editor <ha-yaml-editor
.hass=${this.hass} .hass=${this.hass}

View File

@ -1,6 +1,7 @@
import { HomeAssistant } from "../../../../types";
import { Condition } from "../../common/validate-condition"; import { Condition } from "../../common/validate-condition";
export interface LovelaceConditionEditorConstructor { export interface LovelaceConditionEditorConstructor {
defaultConfig?: Condition; defaultConfig?: Condition;
validateUIConfig?: (condition: Condition) => boolean; validateUIConfig?: (condition: Condition, hass: HomeAssistant) => void;
} }

View File

@ -103,10 +103,17 @@ export class HaCardConditionScreen extends LitElement {
return { condition: "screen", media_query: "" }; return { condition: "screen", media_query: "" };
} }
protected static validateUIConfig(condition: ScreenCondition) { protected static validateUIConfig(
return ( condition: ScreenCondition,
!condition.media_query || mediaQueryReverseMap.get(condition.media_query) hass: HomeAssistant
); ) {
const valid =
!condition.media_query || mediaQueryReverseMap.has(condition.media_query);
if (!valid) {
throw new Error(
hass.localize("ui.errors.config.media_query_not_supported")
);
}
} }
private _schema = memoizeOne( private _schema = memoizeOne(

View File

@ -12,7 +12,7 @@ import { StateCondition } from "../../../common/validate-condition";
const stateConditionStruct = object({ const stateConditionStruct = object({
condition: literal("state"), condition: literal("state"),
entity: string(), entity: optional(string()),
state: optional(string()), state: optional(string()),
state_not: optional(string()), state_not: optional(string()),
}); });
@ -36,6 +36,10 @@ export class HaCardConditionState extends LitElement {
return { condition: "state", entity: "", state: "" }; return { condition: "state", entity: "", state: "" };
} }
protected static validateUIConfig(condition: StateCondition) {
return assert(condition, stateConditionStruct);
}
protected willUpdate(changedProperties: PropertyValues): void { protected willUpdate(changedProperties: PropertyValues): void {
if (!changedProperties.has("condition")) { if (!changedProperties.has("condition")) {
return; return;

View File

@ -163,21 +163,27 @@ export class HuiConditionalCardEditor
` `
: html` : html`
<div class="conditions"> <div class="conditions">
${this.hass!.localize( <ha-alert alert-type="info">
"ui.panel.lovelace.editor.card.conditional.condition_explanation" ${this.hass!.localize(
)} "ui.panel.lovelace.editor.card.conditional.condition_explanation"
${this._config.conditions.map( )}
(cond, idx) => html` </ha-alert>
${this._config.conditions.map((cond, idx) => {
const condition: Condition = {
condition: "state",
...cond,
};
return html`
<div class="condition"> <div class="condition">
<ha-card-condition-editor <ha-card-condition-editor
.index=${idx} .index=${idx}
@value-changed=${this._conditionChanged} @value-changed=${this._conditionChanged}
.hass=${this.hass} .hass=${this.hass}
.condition=${cond} .condition=${condition}
></ha-card-condition-editor> ></ha-card-condition-editor>
</div> </div>
` `;
)} })}
<div> <div>
<ha-button-menu <ha-button-menu
@action=${this._addCondition} @action=${this._addCondition}

View File

@ -1488,7 +1488,8 @@
"key_missing": "Required key ''{key}'' is missing.", "key_missing": "Required key ''{key}'' is missing.",
"key_not_expected": "Key ''{key}'' is not expected or not supported by the visual editor.", "key_not_expected": "Key ''{key}'' is not expected or not supported by the visual editor.",
"key_wrong_type": "The provided value for ''{key}'' is not supported by the visual editor. We support ({type_correct}) but received ({type_wrong}).", "key_wrong_type": "The provided value for ''{key}'' is not supported by the visual editor. We support ({type_correct}) but received ({type_wrong}).",
"no_template_editor_support": "Templates not supported in visual editor" "no_template_editor_support": "Templates not supported in visual editor",
"media_query_not_supported": "This media query is not supported by the visual editor."
}, },
"supervisor": { "supervisor": {
"title": "Could not load the Supervisor panel!", "title": "Could not load the Supervisor panel!",