mirror of
https://github.com/home-assistant/frontend.git
synced 2026-04-21 18:13:04 +00:00
Compare commits
1 Commits
fix-legacy
...
card_picke
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5e53dbfc16 |
@@ -440,10 +440,13 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
|
||||
}
|
||||
|
||||
wa-popover::part(body) {
|
||||
width: max(var(--body-width), 250px);
|
||||
width: max(
|
||||
var(--body-width),
|
||||
var(--ha-generic-picker-min-width, 250px)
|
||||
);
|
||||
max-width: var(
|
||||
--ha-generic-picker-max-width,
|
||||
max(var(--body-width), 250px)
|
||||
max(var(--body-width), var(--ha-generic-picker-min-width, 250px))
|
||||
);
|
||||
max-height: 500px;
|
||||
height: 70vh;
|
||||
|
||||
168
src/panels/lovelace/card-features/registry.ts
Normal file
168
src/panels/lovelace/card-features/registry.ts
Normal file
@@ -0,0 +1,168 @@
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { supportsAlarmModesCardFeature } from "./hui-alarm-modes-card-feature";
|
||||
import { supportsAreaControlsCardFeature } from "./hui-area-controls-card-feature";
|
||||
import { supportsBarGaugeCardFeature } from "./hui-bar-gauge-card-feature";
|
||||
import { supportsButtonCardFeature } from "./hui-button-card-feature";
|
||||
import { supportsClimateFanModesCardFeature } from "./hui-climate-fan-modes-card-feature";
|
||||
import { supportsClimateHvacModesCardFeature } from "./hui-climate-hvac-modes-card-feature";
|
||||
import { supportsClimatePresetModesCardFeature } from "./hui-climate-preset-modes-card-feature";
|
||||
import { supportsClimateSwingHorizontalModesCardFeature } from "./hui-climate-swing-horizontal-modes-card-feature";
|
||||
import { supportsClimateSwingModesCardFeature } from "./hui-climate-swing-modes-card-feature";
|
||||
import { supportsCounterActionsCardFeature } from "./hui-counter-actions-card-feature";
|
||||
import { supportsCoverOpenCloseCardFeature } from "./hui-cover-open-close-card-feature";
|
||||
import { supportsCoverPositionFavoriteCardFeature } from "./hui-cover-position-favorite-card-feature";
|
||||
import { supportsCoverPositionCardFeature } from "./hui-cover-position-card-feature";
|
||||
import { supportsCoverTiltCardFeature } from "./hui-cover-tilt-card-feature";
|
||||
import { supportsCoverTiltFavoriteCardFeature } from "./hui-cover-tilt-favorite-card-feature";
|
||||
import { supportsCoverTiltPositionCardFeature } from "./hui-cover-tilt-position-card-feature";
|
||||
import { supportsDateSetCardFeature } from "./hui-date-set-card-feature";
|
||||
import { supportsFanDirectionCardFeature } from "./hui-fan-direction-card-feature";
|
||||
import { supportsFanOscilatteCardFeature } from "./hui-fan-oscillate-card-feature";
|
||||
import { supportsFanPresetModesCardFeature } from "./hui-fan-preset-modes-card-feature";
|
||||
import { supportsFanSpeedCardFeature } from "./hui-fan-speed-card-feature";
|
||||
import { supportsHourlyForecastCardFeature } from "./hui-hourly-forecast-card-feature";
|
||||
import { supportsHumidifierModesCardFeature } from "./hui-humidifier-modes-card-feature";
|
||||
import { supportsHumidifierToggleCardFeature } from "./hui-humidifier-toggle-card-feature";
|
||||
import { supportsLawnMowerCommandCardFeature } from "./hui-lawn-mower-commands-card-feature";
|
||||
import { supportsLightBrightnessCardFeature } from "./hui-light-brightness-card-feature";
|
||||
import { supportsLightColorFavoritesCardFeature } from "./hui-light-color-favorites-card-feature";
|
||||
import { supportsLightColorTempCardFeature } from "./hui-light-color-temp-card-feature";
|
||||
import { supportsLockCommandsCardFeature } from "./hui-lock-commands-card-feature";
|
||||
import { supportsLockOpenDoorCardFeature } from "./hui-lock-open-door-card-feature";
|
||||
import { supportsMediaPlayerPlaybackCardFeature } from "./hui-media-player-playback-card-feature";
|
||||
import { supportsMediaPlayerSoundModeCardFeature } from "./hui-media-player-sound-mode-card-feature";
|
||||
import { supportsMediaPlayerSourceCardFeature } from "./hui-media-player-source-card-feature";
|
||||
import { supportsMediaPlayerVolumeButtonsCardFeature } from "./hui-media-player-volume-buttons-card-feature";
|
||||
import { supportsMediaPlayerVolumeSliderCardFeature } from "./hui-media-player-volume-slider-card-feature";
|
||||
import { supportsNumericInputCardFeature } from "./hui-numeric-input-card-feature";
|
||||
import { supportsSelectOptionsCardFeature } from "./hui-select-options-card-feature";
|
||||
import { supportsTargetHumidityCardFeature } from "./hui-target-humidity-card-feature";
|
||||
import { supportsTargetTemperatureCardFeature } from "./hui-target-temperature-card-feature";
|
||||
import { supportsToggleCardFeature } from "./hui-toggle-card-feature";
|
||||
import { supportsTrendGraphCardFeature } from "./hui-trend-graph-card-feature";
|
||||
import { supportsUpdateActionsCardFeature } from "./hui-update-actions-card-feature";
|
||||
import { supportsVacuumCommandsCardFeature } from "./hui-vacuum-commands-card-feature";
|
||||
import { supportsValveOpenCloseCardFeature } from "./hui-valve-open-close-card-feature";
|
||||
import { supportsValvePositionFavoriteCardFeature } from "./hui-valve-position-favorite-card-feature";
|
||||
import { supportsValvePositionCardFeature } from "./hui-valve-position-card-feature";
|
||||
import { supportsWaterHeaterOperationModesCardFeature } from "./hui-water-heater-operation-modes-card-feature";
|
||||
import type {
|
||||
LovelaceCardFeatureConfig,
|
||||
LovelaceCardFeatureContext,
|
||||
} from "./types";
|
||||
|
||||
export type FeatureType = LovelaceCardFeatureConfig["type"];
|
||||
|
||||
export type SupportsFeature = (
|
||||
hass: HomeAssistant,
|
||||
context: LovelaceCardFeatureContext
|
||||
) => boolean;
|
||||
|
||||
export const UI_FEATURE_TYPES = [
|
||||
"alarm-modes",
|
||||
"area-controls",
|
||||
"bar-gauge",
|
||||
"button",
|
||||
"climate-fan-modes",
|
||||
"climate-hvac-modes",
|
||||
"climate-preset-modes",
|
||||
"climate-swing-modes",
|
||||
"climate-swing-horizontal-modes",
|
||||
"counter-actions",
|
||||
"cover-open-close",
|
||||
"cover-position-favorite",
|
||||
"cover-position",
|
||||
"cover-tilt-favorite",
|
||||
"cover-tilt-position",
|
||||
"cover-tilt",
|
||||
"date-set",
|
||||
"fan-direction",
|
||||
"fan-oscillate",
|
||||
"fan-preset-modes",
|
||||
"fan-speed",
|
||||
"hourly-forecast",
|
||||
"humidifier-modes",
|
||||
"humidifier-toggle",
|
||||
"lawn-mower-commands",
|
||||
"light-brightness",
|
||||
"light-color-temp",
|
||||
"light-color-favorites",
|
||||
"lock-commands",
|
||||
"lock-open-door",
|
||||
"media-player-playback",
|
||||
"media-player-sound-mode",
|
||||
"media-player-source",
|
||||
"media-player-volume-buttons",
|
||||
"media-player-volume-slider",
|
||||
"numeric-input",
|
||||
"select-options",
|
||||
"trend-graph",
|
||||
"target-humidity",
|
||||
"target-temperature",
|
||||
"toggle",
|
||||
"update-actions",
|
||||
"vacuum-commands",
|
||||
"valve-open-close",
|
||||
"valve-position-favorite",
|
||||
"valve-position",
|
||||
"water-heater-operation-modes",
|
||||
] as const satisfies readonly FeatureType[];
|
||||
|
||||
export type UiFeatureType = (typeof UI_FEATURE_TYPES)[number];
|
||||
|
||||
export const SUPPORTS_FEATURE_TYPES: Record<UiFeatureType, SupportsFeature> = {
|
||||
"alarm-modes": supportsAlarmModesCardFeature,
|
||||
"area-controls": supportsAreaControlsCardFeature,
|
||||
"bar-gauge": supportsBarGaugeCardFeature,
|
||||
button: supportsButtonCardFeature,
|
||||
"climate-fan-modes": supportsClimateFanModesCardFeature,
|
||||
"climate-swing-modes": supportsClimateSwingModesCardFeature,
|
||||
"climate-swing-horizontal-modes":
|
||||
supportsClimateSwingHorizontalModesCardFeature,
|
||||
"climate-hvac-modes": supportsClimateHvacModesCardFeature,
|
||||
"climate-preset-modes": supportsClimatePresetModesCardFeature,
|
||||
"counter-actions": supportsCounterActionsCardFeature,
|
||||
"cover-open-close": supportsCoverOpenCloseCardFeature,
|
||||
"cover-position-favorite": supportsCoverPositionFavoriteCardFeature,
|
||||
"cover-position": supportsCoverPositionCardFeature,
|
||||
"cover-tilt-favorite": supportsCoverTiltFavoriteCardFeature,
|
||||
"cover-tilt-position": supportsCoverTiltPositionCardFeature,
|
||||
"cover-tilt": supportsCoverTiltCardFeature,
|
||||
"date-set": supportsDateSetCardFeature,
|
||||
"fan-direction": supportsFanDirectionCardFeature,
|
||||
"fan-oscillate": supportsFanOscilatteCardFeature,
|
||||
"fan-preset-modes": supportsFanPresetModesCardFeature,
|
||||
"fan-speed": supportsFanSpeedCardFeature,
|
||||
"hourly-forecast": supportsHourlyForecastCardFeature,
|
||||
"humidifier-modes": supportsHumidifierModesCardFeature,
|
||||
"humidifier-toggle": supportsHumidifierToggleCardFeature,
|
||||
"lawn-mower-commands": supportsLawnMowerCommandCardFeature,
|
||||
"light-brightness": supportsLightBrightnessCardFeature,
|
||||
"light-color-temp": supportsLightColorTempCardFeature,
|
||||
"light-color-favorites": supportsLightColorFavoritesCardFeature,
|
||||
"lock-commands": supportsLockCommandsCardFeature,
|
||||
"lock-open-door": supportsLockOpenDoorCardFeature,
|
||||
"media-player-playback": supportsMediaPlayerPlaybackCardFeature,
|
||||
"media-player-sound-mode": supportsMediaPlayerSoundModeCardFeature,
|
||||
"media-player-source": supportsMediaPlayerSourceCardFeature,
|
||||
"media-player-volume-buttons": supportsMediaPlayerVolumeButtonsCardFeature,
|
||||
"media-player-volume-slider": supportsMediaPlayerVolumeSliderCardFeature,
|
||||
"numeric-input": supportsNumericInputCardFeature,
|
||||
"select-options": supportsSelectOptionsCardFeature,
|
||||
"trend-graph": supportsTrendGraphCardFeature,
|
||||
"target-humidity": supportsTargetHumidityCardFeature,
|
||||
"target-temperature": supportsTargetTemperatureCardFeature,
|
||||
toggle: supportsToggleCardFeature,
|
||||
"update-actions": supportsUpdateActionsCardFeature,
|
||||
"vacuum-commands": supportsVacuumCommandsCardFeature,
|
||||
"valve-open-close": supportsValveOpenCloseCardFeature,
|
||||
"valve-position-favorite": supportsValvePositionFavoriteCardFeature,
|
||||
"valve-position": supportsValvePositionCardFeature,
|
||||
"water-heater-operation-modes": supportsWaterHeaterOperationModesCardFeature,
|
||||
};
|
||||
|
||||
export const supportsFeatureType = (
|
||||
hass: HomeAssistant,
|
||||
context: LovelaceCardFeatureContext,
|
||||
type: UiFeatureType
|
||||
): boolean => SUPPORTS_FEATURE_TYPES[type](hass, context);
|
||||
@@ -0,0 +1,15 @@
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
import type { CalendarCardConfig } from "../cards/types";
|
||||
import type { CardSuggestionProvider } from "./types";
|
||||
|
||||
export const calendarCardSuggestions: CardSuggestionProvider<CalendarCardConfig> =
|
||||
{
|
||||
getEntitySuggestion(_hass, entityId) {
|
||||
if (computeDomain(entityId) !== "calendar") return null;
|
||||
return {
|
||||
id: "calendar",
|
||||
label: "Calendar",
|
||||
config: { type: "calendar", entities: [entityId] },
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,272 @@
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import {
|
||||
SUPPORTS_FEATURE_TYPES,
|
||||
type UiFeatureType,
|
||||
} from "../card-features/registry";
|
||||
import type { LovelaceCardFeatureConfig } from "../card-features/types";
|
||||
import type { TileCardConfig } from "../cards/types";
|
||||
import type { CardSuggestion, CardSuggestionProvider } from "./types";
|
||||
|
||||
interface TileVariant {
|
||||
id: string;
|
||||
label: string;
|
||||
features: UiFeatureType[];
|
||||
}
|
||||
|
||||
const TILE_LABEL = "Tile";
|
||||
const TILE_TOGGLE_LABEL = "Tile with toggle";
|
||||
|
||||
const SELECT_VARIANTS: TileVariant[] = [
|
||||
{ id: "tile", label: TILE_LABEL, features: [] },
|
||||
{
|
||||
id: "tile-options",
|
||||
label: "Tile with options",
|
||||
features: ["select-options"],
|
||||
},
|
||||
];
|
||||
|
||||
const NUMERIC_INPUT_VARIANTS: TileVariant[] = [
|
||||
{ id: "tile", label: TILE_LABEL, features: [] },
|
||||
{
|
||||
id: "tile-input",
|
||||
label: "Tile with numeric input",
|
||||
features: ["numeric-input"],
|
||||
},
|
||||
];
|
||||
|
||||
const DATE_VARIANTS: TileVariant[] = [
|
||||
{ id: "tile", label: TILE_LABEL, features: [] },
|
||||
{ id: "tile-date", label: "Tile with date picker", features: ["date-set"] },
|
||||
];
|
||||
|
||||
const DOMAIN_VARIANTS: Record<string, TileVariant[]> = {
|
||||
light: [
|
||||
{ id: "tile", label: TILE_LABEL, features: [] },
|
||||
{ id: "tile-toggle", label: TILE_TOGGLE_LABEL, features: ["toggle"] },
|
||||
{
|
||||
id: "tile-brightness",
|
||||
label: "Tile with brightness",
|
||||
features: ["light-brightness"],
|
||||
},
|
||||
{
|
||||
id: "tile-color-temp",
|
||||
label: "Tile with color temperature",
|
||||
features: ["light-color-temp"],
|
||||
},
|
||||
{
|
||||
id: "tile-color-favorites",
|
||||
label: "Tile with favorite colors",
|
||||
features: ["light-color-favorites"],
|
||||
},
|
||||
],
|
||||
cover: [
|
||||
{ id: "tile", label: TILE_LABEL, features: [] },
|
||||
{
|
||||
id: "tile-open-close",
|
||||
label: "Tile with open/close",
|
||||
features: ["cover-open-close"],
|
||||
},
|
||||
{
|
||||
id: "tile-position",
|
||||
label: "Tile with position",
|
||||
features: ["cover-position"],
|
||||
},
|
||||
{
|
||||
id: "tile-tilt",
|
||||
label: "Tile with tilt",
|
||||
features: ["cover-tilt"],
|
||||
},
|
||||
],
|
||||
climate: [
|
||||
{ id: "tile", label: TILE_LABEL, features: [] },
|
||||
{
|
||||
id: "tile-hvac-modes",
|
||||
label: "Tile with HVAC modes",
|
||||
features: ["climate-hvac-modes"],
|
||||
},
|
||||
],
|
||||
media_player: [
|
||||
{ id: "tile", label: TILE_LABEL, features: [] },
|
||||
{
|
||||
id: "tile-playback",
|
||||
label: "Tile with playback controls",
|
||||
features: ["media-player-playback"],
|
||||
},
|
||||
{
|
||||
id: "tile-volume-slider",
|
||||
label: "Tile with volume slider",
|
||||
features: ["media-player-volume-slider"],
|
||||
},
|
||||
],
|
||||
fan: [
|
||||
{ id: "tile", label: TILE_LABEL, features: [] },
|
||||
{ id: "tile-speed", label: "Tile with speed", features: ["fan-speed"] },
|
||||
{
|
||||
id: "tile-preset-modes",
|
||||
label: "Tile with preset modes",
|
||||
features: ["fan-preset-modes"],
|
||||
},
|
||||
],
|
||||
switch: [
|
||||
{ id: "tile", label: TILE_LABEL, features: [] },
|
||||
{ id: "tile-toggle", label: TILE_TOGGLE_LABEL, features: ["toggle"] },
|
||||
],
|
||||
input_boolean: [
|
||||
{ id: "tile", label: TILE_LABEL, features: [] },
|
||||
{ id: "tile-toggle", label: TILE_TOGGLE_LABEL, features: ["toggle"] },
|
||||
],
|
||||
lock: [
|
||||
{ id: "tile", label: TILE_LABEL, features: [] },
|
||||
{
|
||||
id: "tile-commands",
|
||||
label: "Tile with lock commands",
|
||||
features: ["lock-commands"],
|
||||
},
|
||||
],
|
||||
humidifier: [
|
||||
{ id: "tile", label: TILE_LABEL, features: [] },
|
||||
{
|
||||
id: "tile-toggle",
|
||||
label: "Tile with humidifier toggle",
|
||||
features: ["humidifier-toggle"],
|
||||
},
|
||||
{
|
||||
id: "tile-modes",
|
||||
label: "Tile with humidifier modes",
|
||||
features: ["humidifier-modes"],
|
||||
},
|
||||
],
|
||||
vacuum: [
|
||||
{ id: "tile", label: TILE_LABEL, features: [] },
|
||||
{
|
||||
id: "tile-commands",
|
||||
label: "Tile with vacuum commands",
|
||||
features: ["vacuum-commands"],
|
||||
},
|
||||
],
|
||||
lawn_mower: [
|
||||
{ id: "tile", label: TILE_LABEL, features: [] },
|
||||
{
|
||||
id: "tile-commands",
|
||||
label: "Tile with mower commands",
|
||||
features: ["lawn-mower-commands"],
|
||||
},
|
||||
],
|
||||
valve: [
|
||||
{ id: "tile", label: TILE_LABEL, features: [] },
|
||||
{
|
||||
id: "tile-open-close",
|
||||
label: "Tile with open/close",
|
||||
features: ["valve-open-close"],
|
||||
},
|
||||
{
|
||||
id: "tile-position",
|
||||
label: "Tile with position",
|
||||
features: ["valve-position"],
|
||||
},
|
||||
],
|
||||
alarm_control_panel: [
|
||||
{ id: "tile", label: TILE_LABEL, features: [] },
|
||||
{
|
||||
id: "tile-modes",
|
||||
label: "Tile with alarm modes",
|
||||
features: ["alarm-modes"],
|
||||
},
|
||||
],
|
||||
counter: [
|
||||
{ id: "tile", label: TILE_LABEL, features: [] },
|
||||
{
|
||||
id: "tile-actions",
|
||||
label: "Tile with counter actions",
|
||||
features: ["counter-actions"],
|
||||
},
|
||||
],
|
||||
input_select: SELECT_VARIANTS,
|
||||
select: SELECT_VARIANTS,
|
||||
input_number: NUMERIC_INPUT_VARIANTS,
|
||||
number: NUMERIC_INPUT_VARIANTS,
|
||||
input_datetime: DATE_VARIANTS,
|
||||
date: DATE_VARIANTS,
|
||||
update: [
|
||||
{ id: "tile", label: TILE_LABEL, features: [] },
|
||||
{
|
||||
id: "tile-actions",
|
||||
label: "Tile with update actions",
|
||||
features: ["update-actions"],
|
||||
},
|
||||
],
|
||||
water_heater: [
|
||||
{ id: "tile", label: TILE_LABEL, features: [] },
|
||||
{
|
||||
id: "tile-operation-modes",
|
||||
label: "Tile with operation modes",
|
||||
features: ["water-heater-operation-modes"],
|
||||
},
|
||||
],
|
||||
weather: [
|
||||
{ id: "tile", label: TILE_LABEL, features: [] },
|
||||
{
|
||||
id: "tile-hourly-forecast",
|
||||
label: "Tile with hourly forecast",
|
||||
features: ["hourly-forecast"],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const DEFAULT_VARIANT: TileVariant = {
|
||||
id: "tile",
|
||||
label: TILE_LABEL,
|
||||
features: [],
|
||||
};
|
||||
|
||||
const EXCLUDED_DOMAINS = ["calendar", "todo"];
|
||||
|
||||
const buildTileConfig = (
|
||||
entityId: string,
|
||||
features: UiFeatureType[]
|
||||
): TileCardConfig => {
|
||||
const config: TileCardConfig = { type: "tile", entity: entityId };
|
||||
if (features.length) {
|
||||
config.features = features.map(
|
||||
(type) => ({ type }) as LovelaceCardFeatureConfig
|
||||
);
|
||||
}
|
||||
return config;
|
||||
};
|
||||
|
||||
const allFeaturesSupported = (
|
||||
hass: HomeAssistant,
|
||||
entityId: string,
|
||||
features: UiFeatureType[]
|
||||
): boolean =>
|
||||
features.every((type) => {
|
||||
const supports = SUPPORTS_FEATURE_TYPES[type];
|
||||
if (!supports) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
return supports(hass, { entity_id: entityId });
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
export const tileCardSuggestions: CardSuggestionProvider<TileCardConfig> = {
|
||||
getEntitySuggestion(hass, entityId) {
|
||||
if (EXCLUDED_DOMAINS.includes(computeDomain(entityId))) return null;
|
||||
const variants = DOMAIN_VARIANTS[computeDomain(entityId)] ?? [
|
||||
DEFAULT_VARIANT,
|
||||
];
|
||||
const suggestions: CardSuggestion<TileCardConfig>[] = [];
|
||||
for (const variant of variants) {
|
||||
if (!allFeaturesSupported(hass, entityId, variant.features)) continue;
|
||||
suggestions.push({
|
||||
id: variant.id,
|
||||
label: variant.label,
|
||||
config: buildTileConfig(entityId, variant.features),
|
||||
});
|
||||
}
|
||||
return suggestions.length ? suggestions : null;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
import type { TodoListCardConfig } from "../cards/types";
|
||||
import type { CardSuggestionProvider } from "./types";
|
||||
|
||||
export const todoListCardSuggestions: CardSuggestionProvider<TodoListCardConfig> =
|
||||
{
|
||||
getEntitySuggestion(_hass, entityId) {
|
||||
if (computeDomain(entityId) !== "todo") return null;
|
||||
return {
|
||||
id: "todo-list",
|
||||
label: "To-do list",
|
||||
config: { type: "todo-list", entity: entityId },
|
||||
};
|
||||
},
|
||||
};
|
||||
11
src/panels/lovelace/card-suggestions/registry.ts
Normal file
11
src/panels/lovelace/card-suggestions/registry.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { calendarCardSuggestions } from "./hui-calendar-card-suggestions";
|
||||
import { tileCardSuggestions } from "./hui-tile-card-suggestions";
|
||||
import { todoListCardSuggestions } from "./hui-todo-list-card-suggestions";
|
||||
import type { CardSuggestionProvider } from "./types";
|
||||
|
||||
export const CARD_SUGGESTION_PROVIDERS: Record<string, CardSuggestionProvider> =
|
||||
{
|
||||
tile: tileCardSuggestions,
|
||||
calendar: calendarCardSuggestions,
|
||||
"todo-list": todoListCardSuggestions,
|
||||
};
|
||||
19
src/panels/lovelace/card-suggestions/types.ts
Normal file
19
src/panels/lovelace/card-suggestions/types.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import type { LovelaceCardConfig } from "../../../data/lovelace/config/card";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
|
||||
export interface CardSuggestion<
|
||||
T extends LovelaceCardConfig = LovelaceCardConfig,
|
||||
> {
|
||||
id: string;
|
||||
label: string;
|
||||
config: T;
|
||||
}
|
||||
|
||||
export interface CardSuggestionProvider<
|
||||
T extends LovelaceCardConfig = LovelaceCardConfig,
|
||||
> {
|
||||
getEntitySuggestion(
|
||||
hass: HomeAssistant,
|
||||
entityId: string
|
||||
): CardSuggestion<T> | CardSuggestion<T>[] | null;
|
||||
}
|
||||
57
src/panels/lovelace/common/card-suggestions.ts
Normal file
57
src/panels/lovelace/common/card-suggestions.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import type { LovelaceCardConfig } from "../../../data/lovelace/config/card";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { CARD_SUGGESTION_PROVIDERS } from "../card-suggestions/registry";
|
||||
import type { CardSuggestion } from "../card-suggestions/types";
|
||||
|
||||
export type { CardSuggestion } from "../card-suggestions/types";
|
||||
|
||||
const GRID_COLUMNS = 2;
|
||||
|
||||
const buildMultiEntitySuggestions = (entityIds: string[]): CardSuggestion[] => {
|
||||
const tiles: LovelaceCardConfig[] = entityIds.map((id) => ({
|
||||
type: "tile",
|
||||
entity: id,
|
||||
}));
|
||||
|
||||
return [
|
||||
{
|
||||
id: "grid-of-tiles",
|
||||
label: "Grid of tiles",
|
||||
config: {
|
||||
type: "grid",
|
||||
columns: GRID_COLUMNS,
|
||||
square: false,
|
||||
cards: tiles,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "entities-card",
|
||||
label: "Entities card",
|
||||
config: { type: "entities", entities: entityIds },
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
const collectEntitySuggestions = (
|
||||
hass: HomeAssistant,
|
||||
entityId: string
|
||||
): CardSuggestion[] =>
|
||||
Object.values(CARD_SUGGESTION_PROVIDERS).flatMap((provider) => {
|
||||
try {
|
||||
const result = provider.getEntitySuggestion(hass, entityId);
|
||||
if (!result) return [];
|
||||
return Array.isArray(result) ? result : [result];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
});
|
||||
|
||||
export const generateCardSuggestions = (
|
||||
hass: HomeAssistant,
|
||||
entityIds: string[]
|
||||
): CardSuggestion[] => {
|
||||
const validIds = entityIds.filter((id) => hass.states[id] !== undefined);
|
||||
if (validIds.length === 0) return [];
|
||||
if (validIds.length === 1) return collectEntitySuggestions(hass, validIds[0]);
|
||||
return buildMultiEntitySuggestions(validIds);
|
||||
};
|
||||
@@ -3,7 +3,6 @@ import type { CSSResultGroup } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { cache } from "lit/directives/cache";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-dialog-header";
|
||||
@@ -11,36 +10,18 @@ import "../../../../components/ha-dialog-footer";
|
||||
import "../../../../components/ha-tab-group";
|
||||
import "../../../../components/ha-tab-group-tab";
|
||||
import "../../../../components/ha-dialog";
|
||||
import type { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
|
||||
import type { LovelaceSectionConfig } from "../../../../data/lovelace/config/section";
|
||||
import { isStrategySection } from "../../../../data/lovelace/config/section";
|
||||
import type { LovelaceViewConfig } from "../../../../data/lovelace/config/view";
|
||||
import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
|
||||
import { haStyleDialog } from "../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import {
|
||||
computeCards,
|
||||
computeSection,
|
||||
} from "../../common/generate-lovelace-config";
|
||||
import { addCard } from "../config-util";
|
||||
import {
|
||||
findLovelaceContainer,
|
||||
parseLovelaceContainerPath,
|
||||
} from "../lovelace-path";
|
||||
import { findLovelaceContainer } from "../lovelace-path";
|
||||
import "./hui-card-picker";
|
||||
import "./hui-entity-picker-table";
|
||||
import "./hui-recipe-picker";
|
||||
import type { CreateCardDialogParams } from "./show-create-card-dialog";
|
||||
import { showEditCardDialog } from "./show-edit-card-dialog";
|
||||
import { showSuggestCardDialog } from "./show-suggest-card-dialog";
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
"selected-changed": SelectedChangedEvent;
|
||||
}
|
||||
}
|
||||
|
||||
interface SelectedChangedEvent {
|
||||
selectedEntities: string[];
|
||||
}
|
||||
|
||||
@customElement("hui-dialog-create-card")
|
||||
export class HuiCreateDialogCard
|
||||
@@ -57,9 +38,7 @@ export class HuiCreateDialogCard
|
||||
| LovelaceViewConfig
|
||||
| LovelaceSectionConfig;
|
||||
|
||||
@state() private _selectedEntities: string[] = [];
|
||||
|
||||
@state() private _currTab: "card" | "entity" = "card";
|
||||
@state() private _currTab: "card" | "entity" = "entity";
|
||||
|
||||
@state() private _narrow = false;
|
||||
|
||||
@@ -91,8 +70,7 @@ export class HuiCreateDialogCard
|
||||
private _dialogClosed(): void {
|
||||
this._open = false;
|
||||
this._params = undefined;
|
||||
this._currTab = "card";
|
||||
this._selectedEntities = [];
|
||||
this._currTab = "entity";
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
@@ -116,7 +94,6 @@ export class HuiCreateDialogCard
|
||||
width="large"
|
||||
@keydown=${this._ignoreKeydown}
|
||||
@closed=${this._dialogClosed}
|
||||
class=${classMap({ table: this._currTab === "entity" })}
|
||||
>
|
||||
<ha-dialog-header show-border slot="header">
|
||||
<ha-icon-button
|
||||
@@ -130,31 +107,41 @@ export class HuiCreateDialogCard
|
||||
${!this._params.saveCard
|
||||
? html`
|
||||
<ha-tab-group @wa-tab-show=${this._handleTabChanged}>
|
||||
<ha-tab-group-tab
|
||||
slot="nav"
|
||||
.active=${this._currTab === "entity"}
|
||||
panel="entity"
|
||||
?autofocus=${this._narrow}
|
||||
>${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.cardpicker.by_entity"
|
||||
)}</ha-tab-group-tab
|
||||
>
|
||||
<ha-tab-group-tab
|
||||
slot="nav"
|
||||
.active=${this._currTab === "card"}
|
||||
panel="card"
|
||||
?autofocus=${this._narrow}
|
||||
>
|
||||
${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.cardpicker.by_card"
|
||||
)}
|
||||
</ha-tab-group-tab>
|
||||
<ha-tab-group-tab
|
||||
slot="nav"
|
||||
.active=${this._currTab === "entity"}
|
||||
panel="entity"
|
||||
>${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.cardpicker.by_entity"
|
||||
)}</ha-tab-group-tab
|
||||
>
|
||||
</ha-tab-group>
|
||||
`
|
||||
: nothing}
|
||||
</ha-dialog-header>
|
||||
${cache(
|
||||
this._currTab === "card"
|
||||
this._currTab === "entity"
|
||||
? html`
|
||||
<hui-recipe-picker
|
||||
.hass=${this.hass}
|
||||
?in-section=${this._params!.path.length === 2}
|
||||
.suggestedCards=${this._params.suggestedCards}
|
||||
@config-changed=${this._handleCardPicked}
|
||||
@recipe-cards-picked=${this._handleRecipeCardsPicked}
|
||||
@recipe-browse-cards=${this._handleBrowseCards}
|
||||
></hui-recipe-picker>
|
||||
`
|
||||
: html`
|
||||
<hui-card-picker
|
||||
?autofocus=${!this._narrow}
|
||||
.suggestedCards=${this._params.suggestedCards}
|
||||
@@ -163,13 +150,6 @@ export class HuiCreateDialogCard
|
||||
@config-changed=${this._handleCardPicked}
|
||||
></hui-card-picker>
|
||||
`
|
||||
: html`
|
||||
<hui-entity-picker-table
|
||||
.hass=${this.hass}
|
||||
narrow
|
||||
@selected-changed=${this._handleSelectedChanged}
|
||||
></hui-entity-picker-table>
|
||||
`
|
||||
)}
|
||||
|
||||
<ha-dialog-footer slot="footer">
|
||||
@@ -180,13 +160,6 @@ export class HuiCreateDialogCard
|
||||
>
|
||||
${this.hass!.localize("ui.common.cancel")}
|
||||
</ha-button>
|
||||
${this._selectedEntities.length
|
||||
? html`
|
||||
<ha-button slot="primaryAction" @click=${this._suggestCards}>
|
||||
${this.hass!.localize("ui.common.continue")}
|
||||
</ha-button>
|
||||
`
|
||||
: ""}
|
||||
</ha-dialog-footer>
|
||||
</ha-dialog>
|
||||
`;
|
||||
@@ -205,10 +178,6 @@ export class HuiCreateDialogCard
|
||||
--dialog-z-index: 6;
|
||||
}
|
||||
|
||||
ha-dialog.table {
|
||||
--dialog-content-padding: 0;
|
||||
}
|
||||
|
||||
ha-dialog::part(body) {
|
||||
overflow: hidden;
|
||||
}
|
||||
@@ -227,18 +196,13 @@ export class HuiCreateDialogCard
|
||||
}
|
||||
|
||||
hui-card-picker,
|
||||
hui-entity-picker-table {
|
||||
hui-recipe-picker {
|
||||
height: calc(100vh - 198px);
|
||||
}
|
||||
|
||||
hui-entity-picker-table {
|
||||
display: block;
|
||||
--mdc-shape-small: 0;
|
||||
}
|
||||
|
||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||
hui-card-picker,
|
||||
hui-entity-picker-table {
|
||||
hui-recipe-picker {
|
||||
height: calc(100vh - 158px);
|
||||
}
|
||||
}
|
||||
@@ -246,6 +210,29 @@ export class HuiCreateDialogCard
|
||||
];
|
||||
}
|
||||
|
||||
private _handleBrowseCards(): void {
|
||||
this._currTab = "card";
|
||||
}
|
||||
|
||||
private async _handleRecipeCardsPicked(
|
||||
ev: CustomEvent<{ cards: LovelaceCardConfig[] }>
|
||||
): Promise<void> {
|
||||
const cards = ev.detail.cards;
|
||||
if (!cards.length || this._params!.saveCard) {
|
||||
return;
|
||||
}
|
||||
const containerPath = this._params!.path;
|
||||
const saveConfig = this._params!.saveConfig;
|
||||
|
||||
let newConfig = this._params!.lovelaceConfig;
|
||||
for (const card of cards) {
|
||||
newConfig = addCard(newConfig, containerPath, card);
|
||||
}
|
||||
await saveConfig(newConfig);
|
||||
|
||||
this.closeDialog();
|
||||
}
|
||||
|
||||
private _handleCardPicked(ev) {
|
||||
const config = ev.detail.config;
|
||||
if (this._params!.entities && this._params!.entities.length) {
|
||||
@@ -295,13 +282,7 @@ export class HuiCreateDialogCard
|
||||
if (newTab === this._currTab) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._currTab = newTab;
|
||||
this._selectedEntities = [];
|
||||
}
|
||||
|
||||
private _handleSelectedChanged(ev: CustomEvent): void {
|
||||
this._selectedEntities = ev.detail.selectedEntities;
|
||||
}
|
||||
|
||||
private _cancel(ev?: Event) {
|
||||
@@ -310,45 +291,6 @@ export class HuiCreateDialogCard
|
||||
}
|
||||
this.closeDialog();
|
||||
}
|
||||
|
||||
private _suggestCards(): void {
|
||||
const cardConfig = computeCards(this.hass, this._selectedEntities, {});
|
||||
|
||||
let sectionOptions: Partial<LovelaceSectionConfig> = {};
|
||||
|
||||
const { viewIndex, sectionIndex } = parseLovelaceContainerPath(
|
||||
this._params!.path
|
||||
);
|
||||
const isSection = sectionIndex !== undefined;
|
||||
|
||||
// If we are in a section, we want to keep the section options for the preview
|
||||
if (isSection) {
|
||||
const containerConfig = findLovelaceContainer(
|
||||
this._params!.lovelaceConfig!,
|
||||
[viewIndex, sectionIndex]
|
||||
);
|
||||
if (!isStrategySection(containerConfig)) {
|
||||
const { cards, title, ...rest } = containerConfig;
|
||||
sectionOptions = rest;
|
||||
}
|
||||
}
|
||||
|
||||
const sectionConfig = computeSection(
|
||||
this._selectedEntities,
|
||||
sectionOptions
|
||||
);
|
||||
|
||||
showSuggestCardDialog(this, {
|
||||
lovelaceConfig: this._params!.lovelaceConfig,
|
||||
saveConfig: this._params!.saveConfig,
|
||||
path: this._params!.path as [number],
|
||||
entities: this._selectedEntities,
|
||||
cardConfig,
|
||||
sectionConfig,
|
||||
});
|
||||
|
||||
this.closeDialog();
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
461
src/panels/lovelace/editor/card-editor/hui-recipe-picker.ts
Normal file
461
src/panels/lovelace/editor/card-editor/hui-recipe-picker.ts
Normal file
@@ -0,0 +1,461 @@
|
||||
import { mdiClose, mdiViewGridPlus } from "@mdi/js";
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { computeAreaName } from "../../../../common/entity/compute_area_name";
|
||||
import { computeDeviceName } from "../../../../common/entity/compute_device_name";
|
||||
import { computeDomain } from "../../../../common/entity/compute_domain";
|
||||
import { computeStateName } from "../../../../common/entity/compute_state_name";
|
||||
import "../../../../components/entity/ha-entity-picker";
|
||||
import "../../../../components/entity/state-badge";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-icon-button";
|
||||
import "../../../../components/ha-md-list";
|
||||
import "../../../../components/ha-md-list-item";
|
||||
import "../../../../components/ha-ripple";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import type { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
|
||||
import { domainToName } from "../../../../data/integration";
|
||||
import { haStyleScrollbar } from "../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type { CardSuggestion } from "../../card-suggestions/types";
|
||||
import { generateCardSuggestions } from "../../common/card-suggestions";
|
||||
import { tryCreateCardElement } from "../../create-element/create-card-element";
|
||||
import type { LovelaceCard } from "../../types";
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
"recipe-cards-picked": { cards: LovelaceCardConfig[] };
|
||||
"recipe-browse-cards": undefined;
|
||||
}
|
||||
}
|
||||
|
||||
const PREVIEW_ITEM_CAP = 6;
|
||||
|
||||
const getItemCount = (config: LovelaceCardConfig): number => {
|
||||
const c = config as { cards?: unknown[]; entities?: unknown[] };
|
||||
return c.cards?.length ?? c.entities?.length ?? 0;
|
||||
};
|
||||
|
||||
const cappedPreviewConfig = (
|
||||
config: LovelaceCardConfig
|
||||
): LovelaceCardConfig => {
|
||||
const cast = config as {
|
||||
cards?: LovelaceCardConfig[];
|
||||
entities?: unknown[];
|
||||
};
|
||||
if (cast.cards && cast.cards.length > PREVIEW_ITEM_CAP) {
|
||||
return { ...config, cards: cast.cards.slice(0, PREVIEW_ITEM_CAP) };
|
||||
}
|
||||
if (cast.entities && cast.entities.length > PREVIEW_ITEM_CAP) {
|
||||
return { ...config, entities: cast.entities.slice(0, PREVIEW_ITEM_CAP) };
|
||||
}
|
||||
return config;
|
||||
};
|
||||
|
||||
@customElement("hui-recipe-picker")
|
||||
export class HuiRecipePicker extends LitElement {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean, attribute: "in-section" })
|
||||
public inSection = false;
|
||||
|
||||
@property({ type: Array, attribute: false }) public suggestedCards?: string[];
|
||||
|
||||
@state() private _entityIds: string[] = [];
|
||||
|
||||
private _computeSuggestions = memoizeOne(
|
||||
(
|
||||
hass: HomeAssistant,
|
||||
entityIdsKey: string,
|
||||
suggestedCards: string[] | undefined
|
||||
): CardSuggestion[] => {
|
||||
const entityIds = entityIdsKey ? entityIdsKey.split("|") : [];
|
||||
const suggestions = generateCardSuggestions(hass, entityIds);
|
||||
if (!suggestedCards?.length) return suggestions;
|
||||
const isPrioritized = (s: CardSuggestion) =>
|
||||
suggestedCards.includes(s.config.type);
|
||||
return [
|
||||
...suggestions.filter(isPrioritized),
|
||||
...suggestions.filter((s) => !isPrioritized(s)),
|
||||
];
|
||||
}
|
||||
);
|
||||
|
||||
protected render() {
|
||||
if (!this.hass) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
if (this._entityIds.length === 0) {
|
||||
return this._renderEmptyState();
|
||||
}
|
||||
|
||||
const suggestions = this._computeSuggestions(
|
||||
this.hass,
|
||||
this._entityIds.join("|"),
|
||||
this.suggestedCards
|
||||
);
|
||||
|
||||
return html`
|
||||
<div class="sidebar ha-scrollbar">
|
||||
<ha-md-list>
|
||||
${repeat(
|
||||
this._entityIds,
|
||||
(id: string) => id,
|
||||
(id: string) => this._renderEntityRow(id)
|
||||
)}
|
||||
</ha-md-list>
|
||||
<div class="add-row">
|
||||
<ha-entity-picker
|
||||
.hass=${this.hass}
|
||||
add-button
|
||||
.excludeEntities=${this._entityIds}
|
||||
@value-changed=${this._entityPickerValueChanged}
|
||||
></ha-entity-picker>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content ha-scrollbar">
|
||||
<div class="recipes">
|
||||
${repeat(
|
||||
suggestions,
|
||||
(s) => s.id,
|
||||
(s) => this._renderSuggestionCard(s)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderEmptyState(): TemplateResult {
|
||||
return html`
|
||||
<div class="empty-state">
|
||||
<div class="empty-state-inner">
|
||||
<h2>
|
||||
${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.cardpicker.empty_title"
|
||||
)}
|
||||
</h2>
|
||||
<p>
|
||||
${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.cardpicker.empty_description"
|
||||
)}
|
||||
</p>
|
||||
<div class="empty-state-actions">
|
||||
<ha-entity-picker
|
||||
.hass=${this.hass}
|
||||
add-button
|
||||
.excludeEntities=${this._entityIds}
|
||||
@value-changed=${this._entityPickerValueChanged}
|
||||
></ha-entity-picker>
|
||||
<ha-button appearance="plain" @click=${this._browseCards}>
|
||||
<ha-svg-icon slot="start" .path=${mdiViewGridPlus}></ha-svg-icon>
|
||||
${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.cardpicker.browse_cards"
|
||||
)}
|
||||
</ha-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _browseCards(): void {
|
||||
fireEvent(this, "recipe-browse-cards", undefined);
|
||||
}
|
||||
|
||||
private _renderEntityRow(entityId: string): TemplateResult {
|
||||
const stateObj = this.hass!.states[entityId];
|
||||
const entity = this.hass!.entities[entityId];
|
||||
const device = entity?.device_id
|
||||
? this.hass!.devices[entity.device_id]
|
||||
: undefined;
|
||||
const areaId = entity?.area_id ?? device?.area_id;
|
||||
const area = areaId ? this.hass!.areas[areaId] : undefined;
|
||||
|
||||
const name = stateObj ? computeStateName(stateObj) : entityId;
|
||||
const supporting =
|
||||
(area ? computeAreaName(area) : undefined) ??
|
||||
(device ? computeDeviceName(device) : undefined) ??
|
||||
domainToName(this.hass!.localize, computeDomain(entityId));
|
||||
|
||||
return html`
|
||||
<ha-md-list-item type="text" class="entity-row">
|
||||
${stateObj
|
||||
? html`<state-badge
|
||||
slot="start"
|
||||
.hass=${this.hass}
|
||||
.stateObj=${stateObj}
|
||||
></state-badge>`
|
||||
: nothing}
|
||||
<div slot="headline">${name}</div>
|
||||
<div slot="supporting-text">${supporting}</div>
|
||||
<ha-icon-button
|
||||
slot="end"
|
||||
.path=${mdiClose}
|
||||
.label=${this.hass!.localize("ui.common.remove")}
|
||||
.entityId=${entityId}
|
||||
@click=${this._removeEntity}
|
||||
></ha-icon-button>
|
||||
</ha-md-list-item>
|
||||
`;
|
||||
}
|
||||
|
||||
private _entityPickerValueChanged(ev: CustomEvent<{ value: string }>): void {
|
||||
const value = ev.detail.value;
|
||||
if (!value || this._entityIds.includes(value)) {
|
||||
return;
|
||||
}
|
||||
this._entityIds = [...this._entityIds, value];
|
||||
}
|
||||
|
||||
private _removeEntity(ev: MouseEvent): void {
|
||||
ev.stopPropagation();
|
||||
const target = ev.currentTarget as HTMLElement & { entityId: string };
|
||||
this._entityIds = this._entityIds.filter((id) => id !== target.entityId);
|
||||
}
|
||||
|
||||
private _renderSuggestionCard(suggestion: CardSuggestion): TemplateResult {
|
||||
const totalCount = getItemCount(suggestion.config);
|
||||
const hiddenCount = Math.max(0, totalCount - PREVIEW_ITEM_CAP);
|
||||
const previewConfig = cappedPreviewConfig(suggestion.config);
|
||||
return html`
|
||||
<div class="card" tabindex="0">
|
||||
<div
|
||||
class="overlay"
|
||||
role="button"
|
||||
aria-label=${suggestion.label}
|
||||
@click=${this._suggestionPicked}
|
||||
.suggestion=${suggestion}
|
||||
></div>
|
||||
<div class="card-header">${suggestion.label}</div>
|
||||
<div class="preview">${this._renderPreview(previewConfig)}</div>
|
||||
${hiddenCount > 0
|
||||
? html`
|
||||
<div class="more-badge">
|
||||
${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.cardpicker.more_cards",
|
||||
{ count: hiddenCount }
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
<ha-ripple></ha-ripple>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderPreview(config: LovelaceCardConfig): TemplateResult {
|
||||
let element: LovelaceCard | undefined;
|
||||
try {
|
||||
element = tryCreateCardElement(config) as LovelaceCard;
|
||||
element.hass = this.hass;
|
||||
element.tabIndex = -1;
|
||||
element.addEventListener(
|
||||
"ll-rebuild",
|
||||
(ev) => {
|
||||
ev.stopPropagation();
|
||||
this._rebuildCard(element!, config);
|
||||
},
|
||||
{ once: true }
|
||||
);
|
||||
} catch {
|
||||
element = undefined;
|
||||
}
|
||||
return html`${element ?? nothing}`;
|
||||
}
|
||||
|
||||
private _rebuildCard(
|
||||
cardElToReplace: LovelaceCard,
|
||||
config: LovelaceCardConfig
|
||||
): void {
|
||||
let newCardEl: LovelaceCard;
|
||||
try {
|
||||
newCardEl = tryCreateCardElement(config) as LovelaceCard;
|
||||
newCardEl.hass = this.hass;
|
||||
newCardEl.tabIndex = -1;
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
cardElToReplace.parentElement?.replaceChild(newCardEl, cardElToReplace);
|
||||
}
|
||||
|
||||
private _suggestionPicked(ev: MouseEvent): void {
|
||||
const suggestion: CardSuggestion = (
|
||||
ev.currentTarget as HTMLElement & { suggestion: CardSuggestion }
|
||||
).suggestion;
|
||||
const config = suggestion.config;
|
||||
const wrapper = config as { type: string; cards?: LovelaceCardConfig[] };
|
||||
if (this.inSection && wrapper.type === "grid" && wrapper.cards?.length) {
|
||||
fireEvent(this, "recipe-cards-picked", { cards: wrapper.cards });
|
||||
return;
|
||||
}
|
||||
fireEvent(this, "config-changed", { config });
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyleScrollbar,
|
||||
css`
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
min-height: 0;
|
||||
}
|
||||
.empty-state {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: var(--ha-space-8) var(--ha-space-4);
|
||||
text-align: center;
|
||||
}
|
||||
.empty-state-inner {
|
||||
max-width: 480px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: var(--ha-space-3);
|
||||
}
|
||||
.empty-state h2 {
|
||||
margin: 0;
|
||||
font-size: var(--ha-font-size-xl);
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
}
|
||||
.empty-state p {
|
||||
margin: 0;
|
||||
color: var(--ha-color-text-secondary);
|
||||
line-height: var(--ha-line-height-expanded);
|
||||
}
|
||||
.empty-state-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: var(--ha-space-2);
|
||||
margin-top: var(--ha-space-4);
|
||||
}
|
||||
.empty-state-actions ha-entity-picker {
|
||||
--ha-generic-picker-min-width: 420px;
|
||||
--ha-generic-picker-max-width: 480px;
|
||||
}
|
||||
.sidebar {
|
||||
flex: 0 0 320px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-inline-end: 1px solid var(--divider-color);
|
||||
overflow: auto;
|
||||
}
|
||||
ha-md-list {
|
||||
padding: var(--ha-space-2) 0;
|
||||
}
|
||||
.entity-row {
|
||||
--md-list-item-leading-space: var(--ha-space-3);
|
||||
--md-list-item-trailing-space: var(--ha-space-1);
|
||||
}
|
||||
.add-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--ha-space-2);
|
||||
padding: var(--ha-space-2) var(--ha-space-3);
|
||||
border-top: 1px solid var(--divider-color);
|
||||
}
|
||||
.add-row ha-entity-picker {
|
||||
flex: 1;
|
||||
}
|
||||
.content {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
.recipes {
|
||||
display: grid;
|
||||
gap: var(--ha-space-3);
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
padding: var(--ha-space-3);
|
||||
}
|
||||
.card {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: var(
|
||||
--ha-card-border-radius,
|
||||
var(--ha-border-radius-lg)
|
||||
);
|
||||
background: var(--primary-background-color, #fafafa);
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border: var(--ha-card-border-width, 1px) solid
|
||||
var(--ha-card-border-color, var(--divider-color));
|
||||
}
|
||||
.card-header {
|
||||
color: var(--ha-card-header-color, var(--primary-text-color));
|
||||
font-family: var(--ha-card-header-font-family, inherit);
|
||||
font-size: var(--ha-font-size-m);
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
padding: var(--ha-space-3) var(--ha-space-4);
|
||||
text-align: center;
|
||||
}
|
||||
.preview {
|
||||
pointer-events: none;
|
||||
margin: var(--ha-space-4);
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.preview > :first-child {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
.overlay {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 1;
|
||||
box-sizing: border-box;
|
||||
border-radius: var(
|
||||
--ha-card-border-radius,
|
||||
var(--ha-border-radius-lg)
|
||||
);
|
||||
}
|
||||
.more-badge {
|
||||
margin: 0 var(--ha-space-4) var(--ha-space-3);
|
||||
padding: var(--ha-space-1) var(--ha-space-2);
|
||||
align-self: flex-start;
|
||||
border-radius: var(--ha-border-radius-md);
|
||||
background: var(--ha-color-fill-neutral-quiet-resting);
|
||||
color: var(--ha-color-text-secondary);
|
||||
font-size: var(--ha-font-size-s);
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
:host {
|
||||
flex-direction: column;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.sidebar,
|
||||
.content {
|
||||
flex: 0 0 auto;
|
||||
overflow: visible;
|
||||
}
|
||||
.sidebar {
|
||||
border-inline-end: none;
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
}
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-recipe-picker": HuiRecipePicker;
|
||||
}
|
||||
}
|
||||
@@ -24,119 +24,20 @@ import {
|
||||
stripCustomPrefix,
|
||||
} from "../../../../data/lovelace_custom_cards";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import { supportsAlarmModesCardFeature } from "../../card-features/hui-alarm-modes-card-feature";
|
||||
import { supportsAreaControlsCardFeature } from "../../card-features/hui-area-controls-card-feature";
|
||||
import { supportsBarGaugeCardFeature } from "../../card-features/hui-bar-gauge-card-feature";
|
||||
import { supportsButtonCardFeature } from "../../card-features/hui-button-card-feature";
|
||||
import { supportsClimateFanModesCardFeature } from "../../card-features/hui-climate-fan-modes-card-feature";
|
||||
import { supportsClimateHvacModesCardFeature } from "../../card-features/hui-climate-hvac-modes-card-feature";
|
||||
import { supportsClimatePresetModesCardFeature } from "../../card-features/hui-climate-preset-modes-card-feature";
|
||||
import { supportsClimateSwingHorizontalModesCardFeature } from "../../card-features/hui-climate-swing-horizontal-modes-card-feature";
|
||||
import { supportsClimateSwingModesCardFeature } from "../../card-features/hui-climate-swing-modes-card-feature";
|
||||
import { supportsCounterActionsCardFeature } from "../../card-features/hui-counter-actions-card-feature";
|
||||
import { supportsCoverOpenCloseCardFeature } from "../../card-features/hui-cover-open-close-card-feature";
|
||||
import { supportsCoverPositionFavoriteCardFeature } from "../../card-features/hui-cover-position-favorite-card-feature";
|
||||
import { supportsCoverPositionCardFeature } from "../../card-features/hui-cover-position-card-feature";
|
||||
import { supportsCoverTiltCardFeature } from "../../card-features/hui-cover-tilt-card-feature";
|
||||
import { supportsCoverTiltFavoriteCardFeature } from "../../card-features/hui-cover-tilt-favorite-card-feature";
|
||||
import { supportsCoverTiltPositionCardFeature } from "../../card-features/hui-cover-tilt-position-card-feature";
|
||||
import { supportsDateSetCardFeature } from "../../card-features/hui-date-set-card-feature";
|
||||
import { supportsFanDirectionCardFeature } from "../../card-features/hui-fan-direction-card-feature";
|
||||
import { supportsHourlyForecastCardFeature } from "../../card-features/hui-hourly-forecast-card-feature";
|
||||
import { supportsFanOscilatteCardFeature } from "../../card-features/hui-fan-oscillate-card-feature";
|
||||
import { supportsFanPresetModesCardFeature } from "../../card-features/hui-fan-preset-modes-card-feature";
|
||||
import { supportsFanSpeedCardFeature } from "../../card-features/hui-fan-speed-card-feature";
|
||||
import { supportsHumidifierModesCardFeature } from "../../card-features/hui-humidifier-modes-card-feature";
|
||||
import { supportsHumidifierToggleCardFeature } from "../../card-features/hui-humidifier-toggle-card-feature";
|
||||
import { supportsLawnMowerCommandCardFeature } from "../../card-features/hui-lawn-mower-commands-card-feature";
|
||||
import { supportsLightBrightnessCardFeature } from "../../card-features/hui-light-brightness-card-feature";
|
||||
import { supportsLightColorTempCardFeature } from "../../card-features/hui-light-color-temp-card-feature";
|
||||
import { supportsLockCommandsCardFeature } from "../../card-features/hui-lock-commands-card-feature";
|
||||
import { supportsLockOpenDoorCardFeature } from "../../card-features/hui-lock-open-door-card-feature";
|
||||
import { supportsMediaPlayerPlaybackCardFeature } from "../../card-features/hui-media-player-playback-card-feature";
|
||||
import { supportsMediaPlayerSoundModeCardFeature } from "../../card-features/hui-media-player-sound-mode-card-feature";
|
||||
import { supportsMediaPlayerSourceCardFeature } from "../../card-features/hui-media-player-source-card-feature";
|
||||
import { supportsMediaPlayerVolumeButtonsCardFeature } from "../../card-features/hui-media-player-volume-buttons-card-feature";
|
||||
import { supportsMediaPlayerVolumeSliderCardFeature } from "../../card-features/hui-media-player-volume-slider-card-feature";
|
||||
import { supportsNumericInputCardFeature } from "../../card-features/hui-numeric-input-card-feature";
|
||||
import { supportsSelectOptionsCardFeature } from "../../card-features/hui-select-options-card-feature";
|
||||
import { supportsTargetHumidityCardFeature } from "../../card-features/hui-target-humidity-card-feature";
|
||||
import { supportsTargetTemperatureCardFeature } from "../../card-features/hui-target-temperature-card-feature";
|
||||
import { supportsToggleCardFeature } from "../../card-features/hui-toggle-card-feature";
|
||||
import { supportsTrendGraphCardFeature } from "../../card-features/hui-trend-graph-card-feature";
|
||||
import { supportsUpdateActionsCardFeature } from "../../card-features/hui-update-actions-card-feature";
|
||||
import { supportsVacuumCommandsCardFeature } from "../../card-features/hui-vacuum-commands-card-feature";
|
||||
import { supportsValveOpenCloseCardFeature } from "../../card-features/hui-valve-open-close-card-feature";
|
||||
import { supportsValvePositionFavoriteCardFeature } from "../../card-features/hui-valve-position-favorite-card-feature";
|
||||
import { supportsValvePositionCardFeature } from "../../card-features/hui-valve-position-card-feature";
|
||||
import { supportsWaterHeaterOperationModesCardFeature } from "../../card-features/hui-water-heater-operation-modes-card-feature";
|
||||
import type {
|
||||
LovelaceCardFeatureConfig,
|
||||
LovelaceCardFeatureContext,
|
||||
} from "../../card-features/types";
|
||||
import type { UiFeatureType } from "../../card-features/registry";
|
||||
import {
|
||||
SUPPORTS_FEATURE_TYPES,
|
||||
UI_FEATURE_TYPES,
|
||||
} from "../../card-features/registry";
|
||||
import { getCardFeatureElementClass } from "../../create-element/create-card-feature-element";
|
||||
import { supportsLightColorFavoritesCardFeature } from "../../card-features/hui-light-color-favorites-card-feature";
|
||||
|
||||
export type FeatureType = LovelaceCardFeatureConfig["type"];
|
||||
|
||||
type SupportsFeature = (
|
||||
hass: HomeAssistant,
|
||||
context: LovelaceCardFeatureContext
|
||||
) => boolean;
|
||||
|
||||
const UI_FEATURE_TYPES = [
|
||||
"alarm-modes",
|
||||
"area-controls",
|
||||
"bar-gauge",
|
||||
"button",
|
||||
"climate-fan-modes",
|
||||
"climate-hvac-modes",
|
||||
"climate-preset-modes",
|
||||
"climate-swing-modes",
|
||||
"climate-swing-horizontal-modes",
|
||||
"counter-actions",
|
||||
"cover-open-close",
|
||||
"cover-position-favorite",
|
||||
"cover-position",
|
||||
"cover-tilt-favorite",
|
||||
"cover-tilt-position",
|
||||
"cover-tilt",
|
||||
"date-set",
|
||||
"fan-direction",
|
||||
"fan-oscillate",
|
||||
"fan-preset-modes",
|
||||
"fan-speed",
|
||||
"hourly-forecast",
|
||||
"humidifier-modes",
|
||||
"humidifier-toggle",
|
||||
"lawn-mower-commands",
|
||||
"light-brightness",
|
||||
"light-color-temp",
|
||||
"light-color-favorites",
|
||||
"lock-commands",
|
||||
"lock-open-door",
|
||||
"media-player-playback",
|
||||
"media-player-sound-mode",
|
||||
"media-player-source",
|
||||
"media-player-volume-buttons",
|
||||
"media-player-volume-slider",
|
||||
"numeric-input",
|
||||
"select-options",
|
||||
"trend-graph",
|
||||
"target-humidity",
|
||||
"target-temperature",
|
||||
"toggle",
|
||||
"update-actions",
|
||||
"vacuum-commands",
|
||||
"valve-open-close",
|
||||
"valve-position-favorite",
|
||||
"valve-position",
|
||||
"water-heater-operation-modes",
|
||||
] as const satisfies readonly FeatureType[];
|
||||
|
||||
type UiFeatureTypes = (typeof UI_FEATURE_TYPES)[number];
|
||||
|
||||
const EDITABLES_FEATURE_TYPES = new Set<UiFeatureTypes>([
|
||||
const EDITABLES_FEATURE_TYPES = new Set<UiFeatureType>([
|
||||
"alarm-modes",
|
||||
"area-controls",
|
||||
"bar-gauge",
|
||||
@@ -165,60 +66,6 @@ const EDITABLES_FEATURE_TYPES = new Set<UiFeatureTypes>([
|
||||
"water-heater-operation-modes",
|
||||
]);
|
||||
|
||||
const SUPPORTS_FEATURE_TYPES: Record<
|
||||
UiFeatureTypes,
|
||||
SupportsFeature | undefined
|
||||
> = {
|
||||
"alarm-modes": supportsAlarmModesCardFeature,
|
||||
"area-controls": supportsAreaControlsCardFeature,
|
||||
"bar-gauge": supportsBarGaugeCardFeature,
|
||||
button: supportsButtonCardFeature,
|
||||
"climate-fan-modes": supportsClimateFanModesCardFeature,
|
||||
"climate-swing-modes": supportsClimateSwingModesCardFeature,
|
||||
"climate-swing-horizontal-modes":
|
||||
supportsClimateSwingHorizontalModesCardFeature,
|
||||
"climate-hvac-modes": supportsClimateHvacModesCardFeature,
|
||||
"climate-preset-modes": supportsClimatePresetModesCardFeature,
|
||||
"counter-actions": supportsCounterActionsCardFeature,
|
||||
"cover-open-close": supportsCoverOpenCloseCardFeature,
|
||||
"cover-position-favorite": supportsCoverPositionFavoriteCardFeature,
|
||||
"cover-position": supportsCoverPositionCardFeature,
|
||||
"cover-tilt-favorite": supportsCoverTiltFavoriteCardFeature,
|
||||
"cover-tilt-position": supportsCoverTiltPositionCardFeature,
|
||||
"cover-tilt": supportsCoverTiltCardFeature,
|
||||
"date-set": supportsDateSetCardFeature,
|
||||
"fan-direction": supportsFanDirectionCardFeature,
|
||||
"fan-oscillate": supportsFanOscilatteCardFeature,
|
||||
"fan-preset-modes": supportsFanPresetModesCardFeature,
|
||||
"fan-speed": supportsFanSpeedCardFeature,
|
||||
"hourly-forecast": supportsHourlyForecastCardFeature,
|
||||
"humidifier-modes": supportsHumidifierModesCardFeature,
|
||||
"humidifier-toggle": supportsHumidifierToggleCardFeature,
|
||||
"lawn-mower-commands": supportsLawnMowerCommandCardFeature,
|
||||
"light-brightness": supportsLightBrightnessCardFeature,
|
||||
"light-color-temp": supportsLightColorTempCardFeature,
|
||||
"light-color-favorites": supportsLightColorFavoritesCardFeature,
|
||||
"lock-commands": supportsLockCommandsCardFeature,
|
||||
"lock-open-door": supportsLockOpenDoorCardFeature,
|
||||
"media-player-playback": supportsMediaPlayerPlaybackCardFeature,
|
||||
"media-player-sound-mode": supportsMediaPlayerSoundModeCardFeature,
|
||||
"media-player-source": supportsMediaPlayerSourceCardFeature,
|
||||
"media-player-volume-buttons": supportsMediaPlayerVolumeButtonsCardFeature,
|
||||
"media-player-volume-slider": supportsMediaPlayerVolumeSliderCardFeature,
|
||||
"numeric-input": supportsNumericInputCardFeature,
|
||||
"select-options": supportsSelectOptionsCardFeature,
|
||||
"trend-graph": supportsTrendGraphCardFeature,
|
||||
"target-humidity": supportsTargetHumidityCardFeature,
|
||||
"target-temperature": supportsTargetTemperatureCardFeature,
|
||||
toggle: supportsToggleCardFeature,
|
||||
"update-actions": supportsUpdateActionsCardFeature,
|
||||
"vacuum-commands": supportsVacuumCommandsCardFeature,
|
||||
"valve-open-close": supportsValveOpenCloseCardFeature,
|
||||
"valve-position-favorite": supportsValvePositionFavoriteCardFeature,
|
||||
"valve-position": supportsValvePositionCardFeature,
|
||||
"water-heater-operation-modes": supportsWaterHeaterOperationModesCardFeature,
|
||||
};
|
||||
|
||||
const customCardFeatures = getCustomCardFeatures();
|
||||
|
||||
const CUSTOM_FEATURE_ENTRIES: Record<
|
||||
@@ -276,7 +123,7 @@ export const supportsFeaturesType = (
|
||||
}
|
||||
}
|
||||
|
||||
const supportsFeature = SUPPORTS_FEATURE_TYPES[type];
|
||||
const supportsFeature = SUPPORTS_FEATURE_TYPES[type as UiFeatureType];
|
||||
return !supportsFeature || supportsFeature(hass, context);
|
||||
};
|
||||
|
||||
|
||||
@@ -10089,7 +10089,11 @@
|
||||
"domain": "Domain",
|
||||
"entity": "Entity",
|
||||
"by_entity": "By entity",
|
||||
"by_card": "By card"
|
||||
"by_card": "By card",
|
||||
"empty_title": "What do you want to control?",
|
||||
"empty_description": "Pick an entity to see suggested cards, or browse all card types.",
|
||||
"browse_cards": "Browse all cards",
|
||||
"more_cards": "+{count} more"
|
||||
},
|
||||
"badge_picker": {
|
||||
"no_description": "No description available.",
|
||||
|
||||
141
test/panels/lovelace/common/card-suggestions.test.ts
Normal file
141
test/panels/lovelace/common/card-suggestions.test.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { CARD_SUGGESTION_PROVIDERS } from "../../../../src/panels/lovelace/card-suggestions/registry";
|
||||
import type { CardSuggestionProvider } from "../../../../src/panels/lovelace/card-suggestions/types";
|
||||
import { generateCardSuggestions } from "../../../../src/panels/lovelace/common/card-suggestions";
|
||||
import type { HomeAssistant } from "../../../../src/types";
|
||||
|
||||
const makeState = (
|
||||
entityId: string,
|
||||
state = "on",
|
||||
attributes: Record<string, unknown> = {}
|
||||
): HassEntity => ({
|
||||
entity_id: entityId,
|
||||
state,
|
||||
attributes,
|
||||
last_changed: "",
|
||||
last_updated: "",
|
||||
context: { id: "", parent_id: null, user_id: null },
|
||||
});
|
||||
|
||||
const makeHass = (states: HassEntity[]): HomeAssistant =>
|
||||
({
|
||||
states: Object.fromEntries(states.map((s) => [s.entity_id, s])),
|
||||
}) as unknown as HomeAssistant;
|
||||
|
||||
const registerTestProviders = (
|
||||
providers: Record<string, CardSuggestionProvider>
|
||||
): (() => void) => {
|
||||
const keys = Object.keys(providers);
|
||||
for (const key of keys) {
|
||||
CARD_SUGGESTION_PROVIDERS[key] = providers[key];
|
||||
}
|
||||
return () => {
|
||||
for (const key of keys) {
|
||||
delete CARD_SUGGESTION_PROVIDERS[key];
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
describe("generateCardSuggestions", () => {
|
||||
let cleanupProviders: (() => void) | undefined;
|
||||
|
||||
afterEach(() => {
|
||||
cleanupProviders?.();
|
||||
cleanupProviders = undefined;
|
||||
});
|
||||
|
||||
it("suggests nothing when no entities are picked", () => {
|
||||
expect(generateCardSuggestions(makeHass([]), [])).toEqual([]);
|
||||
});
|
||||
|
||||
it("suggests nothing when the picked entity doesn't exist", () => {
|
||||
expect(generateCardSuggestions(makeHass([]), ["light.ghost"])).toEqual([]);
|
||||
});
|
||||
|
||||
it("ignores unknown entities when picking multiple", () => {
|
||||
const hass = makeHass([
|
||||
makeState("light.a", "on", { supported_color_modes: ["onoff"] }),
|
||||
]);
|
||||
// "light.ghost" has no state — only light.a remains, single-entity path.
|
||||
const suggestions = generateCardSuggestions(hass, [
|
||||
"light.a",
|
||||
"light.ghost",
|
||||
]);
|
||||
expect(suggestions.some((s) => s.id === "tile")).toBe(true);
|
||||
expect(suggestions.every((s) => s.id !== "grid-of-tiles")).toBe(true);
|
||||
});
|
||||
|
||||
it("suggests a grid and an entities card when picking multiple entities", () => {
|
||||
const hass = makeHass([
|
||||
makeState("light.a", "on"),
|
||||
makeState("light.b", "on"),
|
||||
makeState("light.c", "on"),
|
||||
]);
|
||||
|
||||
const suggestions = generateCardSuggestions(hass, [
|
||||
"light.a",
|
||||
"light.b",
|
||||
"light.c",
|
||||
]);
|
||||
|
||||
expect(suggestions.map((s) => s.id)).toEqual([
|
||||
"grid-of-tiles",
|
||||
"entities-card",
|
||||
]);
|
||||
expect(suggestions[0].config.type).toBe("grid");
|
||||
expect(suggestions[1].config).toEqual({
|
||||
type: "entities",
|
||||
entities: ["light.a", "light.b", "light.c"],
|
||||
});
|
||||
});
|
||||
|
||||
it("accepts null, a single suggestion, or a list from each provider", () => {
|
||||
cleanupProviders = registerTestProviders({
|
||||
"test-null": { getEntitySuggestion: () => null },
|
||||
"test-single": {
|
||||
getEntitySuggestion: (_hass, entityId) => ({
|
||||
id: "single",
|
||||
label: "Single",
|
||||
config: { type: "custom:test-single", entity: entityId },
|
||||
}),
|
||||
},
|
||||
"test-array": {
|
||||
getEntitySuggestion: (_hass, entityId) => [
|
||||
{
|
||||
id: "array-a",
|
||||
label: "Array A",
|
||||
config: { type: "custom:test-array", entity: entityId },
|
||||
},
|
||||
{
|
||||
id: "array-b",
|
||||
label: "Array B",
|
||||
config: { type: "custom:test-array", entity: entityId },
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const hass = makeHass([makeState("sensor.a", "1")]);
|
||||
const ids = generateCardSuggestions(hass, ["sensor.a"]).map((s) => s.id);
|
||||
|
||||
expect(ids).toContain("single");
|
||||
expect(ids).toContain("array-a");
|
||||
expect(ids).toContain("array-b");
|
||||
});
|
||||
|
||||
it("keeps working when a provider throws", () => {
|
||||
cleanupProviders = registerTestProviders({
|
||||
"test-throws": {
|
||||
getEntitySuggestion: () => {
|
||||
throw new Error("boom");
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const hass = makeHass([makeState("sensor.a", "1")]);
|
||||
const ids = generateCardSuggestions(hass, ["sensor.a"]).map((s) => s.id);
|
||||
// Tile still provides its default suggestion for sensor.
|
||||
expect(ids).toContain("tile");
|
||||
});
|
||||
});
|
||||
@@ -1,9 +1,5 @@
|
||||
import { beforeAll } from "vitest";
|
||||
global.window = (global.window ?? {}) as any;
|
||||
global.navigator = (global.navigator ?? {}) as any;
|
||||
|
||||
beforeAll(() => {
|
||||
global.window = {} as any;
|
||||
global.navigator = {} as any;
|
||||
|
||||
global.__DEMO__ = false;
|
||||
global.__DEV__ = false;
|
||||
});
|
||||
global.__DEMO__ = false;
|
||||
global.__DEV__ = false;
|
||||
|
||||
Reference in New Issue
Block a user