Compare commits

...
Author SHA1 Message Date
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
29cd46eb9e Update dependency minify-literals to v2.2.0 (#53812)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-26 20:24:56 +02:00
Paul BotteinandGitHub 92b3081896 Clarify the replaced device state in pickers and target rows (#53807) 2026-08-26 19:48:52 +02:00
7 changed files with 154 additions and 91 deletions
+1 -1
View File
@@ -203,7 +203,7 @@
"lodash.merge": "4.6.2",
"lodash.template": "4.18.1",
"map-stream": "0.0.7",
"minify-literals": "2.1.0",
"minify-literals": "2.2.0",
"pinst": "3.0.0",
"prettier": "3.9.6",
"rspack-manifest-plugin": "5.2.2",
+52 -43
View File
@@ -1,4 +1,4 @@
import { mdiAlertOutline } from "@mdi/js";
import { mdiSwapHorizontal } from "@mdi/js";
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
import type { HassEntity } from "home-assistant-js-websocket";
import { css, html, LitElement, nothing, type PropertyValues } from "lit";
@@ -22,6 +22,7 @@ import {
type DeviceRegistryEntry,
} from "../../data/device/device_registry";
import type { HaEntityPickerEntityFilterFunc } from "../../data/entity/entity";
import { domainToName } from "../../data/integration";
import type { HomeAssistant } from "../../types";
import { brandsUrl } from "../../util/brands-url";
import "../ha-alert";
@@ -250,26 +251,26 @@ export class HaDevicePicker extends LitElement {
};
private _valueRenderer = memoizeOne(
(
configEntriesLookup: Record<string, ConfigEntry>,
replacementName: string | undefined
) =>
(configEntriesLookup: Record<string, ConfigEntry>, isReplaced: boolean) =>
(value: string) => {
const deviceId = value;
const device = this.hass.devices[deviceId];
if (!device) {
// When the device was replaced and a replacement is available, show
// the replacement device's name. Otherwise fall back to the normal
// "not found" display of the raw id.
if (replacementName) {
// The removed device has no name left to show, so say what happened
// to it instead. The alert below names the replacements. Without a
// replacement, fall back to the normal "not found" display.
if (isReplaced) {
return html`
<ha-svg-icon
slot="start"
style="color: var(--warning-color)"
.path=${mdiAlertOutline}
.path=${mdiSwapHorizontal}
></ha-svg-icon>
<span slot="headline">${replacementName}</span>
<span slot="headline"
>${this.hass.localize(
"ui.components.device-picker.device_replaced"
)}</span
>
`;
}
return html`<span slot="headline">${deviceId}</span>`;
@@ -406,24 +407,15 @@ export class HaDevicePicker extends LitElement {
// Only treat the value as "replaced" when there is an available
// replacement device; otherwise fall back to normal "not found" behavior.
const canReplace = !!replacement?.candidates.length;
const replacementName = canReplace
? computeDeviceName(
this.hass.devices[
replacement!.primaryId &&
replacement!.candidates.includes(replacement!.primaryId)
? replacement!.primaryId
: replacement!.candidates[0]
]
)
: undefined;
const valueRenderer = this._valueRenderer(
this._configEntryLookup,
replacementName
canReplace
);
return html`
<ha-generic-picker
.noUnknownState=${canReplace}
.hass=${this.hass}
.autofocus=${this.autofocus}
.disabled=${this.disabled}
@@ -443,14 +435,9 @@ export class HaDevicePicker extends LitElement {
.hideClearIcon=${this.hideClearIcon}
.valueRenderer=${valueRenderer}
.searchKeys=${deviceComboBoxKeys}
.unknownItemText=${
replacement?.candidates.length
? this.hass.localize(
"ui.components.device-picker.device_replaced_count",
{ count: replacement.candidates.length }
)
: this.hass.localize("ui.components.device-picker.unknown")
}
.unknownItemText=${this.hass.localize(
"ui.components.device-picker.unknown"
)}
@value-changed=${this._valueChanged}
>
</ha-generic-picker>
@@ -464,30 +451,52 @@ export class HaDevicePicker extends LitElement {
}) {
const { candidates } = replacement;
const replacementName =
candidates.length === 1
? computeDeviceName(this.hass.devices[candidates[0]])
: undefined;
// The split devices all inherit the composite's name, so the integration is
// what tells them apart.
const replacementDevice =
candidates.length === 1 ? this.hass.devices[candidates[0]] : undefined;
const replacementName = replacementDevice
? computeDeviceName(replacementDevice)
: undefined;
const replacementDomain = replacementDevice?.primary_config_entry
? this._configEntryLookup[replacementDevice.primary_config_entry]?.domain
: undefined;
return html`
<ha-alert alert-type="warning">
${
replacementName
replacementName && replacementDomain
? this.hass.localize(
"ui.components.device-picker.device_replaced_by_one",
{ device: replacementName }
)
: this.hass.localize(
"ui.components.device-picker.device_replaced_by_multiple",
{ count: candidates.length }
"ui.components.device-picker.device_replaced_by_one_integration",
{
device: replacementName,
integration: domainToName(
this.hass.localize,
replacementDomain
),
}
)
: replacementName
? this.hass.localize(
"ui.components.device-picker.device_replaced_by_one",
{ device: replacementName }
)
: this.hass.localize(
"ui.components.device-picker.device_replaced_by_multiple",
{ count: candidates.length }
)
}
<ha-button
slot="action"
appearance="plain"
variant="warning"
@click=${this._handleReplace}
>
${this.hass.localize("ui.components.device-picker.replace_device")}
${
candidates.length === 1
? this.hass.localize("ui.components.device-picker.replace_update")
: this.hass.localize("ui.components.device-picker.replace_choose")
}
</ha-button>
</ha-alert>
`;
+10 -1
View File
@@ -111,6 +111,11 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
@property({ type: Boolean, attribute: "no-sort" }) public noSort = false;
// Skip the "unknown value" highlight and note for a value that is not in the
// list but that the value renderer presents on its own.
@property({ type: Boolean, attribute: "no-unknown-state" })
public noUnknownState = false;
@query(".container") private _containerElement?: HTMLDivElement;
@query("ha-picker-combo-box") private _comboBox?: HaPickerComboBox;
@@ -148,7 +153,10 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
private _unsubscribeTinyKeys?: () => void;
protected willUpdate(changedProperties: PropertyValues<this>) {
if (changedProperties.has("value")) {
if (
changedProperties.has("value") ||
changedProperties.has("noUnknownState")
) {
this._setUnknownValue();
}
}
@@ -287,6 +295,7 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
private _setUnknownValue = () => {
const items = this.getItems();
if (
this.noUnknownState ||
this.allowCustomValue ||
this.value === undefined ||
this.value === null ||
@@ -7,6 +7,7 @@ import {
mdiHome,
mdiLabel,
mdiMinusBox,
mdiSwapHorizontal,
mdiTextureBox,
} from "@mdi/js";
import type { HassEntity } from "home-assistant-js-websocket";
@@ -177,36 +178,51 @@ export class HaTargetPickerItemRow extends LitElement {
referrerpolicy="no-referrer"
src=${this._iconImg}
/>`
: fallbackIconPath
? html`<ha-svg-icon .path=${fallbackIconPath}></ha-svg-icon>`
: this.type === "entity"
? html`
<ha-state-icon
.stateObj=${
stateObject ||
({
entity_id: this.itemId,
attributes: {},
} as HassEntity)
}
>
</ha-state-icon>
`
: nothing
: canMigrate
? html`<ha-svg-icon .path=${mdiSwapHorizontal}></ha-svg-icon>`
: fallbackIconPath
? html`<ha-svg-icon .path=${fallbackIconPath}></ha-svg-icon>`
: this.type === "entity"
? html`
<ha-state-icon
.stateObj=${
stateObject ||
({
entity_id: this.itemId,
attributes: {},
} as HassEntity)
}
>
</ha-state-icon>
`
: nothing
}
</div>
<span slot="headline">${(canMigrate && replacement?.name) || name}</span>
<span slot="headline"
>${
canMigrate
? this.hass.localize(
"ui.components.target-picker.device_replaced_headline"
)
: name
}</span
>
${
notFound || (context && !this.hideContext)
? html`<span slot="supporting-text"
>${
notFound
? canMigrate
? this.hass.localize(
"ui.components.target-picker.device_replaced",
{ count: replacement!.candidates.length }
)
? replacement!.candidates.length === 1 && replacement!.name
? this.hass.localize(
"ui.components.target-picker.device_replaced_by_one",
{ device: replacement!.name }
)
: this.hass.localize(
"ui.components.target-picker.device_replaced",
{ count: replacement!.candidates.length }
)
: this.hass.localize(
`ui.components.target-picker.${this.type}_not_found`
)
@@ -256,7 +272,7 @@ export class HaTargetPickerItemRow extends LitElement {
@click=${this._migrate}
>
${this.hass.localize(
"ui.components.target-picker.replace_device"
"ui.components.target-picker.replace_update"
)}
</ha-button>
`
@@ -7,6 +7,7 @@ import {
mdiFormatListBulleted,
mdiMenuDown,
mdiShape,
mdiSwapHorizontal,
} from "@mdi/js";
import type { HassServiceTarget } from "home-assistant-js-websocket";
import {
@@ -321,14 +322,25 @@ export class HaAutomationRowTargets extends LitElement {
let lastTargetType: string | null = null;
// The collapsed summary hides the individual targets, so carry over the
// warning when any of them no longer exists.
const hasMissingTarget = rows.some(
([targetType, targetId]) => !this._checkTargetExists(targetType, targetId)
);
return html`
<ha-dropdown
@wa-select=${this._handleTargetSelect}
@click=${stopPropagation}
@keydown=${stopPropagation}
>
<button slot="trigger" class="target">
<ha-svg-icon .path=${mdiFormatListBulleted}></ha-svg-icon>
<button
slot="trigger"
class=${classMap({ target: true, warning: hasMissingTarget })}
>
<ha-svg-icon
.path=${hasMissingTarget ? mdiAlert : mdiFormatListBulleted}
></ha-svg-icon>
<div class="label">
${this._i18n.localize(
"ui.panel.config.automation.editor.target_summary.targets",
@@ -470,6 +482,7 @@ export class HaAutomationRowTargets extends LitElement {
if (targetType === "device" && this._compositeSplits?.[targetId]) {
// The device was replaced by one or more split devices; make clear
// this reference needs to be updated, distinct from "unknown device".
icon = mdiSwapHorizontal;
label = this._i18n.localize(
"ui.panel.config.automation.editor.target_summary.device_replaced"
);
@@ -686,6 +699,9 @@ export class HaAutomationRowTargets extends LitElement {
background-color: var(--ha-color-fill-warning-quiet-resting);
color: var(--ha-color-on-warning-normal);
}
ha-dropdown-item.warning ha-svg-icon {
color: var(--ha-color-on-warning-normal);
}
ha-dropdown-item.warning:hover {
background-color: var(--ha-color-fill-warning-quiet-hover);
color: var(--ha-color-on-warning-normal);
+9 -5
View File
@@ -802,8 +802,10 @@
"device_not_found": "Device not found",
"entity_not_found": "Entity not found",
"label_not_found": "Label not found",
"device_replaced_headline": "Replaced device",
"device_replaced": "Replaced by {count} {count, plural,\n one {device}\n other {devices}\n}",
"replace_device": "Replace",
"device_replaced_by_one": "Replaced by {device}",
"replace_update": "Update",
"devices_count": "{count} {count, plural,\n one {device}\n other {devices}\n}",
"entities_count": "{count} {count, plural,\n one {entity}\n other {entities}\n}",
"entities_count_filtered": "{count}/{total} {total, plural,\n one {entity}\n other {entities}\n}",
@@ -924,10 +926,12 @@
"no_area": "No area",
"placeholder": "Select a device",
"unknown": "Unknown device selected",
"device_replaced_count": "Replaced by {count} {count, plural,\n one {device}\n other {devices}\n}",
"device_replaced_by_one": "This device was replaced by {device}.",
"device_replaced_by_multiple": "This device was replaced by {count} devices.",
"replace_device": "Replace",
"device_replaced": "Replaced device",
"device_replaced_by_one": "This device no longer exists. It was replaced by {device}.",
"device_replaced_by_one_integration": "This device no longer exists. It was replaced by {device} from {integration}.",
"device_replaced_by_multiple": "This device no longer exists. It was replaced by {count} devices, pick the one to use here.",
"replace_update": "Update",
"replace_choose": "Choose",
"replaced_dialog": {
"title": "Replace device",
"description": "This device was replaced by multiple devices. Choose which one to use.",
+26 -17
View File
@@ -5257,7 +5257,7 @@ __metadata:
languageName: node
linkType: hard
"@sveltejs/acorn-typescript@npm:^1.0.10":
"@sveltejs/acorn-typescript@npm:^1.0.13":
version: 1.0.13
resolution: "@sveltejs/acorn-typescript@npm:1.0.13"
peerDependencies:
@@ -6667,7 +6667,7 @@ __metadata:
languageName: node
linkType: hard
"acorn@npm:^8.10.0, acorn@npm:^8.11.0, acorn@npm:^8.15.0, acorn@npm:^8.16.0, acorn@npm:^8.17.0":
"acorn@npm:^8.10.0, acorn@npm:^8.11.0, acorn@npm:^8.15.0, acorn@npm:^8.16.0, acorn@npm:^8.18.0":
version: 8.18.0
resolution: "acorn@npm:8.18.0"
bin:
@@ -10111,7 +10111,7 @@ __metadata:
map-stream: "npm:0.0.7"
marked: "npm:18.0.10"
memoize-one: "npm:6.0.0"
minify-literals: "npm:2.1.0"
minify-literals: "npm:2.2.0"
node-vibrant: "npm:4.0.4"
object-hash: "npm:3.0.0"
pinst: "npm:3.0.0"
@@ -10190,9 +10190,9 @@ __metadata:
languageName: node
linkType: hard
"html-minifier-next@npm:^7.0.0":
version: 7.5.3
resolution: "html-minifier-next@npm:7.5.3"
"html-minifier-next@npm:^8.1.0":
version: 8.1.0
resolution: "html-minifier-next@npm:8.1.0"
dependencies:
commander: "npm:^15.0.0"
entities: "npm:^8.0.0"
@@ -10207,7 +10207,7 @@ __metadata:
bin:
hmn: cli.js
html-minifier-next: cli.js
checksum: 10/fe4ad84f5c577c2de936c7aad5c0a7619c5e7ce3763f3c2e15f1ecc9629d89ebc578a38c0e44883cc0388253de262962610f7dbc3101f8d16e363c477a6a9b07
checksum: 10/5badf1abd2c6fb9ada2f7dd4929be74f43155510298f7af323b0e39a2c20e2a33ed8de507f105bcea389118f2813e7776b559cf4b2b6397537c5871aa27f2125
languageName: node
linkType: hard
@@ -11449,7 +11449,7 @@ __metadata:
languageName: node
linkType: hard
"lightningcss@npm:1.33.0, lightningcss@npm:^1.32.0, lightningcss@npm:^1.33.0":
"lightningcss@npm:1.33.0, lightningcss@npm:^1.33.0":
version: 1.33.0
resolution: "lightningcss@npm:1.33.0"
dependencies:
@@ -11703,6 +11703,15 @@ __metadata:
languageName: node
linkType: hard
"magic-string@npm:^1.2.2":
version: 1.2.2
resolution: "magic-string@npm:1.2.2"
dependencies:
"@jridgewell/sourcemap-codec": "npm:^1.5.5"
checksum: 10/23514d9fb744d30689037e41440125b6c4bc3fb5b8e6d992798e59f93d7452276f223d062ba9b1ce78fbee5f9cc365c1e0ae3c6babddb74cb3aa9074adb57b03
languageName: node
linkType: hard
"magicast@npm:^0.5.2":
version: 0.5.4
resolution: "magicast@npm:0.5.4"
@@ -11880,16 +11889,16 @@ __metadata:
languageName: node
linkType: hard
"minify-literals@npm:2.1.0":
version: 2.1.0
resolution: "minify-literals@npm:2.1.0"
"minify-literals@npm:2.2.0":
version: 2.2.0
resolution: "minify-literals@npm:2.2.0"
dependencies:
"@sveltejs/acorn-typescript": "npm:^1.0.10"
acorn: "npm:^8.17.0"
html-minifier-next: "npm:^7.0.0"
lightningcss: "npm:^1.32.0"
magic-string: "npm:^0.30.21"
checksum: 10/95bcd25afcf638c00bcd8981c2e6409be8c2feb4fd8f954bc9a547b2ade917810cef17bb075ad95e5ceaac6fffc8e625c161ab77b2363c36de53da6d139de872
"@sveltejs/acorn-typescript": "npm:^1.0.13"
acorn: "npm:^8.18.0"
html-minifier-next: "npm:^8.1.0"
lightningcss: "npm:^1.33.0"
magic-string: "npm:^1.2.2"
checksum: 10/73f17955820675f405b95810f3bf861a05baf426300e8ba46d174987ee45366c885ef1281c40842025529343a4ca71b6fd8294ce674bccbd58685e4a917b9448
languageName: node
linkType: hard