mirror of
https://github.com/home-assistant/frontend.git
synced 2026-06-04 15:32:03 +00:00
Compare commits
25 Commits
20260527.1
..
rc
| Author | SHA1 | Date | |
|---|---|---|---|
| ba26e9f491 | |||
| 8778fe8577 | |||
| 6801aaea30 | |||
| c3f5b6693a | |||
| 68f75c82eb | |||
| 6660e4799c | |||
| 08bfafea21 | |||
| 5677e60fcc | |||
| 73557e6464 | |||
| e9e6c60d8b | |||
| 1651c210be | |||
| 927c036454 | |||
| 0fefcf809f | |||
| a176f3c1ef | |||
| c5152c3472 | |||
| 0150337522 | |||
| 5d55d543b1 | |||
| 4805b22289 | |||
| 8de411abc3 | |||
| e455d4384a | |||
| b0dbd825c8 | |||
| 69d0fcb666 | |||
| f7c3ed3b77 | |||
| 5ee5b5120e | |||
| 58fc8160fd |
@@ -13,6 +13,28 @@ export interface NetworkInfo {
|
||||
supervisor_internet: boolean;
|
||||
}
|
||||
|
||||
interface SupervisorJob {
|
||||
name: string;
|
||||
reference: string | null;
|
||||
uuid: string;
|
||||
progress: number; // float, 0–100
|
||||
stage: string | null;
|
||||
done: boolean;
|
||||
errors: {
|
||||
type: string;
|
||||
message: string;
|
||||
stage: string | null;
|
||||
}[];
|
||||
created: string; // ISO datetime string
|
||||
extra: Record<string, unknown> | null;
|
||||
child_jobs: SupervisorJob[];
|
||||
}
|
||||
|
||||
export interface SupervisorJobInfo {
|
||||
ignore_conditions: string[];
|
||||
jobs: SupervisorJob[];
|
||||
}
|
||||
|
||||
export const ALTERNATIVE_DNS_SERVERS: {
|
||||
ipv4: string[];
|
||||
ipv6: string[];
|
||||
@@ -57,6 +79,15 @@ export async function getSupervisorNetworkInfo(): Promise<NetworkInfo> {
|
||||
return responseData?.data;
|
||||
}
|
||||
|
||||
export async function getSupervisorJobsInfo(): Promise<
|
||||
HassioResponse<SupervisorJobInfo>
|
||||
> {
|
||||
const responseData = await handleFetchPromise<
|
||||
HassioResponse<SupervisorJobInfo>
|
||||
>(fetch("/supervisor-api/jobs/info"));
|
||||
return responseData;
|
||||
}
|
||||
|
||||
export const setSupervisorNetworkDns = async (
|
||||
dnsServerIndex: number,
|
||||
primaryInterface: string
|
||||
|
||||
@@ -2,9 +2,9 @@ import { mdiOpenInNew } from "@mdi/js";
|
||||
import { css, html, nothing, type PropertyValues } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { extractSearchParam } from "../../src/common/url/search-params";
|
||||
import "../../src/components/animation/ha-fade-in";
|
||||
import "../../src/components/ha-alert";
|
||||
import "../../src/components/ha-button";
|
||||
import "../../src/components/animation/ha-fade-in";
|
||||
import "../../src/components/ha-spinner";
|
||||
import "../../src/components/ha-svg-icon";
|
||||
import "../../src/components/progress/ha-progress-bar";
|
||||
@@ -15,6 +15,7 @@ import { haStyle } from "../../src/resources/styles";
|
||||
import "./components/landing-page-logs";
|
||||
import "./components/landing-page-network";
|
||||
import {
|
||||
getSupervisorJobsInfo,
|
||||
getSupervisorNetworkInfo,
|
||||
pingSupervisor,
|
||||
type NetworkInfo,
|
||||
@@ -24,6 +25,7 @@ import { LandingPageBaseElement } from "./landing-page-base-element";
|
||||
export const ASSUME_CORE_START_SECONDS = 60;
|
||||
const SCHEDULE_CORE_CHECK_SECONDS = 1;
|
||||
const SCHEDULE_FETCH_NETWORK_INFO_SECONDS = 5;
|
||||
const SCHEDULE_FETCH_JOBS_INFO_SECONDS = 2;
|
||||
|
||||
@customElement("ha-landing-page")
|
||||
class HaLandingPage extends LandingPageBaseElement {
|
||||
@@ -39,6 +41,8 @@ class HaLandingPage extends LandingPageBaseElement {
|
||||
|
||||
@state() private _coreCheckActive = false;
|
||||
|
||||
@state() private _progress = -1;
|
||||
|
||||
private _mobileApp =
|
||||
extractSearchParam("redirect_uri") === "homeassistant://auth-callback";
|
||||
|
||||
@@ -60,7 +64,14 @@ class HaLandingPage extends LandingPageBaseElement {
|
||||
${!networkIssue && !this._supervisorError
|
||||
? html`
|
||||
<p>${this.localize("subheader")}</p>
|
||||
<ha-progress-bar indeterminate></ha-progress-bar>
|
||||
<ha-progress-bar
|
||||
.indeterminate=${this._progress <= 0}
|
||||
.value=${this._progress > 0 ? this._progress : undefined}
|
||||
.loading=${this._progress >= 0}
|
||||
>${this._progress > 0
|
||||
? `${Math.round(this._progress)}%`
|
||||
: nothing}</ha-progress-bar
|
||||
>
|
||||
`
|
||||
: nothing}
|
||||
${networkIssue || this._networkInfoError
|
||||
@@ -126,6 +137,7 @@ class HaLandingPage extends LandingPageBaseElement {
|
||||
import("../../src/components/ha-language-picker");
|
||||
|
||||
this._fetchSupervisorInfo(true);
|
||||
this._fetchSupervisorJobsInfo();
|
||||
}
|
||||
|
||||
private _scheduleFetchSupervisorInfo() {
|
||||
@@ -138,6 +150,13 @@ class HaLandingPage extends LandingPageBaseElement {
|
||||
);
|
||||
}
|
||||
|
||||
private _scheduleFetchSupervisorJobsInfo() {
|
||||
setTimeout(
|
||||
() => this._fetchSupervisorJobsInfo(),
|
||||
SCHEDULE_FETCH_JOBS_INFO_SECONDS * 1000
|
||||
);
|
||||
}
|
||||
|
||||
private _scheduleTurnOffCoreCheck() {
|
||||
setTimeout(() => {
|
||||
this._coreCheckActive = false;
|
||||
@@ -165,7 +184,7 @@ class HaLandingPage extends LandingPageBaseElement {
|
||||
// assume supervisor update if ping fails -> don't show an error
|
||||
if (!this._coreCheckActive && err.message !== "ping-failed") {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(err);
|
||||
console.error("Failed to fetch supervisor info", err);
|
||||
this._networkInfoError = true;
|
||||
}
|
||||
}
|
||||
@@ -175,6 +194,33 @@ class HaLandingPage extends LandingPageBaseElement {
|
||||
}
|
||||
}
|
||||
|
||||
private async _fetchSupervisorJobsInfo() {
|
||||
try {
|
||||
const jobsInfo = await getSupervisorJobsInfo();
|
||||
const coreInstallJob =
|
||||
jobsInfo.result === "ok"
|
||||
? jobsInfo.data.jobs.find(
|
||||
(job) => job.name === "home_assistant_core_install"
|
||||
)
|
||||
: undefined;
|
||||
if (coreInstallJob) {
|
||||
this._progress = coreInstallJob.progress;
|
||||
} else {
|
||||
this._progress = -1;
|
||||
}
|
||||
} catch (err: any) {
|
||||
await this._checkCoreAvailability();
|
||||
|
||||
if (!this._coreCheckActive) {
|
||||
this._progress = -1;
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("Failed to fetch supervisor jobs info", err);
|
||||
}
|
||||
}
|
||||
|
||||
this._scheduleFetchSupervisorJobsInfo();
|
||||
}
|
||||
|
||||
private async _checkCoreAvailability() {
|
||||
try {
|
||||
const response = await fetch("/manifest.json");
|
||||
@@ -222,21 +268,27 @@ class HaLandingPage extends LandingPageBaseElement {
|
||||
flex-direction: column;
|
||||
gap: var(--ha-space-4);
|
||||
}
|
||||
ha-language-picker {
|
||||
min-width: 200px;
|
||||
}
|
||||
ha-alert p {
|
||||
text-align: unset;
|
||||
}
|
||||
.footer ha-svg-icon {
|
||||
--mdc-icon-size: var(--ha-space-5);
|
||||
}
|
||||
ha-language-picker {
|
||||
margin-inline-start: calc(-1 * var(--ha-space-4));
|
||||
}
|
||||
ha-button {
|
||||
margin-inline-end: calc(-1 * var(--ha-space-2));
|
||||
}
|
||||
ha-fade-in {
|
||||
min-height: calc(100vh - 64px - 88px);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
ha-progress-bar {
|
||||
--ha-progress-bar-track-height: 20px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "home-assistant-frontend"
|
||||
version = "20260527.1"
|
||||
version = "20260527.4"
|
||||
license = "Apache-2.0"
|
||||
license-files = ["LICENSE*"]
|
||||
description = "The Home Assistant frontend"
|
||||
|
||||
@@ -17,6 +17,19 @@ export interface NavigateOptions {
|
||||
// max time to wait for dialogs to close before navigating
|
||||
const DIALOG_WAIT_TIMEOUT = 500;
|
||||
|
||||
/**
|
||||
* Stash a destination URL in the current history entry's state. If the page
|
||||
* is refreshed while a dialog is open, urlSyncMixin will navigate to this URL
|
||||
* on load instead of cleaning up the stale dialog state by going back.
|
||||
* The current URL is not changed.
|
||||
*/
|
||||
export const setRefreshUrl = (path: string) => {
|
||||
mainWindow.history.replaceState(
|
||||
{ ...mainWindow.history.state, refreshUrl: path },
|
||||
""
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Ensures all dialogs are closed before navigation.
|
||||
* Returns true if navigation can proceed, false if a dialog refused to close.
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { LitElement, css, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import "../ha-tooltip";
|
||||
|
||||
export type LiveTestState = "pass" | "fail" | "invalid" | "unknown";
|
||||
|
||||
@@ -13,7 +12,6 @@ export type LiveTestState = "pass" | "fail" | "invalid" | "unknown";
|
||||
*
|
||||
* @attr {"pass"|"fail"|"invalid"|"unknown"} state - The current live-test state. Defaults to `unknown`.
|
||||
* @attr {string} label - Accessible label announced by assistive technology.
|
||||
* @attr {string} message - Optional tooltip body shown on hover/focus.
|
||||
*/
|
||||
@customElement("ha-automation-row-live-test")
|
||||
export class HaAutomationRowLiveTest extends LitElement {
|
||||
@@ -21,8 +19,6 @@ export class HaAutomationRowLiveTest extends LitElement {
|
||||
|
||||
@property() public label = "";
|
||||
|
||||
@property() public message?: string;
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<div
|
||||
@@ -31,39 +27,38 @@ export class HaAutomationRowLiveTest extends LitElement {
|
||||
tabindex="0"
|
||||
aria-label=${this.label}
|
||||
></div>
|
||||
${this.message
|
||||
? html`<ha-tooltip for="indicator">${this.message}</ha-tooltip>`
|
||||
: nothing}
|
||||
`;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
position: absolute;
|
||||
top: -5px;
|
||||
inset-inline-end: -6px;
|
||||
display: inline-block;
|
||||
}
|
||||
#indicator {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: var(--ha-border-radius-circle);
|
||||
border: 3px solid;
|
||||
border: var(--ha-border-width-md) solid;
|
||||
box-sizing: border-box;
|
||||
background-color: var(--card-background-color);
|
||||
box-shadow: 0 0 0 2px var(--card-background-color);
|
||||
transition: all var(--ha-animation-duration-normal) ease-in-out;
|
||||
}
|
||||
:host([state="pass"]) #indicator {
|
||||
background-color: var(--ha-color-fill-success-loud-resting);
|
||||
border-color: var(--ha-color-fill-success-loud-resting);
|
||||
background-color: var(--ha-color-green-60);
|
||||
border-color: var(--ha-color-green-60);
|
||||
}
|
||||
:host([state="fail"]) #indicator {
|
||||
border-color: var(--ha-color-fill-warning-loud-resting);
|
||||
border-color: var(--ha-color-orange-60);
|
||||
}
|
||||
:host([state="invalid"]) #indicator {
|
||||
border-color: var(--ha-color-fill-danger-loud-resting);
|
||||
border-color: var(--ha-color-red-60);
|
||||
}
|
||||
:host([state="unknown"]) #indicator {
|
||||
border-color: var(--ha-color-fill-neutral-loud-resting);
|
||||
border-color: var(--ha-color-neutral-60);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -165,7 +165,8 @@ export class HaAutomationRow extends LitElement {
|
||||
::slotted([slot="leading-icon"]) {
|
||||
color: var(--ha-color-on-neutral-quiet);
|
||||
}
|
||||
:host([building-block]) ::slotted([slot="leading-icon"]) {
|
||||
:host([building-block]) ::slotted([slot="leading-icon"].action-icon),
|
||||
:host([building-block]) ::slotted(#condition-icon) {
|
||||
--mdc-icon-size: var(--ha-space-5);
|
||||
color: var(--white-color);
|
||||
transform: rotate(-45deg);
|
||||
|
||||
@@ -101,18 +101,22 @@ export class HaSankeyChart extends LitElement {
|
||||
const value = this.valueFormatter
|
||||
? this.valueFormatter(data.value)
|
||||
: data.value;
|
||||
// Keep numbers and units left-to-right, even in RTL locales.
|
||||
const formattedValue = html`<div style="direction:ltr; display: inline;">
|
||||
${value}
|
||||
</div>`;
|
||||
if (data.id) {
|
||||
const node = this.data.nodes.find((n) => n.id === data.id);
|
||||
return html`<ha-chart-tooltip-marker
|
||||
.color=${String(params.color ?? "")}
|
||||
></ha-chart-tooltip-marker>
|
||||
${node?.label ?? data.id}<br />${value}`;
|
||||
${node?.label ?? data.id}<br />${formattedValue}`;
|
||||
}
|
||||
if (data.source && data.target) {
|
||||
const source = this.data.nodes.find((n) => n.id === data.source);
|
||||
const target = this.data.nodes.find((n) => n.id === data.target);
|
||||
return html`${source?.label ?? data.source} →
|
||||
${target?.label ?? data.target}<br />${value}`;
|
||||
${target?.label ?? data.target}<br />${formattedValue}`;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
@@ -77,7 +77,7 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
|
||||
| "bottom-start"
|
||||
| "bottom-end"
|
||||
| "left-start"
|
||||
| "left-end" = "bottom-start";
|
||||
| "left-end" = "bottom";
|
||||
|
||||
/** If set picker shows an add button instead of textbox when value isn't set */
|
||||
@property({ attribute: "add-button-label" }) public addButtonLabel?: string;
|
||||
|
||||
@@ -121,7 +121,6 @@ export class HaIconPicker extends LitElement {
|
||||
.label=${this.label}
|
||||
.value=${this._value}
|
||||
.searchFn=${this._filterIcons}
|
||||
popover-placement="bottom-start"
|
||||
@value-changed=${this._valueChanged}
|
||||
>
|
||||
<slot name="start"></slot>
|
||||
|
||||
@@ -152,7 +152,6 @@ export class HaLanguagePicker extends LitElement {
|
||||
<ha-generic-picker
|
||||
.hass=${this.hass}
|
||||
.autofocus=${this.autofocus}
|
||||
popover-placement="bottom-end"
|
||||
.notFoundLabel=${this._notFoundLabel}
|
||||
.emptyLabel=${this.hass?.localize(
|
||||
"ui.components.language-picker.no_languages"
|
||||
|
||||
@@ -82,7 +82,6 @@ export class HaThemePicker extends LitElement {
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
@value-changed=${this._changed}
|
||||
popover-placement="bottom"
|
||||
></ha-generic-picker>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ export const haTopAppBarFixedStyles = css`
|
||||
padding-top: var(--safe-area-inset-top);
|
||||
padding-right: var(--safe-area-inset-right);
|
||||
transition:
|
||||
box-shadow var(--ha-animation-duration-normal) ease,
|
||||
width var(--ha-animation-duration-normal) ease,
|
||||
padding-left var(--ha-animation-duration-normal) ease,
|
||||
padding-right var(--ha-animation-duration-normal) ease;
|
||||
|
||||
@@ -485,6 +485,17 @@ export const migrateAutomationTrigger = (
|
||||
}
|
||||
delete trigger.platform;
|
||||
}
|
||||
|
||||
if ("options" in trigger) {
|
||||
if (trigger.options && "behavior" in trigger.options) {
|
||||
if (trigger.options.behavior === "any") {
|
||||
trigger.options.behavior = "each";
|
||||
} else if (trigger.options.behavior === "last") {
|
||||
trigger.options.behavior = "all";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return trigger;
|
||||
};
|
||||
|
||||
|
||||
@@ -256,6 +256,7 @@ export const normalizeSubscriptionEventData = (
|
||||
dtstart: eventStart,
|
||||
dtend: eventEnd,
|
||||
description: eventData.description ?? undefined,
|
||||
location: eventData.location ?? undefined,
|
||||
uid: eventData.uid ?? undefined,
|
||||
recurrence_id: eventData.recurrence_id ?? undefined,
|
||||
rrule: eventData.rrule ?? undefined,
|
||||
|
||||
@@ -1,26 +1,37 @@
|
||||
import { computeDomain } from "../../common/entity/compute_domain";
|
||||
import { navigate } from "../../common/navigate";
|
||||
import type { LocalizeFunc } from "../../common/translations/localize";
|
||||
import type { LocalizeKeys } from "../../common/translations/localize";
|
||||
import { createSearchParam } from "../../common/url/search-params";
|
||||
import type { EntityRegistryEntry } from "../../data/entity/entity_registry";
|
||||
import { SCENE_IGNORED_DOMAINS, type SceneEntities } from "../../data/scene";
|
||||
import type { SingleHassServiceTarget } from "../../data/target";
|
||||
import {
|
||||
ADD_AUTOMATION_ELEMENT_AREA_TARGET_PARAM,
|
||||
ADD_AUTOMATION_ELEMENT_DEVICE_TARGET_PARAM,
|
||||
ADD_AUTOMATION_ELEMENT_QUERY_PARAM,
|
||||
ADD_AUTOMATION_ELEMENT_ENTITY_TARGET_PARAM,
|
||||
ADD_AUTOMATION_ELEMENT_QUERY_PARAM,
|
||||
} from "../../panels/config/automation/show-add-automation-element-dialog";
|
||||
import type { HomeAssistant, TranslationDict } from "../../types";
|
||||
|
||||
/** Add to action keys are the keys of the translation dictionary for the add to actions. */
|
||||
export type AddToActionKey =
|
||||
TranslationDict["ui"]["dialogs"]["more_info_control"]["add_to"]["actions"] extends infer Actions
|
||||
? keyof Actions
|
||||
: never;
|
||||
/** Add to action keys are the keys of the translation dictionary for the add to action options. */
|
||||
type AddToActionOptions =
|
||||
TranslationDict["ui"]["dialogs"]["more_info_control"]["add_to"]["action_options"];
|
||||
|
||||
export type AddToActionKey = Extract<keyof AddToActionOptions, string>;
|
||||
|
||||
export type AddToAutomationScriptActionKey = Exclude<AddToActionKey, "scene">;
|
||||
|
||||
/** Fully-qualified localize key for an add to action option label. */
|
||||
type AddToActionOptionLabelKey = LocalizeKeys &
|
||||
`ui.dialogs.more_info_control.add_to.action_options.${AddToActionKey}`;
|
||||
|
||||
interface BaseEntityAddToAction {
|
||||
/** Whether the action is enabled and can be selected. */
|
||||
enabled: boolean;
|
||||
/** Translated name of the action */
|
||||
name: string;
|
||||
/** Translated label of the action option */
|
||||
name?: string;
|
||||
/** Fully-qualified localize key for the action option label */
|
||||
nameKey?: AddToActionOptionLabelKey;
|
||||
/** Optional translated description of the action */
|
||||
description?: string;
|
||||
/** MDI icon name (e.g., "mdi:car") */
|
||||
@@ -31,7 +42,7 @@ export interface DefaultEntityAddToAction extends BaseEntityAddToAction {
|
||||
/** Type of action handled in the frontend */
|
||||
type: "default";
|
||||
/** Stable key used to resolve the action handler */
|
||||
key: AddToActionKey;
|
||||
key: AddToAutomationScriptActionKey;
|
||||
}
|
||||
|
||||
export interface ExternalEntityAddToAction extends BaseEntityAddToAction {
|
||||
@@ -48,11 +59,11 @@ export type EntityAddToAction =
|
||||
export type EntityAddToActions = EntityAddToAction[];
|
||||
|
||||
interface ActionDefinition {
|
||||
translation_key: AddToActionKey;
|
||||
translation_key: AddToAutomationScriptActionKey;
|
||||
icon: string;
|
||||
}
|
||||
|
||||
export const DEFAULT_ACTION_DEFS: ActionDefinition[] = [
|
||||
const DEFAULT_ACTION_DEFS: ActionDefinition[] = [
|
||||
{
|
||||
translation_key: "automation_trigger",
|
||||
icon: "mdi:robot-outline",
|
||||
@@ -71,33 +82,49 @@ export const DEFAULT_ACTION_DEFS: ActionDefinition[] = [
|
||||
},
|
||||
];
|
||||
|
||||
export const getDefaultAddToActions = (
|
||||
states: HomeAssistant["states"],
|
||||
localize: LocalizeFunc,
|
||||
formatEntityName: HomeAssistant["formatEntityName"],
|
||||
entityId: string
|
||||
): EntityAddToActions =>
|
||||
export const getDefaultAddToActions = (): EntityAddToActions =>
|
||||
DEFAULT_ACTION_DEFS.map(
|
||||
(def: ActionDefinition): EntityAddToAction => ({
|
||||
type: "default",
|
||||
key: def.translation_key,
|
||||
enabled: true,
|
||||
name: localize(
|
||||
`ui.dialogs.more_info_control.add_to.actions.${def.translation_key}`,
|
||||
{
|
||||
target:
|
||||
states[entityId] !== undefined
|
||||
? formatEntityName(states[entityId], undefined)
|
||||
: entityId,
|
||||
}
|
||||
),
|
||||
nameKey: `ui.dialogs.more_info_control.add_to.action_options.${def.translation_key}`,
|
||||
icon: def.icon,
|
||||
})
|
||||
);
|
||||
|
||||
export const createAddToSceneEntities = (
|
||||
entityIds: string[]
|
||||
): SceneEntities => {
|
||||
const entities: SceneEntities = {};
|
||||
for (const entityId of entityIds) {
|
||||
entities[entityId] = "";
|
||||
}
|
||||
return entities;
|
||||
};
|
||||
|
||||
export const filterAddToSceneEntityIds = (
|
||||
entityIds: string[],
|
||||
entityRegistry: readonly EntityRegistryEntry[],
|
||||
states: HomeAssistant["states"]
|
||||
): string[] => {
|
||||
const entityIdSet = new Set(entityIds);
|
||||
|
||||
return entityRegistry
|
||||
.filter((entry) => entityIdSet.has(entry.entity_id))
|
||||
.filter(
|
||||
(entry) =>
|
||||
!entry.entity_category &&
|
||||
!entry.hidden_by &&
|
||||
!SCENE_IGNORED_DOMAINS.includes(computeDomain(entry.entity_id)) &&
|
||||
states[entry.entity_id]
|
||||
)
|
||||
.map((entry) => entry.entity_id);
|
||||
};
|
||||
|
||||
/** Handler for adding a target to an automation/script. */
|
||||
export function addToActionHandler(
|
||||
key: AddToActionKey,
|
||||
key: AddToAutomationScriptActionKey,
|
||||
target: SingleHassServiceTarget
|
||||
): Promise<boolean> {
|
||||
const searchParams: Record<string, string> = {};
|
||||
@@ -0,0 +1,211 @@
|
||||
import { mdiPlus } from "@mdi/js";
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import type {
|
||||
HASSDomCurrentTargetEvent,
|
||||
HASSDomEvent,
|
||||
} from "../../common/dom/fire_event";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type {
|
||||
LocalizeFunc,
|
||||
LocalizeKeys,
|
||||
} from "../../common/translations/localize";
|
||||
import "../../components/ha-icon";
|
||||
import "../../components/ha-svg-icon";
|
||||
import type { HaListItemButton } from "../../components/item/ha-list-item-button";
|
||||
import "../../components/item/ha-list-item-button";
|
||||
import "../../components/list/ha-list-base";
|
||||
import { consumeLocalize } from "../../common/decorators/consume-context-entry";
|
||||
|
||||
export interface AddToActionListItem {
|
||||
name?: string;
|
||||
nameKey?: LocalizeKeys;
|
||||
description?: string;
|
||||
descriptionKey?: LocalizeKeys;
|
||||
icon?: string;
|
||||
iconPath?: string;
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
export interface AddToActionListSection<
|
||||
Item extends AddToActionListItem = AddToActionListItem,
|
||||
> {
|
||||
title?: string;
|
||||
titleKey?: LocalizeKeys;
|
||||
actions: readonly Item[];
|
||||
empty?: string;
|
||||
emptyKey?: LocalizeKeys;
|
||||
}
|
||||
|
||||
export interface AddToActionListActionSelectedDetail<
|
||||
Item extends AddToActionListItem = AddToActionListItem,
|
||||
> {
|
||||
action: Item;
|
||||
}
|
||||
|
||||
export type AddToActionListActionSelectedEvent<
|
||||
Item extends AddToActionListItem = AddToActionListItem,
|
||||
> = HASSDomEvent<AddToActionListActionSelectedDetail<Item>>;
|
||||
|
||||
@customElement("ha-add-to-action-list")
|
||||
class HaAddToActionList extends LitElement {
|
||||
@state()
|
||||
@consumeLocalize()
|
||||
private _localize!: LocalizeFunc;
|
||||
|
||||
@property({ attribute: false })
|
||||
public sections: readonly AddToActionListSection[] = [];
|
||||
|
||||
protected render(): TemplateResult | typeof nothing {
|
||||
if (!this.sections.length) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`${this.sections.map((section, sectionIndex) =>
|
||||
this._renderSection(section, sectionIndex)
|
||||
)}`;
|
||||
}
|
||||
|
||||
private _renderSection(
|
||||
section: AddToActionListSection,
|
||||
sectionIndex: number
|
||||
): TemplateResult | typeof nothing {
|
||||
if (!section.actions.length && !section.empty && !section.emptyKey) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`
|
||||
<h3 class="section-header">
|
||||
${this._localizeValue(section.title, section.titleKey)}
|
||||
</h3>
|
||||
${section.actions.length
|
||||
? html`<ha-list-base>
|
||||
${section.actions.map((action, actionIndex) =>
|
||||
this._renderActionItem(action, sectionIndex, actionIndex)
|
||||
)}
|
||||
</ha-list-base>`
|
||||
: html`<h4 class="empty">
|
||||
${this._localizeValue(section.empty, section.emptyKey)}
|
||||
</h4>`}
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderActionItem(
|
||||
action: AddToActionListItem,
|
||||
sectionIndex: number,
|
||||
actionIndex: number
|
||||
): TemplateResult {
|
||||
return html`
|
||||
<ha-list-item-button
|
||||
.disabled=${action.enabled === false}
|
||||
data-section-index=${sectionIndex}
|
||||
data-action-index=${actionIndex}
|
||||
.headline=${this._localizeValue(action.name, action.nameKey)}
|
||||
.supportingText=${this._localizeValue(
|
||||
action.description,
|
||||
action.descriptionKey
|
||||
)}
|
||||
@click=${this._actionSelected}
|
||||
>
|
||||
${action.icon
|
||||
? html`<ha-icon
|
||||
class="start-icon"
|
||||
slot="start"
|
||||
.icon=${action.icon}
|
||||
></ha-icon>`
|
||||
: action.iconPath
|
||||
? html`<ha-svg-icon
|
||||
class="start-icon"
|
||||
slot="start"
|
||||
.path=${action.iconPath}
|
||||
></ha-svg-icon>`
|
||||
: nothing}
|
||||
<ha-svg-icon class="plus" slot="end" .path=${mdiPlus}></ha-svg-icon>
|
||||
</ha-list-item-button>
|
||||
`;
|
||||
}
|
||||
|
||||
private _localizeValue(
|
||||
value?: string,
|
||||
localizeKey?: LocalizeKeys
|
||||
): string | undefined {
|
||||
return value || (localizeKey ? this._localize(localizeKey) : undefined);
|
||||
}
|
||||
|
||||
private _actionSelected(
|
||||
ev: HASSDomCurrentTargetEvent<HaListItemButton>
|
||||
): void {
|
||||
const action =
|
||||
this.sections[Number(ev.currentTarget.dataset.sectionIndex)]?.actions[
|
||||
Number(ev.currentTarget.dataset.actionIndex)
|
||||
];
|
||||
|
||||
if (!action) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (action.enabled === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
fireEvent(this, "add-to-list-action-selected", {
|
||||
action,
|
||||
});
|
||||
}
|
||||
|
||||
static styles: CSSResultGroup = css`
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
padding: var(--ha-space-2) var(--ha-space-6) var(--ha-space-1);
|
||||
margin: 0;
|
||||
font-size: var(--ha-font-size-m);
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
.empty {
|
||||
padding: var(--ha-space-2) var(--ha-space-6) var(--ha-space-1);
|
||||
margin: 0;
|
||||
font-size: var(--ha-font-size-m);
|
||||
font-weight: var(--ha-font-weight-normal);
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
ha-list-item-button {
|
||||
--ha-row-item-padding-inline: var(--ha-space-5);
|
||||
}
|
||||
|
||||
ha-icon,
|
||||
ha-svg-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.start-icon {
|
||||
color: var(--ha-color-text-secondary);
|
||||
}
|
||||
|
||||
.plus {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
ha-list-item-button[disabled] .start-icon,
|
||||
ha-list-item-button[disabled] .plus {
|
||||
color: var(--disabled-text-color);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-add-to-action-list": HaAddToActionList;
|
||||
}
|
||||
|
||||
interface HASSDomEvents {
|
||||
"add-to-list-action-selected": AddToActionListActionSelectedDetail;
|
||||
}
|
||||
}
|
||||
@@ -1,26 +1,35 @@
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import { LitElement, css, html } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import "../../components/ha-alert";
|
||||
import "../../components/ha-icon";
|
||||
import "../../components/ha-spinner";
|
||||
import "../../components/item/ha-list-item-button";
|
||||
import "../../components/list/ha-list-base";
|
||||
import type { HaListItemButton } from "../../components/item/ha-list-item-button";
|
||||
import { showToast } from "../../util/toast";
|
||||
|
||||
import type { HASSDomCurrentTargetEvent } from "../../common/dom/fire_event";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { configContext } from "../../data/context";
|
||||
import "../add-to/ha-add-to-action-list";
|
||||
import type {
|
||||
AddToActionListActionSelectedEvent,
|
||||
AddToActionListSection,
|
||||
} from "../add-to/ha-add-to-action-list";
|
||||
import {
|
||||
type EntityAddToAction,
|
||||
type EntityAddToActions,
|
||||
addToActionHandler,
|
||||
getDefaultAddToActions,
|
||||
} from "./add-to";
|
||||
} from "../add-to/add-to";
|
||||
import { consumeLocalize } from "../../common/decorators/consume-context-entry";
|
||||
import type { LocalizeFunc } from "../../common/translations/localize";
|
||||
|
||||
@customElement("ha-more-info-add-to")
|
||||
export class HaMoreInfoAddTo extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@state()
|
||||
@consume({ context: configContext, subscribe: true })
|
||||
private _config?: ContextType<typeof configContext>;
|
||||
|
||||
@state()
|
||||
@consumeLocalize()
|
||||
private _localize!: LocalizeFunc;
|
||||
|
||||
@property({ attribute: false }) public entityId!: string;
|
||||
|
||||
@@ -31,18 +40,13 @@ export class HaMoreInfoAddTo extends LitElement {
|
||||
@state() private _loading = true;
|
||||
|
||||
private async _loadActions() {
|
||||
this._defaultActions = getDefaultAddToActions(
|
||||
this.hass.states,
|
||||
this.hass.localize,
|
||||
this.hass.formatEntityName,
|
||||
this.entityId
|
||||
);
|
||||
this._defaultActions = getDefaultAddToActions();
|
||||
this._externalActions = [];
|
||||
|
||||
if (this.hass.auth.external?.config.hasEntityAddTo) {
|
||||
if (this._config?.auth.external?.config.hasEntityAddTo) {
|
||||
try {
|
||||
const response =
|
||||
await this.hass.auth.external?.sendMessage<"entity/add_to/get_actions">(
|
||||
await this._config.auth.external.sendMessage<"entity/add_to/get_actions">(
|
||||
{
|
||||
type: "entity/add_to/get_actions",
|
||||
payload: { entity_id: this.entityId },
|
||||
@@ -66,13 +70,9 @@ export class HaMoreInfoAddTo extends LitElement {
|
||||
}
|
||||
|
||||
private async _actionSelected(
|
||||
ev: HASSDomCurrentTargetEvent<
|
||||
HaListItemButton & {
|
||||
action: EntityAddToAction;
|
||||
}
|
||||
>
|
||||
ev: AddToActionListActionSelectedEvent<EntityAddToAction>
|
||||
) {
|
||||
const action = ev.currentTarget.action;
|
||||
const { action } = ev.detail;
|
||||
if (!action.enabled) {
|
||||
return;
|
||||
}
|
||||
@@ -82,7 +82,10 @@ export class HaMoreInfoAddTo extends LitElement {
|
||||
if (!action.payload) {
|
||||
throw new Error("Missing external action payload");
|
||||
}
|
||||
this.hass.auth.external!.fireMessage({
|
||||
if (!this._config?.auth.external) {
|
||||
throw new Error("Missing external app connection");
|
||||
}
|
||||
this._config.auth.external.fireMessage({
|
||||
type: "entity/add_to",
|
||||
payload: {
|
||||
entity_id: this.entityId,
|
||||
@@ -92,7 +95,7 @@ export class HaMoreInfoAddTo extends LitElement {
|
||||
fireEvent(this, "add-to-action-selected");
|
||||
} catch (err: unknown) {
|
||||
showToast(this, {
|
||||
message: this.hass.localize(
|
||||
message: this._localize(
|
||||
"ui.dialogs.more_info_control.add_to.action_failed",
|
||||
{
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
@@ -110,24 +113,6 @@ export class HaMoreInfoAddTo extends LitElement {
|
||||
addToActionHandler(action.key, { entity_id: this.entityId });
|
||||
}
|
||||
|
||||
private _renderActionItems(actions: EntityAddToActions) {
|
||||
return actions.map(
|
||||
(action) => html`
|
||||
<ha-list-item-button
|
||||
.disabled=${!action.enabled}
|
||||
.action=${action}
|
||||
@click=${this._actionSelected}
|
||||
>
|
||||
<ha-icon slot="start" .icon=${action.icon}></ha-icon>
|
||||
<span slot="headline">${action.name}</span>
|
||||
${action.description
|
||||
? html`<span slot="supporting-text">${action.description}</span>`
|
||||
: nothing}
|
||||
</ha-list-item-button>
|
||||
`
|
||||
);
|
||||
}
|
||||
|
||||
protected async firstUpdated() {
|
||||
await this._loadActions();
|
||||
this._loading = false;
|
||||
@@ -145,29 +130,38 @@ export class HaMoreInfoAddTo extends LitElement {
|
||||
if (!this._defaultActions.length && !this._externalActions.length) {
|
||||
return html`
|
||||
<ha-alert alert-type="info">
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.add_to.no_actions"
|
||||
)}
|
||||
${this._localize("ui.dialogs.more_info_control.add_to.no_actions")}
|
||||
</ha-alert>
|
||||
`;
|
||||
}
|
||||
|
||||
const automationActions = this._defaultActions.filter(
|
||||
(action) => action.type === "default" && action.key !== "script_action"
|
||||
);
|
||||
const scriptActions = this._defaultActions.filter(
|
||||
(action) => action.type === "default" && action.key === "script_action"
|
||||
);
|
||||
|
||||
const sections: AddToActionListSection<EntityAddToAction>[] = [
|
||||
{
|
||||
titleKey: "ui.dialogs.more_info_control.add_to.automations_heading",
|
||||
actions: automationActions,
|
||||
},
|
||||
{
|
||||
titleKey: "ui.dialogs.more_info_control.add_to.scripts_heading",
|
||||
actions: scriptActions,
|
||||
},
|
||||
{
|
||||
titleKey: "ui.dialogs.more_info_control.add_to.app_actions",
|
||||
actions: this._externalActions,
|
||||
},
|
||||
];
|
||||
|
||||
return html`
|
||||
<ha-list-base>
|
||||
${this._renderActionItems(this._defaultActions)}
|
||||
</ha-list-base>
|
||||
${this._externalActions.length
|
||||
? html`
|
||||
<h2 class="section-title">
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.add_to.app_actions"
|
||||
)}
|
||||
</h2>
|
||||
<ha-list-base>
|
||||
${this._renderActionItems(this._externalActions)}
|
||||
</ha-list-base>
|
||||
`
|
||||
: nothing}
|
||||
<ha-add-to-action-list
|
||||
.sections=${sections}
|
||||
@add-to-list-action-selected=${this._actionSelected}
|
||||
></ha-add-to-action-list>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -183,20 +177,6 @@ export class HaMoreInfoAddTo extends LitElement {
|
||||
align-items: center;
|
||||
padding: var(--ha-space-8);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
padding: 0 var(--ha-space-6);
|
||||
margin: var(--ha-space-4) 0 var(--ha-space-1);
|
||||
font-size: var(--ha-font-size-m);
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
line-height: var(--ha-line-height-normal);
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
ha-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ import { customElement, property, query, state } from "lit/decorators";
|
||||
import { cache } from "lit/directives/cache";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { keyed } from "lit/directives/keyed";
|
||||
import type { RequestSelectedDetail } from "@material/mwc-list/mwc-list-item";
|
||||
import { dynamicElement } from "../../common/dom/dynamic-element-directive";
|
||||
import type { HASSDomEvent } from "../../common/dom/fire_event";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
@@ -517,7 +518,7 @@ export class MoreInfoDialog extends SubscribeMixin(
|
||||
await favoritesHandler.copy(favoritesContext);
|
||||
}
|
||||
|
||||
private _goToAddEntityTo(ev) {
|
||||
private _goToAddEntityTo(ev: CustomEvent<RequestSelectedDetail>) {
|
||||
// Only check for request-selected events (from menu items), not regular clicks (from icon button)
|
||||
if (
|
||||
ev.type === "request-selected" &&
|
||||
@@ -590,10 +591,19 @@ export class MoreInfoDialog extends SubscribeMixin(
|
||||
(v): v is string => Boolean(v)
|
||||
);
|
||||
const defaultTitle = breadcrumb.pop() || entityId;
|
||||
const addToTitle = this.hass.localize(
|
||||
"ui.dialogs.more_info_control.add_to.title",
|
||||
{ target: defaultTitle }
|
||||
);
|
||||
const addToMenuItem = this.hass.localize(
|
||||
"ui.dialogs.more_info_control.add_to.item"
|
||||
);
|
||||
const title =
|
||||
this._currView === "details"
|
||||
? this.hass.localize("ui.dialogs.more_info_control.details")
|
||||
: this._childView?.viewTitle || defaultTitle;
|
||||
: this._currView === "add_to"
|
||||
? addToTitle
|
||||
: this._childView?.viewTitle || defaultTitle;
|
||||
|
||||
const favoritesContext =
|
||||
this._entry && stateObj
|
||||
@@ -711,9 +721,7 @@ export class MoreInfoDialog extends SubscribeMixin(
|
||||
slot="icon"
|
||||
.path=${mdiPlusBoxMultipleOutline}
|
||||
></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.add_to.title"
|
||||
)}
|
||||
${addToMenuItem}
|
||||
</ha-dropdown-item>
|
||||
|
||||
<wa-divider></wa-divider>
|
||||
@@ -814,9 +822,7 @@ export class MoreInfoDialog extends SubscribeMixin(
|
||||
? html`
|
||||
<ha-icon-button
|
||||
slot="headerActionItems"
|
||||
.label=${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.add_to.title"
|
||||
)}
|
||||
.label=${addToMenuItem}
|
||||
.path=${mdiPlusBoxMultipleOutline}
|
||||
@click=${this._goToAddEntityTo}
|
||||
></ha-icon-button>
|
||||
@@ -906,7 +912,6 @@ export class MoreInfoDialog extends SubscribeMixin(
|
||||
: this._currView === "add_to"
|
||||
? html`
|
||||
<ha-more-info-add-to
|
||||
.hass=${this.hass}
|
||||
.entityId=${entityId}
|
||||
@add-to-action-selected=${this._goBack}
|
||||
></ha-more-info-add-to>
|
||||
|
||||
@@ -1,39 +1,20 @@
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import type { LocalizeFunc } from "../../common/translations/localize";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { customElement } from "lit/decorators";
|
||||
import "../../components/ha-dialog";
|
||||
import { DialogMixin } from "../../dialogs/dialog-mixin";
|
||||
import type { AppDialogParams } from "./show-app-dialog";
|
||||
|
||||
@customElement("app-dialog")
|
||||
class DialogApp extends LitElement {
|
||||
@property({ attribute: false }) public localize?: LocalizeFunc;
|
||||
|
||||
@state() private _open = false;
|
||||
|
||||
public async showDialog(params: { localize: LocalizeFunc }): Promise<void> {
|
||||
this.localize = params.localize;
|
||||
this._open = true;
|
||||
}
|
||||
|
||||
public closeDialog(): void {
|
||||
this._open = false;
|
||||
}
|
||||
|
||||
private _dialogClosed(): void {
|
||||
this.localize = undefined;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
class DialogApp extends DialogMixin<AppDialogParams>(LitElement) {
|
||||
protected render() {
|
||||
if (!this.localize) {
|
||||
if (!this.params?.localize) {
|
||||
return nothing;
|
||||
}
|
||||
return html`<ha-dialog
|
||||
.open=${this._open}
|
||||
header-title=${this.localize(
|
||||
open
|
||||
header-title=${this.params.localize(
|
||||
"ui.panel.page-onboarding.welcome.download_app"
|
||||
) || "Click here to download the app"}
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
<div>
|
||||
<div class="app-qr">
|
||||
@@ -45,13 +26,17 @@ class DialogApp extends LitElement {
|
||||
<img
|
||||
loading="lazy"
|
||||
src="/static/images/appstore.svg"
|
||||
alt=${this.localize("ui.panel.page-onboarding.welcome.appstore")}
|
||||
alt=${this.params.localize(
|
||||
"ui.panel.page-onboarding.welcome.appstore"
|
||||
)}
|
||||
class="icon"
|
||||
/>
|
||||
<img
|
||||
loading="lazy"
|
||||
src="/static/images/qr-appstore.svg"
|
||||
alt=${this.localize("ui.panel.page-onboarding.welcome.appstore")}
|
||||
alt=${this.params.localize(
|
||||
"ui.panel.page-onboarding.welcome.appstore"
|
||||
)}
|
||||
/>
|
||||
</a>
|
||||
<a
|
||||
@@ -62,13 +47,17 @@ class DialogApp extends LitElement {
|
||||
<img
|
||||
loading="lazy"
|
||||
src="/static/images/playstore.svg"
|
||||
alt=${this.localize("ui.panel.page-onboarding.welcome.playstore")}
|
||||
alt=${this.params.localize(
|
||||
"ui.panel.page-onboarding.welcome.playstore"
|
||||
)}
|
||||
class="icon"
|
||||
/>
|
||||
<img
|
||||
loading="lazy"
|
||||
src="/static/images/qr-playstore.svg"
|
||||
alt=${this.localize("ui.panel.page-onboarding.welcome.playstore")}
|
||||
alt=${this.params.localize(
|
||||
"ui.panel.page-onboarding.welcome.playstore"
|
||||
)}
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -1,103 +1,87 @@
|
||||
import { mdiAccountGroup, mdiOpenInNew } from "@mdi/js";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { LocalizeFunc } from "../../common/translations/localize";
|
||||
import { customElement } from "lit/decorators";
|
||||
import "../../components/ha-dialog";
|
||||
import "../../components/ha-list";
|
||||
import "../../components/ha-list-item";
|
||||
import "../../components/ha-svg-icon";
|
||||
import "../../components/item/ha-list-item-button";
|
||||
import "../../components/list/ha-list-nav";
|
||||
import { DialogMixin } from "../../dialogs/dialog-mixin";
|
||||
import type { CommunityDialogParams } from "./show-community-dialog";
|
||||
|
||||
@customElement("community-dialog")
|
||||
class DialogCommunity extends LitElement {
|
||||
@property({ attribute: false }) public localize?: LocalizeFunc;
|
||||
|
||||
@state() private _open = false;
|
||||
|
||||
public async showDialog(params): Promise<void> {
|
||||
this.localize = params.localize;
|
||||
this._open = true;
|
||||
}
|
||||
|
||||
public closeDialog(): void {
|
||||
this._open = false;
|
||||
}
|
||||
|
||||
private _dialogClosed(): void {
|
||||
this.localize = undefined;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
class DialogCommunity extends DialogMixin<CommunityDialogParams>(LitElement) {
|
||||
protected render() {
|
||||
if (!this.localize) {
|
||||
if (!this.params?.localize) {
|
||||
return nothing;
|
||||
}
|
||||
return html`<ha-dialog
|
||||
.open=${this._open}
|
||||
header-title=${this.localize(
|
||||
open
|
||||
header-title=${this.params.localize(
|
||||
"ui.panel.page-onboarding.welcome.community"
|
||||
)}
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
<ha-list>
|
||||
<a
|
||||
<ha-list-nav>
|
||||
<ha-list-item-button
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
href="https://community.home-assistant.io/"
|
||||
>
|
||||
<ha-list-item hasMeta graphic="icon">
|
||||
<img
|
||||
src="/static/icons/favicon-192x192.png"
|
||||
slot="graphic"
|
||||
alt="Home Assistant Logo"
|
||||
/>
|
||||
${this.localize("ui.panel.page-onboarding.welcome.forums")}
|
||||
<ha-svg-icon slot="meta" .path=${mdiOpenInNew}></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
</a>
|
||||
<a
|
||||
<img
|
||||
src="/static/icons/favicon-192x192.png"
|
||||
slot="start"
|
||||
alt="Home Assistant Logo"
|
||||
/>
|
||||
<span slot="headline">
|
||||
${this.params.localize("ui.panel.page-onboarding.welcome.forums")}
|
||||
</span>
|
||||
<ha-svg-icon slot="end" .path=${mdiOpenInNew}></ha-svg-icon>
|
||||
</ha-list-item-button>
|
||||
<ha-list-item-button
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
href="https://newsletter.openhomefoundation.org/"
|
||||
>
|
||||
<ha-list-item hasMeta graphic="icon">
|
||||
<img
|
||||
src="/static/icons/logo_ohf.svg"
|
||||
slot="graphic"
|
||||
alt="Open Home Foundation Logo"
|
||||
/>
|
||||
${this.localize(
|
||||
<img
|
||||
src="/static/icons/logo_ohf.svg"
|
||||
slot="start"
|
||||
alt="Open Home Foundation Logo"
|
||||
/>
|
||||
<span slot="headline">
|
||||
${this.params.localize(
|
||||
"ui.panel.page-onboarding.welcome.open_home_newsletter"
|
||||
)}
|
||||
<ha-svg-icon slot="meta" .path=${mdiOpenInNew}></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
</a>
|
||||
<a
|
||||
</span>
|
||||
<ha-svg-icon slot="end" .path=${mdiOpenInNew}></ha-svg-icon>
|
||||
</ha-list-item-button>
|
||||
<ha-list-item-button
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
href="https://www.home-assistant.io/join-chat"
|
||||
>
|
||||
<ha-list-item hasMeta graphic="icon">
|
||||
<img
|
||||
src="/static/images/logo_discord.png"
|
||||
slot="graphic"
|
||||
alt="Discord Logo"
|
||||
/>
|
||||
${this.localize("ui.panel.page-onboarding.welcome.discord")}
|
||||
<ha-svg-icon slot="meta" .path=${mdiOpenInNew}></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
</a>
|
||||
<a
|
||||
<img
|
||||
src="/static/images/logo_discord.png"
|
||||
slot="start"
|
||||
alt="Discord Logo"
|
||||
/>
|
||||
<span slot="headline">
|
||||
${this.params.localize("ui.panel.page-onboarding.welcome.discord")}
|
||||
</span>
|
||||
<ha-svg-icon slot="end" .path=${mdiOpenInNew}></ha-svg-icon>
|
||||
</ha-list-item-button>
|
||||
<ha-list-item-button
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
href="https://fosstodon.org/@homeassistant"
|
||||
>
|
||||
<ha-list-item hasMeta graphic="icon">
|
||||
<ha-svg-icon .path=${mdiAccountGroup} slot="graphic"></ha-svg-icon>
|
||||
${this.localize("ui.panel.page-onboarding.welcome.social_media")}
|
||||
<ha-svg-icon slot="meta" .path=${mdiOpenInNew}></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
</a>
|
||||
</ha-list>
|
||||
<ha-svg-icon .path=${mdiAccountGroup} slot="start"></ha-svg-icon>
|
||||
<span slot="headline">
|
||||
${this.params.localize(
|
||||
"ui.panel.page-onboarding.welcome.social_media"
|
||||
)}
|
||||
</span>
|
||||
<ha-svg-icon slot="end" .path=${mdiOpenInNew}></ha-svg-icon>
|
||||
</ha-list-item-button>
|
||||
</ha-list-nav>
|
||||
</ha-dialog>`;
|
||||
}
|
||||
|
||||
@@ -105,12 +89,12 @@ class DialogCommunity extends LitElement {
|
||||
ha-dialog {
|
||||
--dialog-content-padding: 0;
|
||||
}
|
||||
ha-list-item {
|
||||
height: 56px;
|
||||
--mdc-list-item-meta-size: 20px;
|
||||
img {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
a {
|
||||
text-decoration: none;
|
||||
ha-svg-icon {
|
||||
color: var(--ha-color-text-secondary);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -3,13 +3,18 @@ import type { LocalizeFunc } from "../../common/translations/localize";
|
||||
|
||||
export const loadAppDialog = () => import("./app-dialog");
|
||||
|
||||
export interface AppDialogParams {
|
||||
localize: LocalizeFunc;
|
||||
}
|
||||
|
||||
export const showAppDialog = (
|
||||
element: HTMLElement,
|
||||
params: { localize: LocalizeFunc }
|
||||
params: AppDialogParams
|
||||
): void => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "app-dialog",
|
||||
dialogImport: loadAppDialog,
|
||||
dialogParams: params,
|
||||
addHistory: false,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -3,13 +3,18 @@ import type { LocalizeFunc } from "../../common/translations/localize";
|
||||
|
||||
export const loadCommunityDialog = () => import("./community-dialog");
|
||||
|
||||
export interface CommunityDialogParams {
|
||||
localize: LocalizeFunc;
|
||||
}
|
||||
|
||||
export const showCommunityDialog = (
|
||||
element: HTMLElement,
|
||||
params: { localize: LocalizeFunc }
|
||||
params: CommunityDialogParams
|
||||
): void => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "community-dialog",
|
||||
dialogImport: loadCommunityDialog,
|
||||
dialogParams: params,
|
||||
addHistory: false,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -198,10 +198,12 @@ class SupervisorAppInfo extends MobileAwareMixin(LitElement) {
|
||||
/>
|
||||
`
|
||||
: nothing}
|
||||
${getAppDisplayName(
|
||||
this._currentAddon.name,
|
||||
this._currentAddon.stage
|
||||
)}
|
||||
${!this.narrow
|
||||
? getAppDisplayName(
|
||||
this._currentAddon.name,
|
||||
this._currentAddon.stage
|
||||
)
|
||||
: nothing}
|
||||
<div class="description">
|
||||
${this._currentAddon.version
|
||||
? html`
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import "@home-assistant/webawesome/dist/components/tag/tag";
|
||||
import { mdiHelpCircleOutline } from "@mdi/js";
|
||||
import { mdiCheckCircle, mdiHelpCircleOutline } from "@mdi/js";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
@@ -25,7 +25,9 @@ class SupervisorAppsCardContent extends LitElement {
|
||||
|
||||
@property() public stage: AddonStage = "stable";
|
||||
|
||||
@property() public state: AddonState = null;
|
||||
@property() public state?: AddonState;
|
||||
|
||||
@property({ type: Boolean }) public installed = false;
|
||||
|
||||
@property() public description?: string;
|
||||
|
||||
@@ -77,13 +79,23 @@ class SupervisorAppsCardContent extends LitElement {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
${this.tags?.length || this.state
|
||||
${this.tags?.length || this.state !== undefined || this.installed
|
||||
? html`
|
||||
<div class="footer">
|
||||
<supervisor-apps-state
|
||||
.state=${this.state || "unknown"}
|
||||
></supervisor-apps-state>
|
||||
|
||||
${this.state !== undefined
|
||||
? html`<supervisor-apps-state
|
||||
.state=${this.state || "unknown"}
|
||||
></supervisor-apps-state>`
|
||||
: this.installed
|
||||
? html`<div class="installed">
|
||||
<ha-svg-icon .path=${mdiCheckCircle}></ha-svg-icon>
|
||||
<span
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.apps.state.installed"
|
||||
)}</span
|
||||
>
|
||||
</div>`
|
||||
: html`<span></span>`}
|
||||
${this.tags?.length
|
||||
? html`<div class="tags">
|
||||
${this.tags.map(
|
||||
@@ -159,6 +171,17 @@ class SupervisorAppsCardContent extends LitElement {
|
||||
display: flex;
|
||||
gap: var(--ha-space-2);
|
||||
}
|
||||
.installed {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--ha-space-2);
|
||||
color: var(--ha-color-text-secondary);
|
||||
font-size: var(--ha-font-size-m);
|
||||
}
|
||||
.installed ha-svg-icon {
|
||||
--mdc-icon-size: 16px;
|
||||
color: var(--ha-color-on-success-normal);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
import { mdiArrowUpBoldCircle, mdiPuzzle } from "@mdi/js";
|
||||
import {
|
||||
mdiAlertDecagramOutline,
|
||||
mdiArrowUpBoldCircle,
|
||||
mdiArrowUpBoldCircleOutline,
|
||||
mdiFlask,
|
||||
mdiPuzzle,
|
||||
} from "@mdi/js";
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { navigate } from "../../../common/navigate";
|
||||
import { caseInsensitiveStringCompare } from "../../../common/string/compare";
|
||||
@@ -10,6 +17,7 @@ import type { HassioAddonRepository } from "../../../data/hassio/addon";
|
||||
import type { StoreAddon } from "../../../data/supervisor/store";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import "./components/supervisor-apps-card-content";
|
||||
import type { AppTag } from "./components/supervisor-apps-card-content";
|
||||
import { filterAndSort } from "./components/supervisor-apps-filter";
|
||||
import { supervisorAppsStyle } from "./resources/supervisor-apps-style";
|
||||
|
||||
@@ -54,21 +62,29 @@ export class SupervisorAppsRepositoryEl extends LitElement {
|
||||
<div class="content">
|
||||
<h1>${repo.name}</h1>
|
||||
<div class="card-group">
|
||||
${addons.map(
|
||||
(addon) => html`
|
||||
${addons.map((addon) => {
|
||||
const tags = this._getAppTags(addon);
|
||||
return html`
|
||||
<ha-card
|
||||
outlined
|
||||
.addon=${addon}
|
||||
class=${addon.available ? "" : "not_available"}
|
||||
@click=${this._addonTapped}
|
||||
>
|
||||
<div class="card-content">
|
||||
<div
|
||||
class=${classMap({
|
||||
"card-content": true,
|
||||
"has-footer": tags.length > 0 || addon.installed,
|
||||
})}
|
||||
>
|
||||
<supervisor-apps-card-content
|
||||
.hass=${this.hass}
|
||||
.title=${addon.name}
|
||||
.stage=${addon.stage}
|
||||
.description=${addon.description}
|
||||
.available=${addon.available}
|
||||
.installed=${addon.installed}
|
||||
.tags=${tags}
|
||||
.icon=${addon.installed && addon.update_available
|
||||
? mdiArrowUpBoldCircle
|
||||
: mdiPuzzle}
|
||||
@@ -108,8 +124,8 @@ export class SupervisorAppsRepositoryEl extends LitElement {
|
||||
></supervisor-apps-card-content>
|
||||
</div>
|
||||
</ha-card>
|
||||
`
|
||||
)}
|
||||
`;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -119,6 +135,32 @@ export class SupervisorAppsRepositoryEl extends LitElement {
|
||||
navigate(`/config/app/${ev.currentTarget.addon.slug}/info?store=true`);
|
||||
}
|
||||
|
||||
private _getAppTags(addon: StoreAddon): AppTag[] {
|
||||
const labels: AppTag[] = [];
|
||||
|
||||
if (addon.installed && addon.update_available) {
|
||||
labels.push({
|
||||
label: this.hass.localize(
|
||||
`ui.panel.config.apps.state.update_available`
|
||||
),
|
||||
variant: "brand",
|
||||
iconPath: mdiArrowUpBoldCircleOutline,
|
||||
});
|
||||
}
|
||||
if (addon.stage !== "stable") {
|
||||
labels.push({
|
||||
label: this.hass.localize(
|
||||
`ui.panel.config.apps.dashboard.capability.stages.${addon.stage}`
|
||||
),
|
||||
variant: addon.stage === "experimental" ? "warning" : "danger",
|
||||
iconPath:
|
||||
addon.stage === "experimental" ? mdiFlask : mdiAlertDecagramOutline,
|
||||
});
|
||||
}
|
||||
|
||||
return labels;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
supervisorAppsStyle,
|
||||
@@ -127,6 +169,9 @@ export class SupervisorAppsRepositoryEl extends LitElement {
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
}
|
||||
.card-content.has-footer {
|
||||
padding: var(--ha-space-4) var(--ha-space-4) var(--ha-space-2);
|
||||
}
|
||||
.not_available {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
@@ -12,22 +12,32 @@ import {
|
||||
import { computeAreaName } from "../../../common/entity/compute_area_name";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import "../../../components/ha-adaptive-dialog";
|
||||
import "../../../components/ha-list";
|
||||
import "../../../components/ha-list-item";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import {
|
||||
areasContext,
|
||||
internationalizationContext,
|
||||
} from "../../../data/context";
|
||||
import type { SceneEntities } from "../../../data/scene";
|
||||
import { showSceneEditor } from "../../../data/scene";
|
||||
import "../../../dialogs/add-to/ha-add-to-action-list";
|
||||
import type {
|
||||
AddToActionListActionSelectedEvent,
|
||||
AddToActionListItem,
|
||||
AddToActionListSection,
|
||||
} from "../../../dialogs/add-to/ha-add-to-action-list";
|
||||
import {
|
||||
addToActionHandler,
|
||||
type AddToActionKey,
|
||||
} from "../../../dialogs/more-info/add-to";
|
||||
createAddToSceneEntities,
|
||||
type AddToAutomationScriptActionKey,
|
||||
} from "../../../dialogs/add-to/add-to";
|
||||
import { haStyle, haStyleDialog } from "../../../resources/styles";
|
||||
import type { AreaAddToDialogParams } from "./show-dialog-area-add-to";
|
||||
|
||||
type AreaAddToAction =
|
||||
| (AddToActionListItem & {
|
||||
type: "automation";
|
||||
key: AddToAutomationScriptActionKey;
|
||||
})
|
||||
| (AddToActionListItem & { type: "scene" });
|
||||
|
||||
@customElement("dialog-area-add-to")
|
||||
class DialogAreaAddTo extends LitElement {
|
||||
@state()
|
||||
@@ -65,7 +75,12 @@ class DialogAreaAddTo extends LitElement {
|
||||
<ha-adaptive-dialog
|
||||
.open=${this._open}
|
||||
header-title=${this._i18n.localize(
|
||||
"ui.dialogs.more_info_control.add_to.title"
|
||||
"ui.dialogs.more_info_control.add_to.title",
|
||||
{
|
||||
target:
|
||||
computeAreaName(this._areas[this._params.areaId]) ||
|
||||
this._params.areaId,
|
||||
}
|
||||
)}
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
@@ -79,108 +94,96 @@ class DialogAreaAddTo extends LitElement {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const area = this._areas[this._params.areaId];
|
||||
const areaName = computeAreaName(area) || this._params.areaId;
|
||||
|
||||
return html`
|
||||
<h3 class="section-header">
|
||||
${this._i18n.localize(
|
||||
const sections: AddToActionListSection<AreaAddToAction>[] = [
|
||||
{
|
||||
title: this._i18n.localize(
|
||||
"ui.panel.config.devices.automation.automations_heading"
|
||||
)}
|
||||
</h3>
|
||||
<ha-list>
|
||||
${this._renderActionItem(
|
||||
"automation_trigger",
|
||||
mdiRobotOutline,
|
||||
"ui.dialogs.more_info_control.add_to.actions.automation_trigger",
|
||||
areaName
|
||||
)}
|
||||
${this._renderActionItem(
|
||||
"automation_condition",
|
||||
mdiPlaylistCheck,
|
||||
"ui.dialogs.more_info_control.add_to.actions.automation_condition",
|
||||
areaName
|
||||
)}
|
||||
${this._renderActionItem(
|
||||
"automation_action",
|
||||
mdiPlayCircleOutline,
|
||||
"ui.dialogs.more_info_control.add_to.actions.automation_action",
|
||||
areaName
|
||||
)}
|
||||
</ha-list>
|
||||
<h3 class="section-header">
|
||||
${this._i18n.localize("ui.panel.config.devices.script.scripts_heading")}
|
||||
</h3>
|
||||
<ha-list>
|
||||
${this._renderActionItem(
|
||||
"script_action",
|
||||
mdiScriptTextOutline,
|
||||
"ui.dialogs.more_info_control.add_to.actions.script_action",
|
||||
areaName
|
||||
)}
|
||||
</ha-list>
|
||||
${this._renderSceneSection(areaName)}
|
||||
`;
|
||||
}
|
||||
),
|
||||
actions: [
|
||||
{
|
||||
type: "automation",
|
||||
key: "automation_trigger",
|
||||
iconPath: mdiRobotOutline,
|
||||
name: this._i18n.localize(
|
||||
"ui.dialogs.more_info_control.add_to.action_options.automation_trigger"
|
||||
),
|
||||
},
|
||||
{
|
||||
type: "automation",
|
||||
key: "automation_condition",
|
||||
iconPath: mdiPlaylistCheck,
|
||||
name: this._i18n.localize(
|
||||
"ui.dialogs.more_info_control.add_to.action_options.automation_condition"
|
||||
),
|
||||
},
|
||||
{
|
||||
type: "automation",
|
||||
key: "automation_action",
|
||||
iconPath: mdiPlayCircleOutline,
|
||||
name: this._i18n.localize(
|
||||
"ui.dialogs.more_info_control.add_to.action_options.automation_action"
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: this._i18n.localize(
|
||||
"ui.panel.config.devices.script.scripts_heading"
|
||||
),
|
||||
actions: [
|
||||
{
|
||||
type: "automation",
|
||||
key: "script_action",
|
||||
iconPath: mdiScriptTextOutline,
|
||||
name: this._i18n.localize(
|
||||
"ui.dialogs.more_info_control.add_to.action_options.script_action"
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
private _renderSceneSection(areaName: string) {
|
||||
if (!this._params?.entityIds.length) {
|
||||
return nothing;
|
||||
if (this._params.canCreateScene && this._params.entityIds.length) {
|
||||
sections.push({
|
||||
title: this._i18n.localize(
|
||||
"ui.panel.config.devices.scene.scenes_heading"
|
||||
),
|
||||
actions: [
|
||||
{
|
||||
type: "scene",
|
||||
iconPath: mdiPalette,
|
||||
name: this._i18n.localize(
|
||||
"ui.dialogs.more_info_control.add_to.action_options.scene"
|
||||
),
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
return html`
|
||||
<h3 class="section-header">
|
||||
${this._i18n.localize("ui.panel.config.devices.scene.scenes_heading")}
|
||||
</h3>
|
||||
<ha-list>
|
||||
<ha-list-item
|
||||
graphic="icon"
|
||||
@click=${this._handleCreateScene}
|
||||
data-dialog="close"
|
||||
>
|
||||
<ha-svg-icon slot="graphic" .path=${mdiPalette}></ha-svg-icon>
|
||||
${this._i18n.localize(
|
||||
"ui.dialogs.more_info_control.add_to.actions.scene",
|
||||
{ target: areaName }
|
||||
)}
|
||||
</ha-list-item>
|
||||
</ha-list>
|
||||
<ha-add-to-action-list
|
||||
.sections=${sections}
|
||||
@add-to-list-action-selected=${this._handleActionSelected}
|
||||
></ha-add-to-action-list>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderActionItem(
|
||||
key: AddToActionKey,
|
||||
path: string,
|
||||
translationKey:
|
||||
| "ui.dialogs.more_info_control.add_to.actions.automation_trigger"
|
||||
| "ui.dialogs.more_info_control.add_to.actions.automation_condition"
|
||||
| "ui.dialogs.more_info_control.add_to.actions.automation_action"
|
||||
| "ui.dialogs.more_info_control.add_to.actions.script_action",
|
||||
areaName: string
|
||||
private _handleActionSelected(
|
||||
ev: AddToActionListActionSelectedEvent<AreaAddToAction>
|
||||
) {
|
||||
return html`
|
||||
<ha-list-item
|
||||
graphic="icon"
|
||||
data-type=${key}
|
||||
@click=${this._handleAction}
|
||||
data-dialog="close"
|
||||
>
|
||||
<ha-svg-icon slot="graphic" .path=${path}></ha-svg-icon>
|
||||
${this._i18n.localize(translationKey, { target: areaName })}
|
||||
</ha-list-item>
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleAction(ev: Event) {
|
||||
if (!this._params) {
|
||||
return;
|
||||
}
|
||||
|
||||
const key = (ev.currentTarget as HTMLElement).dataset
|
||||
.type as AddToActionKey;
|
||||
const { action } = ev.detail;
|
||||
|
||||
if (action.type === "scene") {
|
||||
this._handleCreateScene();
|
||||
return;
|
||||
}
|
||||
|
||||
this.closeDialog();
|
||||
addToActionHandler(key, { area_id: this._params.areaId });
|
||||
addToActionHandler(action.key, { area_id: this._params.areaId });
|
||||
}
|
||||
|
||||
private _handleCreateScene() {
|
||||
@@ -188,13 +191,11 @@ class DialogAreaAddTo extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
const entities: SceneEntities = {};
|
||||
for (const entityId of this._params.entityIds) {
|
||||
entities[entityId] = "";
|
||||
}
|
||||
|
||||
this.closeDialog();
|
||||
showSceneEditor({ entities }, this._params.areaId);
|
||||
showSceneEditor(
|
||||
{ entities: createAddToSceneEntities(this._params.entityIds) },
|
||||
this._params.areaId
|
||||
);
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
@@ -205,14 +206,6 @@ class DialogAreaAddTo extends LitElement {
|
||||
ha-adaptive-dialog {
|
||||
--dialog-content-padding: 0;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
padding: var(--ha-space-2) var(--ha-space-4) 0;
|
||||
margin: 0;
|
||||
font-size: var(--ha-font-size-m);
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -60,6 +60,7 @@ import type { SceneEntity } from "../../../data/scene";
|
||||
import type { ScriptEntity } from "../../../data/script";
|
||||
import type { RelatedResult } from "../../../data/search";
|
||||
import { findRelated } from "../../../data/search";
|
||||
import { filterAddToSceneEntityIds } from "../../../dialogs/add-to/add-to";
|
||||
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||
import { showMoreInfoDialog } from "../../../dialogs/more-info/show-ha-more-info-dialog";
|
||||
import "../../../layouts/hass-error-screen";
|
||||
@@ -439,7 +440,7 @@ class HaConfigAreaPage extends SubscribeMixin(LitElement) {
|
||||
.path=${mdiPlus}
|
||||
></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.add_to.title"
|
||||
"ui.dialogs.more_info_control.add_to.item"
|
||||
)}
|
||||
</ha-button>`
|
||||
: nothing}
|
||||
@@ -781,9 +782,17 @@ class HaConfigAreaPage extends SubscribeMixin(LitElement) {
|
||||
if (!area) {
|
||||
return;
|
||||
}
|
||||
const sceneEntityIds = filterAddToSceneEntityIds(
|
||||
this._areaEntityIds,
|
||||
this._entityReg,
|
||||
this.hass.states
|
||||
);
|
||||
showAreaAddToDialog(this, {
|
||||
areaId: area.area_id,
|
||||
entityIds: this._areaEntityIds,
|
||||
entityIds: sceneEntityIds,
|
||||
canCreateScene:
|
||||
isComponentLoaded(this.hass.config, "scene") &&
|
||||
sceneEntityIds.length > 0,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { fireEvent } from "../../../common/dom/fire_event";
|
||||
export interface AreaAddToDialogParams {
|
||||
areaId: string;
|
||||
entityIds: string[];
|
||||
canCreateScene: boolean;
|
||||
}
|
||||
|
||||
export const loadAreaAddToDialog = () => import("./dialog-area-add-to");
|
||||
|
||||
@@ -52,6 +52,7 @@ import type { HaDropdownSelectEvent } from "../../../../components/ha-dropdown";
|
||||
import "../../../../components/ha-dropdown-item";
|
||||
import "../../../../components/ha-expansion-panel";
|
||||
import "../../../../components/ha-icon-button";
|
||||
import "../../../../components/ha-tooltip";
|
||||
import type {
|
||||
AutomationClipboard,
|
||||
Condition,
|
||||
@@ -211,11 +212,27 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
);
|
||||
|
||||
return html`
|
||||
<ha-condition-icon
|
||||
slot="leading-icon"
|
||||
.hass=${this.hass}
|
||||
.condition=${this.condition.condition}
|
||||
></ha-condition-icon>
|
||||
<div id="condition-icon" class="icon-badge-wrapper" slot="leading-icon">
|
||||
<ha-condition-icon
|
||||
.hass=${this.hass}
|
||||
.condition=${this.condition.condition}
|
||||
></ha-condition-icon>
|
||||
${this.optionsInSidebar && this.condition.condition !== "trigger"
|
||||
? html`<ha-automation-row-live-test
|
||||
.state=${this._liveTestResult.state}
|
||||
.label=${this.hass.localize(
|
||||
`ui.panel.config.automation.editor.conditions.live_test_state.${this._liveTestResult.state}`
|
||||
)}
|
||||
></ha-automation-row-live-test>`
|
||||
: nothing}
|
||||
</div>
|
||||
${this.optionsInSidebar &&
|
||||
this.condition.condition !== "trigger" &&
|
||||
this._liveTestResult.message
|
||||
? html`<ha-tooltip for="condition-icon" slot="leading-icon"
|
||||
>${this._liveTestResult.message}</ha-tooltip
|
||||
>`
|
||||
: nothing}
|
||||
<h3 slot="header">
|
||||
${capitalizeFirstLetter(
|
||||
describeCondition(this.condition, this.hass, this._entityReg)
|
||||
@@ -531,17 +548,7 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
@click=${this._toggleSidebar}
|
||||
@toggle-collapsed=${this._toggleCollapse}
|
||||
>${this._renderRow()}
|
||||
<ha-automation-row-live-test
|
||||
slot="icons"
|
||||
.state=${this.condition.condition !== "trigger"
|
||||
? this._liveTestResult.state
|
||||
: "unknown"}
|
||||
.label=${this.hass.localize(
|
||||
`ui.panel.config.automation.editor.conditions.live_test_state.${this.condition.condition !== "trigger" ? this._liveTestResult.state : "unknown"}`
|
||||
)}
|
||||
.message=${this._liveTestResult.message}
|
||||
></ha-automation-row-live-test
|
||||
></ha-automation-row>`
|
||||
</ha-automation-row>`
|
||||
: html`
|
||||
<ha-expansion-panel
|
||||
left-chevron
|
||||
|
||||
@@ -53,6 +53,11 @@ export const rowStyles = css`
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.icon-badge-wrapper {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.note-indicator {
|
||||
color: var(--ha-color-on-neutral-normal);
|
||||
}
|
||||
|
||||
@@ -299,9 +299,10 @@ class DialogNewDashboard extends LitElement implements HassDialog {
|
||||
const options: IFuseOptions<CustomStrategyEntry> = {
|
||||
keys: ["type", "name", "description"],
|
||||
isCaseSensitive: false,
|
||||
threshold: 0.3,
|
||||
ignoreLocation: true,
|
||||
minMatchCharLength: Math.min(filter.length, 2),
|
||||
threshold: 0.3,
|
||||
ignoreDiacritics: true,
|
||||
ignoreLocation: true,
|
||||
};
|
||||
const fuse = new Fuse(strategies, options);
|
||||
return fuse.search(filter).map((result) => result.item);
|
||||
|
||||
@@ -12,8 +12,6 @@ import {
|
||||
import { computeDeviceNameDisplay } from "../../../../common/entity/compute_device_name";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/ha-adaptive-dialog";
|
||||
import "../../../../components/ha-list";
|
||||
import "../../../../components/ha-list-item";
|
||||
import "../../../../components/ha-spinner";
|
||||
import type { AutomationConfig } from "../../../../data/automation";
|
||||
import { showAutomationEditor } from "../../../../data/automation";
|
||||
@@ -35,15 +33,38 @@ import {
|
||||
} from "../../../../data/device/device_automation";
|
||||
import type { ScriptConfig } from "../../../../data/script";
|
||||
import { showScriptEditor } from "../../../../data/script";
|
||||
import type { SceneEntities } from "../../../../data/scene";
|
||||
import { showSceneEditor } from "../../../../data/scene";
|
||||
import "../../../../dialogs/add-to/ha-add-to-action-list";
|
||||
import type {
|
||||
AddToActionListActionSelectedEvent,
|
||||
AddToActionListItem,
|
||||
AddToActionListSection,
|
||||
} from "../../../../dialogs/add-to/ha-add-to-action-list";
|
||||
import {
|
||||
addToActionHandler,
|
||||
type AddToActionKey,
|
||||
} from "../../../../dialogs/more-info/add-to";
|
||||
createAddToSceneEntities,
|
||||
type AddToAutomationScriptActionKey,
|
||||
} from "../../../../dialogs/add-to/add-to";
|
||||
import { haStyle, haStyleDialog } from "../../../../resources/styles";
|
||||
import type { DeviceAddToDialogParams } from "./show-dialog-device-add-to";
|
||||
|
||||
type DeviceLegacyAddToActionType =
|
||||
| "trigger"
|
||||
| "condition"
|
||||
| "automation_action"
|
||||
| "script_action";
|
||||
|
||||
type DeviceAddToAction =
|
||||
| (AddToActionListItem & {
|
||||
kind: "add-to";
|
||||
key: AddToAutomationScriptActionKey;
|
||||
})
|
||||
| (AddToActionListItem & {
|
||||
kind: "legacy";
|
||||
legacyType: DeviceLegacyAddToActionType;
|
||||
})
|
||||
| (AddToActionListItem & { kind: "scene" });
|
||||
|
||||
@customElement("dialog-device-add-to")
|
||||
export class DialogDeviceAddTo extends LitElement {
|
||||
@state()
|
||||
@@ -132,11 +153,18 @@ export class DialogDeviceAddTo extends LitElement {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const deviceName = computeDeviceNameDisplay(
|
||||
this._params.device,
|
||||
this._i18n.localize,
|
||||
this._states
|
||||
);
|
||||
|
||||
return html`
|
||||
<ha-adaptive-dialog
|
||||
.open=${this._open}
|
||||
header-title=${this._i18n.localize(
|
||||
"ui.dialogs.more_info_control.add_to.title"
|
||||
"ui.dialogs.more_info_control.add_to.title",
|
||||
{ target: deviceName }
|
||||
)}
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
@@ -151,80 +179,62 @@ export class DialogDeviceAddTo extends LitElement {
|
||||
if (!this._params) {
|
||||
return nothing;
|
||||
}
|
||||
const deviceName = computeDeviceNameDisplay(
|
||||
this._params.device,
|
||||
this._i18n.localize,
|
||||
this._states
|
||||
);
|
||||
|
||||
const sections: AddToActionListSection<DeviceAddToAction>[] = [
|
||||
{
|
||||
title: this._i18n.localize(
|
||||
"ui.panel.config.devices.automation.automations_heading"
|
||||
),
|
||||
actions: [
|
||||
{
|
||||
kind: "add-to",
|
||||
key: "automation_trigger",
|
||||
iconPath: mdiRobotOutline,
|
||||
name: this._i18n.localize(
|
||||
"ui.dialogs.more_info_control.add_to.action_options.automation_trigger"
|
||||
),
|
||||
},
|
||||
{
|
||||
kind: "add-to",
|
||||
key: "automation_condition",
|
||||
iconPath: mdiPlaylistCheck,
|
||||
name: this._i18n.localize(
|
||||
"ui.dialogs.more_info_control.add_to.action_options.automation_condition"
|
||||
),
|
||||
},
|
||||
{
|
||||
kind: "add-to",
|
||||
key: "automation_action",
|
||||
iconPath: mdiPlayCircleOutline,
|
||||
name: this._i18n.localize(
|
||||
"ui.dialogs.more_info_control.add_to.action_options.automation_action"
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: this._i18n.localize(
|
||||
"ui.panel.config.devices.script.scripts_heading"
|
||||
),
|
||||
actions: [
|
||||
{
|
||||
kind: "add-to",
|
||||
key: "script_action",
|
||||
iconPath: mdiScriptTextOutline,
|
||||
name: this._i18n.localize(
|
||||
"ui.dialogs.more_info_control.add_to.action_options.script_action"
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
this._addSceneSection(sections);
|
||||
|
||||
return html`
|
||||
<h3 class="section-header">
|
||||
${this._i18n.localize(
|
||||
"ui.panel.config.devices.automation.automations_heading"
|
||||
)}
|
||||
</h3>
|
||||
<ha-list>
|
||||
<ha-list-item
|
||||
graphic="icon"
|
||||
data-type="automation_trigger"
|
||||
@click=${this._handleNewAction}
|
||||
data-dialog="close"
|
||||
>
|
||||
<ha-svg-icon slot="graphic" .path=${mdiRobotOutline}></ha-svg-icon>
|
||||
${this._i18n.localize(
|
||||
"ui.dialogs.more_info_control.add_to.actions.automation_trigger",
|
||||
{ target: deviceName }
|
||||
)}
|
||||
</ha-list-item>
|
||||
<ha-list-item
|
||||
graphic="icon"
|
||||
data-type="automation_condition"
|
||||
@click=${this._handleNewAction}
|
||||
data-dialog="close"
|
||||
>
|
||||
<ha-svg-icon slot="graphic" .path=${mdiPlaylistCheck}></ha-svg-icon>
|
||||
${this._i18n.localize(
|
||||
"ui.dialogs.more_info_control.add_to.actions.automation_condition",
|
||||
{ target: deviceName }
|
||||
)}
|
||||
</ha-list-item>
|
||||
<ha-list-item
|
||||
graphic="icon"
|
||||
data-type="automation_action"
|
||||
@click=${this._handleNewAction}
|
||||
data-dialog="close"
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="graphic"
|
||||
.path=${mdiPlayCircleOutline}
|
||||
></ha-svg-icon>
|
||||
${this._i18n.localize(
|
||||
"ui.dialogs.more_info_control.add_to.actions.automation_action",
|
||||
{ target: deviceName }
|
||||
)}
|
||||
</ha-list-item>
|
||||
</ha-list>
|
||||
<h3 class="section-header">
|
||||
${this._i18n.localize("ui.panel.config.devices.script.scripts_heading")}
|
||||
</h3>
|
||||
<ha-list>
|
||||
<ha-list-item
|
||||
graphic="icon"
|
||||
data-type="script_action"
|
||||
@click=${this._handleNewAction}
|
||||
data-dialog="close"
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="graphic"
|
||||
.path=${mdiScriptTextOutline}
|
||||
></ha-svg-icon>
|
||||
${this._i18n.localize(
|
||||
"ui.dialogs.more_info_control.add_to.actions.script_action",
|
||||
{ target: deviceName }
|
||||
)}
|
||||
</ha-list-item>
|
||||
</ha-list>
|
||||
${this._renderSceneSection(deviceName)}
|
||||
<ha-add-to-action-list
|
||||
.sections=${sections}
|
||||
@add-to-list-action-selected=${this._handleActionSelected}
|
||||
></ha-add-to-action-list>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -242,12 +252,6 @@ export class DialogDeviceAddTo extends LitElement {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const deviceName = computeDeviceNameDisplay(
|
||||
this._params.device,
|
||||
this._i18n.localize,
|
||||
this._states
|
||||
);
|
||||
|
||||
const hasTriggers = Boolean(this._triggers?.length);
|
||||
const hasConditions = Boolean(this._conditions?.length);
|
||||
const hasActions = Boolean(this._actions?.length);
|
||||
@@ -263,165 +267,138 @@ export class DialogDeviceAddTo extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<h3 class="section-header">
|
||||
${this._i18n.localize(
|
||||
"ui.panel.config.devices.automation.automations_heading"
|
||||
)}
|
||||
</h3>
|
||||
${hasTriggers || hasConditions || hasActions
|
||||
? html`
|
||||
<ha-list>
|
||||
${hasTriggers
|
||||
? html`
|
||||
<ha-list-item
|
||||
graphic="icon"
|
||||
data-type="trigger"
|
||||
@click=${this._handleLegacyAction}
|
||||
data-dialog="close"
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="graphic"
|
||||
.path=${mdiRobotOutline}
|
||||
></ha-svg-icon>
|
||||
${this._i18n.localize(
|
||||
"ui.dialogs.more_info_control.add_to.actions.automation_trigger",
|
||||
{ target: deviceName }
|
||||
)}
|
||||
</ha-list-item>
|
||||
`
|
||||
: nothing}
|
||||
${hasConditions
|
||||
? html`
|
||||
<ha-list-item
|
||||
graphic="icon"
|
||||
data-type="condition"
|
||||
@click=${this._handleLegacyAction}
|
||||
data-dialog="close"
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="graphic"
|
||||
.path=${mdiPlaylistCheck}
|
||||
></ha-svg-icon>
|
||||
${this._i18n.localize(
|
||||
"ui.dialogs.more_info_control.add_to.actions.automation_condition",
|
||||
{ target: deviceName }
|
||||
)}
|
||||
</ha-list-item>
|
||||
`
|
||||
: nothing}
|
||||
${hasActions
|
||||
? html`
|
||||
<ha-list-item
|
||||
graphic="icon"
|
||||
data-type="automation_action"
|
||||
@click=${this._handleLegacyAction}
|
||||
data-dialog="close"
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="graphic"
|
||||
.path=${mdiPlayCircleOutline}
|
||||
></ha-svg-icon>
|
||||
${this._i18n.localize(
|
||||
"ui.dialogs.more_info_control.add_to.actions.automation_action",
|
||||
{ target: deviceName }
|
||||
)}
|
||||
</ha-list-item>
|
||||
`
|
||||
: nothing}
|
||||
</ha-list>
|
||||
`
|
||||
: html`
|
||||
<ha-list>
|
||||
<ha-list-item noninteractive>
|
||||
${this._i18n.localize(
|
||||
"ui.panel.config.devices.automation.no_automations"
|
||||
)}
|
||||
</ha-list-item>
|
||||
</ha-list>
|
||||
`}
|
||||
<h3 class="section-header">
|
||||
${this._i18n.localize("ui.panel.config.devices.script.scripts_heading")}
|
||||
</h3>
|
||||
${hasActions
|
||||
? html`
|
||||
<ha-list>
|
||||
<ha-list-item
|
||||
graphic="icon"
|
||||
data-type="script_action"
|
||||
@click=${this._handleLegacyAction}
|
||||
data-dialog="close"
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="graphic"
|
||||
.path=${mdiScriptTextOutline}
|
||||
></ha-svg-icon>
|
||||
${this._i18n.localize(
|
||||
"ui.dialogs.more_info_control.add_to.actions.script_action",
|
||||
{ target: deviceName }
|
||||
)}
|
||||
</ha-list-item>
|
||||
</ha-list>
|
||||
`
|
||||
: html`
|
||||
<ha-list>
|
||||
<ha-list-item noninteractive>
|
||||
${this._i18n.localize(
|
||||
"ui.panel.config.devices.script.no_scripts"
|
||||
)}
|
||||
</ha-list-item>
|
||||
</ha-list>
|
||||
`}
|
||||
${this._renderSceneSection(deviceName)}
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderSceneSection(deviceName: string) {
|
||||
if (!this._params?.entityIds.length) {
|
||||
return nothing;
|
||||
const automationActions: DeviceAddToAction[] = [];
|
||||
if (hasTriggers) {
|
||||
automationActions.push({
|
||||
kind: "legacy",
|
||||
legacyType: "trigger",
|
||||
iconPath: mdiRobotOutline,
|
||||
name: this._i18n.localize(
|
||||
"ui.dialogs.more_info_control.add_to.action_options.automation_trigger"
|
||||
),
|
||||
});
|
||||
}
|
||||
if (hasConditions) {
|
||||
automationActions.push({
|
||||
kind: "legacy",
|
||||
legacyType: "condition",
|
||||
iconPath: mdiPlaylistCheck,
|
||||
name: this._i18n.localize(
|
||||
"ui.dialogs.more_info_control.add_to.action_options.automation_condition"
|
||||
),
|
||||
});
|
||||
}
|
||||
if (hasActions) {
|
||||
automationActions.push({
|
||||
kind: "legacy",
|
||||
legacyType: "automation_action",
|
||||
iconPath: mdiPlayCircleOutline,
|
||||
name: this._i18n.localize(
|
||||
"ui.dialogs.more_info_control.add_to.action_options.automation_action"
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
const scriptActions: DeviceAddToAction[] = hasActions
|
||||
? [
|
||||
{
|
||||
kind: "legacy",
|
||||
legacyType: "script_action",
|
||||
iconPath: mdiScriptTextOutline,
|
||||
name: this._i18n.localize(
|
||||
"ui.dialogs.more_info_control.add_to.action_options.script_action"
|
||||
),
|
||||
},
|
||||
]
|
||||
: [];
|
||||
|
||||
const sections: AddToActionListSection<DeviceAddToAction>[] = [
|
||||
{
|
||||
title: this._i18n.localize(
|
||||
"ui.panel.config.devices.automation.automations_heading"
|
||||
),
|
||||
actions: automationActions,
|
||||
empty: automationActions.length
|
||||
? undefined
|
||||
: this._i18n.localize(
|
||||
"ui.panel.config.devices.automation.no_automations"
|
||||
),
|
||||
},
|
||||
{
|
||||
title: this._i18n.localize(
|
||||
"ui.panel.config.devices.script.scripts_heading"
|
||||
),
|
||||
actions: scriptActions,
|
||||
empty: scriptActions.length
|
||||
? undefined
|
||||
: this._i18n.localize("ui.panel.config.devices.script.no_scripts"),
|
||||
},
|
||||
];
|
||||
this._addSceneSection(sections);
|
||||
|
||||
return html`
|
||||
<h3 class="section-header">
|
||||
${this._i18n.localize("ui.panel.config.devices.scene.scenes_heading")}
|
||||
</h3>
|
||||
<ha-list>
|
||||
<ha-list-item
|
||||
graphic="icon"
|
||||
@click=${this._handleCreateScene}
|
||||
data-dialog="close"
|
||||
>
|
||||
<ha-svg-icon slot="graphic" .path=${mdiPalette}></ha-svg-icon>
|
||||
${this._i18n.localize(
|
||||
"ui.dialogs.more_info_control.add_to.actions.scene",
|
||||
{ target: deviceName }
|
||||
)}
|
||||
</ha-list-item>
|
||||
</ha-list>
|
||||
<ha-add-to-action-list
|
||||
.sections=${sections}
|
||||
@add-to-list-action-selected=${this._handleActionSelected}
|
||||
></ha-add-to-action-list>
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleNewAction(ev: Event) {
|
||||
private _addSceneSection(
|
||||
sections: AddToActionListSection<DeviceAddToAction>[]
|
||||
): void {
|
||||
if (!this._params?.canCreateScene || !this._params.entityIds.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
sections.push({
|
||||
title: this._i18n.localize(
|
||||
"ui.panel.config.devices.scene.scenes_heading"
|
||||
),
|
||||
actions: [
|
||||
{
|
||||
kind: "scene",
|
||||
iconPath: mdiPalette,
|
||||
name: this._i18n.localize(
|
||||
"ui.dialogs.more_info_control.add_to.action_options.scene"
|
||||
),
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
private _handleActionSelected(
|
||||
ev: AddToActionListActionSelectedEvent<DeviceAddToAction>
|
||||
) {
|
||||
if (!this._params) {
|
||||
return;
|
||||
}
|
||||
const key = (ev.currentTarget as HTMLElement).dataset
|
||||
.type as AddToActionKey;
|
||||
|
||||
const { action } = ev.detail;
|
||||
if (action.kind === "scene") {
|
||||
this._handleCreateScene();
|
||||
return;
|
||||
}
|
||||
|
||||
if (action.kind === "add-to") {
|
||||
this._handleAddToAction(action.key);
|
||||
return;
|
||||
}
|
||||
|
||||
this._handleLegacyAction(action.legacyType);
|
||||
}
|
||||
|
||||
private _handleAddToAction(key: AddToAutomationScriptActionKey) {
|
||||
if (!this._params) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.closeDialog();
|
||||
addToActionHandler(key, { device_id: this._params.device.id });
|
||||
}
|
||||
|
||||
// When new_triggers_conditions labs feature is promoted, this whole method can be removed.
|
||||
private _handleLegacyAction(ev: Event) {
|
||||
if (!this._params) {
|
||||
return;
|
||||
}
|
||||
const type = (ev.currentTarget as HTMLElement).dataset.type as
|
||||
| "trigger"
|
||||
| "condition"
|
||||
| "automation_action"
|
||||
| "script_action";
|
||||
|
||||
private _handleLegacyAction(type: DeviceLegacyAddToActionType) {
|
||||
this.closeDialog();
|
||||
|
||||
if (type === "script_action") {
|
||||
@@ -430,29 +407,28 @@ export class DialogDeviceAddTo extends LitElement {
|
||||
newScript.sequence = [this._actions[0]];
|
||||
}
|
||||
showScriptEditor(newScript, true);
|
||||
} else {
|
||||
const newAutomation = {} as AutomationConfig;
|
||||
if (type === "trigger" && this._triggers?.length) {
|
||||
newAutomation.triggers = [this._triggers[0]];
|
||||
} else if (type === "condition" && this._conditions?.length) {
|
||||
newAutomation.conditions = [this._conditions[0]];
|
||||
} else if (type === "automation_action" && this._actions?.length) {
|
||||
newAutomation.actions = [this._actions[0]];
|
||||
}
|
||||
showAutomationEditor(newAutomation, true);
|
||||
return;
|
||||
}
|
||||
|
||||
const newAutomation = {} as AutomationConfig;
|
||||
if (type === "trigger" && this._triggers?.length) {
|
||||
newAutomation.triggers = [this._triggers[0]];
|
||||
} else if (type === "condition" && this._conditions?.length) {
|
||||
newAutomation.conditions = [this._conditions[0]];
|
||||
} else if (type === "automation_action" && this._actions?.length) {
|
||||
newAutomation.actions = [this._actions[0]];
|
||||
}
|
||||
showAutomationEditor(newAutomation, true);
|
||||
}
|
||||
|
||||
private _handleCreateScene() {
|
||||
if (!this._params) {
|
||||
return;
|
||||
}
|
||||
const entities: SceneEntities = {};
|
||||
for (const entityId of this._params.entityIds) {
|
||||
entities[entityId] = "";
|
||||
}
|
||||
this.closeDialog();
|
||||
showSceneEditor({ entities });
|
||||
showSceneEditor({
|
||||
entities: createAddToSceneEntities(this._params.entityIds),
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
@@ -469,14 +445,6 @@ export class DialogDeviceAddTo extends LitElement {
|
||||
padding: var(--ha-space-4);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
padding: var(--ha-space-2) var(--ha-space-4) 0;
|
||||
margin: 0;
|
||||
font-size: var(--ha-font-size-m);
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ export interface DeviceAddToDialogParams {
|
||||
device: DeviceRegistryEntry;
|
||||
newTriggersConditions: boolean;
|
||||
entityIds: string[];
|
||||
canCreateScene: boolean;
|
||||
}
|
||||
|
||||
export const loadDeviceAddToDialog = () => import("./ha-device-add-to-dialog");
|
||||
|
||||
@@ -86,6 +86,7 @@ import { domainToName } from "../../../data/integration";
|
||||
import { regenerateEntityIds } from "../../../data/regenerate_entity_ids";
|
||||
import type { RelatedResult } from "../../../data/search";
|
||||
import { findRelated } from "../../../data/search";
|
||||
import { filterAddToSceneEntityIds } from "../../../dialogs/add-to/add-to";
|
||||
import {
|
||||
showAlertDialog,
|
||||
showConfirmationDialog,
|
||||
@@ -424,6 +425,11 @@ export class HaConfigDevicePage extends LitElement {
|
||||
this._entityReg,
|
||||
this.hass.devices
|
||||
);
|
||||
const sceneEntityIds = filterAddToSceneEntityIds(
|
||||
this._entityIds(entities),
|
||||
this._entityReg,
|
||||
this.hass.states
|
||||
);
|
||||
const entitiesByCategory = this._entitiesByCategory(entities);
|
||||
const quickLinkCounts = this._getQuickLinkCounts(entities, this._related);
|
||||
const batteryEntity = this._batteryEntity(entities);
|
||||
@@ -531,7 +537,7 @@ export class HaConfigDevicePage extends LitElement {
|
||||
: this.hass.localize("ui.panel.config.devices.add_prompt_enabled");
|
||||
|
||||
const hasSceneSupport =
|
||||
isComponentLoaded(this.hass.config, "scene") && entities.length;
|
||||
isComponentLoaded(this.hass.config, "scene") && sceneEntityIds.length;
|
||||
|
||||
const relatedCard =
|
||||
isComponentLoaded(this.hass.config, "automation") ||
|
||||
@@ -551,7 +557,7 @@ export class HaConfigDevicePage extends LitElement {
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiPlus}></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.add_to.title"
|
||||
"ui.dialogs.more_info_control.add_to.item"
|
||||
)}
|
||||
</ha-button>
|
||||
</h1>
|
||||
@@ -1366,10 +1372,18 @@ export class HaConfigDevicePage extends LitElement {
|
||||
this._entityReg,
|
||||
this.hass.devices
|
||||
).map((entity) => entity.entity_id);
|
||||
const sceneEntityIds = filterAddToSceneEntityIds(
|
||||
entityIds,
|
||||
this._entityReg,
|
||||
this.hass.states
|
||||
);
|
||||
showDeviceAddToDialog(this, {
|
||||
device,
|
||||
newTriggersConditions: this._newTriggersConditions,
|
||||
entityIds,
|
||||
entityIds: sceneEntityIds,
|
||||
canCreateScene:
|
||||
isComponentLoaded(this.hass.config, "scene") &&
|
||||
sceneEntityIds.length > 0,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+15
-10
@@ -5,7 +5,7 @@ import { dynamicElement } from "../../../../../common/dom/dynamic-element-direct
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import { computeDomain } from "../../../../../common/entity/compute_domain";
|
||||
import { computeDeviceName } from "../../../../../common/entity/compute_device_name";
|
||||
import { navigate } from "../../../../../common/navigate";
|
||||
import { navigate, setRefreshUrl } from "../../../../../common/navigate";
|
||||
import "../../../../../components/ha-dialog-footer";
|
||||
import "../../../../../components/ha-icon-button-arrow-prev";
|
||||
import "../../../../../components/ha-button";
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
type ExtEntityRegistryEntry,
|
||||
} from "../../../../../data/entity/entity_registry";
|
||||
import { showAlertDialog } from "../../../../../dialogs/generic/show-dialog-box";
|
||||
import { OVERRIDE_DEVICE_CLASSES } from "../../../entities/entity-registry-settings-editor";
|
||||
import "./matter-add-device/matter-add-device-apple-home";
|
||||
import "./matter-add-device/matter-add-device-existing";
|
||||
import "./matter-add-device/matter-add-device-generic";
|
||||
@@ -100,6 +101,8 @@ class DialogMatterAddDevice extends LitElement {
|
||||
public showDialog(): void {
|
||||
this._open = true;
|
||||
this._unsub = watchForNewMatterDevice(this.hass, (device) => {
|
||||
// make sure a refresh of the page will navigate to the device page, old iOS apps will refresh the webview when commissioning is done
|
||||
setRefreshUrl(`/config/devices/device/${device.id}`);
|
||||
this._newDevice = device;
|
||||
this._step = "device_added";
|
||||
this._fetchMainEntity();
|
||||
@@ -137,15 +140,17 @@ class DialogMatterAddDevice extends LitElement {
|
||||
entityIds
|
||||
);
|
||||
|
||||
const mainEntry = Object.values(entries).find(
|
||||
(e) => e.original_name === null
|
||||
);
|
||||
if (!mainEntry) return;
|
||||
|
||||
const domain = computeDomain(mainEntry.entity_id);
|
||||
if (domain === "cover" || domain === "binary_sensor") {
|
||||
this._mainEntity = mainEntry;
|
||||
}
|
||||
this._mainEntity = Object.values(entries).find((entry) => {
|
||||
if (entry.entity_category) return false;
|
||||
const domain = computeDomain(entry.entity_id);
|
||||
const deviceClasses = OVERRIDE_DEVICE_CLASSES[domain];
|
||||
if (!deviceClasses) return false;
|
||||
const deviceClass = entry.device_class ?? entry.original_device_class;
|
||||
if (!deviceClass) return false;
|
||||
return deviceClasses.some(
|
||||
(classes) => classes.length > 1 && classes.includes(deviceClass)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
private _dialogClosed(): void {
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
removeSearchParam,
|
||||
} from "../../common/url/search-params";
|
||||
import "../../components/date-picker/ha-date-range-picker";
|
||||
import "../../components/entity/ha-entity-picker";
|
||||
import "../../components/ha-icon-button";
|
||||
import "../../components/ha-icon-button-arrow-prev";
|
||||
import "../../components/ha-menu-button";
|
||||
@@ -305,24 +304,23 @@ export class HaPanelLogbook extends LitElement {
|
||||
:host {
|
||||
--ha-generic-picker-max-width: 400px;
|
||||
}
|
||||
ha-logbook {
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: calc(
|
||||
100vh -
|
||||
168px - var(--safe-area-inset-top, 0px) - var(
|
||||
--safe-area-inset-bottom,
|
||||
100vh - var(--header-height, 0px) - var(
|
||||
--safe-area-inset-top,
|
||||
0px
|
||||
)
|
||||
) - var(--safe-area-inset-bottom, 0px)
|
||||
);
|
||||
overflow-x: hidden;
|
||||
padding: 0 0 16px;
|
||||
}
|
||||
|
||||
:host([narrow]) ha-logbook {
|
||||
height: calc(
|
||||
100vh -
|
||||
250px - var(--safe-area-inset-top, 0px) - var(
|
||||
--safe-area-inset-bottom,
|
||||
0px
|
||||
)
|
||||
);
|
||||
ha-logbook {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
ha-date-range-picker {
|
||||
@@ -337,6 +335,10 @@ export class HaPanelLogbook extends LitElement {
|
||||
ha-date-range-picker {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.filters {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
:host([narrow]) ha-date-range-picker {
|
||||
@@ -360,22 +362,11 @@ export class HaPanelLogbook extends LitElement {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
ha-entity-picker {
|
||||
display: inline-block;
|
||||
flex-grow: 1;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
ha-target-picker {
|
||||
flex: 1;
|
||||
max-width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
:host([narrow]) ha-entity-picker {
|
||||
max-width: none;
|
||||
width: 100%;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -86,7 +86,6 @@ class HuiCounterActionsCardFeature
|
||||
static getStubConfig(): CounterActionsCardFeatureConfig {
|
||||
return {
|
||||
type: "counter-actions",
|
||||
actions: COUNTER_ACTIONS.map((action) => action),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -108,10 +107,12 @@ class HuiCounterActionsCardFeature
|
||||
return null;
|
||||
}
|
||||
|
||||
const actions = this._config?.actions ?? COUNTER_ACTIONS;
|
||||
|
||||
return html`
|
||||
<ha-control-button-group>
|
||||
${this._config?.actions
|
||||
?.filter((action) => COUNTER_ACTIONS.includes(action))
|
||||
${actions
|
||||
.filter((action) => COUNTER_ACTIONS.includes(action))
|
||||
.map((action) => {
|
||||
const button = COUNTER_ACTIONS_BUTTON[action](this._stateObj!);
|
||||
return html`
|
||||
|
||||
@@ -110,20 +110,9 @@ class HuiLawnMowerCommandCardFeature
|
||||
| undefined;
|
||||
}
|
||||
|
||||
static getStubConfig(
|
||||
hass: HomeAssistant,
|
||||
context: LovelaceCardFeatureContext
|
||||
): LawnMowerCommandsCardFeatureConfig {
|
||||
const stateObj = context.entity_id
|
||||
? hass.states[context.entity_id]
|
||||
: undefined;
|
||||
static getStubConfig(): LawnMowerCommandsCardFeatureConfig {
|
||||
return {
|
||||
type: "lawn-mower-commands",
|
||||
commands: stateObj
|
||||
? LAWN_MOWER_COMMANDS.filter((c) =>
|
||||
supportsLawnMowerCommand(stateObj, c)
|
||||
).slice(0, 3)
|
||||
: [],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -162,28 +151,28 @@ class HuiLawnMowerCommandCardFeature
|
||||
|
||||
const stateObj = this._stateObj as LawnMowerEntity;
|
||||
|
||||
const commands = this._config.commands ?? LAWN_MOWER_COMMANDS;
|
||||
|
||||
return html`
|
||||
<ha-control-button-group>
|
||||
${LAWN_MOWER_COMMANDS.filter(
|
||||
(command) =>
|
||||
supportsLawnMowerCommand(stateObj, command) &&
|
||||
this._config?.commands?.includes(command)
|
||||
).map((command) => {
|
||||
const button = LAWN_MOWER_COMMANDS_BUTTONS[command](stateObj);
|
||||
return html`
|
||||
<ha-control-button
|
||||
.entry=${button}
|
||||
.label=${this.hass!.localize(
|
||||
// @ts-ignore
|
||||
`ui.dialogs.more_info_control.lawn_mower.${button.translationKey}`
|
||||
)}
|
||||
@click=${this._onCommandTap}
|
||||
.disabled=${button.disabled || stateObj.state === UNAVAILABLE}
|
||||
>
|
||||
<ha-svg-icon .path=${button.icon}></ha-svg-icon>
|
||||
</ha-control-button>
|
||||
`;
|
||||
})}
|
||||
${commands
|
||||
.filter((command) => supportsLawnMowerCommand(stateObj, command))
|
||||
.map((command) => {
|
||||
const button = LAWN_MOWER_COMMANDS_BUTTONS[command](stateObj);
|
||||
return html`
|
||||
<ha-control-button
|
||||
.entry=${button}
|
||||
.label=${this.hass!.localize(
|
||||
// @ts-ignore
|
||||
`ui.dialogs.more_info_control.lawn_mower.${button.translationKey}`
|
||||
)}
|
||||
@click=${this._onCommandTap}
|
||||
.disabled=${button.disabled || stateObj.state === UNAVAILABLE}
|
||||
>
|
||||
<ha-svg-icon .path=${button.icon}></ha-svg-icon>
|
||||
</ha-control-button>
|
||||
`;
|
||||
})}
|
||||
</ha-control-button-group>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -52,6 +52,12 @@ export const VACUUM_COMMANDS_FEATURES: Record<
|
||||
return_home: [VacuumEntityFeature.RETURN_HOME],
|
||||
};
|
||||
|
||||
export const VACUUM_DEFAULT_COMMANDS: VacuumCommand[] = [
|
||||
"start_pause",
|
||||
"stop",
|
||||
"return_home",
|
||||
];
|
||||
|
||||
export const supportsVacuumCommand = (
|
||||
stateObj: HassEntity,
|
||||
command: VacuumCommand
|
||||
@@ -154,20 +160,9 @@ class HuiVacuumCommandCardFeature
|
||||
| undefined;
|
||||
}
|
||||
|
||||
static getStubConfig(
|
||||
hass: HomeAssistant,
|
||||
context: LovelaceCardFeatureContext
|
||||
): VacuumCommandsCardFeatureConfig {
|
||||
const stateObj = context.entity_id
|
||||
? hass.states[context.entity_id]
|
||||
: undefined;
|
||||
static getStubConfig(): VacuumCommandsCardFeatureConfig {
|
||||
return {
|
||||
type: "vacuum-commands",
|
||||
commands: stateObj
|
||||
? VACUUM_COMMANDS.filter((c) =>
|
||||
supportsVacuumCommand(stateObj, c)
|
||||
).slice(0, 3)
|
||||
: [],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -204,28 +199,28 @@ class HuiVacuumCommandCardFeature
|
||||
|
||||
const stateObj = this._stateObj as VacuumEntity;
|
||||
|
||||
const commands = this._config.commands ?? VACUUM_DEFAULT_COMMANDS;
|
||||
|
||||
return html`
|
||||
<ha-control-button-group>
|
||||
${VACUUM_COMMANDS.filter(
|
||||
(command) =>
|
||||
supportsVacuumCommand(stateObj, command) &&
|
||||
this._config?.commands?.includes(command)
|
||||
).map((command) => {
|
||||
const button = VACUUM_COMMANDS_BUTTONS[command](stateObj);
|
||||
return html`
|
||||
<ha-control-button
|
||||
.entry=${button}
|
||||
.label=${this.hass!.localize(
|
||||
// @ts-ignore
|
||||
`ui.dialogs.more_info_control.vacuum.${button.translationKey}`
|
||||
)}
|
||||
@click=${this._onCommandTap}
|
||||
.disabled=${button.disabled || stateObj.state === UNAVAILABLE}
|
||||
>
|
||||
<ha-svg-icon .path=${button.icon}></ha-svg-icon>
|
||||
</ha-control-button>
|
||||
`;
|
||||
})}
|
||||
${commands
|
||||
.filter((command) => supportsVacuumCommand(stateObj, command))
|
||||
.map((command) => {
|
||||
const button = VACUUM_COMMANDS_BUTTONS[command](stateObj);
|
||||
return html`
|
||||
<ha-control-button
|
||||
.entry=${button}
|
||||
.label=${this.hass!.localize(
|
||||
// @ts-ignore
|
||||
`ui.dialogs.more_info_control.vacuum.${button.translationKey}`
|
||||
)}
|
||||
@click=${this._onCommandTap}
|
||||
.disabled=${button.disabled || stateObj.state === UNAVAILABLE}
|
||||
>
|
||||
<ha-svg-icon .path=${button.icon}></ha-svg-icon>
|
||||
</ha-control-button>
|
||||
`;
|
||||
})}
|
||||
</ha-control-button-group>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -161,7 +161,8 @@ export class HuiEnergyDevicesDetailGraphCard
|
||||
UNIT,
|
||||
this._compareStart,
|
||||
this._compareEnd,
|
||||
this._yAxisFractionDigits
|
||||
this._yAxisFractionDigits,
|
||||
this._legendData
|
||||
)}
|
||||
click-label-for-more-info
|
||||
@dataset-hidden=${this._datasetHidden}
|
||||
@@ -215,7 +216,8 @@ export class HuiEnergyDevicesDetailGraphCard
|
||||
unit: string | undefined,
|
||||
compareStart: Date | undefined,
|
||||
compareEnd: Date | undefined,
|
||||
yAxisFractionDigits: number
|
||||
yAxisFractionDigits: number,
|
||||
legendData: CustomLegendOption["data"]
|
||||
): HaECOption => {
|
||||
const commonOptions = getCommonOptions(
|
||||
start,
|
||||
@@ -230,8 +232,8 @@ export class HuiEnergyDevicesDetailGraphCard
|
||||
yAxisFractionDigits
|
||||
);
|
||||
|
||||
const selected = this._legendData
|
||||
? this._legendData
|
||||
const selected = legendData
|
||||
? legendData
|
||||
.filter(
|
||||
(d) =>
|
||||
d.id && this._hiddenStats.includes(this._getStatIdFromId(d.id))
|
||||
@@ -247,7 +249,7 @@ export class HuiEnergyDevicesDetailGraphCard
|
||||
legend: {
|
||||
show: true,
|
||||
type: "custom",
|
||||
data: this._legendData,
|
||||
data: legendData,
|
||||
selected,
|
||||
},
|
||||
grid: {
|
||||
|
||||
@@ -438,9 +438,7 @@ class HuiEnergySankeyCard
|
||||
}
|
||||
|
||||
private _valueFormatter = (value: number) =>
|
||||
`<div style="direction:ltr; display: inline;">
|
||||
${formatNumber(value, this.hass.locale, value < 0.1 ? { maximumFractionDigits: 3 } : undefined)}
|
||||
kWh</div>`;
|
||||
`${formatNumber(value, this.hass.locale, value < 0.1 ? { maximumFractionDigits: 3 } : undefined)} kWh`;
|
||||
|
||||
private _handleNodeClick(ev: CustomEvent<{ node: Node }>) {
|
||||
const { node } = ev.detail;
|
||||
|
||||
@@ -580,9 +580,7 @@ class HuiPowerSankeyCard
|
||||
}
|
||||
|
||||
private _valueFormatter = (value: number) =>
|
||||
`<div style="direction:ltr; display: inline;">
|
||||
${formatPowerShort(this.hass, value)}
|
||||
</div>`;
|
||||
formatPowerShort(this.hass, value);
|
||||
|
||||
private _handleNodeClick(ev: CustomEvent<{ node: Node }>) {
|
||||
const { node } = ev.detail;
|
||||
|
||||
@@ -511,9 +511,11 @@ class HuiWaterFlowSankeyCard
|
||||
}
|
||||
|
||||
private _valueFormatter = (value: number) =>
|
||||
`<div style="direction:ltr; display: inline;">
|
||||
${formatFlowRateShort(this.hass.locale, this.hass.config.unit_system.length, value)}
|
||||
</div>`;
|
||||
formatFlowRateShort(
|
||||
this.hass.locale,
|
||||
this.hass.config.unit_system.length,
|
||||
value
|
||||
);
|
||||
|
||||
private _handleNodeClick(ev: CustomEvent<{ node: Node }>) {
|
||||
const { node } = ev.detail;
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { IFuseOptions } from "fuse.js";
|
||||
import Fuse from "fuse.js";
|
||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { until } from "lit/directives/until";
|
||||
@@ -66,10 +66,19 @@ export class HuiBadgePicker extends LitElement {
|
||||
|
||||
@state() private _height?: number;
|
||||
|
||||
@query("ha-input-search") private _searchInput?: HaInputSearch;
|
||||
|
||||
private _unusedEntities?: string[];
|
||||
|
||||
private _usedEntities?: string[];
|
||||
|
||||
public async focus(): Promise<void> {
|
||||
await this.updateComplete;
|
||||
// Wait for the input's inner wa-input to render so focus delegation works.
|
||||
await this._searchInput?.updateComplete;
|
||||
this._searchInput?.focus();
|
||||
}
|
||||
|
||||
private _filterBadges = memoizeOne(
|
||||
(badgeElements: BadgeElement[], filter?: string): BadgeElement[] => {
|
||||
if (!filter) {
|
||||
@@ -84,6 +93,7 @@ export class HuiBadgePicker extends LitElement {
|
||||
minMatchCharLength: Math.min(filter.length, 2),
|
||||
threshold: 0.2,
|
||||
ignoreDiacritics: true,
|
||||
ignoreLocation: true,
|
||||
};
|
||||
const fuse = new Fuse(badges, options);
|
||||
badges = fuse.search(filter).map((result) => result.item);
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
} from "@mdi/js";
|
||||
import type { FuseIndex } from "fuse.js";
|
||||
import Fuse from "fuse.js";
|
||||
import { getAreasFloorHierarchy } from "../../../../common/areas/areas-floor-hierarchy";
|
||||
import { computeAreaName } from "../../../../common/entity/compute_area_name";
|
||||
import { computeDeviceName } from "../../../../common/entity/compute_device_name";
|
||||
import { computeDomain } from "../../../../common/entity/compute_domain";
|
||||
@@ -13,7 +14,6 @@ import { computeEntityName } from "../../../../common/entity/compute_entity_name
|
||||
import { computeStateName } from "../../../../common/entity/compute_state_name";
|
||||
import { stringCompare } from "../../../../common/string/compare";
|
||||
import { entityComboBoxKeys } from "../../../../data/entity/entity_picker";
|
||||
import { getFloorAreaLookup } from "../../../../data/floor_registry";
|
||||
import { domainToName } from "../../../../data/integration";
|
||||
import { multiTermSortedSearch } from "../../../../resources/fuseMultiTerm";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
@@ -206,23 +206,24 @@ export function buildEntityTree(input: BuildEntityTreeInput): EntityTree {
|
||||
return stringCompare(an, bn, language);
|
||||
};
|
||||
|
||||
const buildDeviceNodes = (source: Map<string, string[]>): DeviceNode[] =>
|
||||
[...source.entries()]
|
||||
.map(([id, ids]) => {
|
||||
const device = deviceReg[id];
|
||||
return {
|
||||
id,
|
||||
name: (device ? computeDeviceName(device) : undefined) ?? id,
|
||||
entityIds: ids.sort(sortByName),
|
||||
};
|
||||
})
|
||||
.sort((a, b) => stringCompare(a.name, b.name, language));
|
||||
|
||||
const buildAreaNode = (areaId: string): AreaNode | undefined => {
|
||||
const area = areaReg[areaId];
|
||||
if (!area) return undefined;
|
||||
const directIds = (areaDirectEntities.get(areaId) ?? []).sort(sortByName);
|
||||
const byDevice = areaDeviceEntities.get(areaId);
|
||||
const devices: DeviceNode[] = byDevice
|
||||
? [...byDevice.entries()]
|
||||
.map(([id, ids]) => {
|
||||
const device = deviceReg[id];
|
||||
return {
|
||||
id,
|
||||
name: (device ? computeDeviceName(device) : undefined) ?? id,
|
||||
entityIds: ids.sort(sortByName),
|
||||
};
|
||||
})
|
||||
.sort((a, b) => stringCompare(a.name, b.name, language))
|
||||
: [];
|
||||
const devices = byDevice ? buildDeviceNodes(byDevice) : [];
|
||||
if (!directIds.length && !devices.length) return undefined;
|
||||
return {
|
||||
id: area.area_id,
|
||||
@@ -235,14 +236,14 @@ export function buildEntityTree(input: BuildEntityTreeInput): EntityTree {
|
||||
|
||||
const areas = Object.values(areaReg);
|
||||
const floors = Object.values(floorReg);
|
||||
const floorAreaLookup = getFloorAreaLookup(areas);
|
||||
const hierarchy = getAreasFloorHierarchy(floors, areas);
|
||||
|
||||
const floorNodes: FloorNode[] = floors
|
||||
.map((floor) => {
|
||||
const areaList = (floorAreaLookup[floor.floor_id] ?? [])
|
||||
.map((a) => buildAreaNode(a.area_id))
|
||||
.filter((a): a is AreaNode => !!a)
|
||||
.sort((a, b) => stringCompare(a.name, b.name, language));
|
||||
const floorNodes: FloorNode[] = hierarchy.floors
|
||||
.map(({ id, areas: areaIds }) => {
|
||||
const floor = floorReg[id];
|
||||
const areaList = areaIds
|
||||
.map((areaId) => buildAreaNode(areaId))
|
||||
.filter((a): a is AreaNode => !!a);
|
||||
if (!areaList.length) return undefined;
|
||||
return {
|
||||
id: floor.floor_id,
|
||||
@@ -252,26 +253,11 @@ export function buildEntityTree(input: BuildEntityTreeInput): EntityTree {
|
||||
areas: areaList,
|
||||
};
|
||||
})
|
||||
.filter((f): f is FloorNode => !!f)
|
||||
.sort((a, b) => stringCompare(a.name, b.name, language));
|
||||
.filter((f): f is FloorNode => !!f);
|
||||
|
||||
const otherAreas = areas
|
||||
.filter((a) => !a.floor_id || !floorReg[a.floor_id])
|
||||
.map((a) => buildAreaNode(a.area_id))
|
||||
.filter((a): a is AreaNode => !!a)
|
||||
.sort((a, b) => stringCompare(a.name, b.name, language));
|
||||
|
||||
const buildDeviceNodes = (source: Map<string, string[]>): DeviceNode[] =>
|
||||
[...source.entries()]
|
||||
.map(([id, ids]) => {
|
||||
const device = deviceReg[id];
|
||||
return {
|
||||
id,
|
||||
name: (device ? computeDeviceName(device) : undefined) ?? id,
|
||||
entityIds: ids.sort(sortByName),
|
||||
};
|
||||
})
|
||||
.sort((a, b) => stringCompare(a.name, b.name, language));
|
||||
const otherAreas = hierarchy.areas
|
||||
.map((areaId) => buildAreaNode(areaId))
|
||||
.filter((a): a is AreaNode => !!a);
|
||||
|
||||
const buildDomainGroups = (source: Map<string, string[]>): DomainGroup[] =>
|
||||
[...source.entries()]
|
||||
|
||||
@@ -62,19 +62,17 @@ export class HuiCardPicker extends LitElement {
|
||||
|
||||
@state() private _filter = "";
|
||||
|
||||
@query("ha-input-search") private _searchInput?: HTMLElement;
|
||||
@query("ha-input-search") private _searchInput?: HaInputSearch;
|
||||
|
||||
private _unusedEntities?: string[];
|
||||
|
||||
private _usedEntities?: string[];
|
||||
|
||||
public async focus(): Promise<void> {
|
||||
if (this._searchInput) {
|
||||
this._searchInput.focus();
|
||||
} else {
|
||||
await this.updateComplete;
|
||||
this.focus();
|
||||
}
|
||||
await this.updateComplete;
|
||||
// Wait for the input's inner wa-input to render so focus delegation works.
|
||||
await this._searchInput?.updateComplete;
|
||||
this._searchInput?.focus();
|
||||
}
|
||||
|
||||
private _filterCards = memoizeOne(
|
||||
@@ -91,6 +89,7 @@ export class HuiCardPicker extends LitElement {
|
||||
minMatchCharLength: Math.min(filter.length, 2),
|
||||
threshold: 0.2,
|
||||
ignoreDiacritics: true,
|
||||
ignoreLocation: true,
|
||||
};
|
||||
const fuse = new Fuse(cards, options);
|
||||
cards = fuse.search(filter).map((result) => result.item);
|
||||
|
||||
@@ -133,6 +133,7 @@ export class HuiCreateDialogCard
|
||||
this._currTab === "entity"
|
||||
? html`
|
||||
<hui-suggestion-picker
|
||||
?autofocus=${!this._narrow}
|
||||
.hass=${this.hass}
|
||||
.prioritizedCardTypes=${this._params.suggestedCards}
|
||||
@suggestion-picked=${this._handleSuggestionPicked}
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
} from "@mdi/js";
|
||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { transform } from "../../../../common/decorators/transform";
|
||||
@@ -85,6 +85,15 @@ export class HuiSuggestionEntityTree extends LitElement {
|
||||
|
||||
@state() private _fuseIndex?: EntityFuseIndex;
|
||||
|
||||
@query("ha-input-search") private _searchInput?: HaInputSearch;
|
||||
|
||||
public async focus(): Promise<void> {
|
||||
await this.updateComplete;
|
||||
// Wait for the input's inner wa-input to render so focus delegation works.
|
||||
await this._searchInput?.updateComplete;
|
||||
this._searchInput?.focus();
|
||||
}
|
||||
|
||||
public connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
this._loadDomainTranslations();
|
||||
@@ -135,7 +144,7 @@ export class HuiSuggestionEntityTree extends LitElement {
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this.hass || !this._tree) return nothing;
|
||||
if (!this.hass) return nothing;
|
||||
|
||||
return html`
|
||||
<ha-input-search
|
||||
@@ -146,11 +155,13 @@ export class HuiSuggestionEntityTree extends LitElement {
|
||||
)}
|
||||
@input=${this._handleFilterChange}
|
||||
></ha-input-search>
|
||||
${this._filter
|
||||
? this._renderSearchResults()
|
||||
: html`<div class="tree ha-scrollbar">
|
||||
${this._renderTree(this._tree)}
|
||||
</div>`}
|
||||
${this._tree
|
||||
? this._filter
|
||||
? this._renderSearchResults()
|
||||
: html`<div class="tree ha-scrollbar">
|
||||
${this._renderTree(this._tree)}
|
||||
</div>`
|
||||
: nothing}
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { mdiClose, mdiViewGridPlus } from "@mdi/js";
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import memoizeOne from "memoize-one";
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
import type { CardSuggestion } from "../../card-suggestions/types";
|
||||
import "./hui-suggestion-card";
|
||||
import "./hui-suggestion-entity-tree";
|
||||
import type { HuiSuggestionEntityTree } from "./hui-suggestion-entity-tree";
|
||||
|
||||
@customElement("hui-suggestion-picker")
|
||||
export class HuiSuggestionPicker extends LitElement {
|
||||
@@ -38,6 +39,14 @@ export class HuiSuggestionPicker extends LitElement {
|
||||
|
||||
private _narrowMql?: MediaQueryList;
|
||||
|
||||
@query("hui-suggestion-entity-tree")
|
||||
private _entityTree?: HuiSuggestionEntityTree;
|
||||
|
||||
public async focus(): Promise<void> {
|
||||
await this.updateComplete;
|
||||
await this._entityTree?.focus();
|
||||
}
|
||||
|
||||
public connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
this._narrowMql = matchMedia("(max-width: 600px)");
|
||||
|
||||
@@ -30,6 +30,7 @@ import "../../../../components/ha-dropdown-item";
|
||||
import "../../../../components/ha-expansion-panel";
|
||||
import "../../../../components/ha-icon-button";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import "../../../../components/ha-tooltip";
|
||||
import "../../../../components/ha-yaml-editor";
|
||||
import { showAlertDialog } from "../../../../dialogs/generic/show-dialog-box";
|
||||
import { haStyle } from "../../../../resources/styles";
|
||||
@@ -230,11 +231,28 @@ export class HaCardConditionEditor extends LitElement {
|
||||
return html`
|
||||
<div class="container">
|
||||
<ha-expansion-panel left-chevron>
|
||||
<ha-svg-icon
|
||||
<div
|
||||
id="condition-icon"
|
||||
class="icon-badge-wrapper"
|
||||
slot="leading-icon"
|
||||
class="condition-icon"
|
||||
.path=${ICON_CONDITION[condition.condition]}
|
||||
></ha-svg-icon>
|
||||
>
|
||||
<ha-svg-icon
|
||||
.path=${ICON_CONDITION[condition.condition]}
|
||||
></ha-svg-icon>
|
||||
${hideLiveTest
|
||||
? nothing
|
||||
: html`<ha-automation-row-live-test
|
||||
.state=${this._liveTestResult.state}
|
||||
.label=${this.hass.localize(
|
||||
`ui.panel.lovelace.editor.condition-editor.live_test_state.${this._liveTestResult.state}`
|
||||
)}
|
||||
></ha-automation-row-live-test>`}
|
||||
</div>
|
||||
${!hideLiveTest && this._liveTestResult.message
|
||||
? html`<ha-tooltip for="condition-icon" slot="leading-icon"
|
||||
>${this._liveTestResult.message}</ha-tooltip
|
||||
>`
|
||||
: nothing}
|
||||
<h3 slot="header">
|
||||
${this.hass.localize(
|
||||
`ui.panel.lovelace.editor.condition-editor.condition.${condition.condition}.label`
|
||||
@@ -255,18 +273,6 @@ export class HaCardConditionEditor extends LitElement {
|
||||
"ui.panel.lovelace.editor.condition-editor.testing_error"
|
||||
)}
|
||||
</ha-automation-row-event-chip>
|
||||
${hideLiveTest
|
||||
? nothing
|
||||
: html`
|
||||
<ha-automation-row-live-test
|
||||
slot="icons"
|
||||
.state=${this._liveTestResult.state}
|
||||
.label=${this.hass.localize(
|
||||
`ui.panel.lovelace.editor.condition-editor.live_test_state.${this._liveTestResult.state}`
|
||||
)}
|
||||
.message=${this._liveTestResult.message}
|
||||
></ha-automation-row-live-test>
|
||||
`}
|
||||
<ha-dropdown
|
||||
slot="icons"
|
||||
@wa-select=${this._handleAction}
|
||||
@@ -479,17 +485,15 @@ export class HaCardConditionEditor extends LitElement {
|
||||
--expansion-panel-summary-padding: 0 0 0 8px;
|
||||
--expansion-panel-content-padding: 0;
|
||||
}
|
||||
.condition-icon {
|
||||
.icon-badge-wrapper {
|
||||
display: none;
|
||||
}
|
||||
@media (min-width: 870px) {
|
||||
.condition-icon {
|
||||
display: inline-block;
|
||||
.icon-badge-wrapper {
|
||||
display: inline-flex;
|
||||
position: relative;
|
||||
color: var(--secondary-text-color);
|
||||
opacity: 0.9;
|
||||
margin-right: 8px;
|
||||
margin-inline-end: 8px;
|
||||
margin-inline-start: initial;
|
||||
}
|
||||
}
|
||||
h3 {
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import type { HaFormSchema } from "../../../../components/ha-form/types";
|
||||
|
||||
interface CustomizableListSchemaParams {
|
||||
field: string;
|
||||
customize: boolean;
|
||||
options: { value: string; label: string }[];
|
||||
}
|
||||
|
||||
export const customizableListSchema = ({
|
||||
field,
|
||||
customize,
|
||||
options,
|
||||
}: CustomizableListSchemaParams) =>
|
||||
[
|
||||
{
|
||||
name: "customize",
|
||||
selector: { boolean: {} },
|
||||
},
|
||||
...(customize
|
||||
? ([
|
||||
{
|
||||
name: field,
|
||||
selector: {
|
||||
select: {
|
||||
mode: "list",
|
||||
reorder: true,
|
||||
multiple: true,
|
||||
options,
|
||||
},
|
||||
},
|
||||
},
|
||||
] as const satisfies readonly HaFormSchema[])
|
||||
: []),
|
||||
] as const satisfies readonly HaFormSchema[];
|
||||
|
||||
// `customize` is form-only and never stored in the config.
|
||||
export const customizableListData = <T extends object>(
|
||||
config: T,
|
||||
field: string
|
||||
): T & { customize: boolean } => ({
|
||||
...config,
|
||||
customize: (config as Record<string, unknown>)[field] !== undefined,
|
||||
});
|
||||
|
||||
// Dropping the field lets the feature fall back to its own default.
|
||||
export const processCustomizableListValue = <T extends object>(
|
||||
value: T & { customize?: boolean },
|
||||
field: string,
|
||||
defaults: readonly string[]
|
||||
): T => {
|
||||
const { customize, ...rest } = value;
|
||||
const config = rest as Record<string, unknown>;
|
||||
if (customize && !config[field]) {
|
||||
config[field] = [...defaults];
|
||||
} else if (!customize) {
|
||||
delete config[field];
|
||||
}
|
||||
return config as unknown as T;
|
||||
};
|
||||
+34
-36
@@ -2,16 +2,20 @@ import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import type { LocalizeFunc } from "../../../../common/translations/localize";
|
||||
import type { SchemaUnion } from "../../../../components/ha-form/types";
|
||||
import "../../../../components/ha-form/ha-form";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import {
|
||||
COUNTER_ACTIONS,
|
||||
type LovelaceCardFeatureContext,
|
||||
type CounterActionsCardFeatureConfig,
|
||||
import type {
|
||||
CounterActionsCardFeatureConfig,
|
||||
LovelaceCardFeatureContext,
|
||||
} from "../../card-features/types";
|
||||
import { COUNTER_ACTIONS } from "../../card-features/types";
|
||||
import type { LovelaceCardFeatureEditor } from "../../types";
|
||||
import {
|
||||
customizableListData,
|
||||
customizableListSchema,
|
||||
processCustomizableListValue,
|
||||
} from "./customizable-list-feature";
|
||||
|
||||
@customElement("hui-counter-actions-card-feature-editor")
|
||||
export class HuiCounterActionsCardFeatureEditor
|
||||
@@ -28,26 +32,17 @@ export class HuiCounterActionsCardFeatureEditor
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
private _schema = memoizeOne(
|
||||
(localize: LocalizeFunc) =>
|
||||
[
|
||||
{
|
||||
name: "actions",
|
||||
selector: {
|
||||
select: {
|
||||
multiple: true,
|
||||
mode: "list",
|
||||
reorder: true,
|
||||
options: COUNTER_ACTIONS.map((action) => ({
|
||||
value: action,
|
||||
label: `${localize(
|
||||
`ui.panel.lovelace.editor.features.types.counter-actions.actions.${action}`
|
||||
)}`,
|
||||
})),
|
||||
},
|
||||
},
|
||||
},
|
||||
] as const
|
||||
private _schema = memoizeOne((customize: boolean) =>
|
||||
customizableListSchema({
|
||||
field: "actions",
|
||||
customize,
|
||||
options: COUNTER_ACTIONS.map((action) => ({
|
||||
value: action,
|
||||
label: this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.features.types.counter-actions.actions_list.${action}`
|
||||
),
|
||||
})),
|
||||
})
|
||||
);
|
||||
|
||||
protected render() {
|
||||
@@ -55,12 +50,13 @@ export class HuiCounterActionsCardFeatureEditor
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const schema = this._schema(this.hass.localize);
|
||||
const data = customizableListData(this._config, "actions");
|
||||
const schema = this._schema(data.customize);
|
||||
|
||||
return html`
|
||||
<ha-form
|
||||
.hass=${this.hass}
|
||||
.data=${this._config}
|
||||
.data=${data}
|
||||
.schema=${schema}
|
||||
.computeLabel=${this._computeLabelCallback}
|
||||
@value-changed=${this._valueChanged}
|
||||
@@ -69,19 +65,21 @@ export class HuiCounterActionsCardFeatureEditor
|
||||
}
|
||||
|
||||
private _valueChanged(ev: CustomEvent): void {
|
||||
fireEvent(this, "config-changed", { config: ev.detail.value });
|
||||
const config =
|
||||
processCustomizableListValue<CounterActionsCardFeatureConfig>(
|
||||
ev.detail.value,
|
||||
"actions",
|
||||
COUNTER_ACTIONS
|
||||
);
|
||||
fireEvent(this, "config-changed", { config });
|
||||
}
|
||||
|
||||
private _computeLabelCallback = (
|
||||
schema: SchemaUnion<ReturnType<typeof this._schema>>
|
||||
) => {
|
||||
switch (schema.name) {
|
||||
default:
|
||||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.generic.${schema.name}`
|
||||
);
|
||||
}
|
||||
};
|
||||
) =>
|
||||
this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.features.types.counter-actions.${schema.name}`
|
||||
);
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
+39
-38
@@ -3,7 +3,6 @@ import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import type { LocalizeFunc } from "../../../../common/translations/localize";
|
||||
import type { SchemaUnion } from "../../../../components/ha-form/types";
|
||||
import "../../../../components/ha-form/ha-form";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
@@ -14,6 +13,11 @@ import type {
|
||||
} from "../../card-features/types";
|
||||
import { LAWN_MOWER_COMMANDS } from "../../card-features/types";
|
||||
import type { LovelaceCardFeatureEditor } from "../../types";
|
||||
import {
|
||||
customizableListData,
|
||||
customizableListSchema,
|
||||
processCustomizableListValue,
|
||||
} from "./customizable-list-feature";
|
||||
|
||||
@customElement("hui-lawn-mower-commands-card-feature-editor")
|
||||
export class HuiLawnMowerCommandsCardFeatureEditor
|
||||
@@ -31,27 +35,19 @@ export class HuiLawnMowerCommandsCardFeatureEditor
|
||||
}
|
||||
|
||||
private _schema = memoizeOne(
|
||||
(localize: LocalizeFunc, stateObj?: HassEntity) =>
|
||||
[
|
||||
{
|
||||
name: "commands",
|
||||
selector: {
|
||||
select: {
|
||||
multiple: true,
|
||||
mode: "list",
|
||||
options: LAWN_MOWER_COMMANDS.filter(
|
||||
(command) =>
|
||||
stateObj && supportsLawnMowerCommand(stateObj, command)
|
||||
).map((command) => ({
|
||||
value: command,
|
||||
label: `${localize(
|
||||
`ui.panel.lovelace.editor.features.types.lawn-mower-commands.commands_list.${command}`
|
||||
)}`,
|
||||
})),
|
||||
},
|
||||
},
|
||||
},
|
||||
] as const
|
||||
(stateObj: HassEntity | undefined, customize: boolean) =>
|
||||
customizableListSchema({
|
||||
field: "commands",
|
||||
customize,
|
||||
options: LAWN_MOWER_COMMANDS.filter(
|
||||
(command) => stateObj && supportsLawnMowerCommand(stateObj, command)
|
||||
).map((command) => ({
|
||||
value: command,
|
||||
label: this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.features.types.lawn-mower-commands.commands_list.${command}`
|
||||
),
|
||||
})),
|
||||
})
|
||||
);
|
||||
|
||||
protected render() {
|
||||
@@ -60,15 +56,16 @@ export class HuiLawnMowerCommandsCardFeatureEditor
|
||||
}
|
||||
|
||||
const stateObj = this.context?.entity_id
|
||||
? this.hass.states[this.context?.entity_id]
|
||||
? this.hass.states[this.context.entity_id]
|
||||
: undefined;
|
||||
|
||||
const schema = this._schema(this.hass.localize, stateObj);
|
||||
const data = customizableListData(this._config, "commands");
|
||||
const schema = this._schema(stateObj, data.customize);
|
||||
|
||||
return html`
|
||||
<ha-form
|
||||
.hass=${this.hass}
|
||||
.data=${this._config}
|
||||
.data=${data}
|
||||
.schema=${schema}
|
||||
.computeLabel=${this._computeLabelCallback}
|
||||
@value-changed=${this._valueChanged}
|
||||
@@ -77,23 +74,27 @@ export class HuiLawnMowerCommandsCardFeatureEditor
|
||||
}
|
||||
|
||||
private _valueChanged(ev: CustomEvent): void {
|
||||
fireEvent(this, "config-changed", { config: ev.detail.value });
|
||||
const stateObj = this.context?.entity_id
|
||||
? this.hass!.states[this.context.entity_id]
|
||||
: undefined;
|
||||
const defaults = LAWN_MOWER_COMMANDS.filter(
|
||||
(command) => stateObj && supportsLawnMowerCommand(stateObj, command)
|
||||
);
|
||||
const config =
|
||||
processCustomizableListValue<LawnMowerCommandsCardFeatureConfig>(
|
||||
ev.detail.value,
|
||||
"commands",
|
||||
defaults
|
||||
);
|
||||
fireEvent(this, "config-changed", { config });
|
||||
}
|
||||
|
||||
private _computeLabelCallback = (
|
||||
schema: SchemaUnion<ReturnType<typeof this._schema>>
|
||||
) => {
|
||||
switch (schema.name) {
|
||||
case "commands":
|
||||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.features.types.lawn-mower-commands.${schema.name}`
|
||||
);
|
||||
default:
|
||||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.generic.${schema.name}`
|
||||
);
|
||||
}
|
||||
};
|
||||
) =>
|
||||
this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.features.types.lawn-mower-commands.${schema.name}`
|
||||
);
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
+43
-39
@@ -3,17 +3,24 @@ import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import type { LocalizeFunc } from "../../../../common/translations/localize";
|
||||
import type { SchemaUnion } from "../../../../components/ha-form/types";
|
||||
import "../../../../components/ha-form/ha-form";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import { supportsVacuumCommand } from "../../card-features/hui-vacuum-commands-card-feature";
|
||||
import {
|
||||
supportsVacuumCommand,
|
||||
VACUUM_DEFAULT_COMMANDS,
|
||||
} from "../../card-features/hui-vacuum-commands-card-feature";
|
||||
import type {
|
||||
LovelaceCardFeatureContext,
|
||||
VacuumCommandsCardFeatureConfig,
|
||||
} from "../../card-features/types";
|
||||
import { VACUUM_COMMANDS } from "../../card-features/types";
|
||||
import type { LovelaceCardFeatureEditor } from "../../types";
|
||||
import {
|
||||
customizableListData,
|
||||
customizableListSchema,
|
||||
processCustomizableListValue,
|
||||
} from "./customizable-list-feature";
|
||||
|
||||
@customElement("hui-vacuum-commands-card-feature-editor")
|
||||
export class HuiVacuumCommandsCardFeatureEditor
|
||||
@@ -31,27 +38,19 @@ export class HuiVacuumCommandsCardFeatureEditor
|
||||
}
|
||||
|
||||
private _schema = memoizeOne(
|
||||
(localize: LocalizeFunc, stateObj?: HassEntity) =>
|
||||
[
|
||||
{
|
||||
name: "commands",
|
||||
selector: {
|
||||
select: {
|
||||
multiple: true,
|
||||
mode: "list",
|
||||
options: VACUUM_COMMANDS.filter(
|
||||
(command) =>
|
||||
stateObj && supportsVacuumCommand(stateObj, command)
|
||||
).map((command) => ({
|
||||
value: command,
|
||||
label: `${localize(
|
||||
`ui.panel.lovelace.editor.features.types.vacuum-commands.commands_list.${command}`
|
||||
)}`,
|
||||
})),
|
||||
},
|
||||
},
|
||||
},
|
||||
] as const
|
||||
(stateObj: HassEntity | undefined, customize: boolean) =>
|
||||
customizableListSchema({
|
||||
field: "commands",
|
||||
customize,
|
||||
options: VACUUM_COMMANDS.filter(
|
||||
(command) => stateObj && supportsVacuumCommand(stateObj, command)
|
||||
).map((command) => ({
|
||||
value: command,
|
||||
label: this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.features.types.vacuum-commands.commands_list.${command}`
|
||||
),
|
||||
})),
|
||||
})
|
||||
);
|
||||
|
||||
protected render() {
|
||||
@@ -60,15 +59,16 @@ export class HuiVacuumCommandsCardFeatureEditor
|
||||
}
|
||||
|
||||
const stateObj = this.context?.entity_id
|
||||
? this.hass.states[this.context?.entity_id]
|
||||
? this.hass.states[this.context.entity_id]
|
||||
: undefined;
|
||||
|
||||
const schema = this._schema(this.hass.localize, stateObj);
|
||||
const data = customizableListData(this._config, "commands");
|
||||
const schema = this._schema(stateObj, data.customize);
|
||||
|
||||
return html`
|
||||
<ha-form
|
||||
.hass=${this.hass}
|
||||
.data=${this._config}
|
||||
.data=${data}
|
||||
.schema=${schema}
|
||||
.computeLabel=${this._computeLabelCallback}
|
||||
@value-changed=${this._valueChanged}
|
||||
@@ -77,23 +77,27 @@ export class HuiVacuumCommandsCardFeatureEditor
|
||||
}
|
||||
|
||||
private _valueChanged(ev: CustomEvent): void {
|
||||
fireEvent(this, "config-changed", { config: ev.detail.value });
|
||||
const stateObj = this.context?.entity_id
|
||||
? this.hass!.states[this.context.entity_id]
|
||||
: undefined;
|
||||
const defaults = VACUUM_DEFAULT_COMMANDS.filter(
|
||||
(command) => stateObj && supportsVacuumCommand(stateObj, command)
|
||||
);
|
||||
const config =
|
||||
processCustomizableListValue<VacuumCommandsCardFeatureConfig>(
|
||||
ev.detail.value,
|
||||
"commands",
|
||||
defaults
|
||||
);
|
||||
fireEvent(this, "config-changed", { config });
|
||||
}
|
||||
|
||||
private _computeLabelCallback = (
|
||||
schema: SchemaUnion<ReturnType<typeof this._schema>>
|
||||
) => {
|
||||
switch (schema.name) {
|
||||
case "commands":
|
||||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.features.types.vacuum-commands.${schema.name}`
|
||||
);
|
||||
default:
|
||||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.generic.${schema.name}`
|
||||
);
|
||||
}
|
||||
};
|
||||
) =>
|
||||
this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.features.types.vacuum-commands.${schema.name}`
|
||||
);
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -1105,11 +1105,11 @@ class HUIRoot extends LitElement {
|
||||
const lovelace = this.lovelace!;
|
||||
const oldIndex = this._curView as number;
|
||||
const newIndex = (this._curView as number) - 1;
|
||||
this._curView = newIndex;
|
||||
if (!this.config.views[oldIndex].path) {
|
||||
this._navigateToView(newIndex, true);
|
||||
}
|
||||
lovelace.saveConfig(swapView(lovelace.config, oldIndex, newIndex));
|
||||
this._selectView(newIndex);
|
||||
}
|
||||
|
||||
private _moveViewRight(ev) {
|
||||
@@ -1120,11 +1120,11 @@ class HUIRoot extends LitElement {
|
||||
const lovelace = this.lovelace!;
|
||||
const oldIndex = this._curView as number;
|
||||
const newIndex = (this._curView as number) + 1;
|
||||
this._curView = newIndex;
|
||||
if (!this.config.views[oldIndex].path) {
|
||||
this._navigateToView(newIndex, true);
|
||||
}
|
||||
lovelace.saveConfig(swapView(lovelace.config, oldIndex, newIndex));
|
||||
this._selectView(newIndex);
|
||||
}
|
||||
|
||||
private _addView() {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/* eslint-disable no-console */
|
||||
import type { ReactiveElement, PropertyValues } from "lit";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { mainWindow } from "../common/dom/get_main_window";
|
||||
import { closeLastDialog } from "../dialogs/make-dialog-manager";
|
||||
import type { ProvideHassElement } from "../mixins/provide-hass-lit-mixin";
|
||||
@@ -38,6 +39,21 @@ export const urlSyncMixin = <
|
||||
protected firstUpdated(changedProperties: PropertyValues<this>): void {
|
||||
super.firstUpdated(changedProperties);
|
||||
if (mainWindow.history.state?.dialog) {
|
||||
const refreshUrl = mainWindow.history.state.refreshUrl;
|
||||
if (typeof refreshUrl === "string") {
|
||||
// Page was refreshed while a dialog had stashed an intended
|
||||
// destination URL. Clean up the stale dialog state and route
|
||||
// to the intended URL. We bypass navigate() because its
|
||||
// ensureDialogsClosed loop would spin until timeout on the
|
||||
// dangling state.dialog with no actual dialog open.
|
||||
mainWindow.history.replaceState(null, "", refreshUrl);
|
||||
// Defer: the host element's firstUpdated registers the
|
||||
// location-changed listener after super.firstUpdated() returns.
|
||||
setTimeout(() => {
|
||||
fireEvent(mainWindow, "location-changed", { replace: true });
|
||||
});
|
||||
return;
|
||||
}
|
||||
// this is a page refresh with a dialog open
|
||||
// the dialog stack must be empty in this case so this state should be cleaned up
|
||||
mainWindow.history.back();
|
||||
|
||||
@@ -1693,14 +1693,17 @@
|
||||
"last_triggered": "Last triggered"
|
||||
},
|
||||
"add_to": {
|
||||
"title": "Add to",
|
||||
"actions": {
|
||||
"automation_trigger": "Create new automation using {target} as a trigger",
|
||||
"automation_condition": "Create new automation using {target} as a condition",
|
||||
"automation_action": "Create new automation using {target} in an action",
|
||||
"script_action": "Create new script using {target} in an action",
|
||||
"scene": "Create new scene using {target}"
|
||||
"title": "Add {target} to…",
|
||||
"item": "Add to…",
|
||||
"action_options": {
|
||||
"automation_trigger": "Create as a new trigger",
|
||||
"automation_condition": "Create as a new condition",
|
||||
"automation_action": "Create as a new action",
|
||||
"script_action": "Create as a new action",
|
||||
"scene": "Create as a new scene"
|
||||
},
|
||||
"automations_heading": "Automations",
|
||||
"scripts_heading": "Scripts",
|
||||
"app_actions": "App actions",
|
||||
"no_actions": "No actions available",
|
||||
"action_failed": "Failed to perform the action {error}"
|
||||
@@ -10150,6 +10153,7 @@
|
||||
},
|
||||
"vacuum-commands": {
|
||||
"label": "Vacuum commands",
|
||||
"customize": "Customize commands",
|
||||
"commands": "Commands",
|
||||
"commands_list": {
|
||||
"start_pause": "[%key:ui::dialogs::more_info_control::vacuum::start_pause%]",
|
||||
@@ -10221,7 +10225,9 @@
|
||||
},
|
||||
"counter-actions": {
|
||||
"label": "Counter actions",
|
||||
"actions": {
|
||||
"customize": "Customize actions",
|
||||
"actions": "Actions",
|
||||
"actions_list": {
|
||||
"increment": "Increment",
|
||||
"decrement": "Decrement",
|
||||
"reset": "Reset"
|
||||
@@ -10284,6 +10290,7 @@
|
||||
},
|
||||
"lawn-mower-commands": {
|
||||
"label": "Lawn mower commands",
|
||||
"customize": "Customize commands",
|
||||
"commands": "Commands",
|
||||
"commands_list": {
|
||||
"start_pause": "Start Pause",
|
||||
@@ -11199,7 +11206,7 @@
|
||||
},
|
||||
"landing-page": {
|
||||
"header": "Preparing Home Assistant",
|
||||
"subheader": "This may take 20 minutes or more",
|
||||
"subheader": "The latest version of Home Assistant is being downloaded. This may take 20 minutes or more.",
|
||||
"show_details": "Show details",
|
||||
"hide_details": "Hide details",
|
||||
"network_issue": {
|
||||
|
||||
Reference in New Issue
Block a user