mirror of
https://github.com/home-assistant/frontend.git
synced 2026-10-21 13:13:39 +00:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
98d3d31c30 | ||
|
|
03795d29e3 | ||
|
|
45ee91aed2 |
No files matched your search
@@ -0,0 +1,69 @@
|
||||
import type {
|
||||
DataTableFiltersValue,
|
||||
DataTableFiltersValues,
|
||||
} from "../../data/data_table_filters";
|
||||
import { dataTableQueryParamConfig } from "./data-table-query-params";
|
||||
import {
|
||||
decodeQueryParams,
|
||||
hasQueryParams,
|
||||
type QueryParamConfig,
|
||||
type QueryParamValues,
|
||||
type SearchParamsSource,
|
||||
} from "./query-params";
|
||||
|
||||
// The filter-pane keys of the backups table's filters
|
||||
export const BACKUP_TYPE_FILTER = "ha-filter-states";
|
||||
export const BACKUP_LOCATIONS_FILTER = "backup-locations";
|
||||
|
||||
// The filters of the backups table. Search, sorting and grouping are kept by
|
||||
// the data table, see data-table-query-params.
|
||||
export const backupsFilterQueryParamConfig = {
|
||||
list: ["type", "location"],
|
||||
} as const satisfies QueryParamConfig;
|
||||
|
||||
export type BackupsFilterQueryParams = QueryParamValues<
|
||||
typeof backupsFilterQueryParamConfig
|
||||
>;
|
||||
|
||||
const list = (value: DataTableFiltersValue): string[] | undefined =>
|
||||
Array.isArray(value) ? value : undefined;
|
||||
|
||||
export const backupsFilterQueryParams = (
|
||||
filters: DataTableFiltersValues
|
||||
): BackupsFilterQueryParams => ({
|
||||
type: list(filters[BACKUP_TYPE_FILTER]),
|
||||
location: list(filters[BACKUP_LOCATIONS_FILTER]),
|
||||
});
|
||||
|
||||
// What a link says to show, undefined when it has no table params so what was
|
||||
// picked before stays. A link replaces the search and the filters, ones left
|
||||
// behind would hide part of what it shows. The table applies its sorting and
|
||||
// grouping.
|
||||
export interface BackupsLink {
|
||||
search: string;
|
||||
filters: DataTableFiltersValues;
|
||||
}
|
||||
|
||||
export const backupsLinkFromUrl = (
|
||||
searchParams: SearchParamsSource
|
||||
): BackupsLink | undefined => {
|
||||
if (
|
||||
!hasQueryParams(searchParams, backupsFilterQueryParamConfig) &&
|
||||
!hasQueryParams(searchParams, dataTableQueryParamConfig)
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const params = decodeQueryParams(searchParams, backupsFilterQueryParamConfig);
|
||||
const { search } = decodeQueryParams(searchParams, dataTableQueryParamConfig);
|
||||
|
||||
return {
|
||||
search: search ?? "",
|
||||
filters: {
|
||||
// `type=all` asks for every type, as the backups overview links do
|
||||
[BACKUP_TYPE_FILTER]:
|
||||
params.type?.filter((type) => type && type !== "all") ?? [],
|
||||
[BACKUP_LOCATIONS_FILTER]: params.location?.filter(Boolean) ?? [],
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -12,12 +12,20 @@ import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import { QueryParamsController } from "../../../common/controllers/query-params-controller";
|
||||
import { relativeTime } from "../../../common/datetime/relative_time";
|
||||
import { storage } from "../../../common/decorators/storage";
|
||||
import { fireEvent, type HASSDomEvent } from "../../../common/dom/fire_event";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
import { navigate } from "../../../common/navigate";
|
||||
import type { LocalizeFunc } from "../../../common/translations/localize";
|
||||
import {
|
||||
BACKUP_LOCATIONS_FILTER,
|
||||
BACKUP_TYPE_FILTER,
|
||||
backupsFilterQueryParamConfig,
|
||||
backupsFilterQueryParams,
|
||||
backupsLinkFromUrl,
|
||||
} from "../../../common/url/backups-query-params";
|
||||
import type {
|
||||
DataTableColumnContainer,
|
||||
DataTableRowData,
|
||||
@@ -79,9 +87,6 @@ interface BackupRow extends DataTableRowData, BackupContent {
|
||||
agent_ids: string[];
|
||||
}
|
||||
|
||||
const TYPE_FILTER = "ha-filter-states";
|
||||
const LOCATIONS_FILTER = "backup-locations";
|
||||
|
||||
@customElement("ha-config-backup-backups")
|
||||
class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@@ -113,7 +118,7 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
|
||||
})
|
||||
private _storageFilters: DataTableFiltersValues = {};
|
||||
|
||||
private _fromUrl = false;
|
||||
@state() private _search = "";
|
||||
|
||||
@storage({ key: "backups-table-grouping", state: false, subscribe: false })
|
||||
private _activeGrouping?: string = "formatted_type";
|
||||
@@ -134,38 +139,25 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
|
||||
|
||||
private _overflowBackup?: BackupRow;
|
||||
|
||||
// The filters are kept in the URL by the page, the search, sorting and
|
||||
// grouping by the table
|
||||
private _filterParams = new QueryParamsController(
|
||||
this,
|
||||
backupsFilterQueryParamConfig,
|
||||
{
|
||||
enabled: () => true,
|
||||
navigated: () => this._applyLink(),
|
||||
}
|
||||
);
|
||||
|
||||
protected willUpdate(changedProps: PropertyValues) {
|
||||
super.willUpdate(changedProps);
|
||||
if (!this.hasUpdated) {
|
||||
this._filters = this._storageFilters;
|
||||
this._setFiltersFromUrl();
|
||||
this._applyLink();
|
||||
}
|
||||
}
|
||||
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
// Re-apply the type filter from the URL when the page is (re)displayed,
|
||||
// e.g. when navigating back to a cached instance of this page with a
|
||||
// different `type` query param.
|
||||
this._setFiltersFromUrl();
|
||||
window.addEventListener("location-changed", this._locationChanged);
|
||||
window.addEventListener("popstate", this._popState);
|
||||
}
|
||||
|
||||
disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
window.removeEventListener("location-changed", this._locationChanged);
|
||||
window.removeEventListener("popstate", this._popState);
|
||||
}
|
||||
|
||||
private _locationChanged = () => {
|
||||
this._setFiltersFromUrl();
|
||||
};
|
||||
|
||||
private _popState = () => {
|
||||
this._setFiltersFromUrl();
|
||||
};
|
||||
|
||||
private _columns = memoizeOne(
|
||||
(
|
||||
localize: LocalizeFunc,
|
||||
@@ -197,6 +189,7 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
|
||||
filterable: true,
|
||||
sortable: true,
|
||||
groupable: true,
|
||||
urlName: "type",
|
||||
},
|
||||
locations: {
|
||||
title: localize("ui.panel.config.backup.locations"),
|
||||
@@ -355,16 +348,19 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
|
||||
localize: LocalizeFunc,
|
||||
isHassio: boolean
|
||||
): BackupRow[] => {
|
||||
const typeFilter = filters[TYPE_FILTER] as string[] | undefined;
|
||||
const locationFilter = filters[LOCATIONS_FILTER] as string[] | undefined;
|
||||
const typeFilter = filters[BACKUP_TYPE_FILTER];
|
||||
const locationFilter = filters[BACKUP_LOCATIONS_FILTER];
|
||||
|
||||
let filteredBackups = backups;
|
||||
if (typeFilter?.length) {
|
||||
|
||||
if (Array.isArray(typeFilter) && typeFilter.length) {
|
||||
filteredBackups = filteredBackups.filter((backup) => {
|
||||
const type = computeBackupType(backup, isHassio);
|
||||
return typeFilter.includes(type);
|
||||
});
|
||||
}
|
||||
if (locationFilter?.length) {
|
||||
|
||||
if (Array.isArray(locationFilter) && locationFilter.length) {
|
||||
filteredBackups = filteredBackups.filter((backup) =>
|
||||
Object.keys(backup.agents).some((agentId) =>
|
||||
locationFilter.includes(agentId)
|
||||
@@ -462,6 +458,9 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
|
||||
@selection-changed=${this._handleSelectionChanged}
|
||||
@clear-filter=${this._clearFilter}
|
||||
.route=${this.route}
|
||||
sync-url
|
||||
.filter=${this._search}
|
||||
@search-changed=${this._handleSearchChange}
|
||||
@row-click=${this._showBackupDetails}
|
||||
.columns=${this._columns(this.hass.localize, maxDisplayedAgents)}
|
||||
.data=${data}
|
||||
@@ -518,7 +517,7 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
|
||||
|
||||
<ha-filter-states
|
||||
.label=${this.hass.localize("ui.panel.config.backup.backup_type")}
|
||||
.value=${this._filters[TYPE_FILTER]}
|
||||
.value=${this._filters[BACKUP_TYPE_FILTER]}
|
||||
.states=${this._states(this.hass.localize, isHassio)}
|
||||
@data-table-filter-changed=${this._typeFilterChanged}
|
||||
slot="filter-pane"
|
||||
@@ -526,7 +525,7 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
|
||||
></ha-filter-states>
|
||||
<ha-filter-states
|
||||
.label=${this.hass.localize("ui.panel.config.backup.locations")}
|
||||
.value=${this._filters[LOCATIONS_FILTER]}
|
||||
.value=${this._filters[BACKUP_LOCATIONS_FILTER]}
|
||||
.states=${this._locations(
|
||||
this.hass.localize,
|
||||
this.agents,
|
||||
@@ -589,38 +588,49 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
|
||||
);
|
||||
|
||||
private _typeFilterChanged(ev) {
|
||||
this._filters = { ...this._filters, [TYPE_FILTER]: ev.detail.value };
|
||||
if (!this._fromUrl) {
|
||||
this._storageFilters = this._filters;
|
||||
}
|
||||
this._filters = { ...this._filters, [BACKUP_TYPE_FILTER]: ev.detail.value };
|
||||
this._storageFilters = this._filters;
|
||||
this._writeFilters();
|
||||
}
|
||||
|
||||
private _locationsFilterChanged(ev) {
|
||||
this._filters = { ...this._filters, [LOCATIONS_FILTER]: ev.detail.value };
|
||||
if (!this._fromUrl) {
|
||||
this._storageFilters = this._filters;
|
||||
}
|
||||
this._filters = {
|
||||
...this._filters,
|
||||
[BACKUP_LOCATIONS_FILTER]: ev.detail.value,
|
||||
};
|
||||
this._storageFilters = this._filters;
|
||||
this._writeFilters();
|
||||
}
|
||||
|
||||
private _clearFilter() {
|
||||
this._filters = {};
|
||||
if (!this._fromUrl) {
|
||||
this._storageFilters = {};
|
||||
}
|
||||
this._storageFilters = {};
|
||||
this._writeFilters();
|
||||
}
|
||||
|
||||
private _setFiltersFromUrl() {
|
||||
const searchParams = new URLSearchParams(window.location.search);
|
||||
const type = searchParams.get("type");
|
||||
private _handleSearchChange(
|
||||
ev: HASSDomEvent<HASSDomEvents["search-changed"]>
|
||||
) {
|
||||
this._search = ev.detail.value;
|
||||
}
|
||||
|
||||
if (!type) {
|
||||
return;
|
||||
// Links, like the ones on the backups overview, say in the URL what to show.
|
||||
// The router keeps the page open, so a link can land on it.
|
||||
private _applyLink() {
|
||||
const searchParams = this._filterParams.searchParams();
|
||||
const link = searchParams ? backupsLinkFromUrl(searchParams) : undefined;
|
||||
|
||||
if (link) {
|
||||
this._search = link.search;
|
||||
this._filters = link.filters;
|
||||
this._storageFilters = link.filters;
|
||||
}
|
||||
|
||||
this._fromUrl = true;
|
||||
this._filters = {
|
||||
[TYPE_FILTER]: type === "all" ? [] : [type],
|
||||
};
|
||||
this._writeFilters();
|
||||
}
|
||||
|
||||
private _writeFilters() {
|
||||
this._filterParams.write(backupsFilterQueryParams(this._filters));
|
||||
}
|
||||
|
||||
private get _needsOnboarding() {
|
||||
|
||||
@@ -26,11 +26,20 @@ import type { LocalizeFunc } from "../../../common/translations/localize";
|
||||
import { formattersContext } from "../../../data/context";
|
||||
import type { ActionHandlerEvent } from "../../../data/lovelace/action_handler";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { hasTileCardAction } from "../cards/tile/tile-card-helpers";
|
||||
import { actionHandler } from "../common/directives/action-handler-directive";
|
||||
import { findEntities } from "../common/find-entities";
|
||||
import { hasAction } from "../common/has-action";
|
||||
import type { LovelaceBadge, LovelaceBadgeEditor } from "../types";
|
||||
import type { AlertBadgeConfig } from "./types";
|
||||
|
||||
export const ALERT_BADGE_DEFAULT_CONFIG: Partial<AlertBadgeConfig> = {
|
||||
type: "alert",
|
||||
show_name: true,
|
||||
show_state: true,
|
||||
show_icon: true,
|
||||
};
|
||||
|
||||
@customElement("hui-alert-badge")
|
||||
export class HuiAlertBadge extends LitElement implements LovelaceBadge {
|
||||
public static async getConfigElement(): Promise<LovelaceBadgeEditor> {
|
||||
@@ -55,6 +64,10 @@ export class HuiAlertBadge extends LitElement implements LovelaceBadge {
|
||||
return { type: "alert", entity: entity || "" };
|
||||
}
|
||||
|
||||
public static getDefaultConfig(): Partial<AlertBadgeConfig> {
|
||||
return ALERT_BADGE_DEFAULT_CONFIG;
|
||||
}
|
||||
|
||||
@state() private _config?: AlertBadgeConfig;
|
||||
|
||||
@state()
|
||||
@@ -87,7 +100,7 @@ export class HuiAlertBadge extends LitElement implements LovelaceBadge {
|
||||
);
|
||||
}
|
||||
|
||||
this._config = config;
|
||||
this._config = { ...ALERT_BADGE_DEFAULT_CONFIG, ...config };
|
||||
}
|
||||
|
||||
protected render() {
|
||||
@@ -115,10 +128,21 @@ export class HuiAlertBadge extends LitElement implements LovelaceBadge {
|
||||
const pulse = active && this._config.pulse === true;
|
||||
const hasColor = color !== undefined && color !== "none";
|
||||
|
||||
const name = this._formatters.formatEntityName(stateObj, this._config.name);
|
||||
|
||||
const { show_name: showName, show_state: showState } = this._config;
|
||||
|
||||
const content = showState
|
||||
? this._formatters.formatEntityState(stateObj)
|
||||
: showName
|
||||
? name
|
||||
: undefined;
|
||||
|
||||
return html`
|
||||
<ha-badge
|
||||
type="button"
|
||||
.label=${this._formatters.formatEntityName(stateObj)}
|
||||
.type=${hasTileCardAction(this._config) ? "button" : "badge"}
|
||||
.label=${showState && showName ? name : undefined}
|
||||
.iconOnly=${!content}
|
||||
class=${classMap({ alert: true, pulse, "no-color": !hasColor })}
|
||||
style=${styleMap({
|
||||
"--ha-alert-color":
|
||||
@@ -126,20 +150,29 @@ export class HuiAlertBadge extends LitElement implements LovelaceBadge {
|
||||
})}
|
||||
@action=${this._handleAction}
|
||||
.actionHandler=${actionHandler({
|
||||
hasHold: false,
|
||||
hasDoubleClick: false,
|
||||
hasHold: hasAction(this._config.hold_action),
|
||||
hasDoubleClick: hasAction(this._config.double_tap_action),
|
||||
})}
|
||||
>
|
||||
<ha-state-icon slot="icon" .stateObj=${stateObj}></ha-state-icon>
|
||||
${this._formatters.formatEntityState(stateObj)}
|
||||
${
|
||||
this._config.show_icon
|
||||
? html`<ha-state-icon
|
||||
slot="icon"
|
||||
.stateObj=${stateObj}
|
||||
.icon=${this._config.icon}
|
||||
></ha-state-icon>`
|
||||
: nothing
|
||||
}
|
||||
${content}
|
||||
</ha-badge>
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleAction(ev: ActionHandlerEvent): void {
|
||||
if (ev.detail.action === "tap" && this._config) {
|
||||
fireEvent(this, "hass-more-info", { entityId: this._config.entity });
|
||||
}
|
||||
fireEvent(this, "hass-action", {
|
||||
config: this._config!,
|
||||
action: ev.detail.action!,
|
||||
});
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
|
||||
@@ -8,15 +8,17 @@ import memoizeOne from "memoize-one";
|
||||
import { computeCssColor } from "../../../common/color/compute-color";
|
||||
import { hsv2rgb, rgb2hex, rgb2hsv } from "../../../common/color/convert-color";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
|
||||
import { stateActive } from "../../../common/entity/state_active";
|
||||
import { stateColorCss } from "../../../common/entity/state_color";
|
||||
import "../../../components/ha-badge";
|
||||
import "../../../components/ha-state-icon";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import { cameraUrlWithWidthHeight } from "../../../data/camera";
|
||||
import type { ActionHandlerEvent } from "../../../data/lovelace/action_handler";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import {
|
||||
getTileImageUrl,
|
||||
hasTileCardAction,
|
||||
} from "../cards/tile/tile-card-helpers";
|
||||
import { actionHandler } from "../common/directives/action-handler-directive";
|
||||
import { findEntities } from "../common/find-entities";
|
||||
import { handleAction } from "../common/handle-action";
|
||||
@@ -99,12 +101,7 @@ export class HuiEntityBadge extends LitElement implements LovelaceBadge {
|
||||
}
|
||||
|
||||
get hasAction() {
|
||||
return (
|
||||
!this._config?.tap_action ||
|
||||
hasAction(this._config?.tap_action) ||
|
||||
hasAction(this._config?.hold_action) ||
|
||||
hasAction(this._config?.double_tap_action)
|
||||
);
|
||||
return hasTileCardAction(this._config);
|
||||
}
|
||||
|
||||
private _computeStateColor = memoizeOne(
|
||||
@@ -138,21 +135,6 @@ export class HuiEntityBadge extends LitElement implements LovelaceBadge {
|
||||
}
|
||||
);
|
||||
|
||||
private _getImageUrl(stateObj: HassEntity): string | undefined {
|
||||
const entityPicture =
|
||||
stateObj.attributes.entity_picture_local ||
|
||||
stateObj.attributes.entity_picture;
|
||||
|
||||
if (!entityPicture) return undefined;
|
||||
|
||||
let imageUrl = this.hass!.hassUrl(entityPicture);
|
||||
if (computeStateDomain(stateObj) === "camera") {
|
||||
imageUrl = cameraUrlWithWidthHeight(imageUrl, 32, 32);
|
||||
}
|
||||
|
||||
return imageUrl;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._config || !this.hass) {
|
||||
return nothing;
|
||||
@@ -195,7 +177,7 @@ export class HuiEntityBadge extends LitElement implements LovelaceBadge {
|
||||
const showEntityPicture = this._config.show_entity_picture;
|
||||
|
||||
const imageUrl = showEntityPicture
|
||||
? this._getImageUrl(stateObj)
|
||||
? getTileImageUrl(this.hass, stateObj, 32)
|
||||
: undefined;
|
||||
|
||||
const label = showState && showName ? name : undefined;
|
||||
|
||||
@@ -34,9 +34,17 @@ export interface StateLabelBadgeConfig extends LovelaceBadgeConfig {
|
||||
export interface AlertBadgeConfig extends LovelaceBadgeConfig {
|
||||
type: "alert";
|
||||
entity: string;
|
||||
name?: string | EntityNameItem | EntityNameItem[];
|
||||
icon?: string;
|
||||
alert_color?: string;
|
||||
clear_color?: string;
|
||||
pulse?: boolean;
|
||||
show_name?: boolean;
|
||||
show_state?: boolean;
|
||||
show_icon?: boolean;
|
||||
tap_action?: ActionConfig;
|
||||
hold_action?: ActionConfig;
|
||||
double_tap_action?: ActionConfig;
|
||||
}
|
||||
|
||||
export interface ShortcutBadgeConfig extends LovelaceBadgeConfig {
|
||||
|
||||
@@ -25,11 +25,13 @@ import { formattersContext } from "../../../data/context";
|
||||
import type { ActionHandlerEvent } from "../../../data/lovelace/action_handler";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { findEntities } from "../common/find-entities";
|
||||
import { hasAction } from "../common/has-action";
|
||||
import type {
|
||||
LovelaceCard,
|
||||
LovelaceCardEditor,
|
||||
LovelaceGridOptions,
|
||||
} from "../types";
|
||||
import { hasTileCardAction } from "./tile/tile-card-helpers";
|
||||
import { tileCardStyle } from "./tile/tile-card-style";
|
||||
import type { AlertCardConfig } from "./types";
|
||||
|
||||
@@ -84,19 +86,25 @@ export class HuiAlertCard extends LitElement implements LovelaceCard {
|
||||
`Specify an entity from within the ${ALERT_DOMAINS.join(", ")} domains`
|
||||
);
|
||||
}
|
||||
this._config = config;
|
||||
|
||||
this._config = {
|
||||
tap_action: { action: "more-info" },
|
||||
...config,
|
||||
};
|
||||
}
|
||||
|
||||
public getCardSize(): number {
|
||||
return 1;
|
||||
return this._config?.vertical ? 2 : 1;
|
||||
}
|
||||
|
||||
public getGridOptions(): LovelaceGridOptions {
|
||||
const vertical = Boolean(this._config?.vertical);
|
||||
|
||||
return {
|
||||
columns: 6,
|
||||
rows: 1,
|
||||
min_columns: 6,
|
||||
min_rows: 1,
|
||||
rows: vertical ? 2 : 1,
|
||||
min_columns: vertical ? 3 : 6,
|
||||
min_rows: vertical ? 2 : 1,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -110,11 +118,6 @@ export class HuiAlertCard extends LitElement implements LovelaceCard {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const stateDisplay = this._formatters.formatEntityState(stateObj);
|
||||
const areaName = this._formatters.formatEntityName(stateObj, {
|
||||
type: "area",
|
||||
});
|
||||
|
||||
const active = isAlertActive(stateObj);
|
||||
|
||||
const color = active
|
||||
@@ -124,6 +127,10 @@ export class HuiAlertCard extends LitElement implements LovelaceCard {
|
||||
const pulse = active && this._config.pulse === true;
|
||||
const hasColor = color !== undefined && color !== "none";
|
||||
|
||||
const areaName = this._formatters.formatEntityName(stateObj, {
|
||||
type: "area",
|
||||
});
|
||||
|
||||
return html`
|
||||
<ha-card
|
||||
class=${classMap({ pulse, "no-color": !hasColor })}
|
||||
@@ -136,25 +143,40 @@ export class HuiAlertCard extends LitElement implements LovelaceCard {
|
||||
})}
|
||||
>
|
||||
<ha-tile-container
|
||||
.interactive=${true}
|
||||
.actionHandlerOptions=${{ hasHold: false, hasDoubleClick: false }}
|
||||
.vertical=${Boolean(this._config.vertical)}
|
||||
.interactive=${hasTileCardAction(this._config)}
|
||||
.actionHandlerOptions=${{
|
||||
hasHold: hasAction(this._config.hold_action),
|
||||
hasDoubleClick: hasAction(this._config.double_tap_action),
|
||||
}}
|
||||
@action=${this._handleAction}
|
||||
>
|
||||
<ha-tile-icon slot="icon">
|
||||
<ha-state-icon slot="icon" .stateObj=${stateObj}></ha-state-icon>
|
||||
<ha-state-icon
|
||||
slot="icon"
|
||||
.icon=${this._config.icon}
|
||||
.stateObj=${stateObj}
|
||||
></ha-state-icon>
|
||||
</ha-tile-icon>
|
||||
<ha-tile-info slot="info">
|
||||
<span slot="primary"
|
||||
>${this._formatters.formatEntityName(stateObj)}</span
|
||||
>${this._formatters.formatEntityName(
|
||||
stateObj,
|
||||
this._config.name
|
||||
)}</span
|
||||
>
|
||||
<span slot="secondary">
|
||||
${stateDisplay} ·
|
||||
<ha-relative-time
|
||||
.datetime=${stateObj.last_changed}
|
||||
.format=${"short"}
|
||||
></ha-relative-time>
|
||||
${areaName ? html` · ${areaName}` : nothing}
|
||||
</span>
|
||||
${
|
||||
this._config.hide_state
|
||||
? nothing
|
||||
: html`<span slot="secondary">
|
||||
${this._formatters.formatEntityState(stateObj)} ·
|
||||
<ha-relative-time
|
||||
.datetime=${stateObj.last_changed}
|
||||
.format=${"short"}
|
||||
></ha-relative-time>
|
||||
${areaName ? html` · ${areaName}` : nothing}
|
||||
</span>`
|
||||
}
|
||||
</ha-tile-info>
|
||||
</ha-tile-container>
|
||||
</ha-card>
|
||||
@@ -162,9 +184,10 @@ export class HuiAlertCard extends LitElement implements LovelaceCard {
|
||||
}
|
||||
|
||||
private _handleAction(ev: ActionHandlerEvent): void {
|
||||
if (ev.detail.action === "tap" && this._config) {
|
||||
fireEvent(this, "hass-more-info", { entityId: this._config.entity });
|
||||
}
|
||||
fireEvent(this, "hass-action", {
|
||||
config: this._config!,
|
||||
action: ev.detail.action!,
|
||||
});
|
||||
}
|
||||
|
||||
static styles = [
|
||||
|
||||
@@ -8,7 +8,6 @@ import { styleMap } from "lit/directives/style-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { computeCssColor } from "../../../common/color/compute-color";
|
||||
import { hsv2rgb, rgb2hex, rgb2hsv } from "../../../common/color/convert-color";
|
||||
import { DOMAINS_TOGGLE } from "../../../common/const";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
import { stateActive } from "../../../common/entity/state_active";
|
||||
import { stateColorCss } from "../../../common/entity/state_color";
|
||||
@@ -18,7 +17,6 @@ import "../../../components/tile/ha-tile-badge";
|
||||
import "../../../components/tile/ha-tile-container";
|
||||
import "../../../components/tile/ha-tile-icon";
|
||||
import "../../../components/tile/ha-tile-info";
|
||||
import { cameraUrlWithWidthHeight } from "../../../data/camera";
|
||||
import { timerJustFinished } from "../../../data/timer";
|
||||
import type { ActionHandlerEvent } from "../../../data/lovelace/action_handler";
|
||||
import "../../../state-display/state-display";
|
||||
@@ -39,18 +37,16 @@ import type {
|
||||
LovelaceGridOptions,
|
||||
} from "../types";
|
||||
import { renderTileBadge } from "./tile/badges/tile-badge";
|
||||
import {
|
||||
getTileActionDefaults,
|
||||
getTileIconActionConfig,
|
||||
getTileImageUrl,
|
||||
hasTileCardAction,
|
||||
hasTileIconAction,
|
||||
} from "./tile/tile-card-helpers";
|
||||
import { tileCardStyle } from "./tile/tile-card-style";
|
||||
import type { TileCardConfig } from "./types";
|
||||
|
||||
export const getEntityDefaultTileIconAction = (entityId: string) => {
|
||||
const domain = computeDomain(entityId);
|
||||
const supportsIconAction =
|
||||
DOMAINS_TOGGLE.has(domain) ||
|
||||
["button", "input_button", "scene"].includes(domain);
|
||||
|
||||
return supportsIconAction ? "toggle" : "none";
|
||||
};
|
||||
|
||||
@customElement("hui-tile-card")
|
||||
export class HuiTileCard extends LitElement implements LovelaceCard {
|
||||
public static async getConfigElement(): Promise<LovelaceCardEditor> {
|
||||
@@ -124,12 +120,7 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
|
||||
}
|
||||
|
||||
this._config = {
|
||||
tap_action: {
|
||||
action: "more-info",
|
||||
},
|
||||
icon_tap_action: {
|
||||
action: getEntityDefaultTileIconAction(config.entity),
|
||||
},
|
||||
...getTileActionDefaults(config.entity),
|
||||
...config,
|
||||
};
|
||||
this._featureContext = {
|
||||
@@ -184,28 +175,12 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
|
||||
return;
|
||||
}
|
||||
|
||||
const config = {
|
||||
entity: this._config!.entity,
|
||||
tap_action: this._config!.icon_tap_action,
|
||||
hold_action: this._config!.icon_hold_action,
|
||||
double_tap_action: this._config!.icon_double_tap_action,
|
||||
};
|
||||
handleAction(this, this.hass!, config, ev.detail.action!);
|
||||
}
|
||||
|
||||
private _getImageUrl(entity: HassEntity): string | undefined {
|
||||
const entityPicture =
|
||||
entity.attributes.entity_picture_local ||
|
||||
entity.attributes.entity_picture;
|
||||
|
||||
if (!entityPicture) return undefined;
|
||||
|
||||
let imageUrl = this.hass!.hassUrl(entityPicture);
|
||||
if (computeDomain(entity.entity_id) === "camera") {
|
||||
imageUrl = cameraUrlWithWidthHeight(imageUrl, 80, 80);
|
||||
}
|
||||
|
||||
return imageUrl;
|
||||
handleAction(
|
||||
this,
|
||||
this.hass!,
|
||||
getTileIconActionConfig(this._config!.entity, this._config!),
|
||||
ev.detail.action!
|
||||
);
|
||||
}
|
||||
|
||||
private _computeStateColor = memoizeOne(
|
||||
@@ -247,24 +222,6 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
|
||||
}
|
||||
);
|
||||
|
||||
private get _hasCardAction() {
|
||||
return (
|
||||
!this._config?.tap_action ||
|
||||
hasAction(this._config?.tap_action) ||
|
||||
hasAction(this._config?.hold_action) ||
|
||||
hasAction(this._config?.double_tap_action)
|
||||
);
|
||||
}
|
||||
|
||||
private get _hasIconAction() {
|
||||
return (
|
||||
!this._config?.icon_tap_action ||
|
||||
hasAction(this._config?.icon_tap_action) ||
|
||||
hasAction(this._config?.icon_hold_action) ||
|
||||
hasAction(this._config?.icon_double_tap_action)
|
||||
);
|
||||
}
|
||||
|
||||
private _featurePosition = memoizeOne((config: TileCardConfig) => {
|
||||
if (config.vertical) {
|
||||
return "bottom";
|
||||
@@ -317,7 +274,7 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
|
||||
};
|
||||
|
||||
const imageUrl = this._config.show_entity_picture
|
||||
? this._getImageUrl(stateObj)
|
||||
? getTileImageUrl(this.hass, stateObj)
|
||||
: undefined;
|
||||
|
||||
const featurePosition = this._featurePosition(this._config);
|
||||
@@ -334,7 +291,7 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
|
||||
.featurePosition=${featurePosition}
|
||||
.vertical=${Boolean(this._config.vertical)}
|
||||
.fixedInfoHeight=${fixedInfoHeight}
|
||||
.interactive=${this._hasCardAction}
|
||||
.interactive=${hasTileCardAction(this._config)}
|
||||
.actionHandlerOptions=${{
|
||||
hasHold: hasAction(this._config!.hold_action),
|
||||
hasDoubleClick: hasAction(this._config!.double_tap_action),
|
||||
@@ -348,7 +305,7 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
|
||||
hasHold: hasAction(this._config!.icon_hold_action),
|
||||
hasDoubleClick: hasAction(this._config!.icon_double_tap_action),
|
||||
}}
|
||||
.interactive=${this._hasIconAction}
|
||||
.interactive=${hasTileIconAction(this._config)}
|
||||
.accessibleLabel=${name}
|
||||
.imageUrl=${imageUrl}
|
||||
data-domain=${ifDefined(domain)}
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { DOMAINS_TOGGLE } from "../../../../common/const";
|
||||
import { computeDomain } from "../../../../common/entity/compute_domain";
|
||||
import { cameraUrlWithWidthHeight } from "../../../../data/camera";
|
||||
import type { HomeAssistantConnection } from "../../../../types";
|
||||
import type { ActionConfigParams } from "../../common/handle-action";
|
||||
import { hasAction } from "../../common/has-action";
|
||||
import type { TileActionsConfig } from "../types";
|
||||
|
||||
export const getEntityDefaultTileIconAction = (entityId: string) => {
|
||||
const domain = computeDomain(entityId);
|
||||
|
||||
const supportsIconAction =
|
||||
DOMAINS_TOGGLE.has(domain) ||
|
||||
["button", "input_button", "scene"].includes(domain);
|
||||
|
||||
return supportsIconAction ? "toggle" : "none";
|
||||
};
|
||||
|
||||
export const getTileActionDefaults = (
|
||||
entityId: string
|
||||
): Pick<TileActionsConfig, "tap_action" | "icon_tap_action"> => ({
|
||||
tap_action: { action: "more-info" },
|
||||
icon_tap_action: { action: getEntityDefaultTileIconAction(entityId) },
|
||||
});
|
||||
|
||||
export const hasTileCardAction = (config?: TileActionsConfig) =>
|
||||
!config?.tap_action ||
|
||||
hasAction(config.tap_action) ||
|
||||
hasAction(config.hold_action) ||
|
||||
hasAction(config.double_tap_action);
|
||||
|
||||
export const hasTileIconAction = (config?: TileActionsConfig) =>
|
||||
!config?.icon_tap_action ||
|
||||
hasAction(config.icon_tap_action) ||
|
||||
hasAction(config.icon_hold_action) ||
|
||||
hasAction(config.icon_double_tap_action);
|
||||
|
||||
export const getTileIconActionConfig = (
|
||||
entity: string,
|
||||
config: TileActionsConfig
|
||||
): ActionConfigParams => ({
|
||||
entity,
|
||||
tap_action: config.icon_tap_action,
|
||||
hold_action: config.icon_hold_action,
|
||||
double_tap_action: config.icon_double_tap_action,
|
||||
});
|
||||
|
||||
export const getTileImageUrl = (
|
||||
connection: Pick<HomeAssistantConnection, "hassUrl">,
|
||||
stateObj: HassEntity,
|
||||
size = 80
|
||||
): string | undefined => {
|
||||
const entityPicture =
|
||||
stateObj.attributes.entity_picture_local ||
|
||||
stateObj.attributes.entity_picture;
|
||||
|
||||
if (!entityPicture) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const imageUrl = connection.hassUrl(entityPicture);
|
||||
|
||||
return computeDomain(stateObj.entity_id) === "camera"
|
||||
? cameraUrlWithWidthHeight(imageUrl, size, size)
|
||||
: imageUrl;
|
||||
};
|
||||
@@ -687,7 +687,16 @@ export interface WeatherForecastCardConfig extends LovelaceCardConfig {
|
||||
double_tap_action?: ActionConfig;
|
||||
}
|
||||
|
||||
export interface TileCardConfig extends LovelaceCardConfig {
|
||||
export interface TileActionsConfig {
|
||||
tap_action?: ActionConfig;
|
||||
hold_action?: ActionConfig;
|
||||
double_tap_action?: ActionConfig;
|
||||
icon_tap_action?: ActionConfig;
|
||||
icon_hold_action?: ActionConfig;
|
||||
icon_double_tap_action?: ActionConfig;
|
||||
}
|
||||
|
||||
export interface TileCardConfig extends LovelaceCardConfig, TileActionsConfig {
|
||||
entity: string;
|
||||
name?: string | EntityNameItem | EntityNameItem[];
|
||||
hide_state?: boolean;
|
||||
@@ -696,12 +705,6 @@ export interface TileCardConfig extends LovelaceCardConfig {
|
||||
color?: string;
|
||||
show_entity_picture?: boolean;
|
||||
vertical?: boolean;
|
||||
tap_action?: ActionConfig;
|
||||
hold_action?: ActionConfig;
|
||||
double_tap_action?: ActionConfig;
|
||||
icon_tap_action?: ActionConfig;
|
||||
icon_hold_action?: ActionConfig;
|
||||
icon_double_tap_action?: ActionConfig;
|
||||
features?: LovelaceCardFeatureConfig[];
|
||||
features_position?: LovelaceCardFeaturePosition;
|
||||
time_format?: TimestampRenderingFormat;
|
||||
@@ -740,9 +743,16 @@ export interface ShortcutCardConfig extends LovelaceCardConfig {
|
||||
export interface AlertCardConfig extends LovelaceCardConfig {
|
||||
type: "alert";
|
||||
entity: string;
|
||||
name?: string | EntityNameItem | EntityNameItem[];
|
||||
icon?: string;
|
||||
alert_color?: string;
|
||||
clear_color?: string;
|
||||
pulse?: boolean;
|
||||
hide_state?: boolean;
|
||||
vertical?: boolean;
|
||||
tap_action?: ActionConfig;
|
||||
hold_action?: ActionConfig;
|
||||
double_tap_action?: ActionConfig;
|
||||
}
|
||||
|
||||
export interface ToggleGroupCardConfig extends LovelaceCardConfig {
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import { mdiAlertOutline, mdiTextShort } from "@mdi/js";
|
||||
import { DEFAULT_ALERT_COLOR } from "../../../../common/entity/alert_active";
|
||||
import type { LocalizeFunc } from "../../../../common/translations/localize";
|
||||
import type { HaFormSchema } from "../../../../components/ha-form/types";
|
||||
import { contentLayoutSchema, displayedElementsSchema } from "./content-schema";
|
||||
|
||||
export const alertSchema = () =>
|
||||
({
|
||||
name: "alert",
|
||||
type: "expandable",
|
||||
flatten: true,
|
||||
iconPath: mdiAlertOutline,
|
||||
schema: [
|
||||
{
|
||||
name: "",
|
||||
type: "grid",
|
||||
schema: [
|
||||
{
|
||||
name: "clear_color",
|
||||
selector: {
|
||||
ui_color: { include_none: true, default_color: "none" },
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "alert_color",
|
||||
selector: {
|
||||
ui_color: {
|
||||
include_none: true,
|
||||
default_color: DEFAULT_ALERT_COLOR,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{ name: "pulse", selector: { boolean: {} } },
|
||||
],
|
||||
}) as const satisfies HaFormSchema;
|
||||
|
||||
const nameSchema = {
|
||||
name: "name",
|
||||
selector: { entity_name: {} },
|
||||
context: { entity: "entity" },
|
||||
} as const satisfies HaFormSchema;
|
||||
|
||||
const iconSchema = {
|
||||
name: "icon",
|
||||
selector: { icon: {} },
|
||||
context: { icon_entity: "entity" },
|
||||
} as const satisfies HaFormSchema;
|
||||
|
||||
export const alertCardContentSchema = (localize: LocalizeFunc) =>
|
||||
({
|
||||
name: "content",
|
||||
type: "expandable",
|
||||
flatten: true,
|
||||
iconPath: mdiTextShort,
|
||||
schema: [
|
||||
nameSchema,
|
||||
{
|
||||
name: "",
|
||||
type: "grid",
|
||||
schema: [iconSchema, { name: "hide_state", selector: { boolean: {} } }],
|
||||
},
|
||||
contentLayoutSchema(localize),
|
||||
],
|
||||
}) as const satisfies HaFormSchema;
|
||||
|
||||
export const alertBadgeContentSchema = (localize: LocalizeFunc) =>
|
||||
({
|
||||
name: "content",
|
||||
type: "expandable",
|
||||
flatten: true,
|
||||
iconPath: mdiTextShort,
|
||||
schema: [nameSchema, iconSchema, displayedElementsSchema(localize)],
|
||||
}) as const satisfies HaFormSchema;
|
||||
@@ -0,0 +1,246 @@
|
||||
import { mdiTextShort } from "@mdi/js";
|
||||
import type { LocalizeFunc } from "../../../../common/translations/localize";
|
||||
import type { HaFormSchema } from "../../../../components/ha-form/types";
|
||||
|
||||
export interface TileContentFormConfig {
|
||||
hide_state?: boolean;
|
||||
state_content?: string | string[];
|
||||
vertical?: boolean;
|
||||
content_layout?: string;
|
||||
}
|
||||
|
||||
export interface BadgeContentFormConfig {
|
||||
show_name?: boolean;
|
||||
show_state?: boolean;
|
||||
show_icon?: boolean;
|
||||
state_content?: string | string[];
|
||||
displayed_elements?: string[];
|
||||
}
|
||||
|
||||
const colorSchema = {
|
||||
name: "color",
|
||||
selector: {
|
||||
ui_color: {
|
||||
default_color: "state",
|
||||
include_state: true,
|
||||
},
|
||||
},
|
||||
} as const satisfies HaFormSchema;
|
||||
|
||||
export const contentLayoutSchema = (localize: LocalizeFunc) =>
|
||||
({
|
||||
name: "content_layout",
|
||||
required: true,
|
||||
selector: {
|
||||
select: {
|
||||
mode: "box",
|
||||
options: ["horizontal", "vertical"].map((value) => ({
|
||||
label: localize(
|
||||
`ui.panel.lovelace.editor.card.tile.content_layout_options.${value}`
|
||||
),
|
||||
value,
|
||||
image: {
|
||||
src: `/static/images/form/tile_content_layout_${value}.svg`,
|
||||
src_dark: `/static/images/form/tile_content_layout_${value}_dark.svg`,
|
||||
flip_rtl: true,
|
||||
},
|
||||
})),
|
||||
},
|
||||
},
|
||||
}) as const satisfies HaFormSchema;
|
||||
|
||||
export const displayedElementsSchema = (localize: LocalizeFunc) =>
|
||||
({
|
||||
name: "displayed_elements",
|
||||
selector: {
|
||||
select: {
|
||||
mode: "list",
|
||||
multiple: true,
|
||||
options: (["name", "state", "icon"] as const).map((value) => ({
|
||||
value,
|
||||
label: localize(
|
||||
`ui.panel.lovelace.editor.badge.entity.displayed_elements_options.${value}`
|
||||
),
|
||||
})),
|
||||
},
|
||||
},
|
||||
}) as const satisfies HaFormSchema;
|
||||
|
||||
export const tileContentSchema = (
|
||||
localize: LocalizeFunc,
|
||||
showTimeFormat: boolean
|
||||
) =>
|
||||
({
|
||||
name: "content",
|
||||
flatten: true,
|
||||
type: "expandable",
|
||||
iconPath: mdiTextShort,
|
||||
schema: [
|
||||
{
|
||||
name: "name",
|
||||
selector: {
|
||||
entity_name: {},
|
||||
},
|
||||
context: { entity: "entity" },
|
||||
},
|
||||
{
|
||||
name: "",
|
||||
type: "grid",
|
||||
schema: [
|
||||
{
|
||||
name: "icon",
|
||||
selector: {
|
||||
icon: {},
|
||||
},
|
||||
context: { icon_entity: "entity" },
|
||||
},
|
||||
colorSchema,
|
||||
{
|
||||
name: "show_entity_picture",
|
||||
selector: {
|
||||
boolean: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "hide_state",
|
||||
selector: {
|
||||
boolean: {},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "state_content",
|
||||
visible: { field: "hide_state", operator: "not_eq", value: true },
|
||||
selector: {
|
||||
ui_state_content: {
|
||||
allow_context: true,
|
||||
},
|
||||
},
|
||||
context: {
|
||||
filter_entity: "entity",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "time_format",
|
||||
visible: showTimeFormat,
|
||||
selector: {
|
||||
ui_time_format: {},
|
||||
},
|
||||
},
|
||||
contentLayoutSchema(localize),
|
||||
],
|
||||
}) as const satisfies HaFormSchema;
|
||||
|
||||
export const badgeContentSchema = (
|
||||
localize: LocalizeFunc,
|
||||
showTimeFormat: boolean
|
||||
) =>
|
||||
({
|
||||
name: "content",
|
||||
type: "expandable",
|
||||
flatten: true,
|
||||
iconPath: mdiTextShort,
|
||||
schema: [
|
||||
{
|
||||
name: "name",
|
||||
selector: {
|
||||
entity_name: {},
|
||||
},
|
||||
context: { entity: "entity" },
|
||||
},
|
||||
{
|
||||
name: "",
|
||||
type: "grid",
|
||||
schema: [
|
||||
colorSchema,
|
||||
{
|
||||
name: "icon",
|
||||
selector: {
|
||||
icon: {},
|
||||
},
|
||||
context: { icon_entity: "entity" },
|
||||
},
|
||||
{
|
||||
name: "show_entity_picture",
|
||||
selector: {
|
||||
boolean: {},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
displayedElementsSchema(localize),
|
||||
{
|
||||
name: "state_content",
|
||||
selector: {
|
||||
ui_state_content: {
|
||||
allow_name: true,
|
||||
},
|
||||
},
|
||||
context: {
|
||||
filter_entity: "entity",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "time_format",
|
||||
visible: showTimeFormat,
|
||||
selector: {
|
||||
ui_time_format: {},
|
||||
},
|
||||
},
|
||||
],
|
||||
}) as const satisfies HaFormSchema;
|
||||
|
||||
export const tileContentFormData = <T extends TileContentFormConfig>(
|
||||
config: T
|
||||
): T => ({
|
||||
...config,
|
||||
content_layout: config.vertical ? "vertical" : "horizontal",
|
||||
});
|
||||
|
||||
export const tileContentConfigFromForm = <T extends TileContentFormConfig>(
|
||||
value: T
|
||||
): T => {
|
||||
const config = { ...value };
|
||||
|
||||
if (config.hide_state || !config.state_content) {
|
||||
delete config.state_content;
|
||||
}
|
||||
|
||||
if (config.content_layout) {
|
||||
config.vertical = config.content_layout === "vertical";
|
||||
delete config.content_layout;
|
||||
}
|
||||
|
||||
return config;
|
||||
};
|
||||
|
||||
export const badgeContentFormData = <T extends BadgeContentFormConfig>(
|
||||
config: T
|
||||
): T => ({
|
||||
...config,
|
||||
displayed_elements: [
|
||||
...(config.show_name ? ["name"] : []),
|
||||
...(config.show_state ? ["state"] : []),
|
||||
...(config.show_icon ? ["icon"] : []),
|
||||
],
|
||||
});
|
||||
|
||||
export const badgeContentConfigFromForm = <T extends BadgeContentFormConfig>(
|
||||
value: T
|
||||
): T => {
|
||||
const config = { ...value };
|
||||
|
||||
if (!config.state_content) {
|
||||
delete config.state_content;
|
||||
}
|
||||
|
||||
if (config.displayed_elements) {
|
||||
config.show_name = config.displayed_elements.includes("name");
|
||||
config.show_state = config.displayed_elements.includes("state");
|
||||
config.show_icon = config.displayed_elements.includes("icon");
|
||||
delete config.displayed_elements;
|
||||
}
|
||||
|
||||
return config;
|
||||
};
|
||||
@@ -1,53 +1,47 @@
|
||||
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 {
|
||||
ALERT_DOMAINS,
|
||||
DEFAULT_ALERT_COLOR,
|
||||
} from "../../../../common/entity/alert_active";
|
||||
import { ALERT_DOMAINS } from "../../../../common/entity/alert_active";
|
||||
import type { LocalizeFunc } from "../../../../common/translations/localize";
|
||||
import "../../../../components/ha-form/ha-form";
|
||||
import type { SchemaUnion } from "../../../../components/ha-form/types";
|
||||
import type {
|
||||
HaFormSchema,
|
||||
SchemaUnion,
|
||||
} from "../../../../components/ha-form/types";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../../../../types";
|
||||
import type { AlertBadgeConfig } from "../../badges/types";
|
||||
import { ALERT_BADGE_DEFAULT_CONFIG } from "../../badges/hui-alert-badge";
|
||||
import type { LovelaceBadgeEditor } from "../../types";
|
||||
import { actionConfigStruct } from "../structs/action-struct";
|
||||
import { baseLovelaceBadgeConfig } from "../structs/base-badge-struct";
|
||||
import { entityNameStruct } from "../structs/entity-name-struct";
|
||||
import { alertBadgeContentSchema, alertSchema } from "./alert-schema";
|
||||
import {
|
||||
badgeContentConfigFromForm,
|
||||
badgeContentFormData,
|
||||
} from "./content-schema";
|
||||
import { entityInteractionsSchema } from "./interactions-schema";
|
||||
|
||||
const badgeConfigStruct = assign(
|
||||
baseLovelaceBadgeConfig,
|
||||
object({
|
||||
entity: optional(string()),
|
||||
name: optional(entityNameStruct),
|
||||
icon: optional(string()),
|
||||
alert_color: optional(string()),
|
||||
clear_color: optional(string()),
|
||||
pulse: optional(boolean()),
|
||||
show_name: optional(boolean()),
|
||||
show_state: optional(boolean()),
|
||||
show_icon: optional(boolean()),
|
||||
tap_action: optional(actionConfigStruct),
|
||||
hold_action: optional(actionConfigStruct),
|
||||
double_tap_action: optional(actionConfigStruct),
|
||||
})
|
||||
);
|
||||
|
||||
const SCHEMA = [
|
||||
{
|
||||
name: "entity",
|
||||
required: true,
|
||||
selector: { entity: { filter: { domain: ALERT_DOMAINS } } },
|
||||
},
|
||||
{
|
||||
name: "",
|
||||
type: "grid",
|
||||
schema: [
|
||||
{
|
||||
name: "clear_color",
|
||||
selector: { ui_color: { include_none: true, default_color: "none" } },
|
||||
},
|
||||
{
|
||||
name: "alert_color",
|
||||
selector: {
|
||||
ui_color: { include_none: true, default_color: DEFAULT_ALERT_COLOR },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{ name: "pulse", selector: { boolean: {} } },
|
||||
] as const;
|
||||
|
||||
@customElement("hui-alert-badge-editor")
|
||||
export class HuiAlertBadgeEditor
|
||||
extends LitElement
|
||||
@@ -59,9 +53,25 @@ export class HuiAlertBadgeEditor
|
||||
|
||||
public setConfig(config: AlertBadgeConfig): void {
|
||||
assert(config, badgeConfigStruct);
|
||||
this._config = config;
|
||||
this._config = { ...ALERT_BADGE_DEFAULT_CONFIG, ...config };
|
||||
}
|
||||
|
||||
private _schema = memoizeOne(
|
||||
(localize: LocalizeFunc) =>
|
||||
[
|
||||
{
|
||||
name: "entity",
|
||||
required: true,
|
||||
selector: { entity: { filter: { domain: ALERT_DOMAINS } } },
|
||||
},
|
||||
alertSchema(),
|
||||
alertBadgeContentSchema(localize),
|
||||
entityInteractionsSchema(),
|
||||
] as const satisfies readonly HaFormSchema[]
|
||||
);
|
||||
|
||||
private _formData = memoizeOne(badgeContentFormData<AlertBadgeConfig>);
|
||||
|
||||
protected render() {
|
||||
if (!this.hass || !this._config) {
|
||||
return nothing;
|
||||
@@ -70,8 +80,8 @@ export class HuiAlertBadgeEditor
|
||||
return html`
|
||||
<ha-form
|
||||
.hass=${this.hass}
|
||||
.data=${this._config}
|
||||
.schema=${SCHEMA}
|
||||
.data=${this._formData(this._config)}
|
||||
.schema=${this._schema(this.hass.localize)}
|
||||
.computeLabel=${this._computeLabelCallback}
|
||||
.computeHelper=${this._computeHelperCallback}
|
||||
@value-changed=${this._valueChanged}
|
||||
@@ -81,22 +91,40 @@ export class HuiAlertBadgeEditor
|
||||
|
||||
private _valueChanged(ev: ValueChangedEvent<AlertBadgeConfig>): void {
|
||||
ev.stopPropagation();
|
||||
fireEvent(this, "config-changed", { config: ev.detail.value });
|
||||
|
||||
const config = badgeContentConfigFromForm(ev.detail.value);
|
||||
|
||||
fireEvent(this, "config-changed", { config });
|
||||
}
|
||||
|
||||
private _computeLabelCallback = (schema: SchemaUnion<typeof SCHEMA>) => {
|
||||
if (schema.name === "entity") {
|
||||
return this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.card.generic.entity"
|
||||
);
|
||||
private _computeLabelCallback = (
|
||||
schema: SchemaUnion<ReturnType<typeof this._schema>>
|
||||
) => {
|
||||
switch (schema.name) {
|
||||
case "pulse":
|
||||
case "alert_color":
|
||||
case "clear_color":
|
||||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.alert.${schema.name}`
|
||||
);
|
||||
case "alert":
|
||||
return this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.card.alert.alert_section"
|
||||
);
|
||||
case "displayed_elements":
|
||||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.badge.entity.${schema.name}`
|
||||
);
|
||||
default:
|
||||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.generic.${schema.name}`
|
||||
);
|
||||
}
|
||||
|
||||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.alert.${schema.name}`
|
||||
);
|
||||
};
|
||||
|
||||
private _computeHelperCallback = (schema: SchemaUnion<typeof SCHEMA>) => {
|
||||
private _computeHelperCallback = (
|
||||
schema: SchemaUnion<ReturnType<typeof this._schema>>
|
||||
) => {
|
||||
if (
|
||||
schema.name === "pulse" ||
|
||||
schema.name === "alert_color" ||
|
||||
|
||||
@@ -1,53 +1,45 @@
|
||||
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 {
|
||||
ALERT_DOMAINS,
|
||||
DEFAULT_ALERT_COLOR,
|
||||
} from "../../../../common/entity/alert_active";
|
||||
import { ALERT_DOMAINS } from "../../../../common/entity/alert_active";
|
||||
import type { LocalizeFunc } from "../../../../common/translations/localize";
|
||||
import "../../../../components/ha-form/ha-form";
|
||||
import type { SchemaUnion } from "../../../../components/ha-form/types";
|
||||
import type {
|
||||
HaFormSchema,
|
||||
SchemaUnion,
|
||||
} from "../../../../components/ha-form/types";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../../../../types";
|
||||
import type { AlertCardConfig } from "../../cards/types";
|
||||
import type { LovelaceCardEditor } from "../../types";
|
||||
import { actionConfigStruct } from "../structs/action-struct";
|
||||
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
|
||||
import { entityNameStruct } from "../structs/entity-name-struct";
|
||||
import { alertCardContentSchema, alertSchema } from "./alert-schema";
|
||||
import {
|
||||
tileContentConfigFromForm,
|
||||
tileContentFormData,
|
||||
} from "./content-schema";
|
||||
import { entityInteractionsSchema } from "./interactions-schema";
|
||||
|
||||
const cardConfigStruct = assign(
|
||||
baseLovelaceCardConfig,
|
||||
object({
|
||||
entity: optional(string()),
|
||||
name: optional(entityNameStruct),
|
||||
icon: optional(string()),
|
||||
alert_color: optional(string()),
|
||||
clear_color: optional(string()),
|
||||
pulse: optional(boolean()),
|
||||
hide_state: optional(boolean()),
|
||||
vertical: optional(boolean()),
|
||||
tap_action: optional(actionConfigStruct),
|
||||
hold_action: optional(actionConfigStruct),
|
||||
double_tap_action: optional(actionConfigStruct),
|
||||
})
|
||||
);
|
||||
|
||||
const SCHEMA = [
|
||||
{
|
||||
name: "entity",
|
||||
required: true,
|
||||
selector: { entity: { filter: { domain: ALERT_DOMAINS } } },
|
||||
},
|
||||
{
|
||||
name: "",
|
||||
type: "grid",
|
||||
schema: [
|
||||
{
|
||||
name: "clear_color",
|
||||
selector: { ui_color: { include_none: true, default_color: "none" } },
|
||||
},
|
||||
{
|
||||
name: "alert_color",
|
||||
selector: {
|
||||
ui_color: { include_none: true, default_color: DEFAULT_ALERT_COLOR },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{ name: "pulse", selector: { boolean: {} } },
|
||||
] as const;
|
||||
|
||||
@customElement("hui-alert-card-editor")
|
||||
export class HuiAlertCardEditor
|
||||
extends LitElement
|
||||
@@ -62,6 +54,20 @@ export class HuiAlertCardEditor
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
private _schema = memoizeOne(
|
||||
(localize: LocalizeFunc) =>
|
||||
[
|
||||
{
|
||||
name: "entity",
|
||||
required: true,
|
||||
selector: { entity: { filter: { domain: ALERT_DOMAINS } } },
|
||||
},
|
||||
alertSchema(),
|
||||
alertCardContentSchema(localize),
|
||||
entityInteractionsSchema(),
|
||||
] as const satisfies readonly HaFormSchema[]
|
||||
);
|
||||
|
||||
protected render() {
|
||||
if (!this.hass || !this._config) {
|
||||
return nothing;
|
||||
@@ -70,8 +76,8 @@ export class HuiAlertCardEditor
|
||||
return html`
|
||||
<ha-form
|
||||
.hass=${this.hass}
|
||||
.data=${this._config}
|
||||
.schema=${SCHEMA}
|
||||
.data=${tileContentFormData(this._config)}
|
||||
.schema=${this._schema(this.hass.localize)}
|
||||
.computeLabel=${this._computeLabelCallback}
|
||||
.computeHelper=${this._computeHelperCallback}
|
||||
@value-changed=${this._valueChanged}
|
||||
@@ -81,22 +87,41 @@ export class HuiAlertCardEditor
|
||||
|
||||
private _valueChanged(ev: ValueChangedEvent<AlertCardConfig>): void {
|
||||
ev.stopPropagation();
|
||||
fireEvent(this, "config-changed", { config: ev.detail.value });
|
||||
|
||||
const config = tileContentConfigFromForm(ev.detail.value);
|
||||
|
||||
fireEvent(this, "config-changed", { config });
|
||||
}
|
||||
|
||||
private _computeLabelCallback = (schema: SchemaUnion<typeof SCHEMA>) => {
|
||||
if (schema.name === "entity") {
|
||||
return this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.card.generic.entity"
|
||||
);
|
||||
private _computeLabelCallback = (
|
||||
schema: SchemaUnion<ReturnType<typeof this._schema>>
|
||||
) => {
|
||||
switch (schema.name) {
|
||||
case "pulse":
|
||||
case "alert_color":
|
||||
case "clear_color":
|
||||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.alert.${schema.name}`
|
||||
);
|
||||
case "alert":
|
||||
return this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.card.alert.alert_section"
|
||||
);
|
||||
case "hide_state":
|
||||
case "content_layout":
|
||||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.tile.${schema.name}`
|
||||
);
|
||||
default:
|
||||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.generic.${schema.name}`
|
||||
);
|
||||
}
|
||||
|
||||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.alert.${schema.name}`
|
||||
);
|
||||
};
|
||||
|
||||
private _computeHelperCallback = (schema: SchemaUnion<typeof SCHEMA>) => {
|
||||
private _computeHelperCallback = (
|
||||
schema: SchemaUnion<ReturnType<typeof this._schema>>
|
||||
) => {
|
||||
if (
|
||||
schema.name === "pulse" ||
|
||||
schema.name === "alert_color" ||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { mdiGestureTap, mdiTextShort } from "@mdi/js";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
@@ -20,7 +19,7 @@ import type {
|
||||
HaFormSchema,
|
||||
SchemaUnion,
|
||||
} from "../../../../components/ha-form/types";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../../../../types";
|
||||
import {
|
||||
DEFAULT_CONFIG,
|
||||
DISPLAY_TYPES,
|
||||
@@ -28,11 +27,16 @@ import {
|
||||
} from "../../badges/hui-entity-badge";
|
||||
import type { EntityBadgeConfig } from "../../badges/types";
|
||||
import type { LovelaceBadgeEditor } from "../../types";
|
||||
import { ACTION_RELATED_CONTEXT } from "../../components/hui-action-editor";
|
||||
import { actionConfigStruct } from "../structs/action-struct";
|
||||
import { baseLovelaceBadgeConfig } from "../structs/base-badge-struct";
|
||||
import { entityNameStruct } from "../structs/entity-name-struct";
|
||||
import { configElementStyle } from "./config-elements-style";
|
||||
import {
|
||||
badgeContentConfigFromForm,
|
||||
badgeContentFormData,
|
||||
badgeContentSchema,
|
||||
} from "./content-schema";
|
||||
import { entityInteractionsSchema } from "./interactions-schema";
|
||||
import { stateContentHasTimestamp } from "../../../../state-display/state-display";
|
||||
import { timeFormatConfigStruct } from "../../components/types";
|
||||
|
||||
@@ -78,148 +82,12 @@ export class HuiEntityBadgeEditor
|
||||
(localize: LocalizeFunc, showTimeFormat: boolean) =>
|
||||
[
|
||||
{ name: "entity", selector: { entity: {} } },
|
||||
{
|
||||
name: "content",
|
||||
type: "expandable",
|
||||
flatten: true,
|
||||
iconPath: mdiTextShort,
|
||||
schema: [
|
||||
{
|
||||
name: "name",
|
||||
selector: {
|
||||
entity_name: {},
|
||||
},
|
||||
context: { entity: "entity" },
|
||||
},
|
||||
{
|
||||
name: "",
|
||||
type: "grid",
|
||||
schema: [
|
||||
{
|
||||
name: "color",
|
||||
selector: {
|
||||
ui_color: {
|
||||
default_color: "state",
|
||||
include_state: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "icon",
|
||||
selector: {
|
||||
icon: {},
|
||||
},
|
||||
context: { icon_entity: "entity" },
|
||||
},
|
||||
{
|
||||
name: "show_entity_picture",
|
||||
selector: {
|
||||
boolean: {},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "displayed_elements",
|
||||
selector: {
|
||||
select: {
|
||||
mode: "list",
|
||||
multiple: true,
|
||||
options: [
|
||||
{
|
||||
value: "name",
|
||||
label: localize(
|
||||
`ui.panel.lovelace.editor.badge.entity.displayed_elements_options.name`
|
||||
),
|
||||
},
|
||||
{
|
||||
value: "state",
|
||||
label: localize(
|
||||
`ui.panel.lovelace.editor.badge.entity.displayed_elements_options.state`
|
||||
),
|
||||
},
|
||||
{
|
||||
value: "icon",
|
||||
label: localize(
|
||||
`ui.panel.lovelace.editor.badge.entity.displayed_elements_options.icon`
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "state_content",
|
||||
selector: {
|
||||
ui_state_content: {
|
||||
allow_name: true,
|
||||
},
|
||||
},
|
||||
context: {
|
||||
filter_entity: "entity",
|
||||
},
|
||||
},
|
||||
...(showTimeFormat
|
||||
? ([
|
||||
{
|
||||
name: "time_format",
|
||||
selector: {
|
||||
ui_time_format: {},
|
||||
},
|
||||
},
|
||||
] as const satisfies readonly HaFormSchema[])
|
||||
: []),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "interactions",
|
||||
type: "expandable",
|
||||
flatten: true,
|
||||
iconPath: mdiGestureTap,
|
||||
schema: [
|
||||
{
|
||||
name: "tap_action",
|
||||
selector: {
|
||||
ui_action: {
|
||||
default_action: "more-info",
|
||||
},
|
||||
},
|
||||
context: ACTION_RELATED_CONTEXT,
|
||||
},
|
||||
{
|
||||
name: "",
|
||||
type: "optional_actions",
|
||||
flatten: true,
|
||||
schema: (["hold_action", "double_tap_action"] as const).map(
|
||||
(action) => ({
|
||||
name: action,
|
||||
selector: {
|
||||
ui_action: {
|
||||
default_action: "none" as const,
|
||||
},
|
||||
},
|
||||
context: ACTION_RELATED_CONTEXT,
|
||||
})
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
badgeContentSchema(localize, showTimeFormat),
|
||||
entityInteractionsSchema(),
|
||||
] as const satisfies readonly HaFormSchema[]
|
||||
);
|
||||
|
||||
_displayedElements = memoizeOne((config: EntityBadgeConfig) => {
|
||||
const elements: string[] = [];
|
||||
if (config.show_name) {
|
||||
elements.push("name");
|
||||
}
|
||||
if (config.show_state) {
|
||||
elements.push("state");
|
||||
}
|
||||
if (config.show_icon) {
|
||||
elements.push("icon");
|
||||
}
|
||||
return elements;
|
||||
});
|
||||
private _formData = memoizeOne(badgeContentFormData<EntityBadgeConfig>);
|
||||
|
||||
protected render() {
|
||||
if (!this.hass || !this._config) {
|
||||
@@ -238,10 +106,7 @@ export class HuiEntityBadgeEditor
|
||||
|
||||
const schema = this._schema(this.hass!.localize, showTimeFormat);
|
||||
|
||||
const data = {
|
||||
...this._config,
|
||||
displayed_elements: this._displayedElements(this._config),
|
||||
};
|
||||
const data = this._formData(this._config);
|
||||
|
||||
return html`
|
||||
<ha-form
|
||||
@@ -255,24 +120,13 @@ export class HuiEntityBadgeEditor
|
||||
`;
|
||||
}
|
||||
|
||||
private _valueChanged(ev: CustomEvent): void {
|
||||
private _valueChanged(ev: ValueChangedEvent<EntityBadgeConfig>): void {
|
||||
ev.stopPropagation();
|
||||
if (!this._config || !this.hass) {
|
||||
return;
|
||||
}
|
||||
|
||||
const config = { ...ev.detail.value } as EntityBadgeConfig;
|
||||
|
||||
if (!config.state_content) {
|
||||
delete config.state_content;
|
||||
}
|
||||
|
||||
if (config.displayed_elements) {
|
||||
config.show_name = config.displayed_elements.includes("name");
|
||||
config.show_state = config.displayed_elements.includes("state");
|
||||
config.show_icon = config.displayed_elements.includes("icon");
|
||||
delete config.displayed_elements;
|
||||
}
|
||||
const config = badgeContentConfigFromForm(ev.detail.value);
|
||||
|
||||
fireEvent(this, "config-changed", { config });
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { mdiGestureTap, mdiListBox, mdiTextShort } from "@mdi/js";
|
||||
import { mdiListBox } from "@mdi/js";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
@@ -30,8 +30,6 @@ import type {
|
||||
LovelaceCardFeatureConfig,
|
||||
LovelaceCardFeatureContext,
|
||||
} from "../../card-features/types";
|
||||
import { ACTION_RELATED_CONTEXT } from "../../components/hui-action-editor";
|
||||
import { getEntityDefaultTileIconAction } from "../../cards/hui-tile-card";
|
||||
import type { TileCardConfig } from "../../cards/types";
|
||||
import type { LovelaceCardEditor } from "../../types";
|
||||
import { actionConfigStruct } from "../structs/action-struct";
|
||||
@@ -39,7 +37,13 @@ import { baseLovelaceCardConfig } from "../structs/base-card-struct";
|
||||
import { entityNameStruct } from "../structs/entity-name-struct";
|
||||
import type { EditDetailElementEvent, EditSubElementEvent } from "../types";
|
||||
import { configElementStyle } from "./config-elements-style";
|
||||
import {
|
||||
tileContentConfigFromForm,
|
||||
tileContentFormData,
|
||||
tileContentSchema,
|
||||
} from "./content-schema";
|
||||
import { getSupportedFeaturesType } from "./hui-card-features-editor";
|
||||
import { tileInteractionsSchema } from "./interactions-schema";
|
||||
import { stateContentHasTimestamp } from "../../../../state-display/state-display";
|
||||
import { timeFormatConfigStruct } from "../../components/types";
|
||||
|
||||
@@ -96,147 +100,8 @@ export class HuiTileCardEditor
|
||||
) =>
|
||||
[
|
||||
{ name: "entity", selector: { entity: {} } },
|
||||
{
|
||||
name: "content",
|
||||
flatten: true,
|
||||
type: "expandable",
|
||||
iconPath: mdiTextShort,
|
||||
schema: [
|
||||
{
|
||||
name: "name",
|
||||
selector: {
|
||||
entity_name: {},
|
||||
},
|
||||
context: { entity: "entity" },
|
||||
},
|
||||
{
|
||||
name: "",
|
||||
type: "grid",
|
||||
schema: [
|
||||
{
|
||||
name: "icon",
|
||||
selector: {
|
||||
icon: {},
|
||||
},
|
||||
context: { icon_entity: "entity" },
|
||||
},
|
||||
{
|
||||
name: "color",
|
||||
selector: {
|
||||
ui_color: {
|
||||
default_color: "state",
|
||||
include_state: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "show_entity_picture",
|
||||
selector: {
|
||||
boolean: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "hide_state",
|
||||
selector: {
|
||||
boolean: {},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "state_content",
|
||||
visible: { field: "hide_state", operator: "not_eq", value: true },
|
||||
selector: {
|
||||
ui_state_content: {
|
||||
allow_context: true,
|
||||
},
|
||||
},
|
||||
context: {
|
||||
filter_entity: "entity",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "time_format",
|
||||
visible: showTimeFormat,
|
||||
selector: {
|
||||
ui_time_format: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "content_layout",
|
||||
required: true,
|
||||
selector: {
|
||||
select: {
|
||||
mode: "box",
|
||||
options: ["horizontal", "vertical"].map((value) => ({
|
||||
label: localize(
|
||||
`ui.panel.lovelace.editor.card.tile.content_layout_options.${value}`
|
||||
),
|
||||
value,
|
||||
image: {
|
||||
src: `/static/images/form/tile_content_layout_${value}.svg`,
|
||||
src_dark: `/static/images/form/tile_content_layout_${value}_dark.svg`,
|
||||
flip_rtl: true,
|
||||
},
|
||||
})),
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "interactions",
|
||||
type: "expandable",
|
||||
flatten: true,
|
||||
iconPath: mdiGestureTap,
|
||||
schema: [
|
||||
{
|
||||
name: "tap_action",
|
||||
selector: {
|
||||
ui_action: {
|
||||
default_action: "more-info",
|
||||
},
|
||||
},
|
||||
context: ACTION_RELATED_CONTEXT,
|
||||
},
|
||||
{
|
||||
name: "",
|
||||
type: "divider",
|
||||
},
|
||||
{
|
||||
name: "icon_tap_action",
|
||||
selector: {
|
||||
ui_action: {
|
||||
default_action: entityId
|
||||
? getEntityDefaultTileIconAction(entityId)
|
||||
: "more-info",
|
||||
},
|
||||
},
|
||||
context: ACTION_RELATED_CONTEXT,
|
||||
},
|
||||
{
|
||||
name: "",
|
||||
type: "optional_actions",
|
||||
flatten: true,
|
||||
schema: (
|
||||
[
|
||||
"hold_action",
|
||||
"icon_hold_action",
|
||||
"double_tap_action",
|
||||
"icon_double_tap_action",
|
||||
] as const
|
||||
).map((action) => ({
|
||||
name: action,
|
||||
selector: {
|
||||
ui_action: {
|
||||
default_action: "none" as const,
|
||||
},
|
||||
},
|
||||
context: ACTION_RELATED_CONTEXT,
|
||||
})),
|
||||
},
|
||||
],
|
||||
},
|
||||
tileContentSchema(localize, showTimeFormat),
|
||||
tileInteractionsSchema(entityId),
|
||||
] as const satisfies readonly HaFormSchema[]
|
||||
);
|
||||
|
||||
@@ -296,10 +161,7 @@ export class HuiTileCardEditor
|
||||
|
||||
const featuresSchema = this._featuresSchema(this.hass.localize, vertical);
|
||||
|
||||
const data = {
|
||||
...this._config,
|
||||
content_layout: vertical ? "vertical" : "horizontal",
|
||||
};
|
||||
const data = tileContentFormData(this._config);
|
||||
|
||||
// Default features position to bottom and force it to bottom in vertical mode
|
||||
if (!data.features_position || vertical) {
|
||||
@@ -362,24 +224,10 @@ export class HuiTileCardEditor
|
||||
|
||||
const newConfig = ev.detail.value as TileCardConfig;
|
||||
|
||||
let config: TileCardConfig = {
|
||||
let config: TileCardConfig = tileContentConfigFromForm({
|
||||
features: this._config.features,
|
||||
...newConfig,
|
||||
};
|
||||
|
||||
if (config.hide_state) {
|
||||
delete config.state_content;
|
||||
}
|
||||
|
||||
if (!config.state_content) {
|
||||
delete config.state_content;
|
||||
}
|
||||
|
||||
// Convert content_layout to vertical
|
||||
if (config.content_layout) {
|
||||
config.vertical = config.content_layout === "vertical";
|
||||
delete config.content_layout;
|
||||
}
|
||||
});
|
||||
|
||||
config = orderProperties(config, fieldOrder);
|
||||
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
import { mdiGestureTap } from "@mdi/js";
|
||||
import type { HaFormSchema } from "../../../../components/ha-form/types";
|
||||
import { getEntityDefaultTileIconAction } from "../../cards/tile/tile-card-helpers";
|
||||
import { ACTION_RELATED_CONTEXT } from "../../components/hui-action-editor";
|
||||
|
||||
export const tileInteractionsSchema = (entityId: string | undefined) =>
|
||||
({
|
||||
name: "interactions",
|
||||
type: "expandable",
|
||||
flatten: true,
|
||||
iconPath: mdiGestureTap,
|
||||
schema: [
|
||||
{
|
||||
name: "tap_action",
|
||||
selector: {
|
||||
ui_action: {
|
||||
default_action: "more-info",
|
||||
},
|
||||
},
|
||||
context: ACTION_RELATED_CONTEXT,
|
||||
},
|
||||
{
|
||||
name: "",
|
||||
type: "divider",
|
||||
},
|
||||
{
|
||||
name: "icon_tap_action",
|
||||
selector: {
|
||||
ui_action: {
|
||||
default_action: entityId
|
||||
? getEntityDefaultTileIconAction(entityId)
|
||||
: "more-info",
|
||||
},
|
||||
},
|
||||
context: ACTION_RELATED_CONTEXT,
|
||||
},
|
||||
{
|
||||
name: "",
|
||||
type: "optional_actions",
|
||||
flatten: true,
|
||||
schema: (
|
||||
[
|
||||
"hold_action",
|
||||
"icon_hold_action",
|
||||
"double_tap_action",
|
||||
"icon_double_tap_action",
|
||||
] as const
|
||||
).map((action) => ({
|
||||
name: action,
|
||||
selector: {
|
||||
ui_action: {
|
||||
default_action: "none" as const,
|
||||
},
|
||||
},
|
||||
context: ACTION_RELATED_CONTEXT,
|
||||
})),
|
||||
},
|
||||
],
|
||||
}) as const satisfies HaFormSchema;
|
||||
|
||||
export const entityInteractionsSchema = () =>
|
||||
({
|
||||
name: "interactions",
|
||||
type: "expandable",
|
||||
flatten: true,
|
||||
iconPath: mdiGestureTap,
|
||||
schema: [
|
||||
{
|
||||
name: "tap_action",
|
||||
selector: {
|
||||
ui_action: {
|
||||
default_action: "more-info",
|
||||
},
|
||||
},
|
||||
context: ACTION_RELATED_CONTEXT,
|
||||
},
|
||||
{
|
||||
name: "",
|
||||
type: "optional_actions",
|
||||
flatten: true,
|
||||
schema: (["hold_action", "double_tap_action"] as const).map(
|
||||
(action) => ({
|
||||
name: action,
|
||||
selector: {
|
||||
ui_action: {
|
||||
default_action: "none" as const,
|
||||
},
|
||||
},
|
||||
context: ACTION_RELATED_CONTEXT,
|
||||
})
|
||||
),
|
||||
},
|
||||
],
|
||||
}) as const satisfies HaFormSchema;
|
||||
@@ -404,6 +404,7 @@ export const getMyRedirects = (): Redirects => ({
|
||||
backup_list: {
|
||||
component: "backup",
|
||||
redirect: "/config/backup/backups",
|
||||
params: backupsTableParams,
|
||||
},
|
||||
backup_config: {
|
||||
component: "backup",
|
||||
@@ -518,6 +519,16 @@ export const getMyRedirects = (): Redirects => ({
|
||||
const getRedirect = (path: string): Redirect | undefined =>
|
||||
getMyRedirects()?.[path];
|
||||
|
||||
// The backups table reads these from the link
|
||||
const backupsTableParams = {
|
||||
search: "string?",
|
||||
sort: "string?",
|
||||
direction: "string?",
|
||||
group: "string?",
|
||||
type: "string?",
|
||||
location: "string?",
|
||||
} satisfies Redirect["params"];
|
||||
|
||||
export type ParamType = "url" | "string" | "string?";
|
||||
|
||||
export type Redirects = Record<string, Redirect>;
|
||||
|
||||
@@ -10516,7 +10516,8 @@
|
||||
"clear_color": "Clear color",
|
||||
"clear_color_helper": "Used when the entity isn't in an alert state. The background doesn't pulse.",
|
||||
"pulse": "Pulse",
|
||||
"pulse_helper": "Animates the colored background while the entity is in an alert state. When reduced motion is on, the background stays tinted instead."
|
||||
"pulse_helper": "Animates the colored background while the entity is in an alert state. When reduced motion is on, the background stays tinted instead.",
|
||||
"alert_section": "Alert"
|
||||
},
|
||||
"area": {
|
||||
"name": "Area",
|
||||
|
||||
Reference in new issue
Block a user