Merge branch 'dev' into title-alignment-option

This commit is contained in:
Douwe 2024-04-25 15:31:32 +02:00 committed by GitHub
commit 675f1e2e83
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
46 changed files with 3552 additions and 526 deletions

View File

@ -2,6 +2,7 @@ import { html, LitElement, PropertyValues, TemplateResult } from "lit";
import { customElement, query } from "lit/decorators";
import { CoverEntityFeature } from "../../../../src/data/cover";
import { LightColorMode } from "../../../../src/data/light";
import { LockEntityFeature } from "../../../../src/data/lock";
import { VacuumEntityFeature } from "../../../../src/data/vacuum";
import { getEntity } from "../../../../src/fake_data/entity";
import { provideHass } from "../../../../src/fake_data/provide_hass";
@ -20,6 +21,11 @@ const ENTITIES = [
getEntity("light", "unavailable", "unavailable", {
friendly_name: "Unavailable entity",
}),
getEntity("lock", "front_door", "locked", {
friendly_name: "Front Door Lock",
device_class: "lock",
supported_features: LockEntityFeature.OPEN,
}),
getEntity("climate", "thermostat", "heat", {
current_temperature: 73,
min_temp: 45,
@ -138,6 +144,24 @@ const CONFIGS = [
- type: "color-temp"
`,
},
{
heading: "Lock commands feature",
config: `
- type: tile
entity: lock.front_door
features:
- type: "lock-commands"
`,
},
{
heading: "Lock open door feature",
config: `
- type: tile
entity: lock.front_door
features:
- type: "lock-open-door"
`,
},
{
heading: "Vacuum commands feature",
config: `

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",
@ -111,7 +111,7 @@
"fuse.js": "7.0.0",
"google-timezones-json": "1.2.0",
"hls.js": "patch:hls.js@npm%3A1.5.7#~/.yarn/patches/hls.js-npm-1.5.7-f5bbd3d060.patch",
"home-assistant-js-websocket": "9.2.1",
"home-assistant-js-websocket": "9.3.0",
"idb-keyval": "6.2.1",
"intl-messageformat": "10.5.11",
"js-yaml": "4.1.0",
@ -169,7 +169,7 @@
"@rollup/plugin-node-resolve": "15.2.3",
"@rollup/plugin-replace": "5.0.5",
"@types/babel__plugin-transform-runtime": "7.9.5",
"@types/chromecast-caf-receiver": "6.0.13",
"@types/chromecast-caf-receiver": "6.0.14",
"@types/chromecast-caf-sender": "1.0.9",
"@types/color-name": "1.1.4",
"@types/glob": "8.1.0",
@ -207,7 +207,7 @@
"fancy-log": "2.0.0",
"fs-extra": "11.2.0",
"glob": "10.3.12",
"gulp": "5.0.0",
"gulp": "4.0.2",
"gulp-json-transform": "0.5.0",
"gulp-merge-json": "2.2.1",
"gulp-rename": "2.0.0",

View File

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

View File

@ -1,4 +1,4 @@
import { mdiArrowDown, mdiArrowUp, mdiChevronDown } from "@mdi/js";
import { mdiArrowDown, mdiArrowUp, mdiChevronUp } from "@mdi/js";
import deepClone from "deep-clone-simple";
import {
CSSResultGroup,
@ -43,6 +43,10 @@ export interface SelectionChangedEvent {
value: string[];
}
export interface CollapsedChangedEvent {
value: string[];
}
export interface SortingChangedEvent {
column: string;
direction: SortingDirection;
@ -139,6 +143,8 @@ export class HaDataTable extends LitElement {
@property() public sortDirection: SortingDirection = null;
@property({ attribute: false }) public initialCollapsedGroups?: string[];
@state() private _filterable = false;
@state() private _filter = "";
@ -245,8 +251,12 @@ export class HaDataTable extends LitElement {
).length;
}
if (properties.has("groupColumn")) {
if (!this.hasUpdated && this.initialCollapsedGroups) {
this._collapsedGroups = this.initialCollapsedGroups;
fireEvent(this, "collapsed-changed", { value: this._collapsedGroups });
} else if (properties.has("groupColumn")) {
this._collapsedGroups = [];
fireEvent(this, "collapsed-changed", { value: this._collapsedGroups });
}
if (
@ -450,6 +460,8 @@ export class HaDataTable extends LitElement {
}
return html`
<div
@mouseover=${this._setTitle}
@focus=${this._setTitle}
role=${column.main ? "rowheader" : "cell"}
class="mdc-data-table__cell ${classMap({
"mdc-data-table__cell--flex": column.type === "flex",
@ -517,11 +529,7 @@ export class HaDataTable extends LitElement {
}
if (this.appendRow || this.hasFab || this.groupColumn) {
const items = [...data];
if (this.appendRow) {
items.push({ append: true, content: this.appendRow });
}
let items = [...data];
if (this.groupColumn) {
const grouped = groupBy(items, (item) => item[this.groupColumn!]);
@ -570,7 +578,7 @@ export class HaDataTable extends LitElement {
@click=${this._collapseGroup}
>
<ha-icon-button
.path=${mdiChevronDown}
.path=${mdiChevronUp}
class=${this._collapsedGroups.includes(groupName)
? "collapsed"
: ""}
@ -587,14 +595,18 @@ export class HaDataTable extends LitElement {
}
});
this._items = groupedItems;
} else {
this._items = items;
items = groupedItems;
}
if (this.appendRow) {
items.push({ append: true, content: this.appendRow });
}
if (this.hasFab) {
this._items = [...this._items, { empty: true }];
items.push({ empty: true });
}
this._items = items;
} else {
this._items = data;
}
@ -675,6 +687,13 @@ export class HaDataTable extends LitElement {
fireEvent(this, "row-click", { id: rowId }, { bubbles: false });
};
private _setTitle(ev: Event) {
const target = ev.currentTarget as HTMLElement;
if (target.scrollWidth > target.offsetWidth) {
target.setAttribute("title", target.innerText);
}
}
private _checkedRowsChanged() {
// force scroller to update, change it's items
if (this._items.length) {
@ -714,6 +733,7 @@ export class HaDataTable extends LitElement {
} else {
this._collapsedGroups = [...this._collapsedGroups, groupName];
}
fireEvent(this, "collapsed-changed", { value: this._collapsedGroups });
};
static get styles(): CSSResultGroup {
@ -1087,5 +1107,6 @@ declare global {
"selection-changed": SelectionChangedEvent;
"row-click": RowClickedEvent;
"sorting-changed": SortingChangedEvent;
"collapsed-changed": CollapsedChangedEvent;
}
}

View File

@ -69,7 +69,7 @@ export class HaFilterDevices extends LitElement {
@value-changed=${this._handleSearchChange}
>
</search-input-outlined>
<mwc-list class="ha-scrollbar">
<mwc-list class="ha-scrollbar" multi>
<lit-virtualizer
.items=${this._devices(
this.hass.devices,
@ -94,7 +94,7 @@ export class HaFilterDevices extends LitElement {
? nothing
: html`<ha-check-list-item
.value=${device.id}
.selected=${this.value?.includes(device.id)}
.selected=${this.value?.includes(device.id) ?? false}
>
${computeDeviceName(device, this.hass)}
</ha-check-list-item>`;

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}
>
</search-input-outlined>
<mwc-list class="ha-scrollbar">
<mwc-list class="ha-scrollbar" multi>
<lit-virtualizer
.items=${this._entities(
this.hass.states,
@ -108,7 +108,7 @@ export class HaFilterEntities extends LitElement {
? nothing
: html`<ha-check-list-item
.value=${entity.entity_id}
.selected=${this.value?.includes(entity.entity_id)}
.selected=${this.value?.includes(entity.entity_id) ?? false}
graphic="icon"
>
<ha-state-icon

View File

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

View File

@ -62,8 +62,8 @@ export class HaFilterStates extends LitElement {
(item) =>
html`<ha-check-list-item
.value=${item.value}
.selected=${this.value?.includes(item.value)}
.graphic=${hasIcon ? "icon" : undefined}
.selected=${this.value?.includes(item.value) ?? false}
.graphic=${hasIcon ? "icon" : null}
>
${item.icon
? html`<ha-icon

View File

@ -71,6 +71,10 @@ export const computeInitialHaFormData = (
if (selector.country?.countries?.length) {
data[field.name] = selector.country.countries[0];
}
} else if ("language" in selector) {
if (selector.language?.languages?.length) {
data[field.name] = selector.language.languages[0];
}
} else if ("duration" in selector) {
data[field.name] = {
hours: 0,
@ -93,7 +97,9 @@ export const computeInitialHaFormData = (
) {
data[field.name] = {};
} else {
throw new Error("Selector not supported in initial form data");
throw new Error(
`Selector ${Object.keys(selector)[0]} not supported in initial form data`
);
}
}
});

View File

@ -19,7 +19,7 @@ import { customElement, property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../common/dom/fire_event";
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-map";
import type { HaMap } from "./ha-map";
@ -61,7 +61,8 @@ export class HaLocationsEditor extends LitElement {
@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>;
@ -133,7 +134,7 @@ export class HaLocationsEditor extends LitElement {
.layers=${this._getLayers(this._circles, this._locationMarkers)}
.zoom=${this.zoom}
.autoFit=${this.autoFit}
?darkMode=${this.darkMode}
.themeMode=${this.themeMode}
></ha-map>
${this.helper
? html`<ha-input-helper-text>${this.helper}</ha-input-helper-text>`

View File

@ -1,32 +1,32 @@
import { isToday } from "date-fns";
import type {
Circle,
CircleMarker,
LatLngTuple,
LatLngExpression,
LatLngTuple,
Layer,
Map,
Marker,
Polyline,
} from "leaflet";
import { isToday } from "date-fns";
import { css, CSSResultGroup, PropertyValues, ReactiveElement } from "lit";
import { CSSResultGroup, PropertyValues, ReactiveElement, css } from "lit";
import { customElement, property, state } from "lit/decorators";
import { formatDateTime } from "../../common/datetime/format_date_time";
import {
formatTimeWeekday,
formatTimeWithSeconds,
} from "../../common/datetime/format_time";
import {
LeafletModuleType,
setupLeafletMap,
} 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 { computeStateName } from "../../common/entity/compute_state_name";
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-entity-marker";
import { isTouch } from "../../util/is_touch";
const getEntityId = (entity: string | HaMapEntity): string =>
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 darkMode = false;
@property({ attribute: "theme-mode", type: String })
public themeMode: ThemeMode = "auto";
@property({ type: Number }) public zoom = 14;
@ -154,7 +155,7 @@ export class HaMap extends ReactiveElement {
}
if (
!changedProps.has("darkMode") &&
!changedProps.has("themeMode") &&
(!changedProps.has("hass") ||
(oldHass && oldHass.themes?.darkMode === this.hass.themes?.darkMode))
) {
@ -163,12 +164,18 @@ export class HaMap extends ReactiveElement {
this._updateMapStyle();
}
private get _darkMode() {
return (
this.themeMode === "dark" ||
(this.themeMode === "auto" && Boolean(this.hass.themes.darkMode))
);
}
private _updateMapStyle(): void {
const darkMode = this.darkMode || (this.hass.themes.darkMode ?? false);
const forcedDark = this.darkMode;
const map = this.renderRoot.querySelector("#map");
map!.classList.toggle("dark", darkMode);
map!.classList.toggle("forced-dark", forcedDark);
map!.classList.toggle("dark", this._darkMode);
map!.classList.toggle("forced-dark", this.themeMode === "dark");
map!.classList.toggle("forced-light", this.themeMode === "light");
}
private async _loadMap(): Promise<void> {
@ -398,8 +405,7 @@ export class HaMap extends ReactiveElement {
"--dark-primary-color"
);
const className =
this.darkMode || this.hass.themes.darkMode ? "dark" : "light";
const className = this._darkMode ? "dark" : "light";
for (const entity of this.entities) {
const stateObj = hass.states[getEntityId(entity)];
@ -543,27 +549,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

@ -1,6 +1,8 @@
import { EntityFilter } from "../common/entity/entity_filter";
import { HomeAssistant } from "../types";
type StrictConnectionMode = "disabled" | "guard_page" | "drop_connection";
interface CloudStatusNotLoggedIn {
logged_in: false;
cloud: "disconnected" | "connecting" | "connected";
@ -19,6 +21,7 @@ export interface CloudPreferences {
alexa_enabled: boolean;
remote_enabled: boolean;
remote_allow_remote_enable: boolean;
strict_connection: StrictConnectionMode;
google_secure_devices_pin: string | undefined;
cloudhooks: { [webhookId: string]: CloudWebhook };
alexa_report_state: boolean;
@ -141,6 +144,7 @@ export const updateCloudPref = (
google_secure_devices_pin?: CloudPreferences["google_secure_devices_pin"];
tts_default_voice?: CloudPreferences["tts_default_voice"];
remote_allow_remote_enable?: CloudPreferences["remote_allow_remote_enable"];
strict_connection?: CloudPreferences["strict_connection"];
}
) =>
hass.callWS({

View File

@ -5,9 +5,7 @@ import {
import { getExtendedEntityRegistryEntry } from "./entity_registry";
import { showEnterCodeDialog } from "../dialogs/enter-code/show-enter-code-dialog";
import { HomeAssistant } from "../types";
export const FORMAT_TEXT = "text";
export const FORMAT_NUMBER = "number";
import { UNAVAILABLE } from "./entity";
export const enum LockEntityFeature {
OPEN = 1,
@ -24,6 +22,33 @@ export interface LockEntity extends HassEntityBase {
type ProtectedLockService = "lock" | "unlock" | "open";
export function isLocked(stateObj: LockEntity) {
return stateObj.state === "locked";
}
export function isUnlocking(stateObj: LockEntity) {
return stateObj.state === "unlocking";
}
export function isLocking(stateObj: LockEntity) {
return stateObj.state === "locking";
}
export function isJammed(stateObj: LockEntity) {
return stateObj.state === "jammed";
}
export function isAvailable(stateObj: LockEntity) {
if (stateObj.state === UNAVAILABLE) {
return false;
}
const assumedState = stateObj.attributes.assumed_state === true;
return (
assumedState ||
(!isLocking(stateObj) && !isUnlocking(stateObj) && !isJammed(stateObj))
);
}
export const callProtectedLockService = async (
element: HTMLElement,
hass: HomeAssistant,

View File

@ -97,3 +97,14 @@ export const getHassTranslations = async (
});
return result.resources;
};
export const getHassTranslationsPre109 = async (
hass: HomeAssistant,
language: string
): Promise<Record<string, unknown>> => {
const result = await hass.callWS<{ resources: Record<string, unknown> }>({
type: "frontend/get_translations",
language,
});
return result.resources;
};

View File

@ -9,11 +9,12 @@ import "../../../components/ha-control-button";
import "../../../components/ha-control-button-group";
import "../../../components/ha-outlined-icon-button";
import "../../../components/ha-state-icon";
import { UNAVAILABLE } from "../../../data/entity";
import {
LockEntity,
LockEntityFeature,
callProtectedLockService,
isAvailable,
isJammed,
} from "../../../data/lock";
import "../../../state-control/lock/ha-state-control-lock-toggle";
import type { HomeAssistant } from "../../../types";
@ -85,15 +86,13 @@ class MoreInfoLock extends LitElement {
"--state-color": color,
};
const isJammed = this.stateObj.state === "jammed";
return html`
<ha-more-info-state-header
.hass=${this.hass}
.stateObj=${this.stateObj}
></ha-more-info-state-header>
<div class="controls" style=${styleMap(style)}>
${this.stateObj.state === "jammed"
${isJammed(this.stateObj)
? html`
<div class="status">
<span></span>
@ -125,7 +124,7 @@ class MoreInfoLock extends LitElement {
`
: html`
<ha-control-button
.disabled=${this.stateObj.state === UNAVAILABLE}
.disabled=${!isAvailable(this.stateObj)}
class="open-button ${this._buttonState}"
@click=${this._open}
>
@ -139,7 +138,7 @@ class MoreInfoLock extends LitElement {
: nothing}
</div>
<div>
${isJammed
${isJammed(this.stateObj)
? html`
<ha-control-button-group class="jammed">
<ha-control-button @click=${this._unlock}>

View File

@ -55,6 +55,8 @@ export class HaTabsSubpageDataTable extends LitElement {
@property({ type: Boolean, attribute: "main-page" }) public mainPage = false;
@property({ attribute: false }) public initialCollapsedGroups: string[] = [];
/**
* Object with the columns.
* @type {Object}
@ -425,6 +427,7 @@ export class HaTabsSubpageDataTable extends LitElement {
.sortDirection=${this._sortDirection}
.groupColumn=${this._groupColumn}
.groupOrder=${this.groupOrder}
.initialCollapsedGroups=${this.initialCollapsedGroups}
>
${!this.narrow
? html`

View File

@ -41,7 +41,7 @@ import type { HomeAssistant } from "../types";
import { onBoardingStyles } from "./styles";
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";
@customElement("onboarding-location")
@ -199,7 +199,7 @@ class OnboardingLocation extends LitElement {
this._highlightedMarker
)}
zoom="14"
.darkMode=${mql.matches}
.themeMode=${darkMql.matches ? "dark" : "light"}
.disabled=${this._working}
@location-updated=${this._locationChanged}
@marker-clicked=${this._markerClicked}

View File

@ -15,6 +15,7 @@ import {
mdiPlus,
mdiRobotHappy,
mdiTag,
mdiTextureBox,
mdiToggleSwitch,
mdiToggleSwitchOffOutline,
mdiTransitConnection,
@ -37,15 +38,21 @@ import { computeCssColor } from "../../../common/color/compute-color";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { formatShortDateTime } from "../../../common/datetime/format_date_time";
import { relativeTime } from "../../../common/datetime/relative_time";
import { storage } from "../../../common/decorators/storage";
import { HASSDomEvent, fireEvent } from "../../../common/dom/fire_event";
import { computeStateName } from "../../../common/entity/compute_state_name";
import { navigate } from "../../../common/navigate";
import { LocalizeFunc } from "../../../common/translations/localize";
import {
hasRejectedItems,
rejectedItems,
} from "../../../common/util/promise-all-settled-results";
import "../../../components/chips/ha-assist-chip";
import type {
DataTableColumnContainer,
RowClickedEvent,
SelectionChangedEvent,
SortingChangedEvent,
} from "../../../components/data-table/ha-data-table";
import "../../../components/data-table/ha-data-table-labels";
import "../../../components/entity/ha-entity-toggle";
@ -63,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,
@ -100,15 +108,12 @@ 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";
import { showLabelDetailDialog } from "../labels/show-dialog-label-detail";
import { showNewAutomationDialog } from "./show-dialog-new-automation";
import {
hasRejectedItems,
rejectedItems,
} from "../../../common/util/promise-all-settled-results";
type AutomationItem = AutomationEntity & {
name: string;
@ -156,6 +161,19 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
@state() private _overflowAutomation?: AutomationItem;
@storage({ key: "automation-table-sort", state: false, subscribe: false })
private _activeSorting?: SortingChangedEvent;
@storage({ key: "automation-table-grouping", state: false, subscribe: false })
private _activeGrouping?: string;
@storage({
key: "automation-table-collapsed",
state: false,
subscribe: false,
})
private _activeCollapsed?: string;
@query("#overflow-menu") private _overflowMenu!: HaMenu;
private _sizeController = new ResizeController(this, {
@ -388,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) =>
@ -425,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,
@ -470,7 +524,12 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
this.hass.localize,
this.hass.locale
)}
initialGroupColumn="category"
.initialGroupColumn=${this._activeGrouping || "category"}
.initialCollapsedGroups=${this._activeCollapsed}
.initialSorting=${this._activeSorting}
@sorting-changed=${this._handleSortingChanged}
@grouping-changed=${this._handleGroupingChanged}
@collapsed-changed=${this._handleCollapseChanged}
.data=${automations}
.empty=${!this.automations.length}
@row-click=${this._handleRowClicked}
@ -578,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
}
@ -642,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">
@ -1171,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) => {
@ -1238,6 +1371,18 @@ ${rejected
});
}
private _handleSortingChanged(ev: CustomEvent) {
this._activeSorting = ev.detail;
}
private _handleGroupingChanged(ev: CustomEvent) {
this._activeGrouping = ev.detail.value;
}
private _handleCollapseChanged(ev: CustomEvent) {
this._activeCollapsed = ev.detail.value;
}
static get styles(): CSSResultGroup {
return [
haStyle,

View File

@ -97,7 +97,7 @@ export class HaManualAutomationEditor extends LitElement {
<ha-automation-trigger
role="region"
aria-labelledby="triggers-heading"
.triggers=${this.config.trigger}
.triggers=${this.config.trigger || []}
.path=${["trigger"]}
@value-changed=${this._triggerChanged}
@item-moved=${this._itemMoved}

View File

@ -24,6 +24,7 @@ import { extractSearchParam } from "../../../common/url/search-params";
import {
DataTableColumnContainer,
RowClickedEvent,
SortingChangedEvent,
} from "../../../components/data-table/ha-data-table";
import "../../../components/entity/ha-entity-toggle";
import "../../../components/ha-button";
@ -54,6 +55,7 @@ import { documentationUrl } from "../../../util/documentation-url";
import { showToast } from "../../../util/toast";
import { configSections } from "../ha-panel-config";
import { showAddBlueprintDialog } from "./show-dialog-import-blueprint";
import { storage } from "../../../common/decorators/storage";
type BlueprintMetaDataPath = BlueprintMetaData & {
path: string;
@ -92,8 +94,24 @@ class HaBlueprintOverview extends LitElement {
Blueprints
>;
@storage({ key: "blueprint-table-sort", state: false, subscribe: false })
private _activeSorting?: SortingChangedEvent;
@storage({ key: "blueprint-table-grouping", state: false, subscribe: false })
private _activeGrouping?: string;
@storage({
key: "blueprint-table-collapsed",
state: false,
subscribe: false,
})
private _activeCollapsed?: string;
private _processedBlueprints = memoizeOne(
(blueprints: Record<string, Blueprints>): BlueprintMetaDataPath[] => {
(
blueprints: Record<string, Blueprints>,
localize: LocalizeFunc
): BlueprintMetaDataPath[] => {
const result: any[] = [];
Object.entries(blueprints).forEach(([type, typeBlueprints]) =>
Object.entries(typeBlueprints).forEach(([path, blueprint]) => {
@ -101,6 +119,9 @@ class HaBlueprintOverview extends LitElement {
result.push({
name: blueprint.error,
type,
translated_type: localize(
`ui.panel.config.blueprint.overview.types.${type as "automation" | "script"}`
),
error: true,
path,
fullpath: `${type}/${path}`,
@ -109,6 +130,9 @@ class HaBlueprintOverview extends LitElement {
result.push({
...blueprint.metadata,
type,
translated_type: localize(
`ui.panel.config.blueprint.overview.types.${type as "automation" | "script"}`
),
error: false,
path,
fullpath: `${type}/${path}`,
@ -140,14 +164,11 @@ class HaBlueprintOverview extends LitElement {
`
: undefined,
},
type: {
translated_type: {
title: localize("ui.panel.config.blueprint.overview.headers.type"),
template: (blueprint) =>
html`${this.hass.localize(
`ui.panel.config.blueprint.overview.types.${blueprint.type}`
)}`,
sortable: true,
filterable: true,
groupable: true,
hidden: narrow,
direction: "asc",
width: "10%",
@ -256,7 +277,7 @@ class HaBlueprintOverview extends LitElement {
this.hass.language,
this.hass.localize
)}
.data=${this._processedBlueprints(this.blueprints)}
.data=${this._processedBlueprints(this.blueprints, this.hass.localize)}
id="fullpath"
.noDataText=${this.hass.localize(
"ui.panel.config.blueprint.overview.no_blueprints"
@ -281,6 +302,12 @@ class HaBlueprintOverview extends LitElement {
>
</a>
</div>`}
.initialGroupColumn=${this._activeGrouping}
.initialCollapsedGroups=${this._activeCollapsed}
.initialSorting=${this._activeSorting}
@sorting-changed=${this._handleSortingChanged}
@grouping-changed=${this._handleGroupingChanged}
@collapsed-changed=${this._handleCollapseChanged}
>
<ha-icon-button
slot="toolbar-icon"
@ -341,9 +368,10 @@ class HaBlueprintOverview extends LitElement {
}
private _handleRowClicked(ev: HASSDomEvent<RowClickedEvent>) {
const blueprint = this._processedBlueprints(this.blueprints).find(
(b) => b.fullpath === ev.detail.id
)!;
const blueprint = this._processedBlueprints(
this.blueprints,
this.hass.localize
).find((b) => b.fullpath === ev.detail.id)!;
if (blueprint.error) {
showAlertDialog(this, {
title: this.hass.localize("ui.panel.config.blueprint.overview.error", {
@ -502,6 +530,18 @@ class HaBlueprintOverview extends LitElement {
fireEvent(this, "reload-blueprints");
};
private _handleSortingChanged(ev: CustomEvent) {
this._activeSorting = ev.detail;
}
private _handleGroupingChanged(ev: CustomEvent) {
this._activeGrouping = ev.detail.value;
}
private _handleCollapseChanged(ev: CustomEvent) {
this._activeCollapsed = ev.detail.value;
}
static get styles(): CSSResultGroup {
return haStyle;
}

View File

@ -21,6 +21,7 @@ import {
import type { HomeAssistant } from "../../../../types";
import { showToast } from "../../../../util/toast";
import { showCloudCertificateDialog } from "../dialog-cloud-certificate/show-dialog-cloud-certificate";
import { showAlertDialog } from "../../../lovelace/custom-card-helpers";
@customElement("cloud-remote-pref")
export class CloudRemotePref extends LitElement {
@ -33,7 +34,7 @@ export class CloudRemotePref extends LitElement {
return nothing;
}
const { remote_enabled, remote_allow_remote_enable } =
const { remote_enabled, remote_allow_remote_enable, strict_connection } =
this.cloudStatus.prefs;
const {
@ -153,6 +154,61 @@ export class CloudRemotePref extends LitElement {
@change=${this._toggleAllowRemoteEnabledChanged}
></ha-switch>
</ha-settings-row>
<ha-settings-row>
<span slot="heading"
>${this.hass.localize(
"ui.panel.config.cloud.account.remote.strict_connection"
)}</span
>
<span slot="description"
>${this.hass.localize(
"ui.panel.config.cloud.account.remote.strict_connection_secondary"
)}</span
>
<ha-select
.label=${this.hass.localize(
"ui.panel.config.cloud.account.remote.strict_connection_mode"
)}
@selected=${this._setStrictConnectionMode}
naturalMenuWidth
.value=${strict_connection}
>
<ha-list-item value="disabled">
${this.hass.localize(
"ui.panel.config.cloud.account.remote.strict_connection_modes.disabled"
)}
</ha-list-item>
<ha-list-item value="guard_page">
${this.hass.localize(
"ui.panel.config.cloud.account.remote.strict_connection_modes.guard_page"
)}
</ha-list-item>
<ha-list-item value="drop_connection">
${this.hass.localize(
"ui.panel.config.cloud.account.remote.strict_connection_modes.drop_connection"
)}
</ha-list-item>
</ha-select>
</ha-settings-row>
${strict_connection !== "disabled"
? html` <ha-settings-row>
<span slot="heading"
>${this.hass.localize(
"ui.panel.config.cloud.account.remote.strict_connection_link"
)}</span
>
<span slot="description"
>${this.hass.localize(
"ui.panel.config.cloud.account.remote.strict_connection_link_secondary"
)}</span
>
<ha-button @click=${this._createLoginUrl}
>${this.hass.localize(
"ui.panel.config.cloud.account.remote.strict_connection_create_link"
)}</ha-button
>
</ha-settings-row>`
: nothing}
<ha-settings-row>
<span slot="heading"
>${this.hass.localize(
@ -223,6 +279,18 @@ export class CloudRemotePref extends LitElement {
}
}
private async _setStrictConnectionMode(ev) {
const mode = ev.target.value;
try {
await updateCloudPref(this.hass, {
strict_connection: mode,
});
fireEvent(this, "ha-refresh-cloud-status");
} catch (err: any) {
alert(err.message);
}
}
private async _copyURL(ev): Promise<void> {
const url = ev.currentTarget.url;
await copyToClipboard(url);
@ -231,6 +299,40 @@ export class CloudRemotePref extends LitElement {
});
}
private async _createLoginUrl() {
try {
const result = await this.hass.callService(
"cloud",
"create_temporary_strict_connection_url",
undefined,
undefined,
false,
true
);
showAlertDialog(this, {
title: this.hass.localize(
"ui.panel.config.cloud.account.remote.strict_connection_link"
),
text: html`${this.hass.localize(
"ui.panel.config.cloud.account.remote.strict_connection_link_created_message"
)}
<pre>${result.response.url}</pre>
<ha-button
.url=${result.response.url}
@click=${this._copyURL}
unelevated
>
<ha-svg-icon slot="icon" .path=${mdiContentCopy}></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.cloud.account.remote.strict_connection_copy_link"
)}
</ha-button>`,
});
} catch (err: any) {
showAlertDialog(this, { text: err.message });
}
}
static get styles(): CSSResultGroup {
return css`
.preparing {

View File

@ -134,6 +134,9 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
@storage({ key: "devices-table-grouping", state: false, subscribe: false })
private _activeGrouping?: string;
@storage({ key: "devices-table-collapsed", state: false, subscribe: false })
private _activeCollapsed?: string;
private _sizeController = new ResizeController(this, {
callback: (entries) => entries[0]?.contentRect.width,
});
@ -671,11 +674,13 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
)
).length}
.initialGroupColumn=${this._activeGrouping}
.initialCollapsedGroups=${this._activeCollapsed}
.initialSorting=${this._activeSorting}
@clear-filter=${this._clearFilter}
@search-changed=${this._handleSearchChange}
@sorting-changed=${this._handleSortingChanged}
@grouping-changed=${this._handleGroupingChanged}
@collapsed-changed=${this._handleCollapseChanged}
@row-click=${this._handleRowClicked}
clickable
hasFab
@ -1005,6 +1010,10 @@ ${rejected
this._activeGrouping = ev.detail.value;
}
private _handleCollapseChanged(ev: CustomEvent) {
this._activeCollapsed = ev.detail.value;
}
static get styles(): CSSResultGroup {
return [
css`

View File

@ -29,6 +29,7 @@ import { ifDefined } from "lit/directives/if-defined";
import { styleMap } from "lit/directives/style-map";
import memoize from "memoize-one";
import { computeCssColor } from "../../../common/color/compute-color";
import { storage } from "../../../common/decorators/storage";
import type { HASSDomEvent } from "../../../common/dom/fire_event";
import { computeDomain } from "../../../common/entity/compute_domain";
import { computeStateName } from "../../../common/entity/compute_state_name";
@ -37,16 +38,22 @@ import {
protocolIntegrationPicked,
} from "../../../common/integrations/protocolIntegrationPicked";
import { LocalizeFunc } from "../../../common/translations/localize";
import {
hasRejectedItems,
rejectedItems,
} from "../../../common/util/promise-all-settled-results";
import type {
DataTableColumnContainer,
RowClickedEvent,
SelectionChangedEvent,
SortingChangedEvent,
} from "../../../components/data-table/ha-data-table";
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-domains";
import "../../../components/ha-filter-floor-areas";
import "../../../components/ha-filter-integrations";
import "../../../components/ha-filter-labels";
@ -66,6 +73,11 @@ import {
removeEntityRegistryEntry,
updateEntityRegistryEntry,
} from "../../../data/entity_registry";
import {
EntitySources,
fetchEntitySourcesWithCache,
} from "../../../data/entity_sources";
import { domainToName } from "../../../data/integration";
import {
LabelRegistryEntry,
createLabelRegistryEntry,
@ -86,15 +98,6 @@ 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 {
EntitySources,
fetchEntitySourcesWithCache,
} from "../../../data/entity_sources";
import {
hasRejectedItems,
rejectedItems,
} from "../../../common/util/promise-all-settled-results";
import { domainToName } from "../../../data/integration";
export interface StateEntity
extends Omit<EntityRegistryEntry, "id" | "unique_id"> {
@ -151,6 +154,19 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
@state() private _entitySources?: EntitySources;
@storage({ key: "entities-table-sort", state: false, subscribe: false })
private _activeSorting?: SortingChangedEvent;
@storage({ key: "entities-table-grouping", state: false, subscribe: false })
private _activeGrouping?: string;
@storage({
key: "entities-table-collapsed",
state: false,
subscribe: false,
})
private _activeCollapsed?: string;
@query("hass-tabs-subpage-data-table", true)
private _dataTable!: HaTabsSubpageDataTable;
@ -265,7 +281,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
},
domain: {
title: localize("ui.panel.config.entities.picker.headers.domain"),
sortable: true,
sortable: false,
hidden: true,
filterable: true,
groupable: true,
@ -428,6 +444,10 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
entryIds.includes(entity.config_entry_id))
);
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) {
filteredEntities = filteredEntities.filter((entity) =>
entity.labels.some((lbl) => filter.value!.includes(lbl))
@ -603,6 +623,12 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
.filter=${this._filter}
selectable
.selected=${this._selected.length}
.initialGroupColumn=${this._activeGrouping}
.initialCollapsedGroups=${this._activeCollapsed}
.initialSorting=${this._activeSorting}
@sorting-changed=${this._handleSortingChanged}
@grouping-changed=${this._handleGroupingChanged}
@collapsed-changed=${this._handleCollapseChanged}
@selection-changed=${this._handleSelectionChanged}
clickable
@clear-filter=${this._clearFilter}
@ -761,6 +787,15 @@ ${
.narrow=${this.narrow}
@expanded-changed=${this._filterExpanded}
></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
.hass=${this.hass}
.value=${this._filters["ha-filter-integrations"]?.value}
@ -1205,6 +1240,18 @@ ${rejected
});
}
private _handleSortingChanged(ev: CustomEvent) {
this._activeSorting = ev.detail;
}
private _handleGroupingChanged(ev: CustomEvent) {
this._activeGrouping = ev.detail.value;
}
private _handleCollapseChanged(ev: CustomEvent) {
this._activeCollapsed = ev.detail.value;
}
static get styles(): CSSResultGroup {
return [
haStyle,

View File

@ -24,6 +24,7 @@ import {
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { computeCssColor } from "../../../common/color/compute-color";
import { storage } from "../../../common/decorators/storage";
import { HASSDomEvent } from "../../../common/dom/fire_event";
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
import { navigate } from "../../../common/navigate";
@ -40,6 +41,7 @@ import {
DataTableColumnContainer,
RowClickedEvent,
SelectionChangedEvent,
SortingChangedEvent,
} from "../../../components/data-table/ha-data-table";
import "../../../components/data-table/ha-data-table-labels";
import "../../../components/ha-fab";
@ -139,6 +141,19 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
@property({ attribute: false }) public route!: Route;
@storage({ key: "helpers-table-sort", state: false, subscribe: false })
private _activeSorting?: SortingChangedEvent;
@storage({ key: "helpers-table-grouping", state: false, subscribe: false })
private _activeGrouping?: string;
@storage({
key: "helpers-table-collapsed",
state: false,
subscribe: false,
})
private _activeCollapsed?: string;
@state() private _stateItems: HassEntity[] = [];
@state() private _entityEntries?: Record<string, EntityRegistryEntry>;
@ -525,7 +540,12 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
).length}
.columns=${this._columns(this.narrow, this.hass.localize)}
.data=${helpers}
initialGroupColumn="category"
.initialGroupColumn=${this._activeGrouping || "category"}
.initialCollapsedGroups=${this._activeCollapsed}
.initialSorting=${this._activeSorting}
@sorting-changed=${this._handleSortingChanged}
@grouping-changed=${this._handleGroupingChanged}
@collapsed-changed=${this._handleCollapseChanged}
.activeFilters=${this._activeFilters}
@clear-filter=${this._clearFilter}
@row-click=${this._openEditDialog}
@ -1020,6 +1040,18 @@ ${rejected
});
}
private _handleSortingChanged(ev: CustomEvent) {
this._activeSorting = ev.detail;
}
private _handleGroupingChanged(ev: CustomEvent) {
this._activeGrouping = ev.detail.value;
}
private _handleCollapseChanged(ev: CustomEvent) {
this._activeCollapsed = ev.detail.value;
}
static get styles(): CSSResultGroup {
return [
haStyle,

View File

@ -10,15 +10,17 @@ import { LitElement, PropertyValues, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { computeCssColor } from "../../../common/color/compute-color";
import { navigate } from "../../../common/navigate";
import { LocalizeFunc } from "../../../common/translations/localize";
import {
DataTableColumnContainer,
RowClickedEvent,
SortingChangedEvent,
} from "../../../components/data-table/ha-data-table";
import "../../../components/ha-fab";
import "../../../components/ha-icon-button";
import "../../../components/ha-relative-time";
import "../../../components/ha-icon-overflow-menu";
import "../../../components/ha-relative-time";
import {
LabelRegistryEntry,
LabelRegistryEntryMutableParams,
@ -35,7 +37,7 @@ import "../../../layouts/hass-tabs-subpage-data-table";
import { HomeAssistant, Route } from "../../../types";
import { configSections } from "../ha-panel-config";
import { showLabelDetailDialog } from "./show-dialog-label-detail";
import { navigate } from "../../../common/navigate";
import { storage } from "../../../common/decorators/storage";
@customElement("ha-config-labels")
export class HaConfigLabels extends LitElement {
@ -49,6 +51,13 @@ export class HaConfigLabels extends LitElement {
@state() private _labels: LabelRegistryEntry[] = [];
@storage({
key: "labels-table-sort",
state: false,
subscribe: false,
})
private _activeSorting?: SortingChangedEvent;
private _columns = memoizeOne((localize: LocalizeFunc) => {
const columns: DataTableColumnContainer<LabelRegistryEntry> = {
icon: {
@ -149,6 +158,8 @@ export class HaConfigLabels extends LitElement {
.data=${this._data(this._labels)}
.noDataText=${this.hass.localize("ui.panel.config.labels.no_labels")}
hasFab
.initialSorting=${this._activeSorting}
@sorting-changed=${this._handleSortingChanged}
@row-click=${this._editLabel}
clickable
id="label_id"
@ -268,6 +279,10 @@ export class HaConfigLabels extends LitElement {
`/config/automation/dashboard?historyBack=1&label=${label.label_id}`
);
}
private _handleSortingChanged(ev: CustomEvent) {
this._activeSorting = ev.detail;
}
}
declare global {

View File

@ -16,6 +16,7 @@ import { stringCompare } from "../../../../common/string/compare";
import {
DataTableColumnContainer,
RowClickedEvent,
SortingChangedEvent,
} from "../../../../components/data-table/ha-data-table";
import "../../../../components/ha-clickable-list-item";
import "../../../../components/ha-fab";
@ -46,6 +47,7 @@ import { showNewDashboardDialog } from "../../dashboard/show-dialog-new-dashboar
import { lovelaceTabs } from "../ha-config-lovelace";
import { showDashboardConfigureStrategyDialog } from "./show-dialog-lovelace-dashboard-configure-strategy";
import { showDashboardDetailDialog } from "./show-dialog-lovelace-dashboard-detail";
import { storage } from "../../../../common/decorators/storage";
type DataTableItem = Pick<
LovelaceDashboard,
@ -68,6 +70,13 @@ export class HaConfigLovelaceDashboards extends LitElement {
@state() private _dashboards: LovelaceDashboard[] = [];
@storage({
key: "lovelace-dashboards-table-sort",
state: false,
subscribe: false,
})
private _activeSorting?: SortingChangedEvent;
public willUpdate() {
if (!this.hasUpdated) {
this.hass.loadFragmentTranslation("lovelace");
@ -293,6 +302,8 @@ export class HaConfigLovelaceDashboards extends LitElement {
this.hass.localize
)}
.data=${this._getItems(this._dashboards)}
.initialSorting=${this._activeSorting}
@sorting-changed=${this._handleSortingChanged}
@row-click=${this._editDashboard}
id="url_path"
hasFab
@ -440,6 +451,10 @@ export class HaConfigLovelaceDashboards extends LitElement {
},
});
}
private _handleSortingChanged(ev: CustomEvent) {
this._activeSorting = ev.detail;
}
}
declare global {

View File

@ -10,9 +10,11 @@ import {
import { customElement, property, state } from "lit/decorators";
import memoize from "memoize-one";
import { stringCompare } from "../../../../common/string/compare";
import { LocalizeFunc } from "../../../../common/translations/localize";
import {
DataTableColumnContainer,
RowClickedEvent,
SortingChangedEvent,
} from "../../../../components/data-table/ha-data-table";
import "../../../../components/ha-card";
import "../../../../components/ha-fab";
@ -33,10 +35,10 @@ import "../../../../layouts/hass-subpage";
import "../../../../layouts/hass-tabs-subpage-data-table";
import { haStyle } from "../../../../resources/styles";
import { HomeAssistant, Route } from "../../../../types";
import { LocalizeFunc } from "../../../../common/translations/localize";
import { loadLovelaceResources } from "../../../lovelace/common/load-resources";
import { lovelaceResourcesTabs } from "../ha-config-lovelace";
import { showResourceDetailDialog } from "./show-dialog-lovelace-resource-detail";
import { storage } from "../../../../common/decorators/storage";
@customElement("ha-config-lovelace-resources")
export class HaConfigLovelaceRescources extends LitElement {
@ -50,6 +52,13 @@ export class HaConfigLovelaceRescources extends LitElement {
@state() private _resources: LovelaceResource[] = [];
@storage({
key: "lovelace-resources-table-sort",
state: false,
subscribe: false,
})
private _activeSorting?: SortingChangedEvent;
private _columns = memoize(
(
_language,
@ -127,6 +136,8 @@ export class HaConfigLovelaceRescources extends LitElement {
.noDataText=${this.hass.localize(
"ui.panel.config.lovelace.resources.picker.no_resources"
)}
.initialSorting=${this._activeSorting}
@sorting-changed=${this._handleSortingChanged}
@row-click=${this._editResource}
hasFab
clickable
@ -237,6 +248,10 @@ export class HaConfigLovelaceRescources extends LitElement {
});
}
private _handleSortingChanged(ev: CustomEvent) {
this._activeSorting = ev.detail;
}
static get styles(): CSSResultGroup {
return [
haStyle,

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";
@ -32,14 +33,20 @@ import memoizeOne from "memoize-one";
import { computeCssColor } from "../../../common/color/compute-color";
import { formatShortDateTime } from "../../../common/datetime/format_date_time";
import { relativeTime } from "../../../common/datetime/relative_time";
import { storage } from "../../../common/decorators/storage";
import { HASSDomEvent, fireEvent } from "../../../common/dom/fire_event";
import { computeStateName } from "../../../common/entity/compute_state_name";
import { navigate } from "../../../common/navigate";
import { LocalizeFunc } from "../../../common/translations/localize";
import {
hasRejectedItems,
rejectedItems,
} from "../../../common/util/promise-all-settled-results";
import {
DataTableColumnContainer,
RowClickedEvent,
SelectionChangedEvent,
SortingChangedEvent,
} from "../../../components/data-table/ha-data-table";
import "../../../components/data-table/ha-data-table-labels";
import "../../../components/ha-button";
@ -55,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,
@ -91,14 +99,11 @@ 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";
import { showLabelDetailDialog } from "../labels/show-dialog-label-detail";
import {
hasRejectedItems,
rejectedItems,
} from "../../../common/util/promise-all-settled-results";
type SceneItem = SceneEntity & {
name: string;
@ -144,6 +149,19 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
@consume({ context: fullEntitiesContext, subscribe: true })
_entityReg!: EntityRegistryEntry[];
@storage({ key: "scene-table-sort", state: false, subscribe: false })
private _activeSorting?: SortingChangedEvent;
@storage({ key: "scene-table-grouping", state: false, subscribe: false })
private _activeGrouping?: string;
@storage({
key: "scene-table-collapsed",
state: false,
subscribe: false,
})
private _activeCollapsed?: string;
private _sizeController = new ResizeController(this, {
callback: (entries) => entries[0]?.contentRect.width,
});
@ -391,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) =>
@ -427,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,
@ -438,6 +494,7 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
this._labels,
this._filteredScenes
);
return html`
<hass-tabs-subpage-data-table
.hass=${this.hass}
@ -463,7 +520,12 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
).length}
.columns=${this._columns(this.narrow, this.hass.localize)}
id="entity_id"
initialGroupColumn="category"
.initialGroupColumn=${this._activeGrouping || "category"}
.initialCollapsedGroups=${this._activeCollapsed}
.initialSorting=${this._activeSorting}
@sorting-changed=${this._handleSortingChanged}
@grouping-changed=${this._handleGroupingChanged}
@collapsed-changed=${this._handleCollapseChanged}
.data=${scenes}
.empty=${!this.scenes.length}
.activeFilters=${this._activeFilters}
@ -562,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">
${
@ -610,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(
@ -627,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
@ -855,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
@ -975,6 +1111,18 @@ ${rejected
});
}
private _handleSortingChanged(ev: CustomEvent) {
this._activeSorting = ev.detail;
}
private _handleGroupingChanged(ev: CustomEvent) {
this._activeGrouping = ev.detail.value;
}
private _handleCollapseChanged(ev: CustomEvent) {
this._activeCollapsed = ev.detail.value;
}
static get styles(): CSSResultGroup {
return [
haStyle,

View File

@ -13,6 +13,7 @@ import {
mdiPlus,
mdiScriptText,
mdiTag,
mdiTextureBox,
mdiTransitConnection,
} from "@mdi/js";
import { differenceInDays } from "date-fns";
@ -33,14 +34,20 @@ import { computeCssColor } from "../../../common/color/compute-color";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { formatShortDateTime } from "../../../common/datetime/format_date_time";
import { relativeTime } from "../../../common/datetime/relative_time";
import { storage } from "../../../common/decorators/storage";
import { HASSDomEvent, fireEvent } from "../../../common/dom/fire_event";
import { computeStateName } from "../../../common/entity/compute_state_name";
import { navigate } from "../../../common/navigate";
import { LocalizeFunc } from "../../../common/translations/localize";
import {
hasRejectedItems,
rejectedItems,
} from "../../../common/util/promise-all-settled-results";
import {
DataTableColumnContainer,
RowClickedEvent,
SelectionChangedEvent,
SortingChangedEvent,
} from "../../../components/data-table/ha-data-table";
import "../../../components/data-table/ha-data-table-labels";
import "../../../components/ha-fab";
@ -55,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,
@ -92,15 +100,12 @@ 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";
import { configSections } from "../ha-panel-config";
import { showLabelDetailDialog } from "../labels/show-dialog-label-detail";
import {
hasRejectedItems,
rejectedItems,
} from "../../../common/util/promise-all-settled-results";
type ScriptItem = ScriptEntity & {
name: string;
@ -148,6 +153,19 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
@consume({ context: fullEntitiesContext, subscribe: true })
_entityReg!: EntityRegistryEntry[];
@storage({ key: "script-table-sort", state: false, subscribe: false })
private _activeSorting?: SortingChangedEvent;
@storage({ key: "script-table-grouping", state: false, subscribe: false })
private _activeGrouping?: string;
@storage({
key: "script-table-collapsed",
state: false,
subscribe: false,
})
private _activeCollapsed?: string;
private _sizeController = new ResizeController(this, {
callback: (entries) => entries[0]?.contentRect.width,
});
@ -403,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) =>
@ -439,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,
@ -462,7 +518,12 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
{ number: scripts.length }
)}
hasFilters
initialGroupColumn="category"
.initialGroupColumn=${this._activeGrouping || "category"}
.initialCollapsedGroups=${this._activeCollapsed}
.initialSorting=${this._activeSorting}
@sorting-changed=${this._handleSortingChanged}
@grouping-changed=${this._handleGroupingChanged}
@collapsed-changed=${this._handleCollapseChanged}
selectable
.selected=${this._selected.length}
@selection-changed=${this._handleSelectionChanged}
@ -588,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">
${
@ -636,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(
@ -653,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
@ -1091,6 +1186,58 @@ ${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;
}
private _handleGroupingChanged(ev: CustomEvent) {
this._activeGrouping = ev.detail.value;
}
private _handleCollapseChanged(ev: CustomEvent) {
this._activeCollapsed = ev.detail.value;
}
static get styles(): CSSResultGroup {
return [
haStyle,

View File

@ -122,7 +122,7 @@ export class HaManualScriptEditor extends LitElement {
<ha-automation-action
role="region"
aria-labelledby="sequence-heading"
.actions=${this.config.sequence}
.actions=${this.config.sequence || []}
.path=${["sequence"]}
@value-changed=${this._sequenceChanged}
@item-moved=${this._itemMoved}

View File

@ -7,6 +7,7 @@ import { LocalizeFunc } from "../../../common/translations/localize";
import {
DataTableColumnContainer,
RowClickedEvent,
SortingChangedEvent,
} from "../../../components/data-table/ha-data-table";
import "../../../components/data-table/ha-data-table-icon";
import "../../../components/ha-fab";
@ -25,6 +26,7 @@ import { HomeAssistant, Route } from "../../../types";
import { configSections } from "../ha-panel-config";
import { showAddUserDialog } from "./show-dialog-add-user";
import { showUserDetailDialog } from "./show-dialog-user-detail";
import { storage } from "../../../common/decorators/storage";
@customElement("ha-config-users")
export class HaConfigUsers extends LitElement {
@ -38,6 +40,19 @@ export class HaConfigUsers extends LitElement {
@state() private _users: User[] = [];
@storage({ key: "users-table-sort", state: false, subscribe: false })
private _activeSorting?: SortingChangedEvent;
@storage({ key: "users-table-grouping", state: false, subscribe: false })
private _activeGrouping?: string;
@storage({
key: "users-table-collapsed",
state: false,
subscribe: false,
})
private _activeCollapsed?: string;
private _columns = memoizeOne(
(narrow: boolean, localize: LocalizeFunc): DataTableColumnContainer => {
const columns: DataTableColumnContainer<User> = {
@ -70,16 +85,14 @@ export class HaConfigUsers extends LitElement {
hidden: narrow,
template: (user) => html`${user.username || "—"}`,
},
group_ids: {
group: {
title: localize("ui.panel.config.users.picker.headers.group"),
sortable: true,
filterable: true,
groupable: true,
width: "20%",
direction: "asc",
hidden: narrow,
template: (user) => html`
${localize(`groups.${user.group_ids[0]}`)}
`,
},
is_active: {
title: this.hass.localize(
@ -164,7 +177,13 @@ export class HaConfigUsers extends LitElement {
backPath="/config"
.tabs=${configSections.persons}
.columns=${this._columns(this.narrow, this.hass.localize)}
.data=${this._users}
.data=${this._userData(this._users, this.hass.localize)}
.initialGroupColumn=${this._activeGrouping}
.initialCollapsedGroups=${this._activeCollapsed}
.initialSorting=${this._activeSorting}
@sorting-changed=${this._handleSortingChanged}
@grouping-changed=${this._handleGroupingChanged}
@collapsed-changed=${this._handleCollapseChanged}
@row-click=${this._editUser}
hasFab
clickable
@ -181,6 +200,13 @@ export class HaConfigUsers extends LitElement {
`;
}
private _userData = memoizeOne((users: User[], localize: LocalizeFunc) =>
users.map((user) => ({
...user,
group: localize(`groups.${user.group_ids[0]}`),
}))
);
private async _fetchUsers() {
this._users = await fetchUsers(this.hass);
@ -245,6 +271,18 @@ export class HaConfigUsers extends LitElement {
},
});
}
private _handleSortingChanged(ev: CustomEvent) {
this._activeSorting = ev.detail;
}
private _handleGroupingChanged(ev: CustomEvent) {
this._activeGrouping = ev.detail.value;
}
private _handleCollapseChanged(ev: CustomEvent) {
this._activeCollapsed = ev.detail.value;
}
}
declare global {

View File

@ -23,6 +23,7 @@ import {
DataTableRowData,
RowClickedEvent,
SelectionChangedEvent,
SortingChangedEvent,
} from "../../../components/data-table/ha-data-table";
import "../../../components/ha-fab";
import { AlexaEntity, fetchCloudAlexaEntities } from "../../../data/alexa";
@ -52,6 +53,7 @@ import "./expose/expose-assistant-icon";
import { voiceAssistantTabs } from "./ha-config-voice-assistants";
import { showExposeEntityDialog } from "./show-dialog-expose-entity";
import { showVoiceSettingsDialog } from "./show-dialog-voice-settings";
import { storage } from "../../../common/decorators/storage";
@customElement("ha-config-voice-assistants-expose")
export class VoiceAssistantsExpose extends LitElement {
@ -87,6 +89,13 @@ export class VoiceAssistantsExpose extends LitElement {
string[] | undefined
>;
@storage({
key: "voice-expose-table-sort",
state: false,
subscribe: false,
})
private _activeSorting?: SortingChangedEvent;
@query("hass-tabs-subpage-data-table", true)
private _dataTable!: HaTabsSubpageDataTable;
@ -505,6 +514,8 @@ export class VoiceAssistantsExpose extends LitElement {
selectable
.selected=${this._selectedEntities.length}
clickable
.initialSorting=${this._activeSorting}
@sorting-changed=${this._handleSortingChanged}
@selection-changed=${this._handleSelectionChanged}
@clear-filter=${this._clearFilter}
@search-changed=${this._handleSearchChange}
@ -696,6 +707,10 @@ export class VoiceAssistantsExpose extends LitElement {
navigate(window.location.pathname, { replace: true });
}
private _handleSortingChanged(ev: CustomEvent) {
this._activeSorting = ev.detail;
}
static get styles(): CSSResultGroup {
return [
haStyle,

View File

@ -0,0 +1,127 @@
import { mdiLock, mdiLockOpen } from "@mdi/js";
import { HassEntity } from "home-assistant-js-websocket";
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { computeDomain } from "../../../common/entity/compute_domain";
import "../../../components/ha-control-button";
import "../../../components/ha-control-button-group";
import {
callProtectedLockService,
isAvailable,
isLocking,
isUnlocking,
isLocked,
} from "../../../data/lock";
import { HomeAssistant } from "../../../types";
import { LovelaceCardFeature } from "../types";
import { LockCommandsCardFeatureConfig } from "./types";
import { forwardHaptic } from "../../../data/haptics";
export const supportsLockCommandsCardFeature = (stateObj: HassEntity) => {
const domain = computeDomain(stateObj.entity_id);
return domain === "lock";
};
@customElement("hui-lock-commands-card-feature")
class HuiLockCommandsCardFeature
extends LitElement
implements LovelaceCardFeature
{
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public stateObj?: HassEntity;
@state() private _config?: LockCommandsCardFeatureConfig;
static getStubConfig(): LockCommandsCardFeatureConfig {
return {
type: "lock-commands",
};
}
public setConfig(config: LockCommandsCardFeatureConfig): void {
if (!config) {
throw new Error("Invalid configuration");
}
this._config = config;
}
private _onTap(ev): void {
ev.stopPropagation();
const service = ev.target.dataset.service;
if (!this.hass || !this.stateObj || !service) {
return;
}
forwardHaptic("light");
callProtectedLockService(this, this.hass, this.stateObj, service);
}
protected render() {
if (
!this._config ||
!this.hass ||
!this.stateObj ||
!supportsLockCommandsCardFeature(this.stateObj)
) {
return nothing;
}
return html`
<ha-control-button-group>
<ha-control-button
.label=${this.hass.localize("ui.card.lock.lock")}
.disabled=${!isAvailable(this.stateObj) || isLocked(this.stateObj)}
@click=${this._onTap}
data-service="lock"
class=${classMap({
pulse: isLocking(this.stateObj) || isUnlocking(this.stateObj),
})}
>
<ha-svg-icon .path=${mdiLock}></ha-svg-icon>
</ha-control-button>
<ha-control-button
.label=${this.hass.localize("ui.card.lock.unlock")}
.disabled=${!isAvailable(this.stateObj) || !isLocked(this.stateObj)}
@click=${this._onTap}
data-service="unlock"
class=${classMap({
pulse: isLocking(this.stateObj) || isUnlocking(this.stateObj),
})}
>
<ha-svg-icon .path=${mdiLockOpen}></ha-svg-icon>
</ha-control-button>
</ha-control-button-group>
`;
}
static get styles(): CSSResultGroup {
return css`
@keyframes pulse {
0% {
opacity: 1;
}
50% {
opacity: 0;
}
100% {
opacity: 1;
}
}
.pulse {
animation: pulse 1s infinite;
}
ha-control-button-group {
margin: 0 12px 12px 12px;
--control-button-group-spacing: 12px;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-lock-commands-card-feature": HuiLockCommandsCardFeature;
}
}

View File

@ -0,0 +1,154 @@
import { mdiCheck } from "@mdi/js";
import { HassEntity } from "home-assistant-js-websocket";
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { computeDomain } from "../../../common/entity/compute_domain";
import { supportsFeature } from "../../../common/entity/supports-feature";
import "../../../components/ha-control-button";
import "../../../components/ha-control-button-group";
import {
LockEntityFeature,
callProtectedLockService,
isAvailable,
} from "../../../data/lock";
import { HomeAssistant } from "../../../types";
import { LovelaceCardFeature } from "../types";
import { LockOpenDoorCardFeatureConfig } from "./types";
export const supportsLockOpenDoorCardFeature = (stateObj: HassEntity) => {
const domain = computeDomain(stateObj.entity_id);
return domain === "lock" && supportsFeature(stateObj, LockEntityFeature.OPEN);
};
const CONFIRM_TIMEOUT_SECOND = 5;
const OPENED_TIMEOUT_SECOND = 3;
type ButtonState = "normal" | "confirm" | "success";
@customElement("hui-lock-open-door-card-feature")
class HuiLockOpenDoorCardFeature
extends LitElement
implements LovelaceCardFeature
{
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public stateObj?: HassEntity;
@state() public _buttonState: ButtonState = "normal";
@state() private _config?: LockOpenDoorCardFeatureConfig;
private _buttonTimeout?: number;
static getStubConfig(): LockOpenDoorCardFeatureConfig {
return {
type: "lock-open-door",
};
}
public setConfig(config: LockOpenDoorCardFeatureConfig): void {
if (!config) {
throw new Error("Invalid configuration");
}
this._config = config;
}
private _setButtonState(buttonState: ButtonState, timeoutSecond?: number) {
clearTimeout(this._buttonTimeout);
this._buttonState = buttonState;
if (timeoutSecond) {
this._buttonTimeout = window.setTimeout(() => {
this._buttonState = "normal";
}, timeoutSecond * 1000);
}
}
private async _open() {
if (this._buttonState !== "confirm") {
this._setButtonState("confirm", CONFIRM_TIMEOUT_SECOND);
return;
}
if (!this.hass || !this.stateObj) {
return;
}
callProtectedLockService(this, this.hass, this.stateObj!, "open");
this._setButtonState("success", OPENED_TIMEOUT_SECOND);
}
protected render() {
if (
!this._config ||
!this.hass ||
!this.stateObj ||
!supportsLockOpenDoorCardFeature(this.stateObj)
) {
return nothing;
}
return html`
${this._buttonState === "success"
? html`
<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>
<ha-control-button
.disabled=${!isAvailable(this.stateObj)}
class="open-button ${this._buttonState}"
@click=${this._open}
>
${this._buttonState === "confirm"
? this.hass.localize("ui.card.lock.open_door_confirm")
: this.hass.localize("ui.card.lock.open_door")}
</ha-control-button>
</ha-control-button-group>
`}
`;
}
static get styles(): CSSResultGroup {
return css`
ha-control-button {
font-size: 14px;
}
ha-control-button-group {
margin: 0 12px 12px 12px;
--control-button-group-spacing: 12px;
}
.open-button {
width: 130px;
}
.open-button.confirm {
--control-button-background-color: var(--warning-color);
}
.open-success {
font-size: 14px;
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;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-lock-open-door-card-feature": HuiLockOpenDoorCardFeature;
}
}

View File

@ -26,6 +26,14 @@ export interface LightColorTempCardFeatureConfig {
type: "light-color-temp";
}
export interface LockCommandsCardFeatureConfig {
type: "lock-commands";
}
export interface LockOpenDoorCardFeatureConfig {
type: "lock-open-door";
}
export interface FanPresetModesCardFeatureConfig {
type: "fan-preset-modes";
style?: "dropdown" | "icons";
@ -143,6 +151,8 @@ export type LovelaceCardFeatureConfig =
| LawnMowerCommandsCardFeatureConfig
| LightBrightnessCardFeatureConfig
| LightColorTempCardFeatureConfig
| LockCommandsCardFeatureConfig
| LockOpenDoorCardFeatureConfig
| NumericInputCardFeatureConfig
| SelectOptionsCardFeatureConfig
| TargetHumidityCardFeatureConfig

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,17 @@ 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;
const themeMode =
this._config.theme_mode || (this._config.dark_mode ? "dark" : "auto");
return html`
<ha-card id="card" .header=${this._config.title}>
<div id="root">
@ -161,7 +172,7 @@ 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}
.themeMode=${themeMode}
interactiveZones
renderPassive
></ha-map>
@ -170,6 +181,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

@ -14,6 +14,8 @@ import "../card-features/hui-humidifier-toggle-card-feature";
import "../card-features/hui-lawn-mower-commands-card-feature";
import "../card-features/hui-light-brightness-card-feature";
import "../card-features/hui-light-color-temp-card-feature";
import "../card-features/hui-lock-commands-card-feature";
import "../card-features/hui-lock-open-door-card-feature";
import "../card-features/hui-numeric-input-card-feature";
import "../card-features/hui-select-options-card-feature";
import "../card-features/hui-target-temperature-card-feature";
@ -45,6 +47,8 @@ const TYPES: Set<LovelaceCardFeatureConfig["type"]> = new Set([
"lawn-mower-commands",
"light-brightness",
"light-color-temp",
"lock-commands",
"lock-open-door",
"numeric-input",
"select-options",
"target-humidity",

View File

@ -35,6 +35,8 @@ import { supportsHumidifierToggleCardFeature } from "../../card-features/hui-hum
import { supportsLawnMowerCommandCardFeature } from "../../card-features/hui-lawn-mower-commands-card-feature";
import { supportsLightBrightnessCardFeature } from "../../card-features/hui-light-brightness-card-feature";
import { supportsLightColorTempCardFeature } from "../../card-features/hui-light-color-temp-card-feature";
import { supportsLockCommandsCardFeature } from "../../card-features/hui-lock-commands-card-feature";
import { supportsLockOpenDoorCardFeature } from "../../card-features/hui-lock-open-door-card-feature";
import { supportsNumericInputCardFeature } from "../../card-features/hui-numeric-input-card-feature";
import { supportsSelectOptionsCardFeature } from "../../card-features/hui-select-options-card-feature";
import { supportsTargetHumidityCardFeature } from "../../card-features/hui-target-humidity-card-feature";
@ -56,8 +58,8 @@ const UI_FEATURE_TYPES = [
"climate-preset-modes",
"cover-open-close",
"cover-position",
"cover-tilt-position",
"cover-tilt",
"cover-tilt-position",
"fan-preset-modes",
"fan-speed",
"humidifier-modes",
@ -65,6 +67,8 @@ const UI_FEATURE_TYPES = [
"lawn-mower-commands",
"light-brightness",
"light-color-temp",
"lock-commands",
"lock-open-door",
"numeric-input",
"select-options",
"target-humidity",
@ -111,6 +115,8 @@ const SUPPORTS_FEATURE_TYPES: Record<
"lawn-mower-commands": supportsLawnMowerCommandCardFeature,
"light-brightness": supportsLightBrightnessCardFeature,
"light-color-temp": supportsLightColorTempCardFeature,
"lock-commands": supportsLockCommandsCardFeature,
"lock-open-door": supportsLockOpenDoorCardFeature,
"numeric-input": supportsNumericInputCardFeature,
"select-options": supportsSelectOptionsCardFeature,
"target-humidity": supportsTargetHumidityCardFeature,

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

@ -83,7 +83,8 @@ export const connectionMixin = <T extends Constructor<HassBaseEl>>(
service,
serviceData,
target,
notifyOnError = true
notifyOnError = true,
returnResponse = false
) => {
if (__DEV__ || this.hass?.debugConnection) {
// eslint-disable-next-line no-console
@ -101,7 +102,8 @@ export const connectionMixin = <T extends Constructor<HassBaseEl>>(
domain,
service,
serviceData ?? {},
target
target,
returnResponse
)) as ServiceCallResponse;
} catch (err: any) {
if (

View File

@ -1,3 +1,4 @@
import { atLeastVersion } from "../common/config/version";
import { fireEvent } from "../common/dom/fire_event";
import { computeLocalize, LocalizeFunc } from "../common/translations/localize";
import {
@ -8,6 +9,7 @@ import { debounce } from "../common/util/debounce";
import {
FirstWeekday,
getHassTranslations,
getHassTranslationsPre109,
NumberFormat,
saveTranslationPreferences,
TimeFormat,
@ -70,7 +72,7 @@ export default <T extends Constructor<HassBaseEl>>(superClass: T) =>
// eslint-disable-next-line: variable-name
private __coreProgress?: string;
private __loadedFragmetTranslations: Set<string> = new Set();
private __loadedFragmentTranslations: Set<string> = new Set();
private __loadedTranslations: {
// track what things have been loaded
@ -260,7 +262,7 @@ export default <T extends Constructor<HassBaseEl>>(superClass: T) =>
document.querySelector("html")!.setAttribute("lang", hass.language);
this._applyDirection(hass);
this._loadCoreTranslations(hass.language);
this.__loadedFragmetTranslations = new Set();
this.__loadedFragmentTranslations = new Set();
this._loadFragmentTranslations(hass.language, hass.panelUrl);
}
@ -284,6 +286,23 @@ export default <T extends Constructor<HassBaseEl>>(superClass: T) =>
configFlow?: Parameters<typeof getHassTranslations>[4],
force = false
): Promise<LocalizeFunc> {
if (
__BACKWARDS_COMPAT__ &&
!atLeastVersion(this.hass!.connection.haVersion, 0, 109)
) {
if (category !== "state") {
return this.hass!.localize;
}
const resources = await getHassTranslationsPre109(this.hass!, language);
// Ignore the repsonse if user switched languages before we got response
if (this.hass!.language !== language) {
return this.hass!.localize;
}
return this._updateResources(language, resources);
}
let alreadyLoaded: LoadedTranslationCategory;
if (category in this.__loadedTranslations) {
@ -366,12 +385,12 @@ export default <T extends Constructor<HassBaseEl>>(superClass: T) =>
return undefined;
}
if (this.__loadedFragmetTranslations.has(fragment)) {
if (this.__loadedFragmentTranslations.has(fragment)) {
return this.hass!.localize;
}
this.__loadedFragmetTranslations.add(fragment);
this.__loadedFragmentTranslations.add(fragment);
const result = await getTranslation(fragment, language);
return this._updateResources(result.language, result.data);
return this._updateResources(language, result.data);
}
private async _loadCoreTranslations(language: string) {
@ -383,7 +402,7 @@ export default <T extends Constructor<HassBaseEl>>(superClass: T) =>
this.__coreProgress = language;
try {
const result = await getTranslation(null, language);
await this._updateResources(result.language, result.data);
await this._updateResources(language, result.data);
} finally {
this.__coreProgress = undefined;
}

View File

@ -3837,6 +3837,19 @@
"advanced_options": "Advanced options",
"external_activation": "Allow external activation of remote control",
"external_activation_secondary": "Allows you to turn on remote control from your Nabu Casa account page, even if you're outside your local network",
"strict_connection": "Restrict access to logged in users",
"strict_connection_secondary": "When a user is not logged in to your Home Assistant instance, they will not be able to access your instance remotely",
"strict_connection_mode": "Mode",
"strict_connection_modes": {
"disabled": "Disabled",
"guard_page": "Guard page",
"drop_connection": "Drop connection"
},
"strict_connection_link": "Create login link",
"strict_connection_link_secondary": "You can create a link that will give temporary access to the login page.",
"strict_connection_create_link": "Create link",
"strict_connection_link_created_message": "Give this link to the person you want to give remote access to the login page of your Home Assistant instance.",
"strict_connection_copy_link": "Copy link",
"certificate_info": "Certificate info",
"certificate_expire": "Will be renewed at {date}",
"more_info": "More info"
@ -5822,7 +5835,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."
},
@ -5948,6 +5968,12 @@
"light-color-temp": {
"label": "Light color temperature"
},
"lock-commands": {
"label": "Lock commands"
},
"lock-open-door": {
"label": "Lock open door"
},
"vacuum-commands": {
"label": "Vacuum commands",
"commands": "Commands",

View File

@ -139,6 +139,8 @@ export type FullCalendarView =
| "dayGridDay"
| "listWeek";
export type ThemeMode = "auto" | "light" | "dark";
export interface ToggleButton {
label: string;
iconPath?: string;
@ -190,6 +192,7 @@ export interface Context {
export interface ServiceCallResponse {
context: Context;
response?: any;
}
export interface ServiceCallRequest {
@ -241,7 +244,8 @@ export interface HomeAssistant {
service: ServiceCallRequest["service"],
serviceData?: ServiceCallRequest["serviceData"],
target?: ServiceCallRequest["target"],
notifyOnError?: boolean
notifyOnError?: boolean,
returnResponse?: boolean
): Promise<ServiceCallResponse>;
callApi<T>(
method: "GET" | "POST" | "PUT" | "DELETE",

2213
yarn.lock

File diff suppressed because it is too large Load Diff