Compare commits

..

18 Commits

Author SHA1 Message Date
Bram Kragten
e96aca90fe 20240404.0 (#20414) 2024-04-04 16:23:24 +02:00
Bram Kragten
0580a31961 Bumped version to 20240404.0 2024-04-04 16:16:30 +02:00
Bram Kragten
5c42c5130c Add count of items (#20410)
* Add count of items

* Adjust layout, correct filter count
2024-04-04 16:15:13 +02:00
Bram Kragten
72d1e37a23 Fix integration filter search (#20408) 2024-04-04 13:26:26 +02:00
Bram Kragten
61c9072a08 Fix icons in entity settings (#20405) 2024-04-04 13:00:14 +02:00
Bram Kragten
08b25f9c2a Add floor and label support to describe action (#20403) 2024-04-04 13:00:05 +02:00
Samuel Schultze
1a03b49700 Fix calendar range selection (#20394)
fix: calendar range selector
2024-04-04 12:59:54 +02:00
Paul Bottein
2d4a8e2e45 Fix search input outlined background color and margin (#20407) 2024-04-04 12:53:03 +02:00
Bram Kragten
8486377604 Fix z-index create category dialog (#20406) 2024-04-04 10:51:04 +00:00
Bram Kragten
4326519a3f 20240403.1 (#20380) 2024-04-03 16:58:47 +02:00
Bram Kragten
962b30adb9 20240403.0 (#20370) 2024-04-03 14:50:16 +02:00
Bram Kragten
29eb73176a 20240402.2 (#20348) 2024-04-02 23:34:17 +02:00
Bram Kragten
4f1cf1110f 20240402.1 (#20326) 2024-04-02 16:41:12 +02:00
Bram Kragten
d3bf0da289 20240402.0 (#20314) 2024-04-02 11:44:05 +02:00
Paul Bottein
fd06d434f2 20240329.1 (#20280) 2024-03-29 21:25:10 +01:00
Paul Bottein
d24d29e42f 20240329.0 (#20277) 2024-03-29 19:10:34 +01:00
Paul Bottein
e02a47a16a 20240328.0 (#20250) 2024-03-28 16:49:01 +01:00
Bram Kragten
795c16a941 20240327.0 (#20210) 2024-03-27 17:52:08 +01:00
21 changed files with 376 additions and 145 deletions

View File

@@ -161,6 +161,7 @@ const createWebpackConfig = ({
resolve: { resolve: {
extensions: [".ts", ".js", ".json"], extensions: [".ts", ".js", ".json"],
alias: { alias: {
"lit/static-html$": "lit/static-html.js",
"lit/decorators$": "lit/decorators.js", "lit/decorators$": "lit/decorators.js",
"lit/directive$": "lit/directive.js", "lit/directive$": "lit/directive.js",
"lit/directives/until$": "lit/directives/until.js", "lit/directives/until$": "lit/directives/until.js",

View File

@@ -136,7 +136,7 @@ export class DemoAutomationDescribeAction extends LitElement {
<div class="action"> <div class="action">
<span> <span>
${this._action ${this._action
? describeAction(this.hass, [], this._action) ? describeAction(this.hass, [], [], [], this._action)
: "<invalid YAML>"} : "<invalid YAML>"}
</span> </span>
<ha-yaml-editor <ha-yaml-editor
@@ -149,7 +149,7 @@ export class DemoAutomationDescribeAction extends LitElement {
${ACTIONS.map( ${ACTIONS.map(
(conf) => html` (conf) => html`
<div class="action"> <div class="action">
<span>${describeAction(this.hass, [], conf as any)}</span> <span>${describeAction(this.hass, [], [], [], conf as any)}</span>
<pre>${dump(conf)}</pre> <pre>${dump(conf)}</pre>
</div> </div>
` `

View File

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

View File

@@ -11,10 +11,10 @@ import {
} from "../common/datetime/localize_date"; } from "../common/datetime/localize_date";
import { mainWindow } from "../common/dom/get_main_window"; 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({ const CustomDateRangePicker = Vue.extend({
mixins: [DateRangePicker], mixins: [DateRangePicker],
methods: { methods: {
// Set the current date to the left picker instead of the right picker because the right is hidden
selectMonthDate() { selectMonthDate() {
const dt: Date = this.end || new Date(); const dt: Date = this.end || new Date();
// @ts-ignore // @ts-ignore
@@ -23,6 +23,33 @@ const CustomDateRangePicker = Vue.extend({
month: dt.getMonth() + 1, 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);
},
}, },
}); });

View File

@@ -1,4 +1,3 @@
import { SelectedDetail } from "@material/mwc-list";
import { mdiFilterVariantRemove } from "@mdi/js"; import { mdiFilterVariantRemove } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
@@ -56,11 +55,7 @@ export class HaFilterIntegrations extends LitElement {
@value-changed=${this._handleSearchChange} @value-changed=${this._handleSearchChange}
> >
</search-input-outlined> </search-input-outlined>
<mwc-list <mwc-list class="ha-scrollbar" @click=${this._handleItemClick}>
@selected=${this._integrationsSelected}
multi
class="ha-scrollbar"
>
${repeat( ${repeat(
this._integrations(this._manifests, this._filter, this.value), this._integrations(this._manifests, this._filter, this.value),
(i) => i.domain, (i) => i.domain,
@@ -131,34 +126,21 @@ export class HaFilterIntegrations extends LitElement {
) )
); );
private async _integrationsSelected( private _handleItemClick(ev) {
ev: CustomEvent<SelectedDetail<Set<number>>> const listItem = ev.target.closest("ha-check-list-item");
) { const value = listItem?.value;
const integrations = this._integrations( if (!value) {
this._manifests!,
this._filter,
this.value
);
if (!ev.detail.index.size) {
fireEvent(this, "data-table-filter-changed", {
value: [],
items: undefined,
});
this.value = [];
return; return;
} }
if (this.value?.includes(value)) {
const value: string[] = []; this.value = this.value?.filter((val) => val !== value);
} else {
for (const index of ev.detail.index) { this.value = [...(this.value || []), value];
const domain = integrations[index].domain;
value.push(domain);
} }
this.value = value; listItem.selected = this.value?.includes(value);
fireEvent(this, "data-table-filter-changed", { fireEvent(this, "data-table-filter-changed", {
value, value: this.value,
items: undefined, items: undefined,
}); });
} }

View 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;
}
}

View File

@@ -2,9 +2,13 @@ import { MdOutlinedTextField } from "@material/web/textfield/outlined-text-field
import "element-internals-polyfill"; import "element-internals-polyfill";
import { css } from "lit"; import { css } from "lit";
import { customElement } from "lit/decorators"; import { customElement } from "lit/decorators";
import { literal } from "lit/static-html";
import "./ha-outlined-field";
@customElement("ha-outlined-text-field") @customElement("ha-outlined-text-field")
export class HaOutlinedTextField extends MdOutlinedTextField { export class HaOutlinedTextField extends MdOutlinedTextField {
protected readonly fieldTag = literal`ha-outlined-field`;
static override styles = [ static override styles = [
...super.styles, ...super.styles,
css` css`
@@ -25,16 +29,10 @@ export class HaOutlinedTextField extends MdOutlinedTextField {
--md-outlined-field-container-shape-end-end: 10px; --md-outlined-field-container-shape-end-end: 10px;
--md-outlined-field-container-shape-end-start: 10px; --md-outlined-field-container-shape-end-start: 10px;
--md-outlined-field-focus-outline-width: 1px; --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); --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);
border-start-start-radius: var(--_container-shape-start-start);
border-start-end-radius: var(--_container-shape-start-end);
border-end-end-radius: var(--_container-shape-end-end);
border-end-start-radius: var(--_container-shape-end-start);
}
.input { .input {
font-family: Roboto, sans-serif; font-family: Roboto, sans-serif;
} }

View File

@@ -97,7 +97,7 @@ class SearchInputOutlined extends LitElement {
ha-outlined-text-field { ha-outlined-text-field {
display: block; display: block;
width: 100%; 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-svg-icon,
ha-icon-button { ha-icon-button {

View File

@@ -1,3 +1,4 @@
import { consume } from "@lit-labs/context";
import { import {
mdiAlertCircle, mdiAlertCircle,
mdiCircle, mdiCircle,
@@ -6,14 +7,13 @@ import {
mdiProgressWrench, mdiProgressWrench,
mdiRecordCircleOutline, mdiRecordCircleOutline,
} from "@mdi/js"; } from "@mdi/js";
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { import {
css,
CSSResultGroup, CSSResultGroup,
html,
LitElement, LitElement,
PropertyValues, PropertyValues,
TemplateResult, TemplateResult,
css,
html,
nothing, nothing,
} from "lit"; } from "lit";
import { customElement, property, state } from "lit/decorators"; 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 { fireEvent } from "../../common/dom/fire_event";
import { toggleAttribute } from "../../common/dom/toggle_attribute"; import { toggleAttribute } from "../../common/dom/toggle_attribute";
import { import {
EntityRegistryEntry, floorsContext,
subscribeEntityRegistry, fullEntitiesContext,
} from "../../data/entity_registry"; 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 { LogbookEntry } from "../../data/logbook";
import { import {
ChooseAction, ChooseAction,
ChooseActionChoice, ChooseActionChoice,
getActionType,
IfAction, IfAction,
ParallelAction, ParallelAction,
RepeatAction, RepeatAction,
getActionType,
} from "../../data/script"; } from "../../data/script";
import { describeAction } from "../../data/script_i18n"; import { describeAction } from "../../data/script_i18n";
import { import {
ActionTraceStep, ActionTraceStep,
AutomationTraceExtended, AutomationTraceExtended,
ChooseActionTraceStep, ChooseActionTraceStep,
getDataFromPath,
IfActionTraceStep, IfActionTraceStep,
isTriggerPath,
TriggerTraceStep, TriggerTraceStep,
getDataFromPath,
isTriggerPath,
} from "../../data/trace"; } from "../../data/trace";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
import "./ha-timeline"; import "./ha-timeline";
@@ -200,6 +204,8 @@ class ActionRenderer {
constructor( constructor(
private hass: HomeAssistant, private hass: HomeAssistant,
private entityReg: EntityRegistryEntry[], private entityReg: EntityRegistryEntry[],
private labelReg: LabelRegistryEntry[],
private floorReg: FloorRegistryEntry[],
private entries: TemplateResult[], private entries: TemplateResult[],
private trace: AutomationTraceExtended, private trace: AutomationTraceExtended,
private logbookRenderer: LogbookRenderer, private logbookRenderer: LogbookRenderer,
@@ -310,7 +316,14 @@ class ActionRenderer {
this._renderEntry( this._renderEntry(
path, path,
describeAction(this.hass, this.entityReg, data, actionType), describeAction(
this.hass,
this.entityReg,
this.labelReg,
this.floorReg,
data,
actionType
),
undefined, undefined,
data.enabled === false data.enabled === false
); );
@@ -475,7 +488,13 @@ class ActionRenderer {
const name = const name =
repeatConfig.alias || repeatConfig.alias ||
describeAction(this.hass, this.entityReg, repeatConfig); describeAction(
this.hass,
this.entityReg,
this.labelReg,
this.floorReg,
repeatConfig
);
this._renderEntry(repeatPath, name, undefined, disabled); this._renderEntry(repeatPath, name, undefined, disabled);
@@ -631,15 +650,17 @@ export class HaAutomationTracer extends LitElement {
@property({ type: Boolean }) public allowPick = false; @property({ type: Boolean }) public allowPick = false;
@state() private _entityReg: EntityRegistryEntry[] = []; @state()
@consume({ context: fullEntitiesContext, subscribe: true })
_entityReg!: EntityRegistryEntry[];
public hassSubscribe(): UnsubscribeFunc[] { @state()
return [ @consume({ context: labelsContext, subscribe: true })
subscribeEntityRegistry(this.hass.connection!, (entities) => { _labelReg!: LabelRegistryEntry[];
this._entityReg = entities;
}), @state()
]; @consume({ context: floorsContext, subscribe: true })
} _floorReg!: FloorRegistryEntry[];
protected render() { protected render() {
if (!this.trace) { if (!this.trace) {
@@ -657,6 +678,8 @@ export class HaAutomationTracer extends LitElement {
const actionRenderer = new ActionRenderer( const actionRenderer = new ActionRenderer(
this.hass, this.hass,
this._entityReg, this._entityReg,
this._labelReg,
this._floorReg,
entries, entries,
this.trace, this.trace,
logbookRenderer, logbookRenderer,

View File

@@ -2,6 +2,8 @@ import { createContext } from "@lit-labs/context";
import { HassConfig } from "home-assistant-js-websocket"; import { HassConfig } from "home-assistant-js-websocket";
import { HomeAssistant } from "../types"; import { HomeAssistant } from "../types";
import { EntityRegistryEntry } from "./entity_registry"; import { EntityRegistryEntry } from "./entity_registry";
import { FloorRegistryEntry } from "./floor_registry";
import { LabelRegistryEntry } from "./label_registry";
export const connectionContext = export const connectionContext =
createContext<HomeAssistant["connection"]>("connection"); createContext<HomeAssistant["connection"]>("connection");
@@ -25,3 +27,7 @@ export const panelsContext = createContext<HomeAssistant["panels"]>("panels");
export const fullEntitiesContext = export const fullEntitiesContext =
createContext<EntityRegistryEntry[]>("extendedEntities"); createContext<EntityRegistryEntry[]>("extendedEntities");
export const floorsContext = createContext<FloorRegistryEntry[]>("floors");
export const labelsContext = createContext<LabelRegistryEntry[]>("labels");

View File

@@ -14,7 +14,9 @@ import {
computeEntityRegistryName, computeEntityRegistryName,
entityRegistryById, entityRegistryById,
} from "./entity_registry"; } from "./entity_registry";
import { FloorRegistryEntry } from "./floor_registry";
import { domainToName } from "./integration"; import { domainToName } from "./integration";
import { LabelRegistryEntry } from "./label_registry";
import { import {
ActionType, ActionType,
ActionTypes, ActionTypes,
@@ -40,6 +42,8 @@ const actionTranslationBaseKey =
export const describeAction = <T extends ActionType>( export const describeAction = <T extends ActionType>(
hass: HomeAssistant, hass: HomeAssistant,
entityRegistry: EntityRegistryEntry[], entityRegistry: EntityRegistryEntry[],
labelRegistry: LabelRegistryEntry[],
floorRegistry: FloorRegistryEntry[],
action: ActionTypes[T], action: ActionTypes[T],
actionType?: T, actionType?: T,
ignoreAlias = false ignoreAlias = false
@@ -48,6 +52,8 @@ export const describeAction = <T extends ActionType>(
return tryDescribeAction( return tryDescribeAction(
hass, hass,
entityRegistry, entityRegistry,
labelRegistry,
floorRegistry,
action, action,
actionType, actionType,
ignoreAlias ignoreAlias
@@ -66,6 +72,8 @@ export const describeAction = <T extends ActionType>(
const tryDescribeAction = <T extends ActionType>( const tryDescribeAction = <T extends ActionType>(
hass: HomeAssistant, hass: HomeAssistant,
entityRegistry: EntityRegistryEntry[], entityRegistry: EntityRegistryEntry[],
labelRegistry: LabelRegistryEntry[],
floorRegistry: FloorRegistryEntry[],
action: ActionTypes[T], action: ActionTypes[T],
actionType?: T, actionType?: T,
ignoreAlias = false ignoreAlias = false
@@ -82,10 +90,12 @@ const tryDescribeAction = <T extends ActionType>(
const targets: string[] = []; const targets: string[] = [];
if (config.target) { if (config.target) {
for (const [key, label] of Object.entries({ for (const [key, name] of Object.entries({
area_id: "areas", area_id: "areas",
device_id: "devices", device_id: "devices",
entity_id: "entities", entity_id: "entities",
floor_id: "floors",
label_id: "labels",
})) { })) {
if (!(key in config.target)) { if (!(key in config.target)) {
continue; continue;
@@ -99,7 +109,7 @@ const tryDescribeAction = <T extends ActionType>(
targets.push( targets.push(
hass.localize( hass.localize(
`${actionTranslationBaseKey}.service.description.target_template`, `${actionTranslationBaseKey}.service.description.target_template`,
{ name: label } { name }
) )
); );
break; 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 { } else {
targets.push(targetThing); targets.push(targetThing);
} }

View File

@@ -496,8 +496,9 @@ export class HaTabsSubpageDataTable extends LitElement {
${this.showFilters && !showPane ${this.showFilters && !showPane
? html`<ha-dialog ? html`<ha-dialog
open 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-dialog-header slot="heading">
<ha-icon-button <ha-icon-button
@@ -509,7 +510,9 @@ export class HaTabsSubpageDataTable extends LitElement {
)} )}
></ha-icon-button> ></ha-icon-button>
<span slot="title" <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 ${this.filters
? html`<ha-icon-button ? html`<ha-icon-button
@@ -523,8 +526,17 @@ export class HaTabsSubpageDataTable extends LitElement {
: nothing} : nothing}
</ha-dialog-header> </ha-dialog-header>
<div class="filter-dialog-content"> <div class="filter-dialog-content">
<slot name="filter-pane"></slot></div <slot name="filter-pane"></slot>
></ha-dialog>` </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} : nothing}
`; `;
} }
@@ -779,7 +791,6 @@ export class HaTabsSubpageDataTable extends LitElement {
} }
ha-dialog { ha-dialog {
--dialog-z-index: 100;
--mdc-dialog-min-width: calc( --mdc-dialog-min-width: calc(
100vw - env(safe-area-inset-right) - env(safe-area-inset-left) 100vw - env(safe-area-inset-right) - env(safe-area-inset-left)
); );
@@ -794,7 +805,7 @@ export class HaTabsSubpageDataTable extends LitElement {
} }
.filter-dialog-content { .filter-dialog-content {
height: calc(100vh - 1px - var(--header-height)); height: calc(100vh - 1px - 61px - var(--header-height));
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }

View File

@@ -42,8 +42,14 @@ import type { HaYamlEditor } from "../../../../components/ha-yaml-editor";
import { ACTION_ICONS, YAML_ONLY_ACTION_TYPES } from "../../../../data/action"; import { ACTION_ICONS, YAML_ONLY_ACTION_TYPES } from "../../../../data/action";
import { AutomationClipboard } from "../../../../data/automation"; import { AutomationClipboard } from "../../../../data/automation";
import { validateConfig } from "../../../../data/config"; import { validateConfig } from "../../../../data/config";
import { fullEntitiesContext } from "../../../../data/context"; import {
floorsContext,
fullEntitiesContext,
labelsContext,
} from "../../../../data/context";
import { EntityRegistryEntry } from "../../../../data/entity_registry"; import { EntityRegistryEntry } from "../../../../data/entity_registry";
import { FloorRegistryEntry } from "../../../../data/floor_registry";
import { LabelRegistryEntry } from "../../../../data/label_registry";
import { import {
Action, Action,
NonConditionAction, NonConditionAction,
@@ -146,6 +152,14 @@ export default class HaAutomationActionRow extends LitElement {
@consume({ context: fullEntitiesContext, subscribe: true }) @consume({ context: fullEntitiesContext, subscribe: true })
_entityReg!: EntityRegistryEntry[]; _entityReg!: EntityRegistryEntry[];
@state()
@consume({ context: labelsContext, subscribe: true })
_labelReg!: LabelRegistryEntry[];
@state()
@consume({ context: floorsContext, subscribe: true })
_floorReg!: FloorRegistryEntry[];
@state() private _warnings?: string[]; @state() private _warnings?: string[];
@state() private _uiModeAvailable = true; @state() private _uiModeAvailable = true;
@@ -210,7 +224,13 @@ export default class HaAutomationActionRow extends LitElement {
.path=${ACTION_ICONS[type!]} .path=${ACTION_ICONS[type!]}
></ha-svg-icon>`} ></ha-svg-icon>`}
${capitalizeFirstLetter( ${capitalizeFirstLetter(
describeAction(this.hass, this._entityReg, this.action) describeAction(
this.hass,
this._entityReg,
this._labelReg,
this._floorReg,
this.action
)
)} )}
</h3> </h3>
@@ -573,7 +593,15 @@ export default class HaAutomationActionRow extends LitElement {
), ),
inputType: "string", inputType: "string",
placeholder: capitalizeFirstLetter( 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, defaultValue: this.action.alias,
confirmText: this.hass.localize("ui.common.submit"), confirmText: this.hass.localize("ui.common.submit"),

View File

@@ -422,6 +422,14 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
const labelsInOverflow = const labelsInOverflow =
(this._sizeController.value && this._sizeController.value < 700) || (this._sizeController.value && this._sizeController.value < 700) ||
(!this._sizeController.value && this.hass.dockedSidebar === "docked"); (!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` return html`
<hass-tabs-subpage-data-table <hass-tabs-subpage-data-table
.hass=${this.hass} .hass=${this.hass}
@@ -432,13 +440,23 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
id="entity_id" id="entity_id"
.route=${this.route} .route=${this.route}
.tabs=${configSections.automations} .tabs=${configSections.automations}
.searchLabel=${this.hass.localize(
"ui.panel.config.automation.picker.search",
{ number: automations.length }
)}
selectable selectable
.selected=${this._selected.length} .selected=${this._selected.length}
@selection-changed=${this._handleSelectionChanged} @selection-changed=${this._handleSelectionChanged}
hasFilters hasFilters
.filters=${ .filters=${
Object.values(this._filters).filter((filter) => filter.value?.length) Object.values(this._filters).filter((filter) =>
.length 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( .columns=${this._columns(
this.narrow, this.narrow,
@@ -446,14 +464,7 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
this.hass.locale this.hass.locale
)} )}
initialGroupColumn="category" initialGroupColumn="category"
.data=${this._automations( .data=${automations}
this.automations,
this._entityReg,
this.hass.areas,
this._categories,
this._labels,
this._filteredAutomations
)}
.empty=${!this.automations.length} .empty=${!this.automations.length}
@row-click=${this._handleRowClicked} @row-click=${this._handleRowClicked}
.noDataText=${this.hass.localize( .noDataText=${this.hass.localize(

View File

@@ -591,7 +591,8 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
.tabs=${configSections.devices} .tabs=${configSections.devices}
.route=${this.route} .route=${this.route}
.searchLabel=${this.hass.localize( .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)} .columns=${this._columns(this.hass.localize, this.narrow)}
.data=${devicesOutput} .data=${devicesOutput}
@@ -600,8 +601,13 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
@selection-changed=${this._handleSelectionChanged} @selection-changed=${this._handleSelectionChanged}
.filter=${this._filter} .filter=${this._filter}
hasFilters hasFilters
.filters=${Object.values(this._filters).filter( .filters=${Object.values(this._filters).filter((filter) =>
(filter) => filter.value?.length Array.isArray(filter.value)
? filter.value.length
: filter.value &&
Object.values(filter.value).some((val) =>
Array.isArray(val) ? val.length : val
)
).length} ).length}
@clear-filter=${this._clearFilter} @clear-filter=${this._clearFilter}
@search-changed=${this._handleSearchChange} @search-changed=${this._handleSearchChange}

View File

@@ -27,7 +27,6 @@ import {
import { customElement, property, query, state } from "lit/decorators"; import { customElement, property, query, state } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined"; import { ifDefined } from "lit/directives/if-defined";
import { styleMap } from "lit/directives/style-map"; import { styleMap } from "lit/directives/style-map";
import { until } from "lit/directives/until";
import memoize from "memoize-one"; import memoize from "memoize-one";
import { computeCssColor } from "../../../common/color/compute-color"; import { computeCssColor } from "../../../common/color/compute-color";
import type { HASSDomEvent } from "../../../common/dom/fire_event"; import type { HASSDomEvent } from "../../../common/dom/fire_event";
@@ -67,7 +66,6 @@ import {
removeEntityRegistryEntry, removeEntityRegistryEntry,
updateEntityRegistryEntry, updateEntityRegistryEntry,
} from "../../../data/entity_registry"; } from "../../../data/entity_registry";
import { entryIcon } from "../../../data/icons";
import { import {
LabelRegistryEntry, LabelRegistryEntry,
createLabelRegistryEntry, createLabelRegistryEntry,
@@ -207,21 +205,19 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
type: "icon", type: "icon",
template: (entry) => template: (entry) =>
entry.icon entry.icon
? html` ? html`<ha-icon .icon=${entry.icon}></ha-icon>`
<ha-state-icon : entry.entity
title=${ifDefined(entry.entity?.state)} ? html`
slot="item-icon" <ha-state-icon
.hass=${this.hass} title=${ifDefined(entry.entity?.state)}
.stateObj=${entry.entity} slot="item-icon"
></ha-state-icon> .hass=${this.hass}
` .stateObj=${entry.entity}
: html` ></ha-state-icon>
<ha-icon `
icon=${until( : html`<ha-domain-icon
entryIcon(this.hass, entry as EntityRegistryEntry) .domain=${computeDomain(entry.entity_id)}
)} ></ha-domain-icon>`,
></ha-icon>
`,
}, },
name: { name: {
main: true, main: true,
@@ -577,12 +573,19 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
)} )}
.data=${filteredEntities} .data=${filteredEntities}
.searchLabel=${this.hass.localize( .searchLabel=${this.hass.localize(
"ui.panel.config.entities.picker.search" "ui.panel.config.entities.picker.search",
{ number: filteredEntities.length }
)} )}
hasFilters hasFilters
.filters=${ .filters=${
Object.values(this._filters).filter((filter) => filter.value?.length) Object.values(this._filters).filter((filter) =>
.length 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} .filter=${this._filter}
selectable selectable

View File

@@ -35,7 +35,11 @@ import { customElement, property, state } from "lit/decorators";
import { isComponentLoaded } from "../../common/config/is_component_loaded"; import { isComponentLoaded } from "../../common/config/is_component_loaded";
import { listenMediaQuery } from "../../common/dom/media_query"; import { listenMediaQuery } from "../../common/dom/media_query";
import { CloudStatus, fetchCloudStatus } from "../../data/cloud"; import { CloudStatus, fetchCloudStatus } from "../../data/cloud";
import { fullEntitiesContext } from "../../data/context"; import {
floorsContext,
fullEntitiesContext,
labelsContext,
} from "../../data/context";
import { import {
entityRegistryByEntityId, entityRegistryByEntityId,
entityRegistryById, entityRegistryById,
@@ -45,6 +49,8 @@ import { HassRouterPage, RouterOptions } from "../../layouts/hass-router-page";
import { PageNavigation } from "../../layouts/hass-tabs-subpage"; import { PageNavigation } from "../../layouts/hass-tabs-subpage";
import { SubscribeMixin } from "../../mixins/subscribe-mixin"; import { SubscribeMixin } from "../../mixins/subscribe-mixin";
import { HomeAssistant, Route } from "../../types"; import { HomeAssistant, Route } from "../../types";
import { subscribeLabelRegistry } from "../../data/label_registry";
import { subscribeFloorRegistry } from "../../data/floor_registry";
declare global { declare global {
// for fire event // for fire event
@@ -379,11 +385,27 @@ class HaPanelConfig extends SubscribeMixin(HassRouterPage) {
initialValue: [], initialValue: [],
}); });
private _labelsContext = new ContextProvider(this, {
context: labelsContext,
initialValue: [],
});
private _floorsContext = new ContextProvider(this, {
context: floorsContext,
initialValue: [],
});
public hassSubscribe(): UnsubscribeFunc[] { public hassSubscribe(): UnsubscribeFunc[] {
return [ return [
subscribeEntityRegistry(this.hass.connection!, (entities) => { subscribeEntityRegistry(this.hass.connection!, (entities) => {
this._entitiesContext.setValue(entities); this._entitiesContext.setValue(entities);
}), }),
subscribeLabelRegistry(this.hass.connection!, (labels) => {
this._labelsContext.setValue(labels);
}),
subscribeFloorRegistry(this.hass.connection!, (floors) => {
this._floorsContext.setValue(floors);
}),
]; ];
} }

View File

@@ -486,6 +486,16 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
const labelsInOverflow = const labelsInOverflow =
(this._sizeController.value && this._sizeController.value < 700) || (this._sizeController.value && this._sizeController.value < 700) ||
(!this._sizeController.value && this.hass.dockedSidebar === "docked"); (!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` return html`
<hass-tabs-subpage-data-table <hass-tabs-subpage-data-table
.hass=${this.hass} .hass=${this.hass}
@@ -493,24 +503,24 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
back-path="/config" back-path="/config"
.route=${this.route} .route=${this.route}
.tabs=${configSections.devices} .tabs=${configSections.devices}
.searchLabel=${this.hass.localize(
"ui.panel.config.helpers.picker.search",
{ number: helpers.length }
)}
selectable selectable
.selected=${this._selected.length} .selected=${this._selected.length}
@selection-changed=${this._handleSelectionChanged} @selection-changed=${this._handleSelectionChanged}
hasFilters hasFilters
.filters=${Object.values(this._filters).filter( .filters=${Object.values(this._filters).filter((filter) =>
(filter) => filter.value?.length Array.isArray(filter.value)
? filter.value.length
: filter.value &&
Object.values(filter.value).some((val) =>
Array.isArray(val) ? val.length : val
)
).length} ).length}
.columns=${this._columns(this.narrow, this.hass.localize)} .columns=${this._columns(this.narrow, this.hass.localize)}
.data=${this._getItems( .data=${helpers}
this.hass.localize,
this._stateItems,
this._entityEntries,
this._configEntries,
this._entityReg,
this._categories,
this._labels,
this._filteredStateItems
)}
initialGroupColumn="category" initialGroupColumn="category"
.activeFilters=${this._activeFilters} .activeFilters=${this._activeFilters}
@clear-filter=${this._clearFilter} @clear-filter=${this._clearFilter}

View File

@@ -425,6 +425,14 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
const labelsInOverflow = const labelsInOverflow =
(this._sizeController.value && this._sizeController.value < 700) || (this._sizeController.value && this._sizeController.value < 700) ||
(!this._sizeController.value && this.hass.dockedSidebar === "docked"); (!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` return html`
<hass-tabs-subpage-data-table <hass-tabs-subpage-data-table
.hass=${this.hass} .hass=${this.hass}
@@ -432,24 +440,26 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
back-path="/config" back-path="/config"
.route=${this.route} .route=${this.route}
.tabs=${configSections.automations} .tabs=${configSections.automations}
.searchLabel=${this.hass.localize(
"ui.panel.config.scene.picker.search",
{ number: scenes.length }
)}
selectable selectable
.selected=${this._selected.length} .selected=${this._selected.length}
@selection-changed=${this._handleSelectionChanged} @selection-changed=${this._handleSelectionChanged}
hasFilters hasFilters
.filters=${Object.values(this._filters).filter( .filters=${Object.values(this._filters).filter((filter) =>
(filter) => filter.value?.length Array.isArray(filter.value)
? filter.value.length
: filter.value &&
Object.values(filter.value).some((val) =>
Array.isArray(val) ? val.length : val
)
).length} ).length}
.columns=${this._columns(this.narrow, this.hass.localize)} .columns=${this._columns(this.narrow, this.hass.localize)}
id="entity_id" id="entity_id"
initialGroupColumn="category" initialGroupColumn="category"
.data=${this._scenes( .data=${scenes}
this.scenes,
this._entityReg,
this.hass.areas,
this._categories,
this._labels,
this._filteredScenes
)}
.empty=${!this.scenes.length} .empty=${!this.scenes.length}
.activeFilters=${this._activeFilters} .activeFilters=${this._activeFilters}
.noDataText=${this.hass.localize( .noDataText=${this.hass.localize(

View File

@@ -437,6 +437,14 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
const labelsInOverflow = const labelsInOverflow =
(this._sizeController.value && this._sizeController.value < 700) || (this._sizeController.value && this._sizeController.value < 700) ||
(!this._sizeController.value && this.hass.dockedSidebar === "docked"); (!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` return html`
<hass-tabs-subpage-data-table <hass-tabs-subpage-data-table
.hass=${this.hass} .hass=${this.hass}
@@ -444,27 +452,29 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
back-path="/config" back-path="/config"
.route=${this.route} .route=${this.route}
.tabs=${configSections.automations} .tabs=${configSections.automations}
.searchLabel=${this.hass.localize(
"ui.panel.config.script.picker.search",
{ number: scripts.length }
)}
hasFilters hasFilters
initialGroupColumn="category" initialGroupColumn="category"
selectable selectable
.selected=${this._selected.length} .selected=${this._selected.length}
@selection-changed=${this._handleSelectionChanged} @selection-changed=${this._handleSelectionChanged}
.filters=${Object.values(this._filters).filter( .filters=${Object.values(this._filters).filter((filter) =>
(filter) => filter.value?.length Array.isArray(filter.value)
? filter.value.length
: filter.value &&
Object.values(filter.value).some((val) =>
Array.isArray(val) ? val.length : val
)
).length} ).length}
.columns=${this._columns( .columns=${this._columns(
this.narrow, this.narrow,
this.hass.localize, this.hass.localize,
this.hass.locale this.hass.locale
)} )}
.data=${this._scripts( .data=${scripts}
this.scripts,
this._entityReg,
this.hass.areas,
this._categories,
this._labels,
this._filteredScripts
)}
.empty=${!this.scripts.length} .empty=${!this.scripts.length}
.activeFilters=${this._activeFilters} .activeFilters=${this._activeFilters}
id="entity_id" id="entity_id"

View File

@@ -501,6 +501,7 @@
}, },
"subpage-data-table": { "subpage-data-table": {
"filters": "Filters", "filters": "Filters",
"show_results": "show {number} results",
"clear_filter": "Clear filter", "clear_filter": "Clear filter",
"close_filter": "Close filters", "close_filter": "Close filters",
"exit_selection_mode": "Exit selection mode", "exit_selection_mode": "Exit selection mode",
@@ -2270,7 +2271,8 @@
"category": "Category" "category": "Category"
}, },
"create_helper": "Create helper", "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": { "dialog": {
"create": "Create", "create": "Create",
@@ -2684,6 +2686,7 @@
"assign_category": "Assign category", "assign_category": "Assign category",
"no_category_support": "You can't assign an category to this automation", "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.", "no_category_entity_reg": "To assign an category to an automation it needs to have a unique ID.",
"search": "Search {number} automations",
"headers": { "headers": {
"toggle": "Enable/disable", "toggle": "Enable/disable",
"name": "Name", "name": "Name",
@@ -3241,7 +3244,9 @@
"target_template": "templated {name}", "target_template": "templated {name}",
"target_unknown_entity": "unknown entity", "target_unknown_entity": "unknown entity",
"target_unknown_device": "unknown device", "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": { "play_media": {
@@ -3575,7 +3580,8 @@
"delete": "[%key:ui::common::delete%]", "delete": "[%key:ui::common::delete%]",
"duplicate": "[%key:ui::common::duplicate%]", "duplicate": "[%key:ui::common::duplicate%]",
"empty_header": "Create your first script", "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": { "dialog_new": {
"header": "Create script", "header": "Create script",
@@ -3683,7 +3689,8 @@
"no_category_support": "You can't assign an category to this scene", "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.", "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_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": { "editor": {
"default_name": "New scene", "default_name": "New scene",
@@ -4008,7 +4015,7 @@
"confirm_delete": "Are you sure you want to delete this device?", "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}?", "confirm_delete_integration": "Are you sure you want to remove this device from {integration}?",
"picker": { "picker": {
"search": "Search devices", "search": "Search {number} devices",
"state": "State" "state": "State"
} }
}, },
@@ -4019,7 +4026,7 @@
"header": "Entities", "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.", "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.", "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", "unnamed_entity": "Unnamed entity",
"status": { "status": {
"restored": "Restored", "restored": "Restored",