Compare commits

..

1 Commits

Author SHA1 Message Date
Ludeeus a6f8f0495c Do not show skip if entity support auto update 2022-03-31 16:12:48 +00:00
82 changed files with 1008 additions and 1516 deletions
+1 -12
View File
@@ -261,8 +261,6 @@ class DemoHaSelector extends LitElement implements ProvideHassElement {
@state() private _required = false;
@state() private _helper = false;
@state() private _label = true;
private data = SCHEMAS.map(() => ({}));
@@ -420,13 +418,6 @@ class DemoHaSelector extends LitElement implements ProvideHassElement {
@change=${this._handleOptionChange}
></ha-switch>
</ha-formfield>
<ha-formfield label="Helper text">
<ha-switch
.name=${"helper"}
.checked=${this._helper}
@change=${this._handleOptionChange}
></ha-switch>
</ha-formfield>
</div>
${SCHEMAS.map((info, idx) => {
const data = this.data[idx];
@@ -455,7 +446,6 @@ class DemoHaSelector extends LitElement implements ProvideHassElement {
.disabled=${this._disabled}
.required=${this._required}
@value-changed=${valueChanged}
.helper=${this._helper ? "Helper text" : undefined}
></ha-selector>
</ha-settings-row>
`
@@ -476,8 +466,7 @@ class DemoHaSelector extends LitElement implements ProvideHassElement {
width: 60;
}
.options {
max-width: 800px;
margin: 16px auto;
padding: 16px 48px;
}
.options ha-formfield {
margin-right: 16px;
+8 -12
View File
@@ -6,6 +6,7 @@ import {
UPDATE_SUPPORT_PROGRESS,
UPDATE_SUPPORT_INSTALL,
UPDATE_SUPPORT_RELEASE_NOTES,
UPDATE_SUPPORT_AUTO_UPDATE,
} from "../../../../src/data/update";
import "../../../../src/dialogs/more-info/more-info-content";
import { getEntity } from "../../../../src/fake_data/entity";
@@ -18,7 +19,7 @@ import { LONG_TEXT } from "../../data/text";
const base_attributes = {
title: "Awesome",
installed_version: "1.2.2",
current_version: "1.2.2",
latest_version: "1.2.3",
release_url: "https://home-assistant.io",
supported_features: UPDATE_SUPPORT_INSTALL,
@@ -50,7 +51,7 @@ const ENTITIES = [
}),
getEntity("update", "update5", "off", {
...base_attributes,
installed_version: "1.2.3",
current_version: "1.2.3",
friendly_name: "No update",
}),
getEntity("update", "update6", "off", {
@@ -102,8 +103,8 @@ const ENTITIES = [
}),
getEntity("update", "update14", "off", {
...base_attributes,
installed_version: null,
friendly_name: "Update without installed_version",
current_version: null,
friendly_name: "Update without current_version",
}),
getEntity("update", "update15", "off", {
...base_attributes,
@@ -128,16 +129,11 @@ const ENTITIES = [
supported_features:
base_attributes.supported_features + UPDATE_SUPPORT_RELEASE_NOTES,
}),
getEntity("update", "update19", "on", {
getEntity("update", "update18", "on", {
...base_attributes,
friendly_name: "Update with auto update",
auto_update: true,
}),
getEntity("update", "update20", "on", {
...base_attributes,
in_progress: true,
title: undefined,
friendly_name: "Installing without title",
supported_features:
base_attributes.supported_features + UPDATE_SUPPORT_AUTO_UPDATE,
}),
];
@@ -39,14 +39,7 @@ import type { HomeAssistant } from "../../../../src/types";
import { suggestAddonRestart } from "../../dialogs/suggestAddonRestart";
import { hassioStyle } from "../../resources/hassio-style";
const SUPPORTED_UI_TYPES = [
"string",
"select",
"boolean",
"integer",
"float",
"schema",
];
const SUPPORTED_UI_TYPES = ["string", "select", "boolean", "integer", "float"];
const ADDON_YAML_SCHEMA = DEFAULT_SCHEMA.extend([
new Type("!secret", {
@@ -55,8 +48,6 @@ const ADDON_YAML_SCHEMA = DEFAULT_SCHEMA.extend([
}),
]);
const MASKED_FIELDS = ["password", "secret", "token"];
@customElement("hassio-addon-config")
class HassioAddonConfig extends LitElement {
@property({ attribute: false }) public addon!: HassioAddonDetails;
@@ -84,66 +75,19 @@ class HassioAddonConfig extends LitElement {
public computeLabel = (entry: HaFormSchema): string =>
this.addon.translations[this.hass.language]?.configuration?.[entry.name]
?.name ||
this.addon.translations.en?.configuration?.[entry.name]?.name ||
this.addon.translations.en?.configuration?.[entry.name].name ||
entry.name;
public computeHelper = (entry: HaFormSchema): string =>
this.addon.translations[this.hass.language]?.configuration?.[entry.name]
?.description ||
this.addon.translations.en?.configuration?.[entry.name]?.description ||
"";
private _convertSchema = memoizeOne(
// Convert supervisor schema to selectors
(schema: Record<string, any>): HaFormSchema[] =>
schema.map((entry) =>
entry.type === "select"
? {
name: entry.name,
required: entry.required,
selector: { select: { options: entry.options } },
}
: entry.type === "string"
? entry.multiple
? {
name: entry.name,
required: entry.required,
selector: {
select: { options: [], multiple: true, custom_value: true },
},
}
: {
name: entry.name,
required: entry.required,
selector: {
text: {
type:
entry.format || MASKED_FIELDS.includes(entry.name)
? "password"
: "text",
},
},
}
: entry.type === "boolean"
? {
name: entry.name,
required: entry.required,
selector: { boolean: {} },
}
: entry.type === "schema"
? {
name: entry.name,
required: entry.required,
selector: { object: {} },
}
: entry.type === "float" || entry.type === "integer"
? {
name: entry.name,
required: entry.required,
selector: { number: { mode: "box" } },
}
: entry
)
private _schema = memoizeOne((schema: HaFormSchema[]): HaFormSchema[] =>
// @ts-expect-error supervisor does not implement [string, string] for select.options[]
schema.map((entry) =>
entry.type === "select"
? {
...entry,
options: entry.options.map((option) => [option, option]),
}
: entry
)
);
private _filteredShchema = memoizeOne(
@@ -196,8 +140,7 @@ class HassioAddonConfig extends LitElement {
.data=${this._options!}
@value-changed=${this._configChanged}
.computeLabel=${this.computeLabel}
.computeHelper=${this.computeHelper}
.schema=${this._convertSchema(
.schema=${this._schema(
this._showOptional
? this.addon.schema!
: this._filteredShchema(
@@ -254,9 +197,8 @@ class HassioAddonConfig extends LitElement {
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
this._canShowSchema = !this.addon.schema!.find(
(entry) =>
// @ts-ignore
!SUPPORTED_UI_TYPES.includes(entry.type)
// @ts-ignore
(entry) => !SUPPORTED_UI_TYPES.includes(entry.type) || entry.multiple
);
this._yamlMode = !this._canShowSchema;
}
@@ -1,3 +1,4 @@
import { PaperInputElement } from "@polymer/paper-input/paper-input";
import {
css,
CSSResultGroup,
@@ -7,13 +8,10 @@ import {
TemplateResult,
} from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../src/common/dom/fire_event";
import "../../../../src/components/buttons/ha-progress-button";
import "../../../../src/components/ha-alert";
import "../../../../src/components/ha-card";
import "../../../../src/components/ha-form/ha-form";
import type { HaFormSchema } from "../../../../src/components/ha-form/types";
import {
HassioAddonDetails,
HassioAddonSetOptionParams,
@@ -26,6 +24,16 @@ import { HomeAssistant } from "../../../../src/types";
import { suggestAddonRestart } from "../../dialogs/suggestAddonRestart";
import { hassioStyle } from "../../resources/hassio-style";
interface NetworkItem {
description: string;
container: string;
host: number | null;
}
interface NetworkItemInput extends PaperInputElement {
container: string;
}
@customElement("hassio-addon-network")
class HassioAddonNetwork extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -34,13 +42,9 @@ class HassioAddonNetwork extends LitElement {
@property({ attribute: false }) public addon!: HassioAddonDetails;
@state() private _showOptional = false;
@state() private _configHasChanged = false;
@state() private _error?: string;
@state() private _config?: Record<string, any>;
@state() private _config?: NetworkItem[];
public connectedCallback(): void {
super.connectedCallback();
@@ -52,10 +56,6 @@ class HassioAddonNetwork extends LitElement {
return html``;
}
const hasHiddenOptions = Object.keys(this._config).find(
(entry) => this._config![entry] === null
);
return html`
<ha-card
.header=${this.supervisor.localize(
@@ -63,49 +63,52 @@ class HassioAddonNetwork extends LitElement {
)}
>
<div class="card-content">
<p>
${this.supervisor.localize(
"addon.configuration.network.introduction"
)}
</p>
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: ""}
<ha-form
.data=${this._config}
@value-changed=${this._configChanged}
.computeLabel=${this._computeLabel}
.computeHelper=${this._computeHelper}
.schema=${this._createSchema(
this._config,
this._showOptional,
this.hass.userData?.showAdvanced || false
)}
></ha-form>
</div>
${hasHiddenOptions
? html`<ha-formfield
class="show-optional"
.label=${this.supervisor.localize(
"addon.configuration.network.show_disabled"
<table>
<tbody>
<tr>
<th>
${this.supervisor.localize(
"addon.configuration.network.container"
)}
</th>
<th>
${this.supervisor.localize(
"addon.configuration.network.host"
)}
</th>
<th>${this.supervisor.localize("common.description")}</th>
</tr>
${this._config!.map(
(item) => html`
<tr>
<td>${item.container}</td>
<td>
<paper-input
@value-changed=${this._configChanged}
placeholder=${this.supervisor.localize(
"addon.configuration.network.disabled"
)}
.value=${item.host ? String(item.host) : ""}
.container=${item.container}
no-label-float
></paper-input>
</td>
<td>${this._computeDescription(item)}</td>
</tr>
`
)}
>
<ha-switch
@change=${this._toggleOptional}
.checked=${this._showOptional}
>
</ha-switch>
</ha-formfield>`
: ""}
</tbody>
</table>
</div>
<div class="card-actions">
<ha-progress-button class="warning" @click=${this._resetTapped}>
${this.supervisor.localize("common.reset_defaults")}
</ha-progress-button>
<ha-progress-button
@click=${this._saveTapped}
.disabled=${!this._configHasChanged}
>
<ha-progress-button @click=${this._saveTapped}>
${this.supervisor.localize("common.save")}
</ha-progress-button>
</div>
@@ -120,60 +123,50 @@ class HassioAddonNetwork extends LitElement {
}
}
private _createSchema = memoizeOne(
(
config: Record<string, number>,
showOptional: boolean,
advanced: boolean
): HaFormSchema[] =>
(showOptional
? Object.keys(config)
: Object.keys(config).filter((entry) => config[entry] !== null)
).map((entry) => ({
name: entry,
selector: {
number: {
mode: "box",
min: 0,
max: 65535,
unit_of_measurement: advanced ? entry : undefined,
},
},
}))
);
private _computeLabel = (_: HaFormSchema): string => "";
private _computeHelper = (item: HaFormSchema): string =>
this.addon.translations[this.hass.language]?.network?.[item.name] ||
this.addon.translations.en?.network?.[item.name] ||
this.addon.network_description?.[item.name] ||
item.name;
private _computeDescription = (item: NetworkItem): string =>
this.addon.translations[this.hass.language]?.network?.[item.container]
?.description ||
this.addon.translations.en?.network?.[item.container]?.description ||
item.description;
private _setNetworkConfig(): void {
this._config = this.addon.network || {};
const network = this.addon.network || {};
const description = this.addon.network_description || {};
const items: NetworkItem[] = Object.keys(network).map((key) => ({
container: key,
host: network[key],
description: description[key],
}));
this._config = items.sort((a, b) => (a.container > b.container ? 1 : -1));
}
private async _configChanged(ev: CustomEvent): Promise<void> {
this._configHasChanged = true;
this._config! = ev.detail.value;
private async _configChanged(ev: Event): Promise<void> {
const target = ev.target as NetworkItemInput;
this._config!.forEach((item) => {
if (
item.container === target.container &&
item.host !== parseInt(String(target.value), 10)
) {
item.host = target.value ? parseInt(String(target.value), 10) : null;
}
});
}
private async _resetTapped(ev: CustomEvent): Promise<void> {
const button = ev.currentTarget as any;
button.progress = true;
const data: HassioAddonSetOptionParams = {
network: null,
};
try {
await setHassioAddonOption(this.hass, this.addon.slug, data);
this._configHasChanged = false;
const eventdata = {
success: true,
response: undefined,
path: "option",
};
button.actionSuccess();
fireEvent(this, "hass-api-called", eventdata);
if (this.addon?.state === "started") {
await suggestAddonRestart(this, this.hass, this.supervisor, this.addon);
@@ -184,21 +177,19 @@ class HassioAddonNetwork extends LitElement {
"error",
extractApiErrorMessage(err)
);
button.actionError();
}
}
private _toggleOptional() {
this._showOptional = !this._showOptional;
button.progress = false;
}
private async _saveTapped(ev: CustomEvent): Promise<void> {
const button = ev.currentTarget as any;
button.progress = true;
this._error = undefined;
const networkconfiguration = {};
Object.entries(this._config!).forEach(([key, value]) => {
networkconfiguration[key] = value ?? null;
this._config!.forEach((item) => {
networkconfiguration[item.container] = parseInt(String(item.host), 10);
});
const data: HassioAddonSetOptionParams = {
@@ -207,13 +198,11 @@ class HassioAddonNetwork extends LitElement {
try {
await setHassioAddonOption(this.hass, this.addon.slug, data);
this._configHasChanged = false;
const eventdata = {
success: true,
response: undefined,
path: "option",
};
button.actionSuccess();
fireEvent(this, "hass-api-called", eventdata);
if (this.addon?.state === "started") {
await suggestAddonRestart(this, this.hass, this.supervisor, this.addon);
@@ -224,8 +213,8 @@ class HassioAddonNetwork extends LitElement {
"error",
extractApiErrorMessage(err)
);
button.actionError();
}
button.progress = false;
}
static get styles(): CSSResultGroup {
@@ -243,9 +232,6 @@ class HassioAddonNetwork extends LitElement {
display: flex;
justify-content: space-between;
}
.show-optional {
padding: 16px;
}
`,
];
}
@@ -32,6 +32,13 @@ interface AddonCheckboxItem extends CheckboxItem {
const _computeFolders = (folders): CheckboxItem[] => {
const list: CheckboxItem[] = [];
if (folders.includes("homeassistant")) {
list.push({
slug: "homeassistant",
name: "Home Assistant configuration",
checked: false,
});
}
if (folders.includes("ssl")) {
list.push({ slug: "ssl", name: "SSL", checked: false });
}
@@ -93,7 +100,7 @@ export class SupervisorBackupContent extends LitElement {
this.folders = _computeFolders(
this.backup
? this.backup.folders
: ["ssl", "share", "media", "addons/local"]
: ["homeassistant", "ssl", "share", "media", "addons/local"]
);
this.addons = _computeAddons(
this.backup ? this.backup.addons : this.supervisor?.supervisor.addons
@@ -180,7 +187,7 @@ export class SupervisorBackupContent extends LitElement {
>
<ha-checkbox
.checked=${this.homeAssistant}
@change=${this.toggleHomeAssistant}
@click=${this.toggleHomeAssistant}
>
</ha-checkbox>
</ha-formfield>
+1 -1
View File
@@ -1,6 +1,6 @@
[metadata]
name = home-assistant-frontend
version = 20220405.0
version = 20220330.0
author = The Home Assistant Authors
author_email = hello@home-assistant.io
license = Apache-2.0
+2 -7
View File
@@ -29,11 +29,8 @@ import {
mdiPowerPlug,
mdiPowerPlugOff,
mdiRadioboxBlank,
mdiSmoke,
mdiSnowflake,
mdiSmokeDetector,
mdiSmokeDetectorAlert,
mdiSmokeDetectorVariant,
mdiSmokeDetectorVariantAlert,
mdiSquare,
mdiSquareOutline,
mdiStop,
@@ -55,8 +52,6 @@ export const binarySensorIcon = (state?: string, stateObj?: HassEntity) => {
return is_off ? mdiBattery : mdiBatteryOutline;
case "battery_charging":
return is_off ? mdiBattery : mdiBatteryCharging;
case "carbon_monoxide":
return is_off ? mdiSmokeDetector : mdiSmokeDetectorAlert;
case "cold":
return is_off ? mdiThermometer : mdiSnowflake;
case "connectivity":
@@ -73,7 +68,7 @@ export const binarySensorIcon = (state?: string, stateObj?: HassEntity) => {
case "tamper":
return is_off ? mdiCheckCircle : mdiAlertCircle;
case "smoke":
return is_off ? mdiSmokeDetectorVariant : mdiSmokeDetectorVariantAlert;
return is_off ? mdiCheckCircle : mdiSmoke;
case "heat":
return is_off ? mdiThermometer : mdiFire;
case "light":
+9 -10
View File
@@ -8,25 +8,26 @@ import {
mdiCalendar,
mdiCast,
mdiCastConnected,
mdiCheckCircleOutline,
mdiClock,
mdiCloseCircleOutline,
mdiGestureTapButton,
mdiLanConnect,
mdiLanDisconnect,
mdiLightSwitch,
mdiLock,
mdiLockAlert,
mdiLockClock,
mdiLockOpen,
mdiPackage,
mdiPackageDown,
mdiPackageUp,
mdiPowerPlug,
mdiPowerPlugOff,
mdiRestart,
mdiToggleSwitchVariant,
mdiToggleSwitchVariantOff,
mdiToggleSwitch,
mdiToggleSwitchOff,
mdiCheckCircleOutline,
mdiCloseCircleOutline,
mdiWeatherNight,
mdiPackage,
mdiPackageDown,
} from "@mdi/js";
import { HassEntity } from "home-assistant-js-websocket";
import { updateIsInstalling, UpdateEntity } from "../../data/update";
@@ -108,11 +109,9 @@ export const domainIcon = (
case "outlet":
return compareState === "on" ? mdiPowerPlug : mdiPowerPlugOff;
case "switch":
return compareState === "on"
? mdiToggleSwitchVariant
: mdiToggleSwitchVariantOff;
return compareState === "on" ? mdiToggleSwitch : mdiToggleSwitchOff;
default:
return mdiToggleSwitchVariant;
return mdiLightSwitch;
}
case "sensor": {
+2 -2
View File
@@ -347,8 +347,8 @@ class StatisticsChart extends LitElement {
statTypes.forEach((type) => {
let val: number | null;
if (type === "sum") {
if (initVal === null) {
initVal = val = stat.state || 0;
if (!initVal) {
initVal = val = stat.state;
prevSum = stat.sum;
} else {
val = initVal + ((stat.sum || 0) - prevSum!);
@@ -52,8 +52,6 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
@property() public value?: string;
@property() public helper?: string;
@property() public devices?: DeviceRegistryEntry[];
@property() public areas?: AreaRegistryEntry[];
@@ -271,7 +269,6 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
? this.hass.localize("ui.components.device-picker.device")
: this.label}
.value=${this._value}
.helper=${this.helper}
.renderer=${rowRenderer}
.disabled=${this.disabled}
.required=${this.required}
+1 -14
View File
@@ -4,7 +4,6 @@ import { fireEvent } from "../../common/dom/fire_event";
import { PolymerChangedEvent } from "../../polymer-types";
import { HomeAssistant } from "../../types";
import "./ha-device-picker";
import type { HaDevicePickerDeviceFilterFunc } from "./ha-device-picker";
@customElement("ha-devices-picker")
class HaDevicesPicker extends LitElement {
@@ -12,10 +11,6 @@ class HaDevicesPicker extends LitElement {
@property() public value?: string[];
@property() public helper?: string;
@property({ type: Boolean }) public disabled?: boolean;
@property({ type: Boolean }) public required?: boolean;
/**
@@ -42,8 +37,6 @@ class HaDevicesPicker extends LitElement {
@property({ attribute: "pick-device-label" }) public pickDeviceLabel?: string;
@property() public deviceFilter?: HaDevicePickerDeviceFilterFunc;
protected render(): TemplateResult {
if (!this.hass) {
return html``;
@@ -58,13 +51,11 @@ class HaDevicesPicker extends LitElement {
allow-custom-entity
.curValue=${entityId}
.hass=${this.hass}
.deviceFilter=${this.deviceFilter}
.includeDomains=${this.includeDomains}
.excludeDomains=${this.excludeDomains}
.includeDeviceClasses=${this.includeDeviceClasses}
.value=${entityId}
.label=${this.pickedDeviceLabel}
.disabled=${this.disabled}
@value-changed=${this._deviceChanged}
></ha-device-picker>
</div>
@@ -72,16 +63,12 @@ class HaDevicesPicker extends LitElement {
)}
<div>
<ha-device-picker
allow-custom-entity
.hass=${this.hass}
.helper=${this.helper}
.deviceFilter=${this.deviceFilter}
.includeDomains=${this.includeDomains}
.excludeDomains=${this.excludeDomains}
.includeDeviceClasses=${this.includeDeviceClasses}
.label=${this.pickDeviceLabel}
.disabled=${this.disabled}
.required=${this.required && !currentDevices.length}
.required=${this.required}
@value-changed=${this._addDevice}
></ha-device-picker>
</div>
+1 -9
View File
@@ -14,12 +14,8 @@ class HaEntitiesPickerLight extends LitElement {
@property({ type: Array }) public value?: string[];
@property({ type: Boolean }) public disabled?: boolean;
@property({ type: Boolean }) public required?: boolean;
@property() public helper?: string;
/**
* Show entities from specific domains.
* @type {string}
@@ -98,7 +94,6 @@ class HaEntitiesPickerLight extends LitElement {
.entityFilter=${this._entityFilter}
.value=${entityId}
.label=${this.pickedEntityLabel}
.disabled=${this.disabled}
@value-changed=${this._entityChanged}
></ha-entity-picker>
</div>
@@ -106,7 +101,6 @@ class HaEntitiesPickerLight extends LitElement {
)}
<div>
<ha-entity-picker
allow-custom-entity
.hass=${this.hass}
.includeDomains=${this.includeDomains}
.excludeDomains=${this.excludeDomains}
@@ -116,9 +110,7 @@ class HaEntitiesPickerLight extends LitElement {
.includeUnitOfMeasurement=${this.includeUnitOfMeasurement}
.entityFilter=${this._entityFilter}
.label=${this.pickEntityLabel}
.helper=${this.helper}
.disabled=${this.disabled}
.required=${this.required && !currentEntities.length}
.required=${this.required}
@value-changed=${this._addEntity}
></ha-entity-picker>
</div>
@@ -28,8 +28,6 @@ class HaEntityAttributePicker extends LitElement {
@property() public value?: string;
@property() public helper?: string;
@property({ type: Boolean }) private _opened = false;
@query("ha-combo-box", true) private _comboBox!: HaComboBox;
@@ -66,7 +64,6 @@ class HaEntityAttributePicker extends LitElement {
)}
.disabled=${this.disabled || !this.entityId}
.required=${this.required}
.helper=${this.helper}
.allowCustomValue=${this.allowCustomValue}
item-value-path="value"
item-label-path="label"
@@ -48,8 +48,6 @@ export class HaEntityPicker extends LitElement {
@property() public value?: string;
@property() public helper?: string;
/**
* Show entities from specific domains.
* @type {Array}
@@ -306,7 +304,6 @@ export class HaEntityPicker extends LitElement {
.label=${this.label === undefined
? this.hass.localize("ui.components.entity.entity-picker.entity")
: this.label}
.helper=${this.helper}
.allowCustomValue=${this.allowCustomEntity}
.filteredItems=${this._states}
.renderer=${rowRenderer}
-3
View File
@@ -29,8 +29,6 @@ class HaAddonPicker extends LitElement {
@property() public value = "";
@property() public helper?: string;
@state() private _addons?: HassioAddonInfo[];
@property({ type: Boolean }) public disabled = false;
@@ -64,7 +62,6 @@ class HaAddonPicker extends LitElement {
.value=${this._value}
.required=${this.required}
.disabled=${this.disabled}
.helper=${this.helper}
.renderer=${rowRenderer}
.items=${this._addons}
item-value-path="slug"
-3
View File
@@ -49,8 +49,6 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
@property() public value?: string;
@property() public helper?: string;
@property() public placeholder?: string;
@property({ type: Boolean, attribute: "no-add" })
@@ -314,7 +312,6 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
return html`
<ha-combo-box
.hass=${this.hass}
.helper=${this.helper}
item-value-path="area_id"
item-id-path="area_id"
item-label-path="name"
+1 -4
View File
@@ -15,8 +15,6 @@ export class HaAreasPicker extends SubscribeMixin(LitElement) {
@property() public value?: string[];
@property() public helper?: string;
@property() public placeholder?: string;
@property({ type: Boolean, attribute: "no-add" })
@@ -92,7 +90,6 @@ export class HaAreasPicker extends SubscribeMixin(LitElement) {
.noAdd=${this.noAdd}
.hass=${this.hass}
.label=${this.pickAreaLabel}
.helper=${this.helper}
.includeDomains=${this.includeDomains}
.excludeDomains=${this.excludeDomains}
.includeDeviceClasses=${this.includeDeviceClasses}
@@ -100,7 +97,7 @@ export class HaAreasPicker extends SubscribeMixin(LitElement) {
.entityFilter=${this.entityFilter}
.disabled=${this.disabled}
.placeholder=${this.placeholder}
.required=${this.required && !currentAreas.length}
.required=${this.required}
@value-changed=${this._addArea}
></ha-area-picker>
</div>
+9 -5
View File
@@ -5,7 +5,6 @@ import { fireEvent } from "../common/dom/fire_event";
import { stopPropagation } from "../common/dom/stop_propagation";
import "./ha-select";
import "./ha-textfield";
import "./ha-input-helper-text";
export interface TimeChangedEvent {
days?: number;
@@ -131,7 +130,7 @@ export class HaBaseTimeInput extends LitElement {
protected render(): TemplateResult {
return html`
${this.label
? html`<label>${this.label}${this.required ? " *" : ""}</label>`
? html`<label>${this.label}${this.required ? "*" : ""}</label>`
: ""}
<div class="time-input-wrap">
${this.enableDay
@@ -254,9 +253,7 @@ export class HaBaseTimeInput extends LitElement {
<mwc-list-item value="PM">PM</mwc-list-item>
</ha-select>`}
</div>
${this.helper
? html`<ha-input-helper-text>${this.helper}</ha-input-helper-text>`
: ""}
${this.helper ? html`<div class="helper">${this.helper}</div>` : ""}
`;
}
@@ -353,6 +350,13 @@ export class HaBaseTimeInput extends LitElement {
color: var(--mdc-theme-text-primary-on-background, rgba(0, 0, 0, 0.87));
padding-left: 4px;
}
.helper {
color: var(--mdc-text-field-label-ink-color, rgba(0, 0, 0, 0.6));
font-size: 0.75rem;
padding-left: 16px;
padding-right: 16px;
}
`;
}
-13
View File
@@ -117,19 +117,6 @@ export class HaButtonToggleGroup extends LitElement {
--mdc-shape-small: 4px;
border-right-width: 1px;
}
:host([dir="rtl"]) ha-icon-button:first-child,
:host([dir="rtl"]) mwc-button:first-child {
border-radius: 0 4px 4px 0;
border-right-width: 1px;
--mdc-shape-small: 0 4px 4px 0;
--mdc-button-outline-width: 1px;
}
:host([dir="rtl"]) ha-icon-button:last-child,
:host([dir="rtl"]) mwc-button:last-child {
--mdc-shape-small: 4px 0 0 4px;
border-radius: 4px 0 0 4px;
}
`;
}
}
-4
View File
@@ -64,8 +64,6 @@ export class HaComboBox extends LitElement {
@property() public validationMessage?: string;
@property() public helper?: string;
@property({ attribute: "error-message" }) public errorMessage?: string;
@property({ type: Boolean }) public invalid?: boolean;
@@ -149,8 +147,6 @@ export class HaComboBox extends LitElement {
.suffix=${html`<div style="width: 28px;"></div>`}
.icon=${this.icon}
.invalid=${this.invalid}
.helper=${this.helper}
helperPersistent
>
<slot name="icon" slot="leadingIcon"></slot>
</ha-textfield>
-4
View File
@@ -39,15 +39,11 @@ export class HaDateInput extends LitElement {
@property() public label?: string;
@property() public helper?: string;
render() {
return html`<ha-textfield
.label=${this.label}
.helper=${this.helper}
.disabled=${this.disabled}
iconTrailing
helperPersistent
@click=${this._openDialog}
.value=${this.value
? formatDateNumeric(new Date(this.value), this.locale)
+2 -1
View File
@@ -77,7 +77,7 @@ export class HaForm extends LitElement implements HaFormElement {
protected render(): TemplateResult {
return html`
<div class="root" part="root">
<div class="root">
${this.error && this.error.base
? html`
<ha-alert alert-type="error">
@@ -173,6 +173,7 @@ export class HaForm extends LitElement implements HaFormElement {
}
static get styles(): CSSResultGroup {
// .root has overflow: auto to avoid margin collapse
return css`
.root {
margin-bottom: -24px;
-3
View File
@@ -31,8 +31,6 @@ export class HaIconPicker extends LitElement {
@property() public label?: string;
@property() public helper?: string;
@property() public placeholder?: string;
@property() public fallbackPath?: string;
@@ -59,7 +57,6 @@ export class HaIconPicker extends LitElement {
allow-custom-value
.filteredItems=${iconItems}
.label=${this.label}
.helper=${this.helper}
.disabled=${this.disabled}
.required=${this.required}
.placeholder=${this.placeholder}
-25
View File
@@ -1,25 +0,0 @@
import { css, html, LitElement, TemplateResult } from "lit";
import { customElement } from "lit/decorators";
@customElement("ha-input-helper-text")
class InputHelperText extends LitElement {
protected render(): TemplateResult {
return html`<slot></slot>`;
}
static styles = css`
:host {
display: block;
color: var(--mdc-text-field-label-ink-color, rgba(0, 0, 0, 0.6));
font-size: 0.75rem;
padding-left: 16px;
padding-right: 16px;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-input-helper-text": InputHelperText;
}
}
+1 -5
View File
@@ -46,14 +46,11 @@ class HaLabeledSlider extends PolymerElement {
value="{{value}}"
></ha-slider>
</div>
<template is="dom-if" if="[[helper]]">
<ha-input-helper-text>[[helper]]</ha-input-helper-text>
</template>
`;
}
_getTitle() {
return `${this.caption}${this.caption && this.required ? " *" : ""}`;
return `${this.caption}${this.required ? "*" : ""}`;
}
static get properties() {
@@ -65,7 +62,6 @@ class HaLabeledSlider extends PolymerElement {
max: Number,
pin: Boolean,
step: Number,
helper: String,
extra: {
type: Boolean,
+7 -3
View File
@@ -3,6 +3,7 @@ import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import { computeDomain } from "../common/entity/compute_domain";
import { subscribeNotifications } from "../data/persistent_notification";
import { HomeAssistant } from "../types";
import "./ha-icon-button";
@@ -42,15 +43,18 @@ class HaMenuButton extends LitElement {
protected render(): TemplateResult {
const hasNotifications =
this._hasNotifications &&
(this.narrow || this.hass.dockedSidebar === "always_hidden");
(this.narrow || this.hass.dockedSidebar === "always_hidden") &&
(this._hasNotifications ||
Object.keys(this.hass.states).some(
(entityId) => computeDomain(entityId) === "configurator"
));
return html`
<ha-icon-button
.label=${this.hass.localize("ui.sidebar.sidebar_toggle")}
.path=${mdiMenu}
@click=${this._toggleMenu}
></ha-icon-button>
${hasNotifications ? html`<div class="dot"></div>` : ""}
${hasNotifications ? html` <div class="dot"></div> ` : ""}
`;
}
@@ -14,8 +14,6 @@ export class HaAddonSelector extends LitElement {
@property() public label?: string;
@property() public helper?: string;
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public required = true;
@@ -25,7 +23,6 @@ export class HaAddonSelector extends LitElement {
.hass=${this.hass}
.value=${this.value}
.label=${this.label}
.helper=${this.helper}
.disabled=${this.disabled}
.required=${this.required}
allow-custom-entity
@@ -18,8 +18,6 @@ export class HaAreaSelector extends LitElement {
@property() public label?: string;
@property() public helper?: string;
@state() public _configEntries?: ConfigEntry[];
@property({ type: Boolean }) public disabled = false;
@@ -49,7 +47,6 @@ export class HaAreaSelector extends LitElement {
.hass=${this.hass}
.value=${this.value}
.label=${this.label}
.helper=${this.helper}
no-add
.deviceFilter=${this._filterDevices}
.entityFilter=${this._filterEntities}
@@ -69,7 +66,6 @@ export class HaAreaSelector extends LitElement {
<ha-areas-picker
.hass=${this.hass}
.value=${this.value}
.helper=${this.helper}
.pickAreaLabel=${this.label}
no-add
.deviceFilter=${this._filterDevices}
@@ -16,8 +16,6 @@ export class HaSelectorAttribute extends SubscribeMixin(LitElement) {
@property() public label?: string;
@property() public helper?: string;
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public required = true;
@@ -34,7 +32,6 @@ export class HaSelectorAttribute extends SubscribeMixin(LitElement) {
this.context?.filter_entity}
.value=${this.value}
.label=${this.label}
.helper=${this.helper}
.disabled=${this.disabled}
.required=${this.required}
allow-custom-value
@@ -4,7 +4,6 @@ import { fireEvent } from "../../common/dom/fire_event";
import { HomeAssistant } from "../../types";
import "../ha-formfield";
import "../ha-switch";
import "../ha-input-helper-text";
@customElement("ha-selector-boolean")
export class HaBooleanSelector extends LitElement {
@@ -14,23 +13,16 @@ export class HaBooleanSelector extends LitElement {
@property() public label?: string;
@property() public helper?: string;
@property({ type: Boolean }) public disabled = false;
protected render() {
return html`
<ha-formfield alignEnd spaceBetween .label=${this.label}>
<ha-switch
.checked=${this.value}
@change=${this._handleChange}
.disabled=${this.disabled}
></ha-switch>
</ha-formfield>
${this.helper
? html`<ha-input-helper-text>${this.helper}</ha-input-helper-text>`
: ""}
`;
return html`<ha-formfield alignEnd spaceBetween .label=${this.label}>
<ha-switch
.checked=${this.value}
@change=${this._handleChange}
.disabled=${this.disabled}
></ha-switch>
</ha-formfield>`;
}
private _handleChange(ev) {
@@ -43,10 +35,12 @@ export class HaBooleanSelector extends LitElement {
static get styles(): CSSResultGroup {
return css`
ha-formfield {
display: flex;
:host {
height: 56px;
align-items: center;
display: flex;
}
ha-formfield {
width: 100%;
--mdc-typography-body2-font-size: 1em;
}
`;
@@ -16,8 +16,6 @@ export class HaColorRGBSelector extends LitElement {
@property() public label?: string;
@property() public helper?: string;
@property({ type: Boolean, reflect: true }) public disabled = false;
@property({ type: Boolean }) public required = true;
@@ -26,11 +24,9 @@ export class HaColorRGBSelector extends LitElement {
return html`
<ha-textfield
type="color"
helperPersistent
.value=${this.value ? rgb2hex(this.value as any) : ""}
.label=${this.label || ""}
.required=${this.required}
.helper=${this.helper}
.disalbled=${this.disabled}
@change=${this._valueChanged}
></ha-textfield>
@@ -15,8 +15,6 @@ export class HaColorTempSelector extends LitElement {
@property() public label?: string;
@property() public helper?: string;
@property({ type: Boolean, reflect: true }) public disabled = false;
@property({ type: Boolean }) public required = true;
@@ -26,12 +24,11 @@ export class HaColorTempSelector extends LitElement {
<ha-labeled-slider
pin
icon="hass:thermometer"
.caption=${this.label || ""}
.caption=${this.label}
.min=${this.selector.color_temp.min_mireds ?? 153}
.max=${this.selector.color_temp.max_mireds ?? 500}
.value=${this.value}
.disabled=${this.disabled}
.helper=${this.helper}
.required=${this.required}
@change=${this._valueChanged}
></ha-labeled-slider>
@@ -14,8 +14,6 @@ export class HaDateSelector extends LitElement {
@property() public label?: string;
@property() public helper?: string;
@property({ type: Boolean, reflect: true }) public disabled = false;
@property({ type: Boolean }) public required = true;
@@ -28,7 +26,6 @@ export class HaDateSelector extends LitElement {
.disabled=${this.disabled}
.value=${this.value}
.required=${this.required}
.helper=${this.helper}
>
</ha-date-input>
`;
@@ -6,7 +6,6 @@ import type { HomeAssistant } from "../../types";
import "../ha-date-input";
import type { HaDateInput } from "../ha-date-input";
import "../ha-time-input";
import "../ha-input-helper-text";
import type { HaTimeInput } from "../ha-time-input";
@customElement("ha-selector-datetime")
@@ -19,8 +18,6 @@ export class HaDateTimeSelector extends LitElement {
@property() public label?: string;
@property() public helper?: string;
@property({ type: Boolean, reflect: true }) public disabled = false;
@property({ type: Boolean }) public required = true;
@@ -33,28 +30,23 @@ export class HaDateTimeSelector extends LitElement {
const values = this.value?.split(" ");
return html`
<div class="input">
<ha-date-input
.label=${this.label}
.locale=${this.hass.locale}
.disabled=${this.disabled}
.required=${this.required}
.value=${values?.[0]}
@value-changed=${this._valueChanged}
>
</ha-date-input>
<ha-time-input
enable-second
.value=${values?.[1] || "0:00:00"}
.locale=${this.hass.locale}
.disabled=${this.disabled}
.required=${this.required}
@value-changed=${this._valueChanged}
></ha-time-input>
</div>
${this.helper
? html`<ha-input-helper-text>${this.helper}</ha-input-helper-text>`
: ""}
<ha-date-input
.label=${this.label}
.locale=${this.hass.locale}
.disabled=${this.disabled}
.required=${this.required}
.value=${values?.[0]}
@value-changed=${this._valueChanged}
>
</ha-date-input>
<ha-time-input
enable-second
.value=${values?.[1] || "0:00:00"}
.locale=${this.hass.locale}
.disabled=${this.disabled}
.required=${this.required}
@value-changed=${this._valueChanged}
></ha-time-input>
`;
}
@@ -66,7 +58,7 @@ export class HaDateTimeSelector extends LitElement {
}
static styles = css`
.input {
:host {
display: flex;
align-items: center;
flex-direction: row;
@@ -17,8 +17,6 @@ export class HaDeviceSelector extends LitElement {
@property() public label?: string;
@property() public helper?: string;
@state() public _configEntries?: ConfigEntry[];
@property({ type: Boolean }) public disabled = false;
@@ -45,7 +43,6 @@ export class HaDeviceSelector extends LitElement {
.hass=${this.hass}
.value=${this.value}
.label=${this.label}
.helper=${this.helper}
.deviceFilter=${this._filterDevices}
.includeDeviceClasses=${this.selector.device.entity?.device_class
? [this.selector.device.entity.device_class]
@@ -65,15 +62,12 @@ export class HaDeviceSelector extends LitElement {
<ha-devices-picker
.hass=${this.hass}
.value=${this.value}
.helper=${this.helper}
.deviceFilter=${this._filterDevices}
.includeDeviceClasses=${this.selector.device.entity?.device_class
? [this.selector.device.entity.device_class]
: undefined}
.includeDomains=${this.selector.device.entity?.domain
? [this.selector.device.entity.domain]
: undefined}
.disabled=${this.disabled}
.required=${this.required}
></ha-devices-picker>
`;
@@ -23,8 +23,6 @@ export class HaEntitySelector extends LitElement {
@property() public label?: string;
@property() public helper?: string;
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public required = true;
@@ -35,7 +33,6 @@ export class HaEntitySelector extends LitElement {
.hass=${this.hass}
.value=${this.value}
.label=${this.label}
.helper=${this.helper}
.includeEntities=${this.selector.entity.include_entities}
.excludeEntities=${this.selector.entity.exclude_entities}
.entityFilter=${this._filterEntities}
@@ -50,11 +47,9 @@ export class HaEntitySelector extends LitElement {
<ha-entities-picker
.hass=${this.hass}
.value=${this.value}
.helper=${this.helper}
.entityFilter=${this._filterEntities}
.includeEntities=${this.selector.entity.include_entities}
.excludeEntities=${this.selector.entity.exclude_entities}
.entityFilter=${this._filterEntities}
.disabled=${this.disabled}
.required=${this.required}
></ha-entities-picker>
`;
@@ -15,8 +15,6 @@ export class HaIconSelector extends LitElement {
@property() public label?: string;
@property() public helper?: string;
@property({ type: Boolean, reflect: true }) public disabled = false;
@property({ type: Boolean }) public required = true;
@@ -28,7 +26,6 @@ export class HaIconSelector extends LitElement {
.value=${this.value}
.required=${this.required}
.disabled=${this.disabled}
.helper=${this.helper}
.fallbackPath=${this.selector.icon.fallbackPath}
.placeholder=${this.selector.icon.placeholder}
@value-changed=${this._valueChanged}
@@ -20,8 +20,6 @@ export class HaLocationSelector extends LitElement {
@property() public label?: string;
@property() public helper?: string;
@property({ type: Boolean, reflect: true }) public disabled = false;
protected render() {
@@ -29,7 +27,6 @@ export class HaLocationSelector extends LitElement {
<ha-locations-editor
class="flex"
.hass=${this.hass}
.helper=${this.helper}
.locations=${this._location(this.selector, this.value)}
@location-updated=${this._locationChanged}
@radius-updated=${this._radiusChanged}
@@ -33,8 +33,6 @@ export class HaMediaSelector extends LitElement {
@property() public label?: string;
@property() public helper?: string;
@property({ type: Boolean, reflect: true }) public disabled = false;
@property({ type: Boolean, reflect: true }) public required = true;
@@ -88,7 +86,6 @@ export class HaMediaSelector extends LitElement {
.label=${this.label ||
this.hass.localize("ui.components.selectors.media.pick_media_player")}
.disabled=${this.disabled}
.helper=${this.helper}
.required=${this.required}
include-domains='["media_player"]'
allow-custom-entity
@@ -6,7 +6,6 @@ import { NumberSelector } from "../../data/selector";
import { HomeAssistant } from "../../types";
import "../ha-slider";
import "../ha-textfield";
import "../ha-input-helper-text";
@customElement("ha-selector-number")
export class HaNumberSelector extends LitElement {
@@ -27,13 +26,8 @@ export class HaNumberSelector extends LitElement {
@property({ type: Boolean }) public disabled = false;
protected render() {
const isBox = this.selector.number.mode === "box";
return html`
${this.label ? html`${this.label}${this.required ? " *" : ""}` : ""}
<div class="input">
${!isBox
? html`<ha-slider
return html`${this.selector.number.mode !== "box"
? html`${this.label}${this.required ? "*" : ""}<ha-slider
.min=${this.selector.number.min}
.max=${this.selector.number.max}
.value=${this._value}
@@ -45,33 +39,28 @@ export class HaNumberSelector extends LitElement {
@change=${this._handleSliderChange}
>
</ha-slider>`
: ""}
<ha-textfield
inputMode="numeric"
pattern="[0-9]+([\\.][0-9]+)?"
.label=${this.selector.number.mode !== "box" ? undefined : this.label}
.placeholder=${this.placeholder}
class=${classMap({ single: this.selector.number.mode === "box" })}
.min=${this.selector.number.min}
.max=${this.selector.number.max}
.value=${this.value ?? ""}
.step=${this.selector.number.step ?? 1}
helperPersistent
.helper=${isBox ? this.helper : undefined}
.disabled=${this.disabled}
.required=${this.required}
.suffix=${this.selector.number.unit_of_measurement}
type="number"
autoValidate
?no-spinner=${this.selector.number.mode !== "box"}
@input=${this._handleInputChange}
>
</ha-textfield>
</div>
${!isBox && this.helper
? html`<ha-input-helper-text>${this.helper}</ha-input-helper-text>`
: ""}
`;
<ha-textfield
inputMode="numeric"
pattern="[0-9]+([\\.][0-9]+)?"
.label=${this.selector.number.mode !== "box" ? undefined : this.label}
.placeholder=${this.placeholder}
class=${classMap({ single: this.selector.number.mode === "box" })}
.min=${this.selector.number.min}
.max=${this.selector.number.max}
.value=${this.value ?? ""}
.step=${this.selector.number.step ?? 1}
helperPersistent
.helper=${this.helper}
.disabled=${this.disabled}
.required=${this.required}
.suffix=${this.selector.number.unit_of_measurement}
type="number"
autoValidate
?no-spinner=${this.selector.number.mode !== "box"}
@input=${this._handleInputChange}
>
</ha-textfield>`;
}
private get _value() {
@@ -103,7 +92,7 @@ export class HaNumberSelector extends LitElement {
static get styles(): CSSResultGroup {
return css`
.input {
:host {
display: flex;
justify-content: space-between;
align-items: center;
@@ -3,7 +3,6 @@ import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import { HomeAssistant } from "../../types";
import "../ha-yaml-editor";
import "../ha-input-helper-text";
@customElement("ha-selector-object")
export class HaObjectSelector extends LitElement {
@@ -13,8 +12,6 @@ export class HaObjectSelector extends LitElement {
@property() public label?: string;
@property() public helper?: string;
@property() public placeholder?: string;
@property({ type: Boolean }) public disabled = false;
@@ -23,17 +20,13 @@ export class HaObjectSelector extends LitElement {
protected render() {
return html`<ha-yaml-editor
.hass=${this.hass}
.readonly=${this.disabled}
.label=${this.label}
.required=${this.required}
.placeholder=${this.placeholder}
.defaultValue=${this.value}
@value-changed=${this._handleChange}
></ha-yaml-editor>
${this.helper
? html`<ha-input-helper-text>${this.helper}</ha-input-helper-text>`
: ""} `;
.hass=${this.hass}
.readonly=${this.disabled}
.required=${this.required}
.placeholder=${this.placeholder}
.defaultValue=${this.value}
@value-changed=${this._handleChange}
></ha-yaml-editor>`;
}
private _handleChange(ev) {
@@ -58,7 +58,6 @@ export class HaSelectSelector extends LitElement {
`
)}
</div>
${this._renderHelper()}
`;
}
@@ -77,7 +76,6 @@ export class HaSelectSelector extends LitElement {
`
)}
</div>
${this._renderHelper()}
`;
}
@@ -109,9 +107,8 @@ export class HaSelectSelector extends LitElement {
item-label-path="label"
.hass=${this.hass}
.label=${this.label}
.helper=${this.helper}
.disabled=${this.disabled}
.required=${this.required && !value.length}
.required=${this.required}
.value=${this._filter}
.items=${options.filter((item) => !this.value?.includes(item.value))}
@filter-changed=${this._filterChanged}
@@ -134,7 +131,6 @@ export class HaSelectSelector extends LitElement {
item-label-path="label"
.hass=${this.hass}
.label=${this.label}
.helper=${this.helper}
.disabled=${this.disabled}
.required=${this.required}
.items=${options}
@@ -165,12 +161,6 @@ export class HaSelectSelector extends LitElement {
`;
}
private _renderHelper() {
return this.helper
? html`<ha-input-helper-text>${this.helper}</ha-input-helper-text>`
: "";
}
private get _mode(): "list" | "dropdown" {
return (
this.selector.select.mode ||
@@ -26,8 +26,6 @@ export class HaTargetSelector extends SubscribeMixin(LitElement) {
@property() public label?: string;
@property() public helper?: string;
@state() private _entityPlaformLookup?: Record<string, string>;
@state() private _configEntries?: ConfigEntry[];
@@ -66,7 +64,6 @@ export class HaTargetSelector extends SubscribeMixin(LitElement) {
return html`<ha-target-picker
.hass=${this.hass}
.value=${this.value}
.helper=${this.helper}
.deviceFilter=${this._filterDevices}
.entityRegFilter=${this._filterRegEntities}
.entityFilter=${this._filterEntities}
@@ -14,8 +14,6 @@ export class HaTimeSelector extends LitElement {
@property() public label?: string;
@property() public helper?: string;
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public required = false;
@@ -27,7 +25,6 @@ export class HaTimeSelector extends LitElement {
.locale=${this.hass.locale}
.disabled=${this.disabled}
.required=${this.required}
.helper=${this.helper}
.label=${this.label}
enable-second
></ha-time-input>
+13 -20
View File
@@ -36,9 +36,10 @@ import memoizeOne from "memoize-one";
import { LocalStorage } from "../common/decorators/local-storage";
import { fireEvent } from "../common/dom/fire_event";
import { toggleAttribute } from "../common/dom/toggle_attribute";
import { computeDomain } from "../common/entity/compute_domain";
import { computeStateDomain } from "../common/entity/compute_state_domain";
import { stringCompare } from "../common/string/compare";
import { computeRTL } from "../common/util/compute_rtl";
import { throttle } from "../common/util/throttle";
import { ActionHandlerDetail } from "../data/lovelace";
import {
PersistentNotification,
@@ -293,7 +294,11 @@ class HaSidebar extends LitElement {
toggleAttribute(this, "rtl", computeRTL(this.hass));
}
this._calculateCounts();
this._updatesCount = Object.values(this.hass.states).filter(
(entity) =>
computeStateDomain(entity) === "update" &&
updateCanInstall(entity as UpdateEntity)
).length;
if (!SUPPORT_SCROLL_IF_NEEDED) {
return;
@@ -307,21 +312,6 @@ class HaSidebar extends LitElement {
}
}
private _calculateCounts = throttle(() => {
let updateCount = 0;
for (const entityId of Object.keys(this.hass.states)) {
if (
entityId.startsWith("update.") &&
updateCanInstall(this.hass.states[entityId] as UpdateEntity)
) {
updateCount++;
}
}
this._updatesCount = updateCount;
}, 5000);
private _renderHeader() {
return html`<div
class="menu"
@@ -529,9 +519,14 @@ class HaSidebar extends LitElement {
}
private _renderNotifications() {
const notificationCount = this._notifications
let notificationCount = this._notifications
? this._notifications.length
: 0;
for (const entityId in this.hass.states) {
if (computeDomain(entityId) === "configurator") {
notificationCount++;
}
}
return html`<div
class="notifications-container"
@@ -1039,8 +1034,6 @@ class HaSidebar extends LitElement {
.notification-badge,
.configuration-badge {
left: calc(var(--app-drawer-width) - 42px);
position: absolute;
min-width: 20px;
box-sizing: border-box;
border-radius: 50%;
+1 -8
View File
@@ -43,7 +43,6 @@ import type { HaEntityPickerEntityFilterFunc } from "./entity/ha-entity-picker";
import "./ha-area-picker";
import "./ha-icon-button";
import "./ha-svg-icon";
import "./ha-input-helper-text";
@customElement("ha-target-picker")
export class HaTargetPicker extends SubscribeMixin(LitElement) {
@@ -53,8 +52,6 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
@property() public label?: string;
@property() public helper?: string;
/**
* Show only targets with entities from specific domains.
* @type {Array}
@@ -216,11 +213,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
</span>
</span>
</div>
</div>
${this.helper
? html`<ha-input-helper-text>${this.helper}</ha-input-helper-text>`
: ""} `;
</div>`;
}
private async _showPicker(ev) {
-3
View File
@@ -14,8 +14,6 @@ export class HaTimeInput extends LitElement {
@property() public label?: string;
@property() public helper?: string;
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public required = false;
@@ -48,7 +46,6 @@ export class HaTimeInput extends LitElement {
@value-changed=${this._timeChanged}
.enableSecond=${this.enableSecond}
.required=${this.required}
.helper=${this.helper}
></ha-base-time-input>
`;
}
+1 -3
View File
@@ -61,9 +61,7 @@ export class HaYamlEditor extends LitElement {
return html``;
}
return html`
${this.label
? html`<p>${this.label}${this.required ? " *" : ""}</p>`
: ""}
${this.label ? html`<p>${this.label}${this.required ? "*" : ""}</p>` : ""}
<ha-code-editor
.hass=${this.hass}
.value=${this._yaml}
+11 -16
View File
@@ -21,7 +21,6 @@ import type { LeafletModuleType } from "../../common/dom/setup-leaflet-map";
import type { HomeAssistant } from "../../types";
import "./ha-map";
import type { HaMap } from "./ha-map";
import "../ha-input-helper-text";
declare global {
// for fire event
@@ -51,8 +50,6 @@ export class HaLocationsEditor extends LitElement {
@property({ attribute: false }) public locations?: MarkerLocation[];
@property() public helper?: string;
@property({ type: Boolean }) public autoFit = false;
@property({ type: Number }) public zoom = 16;
@@ -105,18 +102,13 @@ export class HaLocationsEditor extends LitElement {
}
protected render(): TemplateResult {
return html`
<ha-map
.hass=${this.hass}
.layers=${this._getLayers(this._circles, this._locationMarkers)}
.zoom=${this.zoom}
.autoFit=${this.autoFit}
.darkMode=${this.darkMode}
></ha-map>
${this.helper
? html`<ha-input-helper-text>${this.helper}</ha-input-helper-text>`
: ""}
`;
return html`<ha-map
.hass=${this.hass}
.layers=${this._getLayers(this._circles, this._locationMarkers)}
.zoom=${this.zoom}
.autoFit=${this.autoFit}
.darkMode=${this.darkMode}
></ha-map>`;
}
private _getLayers = memoizeOne(
@@ -295,10 +287,13 @@ export class HaLocationsEditor extends LitElement {
static get styles(): CSSResultGroup {
return css`
ha-map {
:host {
display: block;
height: 300px;
}
ha-map {
height: 100%;
}
`;
}
}
+1 -3
View File
@@ -8,8 +8,6 @@ import { BlueprintInput } from "./blueprint";
import { DeviceCondition, DeviceTrigger } from "./device_automation";
import { Action, MODES } from "./script";
export const AUTOMATION_DEFAULT_MODE: ManualAutomationConfig["mode"] = "single";
export interface AutomationEntity extends HassEntityBase {
attributes: HassEntityAttributeBase & {
id?: string;
@@ -69,7 +67,7 @@ export interface BaseTrigger {
export interface StateTrigger extends BaseTrigger {
platform: "state";
entity_id: string | string[];
entity_id: string;
attribute?: string;
from?: string | number;
to?: string | string[] | number;
+2 -3
View File
@@ -21,8 +21,7 @@ export type AddonState = "started" | "stopped" | null;
export type AddonRepository = "core" | "local" | string;
interface AddonTranslations {
network?: Record<string, string>;
configuration?: Record<string, { name?: string; description?: string }>;
[key: string]: Record<string, Record<string, Record<string, string>>>;
}
export interface HassioAddonInfo {
@@ -92,7 +91,7 @@ export interface HassioAddonDetails extends HassioAddonInfo {
slug: string;
startup: AddonStartup;
stdin: boolean;
translations: Record<string, AddonTranslations>;
translations: AddonTranslations;
watchdog: null | boolean;
webui: null | string;
}
-278
View File
@@ -1,278 +0,0 @@
import {
mdiEarth,
mdiNavigationVariantOutline,
mdiReload,
mdiServerNetwork,
} from "@mdi/js";
import { canShowPage } from "../common/config/can_show_page";
import { componentsWithService } from "../common/config/components_with_service";
import { computeDomain } from "../common/entity/compute_domain";
import { computeStateDisplay } from "../common/entity/compute_state_display";
import { computeStateName } from "../common/entity/compute_state_name";
import { domainIcon } from "../common/entity/domain_icon";
import { caseInsensitiveStringCompare } from "../common/string/compare";
import { ScorableTextItem } from "../common/string/filter/sequence-matching";
import { PageNavigation } from "../layouts/hass-tabs-subpage";
import { configSections } from "../panels/config/ha-panel-config";
import { HomeAssistant } from "../types";
import { AreaRegistryEntry } from "./area_registry";
import { computeDeviceName, DeviceRegistryEntry } from "./device_registry";
import { EntityRegistryEntry } from "./entity_registry";
import { domainToName } from "./integration";
import { getPanelNameTranslationKey } from "./panel";
export interface QuickBarItem extends ScorableTextItem {
primaryText: string;
primaryTextAlt?: string;
secondaryText?: string;
metaText?: string;
categoryKey:
| "reload"
| "navigation"
| "server_control"
| "entity"
| "suggestion";
actionData: string | string[];
iconPath?: string;
icon?: string;
path?: string;
isSuggestion?: boolean;
}
export type NavigationInfo = PageNavigation &
Pick<QuickBarItem, "primaryText" | "secondaryText">;
export type BaseNavigationCommand = Pick<QuickBarItem, "primaryText" | "path">;
export const generateEntityItems = (
hass: HomeAssistant,
entities: { [entityId: string]: EntityRegistryEntry },
devices: { [deviceId: string]: DeviceRegistryEntry },
areas: { [areaId: string]: AreaRegistryEntry }
): QuickBarItem[] =>
Object.keys(hass.states)
.map((entityId) => {
const entityState = hass.states[entityId];
const entity = entities[entityId];
const deviceName = entity?.device_id
? computeDeviceName(devices[entity.device_id], hass)
: undefined;
const entityItem = {
primaryText: computeStateName(entityState),
primaryTextAlt: computeStateDisplay(
hass.localize,
entityState,
hass.locale
),
secondaryText:
(deviceName ? `${deviceName} | ` : "") +
(hass.userData?.showAdvanced ? entityId : ""),
metaText: entity?.area_id ? areas[entity.area_id].name : undefined,
icon: entityState.attributes.icon,
iconPath: entityState.attributes.icon
? undefined
: domainIcon(computeDomain(entityId), entityState),
actionData: entityId,
categoryKey: "entity" as const,
};
return {
...entityItem,
strings: [entityItem.primaryText, entityItem.secondaryText],
};
})
.sort((a, b) => caseInsensitiveStringCompare(a.primaryText, b.primaryText));
export const generateCommandItems = (
hass: HomeAssistant
): Array<QuickBarItem[]> => [
generateNavigationCommands(hass),
generateReloadCommands(hass),
generateServerControlCommands(hass),
];
export const generateReloadCommands = (hass: HomeAssistant): QuickBarItem[] => {
// Get all domains that have a direct "reload" service
const reloadableDomains = componentsWithService(hass, "reload");
const commands = reloadableDomains.map((domain) => ({
primaryText:
hass.localize(`ui.dialogs.quick-bar.commands.reload.${domain}`) ||
hass.localize(
"ui.dialogs.quick-bar.commands.reload.reload",
"domain",
domainToName(hass.localize, domain)
),
actionData: [domain, "reload"],
secondaryText: "Reload changes made to the domain file",
}));
// Add "frontend.reload_themes"
commands.push({
primaryText: hass.localize("ui.dialogs.quick-bar.commands.reload.themes"),
actionData: ["frontend", "reload_themes"],
secondaryText: "Reload changes made to themes.yaml",
});
// Add "homeassistant.reload_core_config"
commands.push({
primaryText: hass.localize("ui.dialogs.quick-bar.commands.reload.core"),
actionData: ["homeassistant", "reload_core_config"],
secondaryText: "Reload changes made to configuration.yaml",
});
return commands.map((command) => ({
...command,
categoryKey: "reload",
iconPath: mdiReload,
strings: [
`${hass.localize("ui.dialogs.quick-bar.commands.types.reload")} ${
command.primaryText
}`,
],
}));
};
export const generateServerControlCommands = (
hass: HomeAssistant
): QuickBarItem[] => {
const serverActions = ["restart", "stop"];
return serverActions.map((action) => {
const categoryKey: QuickBarItem["categoryKey"] = "server_control";
const item = {
primaryText: hass.localize(
"ui.dialogs.quick-bar.commands.server_control.perform_action",
"action",
hass.localize(`ui.dialogs.quick-bar.commands.server_control.${action}`)
),
categoryKey,
actionData: action,
};
return {
...item,
strings: [
`${hass.localize(
`ui.dialogs.quick-bar.commands.types.${categoryKey}`
)} ${item.primaryText}`,
],
secondaryText: "Control your server",
iconPath: mdiServerNetwork,
};
});
};
export const generateNavigationCommands = (
hass: HomeAssistant
): QuickBarItem[] => {
const panelItems = generateNavigationPanelCommands(hass);
const sectionItems = generateNavigationConfigSectionCommands(hass);
return finalizeNavigationCommands([...panelItems, ...sectionItems], hass);
};
export const generateNavigationPanelCommands = (
hass: HomeAssistant
): BaseNavigationCommand[] =>
Object.keys(hass.panels)
.filter((panelKey) => panelKey !== "_my_redirect")
.map((panelKey) => {
const panel = hass.panels[panelKey];
const translationKey = getPanelNameTranslationKey(panel);
const primaryText =
hass.localize(translationKey) || panel.title || panel.url_path;
return {
primaryText,
path: `/${panel.url_path}`,
icon: panel.icon,
secondaryText: "Panel",
};
});
export const generateNavigationConfigSectionCommands = (
hass: HomeAssistant
): BaseNavigationCommand[] => {
const items: NavigationInfo[] = [];
for (const sectionKey of Object.keys(configSections)) {
for (const page of configSections[sectionKey]) {
if (!canShowPage(hass, page)) {
continue;
}
if (!page.component) {
continue;
}
const info = getNavigationInfoFromConfig(page, hass);
if (!info) {
continue;
}
// Add to list, but only if we do not already have an entry for the same path and component
if (
items.some(
(e) => e.path === info.path && e.component === info.component
)
) {
continue;
}
items.push({
iconPath: mdiNavigationVariantOutline,
...info,
});
}
}
return items;
};
export const getNavigationInfoFromConfig = (
page: PageNavigation,
hass: HomeAssistant
): NavigationInfo | undefined => {
if (!page.component) {
return undefined;
}
const caption = hass.localize(
`ui.dialogs.quick-bar.commands.navigation.${page.component}`
);
if (page.translationKey && caption) {
return {
...page,
primaryText: caption,
secondaryText: "Configuration Page",
};
}
return undefined;
};
const finalizeNavigationCommands = (
items: BaseNavigationCommand[],
hass: HomeAssistant
): QuickBarItem[] =>
items.map((item) => {
const categoryKey: QuickBarItem["categoryKey"] = "navigation";
const navItem = {
secondaryText: "Navigation",
iconPath: mdiEarth,
...item,
actionData: item.path!,
};
return {
categoryKey,
...navItem,
strings: [
`${hass.localize(
`ui.dialogs.quick-bar.commands.types.${categoryKey}`
)} ${navItem.primaryText}`,
],
};
});
+2 -2
View File
@@ -11,10 +11,10 @@ export const UPDATE_SUPPORT_SPECIFIC_VERSION = 2;
export const UPDATE_SUPPORT_PROGRESS = 4;
export const UPDATE_SUPPORT_BACKUP = 8;
export const UPDATE_SUPPORT_RELEASE_NOTES = 16;
export const UPDATE_SUPPORT_AUTO_UPDATE = 32;
interface UpdateEntityAttributes extends HassEntityAttributeBase {
auto_update: boolean | null;
installed_version: string | null;
current_version: string | null;
in_progress: boolean | number;
latest_version: string | null;
release_summary: string | null;
@@ -29,7 +29,6 @@ import {
DeviceRegistryEntry,
subscribeDeviceRegistry,
} from "../../data/device_registry";
import { fetchIntegrationManifest } from "../../data/integration";
import { haStyleDialog } from "../../resources/styles";
import type { HomeAssistant } from "../../types";
import { documentationUrl } from "../../util/documentation-url";
@@ -44,10 +43,10 @@ import "./step-flow-create-entry";
import "./step-flow-external";
import "./step-flow-form";
import "./step-flow-loading";
import "./step-flow-menu";
import "./step-flow-pick-flow";
import "./step-flow-pick-handler";
import "./step-flow-progress";
import "./step-flow-menu";
let instance = 0;
@@ -238,32 +237,22 @@ class DataEntryFlowDialog extends LitElement {
""
: html`
<div class="dialog-actions">
${([
"form",
"menu",
"external",
"progress",
"data_entry_flow_progressed",
].includes(this._step?.type as any) &&
this._params.manifest?.is_built_in) ||
this._params.manifest?.documentation
${["form", "menu", "external"].includes(
this._step?.type as any
)
? html`
<a
href=${this._params.manifest.is_built_in
? documentationUrl(
this.hass,
`/integrations/${this._params.manifest.domain}`
)
: this._params?.manifest?.documentation}
href=${documentationUrl(
this.hass,
`/integrations/${this._step!.handler}`
)}
target="_blank"
rel="noreferrer noopener"
>
<ha-icon-button
><ha-icon-button
.label=${this.hass.localize("ui.common.help")}
.path=${mdiHelpCircle}
?rtl=${computeRTL(this.hass)}
>
</ha-icon-button
></ha-icon-button
></a>
`
: ""}
@@ -438,17 +427,6 @@ class DataEntryFlowDialog extends LitElement {
this._handler = undefined;
}
this._processStep(step);
if (this._params!.manifest === undefined) {
try {
this._params!.manifest = await fetchIntegrationManifest(
this.hass,
this._params?.domain || step.handler
);
} catch (_) {
// No manifest
this._params!.manifest = null;
}
}
} else {
this._step = null;
this._flowsInProgress = flowsInProgress;
@@ -10,7 +10,6 @@ import {
DataEntryFlowStepMenu,
DataEntryFlowStepProgress,
} from "../../data/data_entry_flow";
import { IntegrationManifest } from "../../data/integration";
import { HomeAssistant } from "../../types";
export interface FlowHandlers {
@@ -123,8 +122,6 @@ export interface DataEntryFlowDialogParams {
startFlowHandler?: string;
searchQuery?: string;
continueFlowId?: string;
manifest?: IntegrationManifest | null;
domain?: string;
dialogClosedCallback?: (params: {
flowFinished: boolean;
entryId?: string;
@@ -1,6 +1,6 @@
import { html } from "lit";
import { ConfigEntry } from "../../data/config_entries";
import { domainToName, IntegrationManifest } from "../../data/integration";
import { domainToName } from "../../data/integration";
import {
createOptionsFlow,
deleteOptionsFlow,
@@ -16,15 +16,12 @@ export const loadOptionsFlowDialog = loadDataEntryFlowDialog;
export const showOptionsFlowDialog = (
element: HTMLElement,
configEntry: ConfigEntry,
manifest?: IntegrationManifest | null
configEntry: ConfigEntry
): void =>
showFlowDialog(
element,
{
startFlowHandler: configEntry.entry_id,
domain: configEntry.domain,
manifest,
},
{
loadDevicesAndAreas: false,
@@ -28,7 +28,7 @@ class StepFlowCreateEntry extends LitElement {
const localize = this.hass.localize;
return html`
<h2>${localize("ui.panel.config.integrations.config_flow.success")}!</h2>
<h2>Success!</h2>
<div class="content">
${this.flowConfig.renderCreateEntryDescription(this.hass, this.step)}
${this.step.result?.state === "not_loaded"
@@ -41,11 +41,7 @@ class StepFlowCreateEntry extends LitElement {
${this.devices.length === 0
? ""
: html`
<p>
${localize(
"ui.panel.config.integrations.config_flow.found_following_devices"
)}:
</p>
<p>We found the following devices:</p>
<div class="devices">
${this.devices.map(
(device) =>
@@ -53,12 +49,7 @@ class StepFlowCreateEntry extends LitElement {
<div class="device">
<div>
<b>${computeDeviceName(device, this.hass)}</b><br />
${!device.model && !device.manufacturer
? html`&nbsp;`
: html`${device.model}
${device.manufacturer
? html`(${device.manufacturer})`
: ""}`}
${device.model} (${device.manufacturer})
</div>
<ha-area-picker
.hass=${this.hass}
@@ -190,10 +190,6 @@ class StepFlowForm extends LitElement {
margin-top: 24px;
display: block;
}
h2 {
word-break: break-word;
padding-right: 72px;
}
`,
];
}
@@ -35,7 +35,6 @@ class MoreInfoFan extends LocalizeMixin(EventsMixin(PolymerElement)) {
.has-direction .container-direction,
.has-oscillating .container-oscillating {
display: block;
margin-top: 8px;
}
ha-select {
@@ -14,6 +14,7 @@ import {
UpdateEntity,
updateIsInstalling,
updateReleaseNotes,
UPDATE_SUPPORT_AUTO_UPDATE,
UPDATE_SUPPORT_BACKUP,
UPDATE_SUPPORT_INSTALL,
UPDATE_SUPPORT_PROGRESS,
@@ -56,18 +57,21 @@ class MoreInfoUpdate extends LitElement {
></mwc-linear-progress>`
: html`<mwc-linear-progress indeterminate></mwc-linear-progress>`
: ""}
<h3>${this.stateObj.attributes.title}</h3>
${this.stateObj.attributes.title
? html`<h3>${this.stateObj.attributes.title}</h3>`
: ""}
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: ""}
<div class="row">
<div class="key">
${this.hass.localize(
"ui.dialogs.more_info_control.update.installed_version"
"ui.dialogs.more_info_control.update.current_version"
)}
</div>
<div class="value">
${this.stateObj.attributes.installed_version ??
${this.stateObj.attributes.current_version ??
this.hass.localize("state.default.unavailable")}
</div>
</div>
@@ -127,20 +131,16 @@ class MoreInfoUpdate extends LitElement {
: ""}
<hr />
<div class="actions">
${this.stateObj.attributes.auto_update
${supportsFeature(this.stateObj, UPDATE_SUPPORT_AUTO_UPDATE)
? ""
: html`
<mwc-button
@click=${this._handleSkip}
.disabled=${skippedVersion ||
this.stateObj.state === "off" ||
updateIsInstalling(this.stateObj)}
>
${this.hass.localize(
"ui.dialogs.more_info_control.update.skip"
)}
</mwc-button>
`}
: html`<mwc-button
@click=${this._handleSkip}
.disabled=${skippedVersion ||
this.stateObj.state === "off" ||
updateIsInstalling(this.stateObj)}
>
${this.hass.localize("ui.dialogs.more_info_control.update.skip")}
</mwc-button>`}
${supportsFeature(this.stateObj, UPDATE_SUPPORT_INSTALL)
? html`
<mwc-button
@@ -240,10 +240,6 @@ class MoreInfoUpdate extends LitElement {
width: 100%;
justify-content: center;
}
mwc-linear-progress {
margin-bottom: -10px;
margin-top: -10px;
}
`;
}
}
+500 -284
View File
@@ -2,114 +2,118 @@ import "@lit-labs/virtualizer";
import "@material/mwc-list/mwc-list";
import "@material/mwc-list/mwc-list-item";
import type { ListItem } from "@material/mwc-list/mwc-list-item";
import { mdiClose, mdiMagnify } from "@mdi/js";
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { css, html, LitElement, PropertyValues, TemplateResult } from "lit";
import {
mdiClose,
mdiConsoleLine,
mdiEarth,
mdiMagnify,
mdiReload,
mdiServerNetwork,
} from "@mdi/js";
import { css, html, LitElement, TemplateResult } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import { styleMap } from "lit/directives/style-map";
import memoizeOne from "memoize-one";
import { LocalStorage } from "../../common/decorators/local-storage";
import { canShowPage } from "../../common/config/can_show_page";
import { componentsWithService } from "../../common/config/components_with_service";
import { fireEvent } from "../../common/dom/fire_event";
import { computeDomain } from "../../common/entity/compute_domain";
import { computeStateName } from "../../common/entity/compute_state_name";
import { domainIcon } from "../../common/entity/domain_icon";
import { navigate } from "../../common/navigate";
import { fuzzyFilterSort } from "../../common/string/filter/sequence-matching";
import { caseInsensitiveStringCompare } from "../../common/string/compare";
import {
fuzzyFilterSort,
ScorableTextItem,
} from "../../common/string/filter/sequence-matching";
import { debounce } from "../../common/util/debounce";
import "../../components/ha-chip";
import "../../components/ha-circular-progress";
import "../../components/ha-header-bar";
import "../../components/ha-icon-button";
import "../../components/ha-textfield";
import { domainToName } from "../../data/integration";
import { getPanelNameTranslationKey } from "../../data/panel";
import { PageNavigation } from "../../layouts/hass-tabs-subpage";
import { configSections } from "../../panels/config/ha-panel-config";
import { haStyleDialog, haStyleScrollbar } from "../../resources/styles";
import { HomeAssistant } from "../../types";
import {
AreaRegistryEntry,
subscribeAreaRegistry,
} from "../../data/area_registry";
import {
DeviceRegistryEntry,
subscribeDeviceRegistry,
} from "../../data/device_registry";
import {
EntityRegistryEntry,
subscribeEntityRegistry,
} from "../../data/entity_registry";
import {
generateCommandItems,
generateEntityItems,
QuickBarItem,
} from "../../data/quick-bar";
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
import {
haStyle,
haStyleDialog,
haStyleScrollbar,
} from "../../resources/styles";
import type { HomeAssistant } from "../../types";
import { showConfirmationDialog } from "../generic/show-dialog-box";
ConfirmationDialogParams,
showConfirmationDialog,
} from "../generic/show-dialog-box";
import { QuickBarParams } from "./show-dialog-quick-bar";
interface QuickBarItem extends ScorableTextItem {
primaryText: string;
iconPath?: string;
action(data?: any): void;
}
interface CommandItem extends QuickBarItem {
categoryKey: "reload" | "navigation" | "server_control";
categoryText: string;
}
interface EntityItem extends QuickBarItem {
altText: string;
icon?: string;
}
const isCommandItem = (item: QuickBarItem): item is CommandItem =>
(item as CommandItem).categoryKey !== undefined;
interface QuickBarNavigationItem extends CommandItem {
path: string;
}
type NavigationInfo = PageNavigation & Pick<QuickBarItem, "primaryText">;
type BaseNavigationCommand = Pick<
QuickBarNavigationItem,
"primaryText" | "path"
>;
@customElement("ha-quick-bar")
export class QuickBar extends SubscribeMixin(LitElement) {
export class QuickBar extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _items?: Array<QuickBarItem[]>;
@state() private _commandItems?: CommandItem[];
private _filteredItems?: QuickBarItem[];
@state() private _entityItems?: EntityItem[];
@state() private _filter = "";
@state() private _search = "";
@state() private _opened = false;
@state() private _open = false;
@state() private _done = false;
@state() private _commandMode = false;
@state() private _opened = false;
@state() private _narrow = false;
@state() private _hint?: string;
@state() private _entities?: EntityRegistryEntry[];
@state() private _areas?: AreaRegistryEntry[];
@state() private _devices?: DeviceRegistryEntry[];
@query("ha-textfield", false) private _filterInputField?: HTMLElement;
// @ts-ignore
@LocalStorage("suggestions", true, {
attribute: false,
})
private _suggestions: QuickBarItem[] = [];
private _focusSet = false;
private _focusListElement?: ListItem | null;
private _filterItems = memoizeOne(
(items: QuickBarItem[], filter: string): QuickBarItem[] =>
fuzzyFilterSort<QuickBarItem>(filter.trimLeft(), items)
);
public async showDialog(params: QuickBarParams) {
this._commandMode = params.commandMode || this._toggleIfAlreadyOpened();
this._hint = params.hint;
this._narrow = matchMedia(
"all and (max-width: 450px), all and (max-height: 500px)"
).matches;
}
public hassSubscribe(): UnsubscribeFunc[] {
return [
subscribeAreaRegistry(this.hass.connection!, (areas) => {
this._areas = areas;
}),
subscribeDeviceRegistry(this.hass.connection!, (devices) => {
this._devices = devices;
}),
subscribeEntityRegistry(this.hass.connection!, (entities) => {
this._entities = entities;
}),
];
this._initializeItemsIfNeeded();
this._open = true;
}
public closeDialog() {
this._open = false;
this._opened = false;
this._focusSet = false;
this._filter = "";
@@ -117,46 +121,28 @@ export class QuickBar extends SubscribeMixin(LitElement) {
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
protected willUpdate(changedProperties: PropertyValues): void {
super.willUpdate(changedProperties);
if (
(changedProperties.has("_entity") ||
changedProperties.has("_areas") ||
changedProperties.has("_devices")) &&
this._areas &&
this._devices &&
this._entities
) {
this._initializeItems();
this._opened = true;
}
}
private _getItems = memoizeOne(
(commandMode: boolean, commandItems, entityItems, filter: string) => {
const items = commandMode ? commandItems : entityItems;
protected render(): TemplateResult {
if (!this._opened) {
if (items && filter && filter !== " ") {
return this._filterItems(items, filter);
}
return items;
}
);
protected render() {
if (!this._open) {
return html``;
}
let sectionCount = 0;
if (this._items && this._filter && this._filter !== "") {
const newFilteredItems: QuickBarItem[] = [];
this._items.forEach((arr) => {
const items = this._filterItems(arr, this._filter).slice(0, 3);
if (items.length === 0) {
return;
}
sectionCount++;
newFilteredItems.push(...items);
});
this._filteredItems = newFilteredItems;
} else {
sectionCount++;
this._filteredItems = this._suggestions;
}
const items: QuickBarItem[] | undefined = this._getItems(
this._commandMode,
this._commandItems,
this._entityItems,
this._filter
);
return html`
<ha-dialog
@@ -175,18 +161,28 @@ export class QuickBar extends SubscribeMixin(LitElement) {
aria-label=${this.hass.localize(
"ui.dialogs.quick-bar.filter_placeholder"
)}
.value=${this._search}
.icon=${true}
.value=${this._commandMode ? `>${this._search}` : this._search}
icon
.iconTrailing=${this._search !== undefined || this._narrow}
@input=${this._handleSearchChange}
@keydown=${this._handleInputKeyDown}
@focus=${this._setFocusFirstListItem}
>
<ha-svg-icon
slot="leadingIcon"
class="prefix"
.path=${mdiMagnify}
></ha-svg-icon>
${this._commandMode
? html`
<ha-svg-icon
slot="leadingIcon"
class="prefix"
.path=${mdiConsoleLine}
></ha-svg-icon>
`
: html`
<ha-svg-icon
slot="leadingIcon"
class="prefix"
.path=${mdiMagnify}
></ha-svg-icon>
`}
${this._search || this._narrow
? html`
<div slot="trailingIcon">
@@ -209,11 +205,12 @@ export class QuickBar extends SubscribeMixin(LitElement) {
: ""}
</ha-textfield>
</div>
${!this._filteredItems
? html`
<ha-circular-progress size="small" active></ha-circular-progress>
`
: this._filteredItems.length === 0 && this._filter !== ""
${!items
? html`<ha-circular-progress
size="small"
active
></ha-circular-progress>`
: items.length === 0
? html`
<div class="nothing-found">
${this.hass.localize("ui.dialogs.quick-bar.nothing_found")}
@@ -221,26 +218,26 @@ export class QuickBar extends SubscribeMixin(LitElement) {
`
: html`
<mwc-list>
<lit-virtualizer
scroller
@keydown=${this._handleListItemKeyDown}
@rangechange=${this._handleRangeChanged}
@click=${this._handleItemClick}
class="ha-scrollbar"
style=${styleMap({
height: this._narrow
? "calc(100vh - 56px)"
: `${Math.min(
this._filteredItems.length * 72 +
sectionCount * 37 +
18,
this._done ? 600 : 0
)}px`,
})}
.items=${this._filteredItems}
.renderItem=${this._renderItem}
>
</lit-virtualizer>
${this._opened
? html`<lit-virtualizer
scroller
@keydown=${this._handleListItemKeyDown}
@rangechange=${this._handleRangeChanged}
@click=${this._handleItemClick}
class="ha-scrollbar"
style=${styleMap({
height: this._narrow
? "calc(100vh - 56px)"
: `${Math.min(
items.length * (this._commandMode ? 56 : 72) + 26,
500
)}px`,
})}
.items=${items}
.renderItem=${this._renderItem}
>
</lit-virtualizer>`
: ""}
</mwc-list>
`}
${this._hint ? html`<div class="hint">${this._hint}</div>` : ""}
@@ -248,111 +245,102 @@ export class QuickBar extends SubscribeMixin(LitElement) {
`;
}
private _initializeItemsIfNeeded() {
if (this._commandMode) {
this._commandItems = this._commandItems || this._generateCommandItems();
} else {
this._entityItems = this._entityItems || this._generateEntityItems();
}
}
private _handleOpened() {
this._opened = true;
}
private async _handleRangeChanged(e) {
if (this._focusSet) {
return;
}
if (e.firstVisible > -1) {
this._focusSet = true;
await this.updateComplete;
this._setFocusFirstListItem();
}
}
private _renderItem = (item: QuickBarItem, index: number): TemplateResult => {
if (!item) {
return html``;
}
const previous = this._filteredItems![index - 1];
return html`
<div class="entry-container" style="z-index: 5">
${index === 0 || item?.categoryKey !== previous?.categoryKey
? html`
<div class="entry-title">
${item.isSuggestion
? this.hass.localize(
"ui.dialogs.quick-bar.commands.types.suggestions"
)
: this.hass.localize(
`ui.dialogs.quick-bar.commands.types.${item.categoryKey}`
)}
</div>
`
: ""}
<mwc-list-item
.twoline=${Boolean(item.secondaryText)}
.hasMeta=${Boolean(item.metaText)}
.item=${item}
index=${ifDefined(index)}
graphic="icon"
class=${item.secondaryText ? "single-line" : ""}
>
${item.iconPath
? html`<ha-svg-icon
.path=${item.iconPath}
slot="graphic"
></ha-svg-icon>`
: html`<ha-icon .icon=${item.icon} slot="graphic"></ha-icon>`}
<span
>${item.primaryText}
<span class="secondary">${item.primaryTextAlt}</span></span
>
${item.secondaryText
? html`
<span slot="secondary" class="item-text secondary"
>${item.secondaryText}</span
>
`
: ""}
${item.metaText
? html`<ha-chip slot="meta">${item.metaText}</ha-chip>`
: ""}
</mwc-list-item>
</div>
`;
return isCommandItem(item)
? this._renderCommandItem(item, index)
: this._renderEntityItem(item as EntityItem, index);
};
private _initializeItems() {
const deviceLookup: { [deviceId: string]: DeviceRegistryEntry } = {};
for (const device of this._devices!) {
deviceLookup[device.id] = device;
}
private _renderEntityItem(item: EntityItem, index?: number) {
return html`
<mwc-list-item
.twoline=${Boolean(item.altText)}
.item=${item}
index=${ifDefined(index)}
graphic="icon"
>
${item.iconPath
? html`<ha-svg-icon
.path=${item.iconPath}
class="entity"
slot="graphic"
></ha-svg-icon>`
: html`<ha-icon
.icon=${item.icon}
class="entity"
slot="graphic"
></ha-icon>`}
<span>${item.primaryText}</span>
${item.altText
? html`
<span slot="secondary" class="item-text secondary"
>${item.altText}</span
>
`
: null}
</mwc-list-item>
`;
}
const entityLookup: { [entityId: string]: EntityRegistryEntry } = {};
for (const entity of this._entities!) {
entityLookup[entity.entity_id] = entity;
}
private _renderCommandItem(item: CommandItem, index?: number) {
return html`
<mwc-list-item
.item=${item}
index=${ifDefined(index)}
class="command-item"
hasMeta
>
<span>
<ha-chip
.label=${item.categoryText}
hasIcon
class="command-category ${item.categoryKey}"
>
${item.iconPath
? html`<ha-svg-icon
.path=${item.iconPath}
slot="icon"
></ha-svg-icon>`
: ""}
${item.categoryText}</ha-chip
>
</span>
const areaLookup: { [areaId: string]: AreaRegistryEntry } = {};
for (const area of this._areas!) {
areaLookup[area.area_id] = area;
}
this._items = this._items || [
generateEntityItems(this.hass, entityLookup, deviceLookup, areaLookup),
...generateCommandItems(this.hass),
];
<span class="command-text">${item.primaryText}</span>
</mwc-list-item>
`;
}
private async processItemAndCloseDialog(item: QuickBarItem, index: number) {
if (!this._suggestions.includes(item)) {
this._suggestions.unshift({ ...item, isSuggestion: true });
this._suggestions = this._suggestions.slice(0, 3);
}
this._addSpinnerToCommandItem(index);
this._addSpinnerToItem(index);
switch (item.categoryKey) {
case "entity":
fireEvent(this, "hass-more-info", {
entityId: item.actionData as string,
});
break;
case "reload":
this.hass.callService(item.actionData[0], item.actionData[1]);
break;
case "navigation":
navigate(item.actionData as string);
break;
case "server_control":
showConfirmationDialog(this, {
confirmText: this.hass.localize("ui.dialogs.generic.ok"),
confirm: () =>
this.hass.callService("homeassistant", item.actionData as string),
});
break;
}
await item.action();
this.closeDialog();
}
@@ -371,28 +359,56 @@ export class QuickBar extends SubscribeMixin(LitElement) {
}
}
private _getItemAtIndex(index: number): ListItem | null {
return this.renderRoot.querySelector(`mwc-list-item[index="${index}"]`);
}
private _addSpinnerToCommandItem(index: number): void {
const spinner = document.createElement("ha-circular-progress");
spinner.size = "small";
spinner.slot = "meta";
spinner.active = true;
this._getItemAtIndex(index)?.appendChild(spinner);
}
private _handleSearchChange(ev: CustomEvent): void {
const newFilter = (ev.currentTarget as any).value;
const oldCommandMode = this._commandMode;
const oldSearch = this._search;
let newCommandMode: boolean;
let newSearch: string;
if (newFilter.startsWith(">")) {
newCommandMode = true;
newSearch = newFilter.substring(1);
} else {
newCommandMode = false;
newSearch = newFilter;
}
if (oldCommandMode === newCommandMode && oldSearch === newSearch) {
return;
}
this._commandMode = newCommandMode;
this._search = newSearch;
if (this._hint) {
this._hint = undefined;
}
if (this._focusSet && this._focusListElement) {
if (oldCommandMode !== this._commandMode) {
this._focusSet = false;
// @ts-ignore
this._focusListElement.rippleHandlers.endFocus();
this._initializeItemsIfNeeded();
this._filter = this._search;
} else {
if (this._focusSet && this._focusListElement) {
this._focusSet = false;
// @ts-ignore
this._focusListElement.rippleHandlers.endFocus();
}
this._debouncedSetFilter(this._search);
}
this._debouncedSetFilter(this._search);
}
private _clearSearch() {
@@ -438,62 +454,258 @@ export class QuickBar extends SubscribeMixin(LitElement) {
}
private _handleItemClick(ev) {
const target =
ev.target.nodeName === "MWC-LIST-ITEM"
? ev.target
: ev.target.parentElement === "MWC-LIST-ITEM"
? ev.target.parentElement
: ev.target.parentElement.parentElement;
// Need to resolve when clicking on the chip
if (!target.item) {
return;
}
const listItem = ev.target.closest("mwc-list-item");
this.processItemAndCloseDialog(
target.item,
Number((target as HTMLElement).getAttribute("index"))
listItem.item,
Number(listItem.getAttribute("index"))
);
}
private _handleOpened() {
this.updateComplete.then(() => {
this._done = true;
private _generateEntityItems(): EntityItem[] {
return Object.keys(this.hass.states)
.map((entityId) => {
const entityState = this.hass.states[entityId];
const entityItem = {
primaryText: computeStateName(entityState),
altText: entityId,
icon: entityState.attributes.icon,
iconPath: entityState.attributes.icon
? undefined
: domainIcon(computeDomain(entityId), entityState),
action: () => fireEvent(this, "hass-more-info", { entityId }),
};
return {
...entityItem,
strings: [entityItem.primaryText, entityItem.altText],
};
})
.sort((a, b) =>
caseInsensitiveStringCompare(a.primaryText, b.primaryText)
);
}
private _generateCommandItems(): CommandItem[] {
return [
...this._generateReloadCommands(),
...this._generateServerControlCommands(),
...this._generateNavigationCommands(),
].sort((a, b) =>
caseInsensitiveStringCompare(a.strings.join(" "), b.strings.join(" "))
);
}
private _generateReloadCommands(): CommandItem[] {
// Get all domains that have a direct "reload" service
const reloadableDomains = componentsWithService(this.hass, "reload");
const commands = reloadableDomains.map((domain) => ({
primaryText:
this.hass.localize(`ui.dialogs.quick-bar.commands.reload.${domain}`) ||
this.hass.localize(
"ui.dialogs.quick-bar.commands.reload.reload",
"domain",
domainToName(this.hass.localize, domain)
),
action: () => this.hass.callService(domain, "reload"),
iconPath: mdiReload,
categoryText: this.hass.localize(
`ui.dialogs.quick-bar.commands.types.reload`
),
}));
// Add "frontend.reload_themes"
commands.push({
primaryText: this.hass.localize(
"ui.dialogs.quick-bar.commands.reload.themes"
),
action: () => this.hass.callService("frontend", "reload_themes"),
iconPath: mdiReload,
categoryText: this.hass.localize(
"ui.dialogs.quick-bar.commands.types.reload"
),
});
// Add "homeassistant.reload_core_config"
commands.push({
primaryText: this.hass.localize(
"ui.dialogs.quick-bar.commands.reload.core"
),
action: () =>
this.hass.callService("homeassistant", "reload_core_config"),
iconPath: mdiReload,
categoryText: this.hass.localize(
"ui.dialogs.quick-bar.commands.types.reload"
),
});
return commands.map((command) => ({
...command,
categoryKey: "reload",
strings: [`${command.categoryText} ${command.primaryText}`],
}));
}
private _generateServerControlCommands(): CommandItem[] {
const serverActions = ["restart", "stop"];
return serverActions.map((action) => {
const categoryKey: CommandItem["categoryKey"] = "server_control";
const item = {
primaryText: this.hass.localize(
"ui.dialogs.quick-bar.commands.server_control.perform_action",
"action",
this.hass.localize(
`ui.dialogs.quick-bar.commands.server_control.${action}`
)
),
iconPath: mdiServerNetwork,
categoryText: this.hass.localize(
`ui.dialogs.quick-bar.commands.types.${categoryKey}`
),
categoryKey,
action: () => this.hass.callService("homeassistant", action),
};
return this._generateConfirmationCommand(
{
...item,
strings: [`${item.categoryText} ${item.primaryText}`],
},
this.hass.localize("ui.dialogs.generic.ok")
);
});
}
private async _handleRangeChanged(e) {
if (this._focusSet) {
return;
}
if (e.firstVisible > -1) {
this._focusSet = true;
await this.updateComplete;
this._setFocusFirstListItem();
}
private _generateNavigationCommands(): CommandItem[] {
const panelItems = this._generateNavigationPanelCommands();
const sectionItems = this._generateNavigationConfigSectionCommands();
return this._finalizeNavigationCommands([...panelItems, ...sectionItems]);
}
private _getItemAtIndex(index: number): ListItem | null {
return this.renderRoot.querySelector(`mwc-list-item[index="${index}"]`);
private _generateNavigationPanelCommands(): BaseNavigationCommand[] {
return Object.keys(this.hass.panels)
.filter((panelKey) => panelKey !== "_my_redirect")
.map((panelKey) => {
const panel = this.hass.panels[panelKey];
const translationKey = getPanelNameTranslationKey(panel);
const primaryText =
this.hass.localize(translationKey) || panel.title || panel.url_path;
return {
primaryText,
path: `/${panel.url_path}`,
};
});
}
private _addSpinnerToItem(index: number): void {
const spinner = document.createElement("ha-circular-progress");
spinner.size = "small";
spinner.slot = "meta";
spinner.active = true;
this._getItemAtIndex(index)?.appendChild(spinner);
private _generateNavigationConfigSectionCommands(): BaseNavigationCommand[] {
const items: NavigationInfo[] = [];
for (const sectionKey of Object.keys(configSections)) {
for (const page of configSections[sectionKey]) {
if (!canShowPage(this.hass, page)) {
continue;
}
if (!page.component) {
continue;
}
const info = this._getNavigationInfoFromConfig(page);
if (!info) {
continue;
}
// Add to list, but only if we do not already have an entry for the same path and component
if (
items.some(
(e) => e.path === info.path && e.component === info.component
)
) {
continue;
}
items.push(info);
}
}
return items;
}
private _getNavigationInfoFromConfig(
page: PageNavigation
): NavigationInfo | undefined {
if (!page.component) {
return undefined;
}
const caption = this.hass.localize(
`ui.dialogs.quick-bar.commands.navigation.${page.component}`
);
if (page.translationKey && caption) {
return { ...page, primaryText: caption };
}
return undefined;
}
private _generateConfirmationCommand(
item: CommandItem,
confirmText: ConfirmationDialogParams["confirmText"]
): CommandItem {
return {
...item,
action: () =>
showConfirmationDialog(this, {
confirmText,
confirm: item.action,
}),
};
}
private _finalizeNavigationCommands(
items: BaseNavigationCommand[]
): CommandItem[] {
return items.map((item) => {
const categoryKey: CommandItem["categoryKey"] = "navigation";
const navItem = {
...item,
iconPath: mdiEarth,
categoryText: this.hass.localize(
`ui.dialogs.quick-bar.commands.types.${categoryKey}`
),
action: () => navigate(item.path),
};
return {
...navItem,
strings: [`${navItem.categoryText} ${navItem.primaryText}`],
categoryKey,
};
});
}
private _toggleIfAlreadyOpened() {
return this._opened ? !this._commandMode : false;
}
private _filterItems = memoizeOne(
(items: QuickBarItem[], filter: string): QuickBarItem[] =>
fuzzyFilterSort<QuickBarItem>(filter.trimLeft(), items)
);
static get styles() {
return [
haStyleScrollbar,
haStyleDialog,
haStyle,
css`
.heading {
display: flex;
align-items: center;
--mdc-theme-primary: var(--primary-text-color);
}
.heading ha-textfield {
@@ -527,8 +739,8 @@ export class QuickBar extends SubscribeMixin(LitElement) {
}
}
mwc-list-item ha-icon,
mwc-list-item ha-svg-icon {
ha-icon.entity,
ha-svg-icon.entity {
margin-left: 20px;
}
@@ -536,30 +748,38 @@ export class QuickBar extends SubscribeMixin(LitElement) {
color: var(--primary-text-color);
}
ha-textfield {
--mdc-text-field-fill-color: transparent;
--mdc-theme-primary: var(--divider-color);
--mdc-text-field-idle-line-color: var(--divider-color);
}
ha-textfield ha-icon-button {
--mdc-icon-button-size: 24px;
color: var(--primary-text-color);
}
.entry-container {
width: 100%;
.command-category {
--ha-chip-icon-color: #585858;
--ha-chip-text-color: #212121;
}
.entry-title {
padding-left: 16px;
padding-top: 16px;
color: var(--secondary-text-color);
.command-category.reload {
--ha-chip-background-color: #cddc39;
}
.command-category.navigation {
--ha-chip-background-color: var(--light-primary-color);
}
.command-category.server_control {
--ha-chip-background-color: var(--warning-color);
}
span.command-text {
margin-left: 8px;
}
mwc-list-item {
width: 100%;
box-sizing: border-box;
}
mwc-list-item.command-item {
text-transform: capitalize;
}
.hint {
@@ -581,10 +801,6 @@ export class QuickBar extends SubscribeMixin(LitElement) {
lit-virtualizer {
contain: size layout !important;
}
ha-chip {
margin-left: -48px;
}
`,
];
}
+6 -9
View File
@@ -34,10 +34,7 @@ import { useAmPm } from "../../common/datetime/use_am_pm";
import { fireEvent } from "../../common/dom/fire_event";
import "../../components/ha-button-toggle-group";
import "../../components/ha-icon-button";
import "../../components/ha-icon-button-prev";
import "../../components/ha-icon-button-next";
import { haStyle } from "../../resources/styles";
import { computeRTLDirection } from "../../common/util/compute_rtl";
import type {
CalendarEvent,
CalendarViewChanged,
@@ -127,25 +124,26 @@ export class HAFullCalendar extends LitElement {
"ui.components.calendar.today"
)}</mwc-button
>
<ha-icon-button-prev
<ha-icon-button
.label=${this.hass.localize("ui.common.previous")}
.path=${mdiChevronLeft}
class="prev"
@click=${this._handlePrev}
>
</ha-icon-button-prev>
<ha-icon-button-next
</ha-icon-button>
<ha-icon-button
.label=${this.hass.localize("ui.common.next")}
.path=${mdiChevronRight}
class="next"
@click=${this._handleNext}
>
</ha-icon-button-next>
</ha-icon-button>
</div>
<h1>${this.calendar.view.title}</h1>
<ha-button-toggle-group
.buttons=${viewToggleButtons}
.active=${this._activeView}
@value-changed=${this._handleView}
.dir=${computeRTLDirection(this.hass)}
></ha-button-toggle-group>
`
: html`
@@ -181,7 +179,6 @@ export class HAFullCalendar extends LitElement {
.buttons=${viewToggleButtons}
.active=${this._activeView}
@value-changed=${this._handleView}
.dir=${computeRTLDirection(this.hass)}
></ha-button-toggle-group>
</div>
`}
@@ -165,9 +165,6 @@ export class HaChooseAction extends LitElement implements ActionElement {
right: 0;
padding: 4px;
}
ha-form::part(root) {
overflow: visible;
}
`,
];
}
@@ -8,7 +8,6 @@ import "../../../components/ha-card";
import "../../../components/ha-textarea";
import "../../../components/ha-textfield";
import {
AUTOMATION_DEFAULT_MODE,
Condition,
ManualAutomationConfig,
Trigger,
@@ -100,7 +99,7 @@ export class HaManualAutomationEditor extends LitElement {
.label=${this.hass.localize(
"ui.panel.config.automation.editor.modes.label"
)}
.value=${this.config.mode || AUTOMATION_DEFAULT_MODE}
.value=${this.config.mode}
@selected=${this._modeChanged}
fixedMenuPosition
>
@@ -442,9 +442,6 @@ export default class HaAutomationTriggerRow extends LitElement {
z-index: 3;
--mdc-theme-text-primary-on-background: var(--primary-text-color);
}
.rtl .card-menu {
float: left;
}
.triggered {
cursor: pointer;
position: absolute;
@@ -473,6 +470,9 @@ export default class HaAutomationTriggerRow extends LitElement {
background-color: var(--accent-color);
color: var(--text-accent-color, var(--text-primary-color));
}
.rtl .card-menu {
float: left;
}
mwc-list-item[disabled] {
--mdc-theme-text-primary-on-background: var(--disabled-text-color);
}
@@ -1,7 +1,6 @@
import { html, LitElement, PropertyValues } from "lit";
import { customElement, property } from "lit/decorators";
import {
array,
assert,
assign,
literal,
@@ -11,7 +10,6 @@ import {
union,
} from "superstruct";
import memoizeOne from "memoize-one";
import { ensureArray } from "../../../../../common/ensure-array";
import { fireEvent } from "../../../../../common/dom/fire_event";
import { hasTemplate } from "../../../../../common/string/has-template";
import { StateTrigger } from "../../../../../data/automation";
@@ -26,7 +24,7 @@ const stateTriggerStruct = assign(
baseTriggerStruct,
object({
platform: literal("state"),
entity_id: optional(union([string(), array(string())])),
entity_id: optional(string()),
attribute: optional(string()),
from: optional(string()),
to: optional(string()),
@@ -41,15 +39,11 @@ export class HaStateTrigger extends LitElement implements TriggerElement {
@property() public trigger!: StateTrigger;
public static get defaultConfig() {
return { entity_id: [] };
return { entity_id: "" };
}
private _schema = memoizeOne((entityId) => [
{
name: "entity_id",
required: true,
selector: { entity: { multiple: true } },
},
{ name: "entity_id", required: true, selector: { entity: {} } },
{
name: "attribute",
selector: { attribute: { entity_id: entityId } },
@@ -91,11 +85,7 @@ export class HaStateTrigger extends LitElement implements TriggerElement {
protected render() {
const trgFor = createDurationData(this.trigger.for);
const data = {
...this.trigger,
entity_id: ensureArray(this.trigger.entity_id),
for: trgFor,
};
const data = { ...this.trigger, ...{ for: trgFor } };
const schema = this._schema(this.trigger.entity_id);
return html`
@@ -11,13 +11,10 @@ import {
TemplateResult,
} from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../common/dom/fire_event";
import { stopPropagation } from "../../../common/dom/stop_propagation";
import { computeDomain } from "../../../common/entity/compute_domain";
import { domainIcon } from "../../../common/entity/domain_icon";
import { stringCompare } from "../../../common/string/compare";
import { LocalizeFunc } from "../../../common/translations/localize";
import "../../../components/ha-alert";
import "../../../components/ha-area-picker";
import "../../../components/ha-expansion-panel";
@@ -99,7 +96,7 @@ const OVERRIDE_SENSOR_UNITS = {
pressure: ["hPa", "Pa", "kPa", "bar", "cbar", "mbar", "mmHg", "inHg", "psi"],
};
const SWITCH_AS_DOMAINS = ["cover", "fan", "light", "lock", "siren"];
const SWITCH_AS_DOMAINS = ["light", "lock", "cover", "fan", "siren"];
@customElement("entity-registry-settings")
export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
@@ -276,29 +273,22 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
@selected=${this._deviceClassChanged}
@closed=${stopPropagation}
>
${this._deviceClassesSorted(
domain,
this._deviceClassOptions[0],
this.hass.localize
).map(
(entry) => html`
<mwc-list-item .value=${entry.deviceClass}>
${entry.label}
${this._deviceClassOptions[0].map(
(deviceClass: string) => html`
<mwc-list-item .value=${deviceClass}>
${this.hass.localize(
`ui.dialogs.entity_registry.editor.device_classes.${domain}.${deviceClass}`
)}
</mwc-list-item>
`
)}
${this._deviceClassOptions[0].length &&
this._deviceClassOptions[1].length
? html`<li divider role="separator"></li>`
: ""}
${this._deviceClassesSorted(
domain,
this._deviceClassOptions[1],
this.hass.localize
).map(
(entry) => html`
<mwc-list-item .value=${entry.deviceClass}>
${entry.label}
<li divider role="separator"></li>
${this._deviceClassOptions[1].map(
(deviceClass: string) => html`
<mwc-list-item .value=${deviceClass}>
${this.hass.localize(
`ui.dialogs.entity_registry.editor.device_classes.${domain}.${deviceClass}`
)}
</mwc-list-item>
`
)}
@@ -306,9 +296,9 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
`
: ""}
${this._deviceClass &&
stateObj?.attributes.unit_of_measurement &&
stateObj.attributes.unit_of_measurement &&
OVERRIDE_SENSOR_UNITS[this._deviceClass]?.includes(
stateObj?.attributes.unit_of_measurement
stateObj.attributes.unit_of_measurement
)
? html`
<ha-select
@@ -342,14 +332,10 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
<mwc-list-item value="switch" selected>
${domainToName(this.hass.localize, "switch")}</mwc-list-item
>
<li divider role="separator"></li>
${this._switchAsDomainsSorted(
SWITCH_AS_DOMAINS,
this.hass.localize
).map(
(entry) => html`
<mwc-list-item .value=${entry.domain}>
${entry.label}
${SWITCH_AS_DOMAINS.map(
(as_domain) => html`
<mwc-list-item .value=${as_domain}>
${domainToName(this.hass.localize, as_domain)}
</mwc-list-item>
`
)}
@@ -730,31 +716,9 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
}
private async _showOptionsFlow() {
showOptionsFlowDialog(this, this._helperConfigEntry!, null);
showOptionsFlowDialog(this, this._helperConfigEntry!);
}
private _switchAsDomainsSorted = memoizeOne(
(domains: string[], localize: LocalizeFunc) =>
domains
.map((entry) => ({
domain: entry,
label: domainToName(localize, entry),
}))
.sort((a, b) => stringCompare(a.label, b.label))
);
private _deviceClassesSorted = memoizeOne(
(domain: string, deviceClasses: string[], localize: LocalizeFunc) =>
deviceClasses
.map((entry) => ({
deviceClass: entry,
label: localize(
`ui.dialogs.entity_registry.editor.device_classes.${domain}.${entry}`
),
}))
.sort((a, b) => stringCompare(a.label, b.label))
);
static get styles(): CSSResultGroup {
return [
haStyle,
@@ -700,7 +700,6 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
this._handleFlowUpdated();
},
startFlowHandler: domain,
manifest: this._manifests[domain],
showAdvanced: this.hass.userData?.showAdvanced,
});
}
@@ -482,11 +482,7 @@ export class HaIntegrationCard extends LitElement {
);
private _showOptions(ev) {
showOptionsFlowDialog(
this,
ev.target.closest("ha-card").configEntry,
this.manifest
);
showOptionsFlowDialog(this, ev.target.closest("ha-card").configEntry);
}
private _handleRename(ev: CustomEvent<RequestSelectedDetail>): void {
@@ -179,7 +179,7 @@ class ZWaveJSNodeConfig extends SubscribeMixin(LitElement) {
</em>
</p>
</div>
${this._nodeMetadata.comments?.length > 0
${this._nodeMetadata.comments.length > 0
? html`
<div>
${this._nodeMetadata.comments.map(
@@ -1,7 +1,6 @@
import "@material/mwc-button/mwc-button";
import "@material/mwc-list/mwc-list-item";
import { mdiChevronRight } from "@mdi/js";
import formatISO9075 from "date-fns/formatISO9075";
import {
css,
CSSResultGroup,
@@ -70,11 +69,12 @@ export class DialogStatisticsFixUnsupportedUnitMetadata extends LitElement {
public showDialog(params: DialogStatisticsAdjustSumParams): void {
this._params = params;
// moment is in format YYYY-MM-DD HH:mm:ss because of selector
// Here we create a date with minutes set to %5
const now = new Date();
now.setMinutes(now.getMinutes() - (now.getMinutes() % 5), 0);
this._moment = formatISO9075(now);
this._moment = `${now.getFullYear()}-${
now.getMonth() + 1
}-${now.getDate()} ${now.getHours()}:${
now.getMinutes() - (now.getMinutes() % 5)
}:00`;
this._fetchStats();
}
@@ -167,9 +167,6 @@ export class DialogStatisticsFixUnsupportedUnitMetadata extends LitElement {
time. This can mess up your beautiful graphs! Select a time below to
find the bad moment and adjust the data.
</div>
<div class="text-content">
<b>Statistic:</b> ${this._params!.statistic.statistic_id}
</div>
<ha-selector-datetime
label="Pick a time"
.hass=${this.hass}
@@ -194,7 +191,7 @@ export class DialogStatisticsFixUnsupportedUnitMetadata extends LitElement {
private _renderAdjustStat() {
return html`
<div class="text-content">
<b>Statistic:</b> ${this._params!.statistic.statistic_id}
${this._params!.statistic.name || this._params!.statistic.statistic_id}
</div>
<div class="table-row">
@@ -253,10 +250,7 @@ export class DialogStatisticsFixUnsupportedUnitMetadata extends LitElement {
this._stats5min = undefined;
this._statsHour = undefined;
const statId = this._params!.statistic.statistic_id;
// moment is in format YYYY-MM-DD HH:mm:ss because of selector
// Here we convert it to an ISO string.
const moment = new Date(this._moment!.replace(" ", "T"));
const moment = new Date(this._moment!);
// Search 3 hours before and 3 hours after chosen time
const hourStatStart = new Date(moment.getTime());
-4
View File
@@ -253,10 +253,6 @@ class HaPanelHistory extends LitElement {
padding: 8px 16px 0;
}
:host([narrow]) .filters {
flex-wrap: wrap;
}
ha-date-range-picker {
margin-right: 16px;
max-width: 100%;
@@ -113,7 +113,6 @@ export class HuiAreaCard
.filter(
(entry) =>
!entry.entity_category &&
!entry.hidden_by &&
(entry.area_id
? entry.area_id === areaId
: entry.device_id && devicesInArea.has(entry.device_id))
@@ -200,7 +200,6 @@ class HuiPictureEntityCard extends LitElement implements LovelaceCard {
font-size: 16px;
line-height: 16px;
color: var(--ha-picture-card-text-color, white);
pointer-events: none;
}
.both {
@@ -21,9 +21,6 @@ export const turnOnOffEntity = (
case "input_button":
service = "press";
break;
case "scene":
service = "turn_on";
break;
default:
service = turnOn ? "turn_on" : "turn_off";
}
@@ -15,7 +15,6 @@ import type { LovelaceRowEditor } from "../../types";
import { entitiesConfigStruct } from "../structs/entities-struct";
const SecondaryInfoValues: { [key: string]: { domains?: string[] } } = {
none: {},
"entity-id": {},
"last-changed": {},
"last-updated": {},
@@ -13,7 +13,6 @@ import { computeStateName } from "../../../common/entity/compute_state_name";
import "../../../components/ha-select";
import { UNAVAILABLE } from "../../../data/entity";
import { forwardHaptic } from "../../../data/haptics";
import type { InputSelectEntity } from "../../../data/input_select";
import { SelectEntity, setSelectOption } from "../../../data/select";
import { HomeAssistant } from "../../../types";
import { EntitiesCardEntityConfig } from "../cards/types";
@@ -107,14 +106,9 @@ class HuiSelectEntityRow extends LitElement implements LovelaceRow {
}
private _selectedChanged(ev): void {
const stateObj = this.hass!.states[
this._config!.entity
] as InputSelectEntity;
const stateObj = this.hass!.states[this._config!.entity];
const option = ev.target.value;
if (
option === stateObj.state ||
!stateObj.attributes.options.includes(option)
) {
if (option === stateObj.state) {
return;
}
+1 -14
View File
@@ -7,7 +7,6 @@ import {
mdiFileMultiple,
mdiFormatListBulletedTriangle,
mdiHelp,
mdiMagnify,
mdiHelpCircle,
mdiMicrophone,
mdiPencil,
@@ -73,7 +72,6 @@ import { showEditViewDialog } from "./editor/view-editor/show-edit-view-dialog";
import type { Lovelace } from "./types";
import "./views/hui-view";
import type { HUIView } from "./views/hui-view";
import { showQuickBar } from "../../dialogs/quick-bar/show-dialog-quick-bar";
class HUIRoot extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -266,10 +264,6 @@ class HUIRoot extends LitElement {
</ha-tabs>
`
: html`<div main-title>${this.config.title}</div>`}
<ha-icon-button
.path=${mdiMagnify}
@click=${this._showQuickBar}
></ha-icon-button>
${!this.narrow &&
this._conversation(this.hass.config.components)
? html`
@@ -557,7 +551,7 @@ class HUIRoot extends LitElement {
let newSelectView;
let force = false;
const viewPath = decodeURI(this.route!.path.split("/")[1]);
const viewPath = this.route!.path.split("/")[1];
if (changedProperties.has("route")) {
const views = this.config.views;
@@ -679,13 +673,6 @@ class HUIRoot extends LitElement {
});
}
private _showQuickBar(): void {
showQuickBar(this, {
commandMode: false,
hint: this.hass.localize("ui.dialogs.quick-bar.key_e_hint"),
});
}
private _handleRawEditor(ev: CustomEvent<RequestSelectedDetail>): void {
if (!shouldHandleRequestSelectedEvent(ev)) {
return;
@@ -170,12 +170,11 @@ class PanelMediaBrowser extends LitElement {
media_content_id: undefined,
},
...navigateIdsEncoded.map((navigateId) => {
const decoded = decodeURIComponent(navigateId);
// Don't use split because media_content_id could contain commas
const delimiter = decoded.indexOf(",");
const [media_content_type, media_content_id] =
decodeURIComponent(navigateId).split(",");
return {
media_content_type: decoded.substring(0, delimiter),
media_content_id: decoded.substring(delimiter + 1),
media_content_type,
media_content_id,
};
}),
];
+176 -192
View File
@@ -12,172 +12,158 @@ import "../../layouts/hass-error-screen";
import { HomeAssistant, Route } from "../../types";
import { documentationUrl } from "../../util/documentation-url";
const getRedirect = (
path: string,
hasSupervisor: boolean
): Redirect | undefined =>
((
{
developer_states: {
redirect: "/developer-tools/state",
},
developer_services: {
redirect: "/developer-tools/service",
},
developer_call_service: {
redirect: "/developer-tools/service",
params: {
service: "string",
},
},
developer_template: {
redirect: "/developer-tools/template",
},
developer_events: {
redirect: "/developer-tools/event",
},
developer_statistics: {
redirect: "/developer-tools/statistics",
},
config: {
redirect: "/config",
},
cloud: {
component: "cloud",
redirect: "/config/cloud",
},
integrations: {
redirect: "/config/integrations",
},
config_flow_start: {
redirect: "/config/integrations/add",
params: {
domain: "string",
},
},
config_mqtt: {
component: "mqtt",
redirect: "/config/mqtt",
},
config_zha: {
component: "zha",
redirect: "/config/zha/dashboard",
},
config_zwave_js: {
component: "zwave_js",
redirect: "/config/zwave_js/dashboard",
},
config_energy: {
component: "energy",
redirect: "/config/energy/dashboard",
},
devices: {
redirect: "/config/devices/dashboard",
},
entities: {
redirect: "/config/entities",
},
energy: {
component: "energy",
redirect: "/energy",
},
areas: {
redirect: "/config/areas/dashboard",
},
blueprints: {
component: "blueprint",
redirect: "/config/blueprint/dashboard",
},
blueprint_import: {
component: "blueprint",
redirect: "/config/blueprint/dashboard/import",
params: {
blueprint_url: "url",
},
},
automations: {
component: "automation",
redirect: "/config/automation/dashboard",
},
scenes: {
component: "scene",
redirect: "/config/scene/dashboard",
},
scripts: {
component: "script",
redirect: "/config/script/dashboard",
},
helpers: {
redirect: "/config/helpers",
},
tags: {
component: "tag",
redirect: "/config/tags",
},
lovelace_dashboards: {
component: "lovelace",
redirect: "/config/lovelace/dashboards",
},
lovelace_resources: {
component: "lovelace",
redirect: "/config/lovelace/resources",
},
people: {
component: "person",
redirect: "/config/person",
},
zones: {
component: "zone",
redirect: "/config/zone",
},
users: {
redirect: "/config/users",
},
general: {
redirect: "/config/core",
},
server_controls: {
redirect: "/config/server_control",
},
logs: {
redirect: "/config/logs",
},
info: {
redirect: "/config/info",
},
customize: {
// customize was removed in 2021.12, fallback to dashboard
redirect: "/config/dashboard",
},
profile: {
redirect: "/profile/dashboard",
},
logbook: {
component: "logbook",
redirect: "/logbook",
},
history: {
component: "history",
redirect: "/history",
},
media_browser: {
component: "media_source",
redirect: "/media-browser",
},
backup: {
component: hasSupervisor ? "hassio" : "backup",
redirect: hasSupervisor ? "/hassio/backups" : "/config/backup",
},
supervisor_snapshots: {
component: hasSupervisor ? "hassio" : "backup",
redirect: hasSupervisor ? "/hassio/backups" : "/config/backup",
},
supervisor_backups: {
component: hasSupervisor ? "hassio" : "backup",
redirect: hasSupervisor ? "/hassio/backups" : "/config/backup",
},
} as Redirects
)[path]);
const REDIRECTS: Redirects = {
developer_states: {
redirect: "/developer-tools/state",
},
developer_services: {
redirect: "/developer-tools/service",
},
developer_call_service: {
redirect: "/developer-tools/service",
params: {
service: "string",
},
},
developer_template: {
redirect: "/developer-tools/template",
},
developer_events: {
redirect: "/developer-tools/event",
},
developer_statistics: {
redirect: "/developer-tools/statistics",
},
config: {
redirect: "/config",
},
cloud: {
component: "cloud",
redirect: "/config/cloud",
},
integrations: {
redirect: "/config/integrations",
},
config_flow_start: {
redirect: "/config/integrations/add",
params: {
domain: "string",
},
},
config_mqtt: {
component: "mqtt",
redirect: "/config/mqtt",
},
config_zha: {
component: "zha",
redirect: "/config/zha/dashboard",
},
config_zwave_js: {
component: "zwave_js",
redirect: "/config/zwave_js/dashboard",
},
config_energy: {
component: "energy",
redirect: "/config/energy/dashboard",
},
devices: {
redirect: "/config/devices/dashboard",
},
entities: {
redirect: "/config/entities",
},
energy: {
component: "energy",
redirect: "/energy",
},
areas: {
redirect: "/config/areas/dashboard",
},
blueprints: {
component: "blueprint",
redirect: "/config/blueprint/dashboard",
},
blueprint_import: {
component: "blueprint",
redirect: "/config/blueprint/dashboard/import",
params: {
blueprint_url: "url",
},
},
automations: {
component: "automation",
redirect: "/config/automation/dashboard",
},
scenes: {
component: "scene",
redirect: "/config/scene/dashboard",
},
scripts: {
component: "script",
redirect: "/config/script/dashboard",
},
helpers: {
redirect: "/config/helpers",
},
tags: {
component: "tag",
redirect: "/config/tags",
},
lovelace_dashboards: {
component: "lovelace",
redirect: "/config/lovelace/dashboards",
},
lovelace_resources: {
component: "lovelace",
redirect: "/config/lovelace/resources",
},
backup: {
component: "backup",
redirect: "/config/backup",
},
people: {
component: "person",
redirect: "/config/person",
},
zones: {
component: "zone",
redirect: "/config/zone",
},
users: {
redirect: "/config/users",
},
general: {
redirect: "/config/core",
},
server_controls: {
redirect: "/config/server_control",
},
logs: {
redirect: "/config/logs",
},
info: {
redirect: "/config/info",
},
customize: {
// customize was removed in 2021.12, fallback to dashboard
redirect: "/config/dashboard",
},
profile: {
redirect: "/profile/dashboard",
},
logbook: {
component: "logbook",
redirect: "/logbook",
},
history: {
component: "history",
redirect: "/history",
},
media_browser: {
component: "media_source",
redirect: "/media-browser",
},
};
export type ParamType = "url" | "string";
@@ -198,17 +184,19 @@ class HaPanelMy extends LitElement {
@state() public _error?: string;
private _redirect?: Redirect;
connectedCallback() {
super.connectedCallback();
const path = this.route.path.substring(1);
const hasSupervisor = isComponentLoaded(this.hass, "hassio");
this._redirect = getRedirect(path, hasSupervisor);
if (path === "backup" && isComponentLoaded(this.hass, "hassio")) {
navigate("/hassio/backups", {
replace: true,
});
return;
}
if (path.startsWith("supervisor") && this._redirect === undefined) {
if (!hasSupervisor) {
if (path.startsWith("supervisor")) {
if (!isComponentLoaded(this.hass, "hassio")) {
this._error = "no_supervisor";
return;
}
@@ -218,14 +206,16 @@ class HaPanelMy extends LitElement {
return;
}
if (!this._redirect) {
const redirect = REDIRECTS[path];
if (!redirect) {
this._error = "not_supported";
return;
}
if (
this._redirect.component &&
!isComponentLoaded(this.hass, this._redirect.component)
redirect.component &&
!isComponentLoaded(this.hass, redirect.component)
) {
this._error = "no_component";
return;
@@ -233,7 +223,7 @@ class HaPanelMy extends LitElement {
let url: string;
try {
url = this._createRedirectUrl();
url = this._createRedirectUrl(redirect);
} catch (err: any) {
this._error = "url_error";
return;
@@ -264,16 +254,10 @@ class HaPanelMy extends LitElement {
this.hass.localize(
"ui.panel.my.component_not_loaded",
"integration",
html`<a
target="_blank"
rel="noreferrer noopener"
href=${documentationUrl(
this.hass,
`/integrations/${this._redirect!.component!}`
)}
>
${domainToName(this.hass.localize, this._redirect!.component!)}
</a>`
domainToName(
this.hass.localize,
REDIRECTS[this.route.path.substr(1)].component!
)
) || "This redirect is not supported.";
break;
case "no_supervisor":
@@ -296,18 +280,18 @@ class HaPanelMy extends LitElement {
return html``;
}
private _createRedirectUrl(): string {
const params = this._createRedirectParams();
return `${this._redirect!.redirect}${params}`;
private _createRedirectUrl(redirect: Redirect): string {
const params = this._createRedirectParams(redirect);
return `${redirect.redirect}${params}`;
}
private _createRedirectParams(): string {
private _createRedirectParams(redirect: Redirect): string {
const params = extractSearchParamsObject();
if (!this._redirect!.params && !Object.keys(params).length) {
if (!redirect.params && !Object.keys(params).length) {
return "";
}
const resultParams = {};
Object.entries(this._redirect!.params || {}).forEach(([key, type]) => {
Object.entries(redirect.params || {}).forEach(([key, type]) => {
if (!params[key] || !this._checkParamType(type, params[key])) {
throw Error();
}
-1
View File
@@ -312,7 +312,6 @@ export const haStyleDialog = css`
--mdc-dialog-heading-ink-color: var(--primary-text-color);
--mdc-dialog-content-ink-color: var(--primary-text-color);
--justify-action-buttons: space-between;
--ha-dialog-border-radius: 16px;
}
ha-dialog .form {
+7 -12
View File
@@ -664,10 +664,8 @@
},
"types": {
"reload": "Reload",
"navigation": "Navigation",
"server_control": "Server",
"entity": "Entity",
"suggestion": "Suggestions"
"navigation": "Navigate",
"server_control": "Server"
},
"navigation": {
"logs": "[%key:ui::panel::config::logs::caption%]",
@@ -691,7 +689,7 @@
"server_control": "[%key:ui::panel::config::server_control::caption%]"
}
},
"filter_placeholder": "Search for entities, pages or commands",
"filter_placeholder": "Entity Filter",
"title": "Quick Search",
"key_c_hint": "Press 'c' on any page to open this search bar",
"nothing_found": "Nothing found!"
@@ -733,7 +731,7 @@
"setting": "Setting"
},
"update": {
"installed_version": "Installed version",
"current_version": "Current version",
"latest_version": "Latest version",
"release_announcement": "Read release announcement",
"skip": "Skip",
@@ -1452,7 +1450,7 @@
"internal_url_label": "Local Network",
"external_url_label": "Internet",
"external_use_ha_cloud": "Use Home Assistant Cloud",
"external_get_ha_cloud": "Access from anywhere, add Google & Alexa easily",
"external_get_ha_cloud": "Access from anywhere using Home Assistant Cloud",
"ha_cloud_remote_not_enabled": "Your Home Assistant Cloud remote connection is currently not enabled.",
"enable_remote": "[%key:ui::common::enable%]",
"internal_url_automatic": "Automatic",
@@ -2713,14 +2711,12 @@
"open_configuration_url": "Visit device"
},
"config_flow": {
"success": "Success",
"aborted": "Aborted",
"close": "Close",
"dismiss": "Dismiss dialog",
"finish": "Finish",
"submit": "Submit",
"next": "Next",
"found_following_devices": "We found the following devices",
"no_config_flow": "This integration does not support configuration via the UI. If you followed this link from the Home Assistant website, make sure you run the latest version of Home Assistant.",
"not_all_required_fields": "Not all required fields are filled in.",
"error_saving_area": "Error saving area: {error}",
@@ -4175,8 +4171,7 @@
"container": "Container",
"disabled": "Disabled",
"header": "Network",
"show_disabled": "Show disabled ports",
"introduction": "Change the ports on your host that are exposed by the add-on"
"host": "Host"
}
},
"dashboard": {
@@ -4207,7 +4202,7 @@
},
"rating": {
"title": "Add-on Security Rating",
"description": "Home Assistant provides a security rating to each of the add-ons, which indicates the risks involved when using this add-on. The more access an add-on requires on your system, the lower the score, thus raising the possible security risks.\n\nA score is on a scale from 1 to 8. Where 1 is the lowest score (considered the most insecure and highest risk) and a score of 8 is the highest score (considered the most secure and lowest risk)."
"description": "Home Assistant provides a security rating to each of the add-ons, which indicates the risks involved when using this add-on. The more access an add-on requires on your system, the lower the score, thus raising the possible security risks.\n\nA score is on a scale from 1 to 6. Where 1 is the lowest score (considered the most insecure and highest risk) and a score of 6 is the highest score (considered the most secure and lowest risk)."
},
"host_network": {
"title": "Host Network",