mirror of
https://github.com/home-assistant/frontend.git
synced 2026-02-15 07:25:54 +00:00
Add empty state to Home panel strategies (#29113)
This commit is contained in:
@@ -15,12 +15,13 @@ import {
|
||||
import type { LovelaceDashboardStrategyConfig } from "../../data/lovelace/config/types";
|
||||
import type { HomeAssistant, PanelInfo, Route } from "../../types";
|
||||
import { showToast } from "../../util/toast";
|
||||
import { showAreaRegistryDetailDialog } from "../config/areas/show-dialog-area-registry-detail";
|
||||
import { showDeviceRegistryDetailDialog } from "../config/devices/device-registry-detail/show-dialog-device-registry-detail";
|
||||
import { showAddIntegrationDialog } from "../config/integrations/show-add-integration-dialog";
|
||||
import "../lovelace/hui-root";
|
||||
import type { ExtraActionItem } from "../lovelace/hui-root";
|
||||
import { expandLovelaceConfigStrategies } from "../lovelace/strategies/get-strategy";
|
||||
import type { Lovelace } from "../lovelace/types";
|
||||
import { showAreaRegistryDetailDialog } from "../config/areas/show-dialog-area-registry-detail";
|
||||
import { showDeviceRegistryDetailDialog } from "../config/devices/device-registry-detail/show-dialog-device-registry-detail";
|
||||
import { showEditHomeDialog } from "./dialogs/show-dialog-edit-home";
|
||||
|
||||
@customElement("ha-panel-home")
|
||||
@@ -172,13 +173,26 @@ class PanelHome extends LitElement {
|
||||
private _handleLLCustomEvent = (ev: Event) => {
|
||||
const detail = (ev as CustomEvent).detail;
|
||||
if (detail.home_panel) {
|
||||
const { type, device_id } = detail.home_panel;
|
||||
if (type === "assign_area") {
|
||||
this._showAssignAreaDialog(device_id);
|
||||
const { type } = detail.home_panel;
|
||||
switch (type) {
|
||||
case "assign_area": {
|
||||
const { device_id } = detail.home_panel;
|
||||
this._showAssignAreaDialog(device_id);
|
||||
break;
|
||||
}
|
||||
case "add_integration": {
|
||||
this._showAddIntegrationDialog();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private async _showAddIntegrationDialog() {
|
||||
await this.hass.loadFragmentTranslation("config");
|
||||
showAddIntegrationDialog(this, { navigateToResult: false });
|
||||
}
|
||||
|
||||
private _showAssignAreaDialog(deviceId: string) {
|
||||
const device = this.hass.devices[deviceId];
|
||||
if (!device) {
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { computeCssColor } from "../../../common/color/compute-color";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-icon";
|
||||
@@ -49,17 +52,44 @@ export class HuiEmptyStateCard extends LitElement implements LovelaceCard {
|
||||
>
|
||||
<div class="container">
|
||||
${this._config.icon
|
||||
? html`<ha-icon .icon=${this._config.icon}></ha-icon>`
|
||||
? html`
|
||||
<ha-icon
|
||||
class="card-icon"
|
||||
.icon=${this._config.icon}
|
||||
style=${styleMap({
|
||||
color: this._config.icon_color
|
||||
? computeCssColor(this._config.icon_color)
|
||||
: undefined,
|
||||
})}
|
||||
></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_button_text
|
||||
${this._config.buttons?.length
|
||||
? html`
|
||||
<ha-button @click=${this._handleAction}>
|
||||
${this._config.action_button_text}
|
||||
</ha-button>
|
||||
<div class="buttons">
|
||||
${this._config.buttons.map(
|
||||
(button, index) => html`
|
||||
<ha-button
|
||||
.index=${index}
|
||||
@click=${this._handleButtonAction}
|
||||
appearance=${ifDefined(button.appearance)}
|
||||
variant=${ifDefined(button.variant)}
|
||||
>
|
||||
${button.icon
|
||||
? html`<ha-icon
|
||||
slot="start"
|
||||
.icon=${button.icon}
|
||||
></ha-icon>`
|
||||
: nothing}
|
||||
${button.text}
|
||||
</ha-button>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
@@ -67,9 +97,11 @@ export class HuiEmptyStateCard extends LitElement implements LovelaceCard {
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleAction(): void {
|
||||
if (this._config?.tap_action && this.hass) {
|
||||
handleAction(this, this.hass, this._config, "tap");
|
||||
private _handleButtonAction(ev: Event): void {
|
||||
const index = (ev.currentTarget as any).index;
|
||||
const button = this._config?.buttons?.[index];
|
||||
if (this.hass && button) {
|
||||
handleAction(this, this.hass, { tap_action: button.tap_action }, "tap");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,8 +126,8 @@ export class HuiEmptyStateCard extends LitElement implements LovelaceCard {
|
||||
max-width: 640px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
ha-icon {
|
||||
--mdc-icon-size: var(--ha-space-12);
|
||||
.card-icon {
|
||||
--mdc-icon-size: var(--ha-space-16);
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
h1 {
|
||||
@@ -107,6 +139,12 @@ export class HuiEmptyStateCard extends LitElement implements LovelaceCard {
|
||||
margin: 0;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
.buttons {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: var(--ha-space-2);
|
||||
}
|
||||
.content-only {
|
||||
background: none;
|
||||
box-shadow: none;
|
||||
|
||||
@@ -57,13 +57,21 @@ export interface ConditionalCardConfig extends LovelaceCardConfig {
|
||||
conditions: (Condition | LegacyCondition)[];
|
||||
}
|
||||
|
||||
export interface EmptyStateButtonConfig {
|
||||
text: string;
|
||||
icon?: string;
|
||||
appearance?: "accent" | "filled" | "outlined" | "plain";
|
||||
variant?: "brand" | "neutral" | "success" | "warning" | "danger";
|
||||
tap_action: ActionConfig;
|
||||
}
|
||||
|
||||
export interface EmptyStateCardConfig extends LovelaceCardConfig {
|
||||
content_only?: boolean;
|
||||
icon?: string;
|
||||
icon_color?: string;
|
||||
title?: string;
|
||||
content?: string;
|
||||
action_button_text?: string;
|
||||
tap_action?: ActionConfig;
|
||||
buttons?: EmptyStateButtonConfig[];
|
||||
}
|
||||
|
||||
export interface EntityCardConfig extends LovelaceCardConfig {
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
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 {
|
||||
array,
|
||||
assert,
|
||||
assign,
|
||||
boolean,
|
||||
enums,
|
||||
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";
|
||||
@@ -16,15 +24,25 @@ import type { LovelaceCardEditor } from "../../types";
|
||||
import { actionConfigStruct } from "../structs/action-struct";
|
||||
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
|
||||
|
||||
const buttonStruct = object({
|
||||
text: string(),
|
||||
icon: optional(string()),
|
||||
appearance: optional(enums(["accent", "filled", "outlined", "plain"])),
|
||||
variant: optional(
|
||||
enums(["brand", "neutral", "success", "warning", "danger"])
|
||||
),
|
||||
tap_action: actionConfigStruct,
|
||||
});
|
||||
|
||||
const cardConfigStruct = assign(
|
||||
baseLovelaceCardConfig,
|
||||
object({
|
||||
content_only: optional(boolean()),
|
||||
icon: optional(string()),
|
||||
icon_color: optional(string()),
|
||||
title: optional(string()),
|
||||
content: optional(string()),
|
||||
action_button_text: optional(string()),
|
||||
tap_action: optional(actionConfigStruct),
|
||||
buttons: optional(array(buttonStruct)),
|
||||
})
|
||||
);
|
||||
|
||||
@@ -70,24 +88,66 @@ export class HuiEmptyStateCardEditor
|
||||
},
|
||||
},
|
||||
{ name: "icon", selector: { icon: {} } },
|
||||
{
|
||||
name: "icon_color",
|
||||
selector: {
|
||||
ui_color: {},
|
||||
},
|
||||
},
|
||||
{ name: "title", selector: { text: {} } },
|
||||
{ name: "content", selector: { text: { multiline: true } } },
|
||||
{
|
||||
name: "interactions",
|
||||
type: "expandable",
|
||||
flatten: true,
|
||||
iconPath: mdiGestureTap,
|
||||
schema: [
|
||||
{ name: "action_button_text", selector: { text: {} } },
|
||||
{
|
||||
name: "tap_action",
|
||||
selector: {
|
||||
ui_action: {
|
||||
default_action: "none",
|
||||
name: "buttons",
|
||||
selector: {
|
||||
object: {
|
||||
multiple: true,
|
||||
label_field: "text",
|
||||
fields: {
|
||||
text: {
|
||||
selector: { text: {} },
|
||||
required: true,
|
||||
},
|
||||
icon: {
|
||||
selector: { icon: {} },
|
||||
},
|
||||
appearance: {
|
||||
selector: {
|
||||
select: {
|
||||
options: [
|
||||
{ value: "accent", label: "Accent" },
|
||||
{ value: "filled", label: "Filled" },
|
||||
{ value: "outlined", label: "Outlined" },
|
||||
{ value: "plain", label: "Plain" },
|
||||
],
|
||||
mode: "dropdown",
|
||||
},
|
||||
},
|
||||
},
|
||||
variant: {
|
||||
selector: {
|
||||
select: {
|
||||
options: [
|
||||
{ value: "brand", label: "Brand" },
|
||||
{ value: "neutral", label: "Neutral" },
|
||||
{ value: "success", label: "Success" },
|
||||
{ value: "warning", label: "Warning" },
|
||||
{ value: "danger", label: "Danger" },
|
||||
],
|
||||
mode: "dropdown",
|
||||
},
|
||||
},
|
||||
},
|
||||
tap_action: {
|
||||
selector: {
|
||||
ui_action: {
|
||||
default_action: "none",
|
||||
},
|
||||
},
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
] as const satisfies readonly HaFormSchema[]
|
||||
);
|
||||
@@ -134,7 +194,7 @@ export class HuiEmptyStateCardEditor
|
||||
switch (schema.name) {
|
||||
case "style":
|
||||
case "content":
|
||||
case "action_button_text":
|
||||
case "buttons":
|
||||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.empty_state.${schema.name}`
|
||||
);
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
export interface HomeAreaViewStrategyConfig {
|
||||
type: "home-area";
|
||||
area?: string;
|
||||
home_panel?: boolean;
|
||||
}
|
||||
|
||||
@customElement("home-area-view-strategy")
|
||||
@@ -365,13 +366,46 @@ export class HomeAreaViewStrategy extends ReactiveElement {
|
||||
{
|
||||
type: "empty-state",
|
||||
icon: "mdi:sofa-outline",
|
||||
icon_color: "primary",
|
||||
content_only: true,
|
||||
title: hass.localize(
|
||||
"ui.panel.lovelace.strategy.areas.empty_state_title"
|
||||
"ui.panel.lovelace.strategy.home-area.no_devices_title"
|
||||
),
|
||||
content: hass.localize(
|
||||
"ui.panel.lovelace.strategy.areas.empty_state_content"
|
||||
"ui.panel.lovelace.strategy.home-area.no_devices_content"
|
||||
),
|
||||
...(config.home_panel && hass.user?.is_admin
|
||||
? {
|
||||
buttons: [
|
||||
{
|
||||
icon: "mdi:plus",
|
||||
text: hass.localize(
|
||||
"ui.panel.lovelace.strategy.home-area.no_devices_add_device"
|
||||
),
|
||||
appearance: "plain",
|
||||
variant: "brand",
|
||||
tap_action: {
|
||||
action: "fire-dom-event",
|
||||
home_panel: {
|
||||
type: "add_integration",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: "mdi:home-plus",
|
||||
text: hass.localize(
|
||||
"ui.panel.lovelace.strategy.home-area.no_devices_assign_device"
|
||||
),
|
||||
appearance: "plain",
|
||||
variant: "brand",
|
||||
tap_action: {
|
||||
action: "navigate",
|
||||
navigation_path: "/home/other-devices",
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
: {}),
|
||||
} as EmptyStateCardConfig,
|
||||
],
|
||||
};
|
||||
|
||||
@@ -59,6 +59,7 @@ export class HomeDashboardStrategy extends ReactiveElement {
|
||||
strategy: {
|
||||
type: "home-area",
|
||||
area: area.area_id,
|
||||
home_panel: config.home_panel,
|
||||
} satisfies HomeAreaViewStrategyConfig,
|
||||
};
|
||||
});
|
||||
@@ -92,6 +93,7 @@ export class HomeDashboardStrategy extends ReactiveElement {
|
||||
strategy: {
|
||||
type: "home-overview",
|
||||
favorite_entities: config.favorite_entities,
|
||||
home_panel: config.home_panel,
|
||||
} satisfies HomeOverviewViewStrategyConfig,
|
||||
},
|
||||
...areaViews,
|
||||
|
||||
@@ -142,7 +142,7 @@ export class HomeOtherDevicesViewStrategy extends ReactiveElement {
|
||||
type: "button",
|
||||
icon: "mdi:home-plus",
|
||||
text: hass.localize(
|
||||
"ui.panel.lovelace.strategy.other_devices.assign_area"
|
||||
"ui.panel.lovelace.strategy.home-other-devices.assign_area"
|
||||
),
|
||||
tap_action: {
|
||||
action: "fire-dom-event",
|
||||
@@ -178,7 +178,7 @@ export class HomeOtherDevicesViewStrategy extends ReactiveElement {
|
||||
{
|
||||
type: "heading",
|
||||
heading: hass.localize(
|
||||
"ui.panel.lovelace.strategy.other_devices.helpers"
|
||||
"ui.panel.lovelace.strategy.home-other-devices.helpers"
|
||||
),
|
||||
} satisfies HeadingCardConfig,
|
||||
...helpersEntities.map((e) => ({
|
||||
@@ -197,7 +197,7 @@ export class HomeOtherDevicesViewStrategy extends ReactiveElement {
|
||||
{
|
||||
type: "heading",
|
||||
heading: hass.localize(
|
||||
"ui.panel.lovelace.strategy.other_devices.entities"
|
||||
"ui.panel.lovelace.strategy.home-other-devices.entities"
|
||||
),
|
||||
} satisfies HeadingCardConfig,
|
||||
...otherEntities.map((e) => ({
|
||||
@@ -216,12 +216,13 @@ export class HomeOtherDevicesViewStrategy extends ReactiveElement {
|
||||
{
|
||||
type: "empty-state",
|
||||
icon: "mdi:check-all",
|
||||
icon_color: "primary",
|
||||
content_only: true,
|
||||
title: hass.localize(
|
||||
"ui.panel.lovelace.strategy.other_devices.empty_state_title"
|
||||
"ui.panel.lovelace.strategy.home-other-devices.all_organized_title"
|
||||
),
|
||||
content: hass.localize(
|
||||
"ui.panel.lovelace.strategy.other_devices.empty_state_content"
|
||||
"ui.panel.lovelace.strategy.home-other-devices.all_organized_content"
|
||||
),
|
||||
} as EmptyStateCardConfig,
|
||||
],
|
||||
|
||||
@@ -20,6 +20,7 @@ import type { HomeAssistant } from "../../../../types";
|
||||
import type {
|
||||
AreaCardConfig,
|
||||
DiscoveredDevicesCardConfig,
|
||||
EmptyStateCardConfig,
|
||||
HomeSummaryCard,
|
||||
MarkdownCardConfig,
|
||||
TileCardConfig,
|
||||
@@ -32,6 +33,7 @@ import { OTHER_DEVICES_FILTERS } from "./helpers/other-devices-filters";
|
||||
export interface HomeOverviewViewStrategyConfig {
|
||||
type: "home-overview";
|
||||
favorite_entities?: string[];
|
||||
home_panel?: boolean;
|
||||
}
|
||||
|
||||
const computeAreaCard = (
|
||||
@@ -341,6 +343,59 @@ export class HomeOverviewViewStrategy extends ReactiveElement {
|
||||
}
|
||||
: undefined;
|
||||
|
||||
// No sections, show empty state
|
||||
if (floorsSections.length === 0) {
|
||||
return {
|
||||
type: "panel",
|
||||
cards: [
|
||||
{
|
||||
type: "empty-state",
|
||||
icon: "mdi:home-assistant",
|
||||
icon_color: "primary",
|
||||
content_only: true,
|
||||
title: hass.localize(
|
||||
"ui.panel.lovelace.strategy.home.welcome_title"
|
||||
),
|
||||
content: hass.localize(
|
||||
"ui.panel.lovelace.strategy.home.welcome_content"
|
||||
),
|
||||
...(config.home_panel && hass.user?.is_admin
|
||||
? {
|
||||
buttons: [
|
||||
{
|
||||
icon: "mdi:plus",
|
||||
text: hass.localize(
|
||||
"ui.panel.lovelace.strategy.home.welcome_add_device"
|
||||
),
|
||||
appearance: "filled",
|
||||
variant: "brand",
|
||||
tap_action: {
|
||||
action: "fire-dom-event",
|
||||
home_panel: {
|
||||
type: "add_integration",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: "mdi:home-edit",
|
||||
text: hass.localize(
|
||||
"ui.panel.lovelace.strategy.home.welcome_edit_areas"
|
||||
),
|
||||
appearance: "plain",
|
||||
variant: "brand",
|
||||
tap_action: {
|
||||
action: "navigate",
|
||||
navigation_path: "/config/areas/dashboard",
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
: {}),
|
||||
} as EmptyStateCardConfig,
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
const sections = (
|
||||
[favoritesSection, mobileSummarySection, ...floorsSections] satisfies (
|
||||
| LovelaceSectionRawConfig
|
||||
|
||||
@@ -71,6 +71,7 @@ export class OriginalStatesViewStrategy extends ReactiveElement {
|
||||
{
|
||||
type: "empty-state",
|
||||
icon: "mdi:home-assistant",
|
||||
icon_color: "primary",
|
||||
content_only: true,
|
||||
title: hass.localize(
|
||||
"ui.panel.lovelace.strategy.original-states.empty_state_title"
|
||||
@@ -80,13 +81,19 @@ export class OriginalStatesViewStrategy extends ReactiveElement {
|
||||
),
|
||||
...(hass.user?.is_admin
|
||||
? {
|
||||
action_button_text: hass.localize(
|
||||
"ui.panel.lovelace.strategy.original-states.empty_state_action"
|
||||
),
|
||||
tap_action: {
|
||||
action: "navigate",
|
||||
navigation_path: "/config/integrations/dashboard",
|
||||
},
|
||||
buttons: [
|
||||
{
|
||||
text: hass.localize(
|
||||
"ui.panel.lovelace.strategy.original-states.empty_state_action"
|
||||
),
|
||||
appearance: "filled",
|
||||
variant: "brand",
|
||||
tap_action: {
|
||||
action: "navigate",
|
||||
navigation_path: "/config/integrations/dashboard",
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
: {}),
|
||||
} as EmptyStateCardConfig,
|
||||
|
||||
@@ -7524,8 +7524,6 @@
|
||||
"empty_state_action": "Go to the integrations page"
|
||||
},
|
||||
"areas": {
|
||||
"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",
|
||||
@@ -7559,7 +7557,11 @@
|
||||
"automations": "Automations",
|
||||
"for_you": "For you",
|
||||
"home": "Home",
|
||||
"favorites": "Favorites"
|
||||
"favorites": "Favorites",
|
||||
"welcome_title": "No devices here yet",
|
||||
"welcome_content": "Add lights, switches, sensors, or other smart home devices to get started.",
|
||||
"welcome_add_device": "Add new device",
|
||||
"welcome_edit_areas": "Edit areas"
|
||||
},
|
||||
"common_controls": {
|
||||
"not_loaded": "Usage Prediction integration is not loaded.",
|
||||
@@ -7581,12 +7583,18 @@
|
||||
"media_players": "Media players",
|
||||
"other_media_players": "Other media players"
|
||||
},
|
||||
"other_devices": {
|
||||
"home-other-devices": {
|
||||
"helpers": "Helpers",
|
||||
"entities": "Entities",
|
||||
"assign_area": "Assign area",
|
||||
"empty_state_title": "All devices are organized",
|
||||
"empty_state_content": "There are no unassigned devices left. All devices are organized into areas."
|
||||
"all_organized_title": "All devices are organized",
|
||||
"all_organized_content": "There are no unassigned devices left. All devices are organized into areas."
|
||||
},
|
||||
"home-area": {
|
||||
"no_devices_title": "This is a blank canvas",
|
||||
"no_devices_content": "Add your smart lights, switches, or sensors to this area to get started.",
|
||||
"no_devices_add_device": "Add new device",
|
||||
"no_devices_assign_device": "Assign existing device"
|
||||
}
|
||||
},
|
||||
"cards": {
|
||||
@@ -8283,14 +8291,14 @@
|
||||
},
|
||||
"empty_state": {
|
||||
"name": "Empty state",
|
||||
"description": "The Empty state card displays a centered message with an optional icon and action button.",
|
||||
"description": "The Empty state card displays a centered message with an optional icon and action buttons.",
|
||||
"style": "Style",
|
||||
"style_options": {
|
||||
"card": "Card",
|
||||
"content-only": "Content only"
|
||||
},
|
||||
"content": "Content",
|
||||
"action_text": "Action button text"
|
||||
"buttons": "Buttons"
|
||||
},
|
||||
"button": {
|
||||
"name": "Button",
|
||||
|
||||
Reference in New Issue
Block a user