mirror of
https://github.com/home-assistant/frontend.git
synced 2026-09-10 05:41:44 +00:00
Compare commits
8
Commits
map-zone-dialog
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
58de8df46a | ||
|
|
3447e1d862 | ||
|
|
105a2a2423 | ||
|
|
27efa82a89 | ||
|
|
19cf7f2613 | ||
|
|
fcd25e41a0 | ||
|
|
f942a5afe5 | ||
|
|
89b745b851 |
+1
-1
@@ -234,6 +234,6 @@
|
||||
},
|
||||
"packageManager": "[email protected]",
|
||||
"volta": {
|
||||
"node": "24.20.0"
|
||||
"node": "24.21.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import { mdiCommentTextOutline } from "@mdi/js";
|
||||
import type { HassServiceTarget } from "home-assistant-js-websocket";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { truncateWithEllipsis } from "../../common/string/truncate-with-ellipsis";
|
||||
import type { Condition } from "../../data/automation";
|
||||
import type { ConditionDescription } from "../../data/condition";
|
||||
import { internationalizationContext } from "../../data/context";
|
||||
import "../../panels/config/automation/ha-automation-row-behavior";
|
||||
import "../../panels/config/automation/ha-automation-row-options";
|
||||
import { getDeviceTarget } from "../../panels/config/automation/target/get_device_target";
|
||||
import { getEntityTarget } from "../../panels/config/automation/target/get_entity_target";
|
||||
import "../../panels/config/automation/target/ha-automation-row-targets";
|
||||
import "../ha-svg-icon";
|
||||
import "../ha-tooltip";
|
||||
|
||||
@customElement("ha-automation-condition-summary")
|
||||
export class HaAutomationConditionSummary extends LitElement {
|
||||
@property() public label = "";
|
||||
|
||||
@property({ attribute: false }) public condition?: Condition;
|
||||
|
||||
@property({ attribute: false }) public description?: ConditionDescription;
|
||||
|
||||
@property({ attribute: false }) public isNew = false;
|
||||
|
||||
@state()
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
private _i18n!: ContextType<typeof internationalizationContext>;
|
||||
|
||||
private _getEntityTarget = memoizeOne(getEntityTarget);
|
||||
|
||||
private _getDeviceTarget = memoizeOne(getDeviceTarget);
|
||||
|
||||
protected render() {
|
||||
const descriptionHasTarget = "target" in (this.description || {});
|
||||
const hasEntityTarget =
|
||||
this.condition?.condition === "state" ||
|
||||
this.condition?.condition === "numeric_state";
|
||||
const targetRequired =
|
||||
(descriptionHasTarget || hasEntityTarget) && !this.isNew;
|
||||
const note = this.condition?.note?.trim();
|
||||
|
||||
let target: HassServiceTarget | undefined;
|
||||
if (this.condition) {
|
||||
if (descriptionHasTarget && "target" in this.condition) {
|
||||
target = this.condition.target;
|
||||
} else if (
|
||||
hasEntityTarget &&
|
||||
"entity_id" in this.condition &&
|
||||
this.condition.entity_id
|
||||
) {
|
||||
target = this._getEntityTarget(this.condition.entity_id);
|
||||
} else if ("device_id" in this.condition && this.condition.device_id) {
|
||||
target = this._getDeviceTarget(this.condition.device_id);
|
||||
}
|
||||
}
|
||||
|
||||
return html`
|
||||
<h3>
|
||||
${this.label}
|
||||
${
|
||||
this.description && this.condition
|
||||
? html`<ha-automation-row-behavior
|
||||
mode="condition"
|
||||
.config=${this.condition}
|
||||
></ha-automation-row-behavior>`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
target !== undefined || targetRequired
|
||||
? html`<ha-automation-row-targets
|
||||
.target=${target}
|
||||
.targetRequired=${targetRequired}
|
||||
.selector=${
|
||||
this.description?.target
|
||||
? { target: this.description.target }
|
||||
: undefined
|
||||
}
|
||||
.interactive=${this.condition?.condition !== "device"}
|
||||
></ha-automation-row-targets>`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
this.description && this.condition
|
||||
? html`<ha-automation-row-options
|
||||
.config=${this.condition}
|
||||
></ha-automation-row-options>`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
note
|
||||
? html`
|
||||
<ha-svg-icon
|
||||
id="note-icon"
|
||||
tabindex="0"
|
||||
role="img"
|
||||
.path=${mdiCommentTextOutline}
|
||||
aria-label=${this._i18n.localize(
|
||||
"ui.panel.config.automation.editor.note.label"
|
||||
)}
|
||||
class="note-indicator"
|
||||
></ha-svg-icon>
|
||||
<ha-tooltip for="note-icon"
|
||||
><p>${truncateWithEllipsis(note, 250)}</p></ha-tooltip
|
||||
>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
</h3>
|
||||
`;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
min-width: 0;
|
||||
}
|
||||
h3 {
|
||||
margin: 0;
|
||||
font-size: inherit;
|
||||
font-weight: inherit;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--ha-space-2);
|
||||
padding: var(--ha-space-2) 0;
|
||||
min-height: 32px;
|
||||
max-width: 100%;
|
||||
}
|
||||
.note-indicator {
|
||||
color: var(--ha-color-on-neutral-normal);
|
||||
}
|
||||
ha-tooltip {
|
||||
cursor: default;
|
||||
}
|
||||
ha-tooltip::part(body) {
|
||||
cursor: default;
|
||||
max-width: 300px;
|
||||
}
|
||||
ha-tooltip p {
|
||||
white-space: pre-wrap;
|
||||
margin: 0;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-automation-condition-summary": HaAutomationConditionSummary;
|
||||
}
|
||||
}
|
||||
@@ -50,7 +50,11 @@ export class HaAnalytics extends LitElement {
|
||||
.preference=${"base"}
|
||||
.disabled=${loading}
|
||||
name="base"
|
||||
></ha-switch>
|
||||
>
|
||||
${this.localize(
|
||||
`ui.panel.${this.translationKeyPanel}.analytics.preferences.base.title`
|
||||
)}
|
||||
</ha-switch>
|
||||
</ha-row-item>
|
||||
${ADDITIONAL_PREFERENCES.map(
|
||||
(preference) => html`
|
||||
@@ -72,7 +76,11 @@ export class HaAnalytics extends LitElement {
|
||||
.checked=${!!this.analytics?.preferences[preference]}
|
||||
.preference=${preference}
|
||||
name=${preference}
|
||||
></ha-switch>
|
||||
>
|
||||
${this.localize(
|
||||
`ui.panel.${this.translationKeyPanel}.analytics.preferences.${preference}.title`
|
||||
)}
|
||||
</ha-switch>
|
||||
${
|
||||
baseEnabled
|
||||
? nothing
|
||||
@@ -106,7 +114,11 @@ export class HaAnalytics extends LitElement {
|
||||
.preference=${"diagnostics"}
|
||||
.disabled=${loading}
|
||||
name="diagnostics"
|
||||
></ha-switch>
|
||||
>
|
||||
${this.localize(
|
||||
`ui.panel.${this.translationKeyPanel}.analytics.preferences.diagnostics.title`
|
||||
)}
|
||||
</ha-switch>
|
||||
</ha-row-item>
|
||||
`;
|
||||
}
|
||||
@@ -143,6 +155,19 @@ export class HaAnalytics extends LitElement {
|
||||
color: var(--error-color);
|
||||
}
|
||||
|
||||
/* The visible headline already names the row. Keep the switch's
|
||||
slotted label available to assistive technology without repeating it. */
|
||||
ha-switch::part(label) {
|
||||
position: absolute;
|
||||
overflow: hidden;
|
||||
clip: rect(0 0 0 0);
|
||||
height: 1px;
|
||||
width: 1px;
|
||||
margin: -1px;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
ha-row-item {
|
||||
--ha-row-item-padding-inline: 0;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import { domainToName } from "../../data/integration";
|
||||
import type { DataEntryFlowDialogParams } from "./show-dialog-data-entry-flow";
|
||||
import {
|
||||
loadDataEntryFlowDialog,
|
||||
loadFlowStepTranslations,
|
||||
showFlowDialog,
|
||||
} from "./show-dialog-data-entry-flow";
|
||||
|
||||
@@ -32,6 +33,7 @@ export const showConfigFlowDialog = (
|
||||
// Used as fallback if no header defined for step
|
||||
hass.loadBackendTranslation("title", handler),
|
||||
]);
|
||||
await loadFlowStepTranslations(hass, step);
|
||||
return step;
|
||||
},
|
||||
fetchFlow: async (hass, flowId) => {
|
||||
@@ -45,9 +47,14 @@ export const showConfigFlowDialog = (
|
||||
// Used as fallback if no header defined for step
|
||||
hass.loadBackendTranslation("title", step.handler),
|
||||
]);
|
||||
await loadFlowStepTranslations(hass, step);
|
||||
return step;
|
||||
},
|
||||
handleFlowStep: async (hass, flowId, data) => {
|
||||
const step = await handleConfigFlowStep(hass, flowId, data);
|
||||
await loadFlowStepTranslations(hass, step);
|
||||
return step;
|
||||
},
|
||||
handleFlowStep: handleConfigFlowStep,
|
||||
deleteFlow: deleteConfigFlow,
|
||||
|
||||
renderAbortDescription(hass, step) {
|
||||
|
||||
@@ -154,6 +154,19 @@ export interface FlowConfig {
|
||||
export type LoadingReason =
|
||||
"loading_handlers" | "loading_flow" | "loading_step";
|
||||
|
||||
/**
|
||||
* Load the translations a step resolves against when it points at another
|
||||
* integration, which owns strings shared between integrations.
|
||||
*/
|
||||
export const loadFlowStepTranslations = async (
|
||||
hass: HomeAssistant,
|
||||
step: DataEntryFlowStep
|
||||
): Promise<void> => {
|
||||
if ("translation_domain" in step && step.translation_domain) {
|
||||
await hass.loadBackendTranslation("config", step.translation_domain);
|
||||
}
|
||||
};
|
||||
|
||||
export interface DataEntryFlowDialogParams {
|
||||
startFlowHandler?: string;
|
||||
searchQuery?: string;
|
||||
|
||||
@@ -34,6 +34,8 @@ export const showSubConfigFlowDialog = (
|
||||
hass.loadBackendTranslation("selector", configEntry.domain),
|
||||
// Used as fallback if no header defined for step
|
||||
hass.loadBackendTranslation("title", configEntry.domain),
|
||||
// Shared abort reasons live in the homeassistant integration
|
||||
hass.loadBackendTranslation("config", "homeassistant"),
|
||||
]);
|
||||
return step;
|
||||
},
|
||||
@@ -46,16 +48,24 @@ export const showSubConfigFlowDialog = (
|
||||
configEntry.domain
|
||||
);
|
||||
await hass.loadBackendTranslation("selector", configEntry.domain);
|
||||
await hass.loadBackendTranslation("config", "homeassistant");
|
||||
return step;
|
||||
},
|
||||
handleFlowStep: handleSubConfigFlowStep,
|
||||
deleteFlow: deleteSubConfigFlow,
|
||||
|
||||
renderAbortDescription(hass, step) {
|
||||
const description = hass.localize(
|
||||
`component.${step.translation_domain || configEntry.domain}.config_subentries.${flowType}.abort.${step.reason}`,
|
||||
step.description_placeholders
|
||||
);
|
||||
// A translation domain means the reason is shared by several integrations
|
||||
// and is defined once under `config`, not per subentry type
|
||||
const description = step.translation_domain
|
||||
? hass.localize(
|
||||
`component.${step.translation_domain}.config.abort.${step.reason}`,
|
||||
step.description_placeholders
|
||||
)
|
||||
: hass.localize(
|
||||
`component.${configEntry.domain}.config_subentries.${flowType}.abort.${step.reason}`,
|
||||
step.description_placeholders
|
||||
);
|
||||
|
||||
return description
|
||||
? html`
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
mdiArrowDown,
|
||||
mdiArrowUp,
|
||||
mdiCommentEditOutline,
|
||||
mdiCommentTextOutline,
|
||||
mdiContentCopy,
|
||||
mdiContentCut,
|
||||
mdiContentPaste,
|
||||
@@ -19,7 +18,6 @@ import {
|
||||
mdiStopCircleOutline,
|
||||
} from "@mdi/js";
|
||||
import deepClone from "deep-clone-simple";
|
||||
import type { HassServiceTarget } from "home-assistant-js-websocket";
|
||||
import { dump } from "js-yaml";
|
||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import { LitElement, html, nothing } from "lit";
|
||||
@@ -32,10 +30,10 @@ import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { preventDefaultStopPropagation } from "../../../../common/dom/prevent_default_stop_propagation";
|
||||
import { stopPropagation } from "../../../../common/dom/stop_propagation";
|
||||
import { capitalizeFirstLetter } from "../../../../common/string/capitalize-first-letter";
|
||||
import { truncateWithEllipsis } from "../../../../common/string/truncate-with-ellipsis";
|
||||
import { handleStructError } from "../../../../common/structs/handle-errors";
|
||||
import { copyToClipboard } from "../../../../common/util/copy-clipboard";
|
||||
import "../../../../components/automation/ha-automation-condition-live-test";
|
||||
import "../../../../components/automation/ha-automation-condition-summary";
|
||||
import "../../../../components/automation/ha-automation-row";
|
||||
import type { HaAutomationRow } from "../../../../components/automation/ha-automation-row";
|
||||
import "../../../../components/automation/ha-automation-row-event-chip";
|
||||
@@ -64,7 +62,6 @@ import {
|
||||
} from "../../../../data/config";
|
||||
import { fullEntitiesContext } from "../../../../data/context";
|
||||
import type { EntityRegistryEntry } from "../../../../data/entity/entity_registry";
|
||||
import type { TargetSelector } from "../../../../data/selector";
|
||||
import {
|
||||
showAlertDialog,
|
||||
showPromptDialog,
|
||||
@@ -73,12 +70,7 @@ import type { HomeAssistant } from "../../../../types";
|
||||
import { isMac } from "../../../../util/is_mac";
|
||||
import { showEditorToast } from "../editor-toast";
|
||||
import "../ha-automation-editor-warning";
|
||||
import "../ha-automation-row-behavior";
|
||||
import "../ha-automation-row-options";
|
||||
import { overflowStyles, rowStyles } from "../styles";
|
||||
import { getDeviceTarget } from "../target/get_device_target";
|
||||
import { getEntityTarget } from "../target/get_entity_target";
|
||||
import "../target/ha-automation-row-targets";
|
||||
import "./ha-automation-condition-editor";
|
||||
import type HaAutomationConditionEditor from "./ha-automation-condition-editor";
|
||||
import "./types/ha-automation-condition-and";
|
||||
@@ -181,26 +173,6 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
}
|
||||
|
||||
private _renderRow() {
|
||||
const descriptionHasTarget =
|
||||
"target" in (this.conditionDescriptions[this.condition.condition] || {});
|
||||
|
||||
const hasEntityTarget =
|
||||
this.condition.condition === "state" ||
|
||||
this.condition.condition === "numeric_state";
|
||||
|
||||
const target = this._getTarget(descriptionHasTarget, hasEntityTarget);
|
||||
|
||||
const targetRequired =
|
||||
(descriptionHasTarget || hasEntityTarget) && !this._isNew;
|
||||
|
||||
const conditionTargetSpec =
|
||||
this.conditionDescriptions[this.condition.condition]?.target;
|
||||
|
||||
const noteTooltipText = truncateWithEllipsis(
|
||||
this.condition.note?.trim() || "",
|
||||
250
|
||||
);
|
||||
|
||||
return html`
|
||||
${
|
||||
this.optionsInSidebar && this.condition.condition !== "trigger"
|
||||
@@ -226,56 +198,15 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
></ha-condition-icon>
|
||||
</div>`
|
||||
}
|
||||
<h3 slot="header">
|
||||
${capitalizeFirstLetter(
|
||||
<ha-automation-condition-summary
|
||||
slot="header"
|
||||
.label=${capitalizeFirstLetter(
|
||||
describeCondition(this.condition, this.hass, this._entityReg)
|
||||
)}
|
||||
${
|
||||
this._getType(this.condition, this.conditionDescriptions) ===
|
||||
"platform"
|
||||
? html`<ha-automation-row-behavior
|
||||
mode="condition"
|
||||
.config=${this.condition}
|
||||
></ha-automation-row-behavior>`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
target !== undefined || targetRequired
|
||||
? this._renderTargets(
|
||||
target,
|
||||
targetRequired,
|
||||
conditionTargetSpec,
|
||||
this.condition.condition !== "device"
|
||||
)
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
this._getType(this.condition, this.conditionDescriptions) ===
|
||||
"platform"
|
||||
? html`<ha-automation-row-options
|
||||
.config=${this.condition}
|
||||
></ha-automation-row-options>`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
this.condition.note?.trim()
|
||||
? html`
|
||||
<ha-svg-icon
|
||||
id="note-icon"
|
||||
tabindex="0"
|
||||
.path=${mdiCommentTextOutline}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.note.label"
|
||||
)}
|
||||
class="note-indicator"
|
||||
></ha-svg-icon>
|
||||
<ha-tooltip for="note-icon"
|
||||
><p>${noteTooltipText}</p></ha-tooltip
|
||||
>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
</h3>
|
||||
.condition=${this.condition}
|
||||
.description=${this.conditionDescriptions[this.condition.condition]}
|
||||
.isNew=${this._isNew}
|
||||
></ha-automation-condition-summary>
|
||||
<ha-automation-row-event-chip
|
||||
.show=${this._testing}
|
||||
.variant=${this._testingResult ? "success" : "warning"}
|
||||
@@ -621,45 +552,6 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _getEntityTarget = memoizeOne(getEntityTarget);
|
||||
|
||||
private _getDeviceTarget = memoizeOne(getDeviceTarget);
|
||||
|
||||
private _getTarget(
|
||||
descriptionHasTarget: boolean,
|
||||
hasEntityTarget: boolean
|
||||
): HassServiceTarget | undefined {
|
||||
if (descriptionHasTarget && "target" in this.condition) {
|
||||
return this.condition.target;
|
||||
}
|
||||
if (
|
||||
"entity_id" in this.condition &&
|
||||
this.condition.entity_id &&
|
||||
hasEntityTarget
|
||||
) {
|
||||
return this._getEntityTarget(this.condition.entity_id);
|
||||
}
|
||||
if ("device_id" in this.condition && this.condition.device_id) {
|
||||
return this._getDeviceTarget(this.condition.device_id);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private _renderTargets = memoizeOne(
|
||||
(
|
||||
target?: HassServiceTarget,
|
||||
targetRequired = false,
|
||||
targetSpec?: TargetSelector["target"],
|
||||
interactive = false
|
||||
) =>
|
||||
html`<ha-automation-row-targets
|
||||
.target=${target}
|
||||
.targetRequired=${targetRequired}
|
||||
.selector=${targetSpec ? { target: targetSpec } : undefined}
|
||||
.interactive=${interactive}
|
||||
></ha-automation-row-targets>`
|
||||
);
|
||||
|
||||
protected firstUpdated(changedProperties: PropertyValues<this>): void {
|
||||
super.firstUpdated(changedProperties);
|
||||
|
||||
|
||||
@@ -148,7 +148,7 @@ class DialogRestoreBackup
|
||||
<ha-dialog
|
||||
.open=${this._open}
|
||||
header-title=${dialogTitle}
|
||||
.preventScrimClose=${this.isDirtyState}
|
||||
prevent-scrim-close
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
<div class="content">
|
||||
|
||||
@@ -67,6 +67,7 @@ export class HuiBadge extends ConditionalListenerMixin<LovelaceBadgeConfig>(
|
||||
if (this.hass) {
|
||||
this._element.hass = this.hass;
|
||||
}
|
||||
this._element.preview = this.preview;
|
||||
// Update element when the visibility of the badge changes, e.g. custom badge
|
||||
this._element.addEventListener("badge-visibility-changed", (ev: Event) => {
|
||||
ev.stopPropagation();
|
||||
@@ -128,11 +129,12 @@ export class HuiBadge extends ConditionalListenerMixin<LovelaceBadgeConfig>(
|
||||
}
|
||||
}
|
||||
}
|
||||
if (changedProps.has("hass")) {
|
||||
if (changedProps.has("hass") || changedProps.has("preview")) {
|
||||
try {
|
||||
if (this.hass) {
|
||||
this._element.hass = this.hass;
|
||||
}
|
||||
this._element.preview = this.preview;
|
||||
} catch (e: any) {
|
||||
this._loadElement(createErrorBadgeConfig(e.message, null));
|
||||
}
|
||||
|
||||
@@ -28,6 +28,8 @@ export class HuiEntityFilterBadge
|
||||
|
||||
private _elements?: HuiBadge[];
|
||||
|
||||
private _placeholderBadge?: HTMLElement;
|
||||
|
||||
private _configEntities?: EntityFilterEntityConfig[];
|
||||
|
||||
private _oldEntities?: EntityFilterEntityConfig[];
|
||||
@@ -56,6 +58,7 @@ export class HuiEntityFilterBadge
|
||||
this.removeChild(this.lastChild);
|
||||
}
|
||||
this._elements = undefined;
|
||||
this._placeholderBadge = undefined;
|
||||
|
||||
this._configEntities = processConfigEntities(config.entities);
|
||||
this._oldEntities = undefined;
|
||||
@@ -69,6 +72,7 @@ export class HuiEntityFilterBadge
|
||||
protected shouldUpdate(changedProperties: PropertyValues): boolean {
|
||||
if (
|
||||
changedProperties.has("_config") ||
|
||||
changedProperties.has("preview") ||
|
||||
(changedProperties.has("hass") &&
|
||||
this._haveEntitiesChanged(
|
||||
changedProperties.get("hass") as HomeAssistant | undefined
|
||||
@@ -112,33 +116,42 @@ export class HuiEntityFilterBadge
|
||||
});
|
||||
|
||||
if (entitiesList.length === 0) {
|
||||
this.style.display = "none";
|
||||
this._oldEntities = entitiesList;
|
||||
return;
|
||||
}
|
||||
|
||||
const isSame =
|
||||
this._oldEntities &&
|
||||
entitiesList.length === this._oldEntities.length &&
|
||||
entitiesList.every((entity, idx) => entity === this._oldEntities![idx]);
|
||||
if (!this.preview) {
|
||||
this._placeholderBadge = undefined;
|
||||
this.style.display = "none";
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isSame) {
|
||||
this._elements = [];
|
||||
for (const badgeConfig of entitiesList) {
|
||||
const element = document.createElement("hui-badge");
|
||||
element.hass = this.hass;
|
||||
element.preview = this.preview;
|
||||
element.config = {
|
||||
type: "entity",
|
||||
...badgeConfig,
|
||||
};
|
||||
element.load();
|
||||
this._elements.push(element);
|
||||
this._placeholderBadge ??= document.createElement("ha-badge");
|
||||
} else {
|
||||
this._placeholderBadge = undefined;
|
||||
|
||||
const isSame =
|
||||
!changedProperties.has("preview") &&
|
||||
this._oldEntities &&
|
||||
entitiesList.length === this._oldEntities.length &&
|
||||
entitiesList.every((entity, idx) => entity === this._oldEntities![idx]);
|
||||
|
||||
if (!isSame) {
|
||||
this._elements = [];
|
||||
for (const badgeConfig of entitiesList) {
|
||||
const element = document.createElement("hui-badge");
|
||||
element.hass = this.hass;
|
||||
element.preview = this.preview;
|
||||
element.config = {
|
||||
type: "entity",
|
||||
...badgeConfig,
|
||||
};
|
||||
element.load();
|
||||
this._elements.push(element);
|
||||
}
|
||||
this._oldEntities = entitiesList;
|
||||
}
|
||||
this._oldEntities = entitiesList;
|
||||
}
|
||||
|
||||
if (!this._elements) {
|
||||
if (!this._elements && !this._placeholderBadge) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -146,8 +159,12 @@ export class HuiEntityFilterBadge
|
||||
this.removeChild(this.lastChild);
|
||||
}
|
||||
|
||||
for (const element of this._elements) {
|
||||
this.appendChild(element);
|
||||
if (this._placeholderBadge) {
|
||||
this.appendChild(this._placeholderBadge);
|
||||
} else {
|
||||
for (const element of this._elements!) {
|
||||
this.appendChild(element);
|
||||
}
|
||||
}
|
||||
|
||||
this.style.display = "flex";
|
||||
|
||||
@@ -492,8 +492,8 @@ export class HuiDialogEditCard
|
||||
max-width: var(--ha-view-sections-column-max-width, 500px);
|
||||
}
|
||||
.content .element-editor {
|
||||
padding-inline-start: var(--ha-space-1);
|
||||
padding-inline-end: var(--ha-space-2);
|
||||
margin-inline-start: var(--ha-space-1);
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,8 @@ import {
|
||||
mdiDelete,
|
||||
mdiDotsVertical,
|
||||
mdiFlask,
|
||||
mdiArrowDown,
|
||||
mdiArrowUp,
|
||||
mdiPlaylistEdit,
|
||||
} from "@mdi/js";
|
||||
import deepClone from "deep-clone-simple";
|
||||
@@ -24,7 +26,9 @@ import { storage } from "../../../../common/decorators/storage";
|
||||
import { dynamicElement } from "../../../../common/dom/dynamic-element-directive";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { stopPropagation } from "../../../../common/dom/stop_propagation";
|
||||
import { capitalizeFirstLetter } from "../../../../common/string/capitalize-first-letter";
|
||||
import { handleStructError } from "../../../../common/structs/handle-errors";
|
||||
import "../../../../components/automation/ha-automation-condition-summary";
|
||||
import "../../../../components/automation/ha-automation-row-event-chip";
|
||||
import "../../../../components/automation/ha-automation-row-live-test";
|
||||
import type { LiveTestState } from "../../../../components/automation/ha-automation-row-live-test";
|
||||
@@ -55,6 +59,13 @@ import {
|
||||
CONDITION_ROW_CONFIG_KEYS,
|
||||
pickRowConfig,
|
||||
} from "../../../../data/automation";
|
||||
import { describeCondition } from "../../../../data/automation_i18n";
|
||||
import type { ConditionDescriptions } from "../../../../data/condition";
|
||||
import {
|
||||
conditionDescriptionsContext,
|
||||
fullEntitiesContext,
|
||||
} from "../../../../data/context";
|
||||
import type { EntityRegistryEntry } from "../../../../data/entity/entity_registry";
|
||||
import { ICON_CONDITION } from "../../common/icon-condition";
|
||||
import type {
|
||||
AndCondition,
|
||||
@@ -219,6 +230,22 @@ export class HaCardConditionEditor extends LitElement {
|
||||
|
||||
@property({ attribute: false }) condition!: VisibilityCondition;
|
||||
|
||||
@property({ type: Number }) public index = 0;
|
||||
|
||||
@property({ type: Boolean }) public first = false;
|
||||
|
||||
@property({ type: Boolean }) public last = false;
|
||||
|
||||
@property({ attribute: false }) public sortableData?: VisibilityCondition;
|
||||
|
||||
@state()
|
||||
@consume({ context: fullEntitiesContext, subscribe: true })
|
||||
private _entityReg: EntityRegistryEntry[] = [];
|
||||
|
||||
@state()
|
||||
@consume({ context: conditionDescriptionsContext, subscribe: true })
|
||||
private _conditionDescriptions: ConditionDescriptions = {};
|
||||
|
||||
@state()
|
||||
@consume({ context: conditionsEntityContext, subscribe: true })
|
||||
private _entityContext?: ConditionsEntityContext;
|
||||
@@ -433,6 +460,16 @@ export class HaCardConditionEditor extends LitElement {
|
||||
|
||||
const hideLiveTest = this._hideLiveTest(condition);
|
||||
|
||||
const summaryCondition =
|
||||
condition.condition === "time"
|
||||
? { ...condition, weekday: condition.weekdays }
|
||||
: this._usesAutomationEditor ||
|
||||
CONTAINER_CONDITIONS.includes(condition.condition) ||
|
||||
(!isNoEntityCondition(condition.condition, this._noEntity) &&
|
||||
condition.condition in this._conditionDescriptions)
|
||||
? condition
|
||||
: undefined;
|
||||
|
||||
return html`
|
||||
<div class="container">
|
||||
<ha-expansion-panel left-chevron>
|
||||
@@ -462,13 +499,28 @@ export class HaCardConditionEditor extends LitElement {
|
||||
>`
|
||||
: nothing
|
||||
}
|
||||
<h3 slot="header">
|
||||
${
|
||||
this.hass.localize(
|
||||
`ui.panel.lovelace.editor.condition-editor.condition.${condition.condition}.label`
|
||||
) || condition.condition
|
||||
<ha-automation-condition-summary
|
||||
slot="header"
|
||||
.condition=${summaryCondition}
|
||||
.description=${
|
||||
summaryCondition
|
||||
? this._conditionDescriptions[condition.condition]
|
||||
: undefined
|
||||
}
|
||||
</h3>
|
||||
.label=${
|
||||
summaryCondition
|
||||
? capitalizeFirstLetter(
|
||||
describeCondition(
|
||||
summaryCondition,
|
||||
this.hass,
|
||||
this._entityReg
|
||||
)
|
||||
)
|
||||
: this.hass.localize(
|
||||
`ui.panel.lovelace.editor.condition-editor.condition.${condition.condition}.label`
|
||||
) || condition.condition
|
||||
}
|
||||
></ha-automation-condition-summary>
|
||||
<ha-automation-row-event-chip
|
||||
.show=${this._testingResult !== undefined}
|
||||
.variant=${this._testingResult ? "success" : "warning"}
|
||||
@@ -486,6 +538,7 @@ export class HaCardConditionEditor extends LitElement {
|
||||
)
|
||||
}
|
||||
</ha-automation-row-event-chip>
|
||||
<slot name="drag-handle" slot="icons"></slot>
|
||||
<ha-dropdown
|
||||
slot="icons"
|
||||
@wa-select=${this._handleAction}
|
||||
@@ -530,6 +583,18 @@ export class HaCardConditionEditor extends LitElement {
|
||||
<ha-svg-icon slot="icon" .path=${mdiContentCut}></ha-svg-icon>
|
||||
</ha-dropdown-item>
|
||||
|
||||
<ha-dropdown-item value="move_up" .disabled=${this.first}>
|
||||
${this.hass.localize("ui.panel.config.automation.editor.move_up")}
|
||||
<ha-svg-icon slot="icon" .path=${mdiArrowUp}></ha-svg-icon>
|
||||
</ha-dropdown-item>
|
||||
|
||||
<ha-dropdown-item value="move_down" .disabled=${this.last}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.move_down"
|
||||
)}
|
||||
<ha-svg-icon slot="icon" .path=${mdiArrowDown}></ha-svg-icon>
|
||||
</ha-dropdown-item>
|
||||
|
||||
<ha-dropdown-item
|
||||
value="toggle_yaml"
|
||||
.disabled=${!this._uiAvailable}
|
||||
@@ -632,6 +697,12 @@ export class HaCardConditionEditor extends LitElement {
|
||||
case "cut":
|
||||
this._cutCondition();
|
||||
return;
|
||||
case "move_up":
|
||||
fireEvent(this, "move-up");
|
||||
return;
|
||||
case "move_down":
|
||||
fireEvent(this, "move-down");
|
||||
return;
|
||||
case "toggle_yaml":
|
||||
this._yamlMode = !this._yamlMode;
|
||||
return;
|
||||
@@ -714,11 +785,6 @@ export class HaCardConditionEditor extends LitElement {
|
||||
color: var(--secondary-text-color);
|
||||
opacity: 0.9;
|
||||
}
|
||||
h3 {
|
||||
margin: 0;
|
||||
font-size: inherit;
|
||||
font-weight: inherit;
|
||||
}
|
||||
.content {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
@@ -1,17 +1,24 @@
|
||||
import { consume } from "@lit/context";
|
||||
import { mdiContentPaste, mdiPlus } from "@mdi/js";
|
||||
import { mdiContentPaste, mdiDragHorizontalVariant, mdiPlus } from "@mdi/js";
|
||||
import deepClone from "deep-clone-simple";
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import { storage } from "../../../../common/decorators/storage";
|
||||
import type {
|
||||
HASSDomCurrentTargetEvent,
|
||||
HASSDomTargetEvent,
|
||||
} from "../../../../common/dom/fire_event";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { nextRender } from "../../../../common/util/render-status";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-dropdown";
|
||||
import type { HaDropdownSelectEvent } from "../../../../components/ha-dropdown";
|
||||
import "../../../../components/ha-dropdown-item";
|
||||
import "../../../../components/ha-sortable";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../../../../types";
|
||||
import { ICON_CONDITION } from "../../common/icon-condition";
|
||||
import type {
|
||||
Condition,
|
||||
@@ -78,6 +85,20 @@ export class HaCardConditionsEditor extends LitElement {
|
||||
|
||||
private _focusLastConditionOnChange = false;
|
||||
|
||||
@state() private _rowSortSelected?: number;
|
||||
|
||||
private _conditionKeys = new WeakMap<VisibilityCondition, number>();
|
||||
|
||||
private _nextConditionKey = 0;
|
||||
|
||||
private _getKey(condition: VisibilityCondition): number {
|
||||
if (!this._conditionKeys.has(condition)) {
|
||||
this._conditionKeys.set(condition, this._nextConditionKey++);
|
||||
}
|
||||
|
||||
return this._conditionKeys.get(condition)!;
|
||||
}
|
||||
|
||||
protected firstUpdated() {
|
||||
// Automation condition editors read labels from the config fragment.
|
||||
this.hass.loadFragmentTranslation("config");
|
||||
@@ -128,62 +149,233 @@ export class HaCardConditionsEditor extends LitElement {
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<div class="conditions">
|
||||
${this.conditions.map(
|
||||
(cond, idx) => html`
|
||||
<ha-card-condition-editor
|
||||
.index=${idx}
|
||||
@duplicate-condition=${this._duplicateCondition}
|
||||
@value-changed=${this._conditionChanged}
|
||||
.hass=${this.hass}
|
||||
.condition=${cond}
|
||||
></ha-card-condition-editor>
|
||||
`
|
||||
)}
|
||||
<div>
|
||||
<ha-dropdown @wa-select=${this._addCondition}>
|
||||
<ha-button slot="trigger" appearance="filled">
|
||||
<ha-svg-icon .path=${mdiPlus} slot="start"></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.condition-editor.add"
|
||||
<ha-sortable
|
||||
handle-selector=".handle"
|
||||
draggable-selector="ha-card-condition-editor"
|
||||
group="lovelace-conditions"
|
||||
invert-swap
|
||||
@item-moved=${this._conditionMoved}
|
||||
@item-added=${this._conditionAdded}
|
||||
@item-removed=${this._conditionRemoved}
|
||||
>
|
||||
<div class="conditions">
|
||||
${repeat(
|
||||
this.conditions,
|
||||
(condition) => this._getKey(condition),
|
||||
(cond, idx) => html`
|
||||
<ha-card-condition-editor
|
||||
.sortableData=${cond}
|
||||
.index=${idx}
|
||||
.first=${idx === 0}
|
||||
.last=${idx === this.conditions.length - 1}
|
||||
@duplicate-condition=${this._duplicateCondition}
|
||||
@move-down=${this._moveDown}
|
||||
@move-up=${this._moveUp}
|
||||
@value-changed=${this._conditionChanged}
|
||||
.hass=${this.hass}
|
||||
.condition=${cond}
|
||||
>
|
||||
<div
|
||||
slot="drag-handle"
|
||||
class="handle ${
|
||||
this._rowSortSelected === idx ? "active" : ""
|
||||
}"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
aria-label=${this.hass.localize("ui.common.move")}
|
||||
aria-pressed=${this._rowSortSelected === idx}
|
||||
.index=${idx}
|
||||
@click=${this._handleDragClick}
|
||||
@keydown=${this._handleDragKeydown}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiDragHorizontalVariant}></ha-svg-icon>
|
||||
</div>
|
||||
</ha-card-condition-editor>
|
||||
`
|
||||
)}
|
||||
<div>
|
||||
<ha-dropdown @wa-select=${this._addCondition}>
|
||||
<ha-button slot="trigger" appearance="filled">
|
||||
<ha-svg-icon .path=${mdiPlus} slot="start"></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.condition-editor.add"
|
||||
)}
|
||||
</ha-button>
|
||||
${
|
||||
this._canPaste
|
||||
? html`
|
||||
<ha-dropdown-item value="paste">
|
||||
${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.edit_card.paste_condition"
|
||||
)}
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiContentPaste}
|
||||
></ha-svg-icon>
|
||||
</ha-dropdown-item>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${this._availableConditions.map(
|
||||
(condition) => html`
|
||||
<ha-dropdown-item .value=${condition}>
|
||||
${
|
||||
this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.condition-editor.condition.${condition}.label`
|
||||
) || condition
|
||||
}
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${ICON_CONDITION[condition]}
|
||||
></ha-svg-icon>
|
||||
</ha-dropdown-item>
|
||||
`
|
||||
)}
|
||||
</ha-button>
|
||||
${
|
||||
this._canPaste
|
||||
? html`
|
||||
<ha-dropdown-item value="paste">
|
||||
${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.edit_card.paste_condition"
|
||||
)}
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiContentPaste}
|
||||
></ha-svg-icon>
|
||||
</ha-dropdown-item>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${this._availableConditions.map(
|
||||
(condition) => html`
|
||||
<ha-dropdown-item .value=${condition}>
|
||||
${
|
||||
this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.condition-editor.condition.${condition}.label`
|
||||
) || condition
|
||||
}
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${ICON_CONDITION[condition]}
|
||||
></ha-svg-icon>
|
||||
</ha-dropdown-item>
|
||||
`
|
||||
)}
|
||||
</ha-dropdown>
|
||||
</ha-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ha-sortable>
|
||||
`;
|
||||
}
|
||||
|
||||
private _move(oldIndex: number, newIndex: number) {
|
||||
const conditions = [...this.conditions];
|
||||
const condition = conditions.splice(oldIndex, 1)[0];
|
||||
conditions.splice(newIndex, 0, condition);
|
||||
this.conditions = conditions;
|
||||
if (this._rowSortSelected === oldIndex) {
|
||||
this._rowSortSelected = newIndex;
|
||||
} else if (
|
||||
this._rowSortSelected !== undefined &&
|
||||
oldIndex < this._rowSortSelected &&
|
||||
newIndex >= this._rowSortSelected
|
||||
) {
|
||||
this._rowSortSelected--;
|
||||
} else if (
|
||||
this._rowSortSelected !== undefined &&
|
||||
oldIndex > this._rowSortSelected &&
|
||||
newIndex <= this._rowSortSelected
|
||||
) {
|
||||
this._rowSortSelected++;
|
||||
}
|
||||
fireEvent(this, "value-changed", { value: conditions });
|
||||
}
|
||||
|
||||
private _conditionMoved(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
this._move(ev.detail.oldIndex, ev.detail.newIndex);
|
||||
}
|
||||
|
||||
private async _conditionAdded(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
const { index, data } = ev.detail;
|
||||
if (this._rowSortSelected !== undefined && index <= this._rowSortSelected) {
|
||||
this._rowSortSelected++;
|
||||
}
|
||||
let conditions = [...this.conditions];
|
||||
conditions.splice(index, 0, data);
|
||||
this.conditions = conditions;
|
||||
await nextRender();
|
||||
if (this.conditions !== conditions && !this.conditions.includes(data)) {
|
||||
conditions = [...this.conditions];
|
||||
conditions.splice(index, 0, data);
|
||||
} else {
|
||||
conditions = this.conditions;
|
||||
}
|
||||
fireEvent(this, "value-changed", { value: conditions });
|
||||
}
|
||||
|
||||
private async _conditionRemoved(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
const { index: removedIndex } = ev.detail;
|
||||
const removed = this.conditions[removedIndex];
|
||||
if (this._rowSortSelected === removedIndex) {
|
||||
this._rowSortSelected = undefined;
|
||||
} else if (
|
||||
this._rowSortSelected !== undefined &&
|
||||
removedIndex < this._rowSortSelected
|
||||
) {
|
||||
this._rowSortSelected--;
|
||||
}
|
||||
let conditions = [...this.conditions];
|
||||
conditions.splice(removedIndex, 1);
|
||||
this.conditions = conditions;
|
||||
await nextRender();
|
||||
if (this.conditions !== conditions) {
|
||||
conditions = [...this.conditions];
|
||||
const index = conditions.indexOf(removed);
|
||||
if (index !== -1) {
|
||||
conditions.splice(index, 1);
|
||||
}
|
||||
}
|
||||
fireEvent(this, "value-changed", { value: conditions });
|
||||
}
|
||||
|
||||
private _moveUp(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
const row = ev.currentTarget as HaCardConditionEditor;
|
||||
if (!row.first) {
|
||||
this._move(row.index, row.index - 1);
|
||||
}
|
||||
}
|
||||
|
||||
private _moveDown(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
const row = ev.currentTarget as HaCardConditionEditor;
|
||||
if (!row.last) {
|
||||
this._move(row.index, row.index + 1);
|
||||
}
|
||||
}
|
||||
|
||||
private _handleDragClick(
|
||||
ev: HASSDomCurrentTargetEvent<HTMLElement & { index: number }>
|
||||
) {
|
||||
ev.stopPropagation();
|
||||
this._rowSortSelected =
|
||||
this._rowSortSelected === ev.currentTarget.index
|
||||
? undefined
|
||||
: ev.currentTarget.index;
|
||||
}
|
||||
|
||||
private _handleDragKeydown(
|
||||
ev: KeyboardEvent &
|
||||
HASSDomCurrentTargetEvent<HTMLElement & { index: number }>
|
||||
) {
|
||||
const handle = ev.currentTarget;
|
||||
const selected = this._rowSortSelected === handle.index;
|
||||
|
||||
if (ev.key === "Escape" && selected) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
this._rowSortSelected = undefined;
|
||||
return;
|
||||
}
|
||||
|
||||
if (ev.key === "Enter" || ev.key === " ") {
|
||||
ev.preventDefault();
|
||||
this._handleDragClick(ev);
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
(selected || ev.altKey) &&
|
||||
!ev.ctrlKey &&
|
||||
!ev.metaKey &&
|
||||
!ev.shiftKey &&
|
||||
(ev.key === "ArrowUp" || ev.key === "ArrowDown")
|
||||
) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
const newIndex =
|
||||
ev.key === "ArrowUp" ? handle.index - 1 : handle.index + 1;
|
||||
if (newIndex < 0 || newIndex >= this.conditions.length) {
|
||||
return;
|
||||
}
|
||||
this._move(handle.index, newIndex);
|
||||
handle.focus();
|
||||
}
|
||||
}
|
||||
|
||||
private _addCondition(ev: HaDropdownSelectEvent) {
|
||||
const value = ev.detail.item.value as string;
|
||||
const conditions = [...this.conditions];
|
||||
@@ -225,15 +417,34 @@ export class HaCardConditionsEditor extends LitElement {
|
||||
fireEvent(this, "value-changed", { value: conditions });
|
||||
}
|
||||
|
||||
private _conditionChanged(ev: CustomEvent) {
|
||||
private _conditionChanged(
|
||||
ev: ValueChangedEvent<VisibilityCondition | null> &
|
||||
HASSDomTargetEvent<HaCardConditionEditor>
|
||||
) {
|
||||
ev.stopPropagation();
|
||||
const conditions = [...this.conditions];
|
||||
const newValue = ev.detail.value;
|
||||
const index = (ev.target as any).index;
|
||||
if (
|
||||
newValue !== null &&
|
||||
(typeof newValue !== "object" || Array.isArray(newValue))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const conditions = [...this.conditions];
|
||||
const index = ev.target.index;
|
||||
|
||||
if (newValue === null) {
|
||||
conditions.splice(index, 1);
|
||||
if (this._rowSortSelected === index) {
|
||||
this._rowSortSelected = undefined;
|
||||
} else if (
|
||||
this._rowSortSelected !== undefined &&
|
||||
index < this._rowSortSelected
|
||||
) {
|
||||
this._rowSortSelected--;
|
||||
}
|
||||
} else {
|
||||
this._conditionKeys.set(newValue, this._getKey(conditions[index]));
|
||||
conditions[index] = newValue;
|
||||
}
|
||||
|
||||
@@ -252,6 +463,24 @@ export class HaCardConditionsEditor extends LitElement {
|
||||
margin-top: 12px;
|
||||
scroll-margin-top: 48px;
|
||||
}
|
||||
.handle {
|
||||
padding: var(--ha-space-1);
|
||||
cursor: move;
|
||||
cursor: grab;
|
||||
border-radius: var(--ha-border-radius-pill);
|
||||
}
|
||||
.handle:focus {
|
||||
outline: var(--wa-focus-ring);
|
||||
background: var(--ha-color-fill-neutral-quiet-resting);
|
||||
}
|
||||
.handle.active {
|
||||
outline: var(--wa-focus-ring);
|
||||
background: var(--ha-color-fill-neutral-normal-active);
|
||||
}
|
||||
.handle ha-svg-icon {
|
||||
display: block;
|
||||
pointer-events: none;
|
||||
}
|
||||
ha-dropdown {
|
||||
display: inline-block;
|
||||
margin-top: var(--ha-space-3);
|
||||
|
||||
@@ -43,6 +43,7 @@ export interface Lovelace {
|
||||
|
||||
export interface LovelaceBadge extends HTMLElement {
|
||||
hass?: HomeAssistant;
|
||||
preview?: boolean;
|
||||
connectedWhileHidden?: boolean;
|
||||
setConfig(config: LovelaceBadgeConfig): void;
|
||||
}
|
||||
|
||||
@@ -61,3 +61,59 @@ test("completes onboarding and opens the default dashboard", async ({
|
||||
expect(calls.tokenRequests[1]).toContain("dashboard-auth-code");
|
||||
expectNoPageErrors(errors);
|
||||
});
|
||||
|
||||
test("chooses analytics consent using named switches", async ({
|
||||
page,
|
||||
baseURL,
|
||||
}) => {
|
||||
const errors = trackPageErrors(page);
|
||||
const calls = await setupOnboardingMocks(page);
|
||||
|
||||
await openOnboarding(page, baseURL!);
|
||||
await createOwner(page);
|
||||
await completeCoreConfig(page);
|
||||
|
||||
const analytics = page.locator("onboarding-analytics");
|
||||
const basic = analytics.getByRole("switch", {
|
||||
name: "Basic analytics",
|
||||
exact: true,
|
||||
});
|
||||
const usage = analytics.getByRole("switch", { name: "Usage", exact: true });
|
||||
const statistics = analytics.getByRole("switch", {
|
||||
name: "Statistical data",
|
||||
exact: true,
|
||||
});
|
||||
const diagnostics = analytics.getByRole("switch", {
|
||||
name: "Diagnostics",
|
||||
exact: true,
|
||||
});
|
||||
|
||||
await expect(basic).toBeVisible();
|
||||
await basic.press("Space");
|
||||
await usage.press("Space");
|
||||
await statistics.press("Space");
|
||||
await diagnostics.press("Space");
|
||||
await expect(usage).toBeChecked();
|
||||
await expect(statistics).toBeChecked();
|
||||
await expect(diagnostics).toBeChecked();
|
||||
|
||||
// Withdrawing basic consent also withdraws its dependent categories,
|
||||
// while independently selected crash reporting stays enabled.
|
||||
await basic.press("Space");
|
||||
await expect(usage).not.toBeChecked();
|
||||
await expect(statistics).not.toBeChecked();
|
||||
await expect(diagnostics).toBeChecked();
|
||||
await completeAnalytics(page);
|
||||
await expect.poll(() => calls.analyticsCompleted).toBe(true);
|
||||
|
||||
expect(calls.analyticsPreferences).toMatchObject({
|
||||
type: "analytics/preferences",
|
||||
preferences: {
|
||||
base: false,
|
||||
usage: false,
|
||||
statistics: false,
|
||||
diagnostics: true,
|
||||
},
|
||||
});
|
||||
expectNoPageErrors(errors);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import "../../../../../src/panels/lovelace/editor/conditions/ha-card-conditions-editor";
|
||||
import type { HaCardConditionsEditor } from "../../../../../src/panels/lovelace/editor/conditions/ha-card-conditions-editor";
|
||||
import type { Condition } from "../../../../../src/panels/lovelace/common/validate-condition";
|
||||
|
||||
const CONDITIONS: Condition[] = [
|
||||
{ condition: "state", entity: "light.first", state: "on" },
|
||||
{ condition: "state", entity: "light.second", state: "off" },
|
||||
{ condition: "state", entity: "light.third", state: "on" },
|
||||
];
|
||||
|
||||
const createEditor = () => {
|
||||
const editor = document.createElement(
|
||||
"ha-card-conditions-editor"
|
||||
) as HaCardConditionsEditor;
|
||||
editor.conditions = CONDITIONS;
|
||||
return editor;
|
||||
};
|
||||
|
||||
const waitForValueChanged = (editor: HaCardConditionsEditor) =>
|
||||
new Promise<Condition[]>((resolve) => {
|
||||
editor.addEventListener(
|
||||
"value-changed",
|
||||
(ev) => resolve(ev.detail.value as Condition[]),
|
||||
{ once: true }
|
||||
);
|
||||
});
|
||||
|
||||
describe("ha-card-conditions-editor sorting", () => {
|
||||
it("moves conditions without mutating the input array", async () => {
|
||||
const editor = createEditor();
|
||||
const input = editor.conditions;
|
||||
const changed = waitForValueChanged(editor);
|
||||
const stopPropagation = vi.fn();
|
||||
|
||||
(editor as any)._conditionMoved({
|
||||
detail: { oldIndex: 0, newIndex: 2 },
|
||||
stopPropagation,
|
||||
});
|
||||
|
||||
const value = await changed;
|
||||
expect(stopPropagation).toHaveBeenCalledOnce();
|
||||
expect(value).toEqual([CONDITIONS[1], CONDITIONS[2], CONDITIONS[0]]);
|
||||
expect(value).not.toBe(input);
|
||||
expect(input).toEqual(CONDITIONS);
|
||||
});
|
||||
|
||||
it("inserts a condition received from another list", async () => {
|
||||
const editor = createEditor();
|
||||
const added: Condition = {
|
||||
condition: "user",
|
||||
users: ["test-user"],
|
||||
};
|
||||
const changed = waitForValueChanged(editor);
|
||||
|
||||
await (editor as any)._conditionAdded({
|
||||
detail: { index: 1, data: added },
|
||||
stopPropagation: vi.fn(),
|
||||
});
|
||||
|
||||
await expect(changed).resolves.toEqual([
|
||||
CONDITIONS[0],
|
||||
added,
|
||||
CONDITIONS[1],
|
||||
CONDITIONS[2],
|
||||
]);
|
||||
});
|
||||
|
||||
it("does not insert a cross-level condition twice after a parent rerender", async () => {
|
||||
const editor = createEditor();
|
||||
const added: Condition = {
|
||||
condition: "user",
|
||||
users: ["test-user"],
|
||||
};
|
||||
const changed = waitForValueChanged(editor);
|
||||
const addition = (editor as any)._conditionAdded({
|
||||
detail: { index: 1, data: added },
|
||||
stopPropagation: vi.fn(),
|
||||
});
|
||||
editor.conditions = [CONDITIONS[0], added, CONDITIONS[1], CONDITIONS[2]];
|
||||
|
||||
await addition;
|
||||
|
||||
await expect(changed).resolves.toEqual([
|
||||
CONDITIONS[0],
|
||||
added,
|
||||
CONDITIONS[1],
|
||||
CONDITIONS[2],
|
||||
]);
|
||||
});
|
||||
|
||||
it("removes a condition sent to another list", async () => {
|
||||
const editor = createEditor();
|
||||
const changed = waitForValueChanged(editor);
|
||||
|
||||
await (editor as any)._conditionRemoved({
|
||||
detail: { index: 1 },
|
||||
stopPropagation: vi.fn(),
|
||||
});
|
||||
|
||||
await expect(changed).resolves.toEqual([CONDITIONS[0], CONDITIONS[2]]);
|
||||
});
|
||||
|
||||
it("keeps keyboard selection aligned across cross-list changes", async () => {
|
||||
const editor = createEditor();
|
||||
(editor as any)._rowSortSelected = 1;
|
||||
|
||||
await (editor as any)._conditionAdded({
|
||||
detail: { index: 0, data: { condition: "screen" } },
|
||||
stopPropagation: vi.fn(),
|
||||
});
|
||||
expect((editor as any)._rowSortSelected).toBe(2);
|
||||
|
||||
await (editor as any)._conditionRemoved({
|
||||
detail: { index: 2 },
|
||||
stopPropagation: vi.fn(),
|
||||
});
|
||||
expect((editor as any)._rowSortSelected).toBeUndefined();
|
||||
});
|
||||
|
||||
it("moves a row using its non-pointer actions", async () => {
|
||||
const editor = createEditor();
|
||||
const changed = waitForValueChanged(editor);
|
||||
|
||||
(editor as any)._moveUp({
|
||||
currentTarget: { first: false, index: 1 },
|
||||
stopPropagation: vi.fn(),
|
||||
});
|
||||
|
||||
await expect(changed).resolves.toEqual([
|
||||
CONDITIONS[1],
|
||||
CONDITIONS[0],
|
||||
CONDITIONS[2],
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps keyboard sorting attached to the moved condition", () => {
|
||||
const editor = createEditor();
|
||||
(editor as any)._rowSortSelected = 1;
|
||||
|
||||
(editor as any)._moveUp({
|
||||
currentTarget: { first: false, index: 1 },
|
||||
stopPropagation: vi.fn(),
|
||||
});
|
||||
|
||||
expect((editor as any)._rowSortSelected).toBe(0);
|
||||
});
|
||||
|
||||
it("preserves a row key when a condition is replaced", () => {
|
||||
const editor = createEditor();
|
||||
const original = CONDITIONS[1];
|
||||
const replacement: Condition = {
|
||||
condition: "state",
|
||||
entity: "light.second",
|
||||
state: "unavailable",
|
||||
};
|
||||
const key = (editor as any)._getKey(original);
|
||||
|
||||
(editor as any)._conditionChanged({
|
||||
detail: { value: replacement },
|
||||
target: { index: 1 },
|
||||
stopPropagation: vi.fn(),
|
||||
});
|
||||
|
||||
expect((editor as any)._getKey(replacement)).toBe(key);
|
||||
});
|
||||
|
||||
it("keeps keyboard selection aligned after deletion", () => {
|
||||
const editor = createEditor();
|
||||
(editor as any)._rowSortSelected = 2;
|
||||
|
||||
(editor as any)._conditionChanged({
|
||||
detail: { value: null },
|
||||
target: { index: 0 },
|
||||
stopPropagation: vi.fn(),
|
||||
});
|
||||
|
||||
expect((editor as any)._rowSortSelected).toBe(1);
|
||||
});
|
||||
|
||||
it("does not move a row beyond a list boundary", () => {
|
||||
const editor = createEditor();
|
||||
const changed = vi.fn();
|
||||
editor.addEventListener("value-changed", changed);
|
||||
|
||||
(editor as any)._moveUp({
|
||||
currentTarget: { first: true, index: 0 },
|
||||
stopPropagation: vi.fn(),
|
||||
});
|
||||
(editor as any)._moveDown({
|
||||
currentTarget: { last: true, index: CONDITIONS.length - 1 },
|
||||
stopPropagation: vi.fn(),
|
||||
});
|
||||
|
||||
expect(changed).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user