Compare commits

...

5 Commits

Author SHA1 Message Date
Bram Kragten
e3be190b36 Add no device option to device filter 2024-04-24 12:56:24 +02:00
Bram Kragten
87bcd3e471 Add multiselect area to automation/scene/script (#20604) 2024-04-24 10:10:43 +00:00
Paul Bottein
7e9b01b56d Fix text alignment for open lock card feature (#20603)
Fix text aligment for open lock card feature
2024-04-24 09:36:17 +00:00
karwosts
713763fc21 Fix visual map issues (#20541)
* Fix visual map issues (#15587)

- hover background color of zoom control in dark mode
- map light mode when dark theme is used
- background color of zoom control with map dark mode when light theme is used

* Fix breaking change (#15587)

* Change theme mode selection to use dropdown (#15587)

- Additionally fixed unpleasant horizontal scrollbar in map editor

* Add yaml migration, fix force light/dark options

---------

Co-authored-by: Christoph Wen <wen.christoph@gmail.com>
2024-04-24 11:35:37 +02:00
renovate[bot]
5b7ab1bfcb Update dependency cropperjs to v1.6.2 (#20600)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-24 11:28:58 +02:00
15 changed files with 564 additions and 113 deletions

View File

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

View File

@@ -20,6 +20,8 @@ import "./ha-check-list-item";
import "./ha-expansion-panel";
import "./search-input-outlined";
export const FILTER_NO_DEVICE = "__NO_DEVICE__";
@customElement("ha-filter-devices")
export class HaFilterDevices extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -32,6 +34,9 @@ export class HaFilterDevices extends LitElement {
@property({ type: Boolean }) public narrow = false;
@property({ type: Boolean, attribute: "no-device-option" })
public noDeviceOption = false;
@state() private _shouldRender = false;
@state() private _filter?: string;
@@ -74,6 +79,7 @@ export class HaFilterDevices extends LitElement {
.items=${this._devices(
this.hass.devices,
this._filter || "",
this.noDeviceOption,
this.value
)}
.keyFunction=${this._keyFunction}
@@ -137,9 +143,13 @@ export class HaFilterDevices extends LitElement {
}
private _devices = memoizeOne(
(devices: HomeAssistant["devices"], filter: string, _value) => {
const values = Object.values(devices);
return values
(
devices: HomeAssistant["devices"],
filter: string,
noDeviceOption: boolean,
_value
) => {
const values = Object.values(devices)
.filter(
(device) =>
!filter ||
@@ -152,6 +162,28 @@ export class HaFilterDevices extends LitElement {
this.hass.locale.language
)
);
if (noDeviceOption) {
values.unshift({
id: FILTER_NO_DEVICE,
name: this.hass.localize("ui.panel.config.devices.no_device"),
area_id: null,
configuration_url: null,
config_entries: [],
connections: [],
disabled_by: null,
entry_type: null,
identifiers: [],
manufacturer: null,
model: null,
name_by_user: null,
sw_version: null,
hw_version: null,
via_device_id: null,
serial_number: null,
labels: [],
});
}
return values;
}
);
@@ -171,7 +203,7 @@ export class HaFilterDevices extends LitElement {
for (const deviceId of this.value) {
value.push(deviceId);
if (this.type) {
if (this.type && deviceId !== FILTER_NO_DEVICE) {
relatedPromises.push(findRelated(this.hass, "device", deviceId));
}
}

View File

@@ -133,7 +133,7 @@ export class HaLocationsEditor extends LitElement {
.layers=${this._getLayers(this._circles, this._locationMarkers)}
.zoom=${this.zoom}
.autoFit=${this.autoFit}
?darkMode=${this.darkMode}
?forceDarkMode=${this.darkMode}
></ha-map>
${this.helper
? html`<ha-input-helper-text>${this.helper}</ha-input-helper-text>`

View File

@@ -69,7 +69,9 @@ export class HaMap extends ReactiveElement {
@property({ type: Boolean }) public fitZones = false;
@property({ type: Boolean }) public darkMode = false;
@property({ type: Boolean }) public forceDarkMode = false;
@property({ type: Boolean }) public forceLightMode = false;
@property({ type: Number }) public zoom = 14;
@@ -154,7 +156,8 @@ export class HaMap extends ReactiveElement {
}
if (
!changedProps.has("darkMode") &&
!changedProps.has("forceDarkMode") &&
!changedProps.has("forceLightMode") &&
(!changedProps.has("hass") ||
(oldHass && oldHass.themes?.darkMode === this.hass.themes?.darkMode))
) {
@@ -164,11 +167,13 @@ export class HaMap extends ReactiveElement {
}
private _updateMapStyle(): void {
const darkMode = this.darkMode || (this.hass.themes.darkMode ?? false);
const forcedDark = this.darkMode;
const darkMode =
!this.forceLightMode &&
(this.forceDarkMode || (this.hass.themes.darkMode ?? false));
const map = this.renderRoot.querySelector("#map");
map!.classList.toggle("dark", darkMode);
map!.classList.toggle("forced-dark", forcedDark);
map!.classList.toggle("forced-dark", this.forceDarkMode);
map!.classList.toggle("forced-light", this.forceLightMode);
}
private async _loadMap(): Promise<void> {
@@ -398,8 +403,13 @@ export class HaMap extends ReactiveElement {
"--dark-primary-color"
);
const className =
this.darkMode || this.hass.themes.darkMode ? "dark" : "light";
const className = this.forceLightMode
? "light"
: this.forceDarkMode
? "dark"
: this.hass.themes.darkMode
? "dark"
: "light";
for (const entity of this.entities) {
const stateObj = hass.states[getEntityId(entity)];
@@ -543,27 +553,30 @@ export class HaMap extends ReactiveElement {
background: #090909;
}
#map.forced-dark {
color: #ffffff;
--map-filter: invert(0.9) hue-rotate(170deg) brightness(1.5)
contrast(1.2) saturate(0.3);
}
#map.forced-light {
background: #ffffff;
color: #000000;
--map-filter: invert(0);
}
#map:active {
cursor: grabbing;
cursor: -moz-grabbing;
cursor: -webkit-grabbing;
}
.light {
color: #000000;
}
.dark {
color: #ffffff;
}
.leaflet-tile-pane {
filter: var(--map-filter);
}
.dark .leaflet-bar a {
background-color: var(--card-background-color, #1c1c1c);
background-color: #1c1c1c;
color: #ffffff;
}
.dark .leaflet-bar a:hover {
background-color: #313131;
}
.leaflet-marker-draggable {
cursor: move !important;
}

View File

@@ -15,6 +15,7 @@ import {
mdiPlus,
mdiRobotHappy,
mdiTag,
mdiTextureBox,
mdiToggleSwitch,
mdiToggleSwitchOffOutline,
mdiTransitConnection,
@@ -69,6 +70,7 @@ import type { HaMenu } from "../../../components/ha-menu";
import "../../../components/ha-menu-item";
import "../../../components/ha-sub-menu";
import "../../../components/ha-svg-icon";
import { createAreaRegistryEntry } from "../../../data/area_registry";
import {
AutomationEntity,
deleteAutomation,
@@ -106,6 +108,7 @@ import { haStyle } from "../../../resources/styles";
import { HomeAssistant, Route, ServiceCallResponse } from "../../../types";
import { documentationUrl } from "../../../util/documentation-url";
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 { showCategoryRegistryDetailDialog } from "../category/show-dialog-category-registry-detail";
import { configSections } from "../ha-panel-config";
@@ -403,6 +406,7 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
${this.hass.localize("ui.panel.config.category.editor.add")}
</div>
</ha-menu-item>`;
const labelItems = html`${this._labels?.map((label) => {
const color = label.color ? computeCssColor(label.color) : undefined;
const selected = this._selected.every((entityId) =>
@@ -440,10 +444,45 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
</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");
const labelsInOverflow =
areasInOverflow &&
(!this._sizeController.value || this._sizeController.value < 700);
const automations = this._automations(
this.automations,
this._entityReg,
@@ -598,6 +637,22 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
></ha-svg-icon>
</ha-assist-chip>
${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>`}`
: nothing
}
@@ -662,6 +717,24 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
</ha-sub-menu>`
: 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-svg-icon slot="start" .path=${mdiToggleSwitch}></ha-svg-icon>
<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() {
const promises: Promise<ServiceCallResponse>[] = [];
this._selected.forEach((entityId) => {

View File

@@ -52,7 +52,6 @@ import "../../../components/data-table/ha-data-table-labels";
import "../../../components/ha-alert";
import "../../../components/ha-button-menu";
import "../../../components/ha-check-list-item";
import "../../../components/ha-filter-devices";
import "../../../components/ha-filter-floor-areas";
import "../../../components/ha-filter-integrations";
import "../../../components/ha-filter-labels";
@@ -97,6 +96,7 @@ import { configSections } from "../ha-panel-config";
import "../integrations/ha-integration-overflow-menu";
import { showAddIntegrationDialog } from "../integrations/show-add-integration-dialog";
import { showLabelDetailDialog } from "../labels/show-dialog-label-detail";
import { FILTER_NO_DEVICE } from "../../../components/ha-filter-devices";
export interface StateEntity
extends Omit<EntityRegistryEntry, "id" | "unique_id"> {
@@ -448,8 +448,12 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
entity.labels.some((lbl) => filter.value!.includes(lbl))
);
} else if (filter.items) {
filteredEntities = filteredEntities.filter((entity) =>
filter.items!.has(entity.entity_id)
filteredEntities = filteredEntities.filter(
(entity) =>
filter.items!.has(entity.entity_id) ||
(key === "ha-filter-devices" &&
filter.value?.includes(FILTER_NO_DEVICE) &&
!entity.device_id)
);
}
});
@@ -781,6 +785,7 @@ ${
.expanded=${this._expandedFilter === "ha-filter-devices"}
.narrow=${this.narrow}
@expanded-changed=${this._filterExpanded}
no-device-option
></ha-filter-devices>
<ha-filter-integrations
.hass=${this.hass}

View File

@@ -15,6 +15,7 @@ import {
mdiPlay,
mdiPlus,
mdiTag,
mdiTextureBox,
} from "@mdi/js";
import { differenceInDays } from "date-fns";
import { UnsubscribeFunc } from "home-assistant-js-websocket";
@@ -61,6 +62,7 @@ import "../../../components/ha-menu-item";
import "../../../components/ha-state-icon";
import "../../../components/ha-sub-menu";
import "../../../components/ha-svg-icon";
import { createAreaRegistryEntry } from "../../../data/area_registry";
import {
CategoryRegistryEntry,
createCategoryRegistryEntry,
@@ -97,6 +99,7 @@ import { haStyle } from "../../../resources/styles";
import { HomeAssistant, Route } from "../../../types";
import { documentationUrl } from "../../../util/documentation-url";
import { showToast } from "../../../util/toast";
import { showAreaRegistryDetailDialog } from "../areas/show-dialog-area-registry-detail";
import { showAssignCategoryDialog } from "../category/show-dialog-assign-category";
import { showCategoryRegistryDetailDialog } from "../category/show-dialog-category-registry-detail";
import { configSections } from "../ha-panel-config";
@@ -406,6 +409,7 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
${this.hass.localize("ui.panel.config.category.editor.add")}
</div>
</ha-menu-item>`;
const labelItems = html` ${this._labels?.map((label) => {
const color = label.color ? computeCssColor(label.color) : undefined;
const selected = this._selected.every((entityId) =>
@@ -442,9 +446,46 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
${this.hass.localize("ui.panel.config.labels.add_label")}
</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");
const labelsInOverflow =
areasInOverflow &&
(!this._sizeController.value || this._sizeController.value < 700);
const scenes = this._scenes(
this.scenes,
this._entityReg,
@@ -453,6 +494,7 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
this._labels,
this._filteredScenes
);
return html`
<hass-tabs-subpage-data-table
.hass=${this.hass}
@@ -582,9 +624,25 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
></ha-svg-icon>
</ha-assist-chip>
${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>`}`
: nothing}
${this.narrow || labelsInOverflow
${this.narrow || areasInOverflow
? html`
<ha-button-menu-new has-overflow slot="selection-bar">
${
@@ -630,8 +688,8 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
: nothing
}
${
this.narrow || this.hass.dockedSidebar === "docked"
? html` <ha-sub-menu>
this.narrow || labelsInOverflow
? html`<ha-sub-menu>
<ha-menu-item slot="item">
<div slot="headline">
${this.hass.localize(
@@ -647,6 +705,24 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
</ha-sub-menu>`
: 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>`
: nothing}
${!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) {
const entityReg = this._entityReg.find(
(reg) => reg.entity_id === scene.entity_id

View File

@@ -13,6 +13,7 @@ import {
mdiPlus,
mdiScriptText,
mdiTag,
mdiTextureBox,
mdiTransitConnection,
} from "@mdi/js";
import { differenceInDays } from "date-fns";
@@ -61,6 +62,7 @@ import "../../../components/ha-icon-overflow-menu";
import "../../../components/ha-menu-item";
import "../../../components/ha-sub-menu";
import "../../../components/ha-svg-icon";
import { createAreaRegistryEntry } from "../../../data/area_registry";
import {
CategoryRegistryEntry,
createCategoryRegistryEntry,
@@ -98,6 +100,7 @@ import { haStyle } from "../../../resources/styles";
import { HomeAssistant, Route } from "../../../types";
import { documentationUrl } from "../../../util/documentation-url";
import { showToast } from "../../../util/toast";
import { showAreaRegistryDetailDialog } from "../areas/show-dialog-area-registry-detail";
import { showNewAutomationDialog } from "../automation/show-dialog-new-automation";
import { showAssignCategoryDialog } from "../category/show-dialog-assign-category";
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")}
</div>
</ha-menu-item>`;
const labelItems = html`${this._labels?.map((label) => {
const color = label.color ? computeCssColor(label.color) : undefined;
const selected = this._selected.every((entityId) =>
@@ -454,9 +458,46 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
${this.hass.localize("ui.panel.config.labels.add_label")}
</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");
const labelsInOverflow =
areasInOverflow &&
(!this._sizeController.value || this._sizeController.value < 700);
const scripts = this._scripts(
this.scripts,
this._entityReg,
@@ -608,9 +649,25 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
></ha-svg-icon>
</ha-assist-chip>
${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>`}`
: nothing}
${this.narrow || labelsInOverflow
${this.narrow || areasInOverflow
? html`
<ha-button-menu-new has-overflow slot="selection-bar">
${
@@ -656,8 +713,8 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
: nothing
}
${
this.narrow || this.hass.dockedSidebar === "docked"
? html` <ha-sub-menu>
this.narrow || labelsInOverflow
? html`<ha-sub-menu>
<ha-menu-item slot="item">
<div slot="headline">
${this.hass.localize(
@@ -673,6 +730,24 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
</ha-sub-menu>`
: 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>`
: nothing}
${!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) {
this._activeSorting = ev.detail;
}

View File

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

View File

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

View File

@@ -3,7 +3,7 @@ import { ActionConfig } from "../../../data/lovelace/config/action";
import { LovelaceCardConfig } from "../../../data/lovelace/config/card";
import { Statistic, StatisticType } from "../../../data/recorder";
import { ForecastType } from "../../../data/weather";
import { FullCalendarView, TranslationDict } from "../../../types";
import { FullCalendarView, ThemeMode, TranslationDict } from "../../../types";
import { LovelaceCardFeatureConfig } from "../card-features/types";
import { LegacyStateFilter } from "../common/evaluate-filter";
import { Condition, LegacyCondition } from "../common/validate-condition";
@@ -314,6 +314,7 @@ export interface MapCardConfig extends LovelaceCardConfig {
hours_to_show?: number;
geo_location_sources?: string[];
dark_mode?: boolean;
theme_mode?: ThemeMode;
}
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 { customElement, property, state } from "lit/decorators";
import {
@@ -11,6 +12,7 @@ import {
string,
union,
} from "superstruct";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../common/dom/fire_event";
import { hasLocation } from "../../../../common/entity/has_location";
import "../../../../components/ha-form/ha-form";
@@ -28,6 +30,7 @@ import { processEditorEntities } from "../process-editor-entities";
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
import { EntitiesEditorEvent } from "../types";
import { configElementStyle } from "./config-elements-style";
import { LocalizeFunc } from "../../../../common/translations/localize";
export const mapEntitiesConfigStruct = union([
object({
@@ -50,30 +53,11 @@ const cardConfigStruct = assign(
hours_to_show: optional(number()),
geo_location_sources: optional(array(string())),
auto_fit: optional(boolean()),
theme_mode: optional(string()),
})
);
const SCHEMA = [
{ name: "title", selector: { text: {} } },
{
name: "",
type: "grid",
schema: [
{ name: "aspect_ratio", selector: { text: {} } },
{
name: "default_zoom",
default: DEFAULT_ZOOM,
selector: { number: { mode: "box", min: 0 } },
},
{ name: "dark_mode", selector: { boolean: {} } },
{
name: "hours_to_show",
default: DEFAULT_HOURS_TO_SHOW,
selector: { number: { mode: "box", min: 0 } },
},
],
},
] as const;
const themeModes = ["auto", "light", "dark"] as const;
@customElement("hui-map-card-editor")
export class HuiMapCardEditor extends LitElement implements LovelaceCardEditor {
@@ -83,8 +67,68 @@ export class HuiMapCardEditor extends LitElement implements LovelaceCardEditor {
@state() private _configEntities?: EntityConfig[];
private _schema = memoizeOne(
(localize: LocalizeFunc) =>
[
{ name: "title", selector: { text: {} } },
{
name: "",
type: "expandable",
iconPath: mdiPalette,
title: localize(`ui.panel.lovelace.editor.card.map.appearance`),
schema: [
{
name: "",
type: "grid",
schema: [
{ name: "aspect_ratio", selector: { text: {} } },
{
name: "default_zoom",
default: DEFAULT_ZOOM,
selector: { number: { mode: "box", min: 0 } },
},
{
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",
default: DEFAULT_HOURS_TO_SHOW,
selector: { number: { mode: "box", min: 0 } },
},
],
},
],
},
] as const
);
public setConfig(config: MapCardConfig): void {
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._configEntities = config.entities
? processEditorEntities(config.entities)
@@ -104,33 +148,32 @@ export class HuiMapCardEditor extends LitElement implements LovelaceCardEditor {
<ha-form
.hass=${this.hass}
.data=${this._config}
.schema=${SCHEMA}
.schema=${this._schema(this.hass.localize)}
.computeLabel=${this._computeLabelCallback}
@value-changed=${this._valueChanged}
></ha-form>
<div class="card-config">
<hui-entity-editor
.hass=${this.hass}
.entities=${this._configEntities}
.entityFilter=${hasLocation}
@entities-changed=${this._entitiesValueChanged}
></hui-entity-editor>
<h3>
${this.hass.localize(
"ui.panel.lovelace.editor.card.map.geo_location_sources"
)}
</h3>
<div class="geo_location_sources">
<hui-input-list-editor
.inputLabel=${this.hass.localize(
"ui.panel.lovelace.editor.card.map.source"
)}
.hass=${this.hass}
.value=${this._geo_location_sources}
@value-changed=${this._geoSourcesChanged}
></hui-input-list-editor>
</div>
</div>
<hui-entity-editor
.hass=${this.hass}
.entities=${this._configEntities}
.entityFilter=${hasLocation}
@entities-changed=${this._entitiesValueChanged}
></hui-entity-editor>
<h3>
${this.hass.localize(
"ui.panel.lovelace.editor.card.map.geo_location_sources"
)}
</h3>
<hui-input-list-editor
.inputLabel=${this.hass.localize(
"ui.panel.lovelace.editor.card.map.source"
)}
.hass=${this.hass}
.value=${this._geo_location_sources}
@value-changed=${this._geoSourcesChanged}
></hui-input-list-editor>
`;
}
@@ -170,9 +213,14 @@ export class HuiMapCardEditor extends LitElement implements LovelaceCardEditor {
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) {
case "dark_mode":
case "theme_mode":
return this.hass!.localize(
`ui.panel.lovelace.editor.card.map.${schema.name}`
);
case "default_zoom":
return this.hass!.localize(
`ui.panel.lovelace.editor.card.map.${schema.name}`
@@ -185,16 +233,7 @@ export class HuiMapCardEditor extends LitElement implements LovelaceCardEditor {
};
static get styles(): CSSResultGroup {
return [
configElementStyle,
css`
.geo_location_sources {
padding-left: 20px;
padding-inline-start: 20px;
direction: var(--direction);
}
`,
];
return [configElementStyle, css``];
}
}

View File

@@ -3941,6 +3941,7 @@
"name": "Name",
"update": "Update",
"no_devices": "No devices",
"no_device": "No device",
"enabled_label": "Enable {type}",
"enabled_cause": "The {type} is disabled by {cause}.",
"disabled_by": {
@@ -5835,7 +5836,14 @@
"name": "Map",
"geo_location_sources": "Geolocation sources",
"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",
"description": "The Map card that allows you to display entities on a map."
},

View File

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

View File

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