Add config to empty state card and use it in area empty page (#28946)

* Add config to empty state card and use it in area empty page

* Remove old translations
This commit is contained in:
Paul Bottein 2026-01-12 15:58:59 +01:00 committed by GitHub
parent e89ea47d3a
commit cd062293fc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 310 additions and 42 deletions

View File

@ -1,63 +1,116 @@
import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import "../../../components/ha-card";
import "../../../components/ha-button";
import "../../../components/ha-icon";
import type { HomeAssistant } from "../../../types";
import type { LovelaceCard } from "../types";
import { handleAction } from "../common/handle-action";
import type { LovelaceCard, LovelaceCardEditor } from "../types";
import type { EmptyStateCardConfig } from "./types";
@customElement("hui-empty-state-card")
export class HuiEmptyStateCard extends LitElement implements LovelaceCard {
public static async getConfigElement(): Promise<LovelaceCardEditor> {
await import("../editor/config-elements/hui-empty-state-card-editor");
return document.createElement("hui-empty-state-card-editor");
}
public static getStubConfig(): EmptyStateCardConfig {
return {
type: "empty-state",
title: "Welcome Home",
content: "This is an empty state card.",
};
}
@property({ attribute: false }) public hass?: HomeAssistant;
@state() private _config?: EmptyStateCardConfig;
public getCardSize(): number {
return 2;
}
// eslint-disable-next-line @typescript-eslint/no-empty-function
public setConfig(_config: EmptyStateCardConfig): void {}
public setConfig(config: EmptyStateCardConfig): void {
this._config = config;
}
protected render() {
if (!this.hass) {
if (!this.hass || !this._config) {
return nothing;
}
return html`
<ha-card
.header=${this.hass.localize(
"ui.panel.lovelace.cards.empty_state.title"
)}
class=${classMap({
"content-only": this._config.content_only ?? false,
})}
>
<div class="card-content">
${this.hass.localize(
"ui.panel.lovelace.cards.empty_state.no_devices"
)}
</div>
<div class="card-actions">
<ha-button appearance="plain" href="/config/integrations/dashboard">
${this.hass.localize(
"ui.panel.lovelace.cards.empty_state.go_to_integrations_page"
)}
</ha-button>
<div class="container">
${this._config.icon
? html`<ha-icon .icon=${this._config.icon}></ha-icon>`
: nothing}
${this._config.title ? html`<h1>${this._config.title}</h1>` : nothing}
${this._config.content
? html`<p>${this._config.content}</p>`
: nothing}
${this._config.tap_action && this._config.action_label
? html`
<ha-button @click=${this._handleAction}>
${this._config.action_label}
</ha-button>
`
: nothing}
</div>
</ha-card>
`;
}
private _handleAction(): void {
if (this._config?.tap_action && this.hass) {
handleAction(this, this.hass, this._config, "tap");
}
}
static styles = css`
.content {
margin-top: -1em;
padding: 16px;
:host {
display: block;
height: 100%;
}
.card-actions a {
text-decoration: none;
ha-card {
height: 100%;
}
ha-button {
margin-left: -8px;
margin-inline-start: -8px;
margin-inline-end: initial;
.container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
height: 100%;
padding: var(--ha-space-8) var(--ha-space-4);
box-sizing: border-box;
gap: var(--ha-space-4);
max-width: 640px;
margin: 0 auto;
}
ha-icon {
--mdc-icon-size: var(--ha-space-12);
color: var(--secondary-text-color);
}
h1 {
margin: 0;
font-size: var(--ha-font-size-xl);
font-weight: 500;
}
p {
margin: 0;
color: var(--secondary-text-color);
}
.content-only {
background: none;
box-shadow: none;
border: none;
}
`;
}

View File

@ -58,8 +58,12 @@ export interface ConditionalCardConfig extends LovelaceCardConfig {
}
export interface EmptyStateCardConfig extends LovelaceCardConfig {
content: string;
content_only?: boolean;
icon?: string;
title?: string;
content?: string;
action_label?: string;
tap_action?: ActionConfig;
}
export interface EntityCardConfig extends LovelaceCardConfig {

View File

@ -0,0 +1,153 @@
import { mdiGestureTap } from "@mdi/js";
import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { assert, assign, boolean, object, optional, string } from "superstruct";
import { fireEvent } from "../../../../common/dom/fire_event";
import type { LocalizeFunc } from "../../../../common/translations/localize";
import "../../../../components/ha-form/ha-form";
import type {
HaFormSchema,
SchemaUnion,
} from "../../../../components/ha-form/types";
import type { HomeAssistant } from "../../../../types";
import type { EmptyStateCardConfig } from "../../cards/types";
import type { LovelaceCardEditor } from "../../types";
import { actionConfigStruct } from "../structs/action-struct";
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
const cardConfigStruct = assign(
baseLovelaceCardConfig,
object({
content_only: optional(boolean()),
icon: optional(string()),
title: optional(string()),
content: optional(string()),
action_label: optional(string()),
tap_action: optional(actionConfigStruct),
})
);
@customElement("hui-empty-state-card-editor")
export class HuiEmptyStateCardEditor
extends LitElement
implements LovelaceCardEditor
{
@property({ attribute: false }) public hass?: HomeAssistant;
@state() private _config?: EmptyStateCardConfig;
public setConfig(config: EmptyStateCardConfig): void {
assert(config, cardConfigStruct);
this._config = config;
}
private _schema = memoizeOne(
(localize: LocalizeFunc) =>
[
{
name: "style",
selector: {
select: {
mode: "box",
options: (
[
{ value: "card", image: "card" },
{ value: "content-only", image: "text_only" },
] as const
).map((style) => ({
label: localize(
`ui.panel.lovelace.editor.card.empty_state.style_options.${style.value}`
),
image: {
src: `/static/images/form/markdown_${style.image}.svg`,
src_dark: `/static/images/form/markdown_${style.image}_dark.svg`,
flip_rtl: true,
},
value: style.value,
})),
},
},
},
{ name: "icon", selector: { icon: {} } },
{ name: "title", selector: { text: {} } },
{ name: "content", selector: { text: { multiline: true } } },
{
name: "interactions",
type: "expandable",
flatten: true,
iconPath: mdiGestureTap,
schema: [
{ name: "action_label", selector: { text: {} } },
{
name: "tap_action",
selector: {
ui_action: {
default_action: "none",
},
},
},
],
},
] as const satisfies readonly HaFormSchema[]
);
protected render() {
if (!this.hass || !this._config) {
return nothing;
}
const data = {
...this._config,
style: this._config.content_only ? "content-only" : "card",
};
const schema = this._schema(this.hass.localize);
return html`
<ha-form
.hass=${this.hass}
.data=${data}
.schema=${schema}
.computeLabel=${this._computeLabelCallback}
@value-changed=${this._valueChanged}
></ha-form>
`;
}
private _valueChanged(ev: CustomEvent): void {
const config = { ...ev.detail.value };
if (config.style === "content-only") {
config.content_only = true;
} else {
delete config.content_only;
}
delete config.style;
fireEvent(this, "config-changed", { config });
}
private _computeLabelCallback = (
schema: SchemaUnion<ReturnType<typeof this._schema>>
) => {
switch (schema.name) {
case "style":
case "content":
case "action_label":
return this.hass!.localize(
`ui.panel.lovelace.editor.card.empty_state.${schema.name}`
);
default:
return this.hass!.localize(
`ui.panel.lovelace.editor.card.generic.${schema.name}`
);
}
};
}
declare global {
interface HTMLElementTagNameMap {
"hui-empty-state-card-editor": HuiEmptyStateCardEditor;
}
}

View File

@ -11,7 +11,10 @@ import type { LovelaceBadgeConfig } from "../../../../data/lovelace/config/badge
import type { LovelaceSectionRawConfig } from "../../../../data/lovelace/config/section";
import type { LovelaceViewConfig } from "../../../../data/lovelace/config/view";
import type { HomeAssistant } from "../../../../types";
import type { HeadingCardConfig } from "../../cards/types";
import type {
EmptyStateCardConfig,
HeadingCardConfig,
} from "../../cards/types";
import { computeAreaTileCardConfig } from "../areas/helpers/areas-strategy-helper";
import {
getSummaryLabel,
@ -354,6 +357,26 @@ export class HomeAreaViewStrategy extends ReactiveElement {
});
}
// No sections, show empty state
if (sections.length === 0) {
return {
type: "panel",
cards: [
{
type: "empty-state",
icon: "mdi:sofa-outline",
content_only: true,
title: hass.localize(
"ui.panel.lovelace.strategy.areas.empty_state_title"
),
content: hass.localize(
"ui.panel.lovelace.strategy.areas.empty_state_content"
),
} as EmptyStateCardConfig,
],
};
}
// Allow between 2 and 3 columns (the max should be set to define the width of the header)
const maxColumns = clamp(sections.length, 2, 3);

View File

@ -6,6 +6,7 @@ import type { AreasDisplayValue } from "../../../../components/ha-areas-display-
import { getEnergyPreferences } from "../../../../data/energy";
import type { LovelaceViewConfig } from "../../../../data/lovelace/config/view";
import type { HomeAssistant } from "../../../../types";
import type { EmptyStateCardConfig } from "../../cards/types";
import { generateDefaultViewConfig } from "../../common/generate-lovelace-config";
export interface OriginalStatesViewStrategyConfig {
@ -64,9 +65,33 @@ export class OriginalStatesViewStrategy extends ReactiveElement {
// User has no entities
if (view.cards!.length === 0) {
view.cards!.push({
type: "empty-state",
});
return {
type: "panel",
cards: [
{
type: "empty-state",
icon: "mdi:home-assistant",
content_only: true,
title: hass.localize(
"ui.panel.lovelace.strategy.original-states.empty_state_title"
),
content: hass.localize(
"ui.panel.lovelace.strategy.original-states.empty_state_content"
),
...(hass.user?.is_admin
? {
action_label: hass.localize(
"ui.panel.lovelace.strategy.original-states.empty_state_action"
),
tap_action: {
action: "navigate",
navigation_path: "/config/integrations/dashboard",
},
}
: {}),
} as EmptyStateCardConfig,
],
};
}
return view;

View File

@ -7233,10 +7233,14 @@
"lovelace": {
"strategy": {
"original-states": {
"helpers": "[%key:ui::panel::config::helpers::caption%]"
"helpers": "[%key:ui::panel::config::helpers::caption%]",
"empty_state_title": "Welcome Home",
"empty_state_content": "This page allows you to control your devices, however it looks like you have no devices set up yet.",
"empty_state_action": "Go to the integrations page"
},
"areas": {
"no_entities": "No entities in this area.",
"empty_state_title": "No devices",
"empty_state_content": "There are no devices assigned to this area yet. Assign devices to this area to see them here.",
"sensors": "Sensors",
"sensors_description": "To display temperature and humidity sensors in the overview and in the area view, add a sensor to that area and {edit_the_area} to configure related sensors.",
"edit_the_area": "edit the area",
@ -7308,11 +7312,6 @@
"no_url": "No URL to open specified",
"no_action": "No action to run specified"
},
"empty_state": {
"title": "Welcome Home",
"no_devices": "This page allows you to control your devices, however it looks like you have no devices set up yet. Head to the integrations page to get started.",
"go_to_integrations_page": "Go to the integrations page."
},
"entities": {
"never_triggered": "Never triggered"
},
@ -7984,6 +7983,17 @@
"name": "Entity",
"description": "The Entity card gives you a quick overview of your entity's state."
},
"empty_state": {
"name": "Empty state",
"description": "The Empty state card displays a centered message with an optional icon and action button.",
"style": "Style",
"style_options": {
"card": "Card",
"content-only": "Content only"
},
"content": "Content",
"action_label": "Action label"
},
"button": {
"name": "Button",
"description": "The Button card allows you to add buttons to perform tasks.",