mirror of
https://github.com/home-assistant/frontend.git
synced 2026-07-18 09:07:05 +00:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 959c440844 | |||
| be440b0dd3 |
@@ -22,7 +22,7 @@ jobs:
|
||||
if: github.event_name != 'push' || github.ref_name != 'master'
|
||||
environment:
|
||||
name: Demo Development
|
||||
url: ${{ steps.deploy.outputs.unique_deploy_url }}
|
||||
url: ${{ steps.deploy.outputs.netlify_url }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
|
||||
+4
-4
@@ -102,7 +102,7 @@
|
||||
"dialog-polyfill": "0.5.6",
|
||||
"echarts": "6.1.0",
|
||||
"element-internals-polyfill": "3.0.2",
|
||||
"fuse.js": "7.5.0",
|
||||
"fuse.js": "7.4.2",
|
||||
"gulp-zopfli-green": "7.0.0",
|
||||
"hls.js": "1.6.16",
|
||||
"home-assistant-js-websocket": "9.6.0",
|
||||
@@ -146,13 +146,13 @@
|
||||
"@bundle-stats/plugin-webpack-filter": "4.22.2",
|
||||
"@eslint/js": "10.0.1",
|
||||
"@html-eslint/eslint-plugin": "0.64.0",
|
||||
"@lokalise/node-api": "16.1.0",
|
||||
"@lokalise/node-api": "16.0.0",
|
||||
"@octokit/auth-oauth-device": "8.0.3",
|
||||
"@octokit/plugin-retry": "8.1.0",
|
||||
"@octokit/rest": "22.0.1",
|
||||
"@playwright/test": "1.61.1",
|
||||
"@rsdoctor/rspack-plugin": "1.5.18",
|
||||
"@rspack/core": "2.1.4",
|
||||
"@rspack/core": "2.1.3",
|
||||
"@rspack/dev-server": "2.1.0",
|
||||
"@types/babel__plugin-transform-runtime": "7.9.5",
|
||||
"@types/chromecast-caf-receiver": "6.0.26",
|
||||
@@ -211,7 +211,7 @@
|
||||
"terser-webpack-plugin": "5.6.1",
|
||||
"ts-lit-plugin": "2.0.2",
|
||||
"typescript": "6.0.3",
|
||||
"typescript-eslint": "8.64.0",
|
||||
"typescript-eslint": "8.63.0",
|
||||
"vite-tsconfig-paths": "6.1.1",
|
||||
"vitest": "4.1.10",
|
||||
"webpack-stats-plugin": "1.1.3",
|
||||
|
||||
@@ -1,53 +1,4 @@
|
||||
// Unanchored regex fragments, shared as the single source of truth for both
|
||||
// the boolean validators below and the HTML `pattern` attribute (the browser
|
||||
// anchors a pattern as `^(?:…)$`).
|
||||
const IPV4 =
|
||||
"(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)";
|
||||
const regexp =
|
||||
/^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
|
||||
|
||||
const IPV6 =
|
||||
"(?:([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|::(ffff(:0{1,4})?:)?((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))";
|
||||
|
||||
// CIDR prefix lengths: 0-32 for IPv4, 0-128 for IPv6.
|
||||
const IPV4_PREFIX = "(?:3[0-2]|[12]?[0-9])";
|
||||
const IPV6_PREFIX = "(?:12[0-8]|1[01][0-9]|[1-9]?[0-9])";
|
||||
|
||||
// IPv4 or IPv6 address.
|
||||
export const IP_ADDRESS_PATTERN = `${IPV4}|${IPV6}`;
|
||||
|
||||
// IPv4/IPv6 address, optionally with a CIDR prefix (network).
|
||||
export const IP_ADDRESS_OR_NETWORK_PATTERN = `${IPV4}(?:/${IPV4_PREFIX})?|${IPV6}(?:/${IPV6_PREFIX})?`;
|
||||
|
||||
const anchored = (pattern: string): RegExp => new RegExp(`^(?:${pattern})$`);
|
||||
|
||||
const ipv4Regexp = anchored(IPV4);
|
||||
const ipv6Regexp = anchored(IPV6);
|
||||
|
||||
// IPv4 address, e.g. 192.168.1.10
|
||||
export const isIPAddress = (input: string): boolean => ipv4Regexp.test(input);
|
||||
|
||||
// IPv6 address, e.g. fe80::85d:e82c:9446:7995
|
||||
export const isIPv6Address = (input: string): boolean => ipv6Regexp.test(input);
|
||||
|
||||
// IPv4 or IPv6 address
|
||||
export const isIPAddressV4OrV6 = (input: string): boolean =>
|
||||
isIPAddress(input) || isIPv6Address(input);
|
||||
|
||||
// IP network in CIDR notation, e.g. 192.168.1.0/24 or fd00::/8
|
||||
export const isIPNetwork = (input: string): boolean => {
|
||||
const parts = input.split("/");
|
||||
if (parts.length !== 2) {
|
||||
return false;
|
||||
}
|
||||
const [address, prefix] = parts;
|
||||
if (!/^\d{1,3}$/.test(prefix)) {
|
||||
return false;
|
||||
}
|
||||
const prefixLength = Number(prefix);
|
||||
if (isIPAddress(address)) {
|
||||
return prefixLength >= 0 && prefixLength <= 32;
|
||||
}
|
||||
if (isIPv6Address(address)) {
|
||||
return prefixLength >= 0 && prefixLength <= 128;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
export const isIPAddress = (input: string): boolean => regexp.test(input);
|
||||
|
||||
@@ -73,40 +73,6 @@ interface ClockDatePartSectionData {
|
||||
items: PickerComboBoxItem[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters out sections whose group already has a value in `value`, so only
|
||||
* one non-separator value per group can be selected in the picker. The whole
|
||||
* group of the item at `excludeIndex` (the one being edited) stays
|
||||
* selectable, even if that group has more than one value in `value` (e.g. a
|
||||
* pre-existing config authored outside the picker via YAML).
|
||||
* The separator group is always kept.
|
||||
*/
|
||||
export const getAvailableClockDatePartSections = (
|
||||
sections: ClockDatePartSectionData[],
|
||||
value: string[],
|
||||
excludeIndex?: number
|
||||
): ClockDatePartSectionData[] => {
|
||||
const editedItem = excludeIndex != null ? value[excludeIndex] : undefined;
|
||||
const editedSection = editedItem
|
||||
? getClockDatePartSection(editedItem as ClockCardDatePart)
|
||||
: undefined;
|
||||
|
||||
const usedSections = new Set<ClockDatePartSection>();
|
||||
|
||||
value.forEach((item) => {
|
||||
const section = getClockDatePartSection(item as ClockCardDatePart);
|
||||
|
||||
if (section !== "separator" && section !== editedSection) {
|
||||
usedSections.add(section);
|
||||
}
|
||||
});
|
||||
|
||||
return sections.filter(
|
||||
(sectionData) =>
|
||||
sectionData.id === "separator" || !usedSections.has(sectionData.id)
|
||||
);
|
||||
};
|
||||
|
||||
interface ClockDatePartValueItem {
|
||||
key: string;
|
||||
item: string;
|
||||
@@ -137,17 +103,12 @@ export class HaClockDateFormatPicker extends LitElement {
|
||||
|
||||
@query("ha-generic-picker", true) private _picker?: HaGenericPicker;
|
||||
|
||||
@state() private _editIndex?: number;
|
||||
private _editIndex?: number;
|
||||
|
||||
protected render() {
|
||||
const value = this._value;
|
||||
const valueItems = this._getValueItems(value);
|
||||
const sections = this._buildSections();
|
||||
const pickerSections = getAvailableClockDatePartSections(
|
||||
sections,
|
||||
value,
|
||||
this._editIndex
|
||||
);
|
||||
|
||||
return html`
|
||||
${this.label ? html`<label>${this.label}</label>` : nothing}
|
||||
@@ -156,8 +117,8 @@ export class HaClockDateFormatPicker extends LitElement {
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required && !value.length}
|
||||
.value=${this._getPickerValue()}
|
||||
.sections=${this._getSectionHeaders(pickerSections)}
|
||||
.getItems=${this._getItems(pickerSections)}
|
||||
.sections=${this._getSectionHeaders(sections)}
|
||||
.getItems=${this._getItems(sections)}
|
||||
@value-changed=${this._pickerValueChanged}
|
||||
>
|
||||
<div slot="field" class="container">
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
import type {
|
||||
HaFormBaseSchema,
|
||||
HaFormCondition,
|
||||
HaFormDataContainer,
|
||||
HaFormFieldCondition,
|
||||
HaFormSchema,
|
||||
} from "./types";
|
||||
|
||||
const isEmpty = (value: unknown): boolean =>
|
||||
value === undefined || value === null || value === "";
|
||||
|
||||
const matchFieldCondition = (
|
||||
condition: HaFormFieldCondition,
|
||||
data: HaFormDataContainer | undefined
|
||||
): boolean => {
|
||||
const actual = data?.[condition.field];
|
||||
switch (condition.operator ?? "eq") {
|
||||
case "eq":
|
||||
return actual === condition.value;
|
||||
case "not_eq":
|
||||
return actual !== condition.value;
|
||||
case "in":
|
||||
return (
|
||||
Array.isArray(condition.value) &&
|
||||
condition.value.includes(actual as any)
|
||||
);
|
||||
case "not_in":
|
||||
return (
|
||||
Array.isArray(condition.value) &&
|
||||
!condition.value.includes(actual as any)
|
||||
);
|
||||
case "exists":
|
||||
return !isEmpty(actual);
|
||||
case "not_exists":
|
||||
return isEmpty(actual);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const evaluateCondition = (
|
||||
condition: HaFormCondition,
|
||||
data: HaFormDataContainer | undefined
|
||||
): boolean => {
|
||||
if ("condition" in condition) {
|
||||
switch (condition.condition) {
|
||||
case "and":
|
||||
return condition.conditions.every((c) => evaluateCondition(c, data));
|
||||
case "or":
|
||||
return condition.conditions.some((c) => evaluateCondition(c, data));
|
||||
case "not":
|
||||
return !condition.conditions.some((c) => evaluateCondition(c, data));
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return matchFieldCondition(condition, data);
|
||||
};
|
||||
|
||||
export const isFieldHidden = (
|
||||
schema: HaFormSchema,
|
||||
data: HaFormDataContainer | undefined
|
||||
): boolean => {
|
||||
const { hidden } = schema as HaFormBaseSchema;
|
||||
if (!hidden) {
|
||||
return false;
|
||||
}
|
||||
if (hidden === true) {
|
||||
return true;
|
||||
}
|
||||
const conditions = Array.isArray(hidden) ? hidden : [hidden];
|
||||
return conditions.every((condition) => evaluateCondition(condition, data));
|
||||
};
|
||||
@@ -2,7 +2,6 @@ import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, queryAll } from "lit/decorators";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { isFieldHidden } from "./conditions";
|
||||
import "./ha-form";
|
||||
import type { HaForm } from "./ha-form";
|
||||
import type {
|
||||
@@ -69,21 +68,19 @@ export class HaFormGrid extends LitElement implements HaFormElement {
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
${this.schema.schema
|
||||
.filter((item) => !isFieldHidden(item, this.data))
|
||||
.map(
|
||||
(item) => html`
|
||||
<ha-form
|
||||
.hass=${this.hass}
|
||||
.data=${this.data}
|
||||
.schema=${[item]}
|
||||
.disabled=${this.disabled}
|
||||
.computeLabel=${this.computeLabel}
|
||||
.computeHelper=${this.computeHelper}
|
||||
.localizeValue=${this.localizeValue}
|
||||
></ha-form>
|
||||
`
|
||||
)}
|
||||
${this.schema.schema.map(
|
||||
(item) => html`
|
||||
<ha-form
|
||||
.hass=${this.hass}
|
||||
.data=${this.data}
|
||||
.schema=${[item]}
|
||||
.disabled=${this.disabled}
|
||||
.computeLabel=${this.computeLabel}
|
||||
.computeHelper=${this.computeHelper}
|
||||
.localizeValue=${this.localizeValue}
|
||||
></ha-form>
|
||||
`
|
||||
)}
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { dynamicElement } from "../../common/dom/dynamic-element-directive";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-alert";
|
||||
import "../ha-selector/ha-selector";
|
||||
import { isFieldHidden } from "./conditions";
|
||||
import type { HaFormDataContainer, HaFormElement, HaFormSchema } from "./types";
|
||||
|
||||
const LOAD_ELEMENTS = {
|
||||
@@ -99,11 +98,7 @@ export class HaForm extends LitElement implements HaFormElement {
|
||||
let isValid = true;
|
||||
let firstInvalidElement: HTMLElement | undefined;
|
||||
|
||||
const visibleSchema = this.schema.filter(
|
||||
(item) => !isFieldHidden(item, this.data)
|
||||
);
|
||||
|
||||
visibleSchema.forEach((item, index) => {
|
||||
this.schema.forEach((item, index) => {
|
||||
const element = elements[index];
|
||||
if (!element) {
|
||||
return;
|
||||
@@ -169,10 +164,6 @@ export class HaForm extends LitElement implements HaFormElement {
|
||||
: ""
|
||||
}
|
||||
${this.schema.map((item) => {
|
||||
if (isFieldHidden(item, this.data)) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const error = getError(this.error, item);
|
||||
const warning = getWarning(this.warning, item);
|
||||
|
||||
|
||||
@@ -22,9 +22,6 @@ export interface HaFormBaseSchema {
|
||||
default?: HaFormData;
|
||||
required?: boolean;
|
||||
disabled?: boolean;
|
||||
// Field is hidden while the condition holds. Serializable so it can be
|
||||
// shared with the backend and other renderers.
|
||||
hidden?: boolean | HaFormCondition | HaFormCondition[];
|
||||
description?: {
|
||||
suffix?: string;
|
||||
// This value will be set initially when form is loaded
|
||||
@@ -33,36 +30,6 @@ export interface HaFormBaseSchema {
|
||||
context?: Record<string, string>;
|
||||
}
|
||||
|
||||
export type HaFormConditionOperator =
|
||||
"eq" | "not_eq" | "in" | "not_in" | "exists" | "not_exists";
|
||||
|
||||
export interface HaFormFieldCondition {
|
||||
field: string;
|
||||
operator?: HaFormConditionOperator;
|
||||
value?: HaFormData | readonly HaFormData[];
|
||||
}
|
||||
|
||||
export interface HaFormAndCondition {
|
||||
condition: "and";
|
||||
conditions: readonly HaFormCondition[];
|
||||
}
|
||||
|
||||
export interface HaFormOrCondition {
|
||||
condition: "or";
|
||||
conditions: readonly HaFormCondition[];
|
||||
}
|
||||
|
||||
export interface HaFormNotCondition {
|
||||
condition: "not";
|
||||
conditions: readonly HaFormCondition[];
|
||||
}
|
||||
|
||||
export type HaFormCondition =
|
||||
| HaFormFieldCondition
|
||||
| HaFormAndCondition
|
||||
| HaFormOrCondition
|
||||
| HaFormNotCondition;
|
||||
|
||||
export interface HaFormGridSchema extends HaFormBaseSchema {
|
||||
type: "grid";
|
||||
flatten?: boolean;
|
||||
|
||||
@@ -133,13 +133,7 @@ export class HaAreaSelector extends LitElement {
|
||||
}
|
||||
|
||||
return ensureArray(this.selector.area.entity).some((filter) =>
|
||||
filterSelectorEntities(
|
||||
filter,
|
||||
entity,
|
||||
this._entitySources,
|
||||
this.hass.entities,
|
||||
this.hass.devices
|
||||
)
|
||||
filterSelectorEntities(filter, entity, this._entitySources)
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -147,13 +147,7 @@ export class HaDeviceSelector extends LitElement {
|
||||
|
||||
private _filterEntities = (entity: HassEntity): boolean =>
|
||||
ensureArray(this.selector.device!.entity).some((filter) =>
|
||||
filterSelectorEntities(
|
||||
filter,
|
||||
entity,
|
||||
this._entitySources,
|
||||
this.hass.entities,
|
||||
this.hass.devices
|
||||
)
|
||||
filterSelectorEntities(filter, entity, this._entitySources)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,12 +2,8 @@ import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { ensureArray } from "../../common/array/ensure-array";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { ConfigEntry } from "../../data/config_entries";
|
||||
import { getConfigEntries } from "../../data/config_entries";
|
||||
import { getDeviceIntegrationLookup } from "../../data/device/device_registry";
|
||||
import type { EntitySources } from "../../data/entity/entity_sources";
|
||||
import { fetchEntitySourcesWithCache } from "../../data/entity/entity_sources";
|
||||
import type { EntitySelector } from "../../data/selector";
|
||||
@@ -27,8 +23,6 @@ export class HaEntitySelector extends LitElement {
|
||||
|
||||
@state() private _entitySources?: EntitySources;
|
||||
|
||||
@state() private _configEntries?: ConfigEntry[];
|
||||
|
||||
@property() public value?: any;
|
||||
|
||||
@property() public label?: string;
|
||||
@@ -43,38 +37,12 @@ export class HaEntitySelector extends LitElement {
|
||||
|
||||
@state() private _createDomains: string[] | undefined;
|
||||
|
||||
private _deviceIntegrationLookup = memoizeOne(
|
||||
(
|
||||
entitySources: EntitySources,
|
||||
entities: HomeAssistant["entities"],
|
||||
devices: HomeAssistant["devices"],
|
||||
configEntries?: ConfigEntry[]
|
||||
) =>
|
||||
getDeviceIntegrationLookup(
|
||||
entitySources,
|
||||
Object.values(entities),
|
||||
Object.values(devices),
|
||||
configEntries
|
||||
)
|
||||
);
|
||||
|
||||
// Which async data the current filter needs to be evaluated: a top-level or
|
||||
// device `integration` filter needs entity sources, and a `device.integration`
|
||||
// filter additionally needs config entries (the device integration lookup is
|
||||
// built from both).
|
||||
private _dataNeeds = memoizeOne((selector: EntitySelector) => {
|
||||
const filters = selector.entity?.filter
|
||||
? ensureArray(selector.entity.filter)
|
||||
: [];
|
||||
return {
|
||||
entitySources: filters.some(
|
||||
(f) => f.integration || f.device?.integration
|
||||
),
|
||||
configEntries: filters.some((f) => f.device?.integration),
|
||||
};
|
||||
});
|
||||
|
||||
private _fetchedConfigEntries = false;
|
||||
private _hasIntegration(selector: EntitySelector) {
|
||||
return (
|
||||
selector.entity?.filter &&
|
||||
ensureArray(selector.entity.filter).some((filter) => filter.integration)
|
||||
);
|
||||
}
|
||||
|
||||
protected willUpdate(changedProperties: PropertyValues<this>): void {
|
||||
if (changedProperties.get("selector") && this.value !== undefined) {
|
||||
@@ -89,11 +57,7 @@ export class HaEntitySelector extends LitElement {
|
||||
}
|
||||
|
||||
protected render() {
|
||||
const needs = this._dataNeeds(this.selector);
|
||||
if (
|
||||
(needs.entitySources && !this._entitySources) ||
|
||||
(needs.configEntries && !this._configEntries)
|
||||
) {
|
||||
if (this._hasIntegration(this.selector) && !this._entitySources) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
@@ -132,37 +96,15 @@ export class HaEntitySelector extends LitElement {
|
||||
|
||||
protected updated(changedProps: PropertyValues<this>): void {
|
||||
super.updated(changedProps);
|
||||
|
||||
// The connection changed (e.g. reconnect); refetch config entries.
|
||||
const oldHass = changedProps.get("hass");
|
||||
if (oldHass && oldHass.connection !== this.hass.connection) {
|
||||
this._fetchedConfigEntries = false;
|
||||
this._configEntries = undefined;
|
||||
}
|
||||
|
||||
const needs = this._dataNeeds(this.selector);
|
||||
|
||||
if (needs.entitySources && !this._entitySources) {
|
||||
if (
|
||||
changedProps.has("selector") &&
|
||||
this._hasIntegration(this.selector) &&
|
||||
!this._entitySources
|
||||
) {
|
||||
fetchEntitySourcesWithCache(this.hass).then((sources) => {
|
||||
this._entitySources = sources;
|
||||
});
|
||||
}
|
||||
|
||||
if (needs.configEntries && !this._fetchedConfigEntries) {
|
||||
this._fetchedConfigEntries = true;
|
||||
getConfigEntries(this.hass)
|
||||
.then((entries) => {
|
||||
this._configEntries = entries;
|
||||
})
|
||||
.catch(() => {
|
||||
// Fall back to no entries so the picker still renders. We keep
|
||||
// `_fetchedConfigEntries` set so the failed fetch is not retried on
|
||||
// every re-render; the connection-change handler above retries on
|
||||
// reconnect.
|
||||
this._configEntries = [];
|
||||
});
|
||||
}
|
||||
|
||||
if (changedProps.has("selector")) {
|
||||
this._createDomains = computeCreateDomains(this.selector);
|
||||
}
|
||||
@@ -172,25 +114,8 @@ export class HaEntitySelector extends LitElement {
|
||||
if (!this.selector?.entity?.filter) {
|
||||
return true;
|
||||
}
|
||||
const deviceIntegrationLookup =
|
||||
this._entitySources && this._dataNeeds(this.selector).configEntries
|
||||
? this._deviceIntegrationLookup(
|
||||
this._entitySources,
|
||||
this.hass.entities,
|
||||
this.hass.devices,
|
||||
this._configEntries
|
||||
)
|
||||
: undefined;
|
||||
|
||||
return ensureArray(this.selector.entity.filter).some((filter) =>
|
||||
filterSelectorEntities(
|
||||
filter,
|
||||
entity,
|
||||
this._entitySources,
|
||||
this.hass.entities,
|
||||
this.hass.devices,
|
||||
deviceIntegrationLookup
|
||||
)
|
||||
filterSelectorEntities(filter, entity, this._entitySources)
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -133,13 +133,7 @@ export class HaFloorSelector extends LitElement {
|
||||
}
|
||||
|
||||
return ensureArray(this.selector.floor.entity).some((filter) =>
|
||||
filterSelectorEntities(
|
||||
filter,
|
||||
entity,
|
||||
this._entitySources,
|
||||
this.hass.entities,
|
||||
this.hass.devices
|
||||
)
|
||||
filterSelectorEntities(filter, entity, this._entitySources)
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -93,13 +93,7 @@ export class HaTargetSelector extends LitElement {
|
||||
}
|
||||
|
||||
return ensureArray(this.selector.target.entity).some((filter) =>
|
||||
filterSelectorEntities(
|
||||
filter,
|
||||
entity,
|
||||
this._entitySources,
|
||||
this.hass.entities,
|
||||
this.hass.devices
|
||||
)
|
||||
filterSelectorEntities(filter, entity, this._entitySources)
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -28,10 +28,6 @@ export class HaTextSelector extends LitElement {
|
||||
|
||||
@query("ha-input, ha-textarea") private _input?: HTMLInputElement;
|
||||
|
||||
@query("ha-input-multi") private _inputMulti?: {
|
||||
reportValidity: () => boolean;
|
||||
};
|
||||
|
||||
public async focus() {
|
||||
await this.updateComplete;
|
||||
this._input?.focus();
|
||||
@@ -39,7 +35,7 @@ export class HaTextSelector extends LitElement {
|
||||
|
||||
public reportValidity(): boolean {
|
||||
if (this.selector.text?.multiple) {
|
||||
return this._inputMulti?.reportValidity() ?? true;
|
||||
return true;
|
||||
}
|
||||
return this._input?.reportValidity() ?? true;
|
||||
}
|
||||
@@ -56,8 +52,6 @@ export class HaTextSelector extends LitElement {
|
||||
.inputPrefix=${this.selector.text?.prefix}
|
||||
.helper=${this.helper}
|
||||
.autocomplete=${this.selector.text?.autocomplete}
|
||||
.pattern=${this.selector.text?.pattern}
|
||||
.validationMessage=${this.selector.text?.validation_message}
|
||||
@value-changed=${this._handleChange}
|
||||
>
|
||||
</ha-input-multi>
|
||||
@@ -86,9 +80,6 @@ export class HaTextSelector extends LitElement {
|
||||
.hint=${this.helper}
|
||||
.disabled=${this.disabled}
|
||||
.type=${this.selector.text?.type}
|
||||
.pattern=${this.selector.text?.pattern}
|
||||
.validationMessage=${this.selector.text?.validation_message}
|
||||
.autoValidate=${this.selector.text?.pattern !== undefined}
|
||||
@input=${this._handleChange}
|
||||
@change=${this._handleChange}
|
||||
.label=${this.label || ""}
|
||||
|
||||
@@ -895,13 +895,7 @@ export class HaServiceControl extends LitElement {
|
||||
}
|
||||
if (targetEntities.length) {
|
||||
targetEntities = targetEntities.filter((entity) =>
|
||||
entityMeetsTargetSelector(
|
||||
this.hass.states[entity],
|
||||
targetSelector,
|
||||
undefined,
|
||||
this.hass.entities,
|
||||
this.hass.devices
|
||||
)
|
||||
entityMeetsTargetSelector(this.hass.states[entity], targetSelector)
|
||||
);
|
||||
}
|
||||
target = {
|
||||
|
||||
@@ -2,13 +2,7 @@ import { consume, type ContextType } from "@lit/context";
|
||||
import { mdiDeleteOutline, mdiDragHorizontalVariant, mdiPlus } from "@mdi/js";
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import {
|
||||
customElement,
|
||||
property,
|
||||
query,
|
||||
queryAll,
|
||||
state,
|
||||
} from "lit/decorators";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { uid } from "../../common/util/uid";
|
||||
@@ -56,13 +50,6 @@ class HaInputMulti extends LitElement {
|
||||
|
||||
@property() public autocomplete?: string;
|
||||
|
||||
/** Regular expression each entry is validated against (HTML `pattern`). */
|
||||
@property() public pattern?: string;
|
||||
|
||||
/** Message shown on an entry when it fails `pattern` validation. */
|
||||
@property({ attribute: "validation-message" })
|
||||
public validationMessage?: string;
|
||||
|
||||
@property({ attribute: "add-label" }) public addLabel?: string;
|
||||
|
||||
@property({ attribute: "remove-label" }) public removeLabel?: string;
|
||||
@@ -83,8 +70,6 @@ class HaInputMulti extends LitElement {
|
||||
|
||||
@query("ha-input[data-last]") private _lastInput?: HaInput;
|
||||
|
||||
@queryAll("ha-input") private _inputs?: NodeListOf<HaInput>;
|
||||
|
||||
// Stable key per row, kept in sync with `value`. Because items are plain
|
||||
// strings we cannot use a WeakMap (as the object-based sortable lists do),
|
||||
// so we track keys in a parallel array. Keys stay fixed while a row is
|
||||
@@ -104,16 +89,6 @@ class HaInputMulti extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
public reportValidity(): boolean {
|
||||
let valid = true;
|
||||
this._inputs?.forEach((input) => {
|
||||
if (!input.reportValidity()) {
|
||||
valid = false;
|
||||
}
|
||||
});
|
||||
return valid;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-sortable
|
||||
@@ -134,9 +109,6 @@ class HaInputMulti extends LitElement {
|
||||
.type=${this.inputType}
|
||||
.autocomplete=${this.autocomplete}
|
||||
.disabled=${this.disabled}
|
||||
.pattern=${this.pattern}
|
||||
.validationMessage=${this.validationMessage}
|
||||
.autoValidate=${this.pattern !== undefined}
|
||||
dialogInitialFocus=${index}
|
||||
.index=${index}
|
||||
class="flex-auto"
|
||||
|
||||
@@ -140,7 +140,7 @@ export interface HassioAddonSetOptionParams {
|
||||
export const reloadHassioAddons = async (hass: HomeAssistant) => {
|
||||
await hass.callWS({
|
||||
type: "supervisor/api",
|
||||
endpoint: "/store/reload",
|
||||
endpoint: "/addons/reload",
|
||||
method: "post",
|
||||
});
|
||||
};
|
||||
|
||||
@@ -21,21 +21,6 @@ export interface HttpConfigState {
|
||||
revert_at: string | null;
|
||||
}
|
||||
|
||||
export const HTTP_CONFIG_FIELDS: (keyof HttpConfig)[] = [
|
||||
"server_port",
|
||||
"server_host",
|
||||
"ssl_certificate",
|
||||
"ssl_key",
|
||||
"ssl_peer_certificate",
|
||||
"ssl_profile",
|
||||
"cors_allowed_origins",
|
||||
"use_x_forwarded_for",
|
||||
"trusted_proxies",
|
||||
"use_x_frame_options",
|
||||
"ip_ban_enabled",
|
||||
"login_attempts_threshold",
|
||||
];
|
||||
|
||||
export interface SaveHttpConfigResult {
|
||||
restart: boolean;
|
||||
}
|
||||
|
||||
+10
-57
@@ -266,10 +266,6 @@ interface EntitySelectorFilter {
|
||||
unit_of_measurement?: string | readonly string[];
|
||||
}
|
||||
|
||||
interface EntitySelectorEntityFilter extends EntitySelectorFilter {
|
||||
device?: DeviceSelectorFilter;
|
||||
}
|
||||
|
||||
export interface EntitySelectorExtraOption {
|
||||
id: string;
|
||||
primary: string;
|
||||
@@ -285,7 +281,7 @@ export interface EntitySelector {
|
||||
multiple?: boolean;
|
||||
include_entities?: string[];
|
||||
exclude_entities?: string[];
|
||||
filter?: EntitySelectorEntityFilter | readonly EntitySelectorEntityFilter[];
|
||||
filter?: EntitySelectorFilter | readonly EntitySelectorFilter[];
|
||||
reorder?: boolean;
|
||||
extra_options?: EntitySelectorExtraOption[];
|
||||
} | null;
|
||||
@@ -535,10 +531,6 @@ export interface StringSelector {
|
||||
placeholder?: string;
|
||||
autocomplete?: string;
|
||||
multiple?: true;
|
||||
// Regular expression the value must match (HTML `pattern`); with `multiple`
|
||||
// every entry is validated. `validation_message` is shown when it fails.
|
||||
pattern?: string;
|
||||
validation_message?: string;
|
||||
} | null;
|
||||
}
|
||||
|
||||
@@ -675,9 +667,7 @@ export const expandLabelTarget = (
|
||||
entityMeetsTargetSelector(
|
||||
hass.states[entity.entity_id],
|
||||
targetSelector,
|
||||
entitySources,
|
||||
hass.entities,
|
||||
hass.devices
|
||||
entitySources
|
||||
)
|
||||
) {
|
||||
newEntities.push(entity.entity_id);
|
||||
@@ -743,9 +733,7 @@ export const expandAreaTarget = (
|
||||
entityMeetsTargetSelector(
|
||||
hass.states[entity.entity_id],
|
||||
targetSelector,
|
||||
entitySources,
|
||||
hass.entities,
|
||||
hass.devices
|
||||
entitySources
|
||||
)
|
||||
) {
|
||||
newEntities.push(entity.entity_id);
|
||||
@@ -768,9 +756,7 @@ export const expandDeviceTarget = (
|
||||
entityMeetsTargetSelector(
|
||||
hass.states[entity.entity_id],
|
||||
targetSelector,
|
||||
entitySources,
|
||||
hass.entities,
|
||||
hass.devices
|
||||
entitySources
|
||||
)
|
||||
) {
|
||||
newEntities.push(entity.entity_id);
|
||||
@@ -811,9 +797,7 @@ export const areaMeetsTargetSelector = (
|
||||
entityMeetsTargetSelector(
|
||||
hass.states[entity.entity_id],
|
||||
targetSelector,
|
||||
entitySources,
|
||||
hass.entities,
|
||||
hass.devices
|
||||
entitySources
|
||||
)
|
||||
) {
|
||||
return true;
|
||||
@@ -861,22 +845,14 @@ export const deviceMeetsTargetSelector = (
|
||||
export const entityMeetsTargetSelector = (
|
||||
entity: HassEntity | undefined,
|
||||
targetSelector: TargetSelector,
|
||||
entitySources?: EntitySources,
|
||||
entities?: HomeAssistant["entities"],
|
||||
devices?: HomeAssistant["devices"]
|
||||
entitySources?: EntitySources
|
||||
): boolean => {
|
||||
if (!entity) {
|
||||
return false;
|
||||
}
|
||||
if (targetSelector.target?.entity) {
|
||||
return ensureArray(targetSelector.target!.entity).some((filterEntity) =>
|
||||
filterSelectorEntities(
|
||||
filterEntity,
|
||||
entity,
|
||||
entitySources,
|
||||
entities,
|
||||
devices
|
||||
)
|
||||
filterSelectorEntities(filterEntity, entity, entitySources)
|
||||
);
|
||||
}
|
||||
return true;
|
||||
@@ -915,12 +891,9 @@ export const filterSelectorDevices = (
|
||||
};
|
||||
|
||||
export const filterSelectorEntities = (
|
||||
filterEntity: EntitySelectorEntityFilter,
|
||||
filterEntity: EntitySelectorFilter,
|
||||
entity: HassEntity,
|
||||
entitySources?: EntitySources,
|
||||
entityRegistry?: HomeAssistant["entities"],
|
||||
devices?: HomeAssistant["devices"],
|
||||
deviceIntegrationLookup?: Record<string, Set<string>>
|
||||
entitySources?: EntitySources
|
||||
): boolean => {
|
||||
const {
|
||||
domain: filterDomain,
|
||||
@@ -928,7 +901,6 @@ export const filterSelectorEntities = (
|
||||
supported_features: filterSupportedFeature,
|
||||
unit_of_measurement: filterUnitOfMeasurement,
|
||||
integration: filterIntegration,
|
||||
device: filterDevice,
|
||||
} = filterEntity;
|
||||
|
||||
if (filterDomain) {
|
||||
@@ -975,24 +947,6 @@ export const filterSelectorEntities = (
|
||||
}
|
||||
}
|
||||
|
||||
if (filterDevice) {
|
||||
if (!entityRegistry || !devices) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const deviceId = entityRegistry[entity.entity_id]?.device_id;
|
||||
if (!deviceId) {
|
||||
return false;
|
||||
}
|
||||
const device = devices[deviceId];
|
||||
if (!device) {
|
||||
return false;
|
||||
}
|
||||
if (!filterSelectorDevices(filterDevice, device, deviceIntegrationLookup)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
filterIntegration &&
|
||||
entitySources?.[entity.entity_id]?.domain !== filterIntegration
|
||||
@@ -1062,7 +1016,7 @@ export const handleLegacyDeviceSelector = (
|
||||
export const computeCreateDomains = (
|
||||
selector: EntitySelector | TargetSelector
|
||||
): undefined | string[] => {
|
||||
let entityFilters: EntitySelectorEntityFilter[] | undefined;
|
||||
let entityFilters: EntitySelectorFilter[] | undefined;
|
||||
|
||||
if ("target" in selector) {
|
||||
entityFilters = ensureArray(selector.target?.entity);
|
||||
@@ -1080,7 +1034,6 @@ export const computeCreateDomains = (
|
||||
!entityFilter.integration &&
|
||||
!entityFilter.device_class &&
|
||||
!entityFilter.supported_features &&
|
||||
!entityFilter.device &&
|
||||
entityFilter.domain
|
||||
? ensureArray(entityFilter.domain).filter((domain) =>
|
||||
isHelperDomain(domain)
|
||||
|
||||
@@ -1,27 +1,34 @@
|
||||
import { mdiArrowRight } from "@mdi/js";
|
||||
import { ERR_CONNECTION_LOST } from "home-assistant-js-websocket";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { formatNumericDuration } from "../../common/datetime/format_duration";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { computeRTL } from "../../common/util/compute_rtl";
|
||||
import "../../components/ha-alert";
|
||||
import "../../components/ha-button";
|
||||
import "../../components/ha-dialog-footer";
|
||||
import "../../components/ha-dialog";
|
||||
import "../../components/ha-svg-icon";
|
||||
import type { HttpConfig } from "../../data/http";
|
||||
import {
|
||||
HTTP_CONFIG_FIELDS,
|
||||
promoteHttpConfig,
|
||||
saveHttpConfig,
|
||||
} from "../../data/http";
|
||||
import { promoteHttpConfig, saveHttpConfig } from "../../data/http";
|
||||
import { haStyleDialog } from "../../resources/styles";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { HassDialog } from "../make-dialog-manager";
|
||||
import type { HttpPendingConfigDialogParams } from "./show-dialog-http-pending-config";
|
||||
|
||||
const HTTP_FIELDS: (keyof HttpConfig)[] = [
|
||||
"server_port",
|
||||
"server_host",
|
||||
"ssl_certificate",
|
||||
"ssl_key",
|
||||
"ssl_peer_certificate",
|
||||
"ssl_profile",
|
||||
"cors_allowed_origins",
|
||||
"use_x_forwarded_for",
|
||||
"trusted_proxies",
|
||||
"use_x_frame_options",
|
||||
"ip_ban_enabled",
|
||||
"login_attempts_threshold",
|
||||
];
|
||||
|
||||
@customElement("dialog-http-pending-config")
|
||||
export class DialogHttpPendingConfig
|
||||
extends LitElement
|
||||
@@ -50,10 +57,6 @@ export class DialogHttpPendingConfig
|
||||
this._error = undefined;
|
||||
this._reverted = false;
|
||||
this._startCountdown();
|
||||
// The field labels live in the config panel fragment, which is not loaded
|
||||
// yet when this dialog pops up on startup. Load it so the changed-field
|
||||
// names resolve; the dialog re-renders once hass updates.
|
||||
this.hass.loadFragmentTranslation("config");
|
||||
}
|
||||
|
||||
public closeDialog(): boolean {
|
||||
@@ -111,44 +114,17 @@ export class DialogHttpPendingConfig
|
||||
return [];
|
||||
}
|
||||
const { stable, pending } = this._params.state;
|
||||
return HTTP_CONFIG_FIELDS.filter(
|
||||
return HTTP_FIELDS.filter(
|
||||
(key) => JSON.stringify(stable[key]) !== JSON.stringify(pending[key])
|
||||
);
|
||||
}
|
||||
|
||||
private _formatValue(key: keyof HttpConfig, value: unknown): string {
|
||||
if (value === undefined || value === null || value === "") {
|
||||
return this.hass.localize("ui.dialogs.http_pending_config.not_set");
|
||||
}
|
||||
if (typeof value === "boolean") {
|
||||
return this.hass.localize(value ? "ui.common.yes" : "ui.common.no");
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
return value.length
|
||||
? value.join(", ")
|
||||
: this.hass.localize("ui.dialogs.http_pending_config.not_set");
|
||||
}
|
||||
if (key === "ssl_profile") {
|
||||
return (
|
||||
this.hass.localize(
|
||||
`ui.panel.config.network.http.ssl_profile_${value}` as any
|
||||
) || String(value)
|
||||
);
|
||||
}
|
||||
return String(value);
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._params) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const changes = this._changedFields;
|
||||
const { stable, pending } = this._params.state;
|
||||
const rtl = computeRTL(
|
||||
this.hass.language,
|
||||
this.hass.translationMetadata.translations
|
||||
);
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
@@ -211,25 +187,9 @@ export class DialogHttpPendingConfig
|
||||
${changes.map(
|
||||
(key) => html`
|
||||
<li>
|
||||
<span class="field">
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.network.http.fields.${key}` as any
|
||||
)}
|
||||
</span>
|
||||
<span class="values">
|
||||
<span class="old"
|
||||
>${this._formatValue(key, stable[key])}</span
|
||||
>
|
||||
<ha-svg-icon
|
||||
.path=${mdiArrowRight}
|
||||
style=${styleMap({
|
||||
transform: rtl ? "scaleX(-1)" : "",
|
||||
})}
|
||||
></ha-svg-icon>
|
||||
<span class="new"
|
||||
>${this._formatValue(key, pending![key])}</span
|
||||
>
|
||||
</span>
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.network.http.fields.${key}` as any
|
||||
)}
|
||||
</li>
|
||||
`
|
||||
)}
|
||||
@@ -360,37 +320,15 @@ export class DialogHttpPendingConfig
|
||||
margin-bottom: var(--ha-space-2);
|
||||
}
|
||||
ul {
|
||||
list-style: none;
|
||||
margin: 0 0 var(--ha-space-4) 0;
|
||||
padding: 0;
|
||||
padding-left: var(--ha-space-6);
|
||||
padding-inline-start: var(--ha-space-6);
|
||||
padding-inline-end: initial;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
li {
|
||||
padding: var(--ha-space-2) 0;
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
}
|
||||
li:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
.field {
|
||||
display: block;
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
margin-bottom: var(--ha-space-1);
|
||||
}
|
||||
.values {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--ha-space-2);
|
||||
color: var(--secondary-text-color);
|
||||
word-break: break-word;
|
||||
}
|
||||
.values .new {
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
.values ha-svg-icon {
|
||||
--mdc-icon-size: 18px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
ha-alert {
|
||||
display: block;
|
||||
margin-top: var(--ha-space-4);
|
||||
|
||||
@@ -123,10 +123,6 @@ interface EMOutgoingMessageConnectionStatus extends EMMessage {
|
||||
payload: { event: string };
|
||||
}
|
||||
|
||||
interface EMOutgoingMessageFrontendLoaded extends EMMessage {
|
||||
type: "frontend/loaded"; // Fired once the launch screen is removed (connected and essential data loaded)
|
||||
}
|
||||
|
||||
interface EMOutgoingMessageAppConfiguration extends EMMessage {
|
||||
type: "config_screen/show";
|
||||
}
|
||||
@@ -205,7 +201,6 @@ type EMOutgoingMessageWithoutAnswer =
|
||||
| EMOutgoingMessageBarCodeNotify
|
||||
| EMOutgoingMessageBarCodeScan
|
||||
| EMOutgoingMessageConnectionStatus
|
||||
| EMOutgoingMessageFrontendLoaded
|
||||
| EMOutgoingMessageExoplayerPlayHLS
|
||||
| EMOutgoingMessageExoplayerResize
|
||||
| EMOutgoingMessageExoplayerStop
|
||||
@@ -371,6 +366,7 @@ export interface ExternalConfig {
|
||||
appVersion?: string;
|
||||
hasEntityAddTo?: boolean; // Supports "Add to" from more-info dialog, with action coming from external app
|
||||
hasAssistSettings?: boolean; // Shows the "This device" section in voice assistant settings
|
||||
hasLoader?: boolean; // App renders its own loading screen, so the frontend launch screen stays blank
|
||||
}
|
||||
|
||||
export interface ExternalEntityAddToAction {
|
||||
|
||||
@@ -59,6 +59,9 @@
|
||||
#ha-launch-screen.removing {
|
||||
opacity: 0;
|
||||
}
|
||||
#ha-launch-screen.blank > * {
|
||||
display: none;
|
||||
}
|
||||
#ha-launch-screen svg {
|
||||
width: 112px;
|
||||
flex-shrink: 0;
|
||||
|
||||
@@ -17,6 +17,8 @@ import QuickBarMixin from "../state/quick-bar-mixin";
|
||||
import type { HomeAssistant, Route } from "../types";
|
||||
import { storeState } from "../util/ha-pref-storage";
|
||||
import {
|
||||
blankLaunchScreen,
|
||||
clearLaunchScreenInfoBox,
|
||||
removeLaunchScreen,
|
||||
renderLaunchScreenInfoBox,
|
||||
} from "../util/launch-screen";
|
||||
@@ -59,6 +61,8 @@ export class HomeAssistantAppEl extends QuickBarMixin(HassElement) {
|
||||
|
||||
private _httpPendingDialogOpen = false;
|
||||
|
||||
private _appHandlesLoader = false;
|
||||
|
||||
private _panelUrl: string;
|
||||
|
||||
@storage({ key: "ha-version", state: false, subscribe: false })
|
||||
@@ -120,7 +124,6 @@ export class HomeAssistantAppEl extends QuickBarMixin(HassElement) {
|
||||
this.render = this.renderHass;
|
||||
this.update = super.update;
|
||||
removeLaunchScreen();
|
||||
this.hass.auth.external?.fireMessage({ type: "frontend/loaded" });
|
||||
}
|
||||
super.update(changedProps);
|
||||
}
|
||||
@@ -307,6 +310,11 @@ export class HomeAssistantAppEl extends QuickBarMixin(HassElement) {
|
||||
const { auth, conn } = result;
|
||||
this._checkUpdate(conn);
|
||||
this.initializeHass(auth, conn);
|
||||
if (this.hass?.auth.external?.config.hasLoader) {
|
||||
this._appHandlesLoader = true;
|
||||
blankLaunchScreen();
|
||||
this._renderInitInfo(false);
|
||||
}
|
||||
} catch (_err: any) {
|
||||
this._renderInitInfo(true);
|
||||
}
|
||||
@@ -366,6 +374,10 @@ export class HomeAssistantAppEl extends QuickBarMixin(HassElement) {
|
||||
}
|
||||
|
||||
private _renderInitInfo(error: boolean) {
|
||||
if (this._appHandlesLoader) {
|
||||
clearLaunchScreenInfoBox();
|
||||
return;
|
||||
}
|
||||
renderLaunchScreenInfoBox(
|
||||
html`<ha-init-page
|
||||
.error=${error}
|
||||
|
||||
@@ -3,10 +3,6 @@ import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import {
|
||||
IP_ADDRESS_OR_NETWORK_PATTERN,
|
||||
IP_ADDRESS_PATTERN,
|
||||
} from "../../../common/string/is_ip_address";
|
||||
import type { LocalizeFunc } from "../../../common/translations/localize";
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/ha-button";
|
||||
@@ -14,11 +10,7 @@ import "../../../components/ha-card";
|
||||
import "../../../components/ha-form/ha-form";
|
||||
import type { HaForm } from "../../../components/ha-form/ha-form";
|
||||
import type { SchemaUnion } from "../../../components/ha-form/types";
|
||||
import {
|
||||
fetchHttpConfig,
|
||||
HTTP_CONFIG_FIELDS,
|
||||
saveHttpConfig,
|
||||
} from "../../../data/http";
|
||||
import { fetchHttpConfig, saveHttpConfig } from "../../../data/http";
|
||||
import type { HttpConfig } from "../../../data/http";
|
||||
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
@@ -32,6 +24,10 @@ const SCHEMA = memoizeOne(
|
||||
required: true,
|
||||
selector: { number: { min: 1, max: 65535, mode: "box" } },
|
||||
},
|
||||
{
|
||||
name: "server_host",
|
||||
selector: { text: { multiple: true } },
|
||||
},
|
||||
{
|
||||
name: "ssl",
|
||||
type: "expandable",
|
||||
@@ -85,15 +81,7 @@ const SCHEMA = memoizeOne(
|
||||
},
|
||||
{
|
||||
name: "trusted_proxies",
|
||||
selector: {
|
||||
text: {
|
||||
multiple: true,
|
||||
pattern: IP_ADDRESS_OR_NETWORK_PATTERN,
|
||||
validation_message: localize(
|
||||
"ui.panel.config.network.http.invalid_network"
|
||||
),
|
||||
},
|
||||
},
|
||||
selector: { text: { multiple: true } },
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -120,18 +108,6 @@ const SCHEMA = memoizeOne(
|
||||
flatten: true,
|
||||
title: localize("ui.panel.config.network.http.sections.advanced"),
|
||||
schema: [
|
||||
{
|
||||
name: "server_host",
|
||||
selector: {
|
||||
text: {
|
||||
multiple: true,
|
||||
pattern: IP_ADDRESS_PATTERN,
|
||||
validation_message: localize(
|
||||
"ui.panel.config.network.http.invalid_host"
|
||||
),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "cors_allowed_origins",
|
||||
selector: { text: { multiple: true } },
|
||||
@@ -189,9 +165,6 @@ class HaConfigHttpForm extends LitElement {
|
||||
|
||||
const schema = SCHEMA(this.hass.localize);
|
||||
|
||||
const portChanged =
|
||||
!!this._stable && this._config?.server_port !== this._stable.server_port;
|
||||
|
||||
return html`
|
||||
<ha-card
|
||||
outlined
|
||||
@@ -201,17 +174,6 @@ class HaConfigHttpForm extends LitElement {
|
||||
<p class="description">
|
||||
${this.hass.localize("ui.panel.config.network.http.description")}
|
||||
</p>
|
||||
${
|
||||
portChanged
|
||||
? html`
|
||||
<ha-alert alert-type="warning">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.network.http.port_warning"
|
||||
)}
|
||||
</ha-alert>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
this._error
|
||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||
@@ -356,7 +318,15 @@ class HaConfigHttpForm extends LitElement {
|
||||
) {
|
||||
return;
|
||||
}
|
||||
this._handleSaveError(err);
|
||||
// voluptuous formats errors as "<message> @ data['<field>']".
|
||||
// If a field is identified, mark it inline; otherwise show a card-level
|
||||
// alert.
|
||||
const fieldMatch = err.message?.match(/\bdata\['([^']+)'\]/);
|
||||
if (fieldMatch) {
|
||||
this._fieldErrors = { [fieldMatch[1]]: err.message };
|
||||
} else {
|
||||
this._error = err.message;
|
||||
}
|
||||
} finally {
|
||||
this._saving = false;
|
||||
}
|
||||
@@ -370,34 +340,6 @@ class HaConfigHttpForm extends LitElement {
|
||||
target?.scrollIntoView({ behavior: "smooth", block: "center" });
|
||||
}
|
||||
|
||||
private _handleSaveError(err: any): void {
|
||||
const rawMessage =
|
||||
(typeof err === "string" ? err : err?.message) ||
|
||||
this.hass.localize("ui.panel.config.network.http.save_error");
|
||||
// Voluptuous formats validation errors as
|
||||
// "<reason> @ data['config']['<field>'][<index>]. Got '<value>'"
|
||||
// Strip the internal data path for display and pick the deepest known
|
||||
// field name so it can also be flagged inline.
|
||||
const message =
|
||||
rawMessage.replace(/\s*@\s*data(\['[^']*'\]|\[\d+\])+/g, "").trim() ||
|
||||
rawMessage;
|
||||
const field = [...rawMessage.matchAll(/\['([^']+)'\]/g)]
|
||||
.map((match) => match[1])
|
||||
.reverse()
|
||||
.find((name) => HTTP_CONFIG_FIELDS.includes(name as keyof HttpConfig)) as
|
||||
keyof HttpConfig | undefined;
|
||||
|
||||
if (field) {
|
||||
// Show a card-level alert too — the field may sit in a collapsed section.
|
||||
this._error = `${this.hass.localize(
|
||||
`ui.panel.config.network.http.fields.${field}` as any
|
||||
)}: ${message}`;
|
||||
this._fieldErrors = { [field]: message };
|
||||
} else {
|
||||
this._error = message;
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
|
||||
@@ -49,8 +49,8 @@ import { resolveEntityIDs } from "../../data/selector";
|
||||
import { showAlertDialog } from "../../dialogs/generic/show-dialog-box";
|
||||
import { haStyle, haStyleScrollbar } from "../../resources/styles";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { fileDownload } from "../../util/file_download";
|
||||
import { addEntitiesToLovelaceView } from "../lovelace/editor/add-entities-to-view";
|
||||
import { csvSafeString, csvDownload } from "../../util/csv";
|
||||
|
||||
@customElement("ha-panel-history")
|
||||
class HaPanelHistory extends LitElement {
|
||||
@@ -443,8 +443,8 @@ class HaPanelHistory extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
const csv: string[] = [""]; // headers will be replaced later.
|
||||
const headers = ["entity_id", "state", "last_changed"];
|
||||
const csv: string[][] = [[]]; // headers will be replaced later.
|
||||
const processedDomainAttributes = new Set<string>();
|
||||
const domainAttributes: Record<string, Record<string, number>> = {
|
||||
climate: {
|
||||
@@ -486,7 +486,7 @@ class HaPanelHistory extends LitElement {
|
||||
|
||||
if (entity.statistics) {
|
||||
for (const s of entity.statistics) {
|
||||
csv.push([entityId, s.state, formatDate(s.last_changed)]);
|
||||
csv.push(`${entityId},${s.state},${formatDate(s.last_changed)}\n`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -503,22 +503,25 @@ class HaPanelHistory extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
csv.push(data);
|
||||
csv.push(data.join(",") + "\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const timeline of this._mungedStateHistory.timeline) {
|
||||
const entityId = timeline.entity_id;
|
||||
for (const s of timeline.data) {
|
||||
csv.push([
|
||||
entityId,
|
||||
csvSafeString(s.state),
|
||||
formatDate(s.last_changed),
|
||||
]);
|
||||
const safeState = /,|"/.test(s.state)
|
||||
? `"${s.state.replaceAll('"', '""')}"`
|
||||
: s.state;
|
||||
csv.push(`${entityId},${safeState},${formatDate(s.last_changed)}\n`);
|
||||
}
|
||||
}
|
||||
csv[0] = headers;
|
||||
csvDownload(csv, "history.csv");
|
||||
csv[0] = headers.join(",") + "\n";
|
||||
const blob = new Blob(csv, {
|
||||
type: "text/csv",
|
||||
});
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
fileDownload(url, "history.csv");
|
||||
}
|
||||
|
||||
private _suggestCard() {
|
||||
|
||||
@@ -100,10 +100,6 @@ export class HaLogbook extends LitElement {
|
||||
|
||||
private _readyListenerAttached = false;
|
||||
|
||||
public getEntries(): LogbookEntry[] {
|
||||
return this._logbookEntries || [];
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!isComponentLoaded(this.hass.config, "logbook")) {
|
||||
return nothing;
|
||||
|
||||
@@ -1,15 +1,9 @@
|
||||
import {
|
||||
mdiDotsVertical,
|
||||
mdiDownload,
|
||||
mdiFilterRemove,
|
||||
mdiRefresh,
|
||||
} from "@mdi/js";
|
||||
import { mdiFilterRemove, mdiRefresh } from "@mdi/js";
|
||||
import type { HassServiceTarget } from "home-assistant-js-websocket";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fromUnixTime } from "date-fns";
|
||||
import { storage } from "../../common/decorators/storage";
|
||||
import { navigate } from "../../common/navigate";
|
||||
import { constructUrlCurrentPath } from "../../common/url/construct-url";
|
||||
@@ -24,9 +18,6 @@ import {
|
||||
} from "../../common/url/search-params";
|
||||
import { deepEqual } from "../../common/util/deep-equal";
|
||||
import "../../components/date-picker/ha-date-range-picker";
|
||||
import "../../components/ha-dropdown";
|
||||
import type { HaDropdownSelectEvent } from "../../components/ha-dropdown";
|
||||
import "../../components/ha-dropdown-item";
|
||||
import "../../components/ha-icon-button";
|
||||
import "../../components/ha-target-picker";
|
||||
import "../../components/ha-top-app-bar-fixed";
|
||||
@@ -36,8 +27,6 @@ import { resolveEntityIDs } from "../../data/selector";
|
||||
import { haStyle } from "../../resources/styles";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "./ha-logbook";
|
||||
import { showAlertDialog } from "../../dialogs/generic/show-dialog-box";
|
||||
import { csvDownload, csvSafeString } from "../../util/csv";
|
||||
|
||||
interface LogbookState {
|
||||
time: { range: [Date, Date] };
|
||||
@@ -88,24 +77,12 @@ export class HaPanelLogbook extends LitElement {
|
||||
.path=${mdiFilterRemove}
|
||||
.label=${this.hass.localize("ui.common.reset")}
|
||||
></ha-icon-button>
|
||||
|
||||
<ha-dropdown slot="actionItems" @wa-select=${this._handleMenuAction}>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize("ui.common.menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
|
||||
<ha-dropdown-item value="refresh">
|
||||
${this.hass.localize("ui.common.refresh")}
|
||||
<ha-svg-icon slot="icon" .path=${mdiRefresh}></ha-svg-icon>
|
||||
</ha-dropdown-item>
|
||||
|
||||
<ha-dropdown-item value="download">
|
||||
${this.hass.localize("ui.panel.logbook.download_data")}
|
||||
<ha-svg-icon slot="icon" .path=${mdiDownload}></ha-svg-icon>
|
||||
</ha-dropdown-item>
|
||||
</ha-dropdown>
|
||||
<ha-icon-button
|
||||
slot="actionItems"
|
||||
@click=${this._refreshLogbook}
|
||||
.path=${mdiRefresh}
|
||||
.label=${this.hass!.localize("ui.common.refresh")}
|
||||
></ha-icon-button>
|
||||
|
||||
<div class="content">
|
||||
<div class="filters">
|
||||
@@ -291,67 +268,6 @@ export class HaPanelLogbook extends LitElement {
|
||||
this.shadowRoot!.querySelector("ha-logbook")?.refresh();
|
||||
}
|
||||
|
||||
private async _handleMenuAction(ev: HaDropdownSelectEvent) {
|
||||
const action = ev.detail.item.value;
|
||||
switch (action) {
|
||||
case "download":
|
||||
this._downloadData();
|
||||
break;
|
||||
case "refresh":
|
||||
this._refreshLogbook();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private _downloadData() {
|
||||
const data =
|
||||
this.shadowRoot!.querySelector("ha-logbook")?.getEntries() || [];
|
||||
|
||||
if (data.length === 0) {
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize("ui.panel.logbook.download_data_error"),
|
||||
text: this.hass.localize("ui.panel.logbook.error_no_data"),
|
||||
warning: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const headers = [
|
||||
"time",
|
||||
"entity_id",
|
||||
"state",
|
||||
"event_type",
|
||||
"context_id",
|
||||
"context_user_id",
|
||||
"context_event_type",
|
||||
"context_domain",
|
||||
"context_service",
|
||||
"context_entity_id",
|
||||
"context_state",
|
||||
"context_source",
|
||||
];
|
||||
const csv: string[][] = [headers];
|
||||
|
||||
for (const d of data) {
|
||||
const time = fromUnixTime(d.when).toISOString();
|
||||
csv.push([
|
||||
time,
|
||||
d.entity_id || "",
|
||||
csvSafeString(d.state),
|
||||
csvSafeString(d.attributes?.event_type),
|
||||
d.context_id || "",
|
||||
d.context_user_id || "",
|
||||
csvSafeString(d.context_event_type),
|
||||
d.context_domain || "",
|
||||
d.context_service || "",
|
||||
d.context_entity_id || "",
|
||||
csvSafeString(d.context_state),
|
||||
d.context_source || "",
|
||||
]);
|
||||
}
|
||||
csvDownload(csv, "activity.csv");
|
||||
}
|
||||
|
||||
static get styles() {
|
||||
return [
|
||||
haStyle,
|
||||
|
||||
@@ -23,7 +23,6 @@ import type {
|
||||
LovelaceGridOptions,
|
||||
LovelaceHeaderFooter,
|
||||
} from "../types";
|
||||
import { migrateEntitiesCardConfig } from "./migrate-card-config";
|
||||
import type { EntitiesCardConfig } from "./types";
|
||||
import { haStyleScrollbar } from "../../../resources/styles";
|
||||
|
||||
@@ -50,7 +49,32 @@ export const computeShowHeaderToggle = <
|
||||
return !!config.show_header_toggle;
|
||||
};
|
||||
|
||||
export { migrateEntitiesCardConfig };
|
||||
export const migrateEntitiesCardConfig = (
|
||||
config: EntitiesCardConfig
|
||||
): EntitiesCardConfig => {
|
||||
let changed = false;
|
||||
const newEntities = config.entities?.map((e) => {
|
||||
if (typeof e !== "object") {
|
||||
return e;
|
||||
}
|
||||
if (!("format" in e)) {
|
||||
return e;
|
||||
}
|
||||
changed = true;
|
||||
const { format, ...rest } = e;
|
||||
return {
|
||||
...rest,
|
||||
time_format: (rest as EntityConfig).time_format ?? format,
|
||||
};
|
||||
});
|
||||
if (!changed) {
|
||||
return config;
|
||||
}
|
||||
return {
|
||||
...config,
|
||||
entities: newEntities as (LovelaceRowConfig | string)[],
|
||||
};
|
||||
};
|
||||
|
||||
@customElement("hui-entities-card")
|
||||
class HuiEntitiesCard extends LitElement implements LovelaceCard {
|
||||
|
||||
@@ -31,11 +31,35 @@ import "../components/hui-timestamp-display";
|
||||
import { createEntityNotFoundWarning } from "../components/hui-warning";
|
||||
import "../components/hui-warning-element";
|
||||
import type { LovelaceCard, LovelaceCardEditor } from "../types";
|
||||
import { migrateGlanceCardConfig } from "./migrate-card-config";
|
||||
import type { GlanceCardConfig, GlanceConfigEntity } from "./types";
|
||||
import { TIMESTAMP_STATE_DOMAINS } from "../../../common/const";
|
||||
|
||||
export { migrateGlanceCardConfig };
|
||||
export const migrateGlanceCardConfig = (
|
||||
config: GlanceCardConfig
|
||||
): GlanceCardConfig => {
|
||||
let changed = false;
|
||||
const newEntities = config.entities?.map((e) => {
|
||||
if (typeof e !== "object") {
|
||||
return e;
|
||||
}
|
||||
if (!("format" in e)) {
|
||||
return e;
|
||||
}
|
||||
changed = true;
|
||||
const { format, ...rest } = e;
|
||||
return {
|
||||
...rest,
|
||||
time_format: rest.time_format ?? format,
|
||||
};
|
||||
});
|
||||
if (!changed) {
|
||||
return config;
|
||||
}
|
||||
return {
|
||||
...config,
|
||||
entities: newEntities as (GlanceConfigEntity | string)[],
|
||||
};
|
||||
};
|
||||
|
||||
@customElement("hui-glance-card")
|
||||
export class HuiGlanceCard extends LitElement implements LovelaceCard {
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
import type { EntityConfig, LovelaceRowConfig } from "../entity-rows/types";
|
||||
import type {
|
||||
EntitiesCardConfig,
|
||||
GlanceCardConfig,
|
||||
GlanceConfigEntity,
|
||||
} from "./types";
|
||||
|
||||
export const migrateEntitiesCardConfig = (
|
||||
config: EntitiesCardConfig
|
||||
): EntitiesCardConfig => {
|
||||
let changed = false;
|
||||
const newEntities = config.entities?.map((e) => {
|
||||
if (typeof e !== "object") {
|
||||
return e;
|
||||
}
|
||||
// Custom rows own their config schema and may use `format` with a
|
||||
// different meaning (e.g. custom:multiple-entity-row), so leave it
|
||||
// untouched.
|
||||
if (e.type?.startsWith("custom:")) {
|
||||
return e;
|
||||
}
|
||||
if (!("format" in e)) {
|
||||
return e;
|
||||
}
|
||||
changed = true;
|
||||
const { format, ...rest } = e;
|
||||
return {
|
||||
...rest,
|
||||
time_format: (rest as EntityConfig).time_format ?? format,
|
||||
};
|
||||
});
|
||||
if (!changed) {
|
||||
return config;
|
||||
}
|
||||
return {
|
||||
...config,
|
||||
entities: newEntities as (LovelaceRowConfig | string)[],
|
||||
};
|
||||
};
|
||||
|
||||
export const migrateGlanceCardConfig = (
|
||||
config: GlanceCardConfig
|
||||
): GlanceCardConfig => {
|
||||
let changed = false;
|
||||
const newEntities = config.entities?.map((e) => {
|
||||
if (typeof e !== "object") {
|
||||
return e;
|
||||
}
|
||||
if (!("format" in e)) {
|
||||
return e;
|
||||
}
|
||||
changed = true;
|
||||
const { format, ...rest } = e;
|
||||
return {
|
||||
...rest,
|
||||
time_format: rest.time_format ?? format,
|
||||
};
|
||||
});
|
||||
if (!changed) {
|
||||
return config;
|
||||
}
|
||||
return {
|
||||
...config,
|
||||
entities: newEntities as (GlanceConfigEntity | string)[],
|
||||
};
|
||||
};
|
||||
@@ -54,65 +54,6 @@ const cardConfigStruct = assign(
|
||||
})
|
||||
);
|
||||
|
||||
const SCHEMA = [
|
||||
{ name: "title", selector: { text: {} } },
|
||||
{
|
||||
name: "",
|
||||
type: "grid",
|
||||
schema: [
|
||||
{
|
||||
name: "hours_to_show",
|
||||
default: DEFAULT_HOURS_TO_SHOW,
|
||||
selector: { number: { min: 0, step: "any", mode: "box" } },
|
||||
},
|
||||
{
|
||||
name: "show_names",
|
||||
default: true,
|
||||
required: false,
|
||||
selector: { boolean: {} },
|
||||
},
|
||||
{
|
||||
name: "logarithmic_scale",
|
||||
required: false,
|
||||
selector: { boolean: {} },
|
||||
},
|
||||
{
|
||||
name: "expand_legend",
|
||||
required: false,
|
||||
selector: { boolean: {} },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "",
|
||||
type: "grid",
|
||||
schema: [
|
||||
{
|
||||
name: "min_y_axis",
|
||||
required: false,
|
||||
selector: { number: { mode: "box", step: "any" } },
|
||||
},
|
||||
{
|
||||
name: "max_y_axis",
|
||||
required: false,
|
||||
selector: { number: { mode: "box", step: "any" } },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "fit_y_data",
|
||||
required: false,
|
||||
hidden: {
|
||||
condition: "and",
|
||||
conditions: [
|
||||
{ field: "min_y_axis", operator: "not_exists" },
|
||||
{ field: "max_y_axis", operator: "not_exists" },
|
||||
],
|
||||
},
|
||||
selector: { boolean: {} },
|
||||
},
|
||||
] as const satisfies readonly HaFormSchema[];
|
||||
|
||||
@customElement("hui-history-graph-card-editor")
|
||||
export class HuiHistoryGraphCardEditor
|
||||
extends LitElement
|
||||
@@ -129,6 +70,65 @@ export class HuiHistoryGraphCardEditor
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
private _schema = memoizeOne(
|
||||
(showFitOption: boolean) =>
|
||||
[
|
||||
{ name: "title", selector: { text: {} } },
|
||||
{
|
||||
name: "",
|
||||
type: "grid",
|
||||
schema: [
|
||||
{
|
||||
name: "hours_to_show",
|
||||
default: DEFAULT_HOURS_TO_SHOW,
|
||||
selector: { number: { min: 0, step: "any", mode: "box" } },
|
||||
},
|
||||
{
|
||||
name: "show_names",
|
||||
default: true,
|
||||
required: false,
|
||||
selector: { boolean: {} },
|
||||
},
|
||||
{
|
||||
name: "logarithmic_scale",
|
||||
required: false,
|
||||
selector: { boolean: {} },
|
||||
},
|
||||
{
|
||||
name: "expand_legend",
|
||||
required: false,
|
||||
selector: { boolean: {} },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "",
|
||||
type: "grid",
|
||||
schema: [
|
||||
{
|
||||
name: "min_y_axis",
|
||||
required: false,
|
||||
selector: { number: { mode: "box", step: "any" } },
|
||||
},
|
||||
{
|
||||
name: "max_y_axis",
|
||||
required: false,
|
||||
selector: { number: { mode: "box", step: "any" } },
|
||||
},
|
||||
],
|
||||
},
|
||||
...(showFitOption
|
||||
? [
|
||||
{
|
||||
name: "fit_y_data",
|
||||
required: false,
|
||||
selector: { boolean: {} },
|
||||
},
|
||||
]
|
||||
: []),
|
||||
] as const
|
||||
);
|
||||
|
||||
private _subForm = memoizeOne((localize: LocalizeFunc, entityId: string) => ({
|
||||
schema: [
|
||||
{ name: "entity", selector: { entity: {} }, required: true },
|
||||
@@ -176,6 +176,11 @@ export class HuiHistoryGraphCardEditor
|
||||
`;
|
||||
}
|
||||
|
||||
const schema = this._schema(
|
||||
this._config!.min_y_axis !== undefined ||
|
||||
this._config!.max_y_axis !== undefined
|
||||
);
|
||||
|
||||
const configEntities = this._config.entities
|
||||
? (processEditorEntities(this._config.entities) as GraphEntityConfig[])
|
||||
: [];
|
||||
@@ -183,7 +188,7 @@ export class HuiHistoryGraphCardEditor
|
||||
<ha-form
|
||||
.hass=${this.hass}
|
||||
.data=${this._config}
|
||||
.schema=${SCHEMA}
|
||||
.schema=${schema}
|
||||
.computeLabel=${this._computeLabelCallback}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-form>
|
||||
@@ -278,7 +283,9 @@ export class HuiHistoryGraphCardEditor
|
||||
) as HistoryGraphCardConfig;
|
||||
}
|
||||
|
||||
private _computeLabelCallback = (schema: SchemaUnion<typeof SCHEMA>) => {
|
||||
private _computeLabelCallback = (
|
||||
schema: SchemaUnion<ReturnType<typeof this._schema>>
|
||||
) => {
|
||||
switch (schema.name) {
|
||||
case "show_names":
|
||||
case "logarithmic_scale":
|
||||
|
||||
@@ -92,6 +92,7 @@ export class HuiTileCardEditor
|
||||
(
|
||||
localize: LocalizeFunc,
|
||||
entityId: string | undefined,
|
||||
hideState: boolean,
|
||||
showTimeFormat: boolean
|
||||
) =>
|
||||
[
|
||||
@@ -143,25 +144,31 @@ export class HuiTileCardEditor
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "state_content",
|
||||
hidden: { field: "hide_state", value: true },
|
||||
selector: {
|
||||
ui_state_content: {
|
||||
allow_context: true,
|
||||
},
|
||||
},
|
||||
context: {
|
||||
filter_entity: "entity",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "time_format",
|
||||
hidden: !showTimeFormat,
|
||||
selector: {
|
||||
ui_time_format: {},
|
||||
},
|
||||
},
|
||||
...(!hideState
|
||||
? ([
|
||||
{
|
||||
name: "state_content",
|
||||
selector: {
|
||||
ui_state_content: {
|
||||
allow_context: true,
|
||||
},
|
||||
},
|
||||
context: {
|
||||
filter_entity: "entity",
|
||||
},
|
||||
},
|
||||
] as const satisfies readonly HaFormSchema[])
|
||||
: []),
|
||||
...(showTimeFormat
|
||||
? ([
|
||||
{
|
||||
name: "time_format",
|
||||
selector: {
|
||||
ui_time_format: {},
|
||||
},
|
||||
},
|
||||
] as const satisfies readonly HaFormSchema[])
|
||||
: []),
|
||||
{
|
||||
name: "content_layout",
|
||||
required: true,
|
||||
@@ -286,7 +293,12 @@ export class HuiTileCardEditor
|
||||
this._config.state_content
|
||||
);
|
||||
|
||||
const schema = this._schema(this.hass.localize, entityId, showTimeFormat);
|
||||
const schema = this._schema(
|
||||
this.hass.localize,
|
||||
entityId,
|
||||
this._config.hide_state ?? false,
|
||||
showTimeFormat
|
||||
);
|
||||
|
||||
const vertical = this._config.vertical ?? false;
|
||||
|
||||
|
||||
@@ -1486,7 +1486,6 @@
|
||||
"auto_revert": "Settings will automatically revert in {time}.",
|
||||
"reverted": "Home Assistant reverted the HTTP server settings.",
|
||||
"changes_label": "Changed settings:",
|
||||
"not_set": "Not set",
|
||||
"confirm": "Confirm",
|
||||
"revert": "Revert",
|
||||
"close": "Close",
|
||||
@@ -8680,10 +8679,6 @@
|
||||
"description": "Configure how Home Assistant serves its web interface. Saving restarts Home Assistant.",
|
||||
"save": "Save",
|
||||
"save_no_changes": "Nothing changed — no restart needed.",
|
||||
"save_error": "Could not save the HTTP configuration.",
|
||||
"port_warning": "Clients such as the Home Assistant mobile apps will lose their connection until you update the URL in their settings. If Home Assistant is not confirmed reachable on the new port, the change is rolled back automatically after 5 minutes.",
|
||||
"invalid_host": "Enter a valid IP address.",
|
||||
"invalid_network": "Enter a valid IP address or network.",
|
||||
"save_confirm": {
|
||||
"title": "Restart required",
|
||||
"text": "Saving will restart Home Assistant to apply the new HTTP settings.",
|
||||
@@ -8695,7 +8690,7 @@
|
||||
"ssl": "SSL/TLS",
|
||||
"reverse_proxy": "Reverse proxy",
|
||||
"ip_banning": "IP banning",
|
||||
"advanced": "More options"
|
||||
"advanced": "Advanced"
|
||||
},
|
||||
"fields": {
|
||||
"server_port": "Server port",
|
||||
@@ -11571,11 +11566,6 @@
|
||||
"add_card": "Add current view as card",
|
||||
"add_card_error": "Unable to add card",
|
||||
"error_no_data": "You need to select some data sources first."
|
||||
},
|
||||
"logbook": {
|
||||
"download_data": "[%key:ui::panel::history::download_data%]",
|
||||
"download_data_error": "[%key:ui::panel::history::download_data_error%]",
|
||||
"error_no_data": "No activity for the selected target in the selected time range."
|
||||
}
|
||||
},
|
||||
"tips": {
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
import { fileDownload } from "./file_download";
|
||||
|
||||
export function csvSafeString(s: string | undefined): string {
|
||||
if (!s) return "";
|
||||
return /,|"/.test(s) ? `"${s.replaceAll('"', '""')}"` : s;
|
||||
}
|
||||
|
||||
export function csvDownload(data: string[][], filename: string) {
|
||||
const csv = data.map((row) => row.join(",").concat("\n"));
|
||||
const blob = new Blob(csv, {
|
||||
type: "text/csv",
|
||||
});
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
fileDownload(url, filename);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { TemplateResult } from "lit";
|
||||
import { render } from "lit";
|
||||
import { nothing, render } from "lit";
|
||||
import { parseAnimationDuration } from "../common/util/parse-animation-duration";
|
||||
import { withViewTransition } from "../common/util/view-transition";
|
||||
|
||||
@@ -27,9 +27,21 @@ export const removeLaunchScreen = () => {
|
||||
});
|
||||
};
|
||||
|
||||
export const blankLaunchScreen = () => {
|
||||
const launchScreenElement = document.getElementById("ha-launch-screen");
|
||||
launchScreenElement?.classList.add("blank");
|
||||
};
|
||||
|
||||
export const renderLaunchScreenInfoBox = (content: TemplateResult) => {
|
||||
const infoBoxElement = document.getElementById("ha-launch-screen-info-box");
|
||||
if (infoBoxElement) {
|
||||
render(content, infoBoxElement);
|
||||
}
|
||||
};
|
||||
|
||||
export const clearLaunchScreenInfoBox = () => {
|
||||
const infoBoxElement = document.getElementById("ha-launch-screen-info-box");
|
||||
if (infoBoxElement) {
|
||||
render(nothing, infoBoxElement);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,115 +0,0 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
IP_ADDRESS_OR_NETWORK_PATTERN,
|
||||
IP_ADDRESS_PATTERN,
|
||||
isIPAddress,
|
||||
isIPAddressV4OrV6,
|
||||
isIPNetwork,
|
||||
isIPv6Address,
|
||||
} from "../../../src/common/string/is_ip_address";
|
||||
|
||||
describe("isIPAddress (IPv4)", () => {
|
||||
it("accepts valid IPv4 addresses", () => {
|
||||
expect(isIPAddress("192.168.1.10")).toBe(true);
|
||||
expect(isIPAddress("0.0.0.0")).toBe(true);
|
||||
expect(isIPAddress("255.255.255.255")).toBe(true);
|
||||
});
|
||||
it("rejects invalid IPv4 addresses", () => {
|
||||
expect(isIPAddress("256.1.1.1")).toBe(false);
|
||||
expect(isIPAddress("192.168.1")).toBe(false);
|
||||
expect(isIPAddress("192.168.1.10/24")).toBe(false);
|
||||
expect(isIPAddress("fe80::1")).toBe(false);
|
||||
expect(isIPAddress("")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isIPv6Address", () => {
|
||||
it("accepts valid IPv6 addresses", () => {
|
||||
expect(isIPv6Address("fe80::85d:e82c:9446:7995")).toBe(true);
|
||||
expect(isIPv6Address("::1")).toBe(true);
|
||||
expect(isIPv6Address("1050:0000:0000:0000:0005:0600:300c:326b")).toBe(true);
|
||||
expect(isIPv6Address("::ffff:192.168.1.1")).toBe(true);
|
||||
});
|
||||
it("rejects invalid IPv6 addresses", () => {
|
||||
expect(isIPv6Address("192.168.1.10")).toBe(false);
|
||||
expect(isIPv6Address("fe80::85d::7995")).toBe(false);
|
||||
expect(isIPv6Address("gggg::1")).toBe(false);
|
||||
expect(isIPv6Address("")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isIPAddressV4OrV6", () => {
|
||||
it("accepts both families", () => {
|
||||
expect(isIPAddressV4OrV6("192.168.1.10")).toBe(true);
|
||||
expect(isIPAddressV4OrV6("fe80::1")).toBe(true);
|
||||
});
|
||||
it("rejects networks and garbage", () => {
|
||||
expect(isIPAddressV4OrV6("192.168.1.0/24")).toBe(false);
|
||||
expect(isIPAddressV4OrV6("not-an-ip")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isIPNetwork (CIDR)", () => {
|
||||
it("accepts valid networks", () => {
|
||||
expect(isIPNetwork("192.168.1.0/24")).toBe(true);
|
||||
expect(isIPNetwork("10.0.0.0/8")).toBe(true);
|
||||
expect(isIPNetwork("172.16.0.0/12")).toBe(true);
|
||||
expect(isIPNetwork("0.0.0.0/0")).toBe(true);
|
||||
expect(isIPNetwork("fd00::/8")).toBe(true);
|
||||
expect(isIPNetwork("fe80::/128")).toBe(true);
|
||||
});
|
||||
it("rejects invalid networks", () => {
|
||||
// Prefix out of range — the reported production error.
|
||||
expect(isIPNetwork("172.30.33.0/24444444")).toBe(false);
|
||||
expect(isIPNetwork("192.168.1.0/33")).toBe(false);
|
||||
expect(isIPNetwork("fd00::/129")).toBe(false);
|
||||
expect(isIPNetwork("192.168.1.0")).toBe(false);
|
||||
expect(isIPNetwork("192.168.1.0/24/24")).toBe(false);
|
||||
expect(isIPNetwork("not-a-network/24")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// The HTML `pattern` attribute is anchored by the browser as `^(?:…)$`.
|
||||
const matchesPattern = (pattern: string, value: string): boolean =>
|
||||
new RegExp(`^(?:${pattern})$`).test(value);
|
||||
|
||||
describe("IP_ADDRESS_PATTERN", () => {
|
||||
it("accepts IPv4 and IPv6 addresses", () => {
|
||||
expect(matchesPattern(IP_ADDRESS_PATTERN, "192.168.1.10")).toBe(true);
|
||||
expect(matchesPattern(IP_ADDRESS_PATTERN, "fe80::1")).toBe(true);
|
||||
});
|
||||
it("rejects networks and garbage", () => {
|
||||
expect(matchesPattern(IP_ADDRESS_PATTERN, "192.168.1.0/24")).toBe(false);
|
||||
expect(matchesPattern(IP_ADDRESS_PATTERN, "not-an-ip")).toBe(false);
|
||||
expect(matchesPattern(IP_ADDRESS_PATTERN, "256.1.1.1")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("IP_ADDRESS_OR_NETWORK_PATTERN", () => {
|
||||
it("accepts addresses and networks", () => {
|
||||
expect(matchesPattern(IP_ADDRESS_OR_NETWORK_PATTERN, "192.168.1.10")).toBe(
|
||||
true
|
||||
);
|
||||
expect(
|
||||
matchesPattern(IP_ADDRESS_OR_NETWORK_PATTERN, "192.168.1.0/24")
|
||||
).toBe(true);
|
||||
expect(matchesPattern(IP_ADDRESS_OR_NETWORK_PATTERN, "fd00::/8")).toBe(
|
||||
true
|
||||
);
|
||||
});
|
||||
it("rejects out-of-range prefixes and garbage", () => {
|
||||
// The reported production error.
|
||||
expect(
|
||||
matchesPattern(IP_ADDRESS_OR_NETWORK_PATTERN, "172.30.33.0/24444444")
|
||||
).toBe(false);
|
||||
expect(
|
||||
matchesPattern(IP_ADDRESS_OR_NETWORK_PATTERN, "192.168.1.0/33")
|
||||
).toBe(false);
|
||||
expect(
|
||||
matchesPattern(IP_ADDRESS_OR_NETWORK_PATTERN, "1.1.1.1/233444")
|
||||
).toBe(false);
|
||||
expect(matchesPattern(IP_ADDRESS_OR_NETWORK_PATTERN, "not-an-ip")).toBe(
|
||||
false
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,110 +0,0 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { getAvailableClockDatePartSections } from "../../src/components/ha-clock-date-format-picker";
|
||||
|
||||
type TestSection = "weekday" | "day" | "month" | "year" | "separator";
|
||||
|
||||
const section = (id: TestSection, itemIds: string[]) => ({
|
||||
id,
|
||||
title: id,
|
||||
items: itemIds.map((itemId) => ({ id: itemId, primary: itemId })),
|
||||
});
|
||||
|
||||
const ALL_SECTIONS = [
|
||||
section("day", ["day-numeric", "day-2-digit"]),
|
||||
section("month", ["month-numeric", "month-short", "month-long"]),
|
||||
section("year", ["year-numeric", "year-2-digit"]),
|
||||
section("weekday", ["weekday-short", "weekday-long"]),
|
||||
section("separator", [
|
||||
"separator-dash",
|
||||
"separator-slash",
|
||||
"separator-dot",
|
||||
"separator-new-line",
|
||||
]),
|
||||
];
|
||||
|
||||
describe("getAvailableClockDatePartSections", () => {
|
||||
it("returns every section when no value is set", () => {
|
||||
const result = getAvailableClockDatePartSections(ALL_SECTIONS, []);
|
||||
expect(result.map((sectionData) => sectionData.id)).toEqual([
|
||||
"day",
|
||||
"month",
|
||||
"year",
|
||||
"weekday",
|
||||
"separator",
|
||||
]);
|
||||
});
|
||||
|
||||
it("hides a group's section once a value from that group is used", () => {
|
||||
const result = getAvailableClockDatePartSections(ALL_SECTIONS, [
|
||||
"day-numeric",
|
||||
]);
|
||||
expect(result.map((sectionData) => sectionData.id)).toEqual([
|
||||
"month",
|
||||
"year",
|
||||
"weekday",
|
||||
"separator",
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps the edited item's own group visible via excludeIndex", () => {
|
||||
const value = ["day-numeric", "month-short"];
|
||||
const result = getAvailableClockDatePartSections(ALL_SECTIONS, value, 0);
|
||||
expect(result.map((sectionData) => sectionData.id)).toEqual([
|
||||
"day",
|
||||
"year",
|
||||
"weekday",
|
||||
"separator",
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps the edited item's group visible even when a duplicate of that group exists elsewhere", () => {
|
||||
const value = ["day-numeric", "day-2-digit", "month-long"];
|
||||
const result = getAvailableClockDatePartSections(ALL_SECTIONS, value, 0);
|
||||
expect(result.map((sectionData) => sectionData.id)).toEqual([
|
||||
"day",
|
||||
"year",
|
||||
"weekday",
|
||||
"separator",
|
||||
]);
|
||||
});
|
||||
|
||||
it("does not crash and keeps normal filtering when excludeIndex is out of range", () => {
|
||||
const value = ["day-numeric", "month-long"];
|
||||
const result = getAvailableClockDatePartSections(ALL_SECTIONS, value, 5);
|
||||
expect(result.map((sectionData) => sectionData.id)).toEqual([
|
||||
"year",
|
||||
"weekday",
|
||||
"separator",
|
||||
]);
|
||||
});
|
||||
|
||||
it("never hides the separator section, even with multiple separators used", () => {
|
||||
const value = [
|
||||
"separator-dash",
|
||||
"separator-slash",
|
||||
"separator-dot",
|
||||
"separator-new-line",
|
||||
];
|
||||
const result = getAvailableClockDatePartSections(ALL_SECTIONS, value);
|
||||
expect(result.map((sectionData) => sectionData.id)).toEqual([
|
||||
"day",
|
||||
"month",
|
||||
"year",
|
||||
"weekday",
|
||||
"separator",
|
||||
]);
|
||||
});
|
||||
|
||||
it("treats an unrecognized token as a separator and does not hide any group", () => {
|
||||
const result = getAvailableClockDatePartSections(ALL_SECTIONS, [
|
||||
"not-a-real-part",
|
||||
]);
|
||||
expect(result.map((sectionData) => sectionData.id)).toEqual([
|
||||
"day",
|
||||
"month",
|
||||
"year",
|
||||
"weekday",
|
||||
"separator",
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -1,134 +0,0 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { isFieldHidden } from "../../../src/components/ha-form/conditions";
|
||||
import type { HaFormSchema } from "../../../src/components/ha-form/types";
|
||||
|
||||
const field = (hidden: HaFormSchema["hidden"]): HaFormSchema =>
|
||||
({ name: "field", selector: { text: {} }, hidden }) as HaFormSchema;
|
||||
|
||||
describe("isFieldHidden", () => {
|
||||
it("shows a field without a hidden condition", () => {
|
||||
expect(isFieldHidden(field(undefined), { a: 1 })).toBe(false);
|
||||
});
|
||||
|
||||
it("honors a boolean hidden", () => {
|
||||
expect(isFieldHidden(field(true), {})).toBe(true);
|
||||
expect(isFieldHidden(field(false), {})).toBe(false);
|
||||
});
|
||||
|
||||
describe("operators", () => {
|
||||
it("eq (default) matches equal values", () => {
|
||||
expect(isFieldHidden(field({ field: "a", value: 1 }), { a: 1 })).toBe(
|
||||
true
|
||||
);
|
||||
expect(isFieldHidden(field({ field: "a", value: 1 }), { a: 2 })).toBe(
|
||||
false
|
||||
);
|
||||
});
|
||||
|
||||
it("not_eq matches different values", () => {
|
||||
const schema = field({ field: "a", operator: "not_eq", value: 1 });
|
||||
expect(isFieldHidden(schema, { a: 2 })).toBe(true);
|
||||
expect(isFieldHidden(schema, { a: 1 })).toBe(false);
|
||||
});
|
||||
|
||||
it("in matches membership", () => {
|
||||
const schema = field({ field: "a", operator: "in", value: ["x", "y"] });
|
||||
expect(isFieldHidden(schema, { a: "y" })).toBe(true);
|
||||
expect(isFieldHidden(schema, { a: "z" })).toBe(false);
|
||||
});
|
||||
|
||||
it("not_in matches non-membership", () => {
|
||||
const schema = field({
|
||||
field: "a",
|
||||
operator: "not_in",
|
||||
value: ["x", "y"],
|
||||
});
|
||||
expect(isFieldHidden(schema, { a: "z" })).toBe(true);
|
||||
expect(isFieldHidden(schema, { a: "x" })).toBe(false);
|
||||
});
|
||||
|
||||
it("exists matches a defined non-empty value", () => {
|
||||
const schema = field({ field: "a", operator: "exists" });
|
||||
expect(isFieldHidden(schema, { a: "x" })).toBe(true);
|
||||
expect(isFieldHidden(schema, { a: "" })).toBe(false);
|
||||
expect(isFieldHidden(schema, {})).toBe(false);
|
||||
});
|
||||
|
||||
it("not_exists matches a missing or empty value", () => {
|
||||
const schema = field({ field: "a", operator: "not_exists" });
|
||||
expect(isFieldHidden(schema, {})).toBe(true);
|
||||
expect(isFieldHidden(schema, { a: null } as any)).toBe(true);
|
||||
expect(isFieldHidden(schema, { a: "x" })).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("combinators", () => {
|
||||
it("and requires every condition", () => {
|
||||
const schema = field({
|
||||
condition: "and",
|
||||
conditions: [
|
||||
{ field: "a", value: 1 },
|
||||
{ field: "b", value: 2 },
|
||||
],
|
||||
});
|
||||
expect(isFieldHidden(schema, { a: 1, b: 2 })).toBe(true);
|
||||
expect(isFieldHidden(schema, { a: 1, b: 9 })).toBe(false);
|
||||
});
|
||||
|
||||
it("or requires any condition", () => {
|
||||
const schema = field({
|
||||
condition: "or",
|
||||
conditions: [
|
||||
{ field: "a", value: 1 },
|
||||
{ field: "b", value: 2 },
|
||||
],
|
||||
});
|
||||
expect(isFieldHidden(schema, { a: 9, b: 2 })).toBe(true);
|
||||
expect(isFieldHidden(schema, { a: 9, b: 9 })).toBe(false);
|
||||
});
|
||||
|
||||
it("not negates its conditions", () => {
|
||||
const schema = field({
|
||||
condition: "not",
|
||||
conditions: [{ field: "a", value: 1 }],
|
||||
});
|
||||
expect(isFieldHidden(schema, { a: 2 })).toBe(true);
|
||||
expect(isFieldHidden(schema, { a: 1 })).toBe(false);
|
||||
});
|
||||
|
||||
it("nests combinators", () => {
|
||||
const schema = field({
|
||||
condition: "and",
|
||||
conditions: [
|
||||
{ field: "a", value: 1 },
|
||||
{
|
||||
condition: "or",
|
||||
conditions: [
|
||||
{ field: "b", value: 2 },
|
||||
{ field: "c", value: 3 },
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(isFieldHidden(schema, { a: 1, b: 9, c: 3 })).toBe(true);
|
||||
expect(isFieldHidden(schema, { a: 1, b: 9, c: 9 })).toBe(false);
|
||||
expect(isFieldHidden(schema, { a: 9, b: 2, c: 3 })).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it("treats an array of conditions as AND", () => {
|
||||
const schema = field([
|
||||
{ field: "a", value: 1 },
|
||||
{ field: "b", value: 2 },
|
||||
]);
|
||||
expect(isFieldHidden(schema, { a: 1, b: 2 })).toBe(true);
|
||||
expect(isFieldHidden(schema, { a: 1, b: 9 })).toBe(false);
|
||||
});
|
||||
|
||||
it("handles missing data", () => {
|
||||
expect(isFieldHidden(field({ field: "a", value: 1 }), undefined)).toBe(
|
||||
false
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,134 +0,0 @@
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { DeviceRegistryEntry } from "../../src/data/device/device_registry";
|
||||
import { filterSelectorEntities } from "../../src/data/selector";
|
||||
import type { HomeAssistant } from "../../src/types";
|
||||
|
||||
const entity = {
|
||||
entity_id: "light.living_room",
|
||||
state: "on",
|
||||
attributes: {},
|
||||
} as HassEntity;
|
||||
|
||||
const entityRegistry = {
|
||||
"light.living_room": { device_id: "device_1" },
|
||||
} as unknown as HomeAssistant["entities"];
|
||||
|
||||
const devices = {
|
||||
device_1: {
|
||||
id: "device_1",
|
||||
manufacturer: "Signify",
|
||||
model: "Hue Bulb",
|
||||
model_id: "LCT015",
|
||||
} as DeviceRegistryEntry,
|
||||
} as unknown as HomeAssistant["devices"];
|
||||
|
||||
describe("filterSelectorEntities device filter", () => {
|
||||
it("matches when the nested device manufacturer matches", () => {
|
||||
expect(
|
||||
filterSelectorEntities(
|
||||
{ device: { manufacturer: "Signify" } },
|
||||
entity,
|
||||
undefined,
|
||||
entityRegistry,
|
||||
devices
|
||||
)
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("does not match when the nested device manufacturer differs", () => {
|
||||
expect(
|
||||
filterSelectorEntities(
|
||||
{ device: { manufacturer: "Sonos" } },
|
||||
entity,
|
||||
undefined,
|
||||
entityRegistry,
|
||||
devices
|
||||
)
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("matches when model and model_id both match", () => {
|
||||
expect(
|
||||
filterSelectorEntities(
|
||||
{ device: { model: "Hue Bulb", model_id: "LCT015" } },
|
||||
entity,
|
||||
undefined,
|
||||
entityRegistry,
|
||||
devices
|
||||
)
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("does not match when one of model or model_id differs", () => {
|
||||
expect(
|
||||
filterSelectorEntities(
|
||||
{ device: { model: "Hue Bulb", model_id: "OTHER" } },
|
||||
entity,
|
||||
undefined,
|
||||
entityRegistry,
|
||||
devices
|
||||
)
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("matches the device integration via the lookup", () => {
|
||||
expect(
|
||||
filterSelectorEntities(
|
||||
{ device: { integration: "hue" } },
|
||||
entity,
|
||||
undefined,
|
||||
entityRegistry,
|
||||
devices,
|
||||
{ device_1: new Set(["hue"]) }
|
||||
)
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("does not match a device integration that is absent from the lookup", () => {
|
||||
expect(
|
||||
filterSelectorEntities(
|
||||
{ device: { integration: "zha" } },
|
||||
entity,
|
||||
undefined,
|
||||
entityRegistry,
|
||||
devices,
|
||||
{ device_1: new Set(["hue"]) }
|
||||
)
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("does not match when the entity has no underlying device", () => {
|
||||
expect(
|
||||
filterSelectorEntities(
|
||||
{ device: { manufacturer: "Signify" } },
|
||||
entity,
|
||||
undefined,
|
||||
{} as HomeAssistant["entities"],
|
||||
devices
|
||||
)
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("combines device conditions with other entity conditions (AND)", () => {
|
||||
expect(
|
||||
filterSelectorEntities(
|
||||
{ domain: "light", device: { manufacturer: "Signify" } },
|
||||
entity,
|
||||
undefined,
|
||||
entityRegistry,
|
||||
devices
|
||||
)
|
||||
).toBe(true);
|
||||
|
||||
expect(
|
||||
filterSelectorEntities(
|
||||
{ domain: "switch", device: { manufacturer: "Signify" } },
|
||||
entity,
|
||||
undefined,
|
||||
entityRegistry,
|
||||
devices
|
||||
)
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -1,127 +0,0 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
migrateEntitiesCardConfig,
|
||||
migrateGlanceCardConfig,
|
||||
} from "../../../../src/panels/lovelace/cards/migrate-card-config";
|
||||
import type {
|
||||
EntitiesCardConfig,
|
||||
GlanceCardConfig,
|
||||
} from "../../../../src/panels/lovelace/cards/types";
|
||||
|
||||
describe("migrateEntitiesCardConfig", () => {
|
||||
it("migrates the legacy `format` option to `time_format` on native rows", () => {
|
||||
const config = {
|
||||
type: "entities",
|
||||
entities: [{ entity: "sensor.last_changed", format: "relative" }],
|
||||
} as unknown as EntitiesCardConfig;
|
||||
|
||||
const result = migrateEntitiesCardConfig(config);
|
||||
|
||||
expect(result.entities).toEqual([
|
||||
{ entity: "sensor.last_changed", time_format: "relative" },
|
||||
]);
|
||||
// `format` is dropped after migration
|
||||
expect(result.entities[0]).not.toHaveProperty("format");
|
||||
});
|
||||
|
||||
it("leaves custom rows untouched", () => {
|
||||
const customRow = {
|
||||
type: "custom:multiple-entity-row",
|
||||
entity: "sensor.power",
|
||||
format: "precision1",
|
||||
};
|
||||
const config = {
|
||||
type: "entities",
|
||||
entities: [customRow],
|
||||
} as unknown as EntitiesCardConfig;
|
||||
|
||||
const result = migrateEntitiesCardConfig(config);
|
||||
|
||||
// Nothing changed, so the same config reference is returned and the
|
||||
// custom row keeps its own `format` semantics.
|
||||
expect(result).toBe(config);
|
||||
expect(result.entities[0]).toEqual(customRow);
|
||||
});
|
||||
|
||||
it("returns the same config reference when there is nothing to migrate", () => {
|
||||
const config = {
|
||||
type: "entities",
|
||||
entities: [{ entity: "sensor.temperature" }, { entity: "light.kitchen" }],
|
||||
} as EntitiesCardConfig;
|
||||
|
||||
expect(migrateEntitiesCardConfig(config)).toBe(config);
|
||||
});
|
||||
|
||||
it("passes string rows through untouched", () => {
|
||||
const config = {
|
||||
type: "entities",
|
||||
entities: ["sensor.temperature", { entity: "sensor.x", format: "time" }],
|
||||
} as EntitiesCardConfig;
|
||||
|
||||
const result = migrateEntitiesCardConfig(config);
|
||||
|
||||
expect(result.entities[0]).toBe("sensor.temperature");
|
||||
expect(result.entities[1]).toEqual({
|
||||
entity: "sensor.x",
|
||||
time_format: "time",
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps a pre-existing `time_format` over the legacy `format`", () => {
|
||||
const config = {
|
||||
type: "entities",
|
||||
entities: [
|
||||
{ entity: "sensor.x", format: "relative", time_format: "datetime" },
|
||||
],
|
||||
} as unknown as EntitiesCardConfig;
|
||||
|
||||
const result = migrateEntitiesCardConfig(config);
|
||||
|
||||
expect(result.entities[0]).toEqual({
|
||||
entity: "sensor.x",
|
||||
time_format: "datetime",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("migrateGlanceCardConfig", () => {
|
||||
it("migrates the legacy `format` option to `time_format`", () => {
|
||||
const config = {
|
||||
type: "glance",
|
||||
entities: [{ entity: "sensor.last_changed", format: "relative" }],
|
||||
} as unknown as GlanceCardConfig;
|
||||
|
||||
const result = migrateGlanceCardConfig(config);
|
||||
|
||||
expect(result.entities).toEqual([
|
||||
{ entity: "sensor.last_changed", time_format: "relative" },
|
||||
]);
|
||||
expect(result.entities[0]).not.toHaveProperty("format");
|
||||
});
|
||||
|
||||
it("returns the same config reference when there is nothing to migrate", () => {
|
||||
const config = {
|
||||
type: "glance",
|
||||
entities: ["sensor.temperature", { entity: "light.kitchen" }],
|
||||
} as GlanceCardConfig;
|
||||
|
||||
expect(migrateGlanceCardConfig(config)).toBe(config);
|
||||
});
|
||||
|
||||
it("keeps a pre-existing `time_format` over the legacy `format`", () => {
|
||||
const config = {
|
||||
type: "glance",
|
||||
entities: [
|
||||
{ entity: "sensor.x", format: "relative", time_format: "datetime" },
|
||||
],
|
||||
} as unknown as GlanceCardConfig;
|
||||
|
||||
const result = migrateGlanceCardConfig(config);
|
||||
|
||||
expect(result.entities[0]).toEqual({
|
||||
entity: "sensor.x",
|
||||
time_format: "datetime",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -2566,13 +2566,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@emnapi/core@npm:1.11.2":
|
||||
version: 1.11.2
|
||||
resolution: "@emnapi/core@npm:1.11.2"
|
||||
"@emnapi/core@npm:1.11.1":
|
||||
version: 1.11.1
|
||||
resolution: "@emnapi/core@npm:1.11.1"
|
||||
dependencies:
|
||||
"@emnapi/wasi-threads": "npm:1.2.2"
|
||||
tslib: "npm:^2.4.0"
|
||||
checksum: 10/b3e9693f79ca1d8bb90cb8a950150c7738f54eca0ae1849d3d5103a87ce4e388c2669fb253cc123d3449ad2ae12583435455dbdc0270b1e9efb0cfd502c952b4
|
||||
checksum: 10/9aba37e0c11a75ef8372fd0a9c6e5396f4e8c1ebdd6fee737414787610a9dc1cd9bf188f525153561ca9363896e1135dd240f1ce28f3470dba3ad7e683e6db1a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -2585,12 +2585,12 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@emnapi/runtime@npm:1.11.2":
|
||||
version: 1.11.2
|
||||
resolution: "@emnapi/runtime@npm:1.11.2"
|
||||
"@emnapi/runtime@npm:1.11.1":
|
||||
version: 1.11.1
|
||||
resolution: "@emnapi/runtime@npm:1.11.1"
|
||||
dependencies:
|
||||
tslib: "npm:^2.4.0"
|
||||
checksum: 10/280a219d3bf302615dabaaa248223b0e5fe9c49bacf0987dd958ca37ab425b62cf05287053cb188e711681418aaa5e447eaed6b62a0848c0a1303b2707864600
|
||||
checksum: 10/8f7c622a49314df4d07952110e108e83b0fe129a8ddb9ef1e0ae372d754616169d5b0dd47a0d354a0fea2612abe42cedb582d15916936d1320c6c468acc804cc
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -3434,10 +3434,10 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@lokalise/node-api@npm:16.1.0":
|
||||
version: 16.1.0
|
||||
resolution: "@lokalise/node-api@npm:16.1.0"
|
||||
checksum: 10/f77f9e7d25af18be950f15750863061d00382f0abd2cfab1461b4354ba3d7ecc514e30811bc656e9cf382632d205c099d5d3d974b487dba75ff00dc9c1e193e0
|
||||
"@lokalise/node-api@npm:16.0.0":
|
||||
version: 16.0.0
|
||||
resolution: "@lokalise/node-api@npm:16.0.0"
|
||||
checksum: 10/88075629f30feb19537f8c40dabf7a85cc0539445322c216686f7e5a68ba6735a083d7ce1eee806ce31606d28ee6250ca39b185a783ce5ec263e1e0e71c3a701
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -4735,110 +4735,110 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rspack/binding-darwin-arm64@npm:2.1.4":
|
||||
version: 2.1.4
|
||||
resolution: "@rspack/binding-darwin-arm64@npm:2.1.4"
|
||||
"@rspack/binding-darwin-arm64@npm:2.1.3":
|
||||
version: 2.1.3
|
||||
resolution: "@rspack/binding-darwin-arm64@npm:2.1.3"
|
||||
conditions: os=darwin & cpu=arm64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rspack/binding-darwin-x64@npm:2.1.4":
|
||||
version: 2.1.4
|
||||
resolution: "@rspack/binding-darwin-x64@npm:2.1.4"
|
||||
"@rspack/binding-darwin-x64@npm:2.1.3":
|
||||
version: 2.1.3
|
||||
resolution: "@rspack/binding-darwin-x64@npm:2.1.3"
|
||||
conditions: os=darwin & cpu=x64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rspack/binding-linux-arm64-gnu@npm:2.1.4":
|
||||
version: 2.1.4
|
||||
resolution: "@rspack/binding-linux-arm64-gnu@npm:2.1.4"
|
||||
"@rspack/binding-linux-arm64-gnu@npm:2.1.3":
|
||||
version: 2.1.3
|
||||
resolution: "@rspack/binding-linux-arm64-gnu@npm:2.1.3"
|
||||
conditions: os=linux & cpu=arm64 & libc=glibc
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rspack/binding-linux-arm64-musl@npm:2.1.4":
|
||||
version: 2.1.4
|
||||
resolution: "@rspack/binding-linux-arm64-musl@npm:2.1.4"
|
||||
"@rspack/binding-linux-arm64-musl@npm:2.1.3":
|
||||
version: 2.1.3
|
||||
resolution: "@rspack/binding-linux-arm64-musl@npm:2.1.3"
|
||||
conditions: os=linux & cpu=arm64 & libc=musl
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rspack/binding-linux-riscv64-gnu@npm:2.1.4":
|
||||
version: 2.1.4
|
||||
resolution: "@rspack/binding-linux-riscv64-gnu@npm:2.1.4"
|
||||
"@rspack/binding-linux-riscv64-gnu@npm:2.1.3":
|
||||
version: 2.1.3
|
||||
resolution: "@rspack/binding-linux-riscv64-gnu@npm:2.1.3"
|
||||
conditions: os=linux & cpu=riscv64 & libc=glibc
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rspack/binding-linux-riscv64-musl@npm:2.1.4":
|
||||
version: 2.1.4
|
||||
resolution: "@rspack/binding-linux-riscv64-musl@npm:2.1.4"
|
||||
"@rspack/binding-linux-riscv64-musl@npm:2.1.3":
|
||||
version: 2.1.3
|
||||
resolution: "@rspack/binding-linux-riscv64-musl@npm:2.1.3"
|
||||
conditions: os=linux & cpu=riscv64 & libc=musl
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rspack/binding-linux-x64-gnu@npm:2.1.4":
|
||||
version: 2.1.4
|
||||
resolution: "@rspack/binding-linux-x64-gnu@npm:2.1.4"
|
||||
"@rspack/binding-linux-x64-gnu@npm:2.1.3":
|
||||
version: 2.1.3
|
||||
resolution: "@rspack/binding-linux-x64-gnu@npm:2.1.3"
|
||||
conditions: os=linux & cpu=x64 & libc=glibc
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rspack/binding-linux-x64-musl@npm:2.1.4":
|
||||
version: 2.1.4
|
||||
resolution: "@rspack/binding-linux-x64-musl@npm:2.1.4"
|
||||
"@rspack/binding-linux-x64-musl@npm:2.1.3":
|
||||
version: 2.1.3
|
||||
resolution: "@rspack/binding-linux-x64-musl@npm:2.1.3"
|
||||
conditions: os=linux & cpu=x64 & libc=musl
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rspack/binding-wasm32-wasi@npm:2.1.4":
|
||||
version: 2.1.4
|
||||
resolution: "@rspack/binding-wasm32-wasi@npm:2.1.4"
|
||||
"@rspack/binding-wasm32-wasi@npm:2.1.3":
|
||||
version: 2.1.3
|
||||
resolution: "@rspack/binding-wasm32-wasi@npm:2.1.3"
|
||||
dependencies:
|
||||
"@emnapi/core": "npm:1.11.2"
|
||||
"@emnapi/runtime": "npm:1.11.2"
|
||||
"@emnapi/core": "npm:1.11.1"
|
||||
"@emnapi/runtime": "npm:1.11.1"
|
||||
"@napi-rs/wasm-runtime": "npm:1.1.6"
|
||||
conditions: cpu=wasm32
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rspack/binding-win32-arm64-msvc@npm:2.1.4":
|
||||
version: 2.1.4
|
||||
resolution: "@rspack/binding-win32-arm64-msvc@npm:2.1.4"
|
||||
"@rspack/binding-win32-arm64-msvc@npm:2.1.3":
|
||||
version: 2.1.3
|
||||
resolution: "@rspack/binding-win32-arm64-msvc@npm:2.1.3"
|
||||
conditions: os=win32 & cpu=arm64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rspack/binding-win32-ia32-msvc@npm:2.1.4":
|
||||
version: 2.1.4
|
||||
resolution: "@rspack/binding-win32-ia32-msvc@npm:2.1.4"
|
||||
"@rspack/binding-win32-ia32-msvc@npm:2.1.3":
|
||||
version: 2.1.3
|
||||
resolution: "@rspack/binding-win32-ia32-msvc@npm:2.1.3"
|
||||
conditions: os=win32 & cpu=ia32
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rspack/binding-win32-x64-msvc@npm:2.1.4":
|
||||
version: 2.1.4
|
||||
resolution: "@rspack/binding-win32-x64-msvc@npm:2.1.4"
|
||||
"@rspack/binding-win32-x64-msvc@npm:2.1.3":
|
||||
version: 2.1.3
|
||||
resolution: "@rspack/binding-win32-x64-msvc@npm:2.1.3"
|
||||
conditions: os=win32 & cpu=x64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rspack/binding@npm:2.1.4":
|
||||
version: 2.1.4
|
||||
resolution: "@rspack/binding@npm:2.1.4"
|
||||
"@rspack/binding@npm:2.1.3":
|
||||
version: 2.1.3
|
||||
resolution: "@rspack/binding@npm:2.1.3"
|
||||
dependencies:
|
||||
"@rspack/binding-darwin-arm64": "npm:2.1.4"
|
||||
"@rspack/binding-darwin-x64": "npm:2.1.4"
|
||||
"@rspack/binding-linux-arm64-gnu": "npm:2.1.4"
|
||||
"@rspack/binding-linux-arm64-musl": "npm:2.1.4"
|
||||
"@rspack/binding-linux-riscv64-gnu": "npm:2.1.4"
|
||||
"@rspack/binding-linux-riscv64-musl": "npm:2.1.4"
|
||||
"@rspack/binding-linux-x64-gnu": "npm:2.1.4"
|
||||
"@rspack/binding-linux-x64-musl": "npm:2.1.4"
|
||||
"@rspack/binding-wasm32-wasi": "npm:2.1.4"
|
||||
"@rspack/binding-win32-arm64-msvc": "npm:2.1.4"
|
||||
"@rspack/binding-win32-ia32-msvc": "npm:2.1.4"
|
||||
"@rspack/binding-win32-x64-msvc": "npm:2.1.4"
|
||||
"@rspack/binding-darwin-arm64": "npm:2.1.3"
|
||||
"@rspack/binding-darwin-x64": "npm:2.1.3"
|
||||
"@rspack/binding-linux-arm64-gnu": "npm:2.1.3"
|
||||
"@rspack/binding-linux-arm64-musl": "npm:2.1.3"
|
||||
"@rspack/binding-linux-riscv64-gnu": "npm:2.1.3"
|
||||
"@rspack/binding-linux-riscv64-musl": "npm:2.1.3"
|
||||
"@rspack/binding-linux-x64-gnu": "npm:2.1.3"
|
||||
"@rspack/binding-linux-x64-musl": "npm:2.1.3"
|
||||
"@rspack/binding-wasm32-wasi": "npm:2.1.3"
|
||||
"@rspack/binding-win32-arm64-msvc": "npm:2.1.3"
|
||||
"@rspack/binding-win32-ia32-msvc": "npm:2.1.3"
|
||||
"@rspack/binding-win32-x64-msvc": "npm:2.1.3"
|
||||
dependenciesMeta:
|
||||
"@rspack/binding-darwin-arm64":
|
||||
optional: true
|
||||
@@ -4864,15 +4864,15 @@ __metadata:
|
||||
optional: true
|
||||
"@rspack/binding-win32-x64-msvc":
|
||||
optional: true
|
||||
checksum: 10/425bf152dba708992ce16114ce6bc8dfa424071694e85201317518dce1899eeae873a2adbd23b214bb8d223f7290e000fd78ad9bf262c633e12d3d4ff73bbdcd
|
||||
checksum: 10/7e499489ac0d882ba6573854d8bed7db8fd2ce8c95274a28527b2e8a8692c3c75e34c892cff9385082b3eafe199ececeb0878ab9314adcd54c5ed0db916e410b
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rspack/core@npm:2.1.4":
|
||||
version: 2.1.4
|
||||
resolution: "@rspack/core@npm:2.1.4"
|
||||
"@rspack/core@npm:2.1.3":
|
||||
version: 2.1.3
|
||||
resolution: "@rspack/core@npm:2.1.3"
|
||||
dependencies:
|
||||
"@rspack/binding": "npm:2.1.4"
|
||||
"@rspack/binding": "npm:2.1.3"
|
||||
peerDependencies:
|
||||
"@module-federation/runtime-tools": ^0.24.1 || ^2.0.0
|
||||
"@swc/helpers": ^0.5.23
|
||||
@@ -4881,7 +4881,7 @@ __metadata:
|
||||
optional: true
|
||||
"@swc/helpers":
|
||||
optional: true
|
||||
checksum: 10/3c7aa9e8dbe8b132b51fc017a6c2c76ddf346cf663cad2a8912ba61469d80468acd045503d405bf962bb5ec44f44036512dd6d6233ea6d1419979e5c17e1dbd8
|
||||
checksum: 10/24579403136732ba0710e83851c4ac557f3cab78bb80a58999b02c8a11cb6f0d64fd71010b4fb02824b124aa4da80af544f17e31b59b1a231993124c637cadf7
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -5741,105 +5741,105 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/eslint-plugin@npm:8.64.0":
|
||||
version: 8.64.0
|
||||
resolution: "@typescript-eslint/eslint-plugin@npm:8.64.0"
|
||||
"@typescript-eslint/eslint-plugin@npm:8.63.0":
|
||||
version: 8.63.0
|
||||
resolution: "@typescript-eslint/eslint-plugin@npm:8.63.0"
|
||||
dependencies:
|
||||
"@eslint-community/regexpp": "npm:^4.12.2"
|
||||
"@typescript-eslint/scope-manager": "npm:8.64.0"
|
||||
"@typescript-eslint/type-utils": "npm:8.64.0"
|
||||
"@typescript-eslint/utils": "npm:8.64.0"
|
||||
"@typescript-eslint/visitor-keys": "npm:8.64.0"
|
||||
"@typescript-eslint/scope-manager": "npm:8.63.0"
|
||||
"@typescript-eslint/type-utils": "npm:8.63.0"
|
||||
"@typescript-eslint/utils": "npm:8.63.0"
|
||||
"@typescript-eslint/visitor-keys": "npm:8.63.0"
|
||||
ignore: "npm:^7.0.5"
|
||||
natural-compare: "npm:^1.4.0"
|
||||
ts-api-utils: "npm:^2.5.0"
|
||||
peerDependencies:
|
||||
"@typescript-eslint/parser": ^8.64.0
|
||||
"@typescript-eslint/parser": ^8.63.0
|
||||
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
|
||||
typescript: ">=4.8.4 <6.1.0"
|
||||
checksum: 10/ec7cbcb44968386a4b9aff9272ce6b75bdcc7f398db5f2d07a21854baf0364ca33c74268c0e19d21386c6b87b9358b141df7bef3d26e4e4e7a9eb5f8f394dc75
|
||||
checksum: 10/ab60da4c4a66e8b882b6d585457c6cd352492917940d43b255021f71fc057d327fe8e46856316b312a5c5b78b1245acb129869a7c60a7c2fa65e7822ef58c7c6
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/parser@npm:8.64.0":
|
||||
version: 8.64.0
|
||||
resolution: "@typescript-eslint/parser@npm:8.64.0"
|
||||
"@typescript-eslint/parser@npm:8.63.0":
|
||||
version: 8.63.0
|
||||
resolution: "@typescript-eslint/parser@npm:8.63.0"
|
||||
dependencies:
|
||||
"@typescript-eslint/scope-manager": "npm:8.64.0"
|
||||
"@typescript-eslint/types": "npm:8.64.0"
|
||||
"@typescript-eslint/typescript-estree": "npm:8.64.0"
|
||||
"@typescript-eslint/visitor-keys": "npm:8.64.0"
|
||||
"@typescript-eslint/scope-manager": "npm:8.63.0"
|
||||
"@typescript-eslint/types": "npm:8.63.0"
|
||||
"@typescript-eslint/typescript-estree": "npm:8.63.0"
|
||||
"@typescript-eslint/visitor-keys": "npm:8.63.0"
|
||||
debug: "npm:^4.4.3"
|
||||
peerDependencies:
|
||||
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
|
||||
typescript: ">=4.8.4 <6.1.0"
|
||||
checksum: 10/7239b16a6ca6bb1764ad04bc1eb46997336541d049fcffb4966ef404fbb02324b2b33aed50c2b976359ec8d3c97b90668cfd6aa9177228b4b799152f01a3904a
|
||||
checksum: 10/40ee2a1c703898b9c6df0731e5584ce0bf63d8c13fb464a952500dae156b68b41b522b2cb4755a2f0cb7e0d529b0888e3ba783e3eeb1236c062a3a7ca227f634
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/project-service@npm:8.64.0":
|
||||
version: 8.64.0
|
||||
resolution: "@typescript-eslint/project-service@npm:8.64.0"
|
||||
"@typescript-eslint/project-service@npm:8.63.0":
|
||||
version: 8.63.0
|
||||
resolution: "@typescript-eslint/project-service@npm:8.63.0"
|
||||
dependencies:
|
||||
"@typescript-eslint/tsconfig-utils": "npm:^8.64.0"
|
||||
"@typescript-eslint/types": "npm:^8.64.0"
|
||||
"@typescript-eslint/tsconfig-utils": "npm:^8.63.0"
|
||||
"@typescript-eslint/types": "npm:^8.63.0"
|
||||
debug: "npm:^4.4.3"
|
||||
peerDependencies:
|
||||
typescript: ">=4.8.4 <6.1.0"
|
||||
checksum: 10/511b9a8f1dcb32c99003cab9791309056ac6e889fe46227600deb743e869a63b3e78d7d2d3ef1bf65b2d5e574b73add3040fbef4c0f94aed587368aaea149559
|
||||
checksum: 10/16b07d0e95abfbe56eccc7c67185caf43e9059740c82a09a0f0f4393539f89a19f8c1b996d396f9476401af55bd804897fd19c988712db3e0f36562d2805c223
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/scope-manager@npm:8.64.0":
|
||||
version: 8.64.0
|
||||
resolution: "@typescript-eslint/scope-manager@npm:8.64.0"
|
||||
"@typescript-eslint/scope-manager@npm:8.63.0":
|
||||
version: 8.63.0
|
||||
resolution: "@typescript-eslint/scope-manager@npm:8.63.0"
|
||||
dependencies:
|
||||
"@typescript-eslint/types": "npm:8.64.0"
|
||||
"@typescript-eslint/visitor-keys": "npm:8.64.0"
|
||||
checksum: 10/3c72c4915cee19d632ddc7491c3a4668dd04aa8bcb112fde8ac10aea2cc0364aaa8439d9939d0c62a7b4159fc22f4ced7cba3a77df4422b21d76497068f08076
|
||||
"@typescript-eslint/types": "npm:8.63.0"
|
||||
"@typescript-eslint/visitor-keys": "npm:8.63.0"
|
||||
checksum: 10/6b0183e85d79e287660274d39fed1fc8321ae03ae40545acaad6718fd2524063d302f3348c04576d038a25f16f8c9ca4e8780a0ad754f1a494a00027e867deda
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/tsconfig-utils@npm:8.64.0, @typescript-eslint/tsconfig-utils@npm:^8.64.0":
|
||||
version: 8.64.0
|
||||
resolution: "@typescript-eslint/tsconfig-utils@npm:8.64.0"
|
||||
"@typescript-eslint/tsconfig-utils@npm:8.63.0, @typescript-eslint/tsconfig-utils@npm:^8.63.0":
|
||||
version: 8.63.0
|
||||
resolution: "@typescript-eslint/tsconfig-utils@npm:8.63.0"
|
||||
peerDependencies:
|
||||
typescript: ">=4.8.4 <6.1.0"
|
||||
checksum: 10/905469c5067a9a2d18d065848c6497081ab787b22a9d7c06499f4bb593b7d67f9fb17e7861a114c46626555853cc52d4542db5311c8dc7cd138e45461a73e589
|
||||
checksum: 10/7c23671159f8593b676d53bc52b4a3bd4c3ad62abe2ad99b379b70298abf3b784cdcf18ca2b10f2aa62a9f8e0232e6efb87364940a3bd4a32126a357f39ac3c8
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/type-utils@npm:8.64.0":
|
||||
version: 8.64.0
|
||||
resolution: "@typescript-eslint/type-utils@npm:8.64.0"
|
||||
"@typescript-eslint/type-utils@npm:8.63.0":
|
||||
version: 8.63.0
|
||||
resolution: "@typescript-eslint/type-utils@npm:8.63.0"
|
||||
dependencies:
|
||||
"@typescript-eslint/types": "npm:8.64.0"
|
||||
"@typescript-eslint/typescript-estree": "npm:8.64.0"
|
||||
"@typescript-eslint/utils": "npm:8.64.0"
|
||||
"@typescript-eslint/types": "npm:8.63.0"
|
||||
"@typescript-eslint/typescript-estree": "npm:8.63.0"
|
||||
"@typescript-eslint/utils": "npm:8.63.0"
|
||||
debug: "npm:^4.4.3"
|
||||
ts-api-utils: "npm:^2.5.0"
|
||||
peerDependencies:
|
||||
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
|
||||
typescript: ">=4.8.4 <6.1.0"
|
||||
checksum: 10/7ab1fd8c0d292c0155cf137d316b1618681eca0fe4cf446a05d909de41311ec6bb64fed82513e822063a962508d5b3e615fb0155a4a565d6b9c6cb81d06a5cd2
|
||||
checksum: 10/8a1001edb129aec55c4ef8567f5f33af4b0ee92e89463f45e7d8a006872e438db1a3cbe2364769d205124d9d143018c920e165868788a0e7aee15466ed6d38f0
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/types@npm:8.64.0, @typescript-eslint/types@npm:^8.56.0, @typescript-eslint/types@npm:^8.64.0":
|
||||
version: 8.64.0
|
||||
resolution: "@typescript-eslint/types@npm:8.64.0"
|
||||
checksum: 10/b8951c00ce9b9702f3201f017354774ea5f39c30c2b6f815cb50d91f53f61c325e30f548329daf731d5169d752095cf102da54b754fb202bcfb619faa56fa9f4
|
||||
"@typescript-eslint/types@npm:8.63.0, @typescript-eslint/types@npm:^8.56.0, @typescript-eslint/types@npm:^8.63.0":
|
||||
version: 8.63.0
|
||||
resolution: "@typescript-eslint/types@npm:8.63.0"
|
||||
checksum: 10/f72eb114970cae0da8e53f68cd8dca1b51ac410f4438141a2851655fa07aacb3090e24a2ae53462cf0970e4d5b52cabf9693aa6f07abdff5c210874806427e5b
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/typescript-estree@npm:8.64.0":
|
||||
version: 8.64.0
|
||||
resolution: "@typescript-eslint/typescript-estree@npm:8.64.0"
|
||||
"@typescript-eslint/typescript-estree@npm:8.63.0":
|
||||
version: 8.63.0
|
||||
resolution: "@typescript-eslint/typescript-estree@npm:8.63.0"
|
||||
dependencies:
|
||||
"@typescript-eslint/project-service": "npm:8.64.0"
|
||||
"@typescript-eslint/tsconfig-utils": "npm:8.64.0"
|
||||
"@typescript-eslint/types": "npm:8.64.0"
|
||||
"@typescript-eslint/visitor-keys": "npm:8.64.0"
|
||||
"@typescript-eslint/project-service": "npm:8.63.0"
|
||||
"@typescript-eslint/tsconfig-utils": "npm:8.63.0"
|
||||
"@typescript-eslint/types": "npm:8.63.0"
|
||||
"@typescript-eslint/visitor-keys": "npm:8.63.0"
|
||||
debug: "npm:^4.4.3"
|
||||
minimatch: "npm:^10.2.2"
|
||||
semver: "npm:^7.7.3"
|
||||
@@ -5847,32 +5847,32 @@ __metadata:
|
||||
ts-api-utils: "npm:^2.5.0"
|
||||
peerDependencies:
|
||||
typescript: ">=4.8.4 <6.1.0"
|
||||
checksum: 10/833101d25e820d1d0e317dfc9beca985b3c87e5458b20ed002c86c2d425667aa506f756f1759b54f724033effb529266f836f912c460ee662f347fb43dded6ed
|
||||
checksum: 10/793e0227698f4d8b325922816f27a4e737760aae255874c2021c5e2179499fd036b9e87bf0346ead6360444fb7ab4815bb558c94a17d8481632351bbdb2780bf
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/utils@npm:8.64.0":
|
||||
version: 8.64.0
|
||||
resolution: "@typescript-eslint/utils@npm:8.64.0"
|
||||
"@typescript-eslint/utils@npm:8.63.0":
|
||||
version: 8.63.0
|
||||
resolution: "@typescript-eslint/utils@npm:8.63.0"
|
||||
dependencies:
|
||||
"@eslint-community/eslint-utils": "npm:^4.9.1"
|
||||
"@typescript-eslint/scope-manager": "npm:8.64.0"
|
||||
"@typescript-eslint/types": "npm:8.64.0"
|
||||
"@typescript-eslint/typescript-estree": "npm:8.64.0"
|
||||
"@typescript-eslint/scope-manager": "npm:8.63.0"
|
||||
"@typescript-eslint/types": "npm:8.63.0"
|
||||
"@typescript-eslint/typescript-estree": "npm:8.63.0"
|
||||
peerDependencies:
|
||||
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
|
||||
typescript: ">=4.8.4 <6.1.0"
|
||||
checksum: 10/ff7e5374bd6ef60d7df723e6440468e3a5d4879d1d9876e322904319a1f1d2f98d858fc9b9169dbbd7ee0786a07102a058f13e2b6c16d1bf9aae9eb836a258a3
|
||||
checksum: 10/017bd1998822902b5dde5af9e031908aeb8861565b947aa217d5641bac4758fb831c52629b600bb58a990fcfef1ca58eaf5b44595575bb71a43761c2f40a7f56
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/visitor-keys@npm:8.64.0":
|
||||
version: 8.64.0
|
||||
resolution: "@typescript-eslint/visitor-keys@npm:8.64.0"
|
||||
"@typescript-eslint/visitor-keys@npm:8.63.0":
|
||||
version: 8.63.0
|
||||
resolution: "@typescript-eslint/visitor-keys@npm:8.63.0"
|
||||
dependencies:
|
||||
"@typescript-eslint/types": "npm:8.64.0"
|
||||
"@typescript-eslint/types": "npm:8.63.0"
|
||||
eslint-visitor-keys: "npm:^5.0.0"
|
||||
checksum: 10/d149be4ac0e67c51097cdb7335d8e2d29b9dc0de173961c5cc550e938a00a3af28b1f8ecd7ad832fcfe489d1b11e36fdd394b6ea92115a7558123562e31a704f
|
||||
checksum: 10/9edcff478ba98f81f843b0c02874f3ef58c15b1001b2cf74d878416340c460a7c6e37442b4bee3ef226f0029131d620ee05a9ee3d19ec694ffa07fcf9a7a03ca
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -9470,10 +9470,10 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"fuse.js@npm:7.5.0":
|
||||
version: 7.5.0
|
||||
resolution: "fuse.js@npm:7.5.0"
|
||||
checksum: 10/44dafab623a1144971ccbb380d6874ef6c51a73c85474016b0d47d6ad732d819cdb070f056149cd968b5304e7c0975f1890aaf98f3c4def2e9c21aebe4d62e23
|
||||
"fuse.js@npm:7.4.2":
|
||||
version: 7.4.2
|
||||
resolution: "fuse.js@npm:7.4.2"
|
||||
checksum: 10/1605bb929331056f9215a8f0a19b2d22615d9195e17794f41254bdeeb77b0145ed0ad6b6842227b329783a4ab53586d795924e7a4a3fb664295f365ef6e2405c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -9971,7 +9971,7 @@ __metadata:
|
||||
"@lit/context": "npm:1.1.6"
|
||||
"@lit/reactive-element": "npm:2.1.2"
|
||||
"@lit/task": "npm:1.0.3"
|
||||
"@lokalise/node-api": "npm:16.1.0"
|
||||
"@lokalise/node-api": "npm:16.0.0"
|
||||
"@material/mwc-formfield": "patch:@material/mwc-formfield@npm%3A0.27.0#~/.yarn/patches/@material-mwc-formfield-npm-0.27.0-9528cb60f6.patch"
|
||||
"@material/mwc-list": "patch:@material/mwc-list@npm%3A0.27.0#~/.yarn/patches/@material-mwc-list-npm-0.27.0-5344fc9de4.patch"
|
||||
"@material/web": "npm:2.4.1"
|
||||
@@ -9983,7 +9983,7 @@ __metadata:
|
||||
"@playwright/test": "npm:1.61.1"
|
||||
"@replit/codemirror-indentation-markers": "npm:6.5.3"
|
||||
"@rsdoctor/rspack-plugin": "npm:1.5.18"
|
||||
"@rspack/core": "npm:2.1.4"
|
||||
"@rspack/core": "npm:2.1.3"
|
||||
"@rspack/dev-server": "npm:2.1.0"
|
||||
"@swc/helpers": "npm:0.5.23"
|
||||
"@thomasloven/round-slider": "npm:0.6.0"
|
||||
@@ -10036,7 +10036,7 @@ __metadata:
|
||||
eslint-plugin-wc: "npm:3.1.0"
|
||||
fancy-log: "npm:2.0.0"
|
||||
fs-extra: "npm:11.3.6"
|
||||
fuse.js: "npm:7.5.0"
|
||||
fuse.js: "npm:7.4.2"
|
||||
generate-license-file: "npm:4.2.1"
|
||||
glob: "npm:13.0.6"
|
||||
globals: "npm:17.7.0"
|
||||
@@ -10089,7 +10089,7 @@ __metadata:
|
||||
tinykeys: "patch:tinykeys@npm%3A4.0.0#~/.yarn/patches/tinykeys-npm-4.0.0-a6ca3fd771.patch"
|
||||
ts-lit-plugin: "npm:2.0.2"
|
||||
typescript: "npm:6.0.3"
|
||||
typescript-eslint: "npm:8.64.0"
|
||||
typescript-eslint: "npm:8.63.0"
|
||||
vite-tsconfig-paths: "npm:6.1.1"
|
||||
vitest: "npm:4.1.10"
|
||||
webpack-stats-plugin: "npm:1.1.3"
|
||||
@@ -15088,18 +15088,18 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"typescript-eslint@npm:8.64.0":
|
||||
version: 8.64.0
|
||||
resolution: "typescript-eslint@npm:8.64.0"
|
||||
"typescript-eslint@npm:8.63.0":
|
||||
version: 8.63.0
|
||||
resolution: "typescript-eslint@npm:8.63.0"
|
||||
dependencies:
|
||||
"@typescript-eslint/eslint-plugin": "npm:8.64.0"
|
||||
"@typescript-eslint/parser": "npm:8.64.0"
|
||||
"@typescript-eslint/typescript-estree": "npm:8.64.0"
|
||||
"@typescript-eslint/utils": "npm:8.64.0"
|
||||
"@typescript-eslint/eslint-plugin": "npm:8.63.0"
|
||||
"@typescript-eslint/parser": "npm:8.63.0"
|
||||
"@typescript-eslint/typescript-estree": "npm:8.63.0"
|
||||
"@typescript-eslint/utils": "npm:8.63.0"
|
||||
peerDependencies:
|
||||
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
|
||||
typescript: ">=4.8.4 <6.1.0"
|
||||
checksum: 10/7812e25506003c2a5b395a51ca3c31929028485d191049e6a813e65829e79fcebb07251ba42c5d75af8d15233a5bbb44dd37138345787779eabe07b88fcf75ec
|
||||
checksum: 10/ab0f3324cbf6ab910903977b9364138c67ccad35675962702bfbe052231440439dcdc50de0c01a1eb1ccecde796a4e117858b10166d9d1153b3b2d82c73a92ba
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
||||
Reference in New Issue
Block a user