Merge branch 'dev' into title-alignment-option
This commit is contained in:
commit
675f1e2e83
|
@ -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: `
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>`;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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`
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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>`
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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}>
|
||||
|
|
|
@ -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`
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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`
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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``];
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
Loading…
Reference in New Issue