mirror of
https://github.com/home-assistant/frontend.git
synced 2026-10-26 23:54:13 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
affaeb1bb5 |
No files matched your search
@@ -5,15 +5,31 @@ import {
|
||||
mdiPencil,
|
||||
} from "@mdi/js";
|
||||
import deepClone from "deep-clone-simple";
|
||||
import type { HassEntities } from "home-assistant-js-websocket";
|
||||
import { css, html, LitElement, nothing, type PropertyValues } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { ensureArray } from "../../common/array/ensure-array";
|
||||
import { consume } from "../../common/decorators/consume";
|
||||
import { transform } from "../../common/decorators/transform";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import {
|
||||
formattersContext,
|
||||
internationalizationContext,
|
||||
registriesContext,
|
||||
statesContext,
|
||||
} from "../../data/context";
|
||||
import type { ObjectSelector } from "../../data/selector";
|
||||
import { formatSelectorValue } from "../../data/selector/format_selector_value";
|
||||
import {
|
||||
formatSelectorValue,
|
||||
type FormatSelectorValueOptions,
|
||||
} from "../../data/selector/format_selector_value";
|
||||
import { showFormDialog } from "../../dialogs/form/show-form-dialog";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type {
|
||||
HomeAssistantFormatters,
|
||||
HomeAssistantInternationalization,
|
||||
HomeAssistantRegistries,
|
||||
} from "../../types";
|
||||
import { computeInitialHaFormData } from "../ha-form/compute-initial-ha-form-data";
|
||||
import type { HaFormSchema } from "../ha-form/types";
|
||||
import "../ha-input-helper-text";
|
||||
@@ -26,8 +42,6 @@ import { deepEqual } from "../../common/util/deep-equal";
|
||||
|
||||
@customElement("ha-selector-object")
|
||||
export class HaObjectSelector extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public selector!: ObjectSelector;
|
||||
|
||||
@property() public value?: any;
|
||||
@@ -47,6 +61,28 @@ export class HaObjectSelector extends LitElement {
|
||||
values?: Record<string, string>
|
||||
) => string;
|
||||
|
||||
@state()
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
private _i18n!: HomeAssistantInternationalization;
|
||||
|
||||
@state()
|
||||
@consume({ context: registriesContext, subscribe: true })
|
||||
private _registries!: HomeAssistantRegistries;
|
||||
|
||||
@state()
|
||||
@consume({ context: statesContext, subscribe: true })
|
||||
private _states!: HassEntities;
|
||||
|
||||
@state()
|
||||
@consume({ context: formattersContext, subscribe: true })
|
||||
@transform<
|
||||
HomeAssistantFormatters,
|
||||
HomeAssistantFormatters["formatEntityName"]
|
||||
>({
|
||||
transformer: (formatters) => formatters?.formatEntityName,
|
||||
})
|
||||
private _formatEntityName!: HomeAssistantFormatters["formatEntityName"];
|
||||
|
||||
@query("ha-yaml-editor", true) private _yamlEditor?: HaYamlEditor;
|
||||
|
||||
private _valueChangedFromChild = false;
|
||||
@@ -83,7 +119,12 @@ export class HaObjectSelector extends LitElement {
|
||||
private _formatField(item: any, field: string): string {
|
||||
const fields = this.selector.object!.fields!;
|
||||
const value = formatSelectorValue(
|
||||
this.hass,
|
||||
this._formatOptions(
|
||||
this._i18n,
|
||||
this._registries,
|
||||
this._states,
|
||||
this._formatEntityName
|
||||
),
|
||||
item[field],
|
||||
fields[field]?.selector
|
||||
);
|
||||
@@ -148,14 +189,14 @@ export class HaObjectSelector extends LitElement {
|
||||
slot="end"
|
||||
.item=${item}
|
||||
.index=${index}
|
||||
.label=${this.hass.localize("ui.common.edit")}
|
||||
.label=${this._i18n.localize("ui.common.edit")}
|
||||
.path=${mdiPencil}
|
||||
@click=${this._editItem}
|
||||
></ha-icon-button>
|
||||
<ha-icon-button
|
||||
slot="end"
|
||||
.index=${index}
|
||||
.label=${this.hass.localize("ui.common.delete")}
|
||||
.label=${this._i18n.localize("ui.common.delete")}
|
||||
.path=${multiple ? mdiDelete : mdiClose}
|
||||
@click=${this._deleteItem}
|
||||
></ha-icon-button>
|
||||
@@ -180,7 +221,7 @@ export class HaObjectSelector extends LitElement {
|
||||
</ha-list-base>
|
||||
</ha-sortable>
|
||||
<ha-button appearance="filled" @click=${this._addItem}>
|
||||
${this.hass.localize("ui.common.add")}
|
||||
${this._i18n.localize("ui.common.add")}
|
||||
</ha-button>
|
||||
</div>
|
||||
`;
|
||||
@@ -196,7 +237,7 @@ export class HaObjectSelector extends LitElement {
|
||||
</ha-list-base>`
|
||||
: html`
|
||||
<ha-button appearance="filled" @click=${this._addItem}>
|
||||
${this.hass.localize("ui.common.add")}
|
||||
${this._i18n.localize("ui.common.add")}
|
||||
</ha-button>
|
||||
`
|
||||
}
|
||||
@@ -219,6 +260,23 @@ export class HaObjectSelector extends LitElement {
|
||||
} `;
|
||||
}
|
||||
|
||||
private _formatOptions = memoizeOne(
|
||||
(
|
||||
i18n: HomeAssistantInternationalization,
|
||||
registries: HomeAssistantRegistries,
|
||||
states: HassEntities,
|
||||
formatEntityName: HomeAssistantFormatters["formatEntityName"]
|
||||
): FormatSelectorValueOptions => ({
|
||||
locale: i18n.locale,
|
||||
localize: i18n.localize,
|
||||
states,
|
||||
devices: registries.devices,
|
||||
areas: registries.areas,
|
||||
floors: registries.floors,
|
||||
formatEntityName,
|
||||
})
|
||||
);
|
||||
|
||||
private _schema = memoizeOne((selector: ObjectSelector) => {
|
||||
if (!selector.object || !selector.object.fields) {
|
||||
return [];
|
||||
@@ -262,12 +320,12 @@ export class HaObjectSelector extends LitElement {
|
||||
};
|
||||
|
||||
const newItem = await showFormDialog(this, {
|
||||
title: this.hass.localize("ui.common.add"),
|
||||
title: this._i18n.localize("ui.common.add"),
|
||||
schema,
|
||||
data,
|
||||
computeLabel: this._computeLabel,
|
||||
computeHelper: this._computeHelper,
|
||||
submitText: this.hass.localize("ui.common.add"),
|
||||
submitText: this._i18n.localize("ui.common.add"),
|
||||
});
|
||||
|
||||
if (newItem === null) {
|
||||
@@ -290,12 +348,12 @@ export class HaObjectSelector extends LitElement {
|
||||
const index = ev.currentTarget.index;
|
||||
|
||||
const updatedItem = await showFormDialog(this, {
|
||||
title: this.hass.localize("ui.common.edit"),
|
||||
title: this._i18n.localize("ui.common.edit"),
|
||||
schema: this._schema(this.selector),
|
||||
data: item,
|
||||
computeLabel: this._computeLabel,
|
||||
computeHelper: this._computeHelper,
|
||||
submitText: this.hass.localize("ui.common.save"),
|
||||
submitText: this._i18n.localize("ui.common.save"),
|
||||
});
|
||||
|
||||
if (updatedItem === null) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { HassEntities } from "home-assistant-js-websocket";
|
||||
import { ensureArray } from "../../common/array/ensure-array";
|
||||
import { createDurationData } from "../../common/datetime/create_duration_data";
|
||||
import { formatDurationLong } from "../../common/datetime/format_duration";
|
||||
@@ -5,12 +6,27 @@ import { normalizeDuration } from "../../common/datetime/normalize_duration";
|
||||
import { computeAreaName } from "../../common/entity/compute_area_name";
|
||||
import { DEFAULT_ENTITY_NAME } from "../../common/entity/compute_entity_name_display";
|
||||
import { blankBeforeUnit } from "../../common/translations/blank_before_unit";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { LocalizeFunc } from "../../common/translations/localize";
|
||||
import type { HomeAssistantFormatters } from "../../types";
|
||||
import type { AreaRegistryEntry } from "../area/area_registry";
|
||||
import type { DeviceRegistryEntry } from "../device/device_registry";
|
||||
import type { FloorRegistryEntry } from "../floor_registry";
|
||||
import type { Selector } from "../selector";
|
||||
import { getDurationSelectorMode, getDurationSelectorUnits } from "../selector";
|
||||
import type { FrontendLocaleData } from "../translation";
|
||||
|
||||
export interface FormatSelectorValueOptions {
|
||||
locale: FrontendLocaleData;
|
||||
localize: LocalizeFunc;
|
||||
states: HassEntities;
|
||||
devices: Record<string, DeviceRegistryEntry>;
|
||||
areas: Record<string, AreaRegistryEntry>;
|
||||
floors: Record<string, FloorRegistryEntry>;
|
||||
formatEntityName: HomeAssistantFormatters["formatEntityName"];
|
||||
}
|
||||
|
||||
export const formatSelectorValue = (
|
||||
hass: HomeAssistant,
|
||||
options: FormatSelectorValueOptions,
|
||||
value: any,
|
||||
selector?: Selector
|
||||
) => {
|
||||
@@ -47,7 +63,7 @@ export const formatSelectorValue = (
|
||||
return number;
|
||||
}
|
||||
return unit_of_measurement
|
||||
? `${num}${blankBeforeUnit(unit_of_measurement, hass.locale)}${unit_of_measurement}`
|
||||
? `${num}${blankBeforeUnit(unit_of_measurement, options.locale)}${unit_of_measurement}`
|
||||
: num.toString();
|
||||
})
|
||||
.join(", ");
|
||||
@@ -57,7 +73,7 @@ export const formatSelectorValue = (
|
||||
const floors = ensureArray(value);
|
||||
return floors
|
||||
.map((floorId) => {
|
||||
const floor = hass.floors[floorId];
|
||||
const floor = options.floors[floorId];
|
||||
if (!floor) {
|
||||
return floorId;
|
||||
}
|
||||
@@ -70,7 +86,7 @@ export const formatSelectorValue = (
|
||||
const areas = ensureArray(value);
|
||||
return areas
|
||||
.map((areaId) => {
|
||||
const area = hass.areas[areaId];
|
||||
const area = options.areas[areaId];
|
||||
if (!area) {
|
||||
return areaId;
|
||||
}
|
||||
@@ -83,11 +99,11 @@ export const formatSelectorValue = (
|
||||
const entities = ensureArray(value);
|
||||
return entities
|
||||
.map((entityId) => {
|
||||
const stateObj = hass.states[entityId];
|
||||
const stateObj = options.states[entityId];
|
||||
if (!stateObj) {
|
||||
return entityId;
|
||||
}
|
||||
const name = hass.formatEntityName(stateObj, DEFAULT_ENTITY_NAME);
|
||||
const name = options.formatEntityName(stateObj, DEFAULT_ENTITY_NAME);
|
||||
return name || entityId;
|
||||
})
|
||||
.join(", ");
|
||||
@@ -97,7 +113,7 @@ export const formatSelectorValue = (
|
||||
const devices = ensureArray(value);
|
||||
return devices
|
||||
.map((deviceId) => {
|
||||
const device = hass.devices[deviceId];
|
||||
const device = options.devices[deviceId];
|
||||
if (!device) {
|
||||
return deviceId;
|
||||
}
|
||||
@@ -125,7 +141,7 @@ export const formatSelectorValue = (
|
||||
return Object.entries(fields)
|
||||
.filter(([key]) => key in item && item[key] != null)
|
||||
.map(([key, field]) =>
|
||||
formatSelectorValue(hass, item[key], field.selector)
|
||||
formatSelectorValue(options, item[key], field.selector)
|
||||
)
|
||||
.join(" = ");
|
||||
}
|
||||
@@ -143,13 +159,13 @@ export const formatSelectorValue = (
|
||||
data,
|
||||
getDurationSelectorUnits(selector.duration)
|
||||
);
|
||||
const formatted = formatDurationLong(hass.locale, duration);
|
||||
const formatted = formatDurationLong(options.locale, duration);
|
||||
const mode = getDurationSelectorMode(selector.duration);
|
||||
if (!formatted || mode === "positive") {
|
||||
return formatted;
|
||||
}
|
||||
const sign = negative ? "negative" : "positive";
|
||||
return hass.localize(
|
||||
return options.localize(
|
||||
`ui.components.selectors.duration.summary.${mode}_${sign}`,
|
||||
{ duration: formatted }
|
||||
);
|
||||
|
||||
@@ -1,6 +1,16 @@
|
||||
import { ContextProvider } from "@lit/context";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
formattersContext,
|
||||
internationalizationContext,
|
||||
registriesContext,
|
||||
statesContext,
|
||||
} from "../../../src/data/context";
|
||||
import type { ObjectSelector } from "../../../src/data/selector";
|
||||
import type { HomeAssistant } from "../../../src/types";
|
||||
import type {
|
||||
HomeAssistantFormatters,
|
||||
HomeAssistantInternationalization,
|
||||
} from "../../../src/types";
|
||||
import type { HaObjectSelector } from "../../../src/components/ha-selector/ha-selector-object";
|
||||
import type {
|
||||
FormDialogData,
|
||||
@@ -33,16 +43,6 @@ vi.mock("../../../src/components/ha-yaml-editor", () => {
|
||||
return {};
|
||||
});
|
||||
|
||||
const hass = {
|
||||
localize: (key: string) => key,
|
||||
locale: "en-US",
|
||||
floors: {},
|
||||
areas: {},
|
||||
devices: {},
|
||||
states: {},
|
||||
formatEntityName: () => "",
|
||||
} as unknown as HomeAssistant;
|
||||
|
||||
const selectorConfig = {
|
||||
object: {
|
||||
multiple: true,
|
||||
@@ -67,13 +67,32 @@ const mountSelector = async (
|
||||
value: FormDialogData | FormDialogData[] | "",
|
||||
config: ObjectSelector = selectorConfig
|
||||
) => {
|
||||
const host = document.createElement("div");
|
||||
new ContextProvider(host, {
|
||||
context: internationalizationContext,
|
||||
initialValue: {
|
||||
localize: (key: string) => key,
|
||||
locale: { language: "en" },
|
||||
} as unknown as HomeAssistantInternationalization,
|
||||
});
|
||||
new ContextProvider(host, {
|
||||
context: registriesContext,
|
||||
initialValue: { entities: {}, devices: {}, areas: {}, floors: {} },
|
||||
});
|
||||
new ContextProvider(host, { context: statesContext, initialValue: {} });
|
||||
new ContextProvider(host, {
|
||||
context: formattersContext,
|
||||
initialValue: {
|
||||
formatEntityName: () => "",
|
||||
} as unknown as HomeAssistantFormatters,
|
||||
});
|
||||
const selector = document.createElement(
|
||||
"ha-selector-object"
|
||||
) as HaObjectSelector;
|
||||
selector.hass = hass;
|
||||
selector.selector = config;
|
||||
selector.value = value;
|
||||
document.body.append(selector);
|
||||
host.append(selector);
|
||||
document.body.append(host);
|
||||
await selector.updateComplete;
|
||||
return selector;
|
||||
};
|
||||
|
||||
@@ -1,33 +1,34 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { formatSelectorValue } from "../../src/data/selector/format_selector_value";
|
||||
import type { HomeAssistant } from "../../src/types";
|
||||
import {
|
||||
formatSelectorValue,
|
||||
type FormatSelectorValueOptions,
|
||||
} from "../../src/data/selector/format_selector_value";
|
||||
|
||||
// formatSelectorValue only touches hass for floor/area/entity/device
|
||||
// selectors, none of which these tests exercise.
|
||||
const hass = {} as HomeAssistant;
|
||||
// These selectors read none of the options.
|
||||
const options = {} as FormatSelectorValueOptions;
|
||||
|
||||
describe("formatSelectorValue", () => {
|
||||
it("returns an empty string for nullish values", () => {
|
||||
expect(formatSelectorValue(hass, null)).toBe("");
|
||||
expect(formatSelectorValue(hass, undefined)).toBe("");
|
||||
expect(formatSelectorValue(options, null)).toBe("");
|
||||
expect(formatSelectorValue(options, undefined)).toBe("");
|
||||
});
|
||||
|
||||
it("renders a plain text value", () => {
|
||||
expect(formatSelectorValue(hass, "hello", { text: { type: "text" } })).toBe(
|
||||
"hello"
|
||||
);
|
||||
expect(
|
||||
formatSelectorValue(options, "hello", { text: { type: "text" } })
|
||||
).toBe("hello");
|
||||
});
|
||||
|
||||
it("applies prefix and suffix for text selectors", () => {
|
||||
expect(
|
||||
formatSelectorValue(hass, "5", {
|
||||
formatSelectorValue(options, "5", {
|
||||
text: { type: "text", prefix: "$", suffix: " each" },
|
||||
})
|
||||
).toBe("$5 each");
|
||||
});
|
||||
|
||||
it("masks a password text value instead of revealing it", () => {
|
||||
const result = formatSelectorValue(hass, "hunter2", {
|
||||
const result = formatSelectorValue(options, "hunter2", {
|
||||
text: { type: "password" },
|
||||
});
|
||||
expect(result).not.toContain("hunter2");
|
||||
@@ -35,7 +36,7 @@ describe("formatSelectorValue", () => {
|
||||
});
|
||||
|
||||
it("masks every value of a multiple password selector", () => {
|
||||
const result = formatSelectorValue(hass, ["one", "two"], {
|
||||
const result = formatSelectorValue(options, ["one", "two"], {
|
||||
text: { type: "password" },
|
||||
});
|
||||
expect(result).not.toContain("one");
|
||||
@@ -45,7 +46,7 @@ describe("formatSelectorValue", () => {
|
||||
|
||||
it("masks a nested password field in an object selector preview", () => {
|
||||
const result = formatSelectorValue(
|
||||
hass,
|
||||
options,
|
||||
{ username: "admin", password: "hunter2" },
|
||||
{
|
||||
object: {
|
||||
@@ -63,7 +64,7 @@ describe("formatSelectorValue", () => {
|
||||
});
|
||||
|
||||
describe("formatSelectorValue duration selector", () => {
|
||||
const localizedHass = {
|
||||
const localizedOptions = {
|
||||
locale: { language: "en" },
|
||||
localize: (key: string, values?: Record<string, unknown>) => {
|
||||
const suffix = key.split("ui.components.selectors.duration.summary.")[1];
|
||||
@@ -74,12 +75,12 @@ describe("formatSelectorValue duration selector", () => {
|
||||
signed_positive: `+${values?.duration}`,
|
||||
}[suffix]!;
|
||||
},
|
||||
} as unknown as HomeAssistant;
|
||||
} as unknown as FormatSelectorValueOptions;
|
||||
|
||||
it("formats a positive duration without a sign", () => {
|
||||
expect(
|
||||
formatSelectorValue(
|
||||
localizedHass,
|
||||
localizedOptions,
|
||||
{ hours: 1, minutes: 30 },
|
||||
{ duration: {} }
|
||||
)
|
||||
@@ -89,13 +90,13 @@ describe("formatSelectorValue duration selector", () => {
|
||||
it("formats signed durations with an explicit sign", () => {
|
||||
expect(
|
||||
formatSelectorValue(
|
||||
localizedHass,
|
||||
localizedOptions,
|
||||
{ minutes: -30 },
|
||||
{ duration: { mode: "signed" } }
|
||||
)
|
||||
).toBe("-30 minutes");
|
||||
expect(
|
||||
formatSelectorValue(localizedHass, "00:30:00", {
|
||||
formatSelectorValue(localizedOptions, "00:30:00", {
|
||||
duration: { allow_negative: true },
|
||||
})
|
||||
).toBe("+30 minutes");
|
||||
@@ -104,22 +105,24 @@ describe("formatSelectorValue duration selector", () => {
|
||||
it("formats offsets as before or after", () => {
|
||||
expect(
|
||||
formatSelectorValue(
|
||||
localizedHass,
|
||||
localizedOptions,
|
||||
{ hours: -1, minutes: -30 },
|
||||
{ duration: { mode: "offset" } }
|
||||
)
|
||||
).toBe("1 hour, 30 minutes before");
|
||||
expect(
|
||||
formatSelectorValue(localizedHass, "-00:10:00", {
|
||||
formatSelectorValue(localizedOptions, "-00:10:00", {
|
||||
duration: { mode: "offset" },
|
||||
})
|
||||
).toBe("10 minutes before");
|
||||
expect(
|
||||
formatSelectorValue(localizedHass, 45, { duration: { mode: "offset" } })
|
||||
formatSelectorValue(localizedOptions, 45, {
|
||||
duration: { mode: "offset" },
|
||||
})
|
||||
).toBe("45 seconds after");
|
||||
expect(
|
||||
formatSelectorValue(
|
||||
localizedHass,
|
||||
localizedOptions,
|
||||
{ minutes: -5 },
|
||||
{ duration: { mode: "offset" } }
|
||||
)
|
||||
@@ -129,7 +132,7 @@ describe("formatSelectorValue duration selector", () => {
|
||||
it("returns an empty string for a zero offset", () => {
|
||||
expect(
|
||||
formatSelectorValue(
|
||||
localizedHass,
|
||||
localizedOptions,
|
||||
{ hours: 0, minutes: 0, seconds: 0 },
|
||||
{ duration: { mode: "offset" } }
|
||||
)
|
||||
|
||||
Reference in new issue
Block a user