mirror of
https://github.com/home-assistant/frontend.git
synced 2025-08-25 00:59:29 +00:00
Compare commits
27 Commits
fix-backgr
...
20240404.2
Author | SHA1 | Date | |
---|---|---|---|
![]() |
1914de7ddf | ||
![]() |
2e505cfb1f | ||
![]() |
ab49aca815 | ||
![]() |
c96968e476 | ||
![]() |
8f050516ec | ||
![]() |
10eadbcbbb | ||
![]() |
17141824f7 | ||
![]() |
4cfd6c010f | ||
![]() |
daa9024bff | ||
![]() |
e96aca90fe | ||
![]() |
0580a31961 | ||
![]() |
5c42c5130c | ||
![]() |
72d1e37a23 | ||
![]() |
61c9072a08 | ||
![]() |
08b25f9c2a | ||
![]() |
1a03b49700 | ||
![]() |
2d4a8e2e45 | ||
![]() |
8486377604 | ||
![]() |
4326519a3f | ||
![]() |
962b30adb9 | ||
![]() |
29eb73176a | ||
![]() |
4f1cf1110f | ||
![]() |
d3bf0da289 | ||
![]() |
fd06d434f2 | ||
![]() |
d24d29e42f | ||
![]() |
e02a47a16a | ||
![]() |
795c16a941 |
@@ -161,6 +161,7 @@ const createWebpackConfig = ({
|
||||
resolve: {
|
||||
extensions: [".ts", ".js", ".json"],
|
||||
alias: {
|
||||
"lit/static-html$": "lit/static-html.js",
|
||||
"lit/decorators$": "lit/decorators.js",
|
||||
"lit/directive$": "lit/directive.js",
|
||||
"lit/directives/until$": "lit/directives/until.js",
|
||||
|
@@ -136,7 +136,7 @@ export class DemoAutomationDescribeAction extends LitElement {
|
||||
<div class="action">
|
||||
<span>
|
||||
${this._action
|
||||
? describeAction(this.hass, [], this._action)
|
||||
? describeAction(this.hass, [], [], [], this._action)
|
||||
: "<invalid YAML>"}
|
||||
</span>
|
||||
<ha-yaml-editor
|
||||
@@ -149,7 +149,7 @@ export class DemoAutomationDescribeAction extends LitElement {
|
||||
${ACTIONS.map(
|
||||
(conf) => html`
|
||||
<div class="action">
|
||||
<span>${describeAction(this.hass, [], conf as any)}</span>
|
||||
<span>${describeAction(this.hass, [], [], [], conf as any)}</span>
|
||||
<pre>${dump(conf)}</pre>
|
||||
</div>
|
||||
`
|
||||
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "home-assistant-frontend"
|
||||
version = "20240403.1"
|
||||
version = "20240404.2"
|
||||
license = {text = "Apache-2.0"}
|
||||
description = "The Home Assistant frontend"
|
||||
readme = "README.md"
|
||||
|
9
src/common/util/promise-all-settled-results.ts
Normal file
9
src/common/util/promise-all-settled-results.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export const hasRejectedItems = <T = any>(results: PromiseSettledResult<T>[]) =>
|
||||
results.some((result) => result.status === "rejected");
|
||||
|
||||
export const rejectedItems = <T = any>(
|
||||
results: PromiseSettledResult<T>[]
|
||||
): PromiseRejectedResult[] =>
|
||||
results.filter(
|
||||
(result) => result.status === "rejected"
|
||||
) as PromiseRejectedResult[];
|
@@ -11,10 +11,10 @@ import {
|
||||
} from "../common/datetime/localize_date";
|
||||
import { mainWindow } from "../common/dom/get_main_window";
|
||||
|
||||
// Set the current date to the left picker instead of the right picker because the right is hidden
|
||||
const CustomDateRangePicker = Vue.extend({
|
||||
mixins: [DateRangePicker],
|
||||
methods: {
|
||||
// Set the current date to the left picker instead of the right picker because the right is hidden
|
||||
selectMonthDate() {
|
||||
const dt: Date = this.end || new Date();
|
||||
// @ts-ignore
|
||||
@@ -23,6 +23,33 @@ const CustomDateRangePicker = Vue.extend({
|
||||
month: dt.getMonth() + 1,
|
||||
});
|
||||
},
|
||||
// Fix the start/end date calculation when selecting a date range. The
|
||||
// original code keeps track of the first clicked date (in_selection) but it
|
||||
// never sets it to either the start or end date variables, so if the
|
||||
// in_selection date is between the start and end date that were set by the
|
||||
// hover the selection will enter a broken state that's counter-intuitive
|
||||
// when hovering between weeks and leads to a random date when selecting a
|
||||
// range across months. This bug doesn't seem to be present on v0.6.7 of the
|
||||
// lib
|
||||
hoverDate(value: Date) {
|
||||
if (this.readonly) return;
|
||||
|
||||
if (this.in_selection) {
|
||||
const pickA = this.in_selection as Date;
|
||||
const pickB = value;
|
||||
|
||||
this.start = this.normalizeDatetime(
|
||||
Math.min(pickA.valueOf(), pickB.valueOf()),
|
||||
this.start
|
||||
);
|
||||
this.end = this.normalizeDatetime(
|
||||
Math.max(pickA.valueOf(), pickB.valueOf()),
|
||||
this.end
|
||||
);
|
||||
}
|
||||
|
||||
this.$emit("hover-date", value);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
@@ -1,4 +1,3 @@
|
||||
import { SelectedDetail } from "@material/mwc-list";
|
||||
import { mdiFilterVariantRemove } from "@mdi/js";
|
||||
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
@@ -56,11 +55,7 @@ export class HaFilterIntegrations extends LitElement {
|
||||
@value-changed=${this._handleSearchChange}
|
||||
>
|
||||
</search-input-outlined>
|
||||
<mwc-list
|
||||
@selected=${this._integrationsSelected}
|
||||
multi
|
||||
class="ha-scrollbar"
|
||||
>
|
||||
<mwc-list class="ha-scrollbar" @click=${this._handleItemClick}>
|
||||
${repeat(
|
||||
this._integrations(this._manifests, this._filter, this.value),
|
||||
(i) => i.domain,
|
||||
@@ -131,34 +126,21 @@ export class HaFilterIntegrations extends LitElement {
|
||||
)
|
||||
);
|
||||
|
||||
private async _integrationsSelected(
|
||||
ev: CustomEvent<SelectedDetail<Set<number>>>
|
||||
) {
|
||||
const integrations = this._integrations(
|
||||
this._manifests!,
|
||||
this._filter,
|
||||
this.value
|
||||
);
|
||||
|
||||
if (!ev.detail.index.size) {
|
||||
fireEvent(this, "data-table-filter-changed", {
|
||||
value: [],
|
||||
items: undefined,
|
||||
});
|
||||
this.value = [];
|
||||
private _handleItemClick(ev) {
|
||||
const listItem = ev.target.closest("ha-check-list-item");
|
||||
const value = listItem?.value;
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const value: string[] = [];
|
||||
|
||||
for (const index of ev.detail.index) {
|
||||
const domain = integrations[index].domain;
|
||||
value.push(domain);
|
||||
if (this.value?.includes(value)) {
|
||||
this.value = this.value?.filter((val) => val !== value);
|
||||
} else {
|
||||
this.value = [...(this.value || []), value];
|
||||
}
|
||||
this.value = value;
|
||||
listItem.selected = this.value?.includes(value);
|
||||
|
||||
fireEvent(this, "data-table-filter-changed", {
|
||||
value,
|
||||
value: this.value,
|
||||
items: undefined,
|
||||
});
|
||||
}
|
||||
|
40
src/components/ha-outlined-field.ts
Normal file
40
src/components/ha-outlined-field.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { MdOutlinedField } from "@material/web/field/outlined-field";
|
||||
import "element-internals-polyfill";
|
||||
import { css } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
import { literal } from "lit/static-html";
|
||||
|
||||
@customElement("ha-outlined-field")
|
||||
export class HaOutlinedField extends MdOutlinedField {
|
||||
protected readonly fieldTag = literal`ha-outlined-field`;
|
||||
|
||||
static override styles = [
|
||||
...super.styles,
|
||||
css`
|
||||
.container::before {
|
||||
display: block;
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-color: var(--ha-outlined-field-container-color, transparent);
|
||||
opacity: var(--ha-outlined-field-container-opacity, 1);
|
||||
border-start-start-radius: var(--_container-shape-start-start);
|
||||
border-start-end-radius: var(--_container-shape-start-end);
|
||||
border-end-start-radius: var(--_container-shape-end-start);
|
||||
border-end-end-radius: var(--_container-shape-end-end);
|
||||
}
|
||||
.with-start .start {
|
||||
margin-inline-end: var(--ha-outlined-field-start-margin, 4px);
|
||||
}
|
||||
.with-end .end {
|
||||
margin-inline-start: var(--ha-outlined-field-end-margin, 4px);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-outlined-field": HaOutlinedField;
|
||||
}
|
||||
}
|
@@ -2,9 +2,13 @@ import { MdOutlinedTextField } from "@material/web/textfield/outlined-text-field
|
||||
import "element-internals-polyfill";
|
||||
import { css } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
import { literal } from "lit/static-html";
|
||||
import "./ha-outlined-field";
|
||||
|
||||
@customElement("ha-outlined-text-field")
|
||||
export class HaOutlinedTextField extends MdOutlinedTextField {
|
||||
protected readonly fieldTag = literal`ha-outlined-field`;
|
||||
|
||||
static override styles = [
|
||||
...super.styles,
|
||||
css`
|
||||
@@ -25,12 +29,10 @@ export class HaOutlinedTextField extends MdOutlinedTextField {
|
||||
--md-outlined-field-container-shape-end-end: 10px;
|
||||
--md-outlined-field-container-shape-end-start: 10px;
|
||||
--md-outlined-field-focus-outline-width: 1px;
|
||||
--ha-outlined-field-start-margin: -4px;
|
||||
--ha-outlined-field-end-margin: -4px;
|
||||
--mdc-icon-size: var(--md-input-chip-icon-size, 18px);
|
||||
}
|
||||
md-outlined-field {
|
||||
background: var(--ha-outlined-text-field-container-color, transparent);
|
||||
opacity: var(--ha-outlined-text-field-container-opacity, 1);
|
||||
}
|
||||
.input {
|
||||
font-family: Roboto, sans-serif;
|
||||
}
|
||||
|
@@ -6,6 +6,11 @@ import { MdSubMenu } from "@material/web/menu/sub-menu";
|
||||
@customElement("ha-sub-menu")
|
||||
// @ts-expect-error
|
||||
export class HaSubMenu extends MdSubMenu {
|
||||
async show() {
|
||||
super.show();
|
||||
this.menu.hasOverflow = false;
|
||||
}
|
||||
|
||||
static override styles: CSSResult[] = [
|
||||
MdSubMenu.styles,
|
||||
css`
|
||||
|
@@ -97,7 +97,7 @@ class SearchInputOutlined extends LitElement {
|
||||
ha-outlined-text-field {
|
||||
display: block;
|
||||
width: 100%;
|
||||
--ha-outlined-text-field-container-color: var(--card-background-color);
|
||||
--ha-outlined-field-container-color: var(--card-background-color);
|
||||
}
|
||||
ha-svg-icon,
|
||||
ha-icon-button {
|
||||
|
@@ -1,3 +1,4 @@
|
||||
import { consume } from "@lit-labs/context";
|
||||
import {
|
||||
mdiAlertCircle,
|
||||
mdiCircle,
|
||||
@@ -6,14 +7,13 @@ import {
|
||||
mdiProgressWrench,
|
||||
mdiRecordCircleOutline,
|
||||
} from "@mdi/js";
|
||||
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
css,
|
||||
html,
|
||||
nothing,
|
||||
} from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
@@ -23,27 +23,31 @@ import { relativeTime } from "../../common/datetime/relative_time";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { toggleAttribute } from "../../common/dom/toggle_attribute";
|
||||
import {
|
||||
EntityRegistryEntry,
|
||||
subscribeEntityRegistry,
|
||||
} from "../../data/entity_registry";
|
||||
floorsContext,
|
||||
fullEntitiesContext,
|
||||
labelsContext,
|
||||
} from "../../data/context";
|
||||
import { EntityRegistryEntry } from "../../data/entity_registry";
|
||||
import { FloorRegistryEntry } from "../../data/floor_registry";
|
||||
import { LabelRegistryEntry } from "../../data/label_registry";
|
||||
import { LogbookEntry } from "../../data/logbook";
|
||||
import {
|
||||
ChooseAction,
|
||||
ChooseActionChoice,
|
||||
getActionType,
|
||||
IfAction,
|
||||
ParallelAction,
|
||||
RepeatAction,
|
||||
getActionType,
|
||||
} from "../../data/script";
|
||||
import { describeAction } from "../../data/script_i18n";
|
||||
import {
|
||||
ActionTraceStep,
|
||||
AutomationTraceExtended,
|
||||
ChooseActionTraceStep,
|
||||
getDataFromPath,
|
||||
IfActionTraceStep,
|
||||
isTriggerPath,
|
||||
TriggerTraceStep,
|
||||
getDataFromPath,
|
||||
isTriggerPath,
|
||||
} from "../../data/trace";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import "./ha-timeline";
|
||||
@@ -200,6 +204,8 @@ class ActionRenderer {
|
||||
constructor(
|
||||
private hass: HomeAssistant,
|
||||
private entityReg: EntityRegistryEntry[],
|
||||
private labelReg: LabelRegistryEntry[],
|
||||
private floorReg: FloorRegistryEntry[],
|
||||
private entries: TemplateResult[],
|
||||
private trace: AutomationTraceExtended,
|
||||
private logbookRenderer: LogbookRenderer,
|
||||
@@ -310,7 +316,14 @@ class ActionRenderer {
|
||||
|
||||
this._renderEntry(
|
||||
path,
|
||||
describeAction(this.hass, this.entityReg, data, actionType),
|
||||
describeAction(
|
||||
this.hass,
|
||||
this.entityReg,
|
||||
this.labelReg,
|
||||
this.floorReg,
|
||||
data,
|
||||
actionType
|
||||
),
|
||||
undefined,
|
||||
data.enabled === false
|
||||
);
|
||||
@@ -475,7 +488,13 @@ class ActionRenderer {
|
||||
|
||||
const name =
|
||||
repeatConfig.alias ||
|
||||
describeAction(this.hass, this.entityReg, repeatConfig);
|
||||
describeAction(
|
||||
this.hass,
|
||||
this.entityReg,
|
||||
this.labelReg,
|
||||
this.floorReg,
|
||||
repeatConfig
|
||||
);
|
||||
|
||||
this._renderEntry(repeatPath, name, undefined, disabled);
|
||||
|
||||
@@ -631,15 +650,17 @@ export class HaAutomationTracer extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public allowPick = false;
|
||||
|
||||
@state() private _entityReg: EntityRegistryEntry[] = [];
|
||||
@state()
|
||||
@consume({ context: fullEntitiesContext, subscribe: true })
|
||||
_entityReg!: EntityRegistryEntry[];
|
||||
|
||||
public hassSubscribe(): UnsubscribeFunc[] {
|
||||
return [
|
||||
subscribeEntityRegistry(this.hass.connection!, (entities) => {
|
||||
this._entityReg = entities;
|
||||
}),
|
||||
];
|
||||
}
|
||||
@state()
|
||||
@consume({ context: labelsContext, subscribe: true })
|
||||
_labelReg!: LabelRegistryEntry[];
|
||||
|
||||
@state()
|
||||
@consume({ context: floorsContext, subscribe: true })
|
||||
_floorReg!: FloorRegistryEntry[];
|
||||
|
||||
protected render() {
|
||||
if (!this.trace) {
|
||||
@@ -657,6 +678,8 @@ export class HaAutomationTracer extends LitElement {
|
||||
const actionRenderer = new ActionRenderer(
|
||||
this.hass,
|
||||
this._entityReg,
|
||||
this._labelReg,
|
||||
this._floorReg,
|
||||
entries,
|
||||
this.trace,
|
||||
logbookRenderer,
|
||||
|
@@ -2,6 +2,8 @@ import { createContext } from "@lit-labs/context";
|
||||
import { HassConfig } from "home-assistant-js-websocket";
|
||||
import { HomeAssistant } from "../types";
|
||||
import { EntityRegistryEntry } from "./entity_registry";
|
||||
import { FloorRegistryEntry } from "./floor_registry";
|
||||
import { LabelRegistryEntry } from "./label_registry";
|
||||
|
||||
export const connectionContext =
|
||||
createContext<HomeAssistant["connection"]>("connection");
|
||||
@@ -25,3 +27,7 @@ export const panelsContext = createContext<HomeAssistant["panels"]>("panels");
|
||||
|
||||
export const fullEntitiesContext =
|
||||
createContext<EntityRegistryEntry[]>("extendedEntities");
|
||||
|
||||
export const floorsContext = createContext<FloorRegistryEntry[]>("floors");
|
||||
|
||||
export const labelsContext = createContext<LabelRegistryEntry[]>("labels");
|
||||
|
@@ -14,7 +14,9 @@ import {
|
||||
computeEntityRegistryName,
|
||||
entityRegistryById,
|
||||
} from "./entity_registry";
|
||||
import { FloorRegistryEntry } from "./floor_registry";
|
||||
import { domainToName } from "./integration";
|
||||
import { LabelRegistryEntry } from "./label_registry";
|
||||
import {
|
||||
ActionType,
|
||||
ActionTypes,
|
||||
@@ -40,6 +42,8 @@ const actionTranslationBaseKey =
|
||||
export const describeAction = <T extends ActionType>(
|
||||
hass: HomeAssistant,
|
||||
entityRegistry: EntityRegistryEntry[],
|
||||
labelRegistry: LabelRegistryEntry[],
|
||||
floorRegistry: FloorRegistryEntry[],
|
||||
action: ActionTypes[T],
|
||||
actionType?: T,
|
||||
ignoreAlias = false
|
||||
@@ -48,6 +52,8 @@ export const describeAction = <T extends ActionType>(
|
||||
return tryDescribeAction(
|
||||
hass,
|
||||
entityRegistry,
|
||||
labelRegistry,
|
||||
floorRegistry,
|
||||
action,
|
||||
actionType,
|
||||
ignoreAlias
|
||||
@@ -66,6 +72,8 @@ export const describeAction = <T extends ActionType>(
|
||||
const tryDescribeAction = <T extends ActionType>(
|
||||
hass: HomeAssistant,
|
||||
entityRegistry: EntityRegistryEntry[],
|
||||
labelRegistry: LabelRegistryEntry[],
|
||||
floorRegistry: FloorRegistryEntry[],
|
||||
action: ActionTypes[T],
|
||||
actionType?: T,
|
||||
ignoreAlias = false
|
||||
@@ -82,10 +90,12 @@ const tryDescribeAction = <T extends ActionType>(
|
||||
|
||||
const targets: string[] = [];
|
||||
if (config.target) {
|
||||
for (const [key, label] of Object.entries({
|
||||
for (const [key, name] of Object.entries({
|
||||
area_id: "areas",
|
||||
device_id: "devices",
|
||||
entity_id: "entities",
|
||||
floor_id: "floors",
|
||||
label_id: "labels",
|
||||
})) {
|
||||
if (!(key in config.target)) {
|
||||
continue;
|
||||
@@ -99,7 +109,7 @@ const tryDescribeAction = <T extends ActionType>(
|
||||
targets.push(
|
||||
hass.localize(
|
||||
`${actionTranslationBaseKey}.service.description.target_template`,
|
||||
{ name: label }
|
||||
{ name }
|
||||
)
|
||||
);
|
||||
break;
|
||||
@@ -147,6 +157,32 @@ const tryDescribeAction = <T extends ActionType>(
|
||||
)
|
||||
);
|
||||
}
|
||||
} else if (key === "floor_id") {
|
||||
const floor = floorRegistry.find(
|
||||
(flr) => flr.floor_id === targetThing
|
||||
);
|
||||
if (floor?.name) {
|
||||
targets.push(floor.name);
|
||||
} else {
|
||||
targets.push(
|
||||
hass.localize(
|
||||
`${actionTranslationBaseKey}.service.description.target_unknown_floor`
|
||||
)
|
||||
);
|
||||
}
|
||||
} else if (key === "label_id") {
|
||||
const label = labelRegistry.find(
|
||||
(lbl) => lbl.label_id === targetThing
|
||||
);
|
||||
if (label?.name) {
|
||||
targets.push(label.name);
|
||||
} else {
|
||||
targets.push(
|
||||
hass.localize(
|
||||
`${actionTranslationBaseKey}.service.description.target_unknown_label`
|
||||
)
|
||||
);
|
||||
}
|
||||
} else {
|
||||
targets.push(targetThing);
|
||||
}
|
||||
|
@@ -496,8 +496,9 @@ export class HaTabsSubpageDataTable extends LitElement {
|
||||
${this.showFilters && !showPane
|
||||
? html`<ha-dialog
|
||||
open
|
||||
hideActions
|
||||
.heading=${localize("ui.components.subpage-data-table.filters")}
|
||||
.heading=${localize("ui.components.subpage-data-table.filters", {
|
||||
number: this.data.length,
|
||||
})}
|
||||
>
|
||||
<ha-dialog-header slot="heading">
|
||||
<ha-icon-button
|
||||
@@ -509,7 +510,9 @@ export class HaTabsSubpageDataTable extends LitElement {
|
||||
)}
|
||||
></ha-icon-button>
|
||||
<span slot="title"
|
||||
>${localize("ui.components.subpage-data-table.filters")}</span
|
||||
>${localize("ui.components.subpage-data-table.filters", {
|
||||
number: this.data.length,
|
||||
})}</span
|
||||
>
|
||||
${this.filters
|
||||
? html`<ha-icon-button
|
||||
@@ -523,8 +526,17 @@ export class HaTabsSubpageDataTable extends LitElement {
|
||||
: nothing}
|
||||
</ha-dialog-header>
|
||||
<div class="filter-dialog-content">
|
||||
<slot name="filter-pane"></slot></div
|
||||
></ha-dialog>`
|
||||
<slot name="filter-pane"></slot>
|
||||
</div>
|
||||
<div slot="primaryAction">
|
||||
<ha-button @click=${this._toggleFilters}>
|
||||
${this.hass.localize(
|
||||
"ui.components.subpage-data-table.show_results",
|
||||
{ number: this.data.length }
|
||||
)}
|
||||
</ha-button>
|
||||
</div>
|
||||
</ha-dialog>`
|
||||
: nothing}
|
||||
`;
|
||||
}
|
||||
@@ -779,7 +791,6 @@ export class HaTabsSubpageDataTable extends LitElement {
|
||||
}
|
||||
|
||||
ha-dialog {
|
||||
--dialog-z-index: 100;
|
||||
--mdc-dialog-min-width: calc(
|
||||
100vw - env(safe-area-inset-right) - env(safe-area-inset-left)
|
||||
);
|
||||
@@ -794,7 +805,7 @@ export class HaTabsSubpageDataTable extends LitElement {
|
||||
}
|
||||
|
||||
.filter-dialog-content {
|
||||
height: calc(100vh - 1px - var(--header-height));
|
||||
height: calc(100vh - 1px - 61px - var(--header-height));
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
@@ -1,14 +1,6 @@
|
||||
import { mdiDelete, mdiPlus } from "@mdi/js";
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
PropertyValues,
|
||||
nothing,
|
||||
} from "lit";
|
||||
import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import type { HASSDomEvent } from "../../../common/dom/fire_event";
|
||||
import { LocalizeFunc } from "../../../common/translations/localize";
|
||||
@@ -59,27 +51,24 @@ export class HaConfigApplicationCredentials extends LitElement {
|
||||
title: localize(
|
||||
"ui.panel.config.application_credentials.picker.headers.name"
|
||||
),
|
||||
sortable: true,
|
||||
direction: "asc",
|
||||
grows: true,
|
||||
template: (entry) => html`${entry.name}`,
|
||||
},
|
||||
client_id: {
|
||||
title: localize(
|
||||
"ui.panel.config.application_credentials.picker.headers.client_id"
|
||||
),
|
||||
width: "30%",
|
||||
direction: "asc",
|
||||
hidden: narrow,
|
||||
template: (entry) => html`${entry.client_id}`,
|
||||
},
|
||||
application: {
|
||||
localizedDomain: {
|
||||
title: localize(
|
||||
"ui.panel.config.application_credentials.picker.headers.application"
|
||||
),
|
||||
sortable: true,
|
||||
width: "30%",
|
||||
direction: "asc",
|
||||
template: (entry) => html`${domainToName(localize, entry.domain)}`,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -87,6 +76,14 @@ export class HaConfigApplicationCredentials extends LitElement {
|
||||
}
|
||||
);
|
||||
|
||||
private _getApplicationCredentials = memoizeOne(
|
||||
(applicationCredentials: ApplicationCredential[], localize: LocalizeFunc) =>
|
||||
applicationCredentials.map((credential) => ({
|
||||
...credential,
|
||||
localizedDomain: domainToName(localize, credential.domain),
|
||||
}))
|
||||
);
|
||||
|
||||
protected firstUpdated(changedProperties: PropertyValues) {
|
||||
super.firstUpdated(changedProperties);
|
||||
this._loadTranslations();
|
||||
@@ -102,56 +99,40 @@ export class HaConfigApplicationCredentials extends LitElement {
|
||||
backPath="/config"
|
||||
.tabs=${configSections.devices}
|
||||
.columns=${this._columns(this.narrow, this.hass.localize)}
|
||||
.data=${this._applicationCredentials}
|
||||
.data=${this._getApplicationCredentials(
|
||||
this._applicationCredentials,
|
||||
this.hass.localize
|
||||
)}
|
||||
hasFab
|
||||
selectable
|
||||
.selected=${this._selected.length}
|
||||
@selection-changed=${this._handleSelectionChanged}
|
||||
>
|
||||
${this._selected.length
|
||||
? html`
|
||||
<div
|
||||
class=${classMap({
|
||||
"header-toolbar": this.narrow,
|
||||
"table-header": !this.narrow,
|
||||
})}
|
||||
slot="header"
|
||||
>
|
||||
<p class="selected-txt">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.application_credentials.picker.selected",
|
||||
{ number: this._selected.length }
|
||||
<div class="header-btns" slot="selection-bar">
|
||||
${!this.narrow
|
||||
? html`
|
||||
<mwc-button @click=${this._removeSelected} class="warning"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.application_credentials.picker.remove_selected.button"
|
||||
)}</mwc-button
|
||||
>
|
||||
`
|
||||
: html`
|
||||
<ha-icon-button
|
||||
class="warning"
|
||||
id="remove-btn"
|
||||
@click=${this._removeSelected}
|
||||
.path=${mdiDelete}
|
||||
.label=${this.hass.localize("ui.common.remove")}
|
||||
></ha-icon-button>
|
||||
<ha-help-tooltip
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.application_credentials.picker.remove_selected.button"
|
||||
)}
|
||||
</p>
|
||||
<div class="header-btns">
|
||||
${!this.narrow
|
||||
? html`
|
||||
<mwc-button
|
||||
@click=${this._removeSelected}
|
||||
class="warning"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.application_credentials.picker.remove_selected.button"
|
||||
)}</mwc-button
|
||||
>
|
||||
`
|
||||
: html`
|
||||
<ha-icon-button
|
||||
class="warning"
|
||||
id="remove-btn"
|
||||
@click=${this._removeSelected}
|
||||
.path=${mdiDelete}
|
||||
.label=${this.hass.localize("ui.common.remove")}
|
||||
></ha-icon-button>
|
||||
<ha-help-tooltip
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.application_credentials.picker.remove_selected.button"
|
||||
)}
|
||||
>
|
||||
</ha-help-tooltip>
|
||||
`}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
>
|
||||
</ha-help-tooltip>
|
||||
`}
|
||||
</div>
|
||||
<ha-fab
|
||||
slot="fab"
|
||||
.label=${this.hass.localize(
|
||||
|
@@ -42,8 +42,14 @@ import type { HaYamlEditor } from "../../../../components/ha-yaml-editor";
|
||||
import { ACTION_ICONS, YAML_ONLY_ACTION_TYPES } from "../../../../data/action";
|
||||
import { AutomationClipboard } from "../../../../data/automation";
|
||||
import { validateConfig } from "../../../../data/config";
|
||||
import { fullEntitiesContext } from "../../../../data/context";
|
||||
import {
|
||||
floorsContext,
|
||||
fullEntitiesContext,
|
||||
labelsContext,
|
||||
} from "../../../../data/context";
|
||||
import { EntityRegistryEntry } from "../../../../data/entity_registry";
|
||||
import { FloorRegistryEntry } from "../../../../data/floor_registry";
|
||||
import { LabelRegistryEntry } from "../../../../data/label_registry";
|
||||
import {
|
||||
Action,
|
||||
NonConditionAction,
|
||||
@@ -146,6 +152,14 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
@consume({ context: fullEntitiesContext, subscribe: true })
|
||||
_entityReg!: EntityRegistryEntry[];
|
||||
|
||||
@state()
|
||||
@consume({ context: labelsContext, subscribe: true })
|
||||
_labelReg!: LabelRegistryEntry[];
|
||||
|
||||
@state()
|
||||
@consume({ context: floorsContext, subscribe: true })
|
||||
_floorReg!: FloorRegistryEntry[];
|
||||
|
||||
@state() private _warnings?: string[];
|
||||
|
||||
@state() private _uiModeAvailable = true;
|
||||
@@ -210,7 +224,13 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
.path=${ACTION_ICONS[type!]}
|
||||
></ha-svg-icon>`}
|
||||
${capitalizeFirstLetter(
|
||||
describeAction(this.hass, this._entityReg, this.action)
|
||||
describeAction(
|
||||
this.hass,
|
||||
this._entityReg,
|
||||
this._labelReg,
|
||||
this._floorReg,
|
||||
this.action
|
||||
)
|
||||
)}
|
||||
</h3>
|
||||
|
||||
@@ -573,7 +593,15 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
),
|
||||
inputType: "string",
|
||||
placeholder: capitalizeFirstLetter(
|
||||
describeAction(this.hass, this._entityReg, this.action, undefined, true)
|
||||
describeAction(
|
||||
this.hass,
|
||||
this._entityReg,
|
||||
this._labelReg,
|
||||
this._floorReg,
|
||||
this.action,
|
||||
undefined,
|
||||
true
|
||||
)
|
||||
),
|
||||
defaultValue: this.action.alias,
|
||||
confirmText: this.hass.localize("ui.common.submit"),
|
||||
|
@@ -105,6 +105,10 @@ import { showCategoryRegistryDetailDialog } from "../category/show-dialog-catego
|
||||
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;
|
||||
@@ -196,6 +200,7 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
labels: (labels || []).map(
|
||||
(lbl) => labelReg!.find((label) => label.label_id === lbl)!
|
||||
),
|
||||
selectable: entityRegEntry !== undefined,
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -422,6 +427,14 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
const labelsInOverflow =
|
||||
(this._sizeController.value && this._sizeController.value < 700) ||
|
||||
(!this._sizeController.value && this.hass.dockedSidebar === "docked");
|
||||
const automations = this._automations(
|
||||
this.automations,
|
||||
this._entityReg,
|
||||
this.hass.areas,
|
||||
this._categories,
|
||||
this._labels,
|
||||
this._filteredAutomations
|
||||
);
|
||||
return html`
|
||||
<hass-tabs-subpage-data-table
|
||||
.hass=${this.hass}
|
||||
@@ -432,13 +445,23 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
id="entity_id"
|
||||
.route=${this.route}
|
||||
.tabs=${configSections.automations}
|
||||
.searchLabel=${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.search",
|
||||
{ number: automations.length }
|
||||
)}
|
||||
selectable
|
||||
.selected=${this._selected.length}
|
||||
@selection-changed=${this._handleSelectionChanged}
|
||||
hasFilters
|
||||
.filters=${
|
||||
Object.values(this._filters).filter((filter) => filter.value?.length)
|
||||
.length
|
||||
Object.values(this._filters).filter((filter) =>
|
||||
Array.isArray(filter.value)
|
||||
? filter.value.length
|
||||
: filter.value &&
|
||||
Object.values(filter.value).some((val) =>
|
||||
Array.isArray(val) ? val.length : val
|
||||
)
|
||||
).length
|
||||
}
|
||||
.columns=${this._columns(
|
||||
this.narrow,
|
||||
@@ -446,14 +469,7 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
this.hass.locale
|
||||
)}
|
||||
initialGroupColumn="category"
|
||||
.data=${this._automations(
|
||||
this.automations,
|
||||
this._entityReg,
|
||||
this.hass.areas,
|
||||
this._categories,
|
||||
this._labels,
|
||||
this._filteredAutomations
|
||||
)}
|
||||
.data=${automations}
|
||||
.empty=${!this.automations.length}
|
||||
@row-click=${this._handleRowClicked}
|
||||
.noDataText=${this.hass.localize(
|
||||
@@ -1101,7 +1117,20 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
})
|
||||
);
|
||||
});
|
||||
await Promise.all(promises);
|
||||
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 _handleBulkLabel(ev) {
|
||||
@@ -1124,7 +1153,20 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
})
|
||||
);
|
||||
});
|
||||
await Promise.all(promises);
|
||||
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 _handleBulkEnable() {
|
||||
@@ -1132,7 +1174,20 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
this._selected.forEach((entityId) => {
|
||||
promises.push(turnOnOffEntity(this.hass, entityId, true));
|
||||
});
|
||||
await Promise.all(promises);
|
||||
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 _handleBulkDisable() {
|
||||
@@ -1140,7 +1195,20 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
this._selected.forEach((entityId) => {
|
||||
promises.push(turnOnOffEntity(this.hass, entityId, false));
|
||||
});
|
||||
await Promise.all(promises);
|
||||
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 _bulkCreateCategory() {
|
||||
@@ -1174,6 +1242,7 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
height: 100%;
|
||||
}
|
||||
hass-tabs-subpage-data-table {
|
||||
--data-table-row-height: 60px;
|
||||
|
@@ -69,6 +69,11 @@ 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 {
|
||||
hasRejectedItems,
|
||||
rejectedItems,
|
||||
} from "../../../common/util/promise-all-settled-results";
|
||||
import { showAlertDialog } from "../../lovelace/custom-card-helpers";
|
||||
|
||||
interface DeviceRowData extends DeviceRegistryEntry {
|
||||
device?: DeviceRowData;
|
||||
@@ -591,7 +596,8 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
|
||||
.tabs=${configSections.devices}
|
||||
.route=${this.route}
|
||||
.searchLabel=${this.hass.localize(
|
||||
"ui.panel.config.devices.picker.search"
|
||||
"ui.panel.config.devices.picker.search",
|
||||
{ number: devicesOutput.length }
|
||||
)}
|
||||
.columns=${this._columns(this.hass.localize, this.narrow)}
|
||||
.data=${devicesOutput}
|
||||
@@ -600,8 +606,13 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
|
||||
@selection-changed=${this._handleSelectionChanged}
|
||||
.filter=${this._filter}
|
||||
hasFilters
|
||||
.filters=${Object.values(this._filters).filter(
|
||||
(filter) => filter.value?.length
|
||||
.filters=${Object.values(this._filters).filter((filter) =>
|
||||
Array.isArray(filter.value)
|
||||
? filter.value.length
|
||||
: filter.value &&
|
||||
Object.values(filter.value).some((val) =>
|
||||
Array.isArray(val) ? val.length : val
|
||||
)
|
||||
).length}
|
||||
@clear-filter=${this._clearFilter}
|
||||
@search-changed=${this._handleSearchChange}
|
||||
@@ -818,7 +829,20 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
|
||||
})
|
||||
);
|
||||
});
|
||||
await Promise.all(promises);
|
||||
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 _bulkCreateLabel() {
|
||||
|
@@ -27,7 +27,6 @@ import {
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { until } from "lit/directives/until";
|
||||
import memoize from "memoize-one";
|
||||
import { computeCssColor } from "../../../common/color/compute-color";
|
||||
import type { HASSDomEvent } from "../../../common/dom/fire_event";
|
||||
@@ -67,7 +66,6 @@ import {
|
||||
removeEntityRegistryEntry,
|
||||
updateEntityRegistryEntry,
|
||||
} from "../../../data/entity_registry";
|
||||
import { entryIcon } from "../../../data/icons";
|
||||
import {
|
||||
LabelRegistryEntry,
|
||||
createLabelRegistryEntry,
|
||||
@@ -92,6 +90,10 @@ import {
|
||||
EntitySources,
|
||||
fetchEntitySourcesWithCache,
|
||||
} from "../../../data/entity_sources";
|
||||
import {
|
||||
hasRejectedItems,
|
||||
rejectedItems,
|
||||
} from "../../../common/util/promise-all-settled-results";
|
||||
|
||||
export interface StateEntity
|
||||
extends Omit<EntityRegistryEntry, "id" | "unique_id"> {
|
||||
@@ -207,21 +209,19 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
type: "icon",
|
||||
template: (entry) =>
|
||||
entry.icon
|
||||
? html`
|
||||
<ha-state-icon
|
||||
title=${ifDefined(entry.entity?.state)}
|
||||
slot="item-icon"
|
||||
.hass=${this.hass}
|
||||
.stateObj=${entry.entity}
|
||||
></ha-state-icon>
|
||||
`
|
||||
: html`
|
||||
<ha-icon
|
||||
icon=${until(
|
||||
entryIcon(this.hass, entry as EntityRegistryEntry)
|
||||
)}
|
||||
></ha-icon>
|
||||
`,
|
||||
? html`<ha-icon .icon=${entry.icon}></ha-icon>`
|
||||
: entry.entity
|
||||
? html`
|
||||
<ha-state-icon
|
||||
title=${ifDefined(entry.entity?.state)}
|
||||
slot="item-icon"
|
||||
.hass=${this.hass}
|
||||
.stateObj=${entry.entity}
|
||||
></ha-state-icon>
|
||||
`
|
||||
: html`<ha-domain-icon
|
||||
.domain=${computeDomain(entry.entity_id)}
|
||||
></ha-domain-icon>`,
|
||||
},
|
||||
name: {
|
||||
main: true,
|
||||
@@ -577,12 +577,19 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
)}
|
||||
.data=${filteredEntities}
|
||||
.searchLabel=${this.hass.localize(
|
||||
"ui.panel.config.entities.picker.search"
|
||||
"ui.panel.config.entities.picker.search",
|
||||
{ number: filteredEntities.length }
|
||||
)}
|
||||
hasFilters
|
||||
.filters=${
|
||||
Object.values(this._filters).filter((filter) => filter.value?.length)
|
||||
.length
|
||||
Object.values(this._filters).filter((filter) =>
|
||||
Array.isArray(filter.value)
|
||||
? filter.value.length
|
||||
: filter.value &&
|
||||
Object.values(filter.value).some((val) =>
|
||||
Array.isArray(val) ? val.length : val
|
||||
)
|
||||
).length
|
||||
}
|
||||
.filter=${this._filter}
|
||||
selectable
|
||||
@@ -954,19 +961,41 @@ ${
|
||||
confirm: async () => {
|
||||
let require_restart = false;
|
||||
let reload_delay = 0;
|
||||
await Promise.all(
|
||||
const result = await Promise.allSettled(
|
||||
this._selected.map(async (entity) => {
|
||||
const result = await updateEntityRegistryEntry(this.hass, entity, {
|
||||
disabled_by: null,
|
||||
});
|
||||
if (result.require_restart) {
|
||||
const updateResult = await updateEntityRegistryEntry(
|
||||
this.hass,
|
||||
entity,
|
||||
{
|
||||
disabled_by: null,
|
||||
}
|
||||
);
|
||||
if (updateResult.require_restart) {
|
||||
require_restart = true;
|
||||
}
|
||||
if (result.reload_delay) {
|
||||
reload_delay = Math.max(reload_delay, result.reload_delay);
|
||||
if (updateResult.reload_delay) {
|
||||
reload_delay = Math.max(reload_delay, updateResult.reload_delay);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
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
|
||||
>`,
|
||||
});
|
||||
}
|
||||
|
||||
this._clearSelection();
|
||||
// If restart is required by any entity, show a dialog.
|
||||
// Otherwise, show a dialog explaining that some patience is needed
|
||||
@@ -1065,7 +1094,20 @@ ${
|
||||
})
|
||||
);
|
||||
});
|
||||
await Promise.all(promises);
|
||||
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 _bulkCreateLabel() {
|
||||
|
@@ -35,7 +35,11 @@ import { customElement, property, state } from "lit/decorators";
|
||||
import { isComponentLoaded } from "../../common/config/is_component_loaded";
|
||||
import { listenMediaQuery } from "../../common/dom/media_query";
|
||||
import { CloudStatus, fetchCloudStatus } from "../../data/cloud";
|
||||
import { fullEntitiesContext } from "../../data/context";
|
||||
import {
|
||||
floorsContext,
|
||||
fullEntitiesContext,
|
||||
labelsContext,
|
||||
} from "../../data/context";
|
||||
import {
|
||||
entityRegistryByEntityId,
|
||||
entityRegistryById,
|
||||
@@ -45,6 +49,8 @@ import { HassRouterPage, RouterOptions } from "../../layouts/hass-router-page";
|
||||
import { PageNavigation } from "../../layouts/hass-tabs-subpage";
|
||||
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
|
||||
import { HomeAssistant, Route } from "../../types";
|
||||
import { subscribeLabelRegistry } from "../../data/label_registry";
|
||||
import { subscribeFloorRegistry } from "../../data/floor_registry";
|
||||
|
||||
declare global {
|
||||
// for fire event
|
||||
@@ -379,11 +385,27 @@ class HaPanelConfig extends SubscribeMixin(HassRouterPage) {
|
||||
initialValue: [],
|
||||
});
|
||||
|
||||
private _labelsContext = new ContextProvider(this, {
|
||||
context: labelsContext,
|
||||
initialValue: [],
|
||||
});
|
||||
|
||||
private _floorsContext = new ContextProvider(this, {
|
||||
context: floorsContext,
|
||||
initialValue: [],
|
||||
});
|
||||
|
||||
public hassSubscribe(): UnsubscribeFunc[] {
|
||||
return [
|
||||
subscribeEntityRegistry(this.hass.connection!, (entities) => {
|
||||
this._entitiesContext.setValue(entities);
|
||||
}),
|
||||
subscribeLabelRegistry(this.hass.connection!, (labels) => {
|
||||
this._labelsContext.setValue(labels);
|
||||
}),
|
||||
subscribeFloorRegistry(this.hass.connection!, (floors) => {
|
||||
this._floorsContext.setValue(floors);
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
|
@@ -32,6 +32,10 @@ import {
|
||||
LocalizeKeys,
|
||||
} from "../../../common/translations/localize";
|
||||
import { extractSearchParam } from "../../../common/url/search-params";
|
||||
import {
|
||||
hasRejectedItems,
|
||||
rejectedItems,
|
||||
} from "../../../common/util/promise-all-settled-results";
|
||||
import {
|
||||
DataTableColumnContainer,
|
||||
RowClickedEvent,
|
||||
@@ -486,6 +490,16 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
||||
const labelsInOverflow =
|
||||
(this._sizeController.value && this._sizeController.value < 700) ||
|
||||
(!this._sizeController.value && this.hass.dockedSidebar === "docked");
|
||||
const helpers = this._getItems(
|
||||
this.hass.localize,
|
||||
this._stateItems,
|
||||
this._entityEntries,
|
||||
this._configEntries,
|
||||
this._entityReg,
|
||||
this._categories,
|
||||
this._labels,
|
||||
this._filteredStateItems
|
||||
);
|
||||
return html`
|
||||
<hass-tabs-subpage-data-table
|
||||
.hass=${this.hass}
|
||||
@@ -493,24 +507,24 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
||||
back-path="/config"
|
||||
.route=${this.route}
|
||||
.tabs=${configSections.devices}
|
||||
.searchLabel=${this.hass.localize(
|
||||
"ui.panel.config.helpers.picker.search",
|
||||
{ number: helpers.length }
|
||||
)}
|
||||
selectable
|
||||
.selected=${this._selected.length}
|
||||
@selection-changed=${this._handleSelectionChanged}
|
||||
hasFilters
|
||||
.filters=${Object.values(this._filters).filter(
|
||||
(filter) => filter.value?.length
|
||||
.filters=${Object.values(this._filters).filter((filter) =>
|
||||
Array.isArray(filter.value)
|
||||
? filter.value.length
|
||||
: filter.value &&
|
||||
Object.values(filter.value).some((val) =>
|
||||
Array.isArray(val) ? val.length : val
|
||||
)
|
||||
).length}
|
||||
.columns=${this._columns(this.narrow, this.hass.localize)}
|
||||
.data=${this._getItems(
|
||||
this.hass.localize,
|
||||
this._stateItems,
|
||||
this._entityEntries,
|
||||
this._configEntries,
|
||||
this._entityReg,
|
||||
this._categories,
|
||||
this._labels,
|
||||
this._filteredStateItems
|
||||
)}
|
||||
.data=${helpers}
|
||||
initialGroupColumn="category"
|
||||
.activeFilters=${this._activeFilters}
|
||||
@clear-filter=${this._clearFilter}
|
||||
@@ -791,7 +805,20 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
||||
})
|
||||
);
|
||||
});
|
||||
await Promise.all(promises);
|
||||
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 _handleBulkLabel(ev) {
|
||||
@@ -814,7 +841,20 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
||||
})
|
||||
);
|
||||
});
|
||||
await Promise.all(promises);
|
||||
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 _handleSelectionChanged(
|
||||
|
@@ -95,6 +95,10 @@ import { showAssignCategoryDialog } from "../category/show-dialog-assign-categor
|
||||
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;
|
||||
@@ -178,6 +182,7 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
|
||||
labels: (labels || []).map(
|
||||
(lbl) => labelReg!.find((label) => label.label_id === lbl)!
|
||||
),
|
||||
selectable: entityRegEntry !== undefined,
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -425,6 +430,14 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
|
||||
const labelsInOverflow =
|
||||
(this._sizeController.value && this._sizeController.value < 700) ||
|
||||
(!this._sizeController.value && this.hass.dockedSidebar === "docked");
|
||||
const scenes = this._scenes(
|
||||
this.scenes,
|
||||
this._entityReg,
|
||||
this.hass.areas,
|
||||
this._categories,
|
||||
this._labels,
|
||||
this._filteredScenes
|
||||
);
|
||||
return html`
|
||||
<hass-tabs-subpage-data-table
|
||||
.hass=${this.hass}
|
||||
@@ -432,24 +445,26 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
|
||||
back-path="/config"
|
||||
.route=${this.route}
|
||||
.tabs=${configSections.automations}
|
||||
.searchLabel=${this.hass.localize(
|
||||
"ui.panel.config.scene.picker.search",
|
||||
{ number: scenes.length }
|
||||
)}
|
||||
selectable
|
||||
.selected=${this._selected.length}
|
||||
@selection-changed=${this._handleSelectionChanged}
|
||||
hasFilters
|
||||
.filters=${Object.values(this._filters).filter(
|
||||
(filter) => filter.value?.length
|
||||
.filters=${Object.values(this._filters).filter((filter) =>
|
||||
Array.isArray(filter.value)
|
||||
? filter.value.length
|
||||
: filter.value &&
|
||||
Object.values(filter.value).some((val) =>
|
||||
Array.isArray(val) ? val.length : val
|
||||
)
|
||||
).length}
|
||||
.columns=${this._columns(this.narrow, this.hass.localize)}
|
||||
id="entity_id"
|
||||
initialGroupColumn="category"
|
||||
.data=${this._scenes(
|
||||
this.scenes,
|
||||
this._entityReg,
|
||||
this.hass.areas,
|
||||
this._categories,
|
||||
this._labels,
|
||||
this._filteredScenes
|
||||
)}
|
||||
.data=${scenes}
|
||||
.empty=${!this.scenes.length}
|
||||
.activeFilters=${this._activeFilters}
|
||||
.noDataText=${this.hass.localize(
|
||||
@@ -788,7 +803,20 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
|
||||
})
|
||||
);
|
||||
});
|
||||
await Promise.all(promises);
|
||||
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 _handleBulkLabel(ev) {
|
||||
@@ -811,7 +839,20 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
|
||||
})
|
||||
);
|
||||
});
|
||||
await Promise.all(promises);
|
||||
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 _editCategory(scene: any) {
|
||||
@@ -940,6 +981,7 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
height: 100%;
|
||||
}
|
||||
hass-tabs-subpage-data-table {
|
||||
--data-table-row-height: 60px;
|
||||
|
@@ -97,6 +97,10 @@ import { showAssignCategoryDialog } from "../category/show-dialog-assign-categor
|
||||
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;
|
||||
@@ -185,6 +189,7 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
|
||||
labels: (labels || []).map(
|
||||
(lbl) => labelReg!.find((label) => label.label_id === lbl)!
|
||||
),
|
||||
selectable: entityRegEntry !== undefined,
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -437,6 +442,14 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
|
||||
const labelsInOverflow =
|
||||
(this._sizeController.value && this._sizeController.value < 700) ||
|
||||
(!this._sizeController.value && this.hass.dockedSidebar === "docked");
|
||||
const scripts = this._scripts(
|
||||
this.scripts,
|
||||
this._entityReg,
|
||||
this.hass.areas,
|
||||
this._categories,
|
||||
this._labels,
|
||||
this._filteredScripts
|
||||
);
|
||||
return html`
|
||||
<hass-tabs-subpage-data-table
|
||||
.hass=${this.hass}
|
||||
@@ -444,27 +457,29 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
|
||||
back-path="/config"
|
||||
.route=${this.route}
|
||||
.tabs=${configSections.automations}
|
||||
.searchLabel=${this.hass.localize(
|
||||
"ui.panel.config.script.picker.search",
|
||||
{ number: scripts.length }
|
||||
)}
|
||||
hasFilters
|
||||
initialGroupColumn="category"
|
||||
selectable
|
||||
.selected=${this._selected.length}
|
||||
@selection-changed=${this._handleSelectionChanged}
|
||||
.filters=${Object.values(this._filters).filter(
|
||||
(filter) => filter.value?.length
|
||||
.filters=${Object.values(this._filters).filter((filter) =>
|
||||
Array.isArray(filter.value)
|
||||
? filter.value.length
|
||||
: filter.value &&
|
||||
Object.values(filter.value).some((val) =>
|
||||
Array.isArray(val) ? val.length : val
|
||||
)
|
||||
).length}
|
||||
.columns=${this._columns(
|
||||
this.narrow,
|
||||
this.hass.localize,
|
||||
this.hass.locale
|
||||
)}
|
||||
.data=${this._scripts(
|
||||
this.scripts,
|
||||
this._entityReg,
|
||||
this.hass.areas,
|
||||
this._categories,
|
||||
this._labels,
|
||||
this._filteredScripts
|
||||
)}
|
||||
.data=${scripts}
|
||||
.empty=${!this.scripts.length}
|
||||
.activeFilters=${this._activeFilters}
|
||||
id="entity_id"
|
||||
@@ -857,7 +872,20 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
|
||||
})
|
||||
);
|
||||
});
|
||||
await Promise.all(promises);
|
||||
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 _handleBulkLabel(ev) {
|
||||
@@ -880,7 +908,20 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
|
||||
})
|
||||
);
|
||||
});
|
||||
await Promise.all(promises);
|
||||
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 _handleRowClicked(ev: HASSDomEvent<RowClickedEvent>) {
|
||||
@@ -1056,6 +1097,7 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
height: 100%;
|
||||
}
|
||||
hass-tabs-subpage-data-table {
|
||||
--data-table-row-height: 60px;
|
||||
|
@@ -496,7 +496,10 @@ export class VoiceAssistantsExpose extends LitElement {
|
||||
)}
|
||||
.data=${filteredEntities}
|
||||
.searchLabel=${this.hass.localize(
|
||||
"ui.panel.config.entities.picker.search"
|
||||
"ui.panel.config.entities.picker.search",
|
||||
{
|
||||
number: filteredEntities.length,
|
||||
}
|
||||
)}
|
||||
.filter=${this._filter}
|
||||
selectable
|
||||
|
@@ -52,7 +52,6 @@ export class HuiButtonsBase extends LitElement {
|
||||
.stateObj=${stateObj}
|
||||
.overrideIcon=${entityConf.icon}
|
||||
.overrideImage=${entityConf.image}
|
||||
class=${name ? "" : "no-text"}
|
||||
.stateColor=${true}
|
||||
slot="icon"
|
||||
></state-badge>
|
||||
@@ -92,27 +91,8 @@ export class HuiButtonsBase extends LitElement {
|
||||
color: var(--secondary-text-color);
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin-left: -4px;
|
||||
margin-inline-start: -4px;
|
||||
margin-inline-end: initial;
|
||||
margin-top: -2px;
|
||||
}
|
||||
state-badge.no-text {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
margin-left: -3px;
|
||||
margin-inline-start: -3px;
|
||||
margin-inline-end: initial;
|
||||
margin-top: -3px;
|
||||
}
|
||||
ha-assist-chip state-badge {
|
||||
margin-right: -4px;
|
||||
margin-inline-end: -4px;
|
||||
margin-inline-start: initial;
|
||||
--mdc-icon-size: 18px;
|
||||
}
|
||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||
.ha-scrollbar {
|
||||
flex-wrap: nowrap;
|
||||
|
@@ -501,6 +501,7 @@
|
||||
},
|
||||
"subpage-data-table": {
|
||||
"filters": "Filters",
|
||||
"show_results": "show {number} results",
|
||||
"clear_filter": "Clear filter",
|
||||
"close_filter": "Close filters",
|
||||
"exit_selection_mode": "Exit selection mode",
|
||||
@@ -1866,6 +1867,7 @@
|
||||
"editor": {
|
||||
"confirm_unsaved": "You have unsaved changes. Are you sure you want to leave?"
|
||||
},
|
||||
"multiselect": { "failed": "Failed to update {number} items." },
|
||||
"learn_more": "Learn more"
|
||||
},
|
||||
"updates": {
|
||||
@@ -2270,7 +2272,8 @@
|
||||
"category": "Category"
|
||||
},
|
||||
"create_helper": "Create helper",
|
||||
"no_helpers": "Looks like you don't have any helpers yet!"
|
||||
"no_helpers": "Looks like you don't have any helpers yet!",
|
||||
"search": "Search {number} helpers"
|
||||
},
|
||||
"dialog": {
|
||||
"create": "Create",
|
||||
@@ -2684,6 +2687,7 @@
|
||||
"assign_category": "Assign category",
|
||||
"no_category_support": "You can't assign an category to this automation",
|
||||
"no_category_entity_reg": "To assign an category to an automation it needs to have a unique ID.",
|
||||
"search": "Search {number} automations",
|
||||
"headers": {
|
||||
"toggle": "Enable/disable",
|
||||
"name": "Name",
|
||||
@@ -3241,7 +3245,9 @@
|
||||
"target_template": "templated {name}",
|
||||
"target_unknown_entity": "unknown entity",
|
||||
"target_unknown_device": "unknown device",
|
||||
"target_unknown_area": "unknown area"
|
||||
"target_unknown_area": "unknown area",
|
||||
"target_unknown_floor": "unknown floor",
|
||||
"target_unknown_label": "unknown label"
|
||||
}
|
||||
},
|
||||
"play_media": {
|
||||
@@ -3575,7 +3581,8 @@
|
||||
"delete": "[%key:ui::common::delete%]",
|
||||
"duplicate": "[%key:ui::common::duplicate%]",
|
||||
"empty_header": "Create your first script",
|
||||
"empty_text": "A script is a sequence of actions that can be run from a dashboard, an automation, or be triggered by voice. For example, a ''Wake-up routine''' script that gradually turns on the light in the bedroom and opens the blinds after a delay."
|
||||
"empty_text": "A script is a sequence of actions that can be run from a dashboard, an automation, or be triggered by voice. For example, a ''Wake-up routine''' script that gradually turns on the light in the bedroom and opens the blinds after a delay.",
|
||||
"search": "Search {number} scripts"
|
||||
},
|
||||
"dialog_new": {
|
||||
"header": "Create script",
|
||||
@@ -3683,7 +3690,8 @@
|
||||
"no_category_support": "You can't assign an category to this scene",
|
||||
"no_category_entity_reg": "To assign an category to an scene it needs to have a unique ID.",
|
||||
"empty_header": "Create your first scene",
|
||||
"empty_text": "Scenes capture entities' states, so you can re-experience the same scene later on. For example, a ''Watching TV'' scene that dims the living room lights, sets a warm white color and turns on the TV."
|
||||
"empty_text": "Scenes capture entities' states, so you can re-experience the same scene later on. For example, a ''Watching TV'' scene that dims the living room lights, sets a warm white color and turns on the TV.",
|
||||
"search": "Search {number} scenes"
|
||||
},
|
||||
"editor": {
|
||||
"default_name": "New scene",
|
||||
@@ -4008,7 +4016,7 @@
|
||||
"confirm_delete": "Are you sure you want to delete this device?",
|
||||
"confirm_delete_integration": "Are you sure you want to remove this device from {integration}?",
|
||||
"picker": {
|
||||
"search": "Search devices",
|
||||
"search": "Search {number} devices",
|
||||
"state": "State"
|
||||
}
|
||||
},
|
||||
@@ -4019,7 +4027,7 @@
|
||||
"header": "Entities",
|
||||
"introduction": "Home Assistant keeps a registry of every entity it has ever seen that can be uniquely identified. Each of these entities will have an entity ID assigned which will be reserved for just this entity.",
|
||||
"introduction2": "Use the entity registry to override the name, change the entity ID or remove the entry from Home Assistant.",
|
||||
"search": "Search entities",
|
||||
"search": "Search {number} entities",
|
||||
"unnamed_entity": "Unnamed entity",
|
||||
"status": {
|
||||
"restored": "Restored",
|
||||
|
Reference in New Issue
Block a user