mirror of
https://github.com/home-assistant/frontend.git
synced 2026-08-28 18:07:14 +00:00
Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8e4b6d22bf | ||
|
|
402aeaacd9 | ||
|
|
29cd46eb9e | ||
|
|
92b3081896 | ||
|
|
527319ab5e | ||
|
|
8d3877c388 | ||
|
|
66b9fbd4bf |
+1
-1
@@ -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",
|
||||
|
||||
@@ -12,7 +12,6 @@ import { fullEntitiesContext } from "../../data/context";
|
||||
import type { DeviceAutomation } from "../../data/device/device_automation";
|
||||
import {
|
||||
deviceAutomationsEqual,
|
||||
deviceAutomationsSimilar,
|
||||
sortDeviceAutomations,
|
||||
} from "../../data/device/device_automation";
|
||||
import type { EntityRegistryEntry } from "../../data/entity/entity_registry";
|
||||
@@ -180,12 +179,15 @@ export abstract class HaDeviceAutomationPicker<
|
||||
(a, idx) => value === `${a.device_id}_${idx}`
|
||||
);
|
||||
|
||||
const text = automation
|
||||
const described =
|
||||
automation ?? (this.value?.domain ? this.value : undefined);
|
||||
|
||||
const text = described
|
||||
? this._localizeDeviceAutomation(
|
||||
this.hass.localize,
|
||||
this.hass.states,
|
||||
this._entityReg,
|
||||
automation
|
||||
described
|
||||
)
|
||||
: value === NO_AUTOMATION_KEY
|
||||
? this.NO_AUTOMATION_TEXT
|
||||
@@ -195,29 +197,24 @@ export abstract class HaDeviceAutomationPicker<
|
||||
};
|
||||
|
||||
private async _updateDeviceInfo() {
|
||||
// Asking a removed device for its automations fails rather than returning
|
||||
// an empty list.
|
||||
this._automations = this.deviceId
|
||||
? (
|
||||
await this._fetchDeviceAutomations(this.hass.callWS, this.deviceId)
|
||||
await this._fetchDeviceAutomations(
|
||||
this.hass.callWS,
|
||||
this.deviceId
|
||||
).catch(() => [] as T[])
|
||||
).sort(sortDeviceAutomations)
|
||||
: // No device, clear the list of automations
|
||||
[];
|
||||
|
||||
// If there is no value, or if we have changed the device ID, reset the
|
||||
// value. When the device changed (for example after replacing a removed
|
||||
// device), try to keep the same automation type/subtype on the new device
|
||||
// before falling back to the first available automation.
|
||||
// If there is no value, or if we have changed the device ID, reset the value.
|
||||
if (!this.value || this.value.device_id !== this.deviceId) {
|
||||
const equivalent =
|
||||
this.value && this.deviceId
|
||||
? this._automations.find((automation) =>
|
||||
deviceAutomationsSimilar(automation, this.value!)
|
||||
)
|
||||
: undefined;
|
||||
this._setValue(
|
||||
equivalent ||
|
||||
(this._automations.length
|
||||
? this._automations[0]
|
||||
: this._createNoAutomation(this.deviceId))
|
||||
this._automations.length
|
||||
? this._automations[0]
|
||||
: this._createNoAutomation(this.deviceId)
|
||||
);
|
||||
}
|
||||
this._renderEmpty = true;
|
||||
|
||||
@@ -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";
|
||||
@@ -104,6 +105,14 @@ export class HaDevicePicker extends LitElement {
|
||||
@property({ attribute: "hide-clear-icon", type: Boolean })
|
||||
public hideClearIcon = false;
|
||||
|
||||
/**
|
||||
* The split devices that can actually replace the current value, when the
|
||||
* caller knows better than this picker. Narrows the replacement candidates,
|
||||
* so the user is only asked to choose when there is a real choice.
|
||||
*/
|
||||
@property({ attribute: false })
|
||||
public replacementDeviceIds?: string[];
|
||||
|
||||
@query("ha-generic-picker") private _picker?: HaGenericPicker;
|
||||
|
||||
@state() private _configEntryLookup: Record<string, ConfigEntry> = {};
|
||||
@@ -187,7 +196,8 @@ export class HaDevicePicker extends LitElement {
|
||||
value: string | undefined,
|
||||
_devices: HomeAssistant["devices"],
|
||||
compositeSplits: DeviceCompositeSplits | undefined,
|
||||
items: (DevicePickerItem | string)[]
|
||||
items: (DevicePickerItem | string)[],
|
||||
replacementDeviceIds: string[] | undefined
|
||||
) => {
|
||||
if (!value || !compositeSplits || this.hass.devices[value]) {
|
||||
return undefined;
|
||||
@@ -203,7 +213,11 @@ export class HaDevicePicker extends LitElement {
|
||||
.filter((item): item is DevicePickerItem => typeof item !== "string")
|
||||
.map((item) => item.id)
|
||||
);
|
||||
const candidates = split.split_ids.filter((id) => selectableIds.has(id));
|
||||
const candidates = split.split_ids.filter(
|
||||
(id) =>
|
||||
selectableIds.has(id) &&
|
||||
(!replacementDeviceIds || replacementDeviceIds.includes(id))
|
||||
);
|
||||
return { candidates, primaryId: split.primary_id };
|
||||
}
|
||||
);
|
||||
@@ -250,26 +264,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>`;
|
||||
@@ -399,31 +413,23 @@ export class HaDevicePicker extends LitElement {
|
||||
this.value,
|
||||
this.hass.devices,
|
||||
this._compositeSplits,
|
||||
this._getItems()
|
||||
this._getItems(),
|
||||
this.replacementDeviceIds
|
||||
)
|
||||
: undefined;
|
||||
|
||||
// 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 +449,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 +465,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>
|
||||
`;
|
||||
@@ -498,7 +521,8 @@ export class HaDevicePicker extends LitElement {
|
||||
this.value,
|
||||
this.hass.devices,
|
||||
this._compositeSplits,
|
||||
this._getItems()
|
||||
this._getItems(),
|
||||
this.replacementDeviceIds
|
||||
);
|
||||
if (!replacement?.candidates.length) {
|
||||
return;
|
||||
|
||||
@@ -90,7 +90,7 @@ class HaAlert extends LitElement {
|
||||
static styles = css`
|
||||
.issue-type {
|
||||
position: relative;
|
||||
padding: 8px;
|
||||
padding: var(--ha-alert-padding, 8px);
|
||||
display: flex;
|
||||
}
|
||||
.icon {
|
||||
|
||||
@@ -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>
|
||||
`
|
||||
|
||||
@@ -182,26 +182,69 @@ export const deviceAutomationEditorMode = (
|
||||
: "unknown-device";
|
||||
};
|
||||
|
||||
// Like deviceAutomationsEqual, but ignores device_id and entity_id so an
|
||||
// automation can be matched to the equivalent one on a different device (for
|
||||
// example when a referenced device was replaced by a split device).
|
||||
export const deviceAutomationsSimilar = (
|
||||
const deviceAutomationsSameType = (a: DeviceAutomation, b: DeviceAutomation) =>
|
||||
deviceAutomationIdentifiers
|
||||
.filter((property) => property !== "device_id" && property !== "entity_id")
|
||||
.every((property) => Object.is(a[property], b[property]));
|
||||
|
||||
// An entity can be referenced by its registry id or by its entity id, and the
|
||||
// two sides do not have to agree.
|
||||
const deviceAutomationsSameEntity = (
|
||||
entityRegistry: EntityRegistryEntry[],
|
||||
a: DeviceAutomation,
|
||||
b: DeviceAutomation
|
||||
) => {
|
||||
if (typeof a !== typeof b) {
|
||||
if (!a.entity_id && !b.entity_id) {
|
||||
return true;
|
||||
}
|
||||
if (!a.entity_id || !b.entity_id) {
|
||||
return false;
|
||||
}
|
||||
return deviceAutomationIdentifiers
|
||||
.filter((property) => property !== "device_id" && property !== "entity_id")
|
||||
.every((property) => {
|
||||
const inA = property in a;
|
||||
const inB = property in b;
|
||||
if (!inA && !inB) {
|
||||
return true;
|
||||
}
|
||||
return Object.is(a[property], b[property]);
|
||||
});
|
||||
return (
|
||||
a.entity_id === b.entity_id ||
|
||||
compareEntityIdWithEntityRegId(entityRegistry, a.entity_id, b.entity_id)
|
||||
);
|
||||
};
|
||||
|
||||
// A device exposes the same automation type once per entity, so the same type
|
||||
// on another entity is a different automation, not an equivalent one.
|
||||
export const findEquivalentDeviceAutomation = <T extends DeviceAutomation>(
|
||||
entityRegistry: EntityRegistryEntry[],
|
||||
automations: T[],
|
||||
automation: DeviceAutomation
|
||||
): T | undefined =>
|
||||
automations.find(
|
||||
(candidate) =>
|
||||
deviceAutomationsSameType(candidate, automation) &&
|
||||
deviceAutomationsSameEntity(entityRegistry, candidate, automation)
|
||||
);
|
||||
|
||||
// Among the split devices that replaced a removed device, the ones that offer
|
||||
// the given automation. Nothing in the registry says which of them took it over,
|
||||
// so each candidate has to be asked.
|
||||
export const fetchReplacementDevices = async <T extends DeviceAutomation>(
|
||||
hass: HomeAssistant,
|
||||
entityRegistry: EntityRegistryEntry[],
|
||||
automation: DeviceAutomation,
|
||||
compositeSplits: DeviceCompositeSplits,
|
||||
fetchDeviceAutomations: (callWS: CallWS, deviceId: string) => Promise<T[]>
|
||||
): Promise<string[]> => {
|
||||
const candidates =
|
||||
compositeSplits[automation.device_id]?.split_ids.filter(
|
||||
(id) => id in hass.devices
|
||||
) ?? [];
|
||||
const automationsPerCandidate = await Promise.all(
|
||||
candidates.map((id) =>
|
||||
fetchDeviceAutomations(hass.callWS, id).catch(() => [] as T[])
|
||||
)
|
||||
);
|
||||
return candidates.filter((_id, index) =>
|
||||
findEquivalentDeviceAutomation(
|
||||
entityRegistry,
|
||||
automationsPerCandidate[index],
|
||||
automation
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const compareEntityIdWithEntityRegId = (
|
||||
|
||||
@@ -14,6 +14,8 @@ import type {
|
||||
} from "../../../../../data/device/device_automation";
|
||||
import {
|
||||
deviceAutomationEditorMode,
|
||||
fetchReplacementDevices,
|
||||
fetchDeviceActions,
|
||||
deviceAutomationsEqual,
|
||||
fetchDeviceActionCapabilities,
|
||||
localizeExtraFieldsComputeHelperCallback,
|
||||
@@ -40,6 +42,8 @@ export class HaDeviceAction extends LitElement {
|
||||
|
||||
@state() private _compositeSplits?: DeviceCompositeSplits;
|
||||
|
||||
@state() private _replacementDeviceIds?: string[];
|
||||
|
||||
private _loadingCompositeSplits = false;
|
||||
|
||||
@state()
|
||||
@@ -101,7 +105,17 @@ export class HaDeviceAction extends LitElement {
|
||||
}
|
||||
this._loadingCompositeSplits = true;
|
||||
try {
|
||||
this._compositeSplits = await fetchDeviceCompositeSplits(this.hass);
|
||||
// Resolve the candidates before exposing the split map, so the picker
|
||||
// never offers one that cannot host the automation.
|
||||
const compositeSplits = await fetchDeviceCompositeSplits(this.hass);
|
||||
this._replacementDeviceIds = await fetchReplacementDevices(
|
||||
this.hass,
|
||||
this._entityReg,
|
||||
this.action,
|
||||
compositeSplits,
|
||||
fetchDeviceActions
|
||||
);
|
||||
this._compositeSplits = compositeSplits;
|
||||
} catch (_err) {
|
||||
this._compositeSplits = {};
|
||||
} finally {
|
||||
@@ -115,6 +129,7 @@ export class HaDeviceAction extends LitElement {
|
||||
return html`
|
||||
<ha-device-picker
|
||||
.value=${deviceId}
|
||||
.replacementDeviceIds=${this._replacementDeviceIds}
|
||||
.disabled=${this.disabled}
|
||||
@value-changed=${this._devicePicked}
|
||||
.hass=${this.hass}
|
||||
@@ -185,6 +200,15 @@ export class HaDeviceAction extends LitElement {
|
||||
|
||||
private _devicePicked(ev) {
|
||||
ev.stopPropagation();
|
||||
// The automation exists as is on the replacement, so only the reference
|
||||
// changes and the rest of the configuration is left untouched.
|
||||
if (this._replacementDeviceIds?.includes(ev.target.value)) {
|
||||
this._deviceId = undefined;
|
||||
fireEvent(this, "value-changed", {
|
||||
value: { ...this.action, device_id: ev.target.value },
|
||||
});
|
||||
return;
|
||||
}
|
||||
this._deviceId = ev.target.value;
|
||||
if (this._deviceId === undefined) {
|
||||
fireEvent(this, "value-changed", {
|
||||
|
||||
@@ -14,6 +14,8 @@ import type {
|
||||
} from "../../../../../data/device/device_automation";
|
||||
import {
|
||||
deviceAutomationEditorMode,
|
||||
fetchReplacementDevices,
|
||||
fetchDeviceConditions,
|
||||
deviceAutomationsEqual,
|
||||
fetchDeviceConditionCapabilities,
|
||||
localizeExtraFieldsComputeHelperCallback,
|
||||
@@ -40,6 +42,8 @@ export class HaDeviceCondition extends LitElement {
|
||||
|
||||
@state() private _compositeSplits?: DeviceCompositeSplits;
|
||||
|
||||
@state() private _replacementDeviceIds?: string[];
|
||||
|
||||
private _loadingCompositeSplits = false;
|
||||
|
||||
@state()
|
||||
@@ -102,7 +106,17 @@ export class HaDeviceCondition extends LitElement {
|
||||
}
|
||||
this._loadingCompositeSplits = true;
|
||||
try {
|
||||
this._compositeSplits = await fetchDeviceCompositeSplits(this.hass);
|
||||
// Resolve the candidates before exposing the split map, so the picker
|
||||
// never offers one that cannot host the automation.
|
||||
const compositeSplits = await fetchDeviceCompositeSplits(this.hass);
|
||||
this._replacementDeviceIds = await fetchReplacementDevices(
|
||||
this.hass,
|
||||
this._entityReg,
|
||||
this.condition,
|
||||
compositeSplits,
|
||||
fetchDeviceConditions
|
||||
);
|
||||
this._compositeSplits = compositeSplits;
|
||||
} catch (_err) {
|
||||
this._compositeSplits = {};
|
||||
} finally {
|
||||
@@ -116,6 +130,7 @@ export class HaDeviceCondition extends LitElement {
|
||||
return html`
|
||||
<ha-device-picker
|
||||
.value=${deviceId}
|
||||
.replacementDeviceIds=${this._replacementDeviceIds}
|
||||
@value-changed=${this._devicePicked}
|
||||
.hass=${this.hass}
|
||||
.disabled=${this.disabled}
|
||||
@@ -187,6 +202,15 @@ export class HaDeviceCondition extends LitElement {
|
||||
|
||||
private _devicePicked(ev) {
|
||||
ev.stopPropagation();
|
||||
// The automation exists as is on the replacement, so only the reference
|
||||
// changes and the rest of the configuration is left untouched.
|
||||
if (this._replacementDeviceIds?.includes(ev.target.value)) {
|
||||
this._deviceId = undefined;
|
||||
fireEvent(this, "value-changed", {
|
||||
value: { ...this.condition, device_id: ev.target.value },
|
||||
});
|
||||
return;
|
||||
}
|
||||
this._deviceId = ev.target.value;
|
||||
if (this._deviceId === undefined) {
|
||||
fireEvent(this, "value-changed", {
|
||||
|
||||
@@ -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",
|
||||
@@ -467,9 +479,15 @@ export class HaAutomationRowTargets extends LitElement {
|
||||
warning = true;
|
||||
badgeTargetId = undefined;
|
||||
badgeTargetType = undefined;
|
||||
if (targetType === "device" && this._compositeSplits?.[targetId]) {
|
||||
if (
|
||||
targetType === "device" &&
|
||||
this._compositeSplits?.[targetId]?.split_ids.some(
|
||||
(id) => id in this._registries.devices
|
||||
)
|
||||
) {
|
||||
// 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 +704,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);
|
||||
|
||||
@@ -16,6 +16,8 @@ import type {
|
||||
} from "../../../../../data/device/device_automation";
|
||||
import {
|
||||
deviceAutomationEditorMode,
|
||||
fetchReplacementDevices,
|
||||
fetchDeviceTriggers,
|
||||
deviceAutomationsEqual,
|
||||
fetchDeviceTriggerCapabilities,
|
||||
localizeExtraFieldsComputeHelperCallback,
|
||||
@@ -42,6 +44,8 @@ export class HaDeviceTrigger extends LitElement {
|
||||
|
||||
@state() private _compositeSplits?: DeviceCompositeSplits;
|
||||
|
||||
@state() private _replacementDeviceIds?: string[];
|
||||
|
||||
private _loadingCompositeSplits = false;
|
||||
|
||||
@state()
|
||||
@@ -106,7 +110,17 @@ export class HaDeviceTrigger extends LitElement {
|
||||
}
|
||||
this._loadingCompositeSplits = true;
|
||||
try {
|
||||
this._compositeSplits = await fetchDeviceCompositeSplits(this.hass);
|
||||
// Resolve the candidates before exposing the split map, so the picker
|
||||
// never offers one that cannot host the automation.
|
||||
const compositeSplits = await fetchDeviceCompositeSplits(this.hass);
|
||||
this._replacementDeviceIds = await fetchReplacementDevices(
|
||||
this.hass,
|
||||
this._entityReg,
|
||||
this.trigger,
|
||||
compositeSplits,
|
||||
fetchDeviceTriggers
|
||||
);
|
||||
this._compositeSplits = compositeSplits;
|
||||
} catch (_err) {
|
||||
this._compositeSplits = {};
|
||||
} finally {
|
||||
@@ -120,6 +134,7 @@ export class HaDeviceTrigger extends LitElement {
|
||||
return html`
|
||||
<ha-device-picker
|
||||
.value=${deviceId}
|
||||
.replacementDeviceIds=${this._replacementDeviceIds}
|
||||
@value-changed=${this._devicePicked}
|
||||
.hass=${this.hass}
|
||||
.disabled=${this.disabled}
|
||||
@@ -208,6 +223,15 @@ export class HaDeviceTrigger extends LitElement {
|
||||
|
||||
private _devicePicked(ev) {
|
||||
ev.stopPropagation();
|
||||
// The automation exists as is on the replacement, so only the reference
|
||||
// changes and the rest of the configuration is left untouched.
|
||||
if (this._replacementDeviceIds?.includes(ev.target.value)) {
|
||||
this._deviceId = undefined;
|
||||
fireEvent(this, "value-changed", {
|
||||
value: { ...this.trigger, device_id: ev.target.value },
|
||||
});
|
||||
return;
|
||||
}
|
||||
this._deviceId = ev.target.value;
|
||||
if (this._deviceId === undefined) {
|
||||
fireEvent(this, "value-changed", {
|
||||
|
||||
@@ -57,7 +57,7 @@ export class CloudAccountOverview extends LitElement {
|
||||
|
||||
private _renderTopCard(): TemplateResult {
|
||||
return html`
|
||||
<ha-card outlined>
|
||||
<ha-card outlined class="summary-card">
|
||||
<div class="card-content">
|
||||
<div
|
||||
class="thank-you-header"
|
||||
@@ -121,8 +121,8 @@ export class CloudAccountOverview extends LitElement {
|
||||
<p class="muted">
|
||||
${this.hass.localize("ui.panel.config.cloud.account.funding_note")}
|
||||
</p>
|
||||
${this._renderSubscriptionState()}
|
||||
</div>
|
||||
${this._renderSubscriptionState()}
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
@@ -132,6 +132,7 @@ export class CloudAccountOverview extends LitElement {
|
||||
case "trial":
|
||||
return html`
|
||||
<ha-alert
|
||||
class="subscription-alert"
|
||||
alert-type="warning"
|
||||
.title=${this.hass.localize(
|
||||
"ui.panel.config.cloud.account.overview.trial_title"
|
||||
@@ -156,6 +157,7 @@ export class CloudAccountOverview extends LitElement {
|
||||
case "canceled":
|
||||
return html`
|
||||
<ha-alert
|
||||
class="subscription-alert"
|
||||
alert-type="warning"
|
||||
.title=${this.hass.localize(
|
||||
"ui.panel.config.cloud.account.overview.canceled_title"
|
||||
@@ -181,6 +183,7 @@ export class CloudAccountOverview extends LitElement {
|
||||
case "expired":
|
||||
return html`
|
||||
<ha-alert
|
||||
class="subscription-alert"
|
||||
alert-type="error"
|
||||
.title=${this.hass.localize(
|
||||
"ui.panel.config.cloud.account.overview.expired_title"
|
||||
@@ -636,11 +639,15 @@ export class CloudAccountOverview extends LitElement {
|
||||
width: auto;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
ha-alert {
|
||||
display: block;
|
||||
margin-top: var(--ha-space-3);
|
||||
/* Prevent the embedded .subscription-alert ha-alert from bleeding outside the card */
|
||||
.summary-card {
|
||||
overflow: hidden;
|
||||
}
|
||||
ha-alert ha-button[slot="action"] {
|
||||
.subscription-alert {
|
||||
display: block;
|
||||
--ha-alert-padding: var(--ha-space-3);
|
||||
}
|
||||
.subscription-alert ha-button[slot="action"] {
|
||||
width: max-content;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@@ -395,9 +395,11 @@ export class VoiceAssistantsExpose extends LitElement {
|
||||
aliases: entry?.aliases || [],
|
||||
};
|
||||
}
|
||||
result[entityId].assistants_sortable_key = getAssistantsSortableKey(
|
||||
result[entityId].assistants
|
||||
);
|
||||
if (result[entityId]) {
|
||||
result[entityId].assistants_sortable_key = getAssistantsSortableKey(
|
||||
result[entityId].assistants
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
return Object.values(result);
|
||||
|
||||
@@ -165,7 +165,7 @@ export class HuiViewBackgroundEditor extends LitElement {
|
||||
}
|
||||
|
||||
private _currentBackgroundImage(): string | undefined {
|
||||
const background = this._backgroundData(this._config);
|
||||
const background = this._backgroundData(this.config);
|
||||
return typeof background.image === "object"
|
||||
? background.image.media_content_id
|
||||
: background.image;
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { DeviceTrigger } from "../../src/data/device/device_automation";
|
||||
import {
|
||||
fetchReplacementDevices,
|
||||
findEquivalentDeviceAutomation,
|
||||
} from "../../src/data/device/device_automation";
|
||||
import type { DeviceCompositeSplits } from "../../src/data/device/device_registry";
|
||||
import type { EntityRegistryEntry } from "../../src/data/entity/entity_registry";
|
||||
import type { HomeAssistant } from "../../src/types";
|
||||
|
||||
const entityRegistry = [
|
||||
{ id: "regid1", entity_id: "binary_sensor.one" },
|
||||
{ id: "regid2", entity_id: "binary_sensor.two" },
|
||||
] as EntityRegistryEntry[];
|
||||
|
||||
const trigger = (partial: Partial<DeviceTrigger>): DeviceTrigger =>
|
||||
({
|
||||
trigger: "device",
|
||||
domain: "binary_sensor",
|
||||
device_id: "device1",
|
||||
...partial,
|
||||
}) as DeviceTrigger;
|
||||
|
||||
describe("findEquivalentDeviceAutomation", () => {
|
||||
it("picks the automation on the same entity among several of the same type", () => {
|
||||
const automations = [
|
||||
trigger({ device_id: "device2", type: "turned_on", entity_id: "regid1" }),
|
||||
trigger({ device_id: "device2", type: "turned_on", entity_id: "regid2" }),
|
||||
];
|
||||
|
||||
expect(
|
||||
findEquivalentDeviceAutomation(
|
||||
entityRegistry,
|
||||
automations,
|
||||
trigger({ type: "turned_on", entity_id: "regid2" })
|
||||
)
|
||||
).toBe(automations[1]);
|
||||
});
|
||||
|
||||
it("matches an entity referenced by entity id against one referenced by registry id", () => {
|
||||
const automations = [
|
||||
trigger({ device_id: "device2", type: "turned_on", entity_id: "regid1" }),
|
||||
trigger({ device_id: "device2", type: "turned_on", entity_id: "regid2" }),
|
||||
];
|
||||
|
||||
expect(
|
||||
findEquivalentDeviceAutomation(
|
||||
entityRegistry,
|
||||
automations,
|
||||
trigger({ type: "turned_on", entity_id: "binary_sensor.two" })
|
||||
)
|
||||
).toBe(automations[1]);
|
||||
});
|
||||
|
||||
it("returns undefined when the same type is only offered for another entity", () => {
|
||||
const automations = [
|
||||
trigger({ device_id: "device2", type: "turned_on", entity_id: "regid1" }),
|
||||
trigger({
|
||||
device_id: "device2",
|
||||
type: "turned_off",
|
||||
entity_id: "regid1",
|
||||
}),
|
||||
];
|
||||
|
||||
expect(
|
||||
findEquivalentDeviceAutomation(
|
||||
entityRegistry,
|
||||
automations,
|
||||
trigger({ type: "turned_on", entity_id: "regid2" })
|
||||
)
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it("matches entity-less automations on their subtype", () => {
|
||||
const automations = [
|
||||
trigger({
|
||||
device_id: "device2",
|
||||
domain: "zha",
|
||||
type: "remote_button_short_press",
|
||||
subtype: "button_1",
|
||||
}),
|
||||
trigger({
|
||||
device_id: "device2",
|
||||
domain: "zha",
|
||||
type: "remote_button_short_press",
|
||||
subtype: "button_2",
|
||||
}),
|
||||
];
|
||||
|
||||
expect(
|
||||
findEquivalentDeviceAutomation(
|
||||
entityRegistry,
|
||||
automations,
|
||||
trigger({
|
||||
domain: "zha",
|
||||
type: "remote_button_short_press",
|
||||
subtype: "button_2",
|
||||
})
|
||||
)
|
||||
).toBe(automations[1]);
|
||||
});
|
||||
|
||||
it("returns undefined when the device offers no automation of that type", () => {
|
||||
const automations = [
|
||||
trigger({
|
||||
device_id: "device2",
|
||||
type: "turned_off",
|
||||
entity_id: "regid1",
|
||||
}),
|
||||
];
|
||||
|
||||
expect(
|
||||
findEquivalentDeviceAutomation(
|
||||
entityRegistry,
|
||||
automations,
|
||||
trigger({ type: "turned_on", entity_id: "regid1" })
|
||||
)
|
||||
).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("fetchReplacementDevices", () => {
|
||||
const hass = {
|
||||
callWS: () => Promise.resolve([]),
|
||||
devices: { device2: {}, device3: {} },
|
||||
} as unknown as HomeAssistant;
|
||||
|
||||
const compositeSplits = {
|
||||
removed: { split_ids: ["device2", "device3"], primary_id: "device2" },
|
||||
} as unknown as DeviceCompositeSplits;
|
||||
|
||||
const value = trigger({
|
||||
device_id: "removed",
|
||||
type: "turned_on",
|
||||
entity_id: "regid1",
|
||||
});
|
||||
|
||||
it("keeps only the devices that offer the automation", async () => {
|
||||
const offers = {
|
||||
device2: [
|
||||
trigger({
|
||||
device_id: "device2",
|
||||
type: "turned_on",
|
||||
entity_id: "regid1",
|
||||
}),
|
||||
],
|
||||
device3: [
|
||||
trigger({
|
||||
device_id: "device3",
|
||||
type: "turned_on",
|
||||
entity_id: "regid2",
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
||||
expect(
|
||||
await fetchReplacementDevices(
|
||||
hass,
|
||||
entityRegistry,
|
||||
value,
|
||||
compositeSplits,
|
||||
(_callWS, deviceId) => Promise.resolve(offers[deviceId])
|
||||
)
|
||||
).toEqual(["device2"]);
|
||||
});
|
||||
|
||||
it("drops a device whose automations cannot be listed", async () => {
|
||||
expect(
|
||||
await fetchReplacementDevices(
|
||||
hass,
|
||||
entityRegistry,
|
||||
value,
|
||||
compositeSplits,
|
||||
(_callWS, deviceId) =>
|
||||
deviceId === "device2"
|
||||
? Promise.reject(new Error("unknown device"))
|
||||
: Promise.resolve([
|
||||
trigger({
|
||||
device_id: "device3",
|
||||
type: "turned_on",
|
||||
entity_id: "regid1",
|
||||
}),
|
||||
])
|
||||
)
|
||||
).toEqual(["device3"]);
|
||||
});
|
||||
});
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user