20240424.1 (#20609)

This commit is contained in:
Bram Kragten 2024-04-24 14:45:22 +02:00 committed by GitHub
commit 64f54d9aaa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 752 additions and 125 deletions

View File

@ -102,7 +102,7 @@
"color-name": "2.0.0", "color-name": "2.0.0",
"comlink": "4.4.1", "comlink": "4.4.1",
"core-js": "3.37.0", "core-js": "3.37.0",
"cropperjs": "1.6.1", "cropperjs": "1.6.2",
"date-fns": "3.6.0", "date-fns": "3.6.0",
"date-fns-tz": "3.1.3", "date-fns-tz": "3.1.3",
"deep-clone-simple": "1.1.1", "deep-clone-simple": "1.1.1",

View File

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "home-assistant-frontend" name = "home-assistant-frontend"
version = "20240424.0" version = "20240424.1"
license = {text = "Apache-2.0"} license = {text = "Apache-2.0"}
description = "The Home Assistant frontend" description = "The Home Assistant frontend"
readme = "README.md" readme = "README.md"

View File

@ -69,7 +69,7 @@ export class HaFilterDevices extends LitElement {
@value-changed=${this._handleSearchChange} @value-changed=${this._handleSearchChange}
> >
</search-input-outlined> </search-input-outlined>
<mwc-list class="ha-scrollbar"> <mwc-list class="ha-scrollbar" multi>
<lit-virtualizer <lit-virtualizer
.items=${this._devices( .items=${this._devices(
this.hass.devices, this.hass.devices,

View File

@ -0,0 +1,198 @@
import { mdiFilterVariantRemove } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, 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 { stringCompare } from "../common/string/compare";
import { domainToName } from "../data/integration";
import { haStyleScrollbar } from "../resources/styles";
import type { HomeAssistant } from "../types";
import "./ha-domain-icon";
import "./search-input-outlined";
import { computeDomain } from "../common/entity/compute_domain";
@customElement("ha-filter-domains")
export class HaFilterDomains extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public value?: string[];
@property({ type: Boolean }) public narrow = false;
@property({ type: Boolean, reflect: true }) public expanded = false;
@state() private _shouldRender = false;
@state() private _filter?: string;
protected render() {
return html`
<ha-expansion-panel
leftChevron
.expanded=${this.expanded}
@expanded-will-change=${this._expandedWillChange}
@expanded-changed=${this._expandedChanged}
>
<div slot="header" class="header">
${this.hass.localize(
"ui.panel.config.entities.picker.headers.domain"
)}
${this.value?.length
? html`<div class="badge">${this.value?.length}</div>
<ha-icon-button
.path=${mdiFilterVariantRemove}
@click=${this._clearFilter}
></ha-icon-button>`
: nothing}
</div>
${this._shouldRender
? html`<search-input-outlined
.hass=${this.hass}
.filter=${this._filter}
@value-changed=${this._handleSearchChange}
>
</search-input-outlined>
<mwc-list
class="ha-scrollbar"
@click=${this._handleItemClick}
multi
>
${repeat(
this._domains(this.hass.states, this._filter),
(i) => i,
(domain) =>
html`<ha-check-list-item
.value=${domain}
.selected=${(this.value || []).includes(domain)}
graphic="icon"
>
<ha-domain-icon
slot="graphic"
.hass=${this.hass}
.domain=${domain}
brandFallback
></ha-domain-icon>
${domainToName(this.hass.localize, domain)}
</ha-check-list-item>`
)}
</mwc-list> `
: nothing}
</ha-expansion-panel>
`;
}
private _domains = memoizeOne((states, filter) => {
const domains = new Set<string>();
Object.keys(states).forEach((entityId) => {
domains.add(computeDomain(entityId));
});
return Array.from(domains)
.filter((domain) => !filter || domain.toLowerCase().includes(filter))
.sort((a, b) => stringCompare(a, b, this.hass.locale.language));
});
protected updated(changed) {
if (changed.has("expanded") && this.expanded) {
setTimeout(() => {
if (!this.expanded) return;
this.renderRoot.querySelector("mwc-list")!.style.height =
`${this.clientHeight - 49 - 32}px`; // 32px is the height of the search input
}, 300);
}
}
private _expandedWillChange(ev) {
this._shouldRender = ev.detail.expanded;
}
private _expandedChanged(ev) {
this.expanded = ev.detail.expanded;
}
private _handleItemClick(ev) {
const listItem = ev.target.closest("ha-check-list-item");
const value = listItem?.value;
if (!value) {
return;
}
if (this.value?.includes(value)) {
this.value = this.value?.filter((val) => val !== value);
} else {
this.value = [...(this.value || []), value];
}
listItem.selected = this.value.includes(value);
fireEvent(this, "data-table-filter-changed", {
value: this.value,
items: undefined,
});
}
private _clearFilter(ev) {
ev.preventDefault();
this.value = undefined;
fireEvent(this, "data-table-filter-changed", {
value: undefined,
items: undefined,
});
}
private _handleSearchChange(ev: CustomEvent) {
this._filter = ev.detail.value.toLowerCase();
}
static get styles(): CSSResultGroup {
return [
haStyleScrollbar,
css`
:host {
border-bottom: 1px solid var(--divider-color);
}
:host([expanded]) {
flex: 1;
height: 0;
}
ha-expansion-panel {
--ha-card-border-radius: 0;
--expansion-panel-content-padding: 0;
}
.header {
display: flex;
align-items: center;
}
.header ha-icon-button {
margin-inline-start: auto;
margin-inline-end: 8px;
}
.badge {
display: inline-block;
margin-left: 8px;
margin-inline-start: 8px;
margin-inline-end: 0;
min-width: 16px;
box-sizing: border-box;
border-radius: 50%;
font-weight: 400;
font-size: 11px;
background-color: var(--primary-color);
line-height: 16px;
text-align: center;
padding: 0px 2px;
color: var(--text-primary-color);
}
search-input-outlined {
display: block;
padding: 0 8px;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-filter-domains": HaFilterDomains;
}
}

View File

@ -71,7 +71,7 @@ export class HaFilterEntities extends LitElement {
@value-changed=${this._handleSearchChange} @value-changed=${this._handleSearchChange}
> >
</search-input-outlined> </search-input-outlined>
<mwc-list class="ha-scrollbar"> <mwc-list class="ha-scrollbar" multi>
<lit-virtualizer <lit-virtualizer
.items=${this._entities( .items=${this._entities(
this.hass.states, this.hass.states,

View File

@ -55,7 +55,11 @@ export class HaFilterIntegrations extends LitElement {
@value-changed=${this._handleSearchChange} @value-changed=${this._handleSearchChange}
> >
</search-input-outlined> </search-input-outlined>
<mwc-list class="ha-scrollbar" @click=${this._handleItemClick}> <mwc-list
class="ha-scrollbar"
@click=${this._handleItemClick}
multi
>
${repeat( ${repeat(
this._integrations(this._manifests, this._filter, this.value), this._integrations(this._manifests, this._filter, this.value),
(i) => i.domain, (i) => i.domain,

View File

@ -19,7 +19,7 @@ import { customElement, property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import type { LeafletModuleType } from "../../common/dom/setup-leaflet-map"; import type { LeafletModuleType } from "../../common/dom/setup-leaflet-map";
import type { HomeAssistant } from "../../types"; import type { HomeAssistant, ThemeMode } from "../../types";
import "../ha-input-helper-text"; import "../ha-input-helper-text";
import "./ha-map"; import "./ha-map";
import type { HaMap } from "./ha-map"; import type { HaMap } from "./ha-map";
@ -61,7 +61,8 @@ export class HaLocationsEditor extends LitElement {
@property({ type: Number }) public zoom = 16; @property({ type: Number }) public zoom = 16;
@property({ type: Boolean }) public darkMode = false; @property({ attribute: "theme-mode", type: String })
public themeMode: ThemeMode = "auto";
@state() private _locationMarkers?: Record<string, Marker | Circle>; @state() private _locationMarkers?: Record<string, Marker | Circle>;
@ -133,7 +134,7 @@ export class HaLocationsEditor extends LitElement {
.layers=${this._getLayers(this._circles, this._locationMarkers)} .layers=${this._getLayers(this._circles, this._locationMarkers)}
.zoom=${this.zoom} .zoom=${this.zoom}
.autoFit=${this.autoFit} .autoFit=${this.autoFit}
?darkMode=${this.darkMode} .themeMode=${this.themeMode}
></ha-map> ></ha-map>
${this.helper ${this.helper
? html`<ha-input-helper-text>${this.helper}</ha-input-helper-text>` ? html`<ha-input-helper-text>${this.helper}</ha-input-helper-text>`

View File

@ -1,32 +1,32 @@
import { isToday } from "date-fns";
import type { import type {
Circle, Circle,
CircleMarker, CircleMarker,
LatLngTuple,
LatLngExpression, LatLngExpression,
LatLngTuple,
Layer, Layer,
Map, Map,
Marker, Marker,
Polyline, Polyline,
} from "leaflet"; } from "leaflet";
import { isToday } from "date-fns"; import { CSSResultGroup, PropertyValues, ReactiveElement, css } from "lit";
import { css, CSSResultGroup, PropertyValues, ReactiveElement } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { formatDateTime } from "../../common/datetime/format_date_time";
import {
formatTimeWeekday,
formatTimeWithSeconds,
} from "../../common/datetime/format_time";
import { import {
LeafletModuleType, LeafletModuleType,
setupLeafletMap, setupLeafletMap,
} from "../../common/dom/setup-leaflet-map"; } from "../../common/dom/setup-leaflet-map";
import {
formatTimeWithSeconds,
formatTimeWeekday,
} from "../../common/datetime/format_time";
import { formatDateTime } from "../../common/datetime/format_date_time";
import { computeStateDomain } from "../../common/entity/compute_state_domain"; import { computeStateDomain } from "../../common/entity/compute_state_domain";
import { computeStateName } from "../../common/entity/compute_state_name"; import { computeStateName } from "../../common/entity/compute_state_name";
import { loadPolyfillIfNeeded } from "../../resources/resize-observer.polyfill"; import { loadPolyfillIfNeeded } from "../../resources/resize-observer.polyfill";
import { HomeAssistant } from "../../types"; import { HomeAssistant, ThemeMode } from "../../types";
import { isTouch } from "../../util/is_touch";
import "../ha-icon-button"; import "../ha-icon-button";
import "./ha-entity-marker"; import "./ha-entity-marker";
import { isTouch } from "../../util/is_touch";
const getEntityId = (entity: string | HaMapEntity): string => const getEntityId = (entity: string | HaMapEntity): string =>
typeof entity === "string" ? entity : entity.entity_id; typeof entity === "string" ? entity : entity.entity_id;
@ -69,7 +69,8 @@ export class HaMap extends ReactiveElement {
@property({ type: Boolean }) public fitZones = false; @property({ type: Boolean }) public fitZones = false;
@property({ type: Boolean }) public darkMode = false; @property({ attribute: "theme-mode", type: String })
public themeMode: ThemeMode = "auto";
@property({ type: Number }) public zoom = 14; @property({ type: Number }) public zoom = 14;
@ -154,7 +155,7 @@ export class HaMap extends ReactiveElement {
} }
if ( if (
!changedProps.has("darkMode") && !changedProps.has("themeMode") &&
(!changedProps.has("hass") || (!changedProps.has("hass") ||
(oldHass && oldHass.themes?.darkMode === this.hass.themes?.darkMode)) (oldHass && oldHass.themes?.darkMode === this.hass.themes?.darkMode))
) { ) {
@ -163,12 +164,18 @@ export class HaMap extends ReactiveElement {
this._updateMapStyle(); this._updateMapStyle();
} }
private get _darkMode() {
return (
this.themeMode === "dark" ||
(this.themeMode === "auto" && Boolean(this.hass.themes.darkMode))
);
}
private _updateMapStyle(): void { private _updateMapStyle(): void {
const darkMode = this.darkMode || (this.hass.themes.darkMode ?? false);
const forcedDark = this.darkMode;
const map = this.renderRoot.querySelector("#map"); const map = this.renderRoot.querySelector("#map");
map!.classList.toggle("dark", darkMode); map!.classList.toggle("dark", this._darkMode);
map!.classList.toggle("forced-dark", forcedDark); map!.classList.toggle("forced-dark", this.themeMode === "dark");
map!.classList.toggle("forced-light", this.themeMode === "light");
} }
private async _loadMap(): Promise<void> { private async _loadMap(): Promise<void> {
@ -398,8 +405,7 @@ export class HaMap extends ReactiveElement {
"--dark-primary-color" "--dark-primary-color"
); );
const className = const className = this._darkMode ? "dark" : "light";
this.darkMode || this.hass.themes.darkMode ? "dark" : "light";
for (const entity of this.entities) { for (const entity of this.entities) {
const stateObj = hass.states[getEntityId(entity)]; const stateObj = hass.states[getEntityId(entity)];
@ -543,27 +549,30 @@ export class HaMap extends ReactiveElement {
background: #090909; background: #090909;
} }
#map.forced-dark { #map.forced-dark {
color: #ffffff;
--map-filter: invert(0.9) hue-rotate(170deg) brightness(1.5) --map-filter: invert(0.9) hue-rotate(170deg) brightness(1.5)
contrast(1.2) saturate(0.3); contrast(1.2) saturate(0.3);
} }
#map.forced-light {
background: #ffffff;
color: #000000;
--map-filter: invert(0);
}
#map:active { #map:active {
cursor: grabbing; cursor: grabbing;
cursor: -moz-grabbing; cursor: -moz-grabbing;
cursor: -webkit-grabbing; cursor: -webkit-grabbing;
} }
.light {
color: #000000;
}
.dark {
color: #ffffff;
}
.leaflet-tile-pane { .leaflet-tile-pane {
filter: var(--map-filter); filter: var(--map-filter);
} }
.dark .leaflet-bar a { .dark .leaflet-bar a {
background-color: var(--card-background-color, #1c1c1c); background-color: #1c1c1c;
color: #ffffff; color: #ffffff;
} }
.dark .leaflet-bar a:hover {
background-color: #313131;
}
.leaflet-marker-draggable { .leaflet-marker-draggable {
cursor: move !important; cursor: move !important;
} }

View File

@ -41,7 +41,7 @@ import type { HomeAssistant } from "../types";
import { onBoardingStyles } from "./styles"; import { onBoardingStyles } from "./styles";
const AMSTERDAM: [number, number] = [52.3731339, 4.8903147]; const AMSTERDAM: [number, number] = [52.3731339, 4.8903147];
const mql = matchMedia("(prefers-color-scheme: dark)"); const darkMql = matchMedia("(prefers-color-scheme: dark)");
const LOCATION_MARKER_ID = "location"; const LOCATION_MARKER_ID = "location";
@customElement("onboarding-location") @customElement("onboarding-location")
@ -199,7 +199,7 @@ class OnboardingLocation extends LitElement {
this._highlightedMarker this._highlightedMarker
)} )}
zoom="14" zoom="14"
.darkMode=${mql.matches} .themeMode=${darkMql.matches ? "dark" : "light"}
.disabled=${this._working} .disabled=${this._working}
@location-updated=${this._locationChanged} @location-updated=${this._locationChanged}
@marker-clicked=${this._markerClicked} @marker-clicked=${this._markerClicked}

View File

@ -15,6 +15,7 @@ import {
mdiPlus, mdiPlus,
mdiRobotHappy, mdiRobotHappy,
mdiTag, mdiTag,
mdiTextureBox,
mdiToggleSwitch, mdiToggleSwitch,
mdiToggleSwitchOffOutline, mdiToggleSwitchOffOutline,
mdiTransitConnection, mdiTransitConnection,
@ -69,6 +70,7 @@ import type { HaMenu } from "../../../components/ha-menu";
import "../../../components/ha-menu-item"; import "../../../components/ha-menu-item";
import "../../../components/ha-sub-menu"; import "../../../components/ha-sub-menu";
import "../../../components/ha-svg-icon"; import "../../../components/ha-svg-icon";
import { createAreaRegistryEntry } from "../../../data/area_registry";
import { import {
AutomationEntity, AutomationEntity,
deleteAutomation, deleteAutomation,
@ -106,6 +108,7 @@ import { haStyle } from "../../../resources/styles";
import { HomeAssistant, Route, ServiceCallResponse } from "../../../types"; import { HomeAssistant, Route, ServiceCallResponse } from "../../../types";
import { documentationUrl } from "../../../util/documentation-url"; import { documentationUrl } from "../../../util/documentation-url";
import { turnOnOffEntity } from "../../lovelace/common/entity/turn-on-off-entity"; import { turnOnOffEntity } from "../../lovelace/common/entity/turn-on-off-entity";
import { showAreaRegistryDetailDialog } from "../areas/show-dialog-area-registry-detail";
import { showAssignCategoryDialog } from "../category/show-dialog-assign-category"; import { showAssignCategoryDialog } from "../category/show-dialog-assign-category";
import { showCategoryRegistryDetailDialog } from "../category/show-dialog-category-registry-detail"; import { showCategoryRegistryDetailDialog } from "../category/show-dialog-category-registry-detail";
import { configSections } from "../ha-panel-config"; import { configSections } from "../ha-panel-config";
@ -403,6 +406,7 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
${this.hass.localize("ui.panel.config.category.editor.add")} ${this.hass.localize("ui.panel.config.category.editor.add")}
</div> </div>
</ha-menu-item>`; </ha-menu-item>`;
const labelItems = html`${this._labels?.map((label) => { const labelItems = html`${this._labels?.map((label) => {
const color = label.color ? computeCssColor(label.color) : undefined; const color = label.color ? computeCssColor(label.color) : undefined;
const selected = this._selected.every((entityId) => const selected = this._selected.every((entityId) =>
@ -440,10 +444,45 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
</div></ha-menu-item </div></ha-menu-item
>`; >`;
const labelsInOverflow = const areaItems = html`${Object.values(this.hass.areas).map(
(this._sizeController.value && this._sizeController.value < 700) || (area) =>
html`<ha-menu-item
.value=${area.area_id}
@click=${this._handleBulkArea}
>
${area.icon
? html`<ha-icon slot="start" .icon=${area.icon}></ha-icon>`
: html`<ha-svg-icon
slot="start"
.path=${mdiTextureBox}
></ha-svg-icon>`}
<div slot="headline">${area.name}</div>
</ha-menu-item>`
)}
<ha-menu-item .value=${null} @click=${this._handleBulkArea}>
<div slot="headline">
${this.hass.localize(
"ui.panel.config.devices.picker.bulk_actions.no_area"
)}
</div>
</ha-menu-item>
<md-divider role="separator" tabindex="-1"></md-divider>
<ha-menu-item @click=${this._bulkCreateArea}>
<div slot="headline">
${this.hass.localize(
"ui.panel.config.devices.picker.bulk_actions.add_area"
)}
</div>
</ha-menu-item>`;
const areasInOverflow =
(this._sizeController.value && this._sizeController.value < 900) ||
(!this._sizeController.value && this.hass.dockedSidebar === "docked"); (!this._sizeController.value && this.hass.dockedSidebar === "docked");
const labelsInOverflow =
areasInOverflow &&
(!this._sizeController.value || this._sizeController.value < 700);
const automations = this._automations( const automations = this._automations(
this.automations, this.automations,
this._entityReg, this._entityReg,
@ -598,6 +637,22 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
></ha-svg-icon> ></ha-svg-icon>
</ha-assist-chip> </ha-assist-chip>
${labelItems} ${labelItems}
</ha-button-menu-new>`}
${areasInOverflow
? nothing
: html`<ha-button-menu-new slot="selection-bar">
<ha-assist-chip
slot="trigger"
.label=${this.hass.localize(
"ui.panel.config.devices.picker.bulk_actions.move_area"
)}
>
<ha-svg-icon
slot="trailing-icon"
.path=${mdiMenuDown}
></ha-svg-icon>
</ha-assist-chip>
${areaItems}
</ha-button-menu-new>`}` </ha-button-menu-new>`}`
: nothing : nothing
} }
@ -662,6 +717,24 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
</ha-sub-menu>` </ha-sub-menu>`
: nothing : nothing
} }
${
this.narrow || areasInOverflow
? html`<ha-sub-menu>
<ha-menu-item slot="item">
<div slot="headline">
${this.hass.localize(
"ui.panel.config.devices.picker.bulk_actions.move_area"
)}
</div>
<ha-svg-icon
slot="end"
.path=${mdiChevronRight}
></ha-svg-icon>
</ha-menu-item>
<ha-menu slot="menu">${areaItems}</ha-menu>
</ha-sub-menu>`
: nothing
}
<ha-menu-item @click=${this._handleBulkEnable}> <ha-menu-item @click=${this._handleBulkEnable}>
<ha-svg-icon slot="start" .path=${mdiToggleSwitch}></ha-svg-icon> <ha-svg-icon slot="start" .path=${mdiToggleSwitch}></ha-svg-icon>
<div slot="headline"> <div slot="headline">
@ -1191,6 +1264,46 @@ ${rejected
} }
} }
private async _handleBulkArea(ev) {
const area = ev.currentTarget.value;
this._bulkAddArea(area);
}
private async _bulkAddArea(area: string) {
const promises: Promise<UpdateEntityRegistryEntryResult>[] = [];
this._selected.forEach((entityId) => {
promises.push(
updateEntityRegistryEntry(this.hass, entityId, {
area_id: area,
})
);
});
const result = await Promise.allSettled(promises);
if (hasRejectedItems(result)) {
const rejected = rejectedItems(result);
showAlertDialog(this, {
title: this.hass.localize("ui.panel.config.common.multiselect.failed", {
number: rejected.length,
}),
text: html`<pre>
${rejected
.map((r) => r.reason.message || r.reason.code || r.reason)
.join("\r\n")}</pre
>`,
});
}
}
private async _bulkCreateArea() {
showAreaRegistryDetailDialog(this, {
createEntry: async (values) => {
const area = await createAreaRegistryEntry(this.hass, values);
this._bulkAddArea(area.area_id);
return area;
},
});
}
private async _handleBulkEnable() { private async _handleBulkEnable() {
const promises: Promise<ServiceCallResponse>[] = []; const promises: Promise<ServiceCallResponse>[] = [];
this._selected.forEach((entityId) => { this._selected.forEach((entityId) => {

View File

@ -53,6 +53,7 @@ import "../../../components/ha-alert";
import "../../../components/ha-button-menu"; import "../../../components/ha-button-menu";
import "../../../components/ha-check-list-item"; import "../../../components/ha-check-list-item";
import "../../../components/ha-filter-devices"; import "../../../components/ha-filter-devices";
import "../../../components/ha-filter-domains";
import "../../../components/ha-filter-floor-areas"; import "../../../components/ha-filter-floor-areas";
import "../../../components/ha-filter-integrations"; import "../../../components/ha-filter-integrations";
import "../../../components/ha-filter-labels"; import "../../../components/ha-filter-labels";
@ -443,6 +444,10 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
entryIds.includes(entity.config_entry_id)) entryIds.includes(entity.config_entry_id))
); );
filter.value!.forEach((domain) => filteredDomains.add(domain)); filter.value!.forEach((domain) => filteredDomains.add(domain));
} else if (key === "ha-filter-domains" && filter.value?.length) {
filteredEntities = filteredEntities.filter((entity) =>
filter.value?.includes(computeDomain(entity.entity_id))
);
} else if (key === "ha-filter-labels" && filter.value?.length) { } else if (key === "ha-filter-labels" && filter.value?.length) {
filteredEntities = filteredEntities.filter((entity) => filteredEntities = filteredEntities.filter((entity) =>
entity.labels.some((lbl) => filter.value!.includes(lbl)) entity.labels.some((lbl) => filter.value!.includes(lbl))
@ -782,6 +787,15 @@ ${
.narrow=${this.narrow} .narrow=${this.narrow}
@expanded-changed=${this._filterExpanded} @expanded-changed=${this._filterExpanded}
></ha-filter-devices> ></ha-filter-devices>
<ha-filter-domains
.hass=${this.hass}
.value=${this._filters["ha-filter-domains"]?.value}
@data-table-filter-changed=${this._filterChanged}
slot="filter-pane"
.expanded=${this._expandedFilter === "ha-filter-domains"}
.narrow=${this.narrow}
@expanded-changed=${this._filterExpanded}
></ha-filter-domains>
<ha-filter-integrations <ha-filter-integrations
.hass=${this.hass} .hass=${this.hass}
.value=${this._filters["ha-filter-integrations"]?.value} .value=${this._filters["ha-filter-integrations"]?.value}

View File

@ -15,6 +15,7 @@ import {
mdiPlay, mdiPlay,
mdiPlus, mdiPlus,
mdiTag, mdiTag,
mdiTextureBox,
} from "@mdi/js"; } from "@mdi/js";
import { differenceInDays } from "date-fns"; import { differenceInDays } from "date-fns";
import { UnsubscribeFunc } from "home-assistant-js-websocket"; import { UnsubscribeFunc } from "home-assistant-js-websocket";
@ -61,6 +62,7 @@ import "../../../components/ha-menu-item";
import "../../../components/ha-state-icon"; import "../../../components/ha-state-icon";
import "../../../components/ha-sub-menu"; import "../../../components/ha-sub-menu";
import "../../../components/ha-svg-icon"; import "../../../components/ha-svg-icon";
import { createAreaRegistryEntry } from "../../../data/area_registry";
import { import {
CategoryRegistryEntry, CategoryRegistryEntry,
createCategoryRegistryEntry, createCategoryRegistryEntry,
@ -97,6 +99,7 @@ import { haStyle } from "../../../resources/styles";
import { HomeAssistant, Route } from "../../../types"; import { HomeAssistant, Route } from "../../../types";
import { documentationUrl } from "../../../util/documentation-url"; import { documentationUrl } from "../../../util/documentation-url";
import { showToast } from "../../../util/toast"; import { showToast } from "../../../util/toast";
import { showAreaRegistryDetailDialog } from "../areas/show-dialog-area-registry-detail";
import { showAssignCategoryDialog } from "../category/show-dialog-assign-category"; import { showAssignCategoryDialog } from "../category/show-dialog-assign-category";
import { showCategoryRegistryDetailDialog } from "../category/show-dialog-category-registry-detail"; import { showCategoryRegistryDetailDialog } from "../category/show-dialog-category-registry-detail";
import { configSections } from "../ha-panel-config"; import { configSections } from "../ha-panel-config";
@ -406,6 +409,7 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
${this.hass.localize("ui.panel.config.category.editor.add")} ${this.hass.localize("ui.panel.config.category.editor.add")}
</div> </div>
</ha-menu-item>`; </ha-menu-item>`;
const labelItems = html` ${this._labels?.map((label) => { const labelItems = html` ${this._labels?.map((label) => {
const color = label.color ? computeCssColor(label.color) : undefined; const color = label.color ? computeCssColor(label.color) : undefined;
const selected = this._selected.every((entityId) => const selected = this._selected.every((entityId) =>
@ -442,9 +446,46 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
${this.hass.localize("ui.panel.config.labels.add_label")} ${this.hass.localize("ui.panel.config.labels.add_label")}
</div></ha-menu-item </div></ha-menu-item
>`; >`;
const labelsInOverflow =
(this._sizeController.value && this._sizeController.value < 700) || const areaItems = html`${Object.values(this.hass.areas).map(
(area) =>
html`<ha-menu-item
.value=${area.area_id}
@click=${this._handleBulkArea}
>
${area.icon
? html`<ha-icon slot="start" .icon=${area.icon}></ha-icon>`
: html`<ha-svg-icon
slot="start"
.path=${mdiTextureBox}
></ha-svg-icon>`}
<div slot="headline">${area.name}</div>
</ha-menu-item>`
)}
<ha-menu-item .value=${null} @click=${this._handleBulkArea}>
<div slot="headline">
${this.hass.localize(
"ui.panel.config.devices.picker.bulk_actions.no_area"
)}
</div>
</ha-menu-item>
<md-divider role="separator" tabindex="-1"></md-divider>
<ha-menu-item @click=${this._bulkCreateArea}>
<div slot="headline">
${this.hass.localize(
"ui.panel.config.devices.picker.bulk_actions.add_area"
)}
</div>
</ha-menu-item>`;
const areasInOverflow =
(this._sizeController.value && this._sizeController.value < 900) ||
(!this._sizeController.value && this.hass.dockedSidebar === "docked"); (!this._sizeController.value && this.hass.dockedSidebar === "docked");
const labelsInOverflow =
areasInOverflow &&
(!this._sizeController.value || this._sizeController.value < 700);
const scenes = this._scenes( const scenes = this._scenes(
this.scenes, this.scenes,
this._entityReg, this._entityReg,
@ -453,6 +494,7 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
this._labels, this._labels,
this._filteredScenes this._filteredScenes
); );
return html` return html`
<hass-tabs-subpage-data-table <hass-tabs-subpage-data-table
.hass=${this.hass} .hass=${this.hass}
@ -582,9 +624,25 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
></ha-svg-icon> ></ha-svg-icon>
</ha-assist-chip> </ha-assist-chip>
${labelItems} ${labelItems}
</ha-button-menu-new>`}
${areasInOverflow
? nothing
: html`<ha-button-menu-new slot="selection-bar">
<ha-assist-chip
slot="trigger"
.label=${this.hass.localize(
"ui.panel.config.devices.picker.bulk_actions.move_area"
)}
>
<ha-svg-icon
slot="trailing-icon"
.path=${mdiMenuDown}
></ha-svg-icon>
</ha-assist-chip>
${areaItems}
</ha-button-menu-new>`}` </ha-button-menu-new>`}`
: nothing} : nothing}
${this.narrow || labelsInOverflow ${this.narrow || areasInOverflow
? html` ? html`
<ha-button-menu-new has-overflow slot="selection-bar"> <ha-button-menu-new has-overflow slot="selection-bar">
${ ${
@ -630,7 +688,7 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
: nothing : nothing
} }
${ ${
this.narrow || this.hass.dockedSidebar === "docked" this.narrow || labelsInOverflow
? html`<ha-sub-menu> ? html`<ha-sub-menu>
<ha-menu-item slot="item"> <ha-menu-item slot="item">
<div slot="headline"> <div slot="headline">
@ -647,6 +705,24 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
</ha-sub-menu>` </ha-sub-menu>`
: nothing : nothing
} }
${
this.narrow || areasInOverflow
? html`<ha-sub-menu>
<ha-menu-item slot="item">
<div slot="headline">
${this.hass.localize(
"ui.panel.config.devices.picker.bulk_actions.move_area"
)}
</div>
<ha-svg-icon
slot="end"
.path=${mdiChevronRight}
></ha-svg-icon>
</ha-menu-item>
<ha-menu slot="menu">${areaItems}</ha-menu>
</ha-sub-menu>`
: nothing
}
</ha-button-menu-new>` </ha-button-menu-new>`
: nothing} : nothing}
${!this.scenes.length ${!this.scenes.length
@ -875,6 +951,46 @@ ${rejected
} }
} }
private async _handleBulkArea(ev) {
const area = ev.currentTarget.value;
this._bulkAddArea(area);
}
private async _bulkAddArea(area: string) {
const promises: Promise<UpdateEntityRegistryEntryResult>[] = [];
this._selected.forEach((entityId) => {
promises.push(
updateEntityRegistryEntry(this.hass, entityId, {
area_id: area,
})
);
});
const result = await Promise.allSettled(promises);
if (hasRejectedItems(result)) {
const rejected = rejectedItems(result);
showAlertDialog(this, {
title: this.hass.localize("ui.panel.config.common.multiselect.failed", {
number: rejected.length,
}),
text: html`<pre>
${rejected
.map((r) => r.reason.message || r.reason.code || r.reason)
.join("\r\n")}</pre
>`,
});
}
}
private async _bulkCreateArea() {
showAreaRegistryDetailDialog(this, {
createEntry: async (values) => {
const area = await createAreaRegistryEntry(this.hass, values);
this._bulkAddArea(area.area_id);
return area;
},
});
}
private _editCategory(scene: any) { private _editCategory(scene: any) {
const entityReg = this._entityReg.find( const entityReg = this._entityReg.find(
(reg) => reg.entity_id === scene.entity_id (reg) => reg.entity_id === scene.entity_id

View File

@ -13,6 +13,7 @@ import {
mdiPlus, mdiPlus,
mdiScriptText, mdiScriptText,
mdiTag, mdiTag,
mdiTextureBox,
mdiTransitConnection, mdiTransitConnection,
} from "@mdi/js"; } from "@mdi/js";
import { differenceInDays } from "date-fns"; import { differenceInDays } from "date-fns";
@ -61,6 +62,7 @@ import "../../../components/ha-icon-overflow-menu";
import "../../../components/ha-menu-item"; import "../../../components/ha-menu-item";
import "../../../components/ha-sub-menu"; import "../../../components/ha-sub-menu";
import "../../../components/ha-svg-icon"; import "../../../components/ha-svg-icon";
import { createAreaRegistryEntry } from "../../../data/area_registry";
import { import {
CategoryRegistryEntry, CategoryRegistryEntry,
createCategoryRegistryEntry, createCategoryRegistryEntry,
@ -98,6 +100,7 @@ import { haStyle } from "../../../resources/styles";
import { HomeAssistant, Route } from "../../../types"; import { HomeAssistant, Route } from "../../../types";
import { documentationUrl } from "../../../util/documentation-url"; import { documentationUrl } from "../../../util/documentation-url";
import { showToast } from "../../../util/toast"; import { showToast } from "../../../util/toast";
import { showAreaRegistryDetailDialog } from "../areas/show-dialog-area-registry-detail";
import { showNewAutomationDialog } from "../automation/show-dialog-new-automation"; import { showNewAutomationDialog } from "../automation/show-dialog-new-automation";
import { showAssignCategoryDialog } from "../category/show-dialog-assign-category"; import { showAssignCategoryDialog } from "../category/show-dialog-assign-category";
import { showCategoryRegistryDetailDialog } from "../category/show-dialog-category-registry-detail"; import { showCategoryRegistryDetailDialog } from "../category/show-dialog-category-registry-detail";
@ -418,6 +421,7 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
${this.hass.localize("ui.panel.config.category.editor.add")} ${this.hass.localize("ui.panel.config.category.editor.add")}
</div> </div>
</ha-menu-item>`; </ha-menu-item>`;
const labelItems = html`${this._labels?.map((label) => { const labelItems = html`${this._labels?.map((label) => {
const color = label.color ? computeCssColor(label.color) : undefined; const color = label.color ? computeCssColor(label.color) : undefined;
const selected = this._selected.every((entityId) => const selected = this._selected.every((entityId) =>
@ -454,9 +458,46 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
${this.hass.localize("ui.panel.config.labels.add_label")} ${this.hass.localize("ui.panel.config.labels.add_label")}
</div></ha-menu-item </div></ha-menu-item
>`; >`;
const labelsInOverflow =
(this._sizeController.value && this._sizeController.value < 700) || const areaItems = html`${Object.values(this.hass.areas).map(
(area) =>
html`<ha-menu-item
.value=${area.area_id}
@click=${this._handleBulkArea}
>
${area.icon
? html`<ha-icon slot="start" .icon=${area.icon}></ha-icon>`
: html`<ha-svg-icon
slot="start"
.path=${mdiTextureBox}
></ha-svg-icon>`}
<div slot="headline">${area.name}</div>
</ha-menu-item>`
)}
<ha-menu-item .value=${null} @click=${this._handleBulkArea}>
<div slot="headline">
${this.hass.localize(
"ui.panel.config.devices.picker.bulk_actions.no_area"
)}
</div>
</ha-menu-item>
<md-divider role="separator" tabindex="-1"></md-divider>
<ha-menu-item @click=${this._bulkCreateArea}>
<div slot="headline">
${this.hass.localize(
"ui.panel.config.devices.picker.bulk_actions.add_area"
)}
</div>
</ha-menu-item>`;
const areasInOverflow =
(this._sizeController.value && this._sizeController.value < 900) ||
(!this._sizeController.value && this.hass.dockedSidebar === "docked"); (!this._sizeController.value && this.hass.dockedSidebar === "docked");
const labelsInOverflow =
areasInOverflow &&
(!this._sizeController.value || this._sizeController.value < 700);
const scripts = this._scripts( const scripts = this._scripts(
this.scripts, this.scripts,
this._entityReg, this._entityReg,
@ -608,9 +649,25 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
></ha-svg-icon> ></ha-svg-icon>
</ha-assist-chip> </ha-assist-chip>
${labelItems} ${labelItems}
</ha-button-menu-new>`}
${areasInOverflow
? nothing
: html`<ha-button-menu-new slot="selection-bar">
<ha-assist-chip
slot="trigger"
.label=${this.hass.localize(
"ui.panel.config.devices.picker.bulk_actions.move_area"
)}
>
<ha-svg-icon
slot="trailing-icon"
.path=${mdiMenuDown}
></ha-svg-icon>
</ha-assist-chip>
${areaItems}
</ha-button-menu-new>`}` </ha-button-menu-new>`}`
: nothing} : nothing}
${this.narrow || labelsInOverflow ${this.narrow || areasInOverflow
? html` ? html`
<ha-button-menu-new has-overflow slot="selection-bar"> <ha-button-menu-new has-overflow slot="selection-bar">
${ ${
@ -656,7 +713,7 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
: nothing : nothing
} }
${ ${
this.narrow || this.hass.dockedSidebar === "docked" this.narrow || labelsInOverflow
? html`<ha-sub-menu> ? html`<ha-sub-menu>
<ha-menu-item slot="item"> <ha-menu-item slot="item">
<div slot="headline"> <div slot="headline">
@ -673,6 +730,24 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
</ha-sub-menu>` </ha-sub-menu>`
: nothing : nothing
} }
${
this.narrow || areasInOverflow
? html`<ha-sub-menu>
<ha-menu-item slot="item">
<div slot="headline">
${this.hass.localize(
"ui.panel.config.devices.picker.bulk_actions.move_area"
)}
</div>
<ha-svg-icon
slot="end"
.path=${mdiChevronRight}
></ha-svg-icon>
</ha-menu-item>
<ha-menu slot="menu">${areaItems}</ha-menu>
</ha-sub-menu>`
: nothing
}
</ha-button-menu-new>` </ha-button-menu-new>`
: nothing} : nothing}
${!this.scripts.length ${!this.scripts.length
@ -1111,6 +1186,46 @@ ${rejected
}); });
} }
private async _handleBulkArea(ev) {
const area = ev.currentTarget.value;
this._bulkAddArea(area);
}
private async _bulkAddArea(area: string) {
const promises: Promise<UpdateEntityRegistryEntryResult>[] = [];
this._selected.forEach((entityId) => {
promises.push(
updateEntityRegistryEntry(this.hass, entityId, {
area_id: area,
})
);
});
const result = await Promise.allSettled(promises);
if (hasRejectedItems(result)) {
const rejected = rejectedItems(result);
showAlertDialog(this, {
title: this.hass.localize("ui.panel.config.common.multiselect.failed", {
number: rejected.length,
}),
text: html`<pre>
${rejected
.map((r) => r.reason.message || r.reason.code || r.reason)
.join("\r\n")}</pre
>`,
});
}
}
private async _bulkCreateArea() {
showAreaRegistryDetailDialog(this, {
createEntry: async (values) => {
const area = await createAreaRegistryEntry(this.hass, values);
this._bulkAddArea(area.area_id);
return area;
},
});
}
private _handleSortingChanged(ev: CustomEvent) { private _handleSortingChanged(ev: CustomEvent) {
this._activeSorting = ev.detail; this._activeSorting = ev.detail;
} }

View File

@ -90,12 +90,10 @@ class HuiLockOpenDoorCardFeature
return html` return html`
${this._buttonState === "success" ${this._buttonState === "success"
? html` ? html`
<div class="buttons">
<p class="open-success"> <p class="open-success">
<ha-svg-icon path=${mdiCheck}></ha-svg-icon> <ha-svg-icon path=${mdiCheck}></ha-svg-icon>
${this.hass.localize("ui.card.lock.open_door_success")} ${this.hass.localize("ui.card.lock.open_door_success")}
</p> </p>
</div>
` `
: html` : html`
<ha-control-button-group> <ha-control-button-group>
@ -115,12 +113,6 @@ class HuiLockOpenDoorCardFeature
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return css` return css`
.buttons {
display: flex;
align-items: center;
justify-content: center;
margin-top: 0;
}
ha-control-button { ha-control-button {
font-size: 14px; font-size: 14px;
} }
@ -139,10 +131,14 @@ class HuiLockOpenDoorCardFeature
line-height: 14px; line-height: 14px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center;
flex-direction: row; flex-direction: row;
gap: 8px; gap: 8px;
font-weight: 500; font-weight: 500;
color: var(--success-color); color: var(--success-color);
margin: 0 12px 12px 12px;
height: 40px;
text-align: center;
} }
ha-control-button-group + ha-attributes:not([empty]) { ha-control-button-group + ha-attributes:not([empty]) {
margin-top: 16px; margin-top: 16px;

View File

@ -138,7 +138,7 @@ class HuiMapCard extends LitElement implements LovelaceCard {
includeDomains includeDomains
); );
return { type: "map", entities: foundEntities }; return { type: "map", entities: foundEntities, theme_mode: "auto" };
} }
protected render() { protected render() {
@ -151,6 +151,17 @@ class HuiMapCard extends LitElement implements LovelaceCard {
(${this._error.code}) (${this._error.code})
</ha-alert>`; </ha-alert>`;
} }
const isDarkMode =
this._config.dark_mode || this._config.theme_mode === "dark"
? true
: this._config.theme_mode === "light"
? false
: this.hass.themes.darkMode;
const themeMode =
this._config.theme_mode || (this._config.dark_mode ? "dark" : "auto");
return html` return html`
<ha-card id="card" .header=${this._config.title}> <ha-card id="card" .header=${this._config.title}>
<div id="root"> <div id="root">
@ -161,7 +172,7 @@ class HuiMapCard extends LitElement implements LovelaceCard {
.paths=${this._getHistoryPaths(this._config, this._stateHistory)} .paths=${this._getHistoryPaths(this._config, this._stateHistory)}
.autoFit=${this._config.auto_fit || false} .autoFit=${this._config.auto_fit || false}
.fitZones=${this._config.fit_zones} .fitZones=${this._config.fit_zones}
?darkMode=${this._config.dark_mode} .themeMode=${themeMode}
interactiveZones interactiveZones
renderPassive renderPassive
></ha-map> ></ha-map>
@ -170,6 +181,7 @@ class HuiMapCard extends LitElement implements LovelaceCard {
"ui.panel.lovelace.cards.map.reset_focus" "ui.panel.lovelace.cards.map.reset_focus"
)} )}
.path=${mdiImageFilterCenterFocus} .path=${mdiImageFilterCenterFocus}
style=${isDarkMode ? "color:#ffffff" : "color:#000000"}
@click=${this._fitMap} @click=${this._fitMap}
tabindex="0" tabindex="0"
></ha-icon-button> ></ha-icon-button>

View File

@ -3,7 +3,7 @@ import { ActionConfig } from "../../../data/lovelace/config/action";
import { LovelaceCardConfig } from "../../../data/lovelace/config/card"; import { LovelaceCardConfig } from "../../../data/lovelace/config/card";
import { Statistic, StatisticType } from "../../../data/recorder"; import { Statistic, StatisticType } from "../../../data/recorder";
import { ForecastType } from "../../../data/weather"; import { ForecastType } from "../../../data/weather";
import { FullCalendarView, TranslationDict } from "../../../types"; import { FullCalendarView, ThemeMode, TranslationDict } from "../../../types";
import { LovelaceCardFeatureConfig } from "../card-features/types"; import { LovelaceCardFeatureConfig } from "../card-features/types";
import { LegacyStateFilter } from "../common/evaluate-filter"; import { LegacyStateFilter } from "../common/evaluate-filter";
import { Condition, LegacyCondition } from "../common/validate-condition"; import { Condition, LegacyCondition } from "../common/validate-condition";
@ -314,6 +314,7 @@ export interface MapCardConfig extends LovelaceCardConfig {
hours_to_show?: number; hours_to_show?: number;
geo_location_sources?: string[]; geo_location_sources?: string[];
dark_mode?: boolean; dark_mode?: boolean;
theme_mode?: ThemeMode;
} }
export interface MarkdownCardConfig extends LovelaceCardConfig { export interface MarkdownCardConfig extends LovelaceCardConfig {

View File

@ -1,3 +1,4 @@
import { mdiPalette } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { import {
@ -11,6 +12,7 @@ import {
string, string,
union, union,
} from "superstruct"; } from "superstruct";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../common/dom/fire_event"; import { fireEvent } from "../../../../common/dom/fire_event";
import { hasLocation } from "../../../../common/entity/has_location"; import { hasLocation } from "../../../../common/entity/has_location";
import "../../../../components/ha-form/ha-form"; import "../../../../components/ha-form/ha-form";
@ -28,6 +30,7 @@ import { processEditorEntities } from "../process-editor-entities";
import { baseLovelaceCardConfig } from "../structs/base-card-struct"; import { baseLovelaceCardConfig } from "../structs/base-card-struct";
import { EntitiesEditorEvent } from "../types"; import { EntitiesEditorEvent } from "../types";
import { configElementStyle } from "./config-elements-style"; import { configElementStyle } from "./config-elements-style";
import { LocalizeFunc } from "../../../../common/translations/localize";
export const mapEntitiesConfigStruct = union([ export const mapEntitiesConfigStruct = union([
object({ object({
@ -50,11 +53,30 @@ const cardConfigStruct = assign(
hours_to_show: optional(number()), hours_to_show: optional(number()),
geo_location_sources: optional(array(string())), geo_location_sources: optional(array(string())),
auto_fit: optional(boolean()), auto_fit: optional(boolean()),
theme_mode: optional(string()),
}) })
); );
const SCHEMA = [ const themeModes = ["auto", "light", "dark"] as const;
@customElement("hui-map-card-editor")
export class HuiMapCardEditor extends LitElement implements LovelaceCardEditor {
@property({ attribute: false }) public hass?: HomeAssistant;
@state() private _config?: MapCardConfig;
@state() private _configEntities?: EntityConfig[];
private _schema = memoizeOne(
(localize: LocalizeFunc) =>
[
{ name: "title", selector: { text: {} } }, { name: "title", selector: { text: {} } },
{
name: "",
type: "expandable",
iconPath: mdiPalette,
title: localize(`ui.panel.lovelace.editor.card.map.appearance`),
schema: [
{ {
name: "", name: "",
type: "grid", type: "grid",
@ -65,7 +87,21 @@ const SCHEMA = [
default: DEFAULT_ZOOM, default: DEFAULT_ZOOM,
selector: { number: { mode: "box", min: 0 } }, selector: { number: { mode: "box", min: 0 } },
}, },
{ name: "dark_mode", selector: { boolean: {} } }, {
name: "theme_mode",
default: "auto",
selector: {
select: {
mode: "dropdown",
options: themeModes.map((themeMode) => ({
value: themeMode,
label: localize(
`ui.panel.lovelace.editor.card.map.theme_modes.${themeMode}`
),
})),
},
},
},
{ {
name: "hours_to_show", name: "hours_to_show",
default: DEFAULT_HOURS_TO_SHOW, default: DEFAULT_HOURS_TO_SHOW,
@ -73,18 +109,26 @@ const SCHEMA = [
}, },
], ],
}, },
] as const; ],
},
@customElement("hui-map-card-editor") ] as const
export class HuiMapCardEditor extends LitElement implements LovelaceCardEditor { );
@property({ attribute: false }) public hass?: HomeAssistant;
@state() private _config?: MapCardConfig;
@state() private _configEntities?: EntityConfig[];
public setConfig(config: MapCardConfig): void { public setConfig(config: MapCardConfig): void {
assert(config, cardConfigStruct); assert(config, cardConfigStruct);
// Migrate legacy dark_mode to theme_mode
if (!this._config && !("theme_mode" in config)) {
config = { ...config };
if (config.dark_mode) {
config.theme_mode = "dark";
} else {
config.theme_mode = "auto";
}
delete config.dark_mode;
fireEvent(this, "config-changed", { config: config });
}
this._config = config; this._config = config;
this._configEntities = config.entities this._configEntities = config.entities
? processEditorEntities(config.entities) ? processEditorEntities(config.entities)
@ -104,23 +148,24 @@ export class HuiMapCardEditor extends LitElement implements LovelaceCardEditor {
<ha-form <ha-form
.hass=${this.hass} .hass=${this.hass}
.data=${this._config} .data=${this._config}
.schema=${SCHEMA} .schema=${this._schema(this.hass.localize)}
.computeLabel=${this._computeLabelCallback} .computeLabel=${this._computeLabelCallback}
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
></ha-form> ></ha-form>
<div class="card-config">
<hui-entity-editor <hui-entity-editor
.hass=${this.hass} .hass=${this.hass}
.entities=${this._configEntities} .entities=${this._configEntities}
.entityFilter=${hasLocation} .entityFilter=${hasLocation}
@entities-changed=${this._entitiesValueChanged} @entities-changed=${this._entitiesValueChanged}
></hui-entity-editor> ></hui-entity-editor>
<h3> <h3>
${this.hass.localize( ${this.hass.localize(
"ui.panel.lovelace.editor.card.map.geo_location_sources" "ui.panel.lovelace.editor.card.map.geo_location_sources"
)} )}
</h3> </h3>
<div class="geo_location_sources">
<hui-input-list-editor <hui-input-list-editor
.inputLabel=${this.hass.localize( .inputLabel=${this.hass.localize(
"ui.panel.lovelace.editor.card.map.source" "ui.panel.lovelace.editor.card.map.source"
@ -129,8 +174,6 @@ export class HuiMapCardEditor extends LitElement implements LovelaceCardEditor {
.value=${this._geo_location_sources} .value=${this._geo_location_sources}
@value-changed=${this._geoSourcesChanged} @value-changed=${this._geoSourcesChanged}
></hui-input-list-editor> ></hui-input-list-editor>
</div>
</div>
`; `;
} }
@ -170,9 +213,14 @@ export class HuiMapCardEditor extends LitElement implements LovelaceCardEditor {
fireEvent(this, "config-changed", { config: ev.detail.value }); fireEvent(this, "config-changed", { config: ev.detail.value });
} }
private _computeLabelCallback = (schema: SchemaUnion<typeof SCHEMA>) => { private _computeLabelCallback = (
schema: SchemaUnion<ReturnType<typeof this._schema>>
) => {
switch (schema.name) { switch (schema.name) {
case "dark_mode": case "theme_mode":
return this.hass!.localize(
`ui.panel.lovelace.editor.card.map.${schema.name}`
);
case "default_zoom": case "default_zoom":
return this.hass!.localize( return this.hass!.localize(
`ui.panel.lovelace.editor.card.map.${schema.name}` `ui.panel.lovelace.editor.card.map.${schema.name}`
@ -185,16 +233,7 @@ export class HuiMapCardEditor extends LitElement implements LovelaceCardEditor {
}; };
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return [ return [configElementStyle, css``];
configElementStyle,
css`
.geo_location_sources {
padding-left: 20px;
padding-inline-start: 20px;
direction: var(--direction);
}
`,
];
} }
} }

View File

@ -5835,7 +5835,14 @@
"name": "Map", "name": "Map",
"geo_location_sources": "Geolocation sources", "geo_location_sources": "Geolocation sources",
"dark_mode": "Dark mode?", "dark_mode": "Dark mode?",
"default_zoom": "Default zoom", "appearance": "Appearance",
"theme_mode": "Theme Mode",
"theme_modes": {
"auto": "Auto",
"light": "Light",
"dark": "Dark"
},
"default_zoom": "Default Zoom",
"source": "Source", "source": "Source",
"description": "The Map card that allows you to display entities on a map." "description": "The Map card that allows you to display entities on a map."
}, },

View File

@ -139,6 +139,8 @@ export type FullCalendarView =
| "dayGridDay" | "dayGridDay"
| "listWeek"; | "listWeek";
export type ThemeMode = "auto" | "light" | "dark";
export interface ToggleButton { export interface ToggleButton {
label: string; label: string;
iconPath?: string; iconPath?: string;

View File

@ -7028,10 +7028,10 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"cropperjs@npm:1.6.1": "cropperjs@npm:1.6.2":
version: 1.6.1 version: 1.6.2
resolution: "cropperjs@npm:1.6.1" resolution: "cropperjs@npm:1.6.2"
checksum: 10/3ecd895ba8820021d6efd8effda54fb40a418e6940c307d46c62812d7d0ee10aacde473b44b7cfb315ec0fc2e5c5c573f4de06e8c2e8b7cb134f12f65c1d4aa7 checksum: 10/4b97ac27b7fd65316a531372dd96ce1073b4620c1b012fbad5acb0fdc28952112466bc333a98c510fe2bc9dcdeee58f5c8619522c045c6166300621673e3d759
languageName: node languageName: node
linkType: hard linkType: hard
@ -9674,7 +9674,7 @@ __metadata:
color-name: "npm:2.0.0" color-name: "npm:2.0.0"
comlink: "npm:4.4.1" comlink: "npm:4.4.1"
core-js: "npm:3.37.0" core-js: "npm:3.37.0"
cropperjs: "npm:1.6.1" cropperjs: "npm:1.6.2"
date-fns: "npm:3.6.0" date-fns: "npm:3.6.0"
date-fns-tz: "npm:3.1.3" date-fns-tz: "npm:3.1.3"
deep-clone-simple: "npm:1.1.1" deep-clone-simple: "npm:1.1.1"