mirror of
https://github.com/home-assistant/frontend.git
synced 2026-09-17 07:30:24 +00:00
Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
121fb4fdc5 | ||
|
|
1447296a8f | ||
|
|
aaf1529aa5 | ||
|
|
50624c2772 | ||
|
|
00e27b47b3 |
+3
-17
@@ -5,7 +5,6 @@ import { computeDomain } from "../common/entity/compute_domain";
|
||||
import { computeStateDomain } from "../common/entity/compute_state_domain";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { UNAVAILABLE, UNKNOWN } from "./entity/entity";
|
||||
import { isNumericEntity } from "./history";
|
||||
|
||||
const LOGBOOK_LOCALIZE_PATH = "ui.components.logbook.messages";
|
||||
@@ -281,22 +280,17 @@ const STATE_ACTION_MESSAGES: Record<
|
||||
radio_frequency: "command_sent",
|
||||
};
|
||||
|
||||
const RESTORABLE_BUTTON_DOMAINS = new Set(["button", "input_button"]);
|
||||
|
||||
export const localizeStateMessage = (
|
||||
hass: HomeAssistant,
|
||||
state: string,
|
||||
stateObj: HassEntity,
|
||||
domain: string,
|
||||
entry?: LogbookEntry
|
||||
attributes?: LogbookEntry["attributes"]
|
||||
): string => {
|
||||
if (state === UNKNOWN || state === UNAVAILABLE) {
|
||||
return hass.formatEntityState(stateObj, state);
|
||||
}
|
||||
// Events show the triggered event type, falling back to a generic label when
|
||||
// the type is unknown (the timestamp state is meaningless on its own).
|
||||
if (domain === "event") {
|
||||
const eventType = entry?.attributes?.event_type;
|
||||
const eventType = attributes?.event_type;
|
||||
if (eventType != null) {
|
||||
return hass.formatEntityAttributeValue(stateObj, "event_type", eventType);
|
||||
}
|
||||
@@ -304,15 +298,7 @@ export const localizeStateMessage = (
|
||||
}
|
||||
const actionKey: LogbookActionMessage | undefined =
|
||||
STATE_ACTION_MESSAGES[domain as keyof typeof STATE_ACTION_MESSAGES];
|
||||
const stateTimestamp = Date.parse(state);
|
||||
const matchesEntryTime =
|
||||
entry === undefined ||
|
||||
(Number.isFinite(stateTimestamp) &&
|
||||
Math.abs(stateTimestamp - entry.when * 1000) < 1000);
|
||||
if (
|
||||
actionKey &&
|
||||
(!RESTORABLE_BUTTON_DOMAINS.has(domain) || matchesEntryTime)
|
||||
) {
|
||||
if (actionKey) {
|
||||
return hass.localize(`${LOGBOOK_LOCALIZE_PATH}.${actionKey}`);
|
||||
}
|
||||
// Every other domain reuses the backend state translation, so the logbook
|
||||
|
||||
@@ -3,6 +3,7 @@ import { getCollection } from "home-assistant-js-websocket";
|
||||
import type { HuiBadge } from "../panels/lovelace/badges/hui-badge";
|
||||
import type { HuiCard } from "../panels/lovelace/cards/hui-card";
|
||||
import type { HuiSection } from "../panels/lovelace/sections/hui-section";
|
||||
import type { LovelacePath } from "../panels/lovelace/editor/lovelace-path";
|
||||
import type { Lovelace } from "../panels/lovelace/types";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type { LovelaceSectionConfig } from "./lovelace/config/section";
|
||||
@@ -18,7 +19,9 @@ export interface LovelaceViewElement extends HTMLElement {
|
||||
hass?: HomeAssistant;
|
||||
lovelace?: Lovelace;
|
||||
narrow?: boolean;
|
||||
// Temporary compatibility: custom view layouts still read the view index
|
||||
index?: number;
|
||||
path?: LovelacePath;
|
||||
cards?: HuiCard[];
|
||||
badges?: HuiBadge[];
|
||||
sections?: HuiSection[];
|
||||
@@ -31,8 +34,7 @@ export interface LovelaceSectionElement extends HTMLElement {
|
||||
hass?: HomeAssistant;
|
||||
lovelace?: Lovelace;
|
||||
preview?: boolean;
|
||||
viewIndex?: number;
|
||||
index?: number;
|
||||
path?: LovelacePath;
|
||||
cards?: HuiCard[];
|
||||
isStrategy: boolean;
|
||||
importOnly?: boolean;
|
||||
|
||||
+1
-19
@@ -1,6 +1,4 @@
|
||||
import { ensureArray } from "../common/array/ensure-array";
|
||||
import { isValidEntityId } from "../common/entity/valid_entity_id";
|
||||
import type { Context, HomeAssistant, ServiceCallRequest } from "../types";
|
||||
import type { Context, HomeAssistant } from "../types";
|
||||
import type { Action } from "./script";
|
||||
|
||||
export const callExecuteScript = (
|
||||
@@ -24,19 +22,3 @@ export const serviceCallWillDisconnect = (
|
||||
"update.home_assistant_core_update",
|
||||
"update.home_assistant_operating_system_update",
|
||||
].includes(serviceData?.entity_id));
|
||||
|
||||
// Core merges the target into the service data, so a target entity_id
|
||||
// replaces the legacy service data one rather than adding to it. Its schema
|
||||
// also accepts comma separated ids and lowercases them.
|
||||
export const getServiceCallEntityIds = (
|
||||
serviceData?: ServiceCallRequest["serviceData"],
|
||||
target?: ServiceCallRequest["target"]
|
||||
): string[] => [
|
||||
...new Set(
|
||||
(ensureArray(target?.entity_id ?? serviceData?.entity_id) ?? [])
|
||||
.filter((id): id is string => typeof id === "string")
|
||||
.flatMap((id) => id.split(","))
|
||||
.map((id) => id.trim().toLowerCase())
|
||||
.filter(isValidEntityId)
|
||||
),
|
||||
];
|
||||
|
||||
@@ -11,7 +11,6 @@ import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { consumeLocalize } from "../../../common/decorators/consume-context-entry";
|
||||
import { fireEvent, type HASSDomEvent } from "../../../common/dom/fire_event";
|
||||
import { supportsFeature } from "../../../common/entity/supports-feature";
|
||||
import type { LocalizeFunc } from "../../../common/translations/localize";
|
||||
import "../../../components/ha-attribute-icon";
|
||||
@@ -86,26 +85,12 @@ class MoreInfoLight extends LitElement {
|
||||
|
||||
private _setMainControl(ev: any) {
|
||||
ev.stopPropagation();
|
||||
this._changeMainControl(ev.currentTarget.control);
|
||||
this._mainControl = ev.currentTarget.control;
|
||||
}
|
||||
|
||||
private _resetMainControl(ev: any) {
|
||||
ev.stopPropagation();
|
||||
this._changeMainControl("brightness");
|
||||
}
|
||||
|
||||
public connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
// A container that outlives this control (e.g. more-info-content when the
|
||||
// dialog moves between entities) resyncs with the default control.
|
||||
fireEvent(this, "light-main-control-changed", {
|
||||
control: this._mainControl,
|
||||
});
|
||||
}
|
||||
|
||||
private _changeMainControl(control: MainControl) {
|
||||
this._mainControl = control;
|
||||
fireEvent(this, "light-main-control-changed", { control });
|
||||
this._mainControl = "brightness";
|
||||
}
|
||||
|
||||
private get _stateOverride() {
|
||||
@@ -414,14 +399,4 @@ declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"more-info-light": MoreInfoLight;
|
||||
}
|
||||
|
||||
interface HASSDomEvents {
|
||||
"light-main-control-changed": { control: MainControl };
|
||||
}
|
||||
|
||||
interface HTMLElementEventMap {
|
||||
"light-main-control-changed": HASSDomEvent<
|
||||
HASSDomEvents["light-main-control-changed"]
|
||||
>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { dynamicElement } from "../../common/dom/dynamic-element-directive";
|
||||
import { computeEntityName } from "../../common/entity/compute_entity_name";
|
||||
@@ -12,7 +11,6 @@ import "../../components/ha-badge";
|
||||
import type { ExtEntityRegistryEntry } from "../../data/entity/entity_registry";
|
||||
import { supportsCoverPositionCardFeature } from "../../panels/lovelace/card-features/hui-cover-position-card-feature";
|
||||
import { supportsLightBrightnessCardFeature } from "../../panels/lovelace/card-features/hui-light-brightness-card-feature";
|
||||
import { supportsLightColorTempCardFeature } from "../../panels/lovelace/card-features/hui-light-color-temp-card-feature";
|
||||
import type { LovelaceCardFeatureConfig } from "../../panels/lovelace/card-features/types";
|
||||
import type { TileCardConfig } from "../../panels/lovelace/cards/types";
|
||||
import { importMoreInfoControl } from "../../panels/lovelace/custom-card-helpers";
|
||||
@@ -27,8 +25,6 @@ interface EntityInfo {
|
||||
deviceId: string | undefined;
|
||||
}
|
||||
|
||||
type LightMainControl = HASSDomEvents["light-main-control-changed"]["control"];
|
||||
|
||||
@customElement("more-info-content")
|
||||
class MoreInfoContent extends LitElement {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
@@ -41,17 +37,6 @@ class MoreInfoContent extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public data?: Record<string, any>;
|
||||
|
||||
// Mirrors the mode selected in the light control so group members
|
||||
// show the matching slider.
|
||||
@state() private _lightMainControl: LightMainControl = "brightness";
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues<this>) {
|
||||
super.firstUpdated(changedProps);
|
||||
this.addEventListener("light-main-control-changed", (ev) => {
|
||||
this._lightMainControl = ev.detail.control;
|
||||
});
|
||||
}
|
||||
|
||||
protected render() {
|
||||
let moreInfoType: string | undefined;
|
||||
|
||||
@@ -88,10 +73,7 @@ class MoreInfoContent extends LitElement {
|
||||
? html`
|
||||
<hui-section
|
||||
.hass=${this.hass}
|
||||
.config=${this._entitiesSectionConfig(
|
||||
memberIds,
|
||||
this._lightMainControl
|
||||
)}
|
||||
.config=${this._entitiesSectionConfig(memberIds)}
|
||||
>
|
||||
</hui-section>
|
||||
`
|
||||
@@ -122,110 +104,97 @@ class MoreInfoContent extends LitElement {
|
||||
}
|
||||
);
|
||||
|
||||
private _entitiesSectionConfig = memoizeOne(
|
||||
(entityIds: string[], lightMainControl: LightMainControl) => {
|
||||
const hass = this.hass!;
|
||||
private _entitiesSectionConfig = memoizeOne((entityIds: string[]) => {
|
||||
const hass = this.hass!;
|
||||
|
||||
// Get entity names and areas for all visible entities
|
||||
const entityInfos = entityIds
|
||||
.map<EntityInfo | null>((entityId) => {
|
||||
const entry = hass.entities[entityId];
|
||||
if (entry?.hidden) {
|
||||
return null;
|
||||
}
|
||||
const stateObj = hass.states[entityId];
|
||||
if (!stateObj) {
|
||||
return null;
|
||||
}
|
||||
const entityName = computeEntityName(
|
||||
stateObj,
|
||||
hass.entities,
|
||||
hass.devices
|
||||
);
|
||||
const { area, device } = getEntityContext(
|
||||
stateObj,
|
||||
hass.entities,
|
||||
hass.devices,
|
||||
hass.areas,
|
||||
hass.floors
|
||||
);
|
||||
const areaId = area?.area_id;
|
||||
const deviceId = device?.id;
|
||||
return { entityId, entityName, areaId, deviceId };
|
||||
})
|
||||
.filter(Boolean) as EntityInfo[];
|
||||
|
||||
// Check if all entities have the same entity name
|
||||
const entityNames = new Set(entityInfos.map((info) => info.entityName));
|
||||
const allSameEntityName = entityNames.size === 1;
|
||||
|
||||
// Check if all entities have the same area
|
||||
const areaIds = new Set(entityInfos.map((info) => info.areaId));
|
||||
const allSameArea = areaIds.size === 1;
|
||||
|
||||
// Check if all entities belong to the same device
|
||||
const deviceIds = new Set(entityInfos.map((info) => info.deviceId));
|
||||
const allSameDevice = deviceIds.size === 1;
|
||||
|
||||
// Build name and state content config based on conditions. The device name
|
||||
// is redundant when every member belongs to the same device, so omit it
|
||||
// (and fall back to the entity name so the tile still has a label).
|
||||
const name: EntityNameItem[] = [];
|
||||
|
||||
if (!allSameDevice) {
|
||||
name.push({ type: "device" });
|
||||
}
|
||||
|
||||
if (!allSameEntityName || allSameDevice) {
|
||||
name.push({ type: "entity" });
|
||||
}
|
||||
|
||||
const stateContent = ["state"];
|
||||
if (!allSameArea) {
|
||||
stateContent.push("area_name");
|
||||
}
|
||||
|
||||
const cards = entityInfos.map(({ entityId }) => {
|
||||
const features: LovelaceCardFeatureConfig[] = [];
|
||||
const context = { entity_id: entityId };
|
||||
if (supportsCoverPositionCardFeature(hass, context)) {
|
||||
features.push({
|
||||
type: "cover-position",
|
||||
});
|
||||
} else if (lightMainControl === "color") {
|
||||
// There is no RGB tile feature yet, so when the group is set to
|
||||
// color the members show no control rather than a mismatched
|
||||
// brightness slider.
|
||||
} else if (
|
||||
lightMainControl === "color_temp" &&
|
||||
supportsLightColorTempCardFeature(hass, context)
|
||||
) {
|
||||
features.push({
|
||||
type: "light-color-temp",
|
||||
});
|
||||
} else if (supportsLightBrightnessCardFeature(hass, context)) {
|
||||
features.push({
|
||||
type: "light-brightness",
|
||||
});
|
||||
// Get entity names and areas for all visible entities
|
||||
const entityInfos = entityIds
|
||||
.map<EntityInfo | null>((entityId) => {
|
||||
const entry = hass.entities[entityId];
|
||||
if (entry?.hidden) {
|
||||
return null;
|
||||
}
|
||||
const stateObj = hass.states[entityId];
|
||||
if (!stateObj) {
|
||||
return null;
|
||||
}
|
||||
const entityName = computeEntityName(
|
||||
stateObj,
|
||||
hass.entities,
|
||||
hass.devices
|
||||
);
|
||||
const { area, device } = getEntityContext(
|
||||
stateObj,
|
||||
hass.entities,
|
||||
hass.devices,
|
||||
hass.areas,
|
||||
hass.floors
|
||||
);
|
||||
const areaId = area?.area_id;
|
||||
const deviceId = device?.id;
|
||||
return { entityId, entityName, areaId, deviceId };
|
||||
})
|
||||
.filter(Boolean) as EntityInfo[];
|
||||
|
||||
return {
|
||||
type: "tile",
|
||||
entity: entityId,
|
||||
name,
|
||||
state_content: stateContent,
|
||||
features_position: "inline",
|
||||
features,
|
||||
grid_options: { columns: 12 },
|
||||
} as TileCardConfig;
|
||||
});
|
||||
// Check if all entities have the same entity name
|
||||
const entityNames = new Set(entityInfos.map((info) => info.entityName));
|
||||
const allSameEntityName = entityNames.size === 1;
|
||||
|
||||
// Check if all entities have the same area
|
||||
const areaIds = new Set(entityInfos.map((info) => info.areaId));
|
||||
const allSameArea = areaIds.size === 1;
|
||||
|
||||
// Check if all entities belong to the same device
|
||||
const deviceIds = new Set(entityInfos.map((info) => info.deviceId));
|
||||
const allSameDevice = deviceIds.size === 1;
|
||||
|
||||
// Build name and state content config based on conditions. The device name
|
||||
// is redundant when every member belongs to the same device, so omit it
|
||||
// (and fall back to the entity name so the tile still has a label).
|
||||
const name: EntityNameItem[] = [];
|
||||
|
||||
if (!allSameDevice) {
|
||||
name.push({ type: "device" });
|
||||
}
|
||||
|
||||
if (!allSameEntityName || allSameDevice) {
|
||||
name.push({ type: "entity" });
|
||||
}
|
||||
|
||||
const stateContent = ["state"];
|
||||
if (!allSameArea) {
|
||||
stateContent.push("area_name");
|
||||
}
|
||||
|
||||
const cards = entityInfos.map(({ entityId }) => {
|
||||
const features: LovelaceCardFeatureConfig[] = [];
|
||||
const context = { entity_id: entityId };
|
||||
if (supportsCoverPositionCardFeature(hass, context)) {
|
||||
features.push({
|
||||
type: "cover-position",
|
||||
});
|
||||
} else if (supportsLightBrightnessCardFeature(hass, context)) {
|
||||
features.push({
|
||||
type: "light-brightness",
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
type: "grid",
|
||||
cards,
|
||||
};
|
||||
}
|
||||
);
|
||||
type: "tile",
|
||||
entity: entityId,
|
||||
name,
|
||||
state_content: stateContent,
|
||||
features_position: "inline",
|
||||
features,
|
||||
grid_options: { columns: 12 },
|
||||
} as TileCardConfig;
|
||||
});
|
||||
|
||||
return {
|
||||
type: "grid",
|
||||
cards,
|
||||
};
|
||||
});
|
||||
|
||||
static styles = css`
|
||||
hui-section {
|
||||
|
||||
@@ -184,15 +184,6 @@ interface EMOutgoingMessageAddEntityTo extends EMMessage {
|
||||
};
|
||||
}
|
||||
|
||||
interface EMOutgoingMessageEntityControlled extends EMMessage {
|
||||
type: "entity/controlled";
|
||||
payload: {
|
||||
entity_ids: string[];
|
||||
domain: string;
|
||||
service: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface EMOutgoingMessageMoreInfoOpened extends EMMessage {
|
||||
type: "more_info/opened";
|
||||
payload: {
|
||||
@@ -248,7 +239,6 @@ type EMOutgoingMessageWithoutAnswer =
|
||||
| EMOutgoingMessageImprovScan
|
||||
| EMOutgoingMessageImprovConfigureDevice
|
||||
| EMOutgoingMessageAddEntityTo
|
||||
| EMOutgoingMessageEntityControlled
|
||||
| EMOutgoingMessageFocusElement
|
||||
| EMOutgoingMessageReloadAndClearCache
|
||||
| EMOutgoingMessageAssistSettings;
|
||||
|
||||
@@ -13,13 +13,9 @@ import type { CSSResultGroup, TemplateResult, PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import {
|
||||
fireEvent,
|
||||
type HASSDomTargetEvent,
|
||||
} from "../../../common/dom/fire_event";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { navigate, replaceCurrentUrl } from "../../../common/navigate";
|
||||
import { computeRTL } from "../../../common/util/compute_rtl";
|
||||
import { debounce } from "../../../common/util/debounce";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-dropdown";
|
||||
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
|
||||
@@ -53,14 +49,9 @@ import { haStyle } from "../../../resources/styles";
|
||||
import type { HomeAssistant, Route } from "../../../types";
|
||||
import { fileDownload } from "../../../util/file_download";
|
||||
import "../../../components/ha-trace-picker";
|
||||
import "../../../components/ha-split-panel";
|
||||
import type { HaSplitPanel } from "../../../components/ha-split-panel";
|
||||
|
||||
const TABS = ["details", "timeline", "logbook", "automation_config"] as const;
|
||||
|
||||
const STORAGE_KEY_SPLIT_POSITION = "automation-trace-split-position";
|
||||
const DEFAULT_SPLIT_POSITION = 20;
|
||||
|
||||
@customElement("ha-automation-trace")
|
||||
export class HaAutomationTrace extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@@ -93,8 +84,6 @@ export class HaAutomationTrace extends LitElement {
|
||||
|
||||
@state() private _view: (typeof TABS)[number] | "blueprint" = "details";
|
||||
|
||||
@state() private _splitPosition = DEFAULT_SPLIT_POSITION;
|
||||
|
||||
@query("hat-script-graph") private _graph?: HatScriptGraph;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
@@ -102,6 +91,10 @@ export class HaAutomationTrace extends LitElement {
|
||||
? this.hass.states[this._entityId]
|
||||
: undefined;
|
||||
|
||||
const graph = this._graph;
|
||||
const trackedNodes = graph?.trackedNodes;
|
||||
const renderedNodes = graph?.renderedNodes;
|
||||
|
||||
const title = stateObj?.attributes.friendly_name || this._entityId;
|
||||
|
||||
let devButtons: TemplateResult | string = "";
|
||||
@@ -247,19 +240,94 @@ export class HaAutomationTrace extends LitElement {
|
||||
? ""
|
||||
: html`
|
||||
<div class="main">
|
||||
${
|
||||
this.narrow
|
||||
? this._renderPanes()
|
||||
: html`
|
||||
<ha-split-panel
|
||||
class="split"
|
||||
.position=${this._splitPosition}
|
||||
@wa-reposition=${this._splitRepositioned}
|
||||
<div class="graph">
|
||||
<hat-script-graph
|
||||
.trace=${this._trace}
|
||||
.selected=${this._selected?.path}
|
||||
@graph-node-selected=${this._pickNode}
|
||||
></hat-script-graph>
|
||||
</div>
|
||||
|
||||
<div class="info">
|
||||
<ha-tab-group @wa-tab-show=${this._handleTabChanged}>
|
||||
${TABS.map(
|
||||
(view) => html`
|
||||
<ha-tab-group-tab
|
||||
slot="nav"
|
||||
.active=${this._view === view}
|
||||
.panel=${view}
|
||||
>
|
||||
${this._renderPanes()}
|
||||
</ha-split-panel>
|
||||
${this.hass!.localize(
|
||||
`ui.panel.config.automation.trace.tabs.${view}`
|
||||
)}
|
||||
</ha-tab-group-tab>
|
||||
`
|
||||
}
|
||||
)}
|
||||
${
|
||||
this._trace.blueprint_inputs
|
||||
? html`
|
||||
<ha-tab-group-tab
|
||||
slot="nav"
|
||||
.active=${this._view === "blueprint"}
|
||||
panel="blueprint"
|
||||
>
|
||||
${this.hass!.localize(
|
||||
`ui.panel.config.automation.trace.tabs.blueprint_config`
|
||||
)}
|
||||
</ha-tab-group-tab>
|
||||
`
|
||||
: ""
|
||||
}
|
||||
</ha-tab-group>
|
||||
${
|
||||
this._selected === undefined ||
|
||||
this._logbookEntries === undefined ||
|
||||
trackedNodes === undefined
|
||||
? nothing
|
||||
: this._view === "details"
|
||||
? html`
|
||||
<ha-trace-path-details
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.trace=${this._trace}
|
||||
.selected=${this._selected}
|
||||
.logbookEntries=${this._logbookEntries}
|
||||
.trackedNodes=${trackedNodes}
|
||||
.renderedNodes=${renderedNodes!}
|
||||
></ha-trace-path-details>
|
||||
`
|
||||
: this._view === "automation_config"
|
||||
? html`
|
||||
<ha-trace-config
|
||||
.trace=${this._trace}
|
||||
></ha-trace-config>
|
||||
`
|
||||
: this._view === "logbook"
|
||||
? html`
|
||||
<ha-trace-logbook
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.trace=${this._trace}
|
||||
.logbookEntries=${this._logbookEntries}
|
||||
></ha-trace-logbook>
|
||||
`
|
||||
: this._view === "blueprint"
|
||||
? html`
|
||||
<ha-trace-blueprint-config
|
||||
.trace=${this._trace}
|
||||
></ha-trace-blueprint-config>
|
||||
`
|
||||
: html`
|
||||
<ha-trace-timeline
|
||||
.hass=${this.hass}
|
||||
.trace=${this._trace}
|
||||
.logbookEntries=${this._logbookEntries}
|
||||
.selected=${this._selected}
|
||||
@value-changed=${this._timelinePathPicked}
|
||||
></ha-trace-timeline>
|
||||
`
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
@@ -267,115 +335,12 @@ export class HaAutomationTrace extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderPanes(): TemplateResult {
|
||||
const graph = this._graph;
|
||||
const trackedNodes = graph?.trackedNodes;
|
||||
const renderedNodes = graph?.renderedNodes;
|
||||
|
||||
return html`
|
||||
<div class="graph" slot="start">
|
||||
<hat-script-graph
|
||||
.trace=${this._trace}
|
||||
.selected=${this._selected?.path}
|
||||
@graph-node-selected=${this._pickNode}
|
||||
></hat-script-graph>
|
||||
</div>
|
||||
|
||||
<div class="info" slot="end">
|
||||
<ha-tab-group @wa-tab-show=${this._handleTabChanged}>
|
||||
${TABS.map(
|
||||
(view) => html`
|
||||
<ha-tab-group-tab
|
||||
slot="nav"
|
||||
.active=${this._view === view}
|
||||
.panel=${view}
|
||||
>
|
||||
${this.hass!.localize(
|
||||
`ui.panel.config.automation.trace.tabs.${view}`
|
||||
)}
|
||||
</ha-tab-group-tab>
|
||||
`
|
||||
)}
|
||||
${
|
||||
this._trace!.blueprint_inputs
|
||||
? html`
|
||||
<ha-tab-group-tab
|
||||
slot="nav"
|
||||
.active=${this._view === "blueprint"}
|
||||
panel="blueprint"
|
||||
>
|
||||
${this.hass!.localize(
|
||||
`ui.panel.config.automation.trace.tabs.blueprint_config`
|
||||
)}
|
||||
</ha-tab-group-tab>
|
||||
`
|
||||
: ""
|
||||
}
|
||||
</ha-tab-group>
|
||||
${
|
||||
this._selected === undefined ||
|
||||
this._logbookEntries === undefined ||
|
||||
trackedNodes === undefined
|
||||
? nothing
|
||||
: this._view === "details"
|
||||
? html`
|
||||
<ha-trace-path-details
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.trace=${this._trace}
|
||||
.selected=${this._selected}
|
||||
.logbookEntries=${this._logbookEntries}
|
||||
.trackedNodes=${trackedNodes}
|
||||
.renderedNodes=${renderedNodes!}
|
||||
></ha-trace-path-details>
|
||||
`
|
||||
: this._view === "automation_config"
|
||||
? html`
|
||||
<ha-trace-config .trace=${this._trace}></ha-trace-config>
|
||||
`
|
||||
: this._view === "logbook"
|
||||
? html`
|
||||
<ha-trace-logbook
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.trace=${this._trace}
|
||||
.logbookEntries=${this._logbookEntries}
|
||||
></ha-trace-logbook>
|
||||
`
|
||||
: this._view === "blueprint"
|
||||
? html`
|
||||
<ha-trace-blueprint-config
|
||||
.trace=${this._trace}
|
||||
></ha-trace-blueprint-config>
|
||||
`
|
||||
: html`
|
||||
<ha-trace-timeline
|
||||
.hass=${this.hass}
|
||||
.trace=${this._trace}
|
||||
.logbookEntries=${this._logbookEntries}
|
||||
.selected=${this._selected}
|
||||
@value-changed=${this._timelinePathPicked}
|
||||
></ha-trace-timeline>
|
||||
`
|
||||
}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues<this>) {
|
||||
super.firstUpdated(changedProps);
|
||||
|
||||
this.hass.loadBackendTranslation("triggers");
|
||||
this.hass.loadBackendTranslation("conditions");
|
||||
|
||||
const storedPosition = localStorage?.[STORAGE_KEY_SPLIT_POSITION];
|
||||
if (storedPosition) {
|
||||
const parsed = parseFloat(storedPosition);
|
||||
if (!isNaN(parsed) && parsed > 0 && parsed < 100) {
|
||||
this._splitPosition = parsed;
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.automationId) {
|
||||
return;
|
||||
}
|
||||
@@ -463,19 +428,6 @@ export class HaAutomationTrace extends LitElement {
|
||||
this._selected = ev.detail;
|
||||
}
|
||||
|
||||
private _splitRepositioned(ev: HASSDomTargetEvent<HaSplitPanel>) {
|
||||
this._splitPosition = ev.target.position;
|
||||
this._storeSplitPosition();
|
||||
}
|
||||
|
||||
private _storeSplitPosition = debounce(
|
||||
() => {
|
||||
localStorage[STORAGE_KEY_SPLIT_POSITION] = String(this._splitPosition);
|
||||
},
|
||||
500,
|
||||
false
|
||||
);
|
||||
|
||||
private _refreshTraces() {
|
||||
this._loadTraces();
|
||||
}
|
||||
@@ -666,22 +618,13 @@ export class HaAutomationTrace extends LitElement {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
ha-split-panel.split {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
--ha-split-panel-min: 10%;
|
||||
--ha-split-panel-max: 80%;
|
||||
--ha-split-panel-divider-hit-area: var(--ha-space-4);
|
||||
}
|
||||
|
||||
.graph {
|
||||
border-right: 1px solid var(--divider-color);
|
||||
max-width: 50%;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
}
|
||||
hat-script-graph {
|
||||
flex: 1;
|
||||
@@ -699,8 +642,6 @@ export class HaAutomationTrace extends LitElement {
|
||||
}
|
||||
.info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
background-color: var(--card-background-color);
|
||||
}
|
||||
|
||||
@@ -454,11 +454,9 @@ export const cleanupRemovedGeneratedTriggerReferences = (
|
||||
};
|
||||
|
||||
/**
|
||||
* Assign a fresh generated ID to every referenced leaf sharing a stored ID.
|
||||
* Assign a fresh generated ID to every leaf sharing a stored ID, including manual IDs.
|
||||
* Expand trigger-condition references to all replacements for the old ID, preserving
|
||||
* their original "any of these triggers" meaning. Duplicate IDs that no trigger
|
||||
* condition references are stripped instead, so the fix does not create generated
|
||||
* IDs nobody uses. Return the original config if IDs are unique.
|
||||
* their original "any of these triggers" meaning. Return the original config if IDs are unique.
|
||||
* Templates and action data that inspect trigger.id directly are not rewritten;
|
||||
* the controller's confirmation dialog warns about that limitation.
|
||||
*/
|
||||
@@ -472,21 +470,6 @@ export const makeDuplicateTriggerIdsUnique = (
|
||||
return config;
|
||||
}
|
||||
|
||||
const referencedIds = new Set<string>();
|
||||
new AutomationTriggerConditionMapper((condition) =>
|
||||
mapReferencedTriggerIds(condition, (id) => {
|
||||
referencedIds.add(id);
|
||||
return id;
|
||||
})
|
||||
).map(config);
|
||||
|
||||
const referencedDuplicates = new Set(
|
||||
[...duplicates].filter((id) => referencedIds.has(id))
|
||||
);
|
||||
const unreferencedDuplicates = new Set(
|
||||
[...duplicates].filter((id) => !referencedIds.has(id))
|
||||
);
|
||||
|
||||
const reservedIds = new Set(ids.filter((id) => !duplicates.has(id)));
|
||||
const generatedIds = new Set<string>();
|
||||
const assignments = new Map<Trigger, string>();
|
||||
@@ -496,7 +479,7 @@ export const makeDuplicateTriggerIdsUnique = (
|
||||
// trigger its own ID, references to the old ID must expand to every new ID.
|
||||
flattenTriggers(config.triggers).forEach((trigger) => {
|
||||
const id = getTriggerId(trigger);
|
||||
if (!id || !referencedDuplicates.has(id)) {
|
||||
if (!id || !duplicates.has(id)) {
|
||||
return;
|
||||
}
|
||||
const generatedId = getGeneratedTriggerId(reservedIds, generatedIds);
|
||||
@@ -504,32 +487,14 @@ export const makeDuplicateTriggerIdsUnique = (
|
||||
replacementIds.set(id, [...(replacementIds.get(id) || []), generatedId]);
|
||||
});
|
||||
|
||||
const triggers = walkLeafTriggers(config.triggers, (trigger) => {
|
||||
if (assignments.has(trigger)) {
|
||||
return { ...trigger, id: assignments.get(trigger) };
|
||||
}
|
||||
if (isTriggerList(trigger)) {
|
||||
return trigger;
|
||||
}
|
||||
const id = getTriggerId(trigger);
|
||||
if (id && unreferencedDuplicates.has(id)) {
|
||||
const { id: _id, ...rest } = trigger;
|
||||
return rest as Trigger;
|
||||
}
|
||||
return trigger;
|
||||
}) as Trigger | Trigger[];
|
||||
|
||||
if (!referencedDuplicates.size) {
|
||||
return {
|
||||
...config,
|
||||
triggers,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...new AutomationTriggerConditionMapper((condition) =>
|
||||
mapReferencedTriggerIds(condition, (id) => replacementIds.get(id) ?? id)
|
||||
).map(config),
|
||||
triggers,
|
||||
triggers: walkLeafTriggers(config.triggers, (trigger) =>
|
||||
assignments.has(trigger)
|
||||
? { ...trigger, id: assignments.get(trigger) }
|
||||
: trigger
|
||||
) as Trigger | Trigger[],
|
||||
};
|
||||
};
|
||||
|
||||
@@ -13,12 +13,8 @@ import type { CSSResultGroup, TemplateResult, PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import {
|
||||
fireEvent,
|
||||
type HASSDomTargetEvent,
|
||||
} from "../../../common/dom/fire_event";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { navigate, replaceCurrentUrl } from "../../../common/navigate";
|
||||
import { debounce } from "../../../common/util/debounce";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-dropdown";
|
||||
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
|
||||
@@ -49,14 +45,9 @@ import { haStyle } from "../../../resources/styles";
|
||||
import type { HomeAssistant, Route } from "../../../types";
|
||||
import { fileDownload } from "../../../util/file_download";
|
||||
import "../../../components/ha-trace-picker";
|
||||
import "../../../components/ha-split-panel";
|
||||
import type { HaSplitPanel } from "../../../components/ha-split-panel";
|
||||
|
||||
const TABS = ["details", "timeline", "logbook", "config"] as const;
|
||||
|
||||
const STORAGE_KEY_SPLIT_POSITION = "script-trace-split-position";
|
||||
const DEFAULT_SPLIT_POSITION = 20;
|
||||
|
||||
@customElement("ha-script-trace")
|
||||
export class HaScriptTrace extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@@ -89,8 +80,6 @@ export class HaScriptTrace extends LitElement {
|
||||
|
||||
@state() private _view: (typeof TABS)[number] | "blueprint" = "details";
|
||||
|
||||
@state() private _splitPosition = DEFAULT_SPLIT_POSITION;
|
||||
|
||||
@query("hat-script-graph") private _graph?: HatScriptGraph;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
@@ -98,6 +87,10 @@ export class HaScriptTrace extends LitElement {
|
||||
? this.hass.states[this._entityId]
|
||||
: undefined;
|
||||
|
||||
const graph = this._graph;
|
||||
const trackedNodes = graph?.trackedNodes;
|
||||
const renderedNodes = graph?.renderedNodes;
|
||||
|
||||
const title = stateObj?.attributes.friendly_name || this._entityId;
|
||||
|
||||
let devButtons: TemplateResult | string = "";
|
||||
@@ -227,19 +220,96 @@ export class HaScriptTrace extends LitElement {
|
||||
? ""
|
||||
: html`
|
||||
<div class="main">
|
||||
${
|
||||
this.narrow
|
||||
? this._renderPanes()
|
||||
: html`
|
||||
<ha-split-panel
|
||||
class="split"
|
||||
.position=${this._splitPosition}
|
||||
@wa-reposition=${this._splitRepositioned}
|
||||
<div class="graph">
|
||||
<hat-script-graph
|
||||
.trace=${this._trace}
|
||||
.selected=${this._selected?.path}
|
||||
@graph-node-selected=${this._pickNode}
|
||||
></hat-script-graph>
|
||||
</div>
|
||||
|
||||
<div class="info">
|
||||
<ha-tab-group @wa-tab-show=${this._handleTabChanged}>
|
||||
${TABS.map(
|
||||
(view) => html`
|
||||
<ha-tab-group-tab
|
||||
slot="nav"
|
||||
.active=${this._view === view}
|
||||
.panel=${view}
|
||||
>
|
||||
${this._renderPanes()}
|
||||
</ha-split-panel>
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.automation.trace.tabs.${
|
||||
view === "config" ? "script_config" : view
|
||||
}`
|
||||
)}
|
||||
</ha-tab-group-tab>
|
||||
`
|
||||
}
|
||||
)}
|
||||
${
|
||||
this._trace.blueprint_inputs
|
||||
? html`
|
||||
<ha-tab-group-tab
|
||||
slot="nav"
|
||||
.active=${this._view === "blueprint"}
|
||||
panel="blueprint"
|
||||
>
|
||||
${this.hass!.localize(
|
||||
`ui.panel.config.automation.trace.tabs.blueprint_config`
|
||||
)}
|
||||
</ha-tab-group-tab>
|
||||
`
|
||||
: ""
|
||||
}
|
||||
</ha-tab-group>
|
||||
${
|
||||
this._selected === undefined ||
|
||||
this._logbookEntries === undefined ||
|
||||
trackedNodes === undefined
|
||||
? ""
|
||||
: this._view === "details"
|
||||
? html`
|
||||
<ha-trace-path-details
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.trace=${this._trace}
|
||||
.selected=${this._selected}
|
||||
.logbookEntries=${this._logbookEntries}
|
||||
.trackedNodes=${trackedNodes}
|
||||
.renderedNodes=${renderedNodes!}
|
||||
></ha-trace-path-details>
|
||||
`
|
||||
: this._view === "config"
|
||||
? html`
|
||||
<ha-trace-config
|
||||
.trace=${this._trace}
|
||||
></ha-trace-config>
|
||||
`
|
||||
: this._view === "logbook"
|
||||
? html`
|
||||
<ha-trace-logbook
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.trace=${this._trace}
|
||||
.logbookEntries=${this._logbookEntries}
|
||||
></ha-trace-logbook>
|
||||
`
|
||||
: this._view === "blueprint"
|
||||
? html`
|
||||
<ha-trace-blueprint-config
|
||||
.trace=${this._trace}
|
||||
></ha-trace-blueprint-config>
|
||||
`
|
||||
: html`
|
||||
<ha-trace-timeline
|
||||
.hass=${this.hass}
|
||||
.trace=${this._trace}
|
||||
.logbookEntries=${this._logbookEntries}
|
||||
.selected=${this._selected}
|
||||
@value-changed=${this._timelinePathPicked}
|
||||
></ha-trace-timeline>
|
||||
`
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
@@ -247,114 +317,9 @@ export class HaScriptTrace extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderPanes(): TemplateResult {
|
||||
const graph = this._graph;
|
||||
const trackedNodes = graph?.trackedNodes;
|
||||
const renderedNodes = graph?.renderedNodes;
|
||||
|
||||
return html`
|
||||
<div class="graph" slot="start">
|
||||
<hat-script-graph
|
||||
.trace=${this._trace}
|
||||
.selected=${this._selected?.path}
|
||||
@graph-node-selected=${this._pickNode}
|
||||
></hat-script-graph>
|
||||
</div>
|
||||
|
||||
<div class="info" slot="end">
|
||||
<ha-tab-group @wa-tab-show=${this._handleTabChanged}>
|
||||
${TABS.map(
|
||||
(view) => html`
|
||||
<ha-tab-group-tab
|
||||
slot="nav"
|
||||
.active=${this._view === view}
|
||||
.panel=${view}
|
||||
>
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.automation.trace.tabs.${
|
||||
view === "config" ? "script_config" : view
|
||||
}`
|
||||
)}
|
||||
</ha-tab-group-tab>
|
||||
`
|
||||
)}
|
||||
${
|
||||
this._trace!.blueprint_inputs
|
||||
? html`
|
||||
<ha-tab-group-tab
|
||||
slot="nav"
|
||||
.active=${this._view === "blueprint"}
|
||||
panel="blueprint"
|
||||
>
|
||||
${this.hass!.localize(
|
||||
`ui.panel.config.automation.trace.tabs.blueprint_config`
|
||||
)}
|
||||
</ha-tab-group-tab>
|
||||
`
|
||||
: ""
|
||||
}
|
||||
</ha-tab-group>
|
||||
${
|
||||
this._selected === undefined ||
|
||||
this._logbookEntries === undefined ||
|
||||
trackedNodes === undefined
|
||||
? ""
|
||||
: this._view === "details"
|
||||
? html`
|
||||
<ha-trace-path-details
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.trace=${this._trace}
|
||||
.selected=${this._selected}
|
||||
.logbookEntries=${this._logbookEntries}
|
||||
.trackedNodes=${trackedNodes}
|
||||
.renderedNodes=${renderedNodes!}
|
||||
></ha-trace-path-details>
|
||||
`
|
||||
: this._view === "config"
|
||||
? html`
|
||||
<ha-trace-config .trace=${this._trace}></ha-trace-config>
|
||||
`
|
||||
: this._view === "logbook"
|
||||
? html`
|
||||
<ha-trace-logbook
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.trace=${this._trace}
|
||||
.logbookEntries=${this._logbookEntries}
|
||||
></ha-trace-logbook>
|
||||
`
|
||||
: this._view === "blueprint"
|
||||
? html`
|
||||
<ha-trace-blueprint-config
|
||||
.trace=${this._trace}
|
||||
></ha-trace-blueprint-config>
|
||||
`
|
||||
: html`
|
||||
<ha-trace-timeline
|
||||
.hass=${this.hass}
|
||||
.trace=${this._trace}
|
||||
.logbookEntries=${this._logbookEntries}
|
||||
.selected=${this._selected}
|
||||
@value-changed=${this._timelinePathPicked}
|
||||
></ha-trace-timeline>
|
||||
`
|
||||
}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues<this>) {
|
||||
super.firstUpdated(changedProps);
|
||||
|
||||
const storedPosition = localStorage?.[STORAGE_KEY_SPLIT_POSITION];
|
||||
if (storedPosition) {
|
||||
const parsed = parseFloat(storedPosition);
|
||||
if (!isNaN(parsed) && parsed > 0 && parsed < 100) {
|
||||
this._splitPosition = parsed;
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.scriptId) {
|
||||
return;
|
||||
}
|
||||
@@ -444,19 +409,6 @@ export class HaScriptTrace extends LitElement {
|
||||
this._selected = ev.detail;
|
||||
}
|
||||
|
||||
private _splitRepositioned(ev: HASSDomTargetEvent<HaSplitPanel>) {
|
||||
this._splitPosition = ev.target.position;
|
||||
this._storeSplitPosition();
|
||||
}
|
||||
|
||||
private _storeSplitPosition = debounce(
|
||||
() => {
|
||||
localStorage[STORAGE_KEY_SPLIT_POSITION] = String(this._splitPosition);
|
||||
},
|
||||
500,
|
||||
false
|
||||
);
|
||||
|
||||
private _refreshTraces() {
|
||||
this._loadTraces();
|
||||
}
|
||||
@@ -642,22 +594,13 @@ export class HaScriptTrace extends LitElement {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
ha-split-panel.split {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
--ha-split-panel-min: 10%;
|
||||
--ha-split-panel-max: 80%;
|
||||
--ha-split-panel-divider-hit-area: var(--ha-space-4);
|
||||
}
|
||||
|
||||
.graph {
|
||||
border-right: 1px solid var(--divider-color);
|
||||
max-width: 50%;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
}
|
||||
hat-script-graph {
|
||||
flex: 1;
|
||||
@@ -675,8 +618,6 @@ export class HaScriptTrace extends LitElement {
|
||||
}
|
||||
.info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
background-color: var(--card-background-color);
|
||||
}
|
||||
|
||||
@@ -320,7 +320,13 @@ const computeLogbookValue = (
|
||||
if (item.entity_id && item.state) {
|
||||
return {
|
||||
text: stateObj
|
||||
? localizeStateMessage(hass, item.state, stateObj, domain!, item)
|
||||
? localizeStateMessage(
|
||||
hass,
|
||||
item.state,
|
||||
stateObj,
|
||||
domain!,
|
||||
item.attributes
|
||||
)
|
||||
: item.state,
|
||||
type: "state",
|
||||
};
|
||||
|
||||
@@ -6,14 +6,15 @@ import { classMap } from "lit/directives/class-map";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import { consumeLocalize } from "../../../common/decorators/consume-context-entry";
|
||||
import type { LocalizeFunc } from "../../../common/translations/localize";
|
||||
import type { HASSDomEvent } from "../../../common/dom/fire_event";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import "../../../components/ha-ripple";
|
||||
import "../../../components/ha-sortable";
|
||||
import type { HaSortableOptions } from "../../../components/ha-sortable";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import "../components/hui-badge-edit-mode";
|
||||
import { moveBadge } from "../editor/config-util";
|
||||
import type { LovelaceCardPath } from "../editor/lovelace-path";
|
||||
import type { LovelacePath } from "../editor/lovelace-path";
|
||||
import { moveAtPath } from "../editor/lovelace-path";
|
||||
import type { Lovelace } from "../types";
|
||||
import type { HuiBadge } from "./hui-badge";
|
||||
|
||||
@@ -30,7 +31,7 @@ export class HuiViewBadges extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public badges: HuiBadge[] = [];
|
||||
|
||||
@property({ attribute: false }) public viewIndex!: number;
|
||||
@property({ attribute: false }) public path!: LovelacePath;
|
||||
|
||||
@property({ type: Boolean, attribute: "show-add-label" })
|
||||
public showAddLabel!: boolean;
|
||||
@@ -82,27 +83,29 @@ export class HuiViewBadges extends LitElement {
|
||||
return this._badgeConfigKeys.get(badge)!;
|
||||
}
|
||||
|
||||
private _badgeMoved(ev) {
|
||||
private _badgeMoved(ev: HASSDomEvent<HASSDomEvents["item-moved"]>) {
|
||||
ev.stopPropagation();
|
||||
const { oldIndex, newIndex } = ev.detail;
|
||||
const newConfig = moveBadge(
|
||||
this.lovelace!.config,
|
||||
[this.viewIndex!, oldIndex],
|
||||
[this.viewIndex!, newIndex]
|
||||
const newConfig = moveAtPath(
|
||||
this.lovelace.config,
|
||||
[...this.path, oldIndex],
|
||||
[...this.path, newIndex]
|
||||
);
|
||||
this.lovelace!.saveConfig(newConfig);
|
||||
this.lovelace.saveConfig(newConfig);
|
||||
}
|
||||
|
||||
private _badgeAdded(ev) {
|
||||
private _badgeAdded(ev: HASSDomEvent<HASSDomEvents["item-added"]>) {
|
||||
ev.stopPropagation();
|
||||
const { index, data } = ev.detail;
|
||||
const oldPath = data as LovelaceCardPath;
|
||||
const newPath = [this.viewIndex!, index] as LovelaceCardPath;
|
||||
const newConfig = moveBadge(this.lovelace!.config, oldPath, newPath);
|
||||
this.lovelace!.saveConfig(newConfig);
|
||||
const oldPath = data as LovelacePath;
|
||||
const newConfig = moveAtPath(this.lovelace.config, oldPath, [
|
||||
...this.path,
|
||||
index,
|
||||
]);
|
||||
this.lovelace.saveConfig(newConfig);
|
||||
}
|
||||
|
||||
private _badgeRemoved(ev) {
|
||||
private _badgeRemoved(ev: HASSDomEvent<HASSDomEvents["item-removed"]>) {
|
||||
ev.stopPropagation();
|
||||
// Do nothing, it's handled by the "item-added" event from the new parent.
|
||||
}
|
||||
@@ -116,7 +119,7 @@ export class HuiViewBadges extends LitElement {
|
||||
}
|
||||
|
||||
private _addBadge() {
|
||||
fireEvent(this, "ll-create-badge");
|
||||
fireEvent(this, "ll-create-badge", { path: this.path });
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -139,7 +142,6 @@ export class HuiViewBadges extends LitElement {
|
||||
@drag-end=${this._dragEnd}
|
||||
group="badge"
|
||||
draggable-selector="[data-sortable]"
|
||||
.rollback=${false}
|
||||
.options=${BADGE_SORTABLE_OPTIONS}
|
||||
invert-swap
|
||||
>
|
||||
@@ -148,26 +150,20 @@ export class HuiViewBadges extends LitElement {
|
||||
badges,
|
||||
(badge) => this._getBadgeKey(badge),
|
||||
(badge, idx) => {
|
||||
const badgePath = [
|
||||
this.viewIndex,
|
||||
idx,
|
||||
] as LovelaceCardPath;
|
||||
if (!editMode) {
|
||||
return badge;
|
||||
}
|
||||
const badgePath = [...this.path, idx];
|
||||
return html`
|
||||
${
|
||||
editMode
|
||||
? html`
|
||||
<hui-badge-edit-mode
|
||||
data-sortable
|
||||
.lovelace=${this.lovelace}
|
||||
.path=${badgePath}
|
||||
.hiddenOverlay=${this._dragging}
|
||||
.sortableData=${badgePath}
|
||||
>
|
||||
${badge}
|
||||
</hui-badge-edit-mode>
|
||||
`
|
||||
: badge
|
||||
}
|
||||
<hui-badge-edit-mode
|
||||
data-sortable
|
||||
.lovelace=${this.lovelace}
|
||||
.path=${badgePath}
|
||||
.hiddenOverlay=${this._dragging}
|
||||
.sortableData=${badgePath}
|
||||
>
|
||||
${badge}
|
||||
</hui-badge-edit-mode>
|
||||
`;
|
||||
}
|
||||
)}
|
||||
@@ -238,6 +234,7 @@ export class HuiViewBadges extends LitElement {
|
||||
margin-right: -8px;
|
||||
margin-inline-end: -8px;
|
||||
margin-inline-start: 0;
|
||||
order: 2;
|
||||
}
|
||||
|
||||
.badges > * {
|
||||
@@ -252,6 +249,7 @@ export class HuiViewBadges extends LitElement {
|
||||
}
|
||||
|
||||
.add {
|
||||
order: 1;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
/**
|
||||
* Home-circle stroke lengths for the energy distribution card.
|
||||
*
|
||||
* Hourly allocation can produce used_solar + used_battery + used_grid larger
|
||||
* than net used_total when some hours export more than they produce. Dividing
|
||||
* by net used_total then overflows the circle and the leftover grid arc goes
|
||||
* negative, which browsers paint as a solid grid ring.
|
||||
*
|
||||
* These arcs use the sum of the allocated home flows as the denominator so
|
||||
* they always fit the circumference.
|
||||
*/
|
||||
export const ENERGY_DISTRIBUTION_HOME_CIRCLE_CIRCUMFERENCE = 238.76104;
|
||||
|
||||
export interface EnergyDistributionHomeCircleArcs {
|
||||
solar?: number;
|
||||
battery?: number;
|
||||
lowCarbon?: number;
|
||||
highCarbon?: number;
|
||||
grid?: number;
|
||||
}
|
||||
|
||||
export const computeEnergyDistributionHomeCircleArcs = ({
|
||||
usedSolar = 0,
|
||||
usedBattery = 0,
|
||||
usedGrid = 0,
|
||||
hasSolar,
|
||||
hasGrid,
|
||||
highCarbonConsumption,
|
||||
circumference = ENERGY_DISTRIBUTION_HOME_CIRCLE_CIRCUMFERENCE,
|
||||
}: {
|
||||
usedSolar?: number;
|
||||
usedBattery?: number;
|
||||
usedGrid?: number;
|
||||
hasSolar: boolean;
|
||||
hasGrid: boolean;
|
||||
highCarbonConsumption?: number;
|
||||
circumference?: number;
|
||||
}): EnergyDistributionHomeCircleArcs => {
|
||||
const solar = Math.max(usedSolar, 0);
|
||||
const battery = Math.max(usedBattery, 0);
|
||||
const grid = Math.max(usedGrid, 0);
|
||||
const ringTotal = solar + battery + grid;
|
||||
|
||||
const arcs: EnergyDistributionHomeCircleArcs = {};
|
||||
// Leave arcs unset so the card can fall back to the plain home border
|
||||
// instead of painting zero-length dashes over a borderless circle.
|
||||
if (ringTotal <= 0) {
|
||||
return arcs;
|
||||
}
|
||||
|
||||
const share = (value: number): number => circumference * (value / ringTotal);
|
||||
|
||||
if (hasSolar) {
|
||||
arcs.solar = share(solar);
|
||||
}
|
||||
if (battery > 0) {
|
||||
arcs.battery = share(battery);
|
||||
}
|
||||
if (hasGrid) {
|
||||
if (highCarbonConsumption !== undefined) {
|
||||
const highCarbon = Math.min(Math.max(highCarbonConsumption, 0), grid);
|
||||
arcs.highCarbon = share(highCarbon);
|
||||
// Keep 0 defined: the card mounts the home SVG when solar or
|
||||
// lowCarbon is defined, not when the low-carbon stroke is painted.
|
||||
arcs.lowCarbon = share(grid - highCarbon);
|
||||
arcs.grid = arcs.highCarbon;
|
||||
} else {
|
||||
arcs.grid = share(grid);
|
||||
}
|
||||
}
|
||||
|
||||
return arcs;
|
||||
};
|
||||
@@ -37,10 +37,8 @@ import type { LovelaceCard } from "../../types";
|
||||
import type { EnergyDistributionCardConfig } from "../types";
|
||||
import { formatNumber } from "../../../../common/number/format_number";
|
||||
import { round } from "../../../../common/number/round";
|
||||
import {
|
||||
ENERGY_DISTRIBUTION_HOME_CIRCLE_CIRCUMFERENCE as CIRCLE_CIRCUMFERENCE,
|
||||
computeEnergyDistributionHomeCircleArcs,
|
||||
} from "./energy-distribution-home-circle";
|
||||
|
||||
const CIRCLE_CIRCUMFERENCE = 238.76104;
|
||||
|
||||
// Flows are differences of sums; anything that rounds to 0 Wh is noise
|
||||
const hasFlow = (value: number | null): value is number =>
|
||||
@@ -323,8 +321,22 @@ class HuiEnergyDistrubutionCard
|
||||
|
||||
const totalHomeConsumption = Math.max(0, consumption.total.used_total);
|
||||
|
||||
let homeSolarCircumference: number | undefined;
|
||||
if (hasSolarProduction) {
|
||||
homeSolarCircumference =
|
||||
CIRCLE_CIRCUMFERENCE * (solarConsumption! / totalHomeConsumption);
|
||||
}
|
||||
|
||||
let homeBatteryCircumference: number | undefined;
|
||||
if (batteryConsumption) {
|
||||
homeBatteryCircumference =
|
||||
CIRCLE_CIRCUMFERENCE * (batteryConsumption / totalHomeConsumption);
|
||||
}
|
||||
|
||||
let lowCarbonEnergy: number | undefined;
|
||||
let highCarbonConsumption: number | undefined;
|
||||
|
||||
let homeLowCarbonCircumference: number | undefined;
|
||||
let homeHighCarbonCircumference: number | undefined;
|
||||
|
||||
// This fallback is used in the demo
|
||||
let electricityMapUrl = "https://app.electricitymaps.com";
|
||||
@@ -348,6 +360,7 @@ class HuiEnergyDistrubutionCard
|
||||
if (highCarbonEnergy !== null) {
|
||||
lowCarbonEnergy = totalFromGrid - highCarbonEnergy;
|
||||
|
||||
let highCarbonConsumption: number;
|
||||
if (gridConsumption !== totalFromGrid) {
|
||||
// Only get the part that was used for consumption and not the battery
|
||||
highCarbonConsumption =
|
||||
@@ -355,23 +368,18 @@ class HuiEnergyDistrubutionCard
|
||||
} else {
|
||||
highCarbonConsumption = highCarbonEnergy;
|
||||
}
|
||||
|
||||
homeHighCarbonCircumference =
|
||||
CIRCLE_CIRCUMFERENCE * (highCarbonConsumption / totalHomeConsumption);
|
||||
|
||||
homeLowCarbonCircumference =
|
||||
CIRCLE_CIRCUMFERENCE -
|
||||
(homeSolarCircumference || 0) -
|
||||
(homeBatteryCircumference || 0) -
|
||||
homeHighCarbonCircumference;
|
||||
}
|
||||
}
|
||||
|
||||
const {
|
||||
solar: homeSolarCircumference,
|
||||
battery: homeBatteryCircumference,
|
||||
lowCarbon: homeLowCarbonCircumference,
|
||||
grid: homeGridCircumference,
|
||||
} = computeEnergyDistributionHomeCircleArcs({
|
||||
usedSolar: solarConsumption ?? 0,
|
||||
usedBattery: batteryConsumption ?? 0,
|
||||
usedGrid: gridConsumption,
|
||||
hasSolar: hasSolarProduction,
|
||||
hasGrid: Boolean(hasGrid),
|
||||
highCarbonConsumption,
|
||||
});
|
||||
|
||||
const totalLines =
|
||||
gridConsumption +
|
||||
(solarConsumption || 0) +
|
||||
@@ -662,8 +670,16 @@ class HuiEnergyDistrubutionCard
|
||||
cx="40"
|
||||
cy="40"
|
||||
r="38"
|
||||
stroke-dasharray="${homeGridCircumference} ${
|
||||
CIRCLE_CIRCUMFERENCE - (homeGridCircumference ?? 0)
|
||||
stroke-dasharray="${
|
||||
homeHighCarbonCircumference ??
|
||||
CIRCLE_CIRCUMFERENCE -
|
||||
homeSolarCircumference! -
|
||||
(homeBatteryCircumference || 0)
|
||||
} ${
|
||||
homeHighCarbonCircumference !== undefined
|
||||
? CIRCLE_CIRCUMFERENCE - homeHighCarbonCircumference
|
||||
: homeSolarCircumference! +
|
||||
(homeBatteryCircumference || 0)
|
||||
}"
|
||||
stroke-dashoffset="0"
|
||||
shape-rendering="geometricPrecision"
|
||||
|
||||
@@ -27,19 +27,15 @@ import {
|
||||
} from "../../../data/lovelace/config/badge";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import { showEditBadgeDialog } from "../editor/badge-editor/show-edit-badge-dialog";
|
||||
import type { LovelaceCardPath } from "../editor/lovelace-path";
|
||||
import {
|
||||
findLovelaceItems,
|
||||
getLovelaceContainerPath,
|
||||
parseLovelaceCardPath,
|
||||
} from "../editor/lovelace-path";
|
||||
import type { LovelacePath } from "../editor/lovelace-path";
|
||||
import { getAtPath, getParentPath } from "../editor/lovelace-path";
|
||||
import type { Lovelace } from "../types";
|
||||
|
||||
@customElement("hui-badge-edit-mode")
|
||||
export class HuiBadgeEditMode extends LitElement {
|
||||
@property({ attribute: false }) public lovelace!: Lovelace;
|
||||
|
||||
@property({ type: Array }) public path!: LovelaceCardPath;
|
||||
@property({ attribute: false }) public path!: LovelacePath;
|
||||
|
||||
@property({ attribute: "hidden-overlay", type: Boolean })
|
||||
public hiddenOverlay = false;
|
||||
@@ -63,11 +59,15 @@ export class HuiBadgeEditMode extends LitElement {
|
||||
subscribe: false,
|
||||
storage: "sessionStorage",
|
||||
})
|
||||
protected _clipboard?: string | Partial<LovelaceBadgeConfig>;
|
||||
protected _clipboard?: LovelaceBadgeConfig;
|
||||
|
||||
private get _badges() {
|
||||
const containerPath = getLovelaceContainerPath(this.path!);
|
||||
return findLovelaceItems("badges", this.lovelace!.config, containerPath)!;
|
||||
private get _badgeConfig() {
|
||||
return ensureBadgeConfig(
|
||||
getAtPath<Partial<LovelaceBadgeConfig> | string>(
|
||||
this.lovelace.config,
|
||||
this.path
|
||||
)!
|
||||
);
|
||||
}
|
||||
|
||||
private _touchStarted = false;
|
||||
@@ -207,33 +207,28 @@ export class HuiBadgeEditMode extends LitElement {
|
||||
|
||||
private _cutBadge(): void {
|
||||
this._copyBadge();
|
||||
fireEvent(this, "ll-delete-badge", { path: this.path!, silent: true });
|
||||
fireEvent(this, "ll-delete-badge", { path: this.path, silent: true });
|
||||
}
|
||||
|
||||
private _copyBadge(): void {
|
||||
const { cardIndex } = parseLovelaceCardPath(this.path!);
|
||||
const cardConfig = this._badges[cardIndex];
|
||||
this._clipboard = deepClone(cardConfig);
|
||||
this._clipboard = deepClone(this._badgeConfig);
|
||||
}
|
||||
|
||||
private _duplicateBadge(): void {
|
||||
const { cardIndex } = parseLovelaceCardPath(this.path!);
|
||||
const containerPath = getLovelaceContainerPath(this.path!);
|
||||
const badgeConfig = ensureBadgeConfig(this._badges![cardIndex]);
|
||||
showEditBadgeDialog(this, {
|
||||
lovelaceConfig: this.lovelace!.config,
|
||||
saveConfig: this.lovelace!.saveConfig,
|
||||
path: containerPath as [number],
|
||||
badgeConfig,
|
||||
lovelaceConfig: this.lovelace.config,
|
||||
saveConfig: this.lovelace.saveConfig,
|
||||
path: getParentPath(this.path),
|
||||
badgeConfig: this._badgeConfig,
|
||||
});
|
||||
}
|
||||
|
||||
private _editBadge(): void {
|
||||
fireEvent(this, "ll-edit-badge", { path: this.path! });
|
||||
fireEvent(this, "ll-edit-badge", { path: this.path });
|
||||
}
|
||||
|
||||
private _deleteBadge(): void {
|
||||
fireEvent(this, "ll-delete-badge", { path: this.path!, silent: false });
|
||||
fireEvent(this, "ll-delete-badge", { path: this.path, silent: false });
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
|
||||
@@ -21,14 +21,14 @@ import "../../../components/ha-dropdown-item";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import type { LovelaceCardPath } from "../editor/lovelace-path";
|
||||
import type { LovelacePath } from "../editor/lovelace-path";
|
||||
import type { Lovelace } from "../types";
|
||||
|
||||
@customElement("hui-card-edit-mode")
|
||||
export class HuiCardEditMode extends LitElement {
|
||||
@property({ attribute: false }) public lovelace!: Lovelace;
|
||||
|
||||
@property({ type: Array }) public path!: LovelaceCardPath;
|
||||
@property({ attribute: false }) public path!: LovelacePath;
|
||||
|
||||
@property({ type: Boolean, attribute: "hidden-overlay" })
|
||||
public hiddenOverlay = false;
|
||||
|
||||
@@ -21,6 +21,7 @@ import "../../../components/ha-dropdown-item";
|
||||
import "../../../components/ha-icon-button";
|
||||
import type { LovelaceCardConfig } from "../../../data/lovelace/config/card";
|
||||
import { saveConfig } from "../../../data/lovelace/config/types";
|
||||
import type { LovelaceViewConfig } from "../../../data/lovelace/config/view";
|
||||
import { isStrategyView } from "../../../data/lovelace/config/view";
|
||||
import {
|
||||
showAlertDialog,
|
||||
@@ -31,16 +32,16 @@ import type { HomeAssistant } from "../../../types";
|
||||
import { computeCardSize } from "../common/compute-card-size";
|
||||
import {
|
||||
addCard,
|
||||
deleteCard,
|
||||
moveCardToContainer,
|
||||
moveCardToIndex,
|
||||
} from "../editor/config-util";
|
||||
import type { LovelacePath } from "../editor/lovelace-path";
|
||||
import {
|
||||
type LovelaceCardPath,
|
||||
type LovelaceContainerPath,
|
||||
findLovelaceItems,
|
||||
getLovelaceContainerPath,
|
||||
parseLovelaceCardPath,
|
||||
deleteAtPath,
|
||||
getAtPath,
|
||||
getParentPath,
|
||||
getViewPath,
|
||||
normalizeCardPath,
|
||||
} from "../editor/lovelace-path";
|
||||
import { showSelectViewDialog } from "../editor/select-view/show-select-view-dialog";
|
||||
import type { Lovelace, LovelaceCard } from "../types";
|
||||
@@ -52,7 +53,7 @@ export class HuiCardOptions extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public lovelace?: Lovelace;
|
||||
|
||||
@property({ type: Array }) public path?: LovelaceCardPath;
|
||||
@property({ attribute: false }) public path?: LovelacePath;
|
||||
|
||||
@queryAssignedElements() private _assignedElements?: LovelaceCard[];
|
||||
|
||||
@@ -73,24 +74,37 @@ export class HuiCardOptions extends LitElement {
|
||||
: 1;
|
||||
}
|
||||
|
||||
protected willUpdate(changedProps: PropertyValues<this>) {
|
||||
// Temporary compatibility: custom view layouts still set [view, card] index tuples
|
||||
if (changedProps.has("path") && this.path) {
|
||||
this.path = normalizeCardPath(this.path);
|
||||
}
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues<this>) {
|
||||
if (!changedProps.has("path") || !this.path) {
|
||||
return;
|
||||
}
|
||||
const { viewIndex } = parseLovelaceCardPath(this.path);
|
||||
this.classList.toggle(
|
||||
"panel",
|
||||
this.lovelace!.config.views[viewIndex].panel
|
||||
const viewPath = getViewPath(this.path);
|
||||
const viewConfig = getAtPath<LovelaceViewConfig>(
|
||||
this.lovelace!.config,
|
||||
viewPath
|
||||
);
|
||||
this.classList.toggle("panel", viewConfig?.panel);
|
||||
}
|
||||
|
||||
private get _cards() {
|
||||
const containerPath = getLovelaceContainerPath(this.path!);
|
||||
return findLovelaceItems("cards", this.lovelace!.config, containerPath)!;
|
||||
const cardsPath = getParentPath(this.path!);
|
||||
return getAtPath<LovelaceCardConfig[]>(this.lovelace!.config, cardsPath)!;
|
||||
}
|
||||
|
||||
private get _cardIndex(): number {
|
||||
const path = this.path!;
|
||||
return path[path.length - 1] as number;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const { cardIndex } = parseLovelaceCardPath(this.path!);
|
||||
const cardIndex = this._cardIndex;
|
||||
|
||||
return html`
|
||||
<div class="card"><slot></slot></div>
|
||||
@@ -298,21 +312,23 @@ export class HuiCardOptions extends LitElement {
|
||||
private _decreaseCardPosiion(): void {
|
||||
const lovelace = this.lovelace!;
|
||||
const path = this.path!;
|
||||
const { cardIndex } = parseLovelaceCardPath(path);
|
||||
lovelace.saveConfig(moveCardToIndex(lovelace.config, path, cardIndex - 1));
|
||||
lovelace.saveConfig(
|
||||
moveCardToIndex(lovelace.config, path, this._cardIndex - 1)
|
||||
);
|
||||
}
|
||||
|
||||
private _increaseCardPosition(): void {
|
||||
const lovelace = this.lovelace!;
|
||||
const path = this.path!;
|
||||
const { cardIndex } = parseLovelaceCardPath(path);
|
||||
lovelace.saveConfig(moveCardToIndex(lovelace.config, path, cardIndex + 1));
|
||||
lovelace.saveConfig(
|
||||
moveCardToIndex(lovelace.config, path, this._cardIndex + 1)
|
||||
);
|
||||
}
|
||||
|
||||
private async _changeCardPosition(): Promise<void> {
|
||||
const lovelace = this.lovelace!;
|
||||
const path = this.path!;
|
||||
const { cardIndex } = parseLovelaceCardPath(path);
|
||||
const cardIndex = this._cardIndex;
|
||||
const positionString = await showPromptDialog(this, {
|
||||
title: this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.change_position.title"
|
||||
@@ -363,7 +379,7 @@ export class HuiCardOptions extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
const toPath: LovelaceContainerPath = [viewIndex];
|
||||
const toPath: LovelacePath = ["views", viewIndex];
|
||||
|
||||
if (urlPath === this.lovelace!.urlPath) {
|
||||
this.lovelace!.saveConfig(
|
||||
@@ -382,15 +398,17 @@ export class HuiCardOptions extends LitElement {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const { cardIndex } = parseLovelaceCardPath(this.path!);
|
||||
const card = this._cards[cardIndex];
|
||||
const card = getAtPath<LovelaceCardConfig>(
|
||||
this.lovelace.config,
|
||||
this.path!
|
||||
)!;
|
||||
await saveConfig(
|
||||
this.hass!,
|
||||
urlPath,
|
||||
addCard(newConfig, toPath, card)
|
||||
);
|
||||
this.lovelace!.saveConfig(
|
||||
deleteCard(this.lovelace!.config, this.path!)
|
||||
deleteAtPath(this.lovelace!.config, this.path!)
|
||||
);
|
||||
|
||||
this.lovelace.showToast({
|
||||
|
||||
@@ -14,11 +14,13 @@ import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
|
||||
import "../../../components/ha-dropdown-item";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import type { LovelaceSectionRawConfig } from "../../../data/lovelace/config/section";
|
||||
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { deleteSection, duplicateSection } from "../editor/config-util";
|
||||
import { findLovelaceContainer } from "../editor/lovelace-path";
|
||||
import { duplicateSection } from "../editor/config-util";
|
||||
import type { LovelacePath } from "../editor/lovelace-path";
|
||||
import { deleteAtPath, getAtPath } from "../editor/lovelace-path";
|
||||
import { showEditSectionDialog } from "../editor/section-editor/show-edit-section-dialog";
|
||||
import type { Lovelace } from "../types";
|
||||
|
||||
@@ -28,9 +30,7 @@ export class HuiSectionEditMode extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public lovelace!: Lovelace;
|
||||
|
||||
@property({ attribute: false }) public index!: number;
|
||||
|
||||
@property({ attribute: false }) public viewIndex!: number;
|
||||
@property({ attribute: false }) public path!: LovelacePath;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
@@ -98,26 +98,22 @@ export class HuiSectionEditMode extends LitElement {
|
||||
saveConfig: (newConfig) => {
|
||||
this.lovelace!.saveConfig(newConfig);
|
||||
},
|
||||
viewIndex: this.viewIndex,
|
||||
sectionIndex: this.index,
|
||||
path: this.path,
|
||||
});
|
||||
}
|
||||
|
||||
private _duplicateSection(): void {
|
||||
const newConfig = duplicateSection(
|
||||
this.lovelace!.config,
|
||||
this.viewIndex,
|
||||
this.index
|
||||
);
|
||||
const newConfig = duplicateSection(this.lovelace!.config, this.path);
|
||||
this.lovelace!.saveConfig(newConfig);
|
||||
}
|
||||
|
||||
private async _deleteSection() {
|
||||
const path = [this.viewIndex, this.index] as [number, number];
|
||||
const section = getAtPath<LovelaceSectionRawConfig>(
|
||||
this.lovelace!.config,
|
||||
this.path
|
||||
);
|
||||
|
||||
const section = findLovelaceContainer(this.lovelace!.config, path);
|
||||
|
||||
const cardCount = "cards" in section && section.cards?.length;
|
||||
const cardCount = section && "cards" in section && section.cards?.length;
|
||||
|
||||
if (cardCount) {
|
||||
const confirm = await showConfirmationDialog(this, {
|
||||
@@ -134,11 +130,7 @@ export class HuiSectionEditMode extends LitElement {
|
||||
if (!confirm) return;
|
||||
}
|
||||
|
||||
const newConfig = deleteSection(
|
||||
this.lovelace!.config,
|
||||
this.viewIndex,
|
||||
this.index
|
||||
);
|
||||
const newConfig = deleteAtPath(this.lovelace!.config, this.path);
|
||||
this.lovelace!.saveConfig(newConfig);
|
||||
}
|
||||
|
||||
|
||||
@@ -128,7 +128,7 @@ export const addEntitiesToLovelaceView = async (
|
||||
alert(hass.localize("ui.panel.lovelace.add_entities.saving_failed"));
|
||||
}
|
||||
},
|
||||
path: [0],
|
||||
path: ["views", 0],
|
||||
entities,
|
||||
});
|
||||
return;
|
||||
@@ -154,7 +154,7 @@ export const addEntitiesToLovelaceView = async (
|
||||
);
|
||||
}
|
||||
},
|
||||
path: [viewIndex],
|
||||
path: ["views", viewIndex],
|
||||
entities,
|
||||
});
|
||||
},
|
||||
|
||||
@@ -11,12 +11,10 @@ import "../../../../components/ha-dialog-header";
|
||||
import "../../../../components/ha-tab-group";
|
||||
import "../../../../components/ha-tab-group-tab";
|
||||
import type { LovelaceBadgeConfig } from "../../../../data/lovelace/config/badge";
|
||||
import type { LovelaceViewConfig } from "../../../../data/lovelace/config/view";
|
||||
import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
|
||||
import { haStyleDialog } from "../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import { addBadge } from "../config-util";
|
||||
import { findLovelaceContainer } from "../lovelace-path";
|
||||
import { appendAtPath, getAtPath, getParentPath } from "../lovelace-path";
|
||||
import "./hui-badge-picker";
|
||||
import "./hui-badge-suggestion-picker";
|
||||
import type { CreateBadgeDialogParams } from "./show-create-badge-dialog";
|
||||
@@ -33,7 +31,7 @@ export class HuiCreateDialogBadge
|
||||
|
||||
@state() private _open = false;
|
||||
|
||||
@state() private _containerConfig!: LovelaceViewConfig;
|
||||
@state() private _containerConfig!: { title?: string };
|
||||
|
||||
@state() private _currTab: "badge" | "entity" = "entity";
|
||||
|
||||
@@ -46,10 +44,11 @@ export class HuiCreateDialogBadge
|
||||
"all and (max-width: 450px), all and (max-height: 500px)"
|
||||
).matches;
|
||||
|
||||
const containerConfig = findLovelaceContainer(
|
||||
const containerPath = getParentPath(params.path);
|
||||
const containerConfig = getAtPath<{ title?: string }>(
|
||||
params.lovelaceConfig,
|
||||
params.path
|
||||
);
|
||||
containerPath
|
||||
)!;
|
||||
|
||||
if ("strategy" in containerConfig) {
|
||||
throw new Error("Can't edit strategy");
|
||||
@@ -221,7 +220,7 @@ export class HuiCreateDialogBadge
|
||||
const lovelaceConfig = this._params!.lovelaceConfig;
|
||||
const containerPath = this._params!.path;
|
||||
const saveConfig = this._params!.saveConfig;
|
||||
const newConfig = addBadge(lovelaceConfig, containerPath, config);
|
||||
const newConfig = appendAtPath(lovelaceConfig, containerPath, config);
|
||||
await saveConfig(newConfig);
|
||||
this.closeDialog();
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@ import "../../../../components/ha-icon-button";
|
||||
import "../../../../components/ha-spinner";
|
||||
import type { LovelaceBadgeConfig } from "../../../../data/lovelace/config/badge";
|
||||
import { ensureBadgeConfig } from "../../../../data/lovelace/config/badge";
|
||||
import type { LovelaceViewConfig } from "../../../../data/lovelace/config/view";
|
||||
import {
|
||||
getCustomBadgeEntry,
|
||||
isCustomType,
|
||||
@@ -33,11 +32,16 @@ import type { HomeAssistant } from "../../../../types";
|
||||
import { showSaveSuccessToast } from "../../../../util/toast-saved-success";
|
||||
import "../../badges/hui-badge";
|
||||
import { getConfigEntityId } from "../../common/get-config-entity-id";
|
||||
import { addBadge, replaceBadge } from "../config-util";
|
||||
import { getBadgeDefaultConfig } from "../get-badge-default-config";
|
||||
import { getBadgeDocumentationURL } from "../get-dashboard-documentation-url";
|
||||
import type { ConfigChangedEvent } from "../hui-element-editor";
|
||||
import { findLovelaceContainer } from "../lovelace-path";
|
||||
import type { LovelacePath } from "../lovelace-path";
|
||||
import {
|
||||
appendAtPath,
|
||||
getAtPath,
|
||||
getParentPath,
|
||||
setAtPath,
|
||||
} from "../lovelace-path";
|
||||
import type { GUIModeChangedEvent } from "../types";
|
||||
import "./hui-badge-element-editor";
|
||||
import type { HuiBadgeElementEditor } from "./hui-badge-element-editor";
|
||||
@@ -69,7 +73,7 @@ export class HuiDialogEditBadge
|
||||
|
||||
@state() private _badgeConfig?: LovelaceBadgeConfig;
|
||||
|
||||
@state() private _containerConfig!: LovelaceViewConfig;
|
||||
@state() private _containerConfig!: { title?: string };
|
||||
|
||||
@state() private _saving = false;
|
||||
|
||||
@@ -90,10 +94,11 @@ export class HuiDialogEditBadge
|
||||
this._guiModeAvailable = true;
|
||||
this._open = true;
|
||||
|
||||
const containerConfig = findLovelaceContainer(
|
||||
const containerPath = getParentPath(this._collectionPath);
|
||||
const containerConfig = getAtPath<{ title?: string }>(
|
||||
params.lovelaceConfig,
|
||||
params.path
|
||||
);
|
||||
containerPath
|
||||
)!;
|
||||
|
||||
if ("strategy" in containerConfig) {
|
||||
throw new Error("Can't edit strategy");
|
||||
@@ -104,7 +109,10 @@ export class HuiDialogEditBadge
|
||||
if ("badgeConfig" in params) {
|
||||
this._badgeConfig = params.badgeConfig;
|
||||
} else {
|
||||
const badge = this._containerConfig.badges?.[params.badgeIndex];
|
||||
const badge = getAtPath<Partial<LovelaceBadgeConfig> | string>(
|
||||
params.lovelaceConfig,
|
||||
params.path
|
||||
);
|
||||
this._badgeConfig = badge != null ? ensureBadgeConfig(badge) : badge;
|
||||
}
|
||||
|
||||
@@ -125,6 +133,11 @@ export class HuiDialogEditBadge
|
||||
}
|
||||
}
|
||||
|
||||
private get _collectionPath(): LovelacePath {
|
||||
const params = this._params!;
|
||||
return "badgeConfig" in params ? params.path : getParentPath(params.path);
|
||||
}
|
||||
|
||||
public closeDialog(): boolean {
|
||||
if (this.isEffectiveDirtyState) {
|
||||
this._confirmCancel();
|
||||
@@ -408,15 +421,15 @@ export class HuiDialogEditBadge
|
||||
return;
|
||||
}
|
||||
this._saving = true;
|
||||
const path = this._params!.path;
|
||||
await this._params!.saveConfig(
|
||||
"badgeConfig" in this._params!
|
||||
? addBadge(this._params!.lovelaceConfig, path, this._badgeConfig!)
|
||||
: replaceBadge(
|
||||
this._params!.lovelaceConfig,
|
||||
[...path, this._params!.badgeIndex],
|
||||
const params = this._params!;
|
||||
await params.saveConfig(
|
||||
"badgeConfig" in params
|
||||
? appendAtPath(
|
||||
params.lovelaceConfig,
|
||||
this._collectionPath,
|
||||
this._badgeConfig!
|
||||
)
|
||||
: setAtPath(params.lovelaceConfig, params.path, this._badgeConfig!)
|
||||
);
|
||||
this._saving = false;
|
||||
this._markDirtyStateClean();
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import type { LovelaceConfig } from "../../../../data/lovelace/config/types";
|
||||
import type { LovelacePath } from "../lovelace-path";
|
||||
|
||||
export interface CreateBadgeDialogParams {
|
||||
lovelaceConfig: LovelaceConfig;
|
||||
saveConfig: (config: LovelaceConfig) => void;
|
||||
path: [number];
|
||||
path: LovelacePath;
|
||||
suggestedBadges?: string[];
|
||||
entities?: string[]; // We can pass entity id's that will be added to the config when a badge is picked
|
||||
}
|
||||
|
||||
@@ -1,19 +1,13 @@
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import type { LovelaceBadgeConfig } from "../../../../data/lovelace/config/badge";
|
||||
import type { LovelaceConfig } from "../../../../data/lovelace/config/types";
|
||||
import type { LovelacePath } from "../lovelace-path";
|
||||
|
||||
export type EditBadgeDialogParams = {
|
||||
lovelaceConfig: LovelaceConfig;
|
||||
saveConfig: (config: LovelaceConfig) => void;
|
||||
path: [number];
|
||||
} & (
|
||||
| {
|
||||
badgeIndex: number;
|
||||
}
|
||||
| {
|
||||
badgeConfig: LovelaceBadgeConfig;
|
||||
}
|
||||
);
|
||||
path: LovelacePath;
|
||||
} & ({ badgeConfig: LovelaceBadgeConfig } | {});
|
||||
|
||||
export const importEditBadgeDialog = () => import("./hui-dialog-edit-badge");
|
||||
|
||||
|
||||
@@ -11,13 +11,20 @@ import "../../../../components/ha-dialog-header";
|
||||
import "../../../../components/ha-tab-group";
|
||||
import "../../../../components/ha-tab-group-tab";
|
||||
import type { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
|
||||
import type { LovelaceSectionConfig } from "../../../../data/lovelace/config/section";
|
||||
import type { LovelaceViewConfig } from "../../../../data/lovelace/config/view";
|
||||
import type {
|
||||
LovelaceSectionConfig,
|
||||
LovelaceSectionRawConfig,
|
||||
} from "../../../../data/lovelace/config/section";
|
||||
import type {
|
||||
LovelaceViewConfig,
|
||||
LovelaceViewRawConfig,
|
||||
} from "../../../../data/lovelace/config/view";
|
||||
import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
|
||||
import { haStyleDialog } from "../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import { addCard } from "../config-util";
|
||||
import { findLovelaceContainer } from "../lovelace-path";
|
||||
import { addCardAtPath, getCardSectionConfig } from "../config-util";
|
||||
import type { LovelacePath } from "../lovelace-path";
|
||||
import { getAtPath, getParentPath, getPathTarget } from "../lovelace-path";
|
||||
import "./hui-card-picker";
|
||||
import "./hui-suggestion-picker";
|
||||
import type { CreateCardDialogParams } from "./show-create-card-dialog";
|
||||
@@ -48,10 +55,13 @@ export class HuiCreateDialogCard
|
||||
"all and (max-width: 450px), all and (max-height: 500px)"
|
||||
).matches;
|
||||
|
||||
const containerConfig = findLovelaceContainer(
|
||||
params.lovelaceConfig,
|
||||
params.path
|
||||
);
|
||||
const containerConfig = getAtPath<
|
||||
LovelaceViewRawConfig | LovelaceSectionRawConfig
|
||||
>(params.lovelaceConfig, this._containerPath(params.path));
|
||||
|
||||
if (!containerConfig) {
|
||||
throw new Error("Container does not exist");
|
||||
}
|
||||
|
||||
if ("strategy" in containerConfig) {
|
||||
throw new Error("Can't edit strategy");
|
||||
@@ -66,6 +76,13 @@ export class HuiCreateDialogCard
|
||||
return true;
|
||||
}
|
||||
|
||||
private _containerPath(path: LovelacePath): LovelacePath {
|
||||
const parentPath = getParentPath(path);
|
||||
return getPathTarget(path) === "slot"
|
||||
? getParentPath(parentPath)
|
||||
: parentPath;
|
||||
}
|
||||
|
||||
private _dialogClosed(): void {
|
||||
this._open = false;
|
||||
this._params = undefined;
|
||||
@@ -103,7 +120,7 @@ export class HuiCreateDialogCard
|
||||
<span slot="title">${title}</span>
|
||||
|
||||
${
|
||||
!this._params.saveCard
|
||||
getPathTarget(this._params.path) !== "slot"
|
||||
? html`
|
||||
<ha-tab-group @wa-tab-show=${this._handleTabChanged}>
|
||||
<ha-tab-group-tab
|
||||
@@ -223,15 +240,10 @@ export class HuiCreateDialogCard
|
||||
ev: CustomEvent<{ config: LovelaceCardConfig }>
|
||||
): Promise<void> {
|
||||
const config = ev.detail.config;
|
||||
if (this._params!.saveCard) {
|
||||
await this._params!.saveCard(config);
|
||||
} else {
|
||||
const lovelaceConfig = this._params!.lovelaceConfig;
|
||||
const containerPath = this._params!.path;
|
||||
const saveConfig = this._params!.saveConfig;
|
||||
const newConfig = addCard(lovelaceConfig, containerPath, config);
|
||||
await saveConfig(newConfig);
|
||||
}
|
||||
const lovelaceConfig = this._params!.lovelaceConfig;
|
||||
const path = this._params!.path;
|
||||
const saveConfig = this._params!.saveConfig;
|
||||
await saveConfig(addCardAtPath(lovelaceConfig, path, config));
|
||||
this.closeDialog();
|
||||
}
|
||||
|
||||
@@ -245,34 +257,17 @@ export class HuiCreateDialogCard
|
||||
}
|
||||
}
|
||||
|
||||
if (this._params!.saveCard) {
|
||||
showEditCardDialog(this, {
|
||||
lovelaceConfig: this._params!.lovelaceConfig,
|
||||
saveCardConfig: this._params!.saveCard,
|
||||
cardConfig: config,
|
||||
isNew: true,
|
||||
});
|
||||
this.closeDialog();
|
||||
return;
|
||||
}
|
||||
|
||||
const lovelaceConfig = this._params!.lovelaceConfig;
|
||||
const containerPath = this._params!.path;
|
||||
const path = this._params!.path;
|
||||
const saveConfig = this._params!.saveConfig;
|
||||
|
||||
const sectionConfig =
|
||||
containerPath.length === 2
|
||||
? findLovelaceContainer(lovelaceConfig, containerPath)
|
||||
: undefined;
|
||||
|
||||
showEditCardDialog(this, {
|
||||
lovelaceConfig,
|
||||
saveCardConfig: async (newCardConfig) => {
|
||||
const newConfig = addCard(lovelaceConfig, containerPath, newCardConfig);
|
||||
await saveConfig(newConfig);
|
||||
await saveConfig(addCardAtPath(lovelaceConfig, path, newCardConfig));
|
||||
},
|
||||
cardConfig: config,
|
||||
sectionConfig,
|
||||
sectionConfig: getCardSectionConfig(lovelaceConfig, path),
|
||||
isNew: true,
|
||||
});
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import type { HaYamlEditor } from "../../../../components/ha-yaml-editor";
|
||||
import type { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
|
||||
import type { LovelaceSectionConfig } from "../../../../data/lovelace/config/section";
|
||||
import type { LovelaceConfig } from "../../../../data/lovelace/config/types";
|
||||
import type { LovelaceViewRawConfig } from "../../../../data/lovelace/config/view";
|
||||
import { isStrategyView } from "../../../../data/lovelace/config/view";
|
||||
import { haStyleDialog } from "../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
@@ -21,8 +22,8 @@ import "../../cards/hui-card";
|
||||
import "../../sections/hui-section";
|
||||
import { getViewType } from "../../views/get-view-type";
|
||||
import { addCards, addSection } from "../config-util";
|
||||
import type { LovelaceContainerPath } from "../lovelace-path";
|
||||
import { parseLovelaceContainerPath } from "../lovelace-path";
|
||||
import type { LovelacePath } from "../lovelace-path";
|
||||
import { getAtPath, getParentPath, getViewPath } from "../lovelace-path";
|
||||
import { showCreateCardDialog } from "./show-create-card-dialog";
|
||||
import type { SuggestCardDialogParams } from "./show-suggest-card-dialog";
|
||||
|
||||
@@ -74,11 +75,16 @@ export class HuiDialogSuggestCard extends LitElement {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { viewIndex } = parseLovelaceContainerPath(this._params.path);
|
||||
const viewConfig = this._params!.lovelaceConfig.views[viewIndex];
|
||||
const viewPath = getViewPath(this._params.path);
|
||||
const viewConfig = getAtPath<LovelaceViewRawConfig>(
|
||||
this._params.lovelaceConfig,
|
||||
viewPath
|
||||
);
|
||||
|
||||
return (
|
||||
!isStrategyView(viewConfig) && getViewType(viewConfig) === "sections"
|
||||
!!viewConfig &&
|
||||
!isStrategyView(viewConfig) &&
|
||||
getViewType(viewConfig) === "sections"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -228,7 +234,7 @@ export class HuiDialogSuggestCard extends LitElement {
|
||||
showCreateCardDialog(this, {
|
||||
lovelaceConfig: this._params!.lovelaceConfig,
|
||||
saveConfig: this._params!.saveConfig,
|
||||
path: this._params!.path,
|
||||
path: [...this._params!.path, "cards"],
|
||||
entities: this._params!.entities,
|
||||
});
|
||||
this.closeDialog();
|
||||
@@ -236,28 +242,27 @@ export class HuiDialogSuggestCard extends LitElement {
|
||||
|
||||
private _computeNewConfig(
|
||||
config: LovelaceConfig,
|
||||
path: LovelaceContainerPath
|
||||
path: LovelacePath
|
||||
): LovelaceConfig {
|
||||
if (!this._viewSupportsSection) {
|
||||
return addCards(config, path, this._cardConfig!);
|
||||
}
|
||||
|
||||
const { viewIndex, sectionIndex } = parseLovelaceContainerPath(path);
|
||||
|
||||
// If container is a view, add a section
|
||||
if (sectionIndex === undefined) {
|
||||
const parentPath = getParentPath(path);
|
||||
if (parentPath[parentPath.length - 1] !== "sections") {
|
||||
const newSection = this._sectionConfig ?? {
|
||||
type: "grid",
|
||||
cards: this._cardConfig,
|
||||
};
|
||||
return addSection(config, viewIndex, newSection);
|
||||
return addSection(config, path, newSection);
|
||||
}
|
||||
|
||||
// Else add cards to section
|
||||
const newCards = this._sectionConfig
|
||||
? this._sectionConfig.cards || []
|
||||
: this._cardConfig!;
|
||||
return addCards(config, [viewIndex, sectionIndex], newCards);
|
||||
return addCards(config, path, newCards);
|
||||
}
|
||||
|
||||
private async _save(): Promise<void> {
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import type { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
|
||||
import type { LovelaceConfig } from "../../../../data/lovelace/config/types";
|
||||
import type { LovelaceContainerPath } from "../lovelace-path";
|
||||
import type { LovelacePath } from "../lovelace-path";
|
||||
|
||||
export interface CreateCardDialogParams {
|
||||
lovelaceConfig: LovelaceConfig;
|
||||
saveConfig: (config: LovelaceConfig) => void;
|
||||
path: LovelaceContainerPath;
|
||||
path: LovelacePath;
|
||||
suggestedCards?: string[];
|
||||
entities?: string[]; // We can pass entity id's that will be added to the config when a card is picked
|
||||
saveCard?: (cardConfig: LovelaceCardConfig) => void; // Optional: pick a single card and return it via callback, hides entity tab
|
||||
}
|
||||
|
||||
export const importCreateCardDialog = () => import("./hui-dialog-create-card");
|
||||
|
||||
@@ -2,13 +2,13 @@ import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import type { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
|
||||
import type { LovelaceSectionConfig } from "../../../../data/lovelace/config/section";
|
||||
import type { LovelaceConfig } from "../../../../data/lovelace/config/types";
|
||||
import type { LovelaceContainerPath } from "../lovelace-path";
|
||||
import type { LovelacePath } from "../lovelace-path";
|
||||
|
||||
export interface SuggestCardDialogParams {
|
||||
lovelaceConfig?: LovelaceConfig;
|
||||
yaml?: boolean;
|
||||
saveConfig?: (config: LovelaceConfig) => void;
|
||||
path?: LovelaceContainerPath;
|
||||
path?: LovelacePath;
|
||||
entities?: string[]; // We pass this to create dialog when user chooses "Pick own"
|
||||
cardConfig: LovelaceCardConfig[]; // We can pass a suggested config,s
|
||||
sectionConfig?: LovelaceSectionConfig;
|
||||
|
||||
@@ -1,180 +1,97 @@
|
||||
import deepClone from "deep-clone-simple";
|
||||
import type { LovelaceBadgeConfig } from "../../../data/lovelace/config/badge";
|
||||
import { ensureBadgeConfig } from "../../../data/lovelace/config/badge";
|
||||
import type { LovelaceCardConfig } from "../../../data/lovelace/config/card";
|
||||
import type { LovelaceSectionRawConfig } from "../../../data/lovelace/config/section";
|
||||
import type {
|
||||
LovelaceSectionConfig,
|
||||
LovelaceSectionRawConfig,
|
||||
} from "../../../data/lovelace/config/section";
|
||||
import { isStrategySection } from "../../../data/lovelace/config/section";
|
||||
import type { LovelaceConfig } from "../../../data/lovelace/config/types";
|
||||
import type { LovelaceViewConfig } from "../../../data/lovelace/config/view";
|
||||
import { isStrategyView } from "../../../data/lovelace/config/view";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import type { LovelaceCardPath, LovelaceContainerPath } from "./lovelace-path";
|
||||
import type { LovelacePath } from "./lovelace-path";
|
||||
import {
|
||||
findLovelaceContainer,
|
||||
findLovelaceItems,
|
||||
getLovelaceContainerPath,
|
||||
parseLovelaceCardPath,
|
||||
parseLovelaceContainerPath,
|
||||
updateLovelaceContainer,
|
||||
updateLovelaceItems,
|
||||
appendAtPath,
|
||||
deleteAtPath,
|
||||
getAtPath,
|
||||
getParentPath,
|
||||
insertAtPath,
|
||||
getPathTarget,
|
||||
moveAtPath,
|
||||
pathEquals,
|
||||
setAtPath,
|
||||
} from "./lovelace-path";
|
||||
|
||||
export const addCard = (
|
||||
config: LovelaceConfig,
|
||||
path: LovelaceContainerPath,
|
||||
containerPath: LovelacePath,
|
||||
cardConfig: LovelaceCardConfig
|
||||
): LovelaceConfig => {
|
||||
const cards = findLovelaceItems("cards", config, path);
|
||||
const newCards = cards ? [...cards, cardConfig] : [cardConfig];
|
||||
const newConfig = updateLovelaceItems("cards", config, path, newCards);
|
||||
return newConfig;
|
||||
};
|
||||
): LovelaceConfig =>
|
||||
appendAtPath(config, [...containerPath, "cards"], cardConfig);
|
||||
|
||||
export const addCards = (
|
||||
config: LovelaceConfig,
|
||||
path: LovelaceContainerPath,
|
||||
containerPath: LovelacePath,
|
||||
cardConfigs: LovelaceCardConfig[]
|
||||
): LovelaceConfig => {
|
||||
const cards = findLovelaceItems("cards", config, path);
|
||||
const newCards = cards ? [...cards, ...cardConfigs] : [...cardConfigs];
|
||||
const newConfig = updateLovelaceItems("cards", config, path, newCards);
|
||||
return newConfig;
|
||||
};
|
||||
): LovelaceConfig =>
|
||||
cardConfigs.reduce(
|
||||
(newConfig, cardConfig) => addCard(newConfig, containerPath, cardConfig),
|
||||
config
|
||||
);
|
||||
|
||||
export const replaceCard = (
|
||||
export const addCardAtPath = (
|
||||
config: LovelaceConfig,
|
||||
path: LovelaceCardPath,
|
||||
path: LovelacePath,
|
||||
cardConfig: LovelaceCardConfig
|
||||
): LovelaceConfig => {
|
||||
const { cardIndex } = parseLovelaceCardPath(path);
|
||||
const containerPath = getLovelaceContainerPath(path);
|
||||
|
||||
const cards = findLovelaceItems("cards", config, containerPath);
|
||||
|
||||
const newCards = (cards ?? []).map((origConf, ind) =>
|
||||
ind === cardIndex ? cardConfig : origConf
|
||||
);
|
||||
|
||||
const newConfig = updateLovelaceItems(
|
||||
"cards",
|
||||
config,
|
||||
containerPath,
|
||||
newCards
|
||||
);
|
||||
return newConfig;
|
||||
};
|
||||
|
||||
export const deleteCard = (
|
||||
config: LovelaceConfig,
|
||||
path: LovelaceCardPath
|
||||
): LovelaceConfig => {
|
||||
const { cardIndex } = parseLovelaceCardPath(path);
|
||||
const containerPath = getLovelaceContainerPath(path);
|
||||
|
||||
const cards = findLovelaceItems("cards", config, containerPath);
|
||||
|
||||
const newCards = (cards ?? []).filter((_origConf, ind) => ind !== cardIndex);
|
||||
|
||||
const newConfig = updateLovelaceItems(
|
||||
"cards",
|
||||
config,
|
||||
containerPath,
|
||||
newCards
|
||||
);
|
||||
return newConfig;
|
||||
};
|
||||
|
||||
export const insertCard = (
|
||||
config: LovelaceConfig,
|
||||
path: LovelaceCardPath,
|
||||
cardConfig: LovelaceCardConfig
|
||||
) => {
|
||||
const { cardIndex } = parseLovelaceCardPath(path);
|
||||
const containerPath = getLovelaceContainerPath(path);
|
||||
|
||||
const cards = findLovelaceItems("cards", config, containerPath);
|
||||
|
||||
const newCards = cards
|
||||
? [...cards.slice(0, cardIndex), cardConfig, ...cards.slice(cardIndex)]
|
||||
: [cardConfig];
|
||||
|
||||
const newConfig = updateLovelaceItems(
|
||||
"cards",
|
||||
config,
|
||||
containerPath,
|
||||
newCards
|
||||
);
|
||||
return newConfig;
|
||||
};
|
||||
): LovelaceConfig =>
|
||||
getPathTarget(path) === "slot"
|
||||
? setAtPath(config, path, cardConfig)
|
||||
: appendAtPath(config, path, cardConfig);
|
||||
|
||||
export const moveCardToIndex = (
|
||||
config: LovelaceConfig,
|
||||
path: LovelaceCardPath,
|
||||
cardPath: LovelacePath,
|
||||
index: number
|
||||
): LovelaceConfig => {
|
||||
const { cardIndex } = parseLovelaceCardPath(path);
|
||||
const containerPath = getLovelaceContainerPath(path);
|
||||
|
||||
const cards = findLovelaceItems("cards", config, containerPath);
|
||||
|
||||
const newCards = cards ? [...cards] : [];
|
||||
|
||||
const oldIndex = cardIndex;
|
||||
const newIndex = Math.max(Math.min(index, newCards.length - 1), 0);
|
||||
|
||||
const card = newCards[oldIndex];
|
||||
newCards.splice(oldIndex, 1);
|
||||
newCards.splice(newIndex, 0, card);
|
||||
|
||||
const newConfig = updateLovelaceItems(
|
||||
"cards",
|
||||
config,
|
||||
containerPath,
|
||||
newCards
|
||||
);
|
||||
return newConfig;
|
||||
const collectionPath = getParentPath(cardPath);
|
||||
const cards = getAtPath<LovelaceCardConfig[]>(config, collectionPath) ?? [];
|
||||
const newIndex = Math.max(Math.min(index, cards.length - 1), 0);
|
||||
return moveAtPath(config, cardPath, [...collectionPath, newIndex]);
|
||||
};
|
||||
|
||||
export const moveCardToContainer = (
|
||||
config: LovelaceConfig,
|
||||
fromPath: LovelaceCardPath,
|
||||
toPath: LovelaceContainerPath
|
||||
cardPath: LovelacePath,
|
||||
containerPath: LovelacePath
|
||||
): LovelaceConfig => {
|
||||
const {
|
||||
cardIndex: fromCardIndex,
|
||||
viewIndex: fromViewIndex,
|
||||
sectionIndex: fromSectionIndex,
|
||||
} = parseLovelaceCardPath(fromPath);
|
||||
const { viewIndex: toViewIndex, sectionIndex: toSectionIndex } =
|
||||
parseLovelaceContainerPath(toPath);
|
||||
|
||||
if (fromViewIndex === toViewIndex && fromSectionIndex === toSectionIndex) {
|
||||
const fromCardsPath = getParentPath(cardPath);
|
||||
const toCardsPath = [...containerPath, "cards"];
|
||||
if (pathEquals(fromCardsPath, toCardsPath)) {
|
||||
throw new Error("You cannot move a card to the view or section it is in.");
|
||||
}
|
||||
|
||||
const fromContainerPath = getLovelaceContainerPath(fromPath);
|
||||
const cards = findLovelaceItems("cards", config, fromContainerPath);
|
||||
const card = cards![fromCardIndex];
|
||||
|
||||
let newConfig = addCard(config, toPath, card);
|
||||
newConfig = deleteCard(newConfig, fromPath);
|
||||
|
||||
return newConfig;
|
||||
const card = getAtPath<LovelaceCardConfig>(config, cardPath)!;
|
||||
const newConfig = addCard(config, containerPath, card);
|
||||
return deleteAtPath(newConfig, cardPath);
|
||||
};
|
||||
|
||||
export const moveCard = (
|
||||
export const getCardSectionConfig = (
|
||||
config: LovelaceConfig,
|
||||
fromPath: LovelaceCardPath,
|
||||
toPath: LovelaceCardPath
|
||||
): LovelaceConfig => {
|
||||
const { cardIndex: fromCardIndex } = parseLovelaceCardPath(fromPath);
|
||||
const fromContainerPath = getLovelaceContainerPath(fromPath);
|
||||
const cards = findLovelaceItems("cards", config, fromContainerPath);
|
||||
const card = cards![fromCardIndex];
|
||||
|
||||
let newConfig = deleteCard(config, fromPath);
|
||||
newConfig = insertCard(newConfig, toPath, card);
|
||||
|
||||
return newConfig;
|
||||
path: LovelacePath
|
||||
): LovelaceSectionConfig | undefined => {
|
||||
const parentPath = getParentPath(path);
|
||||
const containerPath =
|
||||
getPathTarget(path) === "item" ? getParentPath(parentPath) : parentPath;
|
||||
if (
|
||||
containerPath[containerPath.length - 2] !== "sections" ||
|
||||
typeof containerPath[containerPath.length - 1] !== "number"
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
const section = getAtPath<LovelaceSectionRawConfig>(config, containerPath);
|
||||
if (!section || isStrategySection(section)) {
|
||||
return undefined;
|
||||
}
|
||||
return section;
|
||||
};
|
||||
|
||||
export const addView = (
|
||||
@@ -266,197 +183,34 @@ export const moveViewToDashboard = (
|
||||
|
||||
export const addSection = (
|
||||
config: LovelaceConfig,
|
||||
viewIndex: number,
|
||||
containerPath: LovelacePath,
|
||||
sectionConfig: LovelaceSectionRawConfig
|
||||
): LovelaceConfig => {
|
||||
const view = findLovelaceContainer(config, [viewIndex]);
|
||||
if (isStrategyView(view)) {
|
||||
throw new Error("Deleting sections in a strategy is not supported.");
|
||||
}
|
||||
const sections = view.sections
|
||||
? [...view.sections, sectionConfig]
|
||||
: [sectionConfig];
|
||||
|
||||
const newConfig = updateLovelaceContainer(config, [viewIndex], {
|
||||
...view,
|
||||
sections,
|
||||
});
|
||||
return newConfig;
|
||||
};
|
||||
|
||||
export const deleteSection = (
|
||||
config: LovelaceConfig,
|
||||
viewIndex: number,
|
||||
sectionIndex: number
|
||||
): LovelaceConfig => {
|
||||
const view = findLovelaceContainer(config, [viewIndex]);
|
||||
if (isStrategyView(view)) {
|
||||
throw new Error("Deleting sections in a strategy is not supported.");
|
||||
}
|
||||
const sections = view.sections?.filter(
|
||||
(_origSection, index) => index !== sectionIndex
|
||||
);
|
||||
|
||||
const newConfig = updateLovelaceContainer(config, [viewIndex], {
|
||||
...view,
|
||||
sections,
|
||||
});
|
||||
return newConfig;
|
||||
};
|
||||
): LovelaceConfig =>
|
||||
appendAtPath(config, [...containerPath, "sections"], sectionConfig);
|
||||
|
||||
export const duplicateSection = (
|
||||
config: LovelaceConfig,
|
||||
viewIndex: number,
|
||||
sectionIndex: number
|
||||
sectionPath: LovelacePath
|
||||
): LovelaceConfig => {
|
||||
const view = findLovelaceContainer(config, [viewIndex]);
|
||||
if (isStrategyView(view)) {
|
||||
throw new Error("Duplicating sections in a strategy is not supported.");
|
||||
}
|
||||
const clone = deepClone(view.sections![sectionIndex]);
|
||||
return insertSection(config, viewIndex, sectionIndex + 1, clone);
|
||||
};
|
||||
|
||||
export const insertSection = (
|
||||
config: LovelaceConfig,
|
||||
viewIndex: number,
|
||||
sectionIndex: number,
|
||||
sectionConfig: LovelaceSectionRawConfig
|
||||
): LovelaceConfig => {
|
||||
const view = findLovelaceContainer(config, [viewIndex]);
|
||||
if (isStrategyView(view)) {
|
||||
throw new Error("Inserting sections in a strategy is not supported.");
|
||||
}
|
||||
const sections = view.sections
|
||||
? [
|
||||
...view.sections.slice(0, sectionIndex),
|
||||
sectionConfig,
|
||||
...view.sections.slice(sectionIndex),
|
||||
]
|
||||
: [sectionConfig];
|
||||
|
||||
const newConfig = updateLovelaceContainer(config, [viewIndex], {
|
||||
...view,
|
||||
sections,
|
||||
});
|
||||
return newConfig;
|
||||
};
|
||||
|
||||
export const moveSection = (
|
||||
config: LovelaceConfig,
|
||||
fromPath: [number, number],
|
||||
toPath: [number, number]
|
||||
): LovelaceConfig => {
|
||||
const section = findLovelaceContainer(config, fromPath);
|
||||
|
||||
let newConfig = deleteSection(config, fromPath[0], fromPath[1]);
|
||||
newConfig = insertSection(newConfig, toPath[0], toPath[1], section);
|
||||
|
||||
return newConfig;
|
||||
const index = sectionPath[sectionPath.length - 1] as number;
|
||||
const sectionsPath = getParentPath(sectionPath);
|
||||
const section = getAtPath<LovelaceSectionRawConfig>(config, sectionPath);
|
||||
return insertAtPath(config, [...sectionsPath, index + 1], deepClone(section));
|
||||
};
|
||||
|
||||
export const addBadge = (
|
||||
config: LovelaceConfig,
|
||||
path: LovelaceContainerPath,
|
||||
containerPath: LovelacePath,
|
||||
badgeConfig: LovelaceBadgeConfig
|
||||
): LovelaceConfig => {
|
||||
const badges = findLovelaceItems("badges", config, path);
|
||||
const newBadges = badges ? [...badges, badgeConfig] : [badgeConfig];
|
||||
const newConfig = updateLovelaceItems("badges", config, path, newBadges);
|
||||
return newConfig;
|
||||
};
|
||||
): LovelaceConfig =>
|
||||
appendAtPath(config, [...containerPath, "badges"], badgeConfig);
|
||||
|
||||
export const addBadges = (
|
||||
config: LovelaceConfig,
|
||||
path: LovelaceContainerPath,
|
||||
badgeConfig: LovelaceBadgeConfig[]
|
||||
): LovelaceConfig => {
|
||||
const badges = findLovelaceItems("badges", config, path);
|
||||
const newBadges = badges ? [...badges, ...badgeConfig] : [...badgeConfig];
|
||||
const newConfig = updateLovelaceItems("badges", config, path, newBadges);
|
||||
return newConfig;
|
||||
};
|
||||
|
||||
export const replaceBadge = (
|
||||
config: LovelaceConfig,
|
||||
path: LovelaceCardPath,
|
||||
cardConfig: LovelaceBadgeConfig
|
||||
): LovelaceConfig => {
|
||||
const { cardIndex } = parseLovelaceCardPath(path);
|
||||
const containerPath = getLovelaceContainerPath(path);
|
||||
|
||||
const badges = findLovelaceItems("badges", config, containerPath);
|
||||
|
||||
const newBadges = (badges ?? []).map((origConf, ind) =>
|
||||
ind === cardIndex ? cardConfig : origConf
|
||||
containerPath: LovelacePath,
|
||||
badgeConfigs: LovelaceBadgeConfig[]
|
||||
): LovelaceConfig =>
|
||||
badgeConfigs.reduce(
|
||||
(newConfig, badgeConfig) => addBadge(newConfig, containerPath, badgeConfig),
|
||||
config
|
||||
);
|
||||
|
||||
const newConfig = updateLovelaceItems(
|
||||
"badges",
|
||||
config,
|
||||
containerPath,
|
||||
newBadges
|
||||
);
|
||||
return newConfig;
|
||||
};
|
||||
|
||||
export const deleteBadge = (
|
||||
config: LovelaceConfig,
|
||||
path: LovelaceCardPath
|
||||
): LovelaceConfig => {
|
||||
const { cardIndex } = parseLovelaceCardPath(path);
|
||||
const containerPath = getLovelaceContainerPath(path);
|
||||
|
||||
const badges = findLovelaceItems("badges", config, containerPath);
|
||||
|
||||
const newBadges = (badges ?? []).filter(
|
||||
(_origConf, ind) => ind !== cardIndex
|
||||
);
|
||||
|
||||
const newConfig = updateLovelaceItems(
|
||||
"badges",
|
||||
config,
|
||||
containerPath,
|
||||
newBadges
|
||||
);
|
||||
return newConfig;
|
||||
};
|
||||
|
||||
export const insertBadge = (
|
||||
config: LovelaceConfig,
|
||||
path: LovelaceCardPath,
|
||||
badgeConfig: LovelaceBadgeConfig
|
||||
) => {
|
||||
const { cardIndex } = parseLovelaceCardPath(path);
|
||||
const containerPath = getLovelaceContainerPath(path);
|
||||
|
||||
const badges = findLovelaceItems("badges", config, containerPath);
|
||||
|
||||
const newBadges = badges
|
||||
? [...badges.slice(0, cardIndex), badgeConfig, ...badges.slice(cardIndex)]
|
||||
: [badgeConfig];
|
||||
|
||||
const newConfig = updateLovelaceItems(
|
||||
"badges",
|
||||
config,
|
||||
containerPath,
|
||||
newBadges
|
||||
);
|
||||
return newConfig;
|
||||
};
|
||||
|
||||
export const moveBadge = (
|
||||
config: LovelaceConfig,
|
||||
fromPath: LovelaceCardPath,
|
||||
toPath: LovelaceCardPath
|
||||
): LovelaceConfig => {
|
||||
const { cardIndex: fromCardIndex } = parseLovelaceCardPath(fromPath);
|
||||
const fromContainerPath = getLovelaceContainerPath(fromPath);
|
||||
const badges = findLovelaceItems("badges", config, fromContainerPath);
|
||||
const badge = badges![fromCardIndex];
|
||||
|
||||
let newConfig = deleteBadge(config, fromPath);
|
||||
newConfig = insertBadge(newConfig, toPath, ensureBadgeConfig(badge));
|
||||
|
||||
return newConfig;
|
||||
};
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import type { Lovelace } from "../types";
|
||||
import { deleteBadge } from "./config-util";
|
||||
import type { LovelaceCardPath } from "./lovelace-path";
|
||||
import type { LovelacePath } from "./lovelace-path";
|
||||
import { deleteAtPath } from "./lovelace-path";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
|
||||
export interface DeleteBadgeParams {
|
||||
path: LovelaceCardPath;
|
||||
path: LovelacePath;
|
||||
silent: boolean;
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ export async function performDeleteBadge(
|
||||
try {
|
||||
const { path, silent } = params;
|
||||
const oldConfig = lovelace.config;
|
||||
const newConfig = deleteBadge(oldConfig, path);
|
||||
const newConfig = deleteAtPath(oldConfig, path);
|
||||
await lovelace.saveConfig(newConfig);
|
||||
|
||||
if (silent) {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import type { Lovelace } from "../types";
|
||||
import { deleteCard } from "./config-util";
|
||||
import type { LovelaceCardPath } from "./lovelace-path";
|
||||
import type { LovelacePath } from "./lovelace-path";
|
||||
import { deleteAtPath } from "./lovelace-path";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
|
||||
export interface DeleteCardParams {
|
||||
path: LovelaceCardPath;
|
||||
path: LovelacePath;
|
||||
silent: boolean;
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ export async function performDeleteCard(
|
||||
try {
|
||||
const { path, silent } = params;
|
||||
const oldConfig = lovelace.config;
|
||||
const newConfig = deleteCard(oldConfig, path);
|
||||
const newConfig = deleteAtPath(oldConfig, path);
|
||||
await lovelace.saveConfig(newConfig);
|
||||
|
||||
if (silent) {
|
||||
|
||||
@@ -1,212 +1,245 @@
|
||||
import type { LovelaceBadgeConfig } from "../../../data/lovelace/config/badge";
|
||||
import type { LovelaceCardConfig } from "../../../data/lovelace/config/card";
|
||||
import type { LovelaceSectionRawConfig } from "../../../data/lovelace/config/section";
|
||||
import { isStrategySection } from "../../../data/lovelace/config/section";
|
||||
import type { LovelaceConfig } from "../../../data/lovelace/config/types";
|
||||
import type { LovelaceViewRawConfig } from "../../../data/lovelace/config/view";
|
||||
import { isStrategyView } from "../../../data/lovelace/config/view";
|
||||
|
||||
export type LovelaceCardPath = [number, number] | [number, number, number];
|
||||
export type LovelaceContainerPath = [number] | [number, number];
|
||||
export type LovelacePath = (string | number)[];
|
||||
export type LovelaceItemKind = "view" | "section" | "card" | "badge";
|
||||
export type LovelacePathTarget = "item" | "list" | "slot" | "node";
|
||||
|
||||
export const parseLovelaceCardPath = (
|
||||
path: LovelaceCardPath
|
||||
): { viewIndex: number; sectionIndex?: number; cardIndex: number } => {
|
||||
if (path.length === 2) {
|
||||
return {
|
||||
viewIndex: path[0],
|
||||
cardIndex: path[1],
|
||||
};
|
||||
const LIST_KEYS: Record<string, LovelaceItemKind> = {
|
||||
views: "view",
|
||||
sections: "section",
|
||||
cards: "card",
|
||||
badges: "badge",
|
||||
};
|
||||
|
||||
const SLOT_KEYS: Record<string, LovelaceItemKind> = {
|
||||
card: "card",
|
||||
};
|
||||
|
||||
export const stringifyPath = (path: LovelacePath): string => path.join("/");
|
||||
|
||||
export const parsePath = (path: string): LovelacePath =>
|
||||
path === ""
|
||||
? []
|
||||
: path
|
||||
.split("/")
|
||||
.map((segment) => (/^\d+$/.test(segment) ? Number(segment) : segment));
|
||||
|
||||
export const pathEquals = (a: LovelacePath, b: LovelacePath): boolean =>
|
||||
a.length === b.length && a.every((segment, index) => segment === b[index]);
|
||||
|
||||
export const isAncestorPath = (
|
||||
ancestor: LovelacePath,
|
||||
path: LovelacePath
|
||||
): boolean =>
|
||||
ancestor.length < path.length &&
|
||||
ancestor.every((segment, index) => segment === path[index]);
|
||||
|
||||
export const getParentPath = (path: LovelacePath): LovelacePath =>
|
||||
path.slice(0, -1);
|
||||
|
||||
export const getViewPath = (path: LovelacePath): LovelacePath =>
|
||||
path.slice(0, 2);
|
||||
|
||||
export const getPathTarget = (path: LovelacePath): LovelacePathTarget => {
|
||||
const last = path[path.length - 1];
|
||||
if (typeof last === "number") {
|
||||
return "item";
|
||||
}
|
||||
if (last in LIST_KEYS) {
|
||||
return "list";
|
||||
}
|
||||
if (last in SLOT_KEYS) {
|
||||
return "slot";
|
||||
}
|
||||
return "node";
|
||||
};
|
||||
|
||||
// Temporary compatibility: custom view layouts still pass [view, card] or [view, section, card] index tuples
|
||||
export const normalizeCardPath = (path: LovelacePath): LovelacePath => {
|
||||
if (path.length < 2 || path.some((segment) => typeof segment === "string")) {
|
||||
return path;
|
||||
}
|
||||
const [viewIndex, ...rest] = path;
|
||||
return rest.length === 1
|
||||
? ["views", viewIndex, "cards", rest[0]]
|
||||
: ["views", viewIndex, "sections", rest[0], "cards", rest[1]];
|
||||
};
|
||||
|
||||
export const getItemKind = (
|
||||
path: LovelacePath
|
||||
): LovelaceItemKind | undefined => {
|
||||
for (let index = path.length - 1; index >= 0; index--) {
|
||||
const segment = path[index];
|
||||
if (typeof segment === "string") {
|
||||
return LIST_KEYS[segment] ?? SLOT_KEYS[segment];
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const isRecord = (node: unknown): node is Record<string, unknown> =>
|
||||
typeof node === "object" && node !== null && !Array.isArray(node);
|
||||
|
||||
const isStrategyNode = (node: unknown): boolean =>
|
||||
isRecord(node) && "strategy" in node;
|
||||
|
||||
const strategyError = (path: LovelacePath, depth: number): Error =>
|
||||
new Error(
|
||||
`Cannot edit inside a strategy: ${stringifyPath(path.slice(0, depth))}`
|
||||
);
|
||||
|
||||
const getChild = (node: unknown, segment: string | number): unknown => {
|
||||
if (Array.isArray(node)) {
|
||||
return typeof segment === "number" ? node[segment] : undefined;
|
||||
}
|
||||
if (isRecord(node) && typeof segment === "string") {
|
||||
return node[segment];
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const readAtPath = (node: unknown, path: LovelacePath, depth = 0): unknown => {
|
||||
if (depth === path.length) {
|
||||
return node;
|
||||
}
|
||||
if (node === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
if (isStrategyNode(node)) {
|
||||
throw strategyError(path, depth);
|
||||
}
|
||||
return readAtPath(getChild(node, path[depth]), path, depth + 1);
|
||||
};
|
||||
|
||||
const updateAtPath = (
|
||||
node: unknown,
|
||||
path: LovelacePath,
|
||||
updater: (node: unknown) => unknown,
|
||||
depth = 0
|
||||
): unknown => {
|
||||
if (depth === path.length) {
|
||||
return updater(node);
|
||||
}
|
||||
if (isStrategyNode(node)) {
|
||||
throw strategyError(path, depth);
|
||||
}
|
||||
const segment = path[depth];
|
||||
if (typeof segment === "number") {
|
||||
if (!Array.isArray(node) || segment >= node.length) {
|
||||
throw new Error(
|
||||
`Cannot edit missing item: ${stringifyPath(path.slice(0, depth + 1))}`
|
||||
);
|
||||
}
|
||||
const items = node.slice();
|
||||
items[segment] = updateAtPath(items[segment], path, updater, depth + 1);
|
||||
return items;
|
||||
}
|
||||
const record = isRecord(node) ? node : {};
|
||||
return {
|
||||
viewIndex: path[0],
|
||||
sectionIndex: path[1],
|
||||
cardIndex: path[2],
|
||||
...record,
|
||||
[segment]: updateAtPath(record[segment], path, updater, depth + 1),
|
||||
};
|
||||
};
|
||||
|
||||
export const parseLovelaceContainerPath = (
|
||||
path: LovelaceContainerPath
|
||||
): { viewIndex: number; sectionIndex?: number } => {
|
||||
if (path.length === 1) {
|
||||
return {
|
||||
viewIndex: path[0],
|
||||
};
|
||||
}
|
||||
return {
|
||||
viewIndex: path[0],
|
||||
sectionIndex: path[1],
|
||||
};
|
||||
};
|
||||
|
||||
export const getLovelaceContainerPath = (
|
||||
path: LovelaceCardPath
|
||||
): LovelaceContainerPath => path.slice(0, -1) as LovelaceContainerPath;
|
||||
|
||||
interface FindLovelaceContainer {
|
||||
(config: LovelaceConfig, path: [number]): LovelaceViewRawConfig;
|
||||
(config: LovelaceConfig, path: [number, number]): LovelaceSectionRawConfig;
|
||||
(
|
||||
config: LovelaceConfig,
|
||||
path: LovelaceContainerPath
|
||||
): LovelaceViewRawConfig | LovelaceSectionRawConfig;
|
||||
}
|
||||
export const findLovelaceContainer: FindLovelaceContainer = ((
|
||||
const update = (
|
||||
config: LovelaceConfig,
|
||||
path: LovelaceContainerPath
|
||||
): LovelaceViewRawConfig | LovelaceSectionRawConfig => {
|
||||
const { viewIndex, sectionIndex } = parseLovelaceContainerPath(path);
|
||||
path: LovelacePath,
|
||||
updater: (node: unknown) => unknown
|
||||
): LovelaceConfig => updateAtPath(config, path, updater) as LovelaceConfig;
|
||||
|
||||
const view = config.views[viewIndex];
|
||||
|
||||
if (!view) {
|
||||
throw new Error("View does not exist");
|
||||
}
|
||||
if (sectionIndex === undefined) {
|
||||
return view;
|
||||
}
|
||||
if (isStrategyView(view)) {
|
||||
throw new Error("Can not find section in a strategy view");
|
||||
}
|
||||
|
||||
const section = view.sections?.[sectionIndex];
|
||||
|
||||
if (!section) {
|
||||
throw new Error("Section does not exist");
|
||||
}
|
||||
return section;
|
||||
}) as FindLovelaceContainer;
|
||||
|
||||
export const updateLovelaceContainer = (
|
||||
export const getAtPath = <T = unknown>(
|
||||
config: LovelaceConfig,
|
||||
path: LovelaceContainerPath,
|
||||
containerConfig: LovelaceViewRawConfig | LovelaceSectionRawConfig
|
||||
path: LovelacePath
|
||||
): T | undefined => readAtPath(config, path) as T | undefined;
|
||||
|
||||
export const setAtPath = (
|
||||
config: LovelaceConfig,
|
||||
path: LovelacePath,
|
||||
value: unknown
|
||||
): LovelaceConfig => update(config, path, () => value);
|
||||
|
||||
export const insertAtPath = (
|
||||
config: LovelaceConfig,
|
||||
path: LovelacePath,
|
||||
value: unknown
|
||||
): LovelaceConfig => {
|
||||
const { viewIndex, sectionIndex } = parseLovelaceContainerPath(path);
|
||||
|
||||
let updated = false;
|
||||
const newViews = config.views.map((view, vIndex) => {
|
||||
if (vIndex !== viewIndex) return view;
|
||||
|
||||
if (sectionIndex === undefined) {
|
||||
updated = true;
|
||||
return containerConfig as LovelaceViewRawConfig;
|
||||
}
|
||||
|
||||
if (isStrategyView(view)) {
|
||||
throw new Error("Can not update section in a strategy view");
|
||||
}
|
||||
|
||||
if (view.sections === undefined) {
|
||||
throw new Error("Section does not exist");
|
||||
}
|
||||
|
||||
const newSections = view.sections.map((section, sIndex) => {
|
||||
if (sIndex !== sectionIndex) return section;
|
||||
updated = true;
|
||||
return containerConfig as LovelaceSectionRawConfig;
|
||||
});
|
||||
return {
|
||||
...view,
|
||||
sections: newSections,
|
||||
};
|
||||
});
|
||||
|
||||
if (!updated) {
|
||||
throw new Error("Can not update cards in a non-existing view/section");
|
||||
const index = path[path.length - 1];
|
||||
if (typeof index !== "number") {
|
||||
return setAtPath(config, path, value);
|
||||
}
|
||||
return {
|
||||
...config,
|
||||
views: newViews,
|
||||
};
|
||||
return update(config, getParentPath(path), (node) => {
|
||||
const items = Array.isArray(node) ? node.slice() : [];
|
||||
items.splice(Math.max(0, Math.min(index, items.length)), 0, value);
|
||||
return items;
|
||||
});
|
||||
};
|
||||
|
||||
interface LovelaceItemKeys {
|
||||
cards: LovelaceCardConfig[];
|
||||
badges: (Partial<LovelaceBadgeConfig> | string)[];
|
||||
}
|
||||
|
||||
export const updateLovelaceItems = <T extends keyof LovelaceItemKeys>(
|
||||
key: T,
|
||||
export const appendAtPath = (
|
||||
config: LovelaceConfig,
|
||||
path: LovelaceContainerPath,
|
||||
items: LovelaceItemKeys[T]
|
||||
collectionPath: LovelacePath,
|
||||
value: unknown
|
||||
): LovelaceConfig =>
|
||||
update(config, collectionPath, (node) =>
|
||||
Array.isArray(node) ? [...node, value] : [value]
|
||||
);
|
||||
|
||||
export const deleteAtPath = (
|
||||
config: LovelaceConfig,
|
||||
path: LovelacePath
|
||||
): LovelaceConfig => {
|
||||
const { viewIndex, sectionIndex } = parseLovelaceContainerPath(path);
|
||||
|
||||
let updated = false;
|
||||
const newViews = config.views.map((view, vIndex) => {
|
||||
if (vIndex !== viewIndex) return view;
|
||||
if (isStrategyView(view)) {
|
||||
throw new Error(`Can not update ${key} in a strategy view`);
|
||||
if (path.length === 0 || getAtPath(config, path) === undefined) {
|
||||
return config;
|
||||
}
|
||||
const key = path[path.length - 1];
|
||||
return update(config, getParentPath(path), (node) => {
|
||||
if (typeof key === "number") {
|
||||
return Array.isArray(node)
|
||||
? node.filter((_item, index) => index !== key)
|
||||
: node;
|
||||
}
|
||||
if (sectionIndex === undefined) {
|
||||
updated = true;
|
||||
return {
|
||||
...view,
|
||||
[key]: items,
|
||||
};
|
||||
if (!isRecord(node)) {
|
||||
return node;
|
||||
}
|
||||
|
||||
if (view.sections === undefined) {
|
||||
throw new Error("Section does not exist");
|
||||
}
|
||||
|
||||
const newSections = view.sections.map((section, sIndex) => {
|
||||
if (sIndex !== sectionIndex) return section;
|
||||
if (isStrategySection(section)) {
|
||||
throw new Error(`Can not update ${key} in a strategy section`);
|
||||
}
|
||||
updated = true;
|
||||
return {
|
||||
...section,
|
||||
[key]: items,
|
||||
};
|
||||
});
|
||||
return {
|
||||
...view,
|
||||
sections: newSections,
|
||||
};
|
||||
const record = { ...node };
|
||||
delete record[key];
|
||||
return record;
|
||||
});
|
||||
|
||||
if (!updated) {
|
||||
throw new Error(`Can not update ${key} in a non-existing view/section`);
|
||||
}
|
||||
return {
|
||||
...config,
|
||||
views: newViews,
|
||||
};
|
||||
};
|
||||
|
||||
export const findLovelaceItems = <T extends keyof LovelaceItemKeys>(
|
||||
key: T,
|
||||
export const moveAtPath = (
|
||||
config: LovelaceConfig,
|
||||
path: LovelaceContainerPath
|
||||
): LovelaceItemKeys[T] | undefined => {
|
||||
const { viewIndex, sectionIndex } = parseLovelaceContainerPath(path);
|
||||
|
||||
const view = config.views[viewIndex];
|
||||
|
||||
if (!view) {
|
||||
throw new Error("View does not exist");
|
||||
from: LovelacePath,
|
||||
to: LovelacePath
|
||||
): LovelaceConfig => {
|
||||
if (pathEquals(from, to)) {
|
||||
return config;
|
||||
}
|
||||
if (isStrategyView(view)) {
|
||||
throw new Error("Can not find cards in a strategy view");
|
||||
if (isAncestorPath(from, to)) {
|
||||
throw new Error(
|
||||
`Cannot move ${stringifyPath(from)} into itself: ${stringifyPath(to)}`
|
||||
);
|
||||
}
|
||||
if (sectionIndex === undefined) {
|
||||
return view[key] as LovelaceItemKeys[T] | undefined;
|
||||
const value = getAtPath(config, from);
|
||||
if (value === undefined) {
|
||||
throw new Error(`Nothing to move at ${stringifyPath(from)}`);
|
||||
}
|
||||
|
||||
const section = view.sections?.[sectionIndex];
|
||||
|
||||
if (!section) {
|
||||
throw new Error("Section does not exist");
|
||||
const deleted = deleteAtPath(config, from);
|
||||
const target = [...to];
|
||||
const sameList =
|
||||
to.length === from.length &&
|
||||
pathEquals(getParentPath(from), getParentPath(to));
|
||||
const shiftedIndex = from.length - 1;
|
||||
const fromIndex = from[shiftedIndex];
|
||||
const toIndex = to[shiftedIndex];
|
||||
if (
|
||||
!sameList &&
|
||||
to.length > from.length &&
|
||||
isAncestorPath(getParentPath(from), to) &&
|
||||
typeof toIndex === "number" &&
|
||||
typeof fromIndex === "number" &&
|
||||
toIndex > fromIndex
|
||||
) {
|
||||
target[shiftedIndex] = toIndex - 1;
|
||||
}
|
||||
if (isStrategySection(section)) {
|
||||
throw new Error("Can not find cards in a strategy section");
|
||||
}
|
||||
if (key === "cards") {
|
||||
return section[key as "cards"] as LovelaceItemKeys[T] | undefined;
|
||||
}
|
||||
throw new Error(`${key} is not supported in section`);
|
||||
return insertAtPath(deleted, target, value);
|
||||
};
|
||||
|
||||
@@ -24,10 +24,8 @@ import type { HaYamlEditor } from "../../../../components/ha-yaml-editor";
|
||||
import type { LovelaceSectionRawConfig } from "../../../../data/lovelace/config/section";
|
||||
import type { LovelaceConfig } from "../../../../data/lovelace/config/types";
|
||||
import { saveConfig } from "../../../../data/lovelace/config/types";
|
||||
import {
|
||||
isStrategyView,
|
||||
type LovelaceViewConfig,
|
||||
} from "../../../../data/lovelace/config/view";
|
||||
import type { LovelaceViewRawConfig } from "../../../../data/lovelace/config/view";
|
||||
import { isStrategyView } from "../../../../data/lovelace/config/view";
|
||||
import { showAlertDialog } from "../../../../dialogs/generic/show-dialog-box";
|
||||
import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
|
||||
import { DirtyStateProviderMixin } from "../../../../mixins/dirty-state-provider-mixin";
|
||||
@@ -37,10 +35,13 @@ import {
|
||||
} from "../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type { Lovelace } from "../../types";
|
||||
import { addSection, deleteSection, moveSection } from "../config-util";
|
||||
import { addSection } from "../config-util";
|
||||
import {
|
||||
findLovelaceContainer,
|
||||
updateLovelaceContainer,
|
||||
deleteAtPath,
|
||||
getAtPath,
|
||||
getViewPath,
|
||||
moveAtPath,
|
||||
setAtPath,
|
||||
} from "../lovelace-path";
|
||||
import { showSelectViewDialog } from "../select-view/show-select-view-dialog";
|
||||
import "./hui-section-settings-editor";
|
||||
@@ -65,7 +66,7 @@ export class HuiDialogEditSection
|
||||
|
||||
@state() private _config?: LovelaceSectionRawConfig;
|
||||
|
||||
@state() private _viewConfig?: LovelaceViewConfig;
|
||||
@state() private _viewConfig?: LovelaceViewRawConfig;
|
||||
|
||||
@state() private _yamlMode = false;
|
||||
|
||||
@@ -91,13 +92,15 @@ export class HuiDialogEditSection
|
||||
|
||||
this.lovelace = params.lovelace;
|
||||
|
||||
this._config = findLovelaceContainer(this._params.lovelaceConfig, [
|
||||
this._params.viewIndex,
|
||||
this._params.sectionIndex,
|
||||
]);
|
||||
this._viewConfig = findLovelaceContainer(this._params.lovelaceConfig, [
|
||||
this._params.viewIndex,
|
||||
]);
|
||||
this._config = getAtPath<LovelaceSectionRawConfig>(
|
||||
params.lovelaceConfig,
|
||||
params.path
|
||||
);
|
||||
const viewPath = getViewPath(params.path);
|
||||
this._viewConfig = getAtPath<LovelaceViewRawConfig>(
|
||||
params.lovelaceConfig,
|
||||
viewPath
|
||||
);
|
||||
this._initDirtyTracking({ type: "deep" }, this._config);
|
||||
}
|
||||
|
||||
@@ -316,8 +319,7 @@ export class HuiDialogEditSection
|
||||
return;
|
||||
}
|
||||
|
||||
const fromViewIndex = this._params.viewIndex;
|
||||
const fromSectionIndex = this._params.sectionIndex;
|
||||
const fromPath = this._params.path;
|
||||
|
||||
// Same dashboard
|
||||
if (urlPath === this.lovelace.urlPath) {
|
||||
@@ -325,11 +327,12 @@ export class HuiDialogEditSection
|
||||
const toIndex = toView.sections?.length ?? 0;
|
||||
try {
|
||||
await this.lovelace.saveConfig(
|
||||
moveSection(
|
||||
oldConfig,
|
||||
[fromViewIndex, fromSectionIndex],
|
||||
[viewIndex, toIndex]
|
||||
)
|
||||
moveAtPath(oldConfig, fromPath, [
|
||||
"views",
|
||||
viewIndex,
|
||||
"sections",
|
||||
toIndex,
|
||||
])
|
||||
);
|
||||
this.lovelace.showToast({
|
||||
message: this.hass!.localize(
|
||||
@@ -361,20 +364,18 @@ export class HuiDialogEditSection
|
||||
const oldFromConfig = this.lovelace.config;
|
||||
const oldToConfig = selectedDashConfig;
|
||||
try {
|
||||
const section = findLovelaceContainer(oldFromConfig, [
|
||||
fromViewIndex,
|
||||
fromSectionIndex,
|
||||
]) as LovelaceSectionRawConfig;
|
||||
const section = getAtPath<LovelaceSectionRawConfig>(
|
||||
oldFromConfig,
|
||||
fromPath
|
||||
)!;
|
||||
|
||||
await saveConfig(
|
||||
this.hass!,
|
||||
urlPath,
|
||||
addSection(oldToConfig, viewIndex, section)
|
||||
addSection(oldToConfig, ["views", viewIndex], section)
|
||||
);
|
||||
|
||||
await this.lovelace.saveConfig(
|
||||
deleteSection(oldFromConfig, fromViewIndex, fromSectionIndex)
|
||||
);
|
||||
await this.lovelace.saveConfig(deleteAtPath(oldFromConfig, fromPath));
|
||||
|
||||
this.lovelace.showToast({
|
||||
message: this.hass!.localize(
|
||||
@@ -426,9 +427,9 @@ export class HuiDialogEditSection
|
||||
if (!this._params || !this._config) {
|
||||
return;
|
||||
}
|
||||
const newConfig = updateLovelaceContainer(
|
||||
const newConfig = setAtPath(
|
||||
this._params.lovelaceConfig,
|
||||
[this._params.viewIndex, this._params.sectionIndex],
|
||||
this._params.path,
|
||||
this._config
|
||||
);
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import type { LovelaceConfig } from "../../../../data/lovelace/config/types";
|
||||
import type { Lovelace } from "../../types";
|
||||
import type { LovelacePath } from "../lovelace-path";
|
||||
|
||||
export interface EditSectionDialogParams {
|
||||
lovelace: Lovelace;
|
||||
lovelaceConfig: LovelaceConfig;
|
||||
saveConfig: (config: LovelaceConfig) => void;
|
||||
viewIndex: number;
|
||||
sectionIndex: number;
|
||||
path: LovelacePath;
|
||||
}
|
||||
|
||||
const importEditSectionDialog = () => import("./hui-dialog-edit-section");
|
||||
|
||||
@@ -119,7 +119,7 @@ export class HuiUnusedEntities extends LitElement {
|
||||
showSuggestCardDialog(this, {
|
||||
lovelaceConfig: this.lovelace.config!,
|
||||
saveConfig: this.lovelace.saveConfig,
|
||||
path: [0],
|
||||
path: ["views", 0],
|
||||
entities: this._selectedEntities,
|
||||
cardConfig,
|
||||
sectionConfig,
|
||||
@@ -133,7 +133,7 @@ export class HuiUnusedEntities extends LitElement {
|
||||
showSuggestCardDialog(this, {
|
||||
lovelaceConfig: this.lovelace.config!,
|
||||
saveConfig: this.lovelace.saveConfig,
|
||||
path: [viewIndex],
|
||||
path: ["views", viewIndex],
|
||||
entities: this._selectedEntities,
|
||||
cardConfig,
|
||||
sectionConfig,
|
||||
|
||||
@@ -17,8 +17,8 @@ import type { HomeAssistant } from "../../../types";
|
||||
import type { HuiCard } from "../cards/hui-card";
|
||||
import { computeCardGridSize } from "../common/compute-card-grid-size";
|
||||
import "../components/hui-card-edit-mode";
|
||||
import { moveCard } from "../editor/config-util";
|
||||
import type { LovelaceCardPath } from "../editor/lovelace-path";
|
||||
import type { LovelacePath } from "../editor/lovelace-path";
|
||||
import { moveAtPath } from "../editor/lovelace-path";
|
||||
import type { Lovelace } from "../types";
|
||||
|
||||
const CARD_SORTABLE_OPTIONS: HaSortableOptions = {
|
||||
@@ -44,9 +44,7 @@ export class GridSection extends LitElement implements LovelaceSectionElement {
|
||||
|
||||
@property({ attribute: false }) public lovelace?: Lovelace;
|
||||
|
||||
@property({ type: Number }) public index?: number;
|
||||
|
||||
@property({ attribute: false }) public viewIndex?: number;
|
||||
@property({ attribute: false }) public path?: LovelacePath;
|
||||
|
||||
@property({ attribute: false }) public isStrategy = false;
|
||||
|
||||
@@ -110,11 +108,9 @@ export class GridSection extends LitElement implements LovelaceSectionElement {
|
||||
|
||||
const { rows, columns } = computeCardGridSize(gridOptions);
|
||||
|
||||
const cardPath: LovelaceCardPath = [
|
||||
this.viewIndex!,
|
||||
this.index!,
|
||||
idx,
|
||||
];
|
||||
const cardPath: LovelacePath | undefined = editMode
|
||||
? [...this.path!, "cards", idx]
|
||||
: undefined;
|
||||
return html`
|
||||
<div
|
||||
style=${styleMap({
|
||||
@@ -133,7 +129,7 @@ export class GridSection extends LitElement implements LovelaceSectionElement {
|
||||
? html`
|
||||
<hui-card-edit-mode
|
||||
.lovelace=${this.lovelace!}
|
||||
.path=${cardPath}
|
||||
.path=${cardPath!}
|
||||
.hiddenOverlay=${this._dragging}
|
||||
.noEdit=${this.importOnly}
|
||||
.noDuplicate=${this.importOnly}
|
||||
@@ -174,19 +170,22 @@ export class GridSection extends LitElement implements LovelaceSectionElement {
|
||||
private _cardMoved(ev) {
|
||||
ev.stopPropagation();
|
||||
const { oldIndex, newIndex } = ev.detail;
|
||||
const newConfig = moveCard(
|
||||
const newConfig = moveAtPath(
|
||||
this.lovelace!.config,
|
||||
[this.viewIndex!, this.index!, oldIndex],
|
||||
[this.viewIndex!, this.index!, newIndex]
|
||||
[...this.path!, "cards", oldIndex],
|
||||
[...this.path!, "cards", newIndex]
|
||||
);
|
||||
this.lovelace!.saveConfig(newConfig);
|
||||
}
|
||||
|
||||
private _cardAdded(ev) {
|
||||
ev.stopPropagation();
|
||||
const { index, data } = ev.detail;
|
||||
const oldPath = data as LovelaceCardPath;
|
||||
const newPath = [this.viewIndex!, this.index!, index] as LovelaceCardPath;
|
||||
const newConfig = moveCard(this.lovelace!.config, oldPath, newPath);
|
||||
const newConfig = moveAtPath(this.lovelace!.config, data as LovelacePath, [
|
||||
...this.path!,
|
||||
"cards",
|
||||
index,
|
||||
]);
|
||||
this.lovelace!.saveConfig(newConfig);
|
||||
}
|
||||
|
||||
@@ -204,7 +203,10 @@ export class GridSection extends LitElement implements LovelaceSectionElement {
|
||||
}
|
||||
|
||||
private _addCard() {
|
||||
fireEvent(this, "ll-create-card", { suggested: ["tile", "heading"] });
|
||||
fireEvent(this, "ll-create-card", {
|
||||
path: [...this.path!, "cards"],
|
||||
suggested: ["tile", "heading"],
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import deepClone from "deep-clone-simple";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { ReactiveElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { storage } from "../../../common/decorators/storage";
|
||||
import { deepEqual } from "../../../common/util/deep-equal";
|
||||
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
@@ -20,11 +18,7 @@ import { ConditionalListenerMixin } from "../../../mixins/conditional-listener-m
|
||||
import "../cards/hui-card";
|
||||
import type { HuiCard } from "../cards/hui-card";
|
||||
import { createSectionElement } from "../create-element/create-section-element";
|
||||
import { showCreateCardDialog } from "../editor/card-editor/show-create-card-dialog";
|
||||
import { showEditCardDialog } from "../editor/card-editor/show-edit-card-dialog";
|
||||
import { addCard, replaceCard } from "../editor/config-util";
|
||||
import { performDeleteCard } from "../editor/delete-card";
|
||||
import { parseLovelaceCardPath } from "../editor/lovelace-path";
|
||||
import type { LovelacePath } from "../editor/lovelace-path";
|
||||
import {
|
||||
checkStrategyShouldRegenerate,
|
||||
generateLovelaceSectionStrategy,
|
||||
@@ -53,9 +47,7 @@ export class HuiSection extends ConditionalListenerMixin<LovelaceSectionConfig>(
|
||||
@property({ type: Boolean, attribute: "import-only" })
|
||||
public importOnly = false;
|
||||
|
||||
@property({ type: Number }) public index!: number;
|
||||
|
||||
@property({ attribute: false }) public viewIndex!: number;
|
||||
@property({ attribute: false }) public path!: LovelacePath;
|
||||
|
||||
@state() private _cards: HuiCard[] = [];
|
||||
|
||||
@@ -63,14 +55,6 @@ export class HuiSection extends ConditionalListenerMixin<LovelaceSectionConfig>(
|
||||
|
||||
private _layoutElement?: LovelaceSectionElement;
|
||||
|
||||
@storage({
|
||||
key: "dashboardCardClipboard",
|
||||
state: false,
|
||||
subscribe: false,
|
||||
storage: "sessionStorage",
|
||||
})
|
||||
protected _clipboard?: LovelaceCardConfig;
|
||||
|
||||
private _createCardElement(cardConfig: LovelaceCardConfig) {
|
||||
const element = document.createElement("hui-card");
|
||||
element.hass = this.hass;
|
||||
@@ -193,6 +177,9 @@ export class HuiSection extends ConditionalListenerMixin<LovelaceSectionConfig>(
|
||||
if (changedProperties.has("importOnly")) {
|
||||
this._layoutElement.importOnly = this.importOnly;
|
||||
}
|
||||
if (changedProperties.has("path")) {
|
||||
this._layoutElement.path = this.path;
|
||||
}
|
||||
if (changedProperties.has("_cards")) {
|
||||
this._layoutElement.cards = this._cards;
|
||||
}
|
||||
@@ -251,8 +238,7 @@ export class HuiSection extends ConditionalListenerMixin<LovelaceSectionConfig>(
|
||||
this._layoutElement!.isStrategy = isStrategy;
|
||||
this._layoutElement!.hass = this.hass;
|
||||
this._layoutElement!.lovelace = this.lovelace;
|
||||
this._layoutElement!.index = this.index;
|
||||
this._layoutElement!.viewIndex = this.viewIndex;
|
||||
this._layoutElement!.path = this.path;
|
||||
this._layoutElement!.cards = this._cards;
|
||||
|
||||
if (addLayoutElement) {
|
||||
@@ -317,81 +303,6 @@ export class HuiSection extends ConditionalListenerMixin<LovelaceSectionConfig>(
|
||||
config
|
||||
) as LovelaceSectionElement;
|
||||
this._layoutElementType = config.type;
|
||||
this._layoutElement.addEventListener("ll-create-card", (ev) => {
|
||||
ev.stopPropagation();
|
||||
if (!this.lovelace) return;
|
||||
showCreateCardDialog(this, {
|
||||
lovelaceConfig: this.lovelace.config,
|
||||
saveConfig: this.lovelace.saveConfig,
|
||||
path: [this.viewIndex, this.index],
|
||||
suggestedCards: ev.detail?.suggested,
|
||||
});
|
||||
});
|
||||
this._layoutElement.addEventListener("ll-edit-card", (ev) => {
|
||||
ev.stopPropagation();
|
||||
if (!this.lovelace) return;
|
||||
const { cardIndex } = parseLovelaceCardPath(ev.detail.path);
|
||||
const sectionConfig = this.config;
|
||||
if (isStrategySection(sectionConfig)) {
|
||||
return;
|
||||
}
|
||||
const cardConfig = sectionConfig.cards![cardIndex];
|
||||
showEditCardDialog(this, {
|
||||
lovelaceConfig: this.lovelace.config,
|
||||
saveCardConfig: async (newCardConfig) => {
|
||||
const newConfig = replaceCard(
|
||||
this.lovelace!.config,
|
||||
[this.viewIndex, this.index, cardIndex],
|
||||
newCardConfig
|
||||
);
|
||||
await this.lovelace!.saveConfig(newConfig);
|
||||
},
|
||||
sectionConfig,
|
||||
cardConfig,
|
||||
});
|
||||
});
|
||||
this._layoutElement.addEventListener("ll-delete-card", (ev) => {
|
||||
ev.stopPropagation();
|
||||
if (!this.lovelace) return;
|
||||
performDeleteCard(this.hass, this.lovelace, ev.detail);
|
||||
});
|
||||
this._layoutElement.addEventListener("ll-duplicate-card", (ev) => {
|
||||
ev.stopPropagation();
|
||||
if (!this.lovelace) return;
|
||||
const { cardIndex } = parseLovelaceCardPath(ev.detail.path);
|
||||
const sectionConfig = this.config;
|
||||
if (isStrategySection(sectionConfig)) {
|
||||
return;
|
||||
}
|
||||
const cardConfig = sectionConfig.cards![cardIndex];
|
||||
|
||||
showEditCardDialog(this, {
|
||||
lovelaceConfig: this.lovelace!.config,
|
||||
saveCardConfig: async (newCardConfig) => {
|
||||
const newConfig = addCard(
|
||||
this.lovelace!.config,
|
||||
[this.viewIndex, this.index],
|
||||
newCardConfig
|
||||
);
|
||||
await this.lovelace!.saveConfig(newConfig);
|
||||
},
|
||||
cardConfig,
|
||||
sectionConfig,
|
||||
isNew: true,
|
||||
});
|
||||
});
|
||||
this._layoutElement.addEventListener("ll-copy-card", (ev) => {
|
||||
ev.stopPropagation();
|
||||
if (!this.lovelace) return;
|
||||
const { cardIndex } = parseLovelaceCardPath(ev.detail.path);
|
||||
const sectionConfig = this.config;
|
||||
|
||||
if (isStrategySection(sectionConfig)) {
|
||||
return;
|
||||
}
|
||||
const cardConfig = sectionConfig.cards![cardIndex];
|
||||
this._clipboard = deepClone(cardConfig);
|
||||
});
|
||||
}
|
||||
|
||||
private _createCards(config: LovelaceSectionConfig): void {
|
||||
|
||||
@@ -13,6 +13,7 @@ import type { HuiBadge } from "../badges/hui-badge";
|
||||
import "../badges/hui-view-badges";
|
||||
import type { HuiCard } from "../cards/hui-card";
|
||||
import { computeCardSize } from "../common/compute-card-size";
|
||||
import type { LovelacePath } from "../editor/lovelace-path";
|
||||
import type { Lovelace } from "../types";
|
||||
|
||||
// Find column with < 5 size, else smallest column
|
||||
@@ -41,7 +42,7 @@ export class MasonryView extends LitElement implements LovelaceViewElement {
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@property({ type: Number }) public index?: number;
|
||||
@property({ attribute: false }) public path?: LovelacePath;
|
||||
|
||||
@property({ attribute: false }) public isStrategy = false;
|
||||
|
||||
@@ -85,7 +86,7 @@ export class MasonryView extends LitElement implements LovelaceViewElement {
|
||||
<hui-view-badges
|
||||
.badges=${this.badges}
|
||||
.lovelace=${this.lovelace}
|
||||
.viewIndex=${this.index}
|
||||
.path=${[...this.path!, "badges"]}
|
||||
show-add-label
|
||||
></hui-view-badges>
|
||||
<div
|
||||
@@ -160,7 +161,7 @@ export class MasonryView extends LitElement implements LovelaceViewElement {
|
||||
}
|
||||
|
||||
private _addCard(): void {
|
||||
fireEvent(this, "ll-create-card");
|
||||
fireEvent(this, "ll-create-card", { path: [...this.path!, "cards"] });
|
||||
}
|
||||
|
||||
private _createRootElement(columns: HTMLDivElement[]) {
|
||||
@@ -268,7 +269,7 @@ export class MasonryView extends LitElement implements LovelaceViewElement {
|
||||
const wrapper = document.createElement("hui-card-options");
|
||||
wrapper.hass = this.hass;
|
||||
wrapper.lovelace = this.lovelace;
|
||||
wrapper.path = [this.index!, index];
|
||||
wrapper.path = [...this.path!, "cards", index];
|
||||
card.preview = true;
|
||||
wrapper.appendChild(card);
|
||||
columnEl.appendChild(wrapper);
|
||||
|
||||
@@ -12,6 +12,7 @@ import type { LovelaceViewConfig } from "../../../data/lovelace/config/view";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import type { HuiCard } from "../cards/hui-card";
|
||||
import type { HuiCardOptions } from "../components/hui-card-options";
|
||||
import type { LovelacePath } from "../editor/lovelace-path";
|
||||
import type { Lovelace } from "../types";
|
||||
|
||||
let editCodeLoaded = false;
|
||||
@@ -22,7 +23,7 @@ export class PanelView extends LitElement implements LovelaceViewElement {
|
||||
|
||||
@property({ attribute: false }) public lovelace?: Lovelace;
|
||||
|
||||
@property({ type: Number }) public index?: number;
|
||||
@property({ attribute: false }) public path?: LovelacePath;
|
||||
|
||||
@property({ attribute: false }) public isStrategy = false;
|
||||
|
||||
@@ -96,7 +97,7 @@ export class PanelView extends LitElement implements LovelaceViewElement {
|
||||
}
|
||||
|
||||
private _addCard(): void {
|
||||
fireEvent(this, "ll-create-card");
|
||||
fireEvent(this, "ll-create-card", { path: [...this.path!, "cards"] });
|
||||
}
|
||||
|
||||
private _createCard(): void {
|
||||
@@ -117,7 +118,7 @@ export class PanelView extends LitElement implements LovelaceViewElement {
|
||||
const wrapper = document.createElement("hui-card-options");
|
||||
wrapper.hass = this.hass;
|
||||
wrapper.lovelace = this.lovelace;
|
||||
wrapper.path = [this.index!, 0];
|
||||
wrapper.path = [...this.path!, "cards", 0];
|
||||
wrapper.hidePosition = true;
|
||||
card.preview = true;
|
||||
wrapper.appendChild(card);
|
||||
|
||||
@@ -22,13 +22,9 @@ import type { HomeAssistant } from "../../../types";
|
||||
import type { HuiBadge } from "../badges/hui-badge";
|
||||
import type { HuiCard } from "../cards/hui-card";
|
||||
import "../components/hui-section-edit-mode";
|
||||
import { addSection, moveCard, moveSection } from "../editor/config-util";
|
||||
import type { LovelaceCardPath } from "../editor/lovelace-path";
|
||||
import {
|
||||
findLovelaceItems,
|
||||
getLovelaceContainerPath,
|
||||
parseLovelaceCardPath,
|
||||
} from "../editor/lovelace-path";
|
||||
import { addSection } from "../editor/config-util";
|
||||
import type { LovelacePath } from "../editor/lovelace-path";
|
||||
import { getAtPath, moveAtPath } from "../editor/lovelace-path";
|
||||
import type { HuiSection } from "../sections/hui-section";
|
||||
import "../sections/hui-section-background";
|
||||
import type { Lovelace } from "../types";
|
||||
@@ -48,7 +44,7 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
|
||||
|
||||
@property({ attribute: false }) public lovelace?: Lovelace;
|
||||
|
||||
@property({ type: Number }) public index?: number;
|
||||
@property({ attribute: false }) public path?: LovelacePath;
|
||||
|
||||
@property({ attribute: false }) public isStrategy = false;
|
||||
|
||||
@@ -223,7 +219,7 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
|
||||
.hass=${this.hass}
|
||||
.badges=${this.badges}
|
||||
.lovelace=${this.lovelace}
|
||||
.viewIndex=${this.index}
|
||||
.path=${this.path!}
|
||||
.config=${this._config?.header}
|
||||
></hui-view-header>
|
||||
${
|
||||
@@ -287,8 +283,7 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
|
||||
<hui-section-edit-mode
|
||||
.hass=${this.hass}
|
||||
.lovelace=${this.lovelace}
|
||||
.index=${idx}
|
||||
.viewIndex=${this.index}
|
||||
.path=${[...this.path!, "sections", idx]}
|
||||
>
|
||||
${this._renderSection(
|
||||
section,
|
||||
@@ -354,7 +349,7 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
|
||||
.hass=${this.hass}
|
||||
.badges=${this.badges}
|
||||
.lovelace=${this.lovelace}
|
||||
.viewIndex=${this.index}
|
||||
.path=${this.path!}
|
||||
.config=${this._config.sidebar}
|
||||
@sidebar-visibility-changed=${
|
||||
this._handleSidebarVisibilityChanged
|
||||
@@ -367,7 +362,7 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
|
||||
<hui-view-footer
|
||||
.hass=${this.hass}
|
||||
.lovelace=${this.lovelace}
|
||||
.viewIndex=${this.index}
|
||||
.path=${this.path!}
|
||||
.config=${this._config?.footer}
|
||||
></hui-view-footer>
|
||||
<div class="imported-cards-section">
|
||||
@@ -394,7 +389,7 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
|
||||
.config=${this._importedCardSectionConfig(
|
||||
this._config.cards
|
||||
)}
|
||||
.viewIndex=${this.index}
|
||||
.path=${this.path!}
|
||||
preview
|
||||
import-only
|
||||
></hui-section>
|
||||
@@ -408,32 +403,36 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
|
||||
}
|
||||
|
||||
private _handleCardAdded(ev) {
|
||||
const { data } = ev.detail;
|
||||
const oldPath = data as LovelaceCardPath;
|
||||
|
||||
const { cardIndex } = parseLovelaceCardPath(oldPath);
|
||||
const containerPath = getLovelaceContainerPath(oldPath);
|
||||
const cards = findLovelaceItems(
|
||||
"cards",
|
||||
this.lovelace!.config,
|
||||
containerPath
|
||||
);
|
||||
const cardConfig = cards![cardIndex];
|
||||
ev.stopPropagation();
|
||||
// The dropped node only serves as a drop target, the new section renders the card from the config
|
||||
(ev.detail.item as HTMLElement).remove();
|
||||
const oldPath = ev.detail.data as LovelacePath;
|
||||
const config = this.lovelace!.config;
|
||||
const cardConfig = getAtPath<LovelaceCardConfig>(config, oldPath);
|
||||
if (!cardConfig) {
|
||||
return;
|
||||
}
|
||||
|
||||
const configWithNewSection = addSection(
|
||||
this.lovelace!.config,
|
||||
this.index!,
|
||||
config,
|
||||
this.path!,
|
||||
generateDefaultSection(this.hass.localize, cardConfig.type !== "heading") // If we move a heading card, we don't want to include a heading in the new section
|
||||
);
|
||||
const viewConfig = configWithNewSection.views[
|
||||
this.index!
|
||||
] as LovelaceViewConfig;
|
||||
const newPath = [
|
||||
this.index!,
|
||||
viewConfig.sections!.length - 1,
|
||||
1,
|
||||
] as LovelaceCardPath;
|
||||
const newConfig = moveCard(configWithNewSection, oldPath, newPath);
|
||||
const sectionsPath = [...this.path!, "sections"];
|
||||
const newIndex =
|
||||
getAtPath<unknown[]>(configWithNewSection, sectionsPath)!.length - 1;
|
||||
const cardCount =
|
||||
getAtPath<unknown[]>(configWithNewSection, [
|
||||
...sectionsPath,
|
||||
newIndex,
|
||||
"cards",
|
||||
])?.length ?? 0;
|
||||
const newConfig = moveAtPath(configWithNewSection, oldPath, [
|
||||
...sectionsPath,
|
||||
newIndex,
|
||||
"cards",
|
||||
cardCount,
|
||||
]);
|
||||
this.lovelace!.saveConfig(newConfig);
|
||||
}
|
||||
|
||||
@@ -471,7 +470,7 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
|
||||
private _createSection(): void {
|
||||
const newConfig = addSection(
|
||||
this.lovelace!.config,
|
||||
this.index!,
|
||||
this.path!,
|
||||
generateDefaultSection(this.hass.localize, true)
|
||||
);
|
||||
this.lovelace!.saveConfig(newConfig);
|
||||
@@ -481,10 +480,10 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
|
||||
ev.stopPropagation();
|
||||
const { oldIndex, newIndex } = ev.detail;
|
||||
|
||||
const newConfig = moveSection(
|
||||
const newConfig = moveAtPath(
|
||||
this.lovelace!.config,
|
||||
[this.index!, oldIndex],
|
||||
[this.index!, newIndex]
|
||||
[...this.path!, "sections", oldIndex],
|
||||
[...this.path!, "sections", newIndex]
|
||||
);
|
||||
this.lovelace!.saveConfig(newConfig);
|
||||
}
|
||||
|
||||
@@ -11,7 +11,8 @@ import type { HuiBadge } from "../badges/hui-badge";
|
||||
import "../badges/hui-view-badges";
|
||||
import type { HuiCard } from "../cards/hui-card";
|
||||
import type { HuiCardOptions } from "../components/hui-card-options";
|
||||
import { replaceCard } from "../editor/config-util";
|
||||
import type { LovelacePath } from "../editor/lovelace-path";
|
||||
import { setAtPath } from "../editor/lovelace-path";
|
||||
import type { Lovelace } from "../types";
|
||||
|
||||
@customElement("hui-sidebar-view")
|
||||
@@ -20,7 +21,7 @@ export class SideBarView extends LitElement implements LovelaceViewElement {
|
||||
|
||||
@property({ attribute: false }) public lovelace?: Lovelace;
|
||||
|
||||
@property({ type: Number }) public index?: number;
|
||||
@property({ attribute: false }) public path?: LovelacePath;
|
||||
|
||||
@property({ attribute: false }) public isStrategy = false;
|
||||
|
||||
@@ -93,7 +94,7 @@ export class SideBarView extends LitElement implements LovelaceViewElement {
|
||||
<hui-view-badges
|
||||
.badges=${this.badges}
|
||||
.lovelace=${this.lovelace}
|
||||
.viewIndex=${this.index}
|
||||
.path=${[...this.path!, "badges"]}
|
||||
show-add-label
|
||||
></hui-view-badges>
|
||||
<div
|
||||
@@ -113,7 +114,7 @@ export class SideBarView extends LitElement implements LovelaceViewElement {
|
||||
}
|
||||
|
||||
private _addCard(): void {
|
||||
fireEvent(this, "ll-create-card");
|
||||
fireEvent(this, "ll-create-card", { path: [...this.path!, "cards"] });
|
||||
}
|
||||
|
||||
private _createCards(): void {
|
||||
@@ -156,7 +157,7 @@ export class SideBarView extends LitElement implements LovelaceViewElement {
|
||||
element = document.createElement("hui-card-options");
|
||||
element.hass = this.hass;
|
||||
element.lovelace = this.lovelace;
|
||||
element.path = [this.index!, idx];
|
||||
element.path = [...this.path!, "cards", idx];
|
||||
card.preview = true;
|
||||
const movePositionButton = document.createElement("ha-icon-button");
|
||||
movePositionButton.slot = "buttons";
|
||||
@@ -168,7 +169,7 @@ export class SideBarView extends LitElement implements LovelaceViewElement {
|
||||
movePositionButton.appendChild(moveIcon);
|
||||
movePositionButton.addEventListener("click", () => {
|
||||
this.lovelace!.saveConfig(
|
||||
replaceCard(this.lovelace!.config, [this.index!, idx], {
|
||||
setAtPath(this.lovelace!.config, [...this.path!, "cards", idx], {
|
||||
...cardConfig!,
|
||||
view_layout: {
|
||||
position:
|
||||
|
||||
@@ -9,15 +9,14 @@ import "../../../components/ha-svg-icon";
|
||||
import type { LovelaceCardConfig } from "../../../data/lovelace/config/card";
|
||||
import {
|
||||
DEFAULT_FOOTER_MAX_WIDTH_PX,
|
||||
type LovelaceViewConfig,
|
||||
type LovelaceViewFooterConfig,
|
||||
} from "../../../data/lovelace/config/view";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import type { HuiCard } from "../cards/hui-card";
|
||||
import { computeCardGridSize } from "../common/compute-card-grid-size";
|
||||
import { showCreateCardDialog } from "../editor/card-editor/show-create-card-dialog";
|
||||
import { showEditCardDialog } from "../editor/card-editor/show-edit-card-dialog";
|
||||
import { replaceView } from "../editor/config-util";
|
||||
import type { LovelacePath } from "../editor/lovelace-path";
|
||||
import { setAtPath } from "../editor/lovelace-path";
|
||||
import { showEditViewFooterDialog } from "../editor/view-footer/show-edit-view-footer-dialog";
|
||||
import type { Lovelace } from "../types";
|
||||
|
||||
@@ -31,7 +30,7 @@ export class HuiViewFooter extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public config?: LovelaceViewFooterConfig;
|
||||
|
||||
@property({ attribute: false }) public viewIndex!: number;
|
||||
@property({ attribute: false }) public path!: LovelacePath;
|
||||
|
||||
public connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
@@ -90,24 +89,18 @@ export class HuiViewFooter extends LitElement {
|
||||
return element;
|
||||
}
|
||||
|
||||
private get _cardPath(): LovelacePath {
|
||||
return [...this.path, "footer", "card"];
|
||||
}
|
||||
|
||||
private _addCard() {
|
||||
showCreateCardDialog(this, {
|
||||
lovelaceConfig: this.lovelace.config,
|
||||
saveConfig: this.lovelace.saveConfig,
|
||||
path: [this.viewIndex],
|
||||
saveCard: (newCardConfig: LovelaceCardConfig) => {
|
||||
this._saveFooterConfig({ ...this.config, card: newCardConfig });
|
||||
},
|
||||
path: this._cardPath,
|
||||
});
|
||||
}
|
||||
|
||||
private _deleteCard(ev) {
|
||||
ev.stopPropagation();
|
||||
const newConfig = { ...this.config };
|
||||
delete newConfig.card;
|
||||
this._saveFooterConfig(newConfig);
|
||||
}
|
||||
|
||||
private _configure() {
|
||||
showEditViewFooterDialog(this, {
|
||||
config: this.config || {},
|
||||
@@ -117,34 +110,10 @@ export class HuiViewFooter extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _editCard(ev) {
|
||||
ev.stopPropagation();
|
||||
const cardConfig = this.config?.card;
|
||||
if (!cardConfig) return;
|
||||
|
||||
showEditCardDialog(this, {
|
||||
cardConfig,
|
||||
lovelaceConfig: this.lovelace.config,
|
||||
saveCardConfig: (newCardConfig: LovelaceCardConfig) => {
|
||||
this._saveFooterConfig({ ...this.config, card: newCardConfig });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private _saveFooterConfig(footerConfig: LovelaceViewFooterConfig) {
|
||||
const viewConfig = this.lovelace.config.views[
|
||||
this.viewIndex
|
||||
] as LovelaceViewConfig;
|
||||
|
||||
const config = { ...viewConfig, footer: footerConfig };
|
||||
|
||||
const updatedConfig = replaceView(
|
||||
this.hass,
|
||||
this.lovelace.config,
|
||||
this.viewIndex,
|
||||
config
|
||||
this.lovelace.saveConfig(
|
||||
setAtPath(this.lovelace.config, [...this.path, "footer"], footerConfig)
|
||||
);
|
||||
this.lovelace.saveConfig(updatedConfig);
|
||||
}
|
||||
|
||||
private _renderCard(card: HuiCard, editMode: boolean) {
|
||||
@@ -164,10 +133,8 @@ export class HuiViewFooter extends LitElement {
|
||||
editMode
|
||||
? html`
|
||||
<hui-card-edit-mode
|
||||
@ll-edit-card=${this._editCard}
|
||||
@ll-delete-card=${this._deleteCard}
|
||||
.lovelace=${this.lovelace!}
|
||||
.path=${[0]}
|
||||
.path=${this._cardPath}
|
||||
no-duplicate
|
||||
no-move
|
||||
>
|
||||
|
||||
@@ -8,16 +8,14 @@ import "../../../components/ha-ripple";
|
||||
import "../../../components/ha-sortable";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import type { LovelaceCardConfig } from "../../../data/lovelace/config/card";
|
||||
import type {
|
||||
LovelaceViewConfig,
|
||||
LovelaceViewHeaderConfig,
|
||||
} from "../../../data/lovelace/config/view";
|
||||
import type { LovelaceViewHeaderConfig } from "../../../data/lovelace/config/view";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import type { HuiBadge } from "../badges/hui-badge";
|
||||
import "../badges/hui-view-badges";
|
||||
import type { HuiCard } from "../cards/hui-card";
|
||||
import { showEditCardDialog } from "../editor/card-editor/show-edit-card-dialog";
|
||||
import { replaceView } from "../editor/config-util";
|
||||
import type { LovelacePath } from "../editor/lovelace-path";
|
||||
import { setAtPath } from "../editor/lovelace-path";
|
||||
import { showEditViewHeaderDialog } from "../editor/view-header/show-edit-view-header-dialog";
|
||||
import type { Lovelace } from "../types";
|
||||
|
||||
@@ -37,7 +35,7 @@ export class HuiViewHeader extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public config?: LovelaceViewHeaderConfig;
|
||||
|
||||
@property({ attribute: false }) public viewIndex!: number;
|
||||
@property({ attribute: false }) public path!: LovelacePath;
|
||||
|
||||
private _checkHidden() {
|
||||
const allHidden =
|
||||
@@ -129,55 +127,22 @@ export class HuiViewHeader extends LitElement {
|
||||
cardConfig,
|
||||
lovelaceConfig: this.lovelace.config,
|
||||
saveCardConfig: (newCardConfig: LovelaceCardConfig) => {
|
||||
const newConfig = { ...this.config };
|
||||
newConfig.card = newCardConfig;
|
||||
this._saveHeaderConfig(newConfig);
|
||||
this.lovelace.saveConfig(
|
||||
setAtPath(this.lovelace.config, this._cardPath, newCardConfig)
|
||||
);
|
||||
},
|
||||
isNew: true,
|
||||
});
|
||||
}
|
||||
|
||||
private _deleteCard(ev) {
|
||||
ev.stopPropagation();
|
||||
const newConfig = { ...this.config };
|
||||
delete newConfig.card;
|
||||
this._saveHeaderConfig(newConfig);
|
||||
}
|
||||
|
||||
private _editCard(ev) {
|
||||
ev.stopPropagation();
|
||||
const cardConfig = this.config!.card;
|
||||
|
||||
if (!cardConfig) {
|
||||
return;
|
||||
}
|
||||
|
||||
showEditCardDialog(this, {
|
||||
cardConfig,
|
||||
lovelaceConfig: this.lovelace.config,
|
||||
saveCardConfig: (newCardConfig: LovelaceCardConfig) => {
|
||||
const newConfig = { ...this.config };
|
||||
newConfig.card = newCardConfig;
|
||||
this._saveHeaderConfig(newConfig);
|
||||
},
|
||||
});
|
||||
private get _cardPath(): LovelacePath {
|
||||
return [...this.path, "header", "card"];
|
||||
}
|
||||
|
||||
private _saveHeaderConfig(headerConfig: LovelaceViewHeaderConfig) {
|
||||
const viewConfig = this.lovelace.config.views[
|
||||
this.viewIndex
|
||||
] as LovelaceViewConfig;
|
||||
|
||||
const config = { ...viewConfig };
|
||||
config.header = headerConfig;
|
||||
|
||||
const updatedConfig = replaceView(
|
||||
this.hass,
|
||||
this.lovelace.config,
|
||||
this.viewIndex,
|
||||
config
|
||||
this.lovelace.saveConfig(
|
||||
setAtPath(this.lovelace.config, [...this.path, "header"], headerConfig)
|
||||
);
|
||||
this.lovelace.saveConfig(updatedConfig);
|
||||
}
|
||||
|
||||
private _configure = () => {
|
||||
@@ -243,10 +208,8 @@ export class HuiViewHeader extends LitElement {
|
||||
? card
|
||||
? html`
|
||||
<hui-card-edit-mode
|
||||
@ll-edit-card=${this._editCard}
|
||||
@ll-delete-card=${this._deleteCard}
|
||||
.lovelace=${this.lovelace!}
|
||||
.path=${[0]}
|
||||
.path=${this._cardPath}
|
||||
no-duplicate
|
||||
no-move
|
||||
>
|
||||
@@ -277,7 +240,7 @@ export class HuiViewHeader extends LitElement {
|
||||
<hui-view-badges
|
||||
.badges=${this.badges}
|
||||
.lovelace=${this.lovelace!}
|
||||
.viewIndex=${this.viewIndex!}
|
||||
.path=${[...this.path, "badges"]}
|
||||
.showAddLabel=${this.badges.length === 0}
|
||||
></hui-view-badges>
|
||||
</div>
|
||||
|
||||
@@ -7,6 +7,7 @@ import type { LovelaceViewSidebarConfig } from "../../../data/lovelace/config/vi
|
||||
import { ConditionalListenerMixin } from "../../../mixins/conditional-listener-mixin";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import "../sections/hui-section";
|
||||
import type { LovelacePath } from "../editor/lovelace-path";
|
||||
import type { Lovelace } from "../types";
|
||||
import type { LovelaceSectionConfig } from "../../../data/lovelace/config/section";
|
||||
|
||||
@@ -22,7 +23,7 @@ export class HuiViewSidebar extends ConditionalListenerMixin<LovelaceViewSidebar
|
||||
|
||||
@property({ attribute: false }) public config?: LovelaceViewSidebarConfig;
|
||||
|
||||
@property({ attribute: false }) public viewIndex!: number;
|
||||
@property({ attribute: false }) public path!: LovelacePath;
|
||||
|
||||
private _visible = true;
|
||||
|
||||
@@ -63,12 +64,12 @@ export class HuiViewSidebar extends ConditionalListenerMixin<LovelaceViewSidebar
|
||||
${repeat(
|
||||
this.config?.sections ?? [],
|
||||
(section) => this._getSectionKey(section),
|
||||
(section) => html`
|
||||
(section, idx) => html`
|
||||
<hui-section
|
||||
.config=${section}
|
||||
.hass=${this.hass}
|
||||
.preview=${this.lovelace.editMode}
|
||||
.viewIndex=${this.viewIndex}
|
||||
.path=${[...this.path, "sidebar", "sections", idx]}
|
||||
></hui-section>
|
||||
`
|
||||
)}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { consume } from "@lit/context";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { ReactiveElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { storage } from "../../../common/decorators/storage";
|
||||
import type { HASSDomEvent } from "../../../common/dom/fire_event";
|
||||
import { debounce } from "../../../common/util/debounce";
|
||||
@@ -32,7 +33,7 @@ import { showCreateBadgeDialog } from "../editor/badge-editor/show-create-badge-
|
||||
import { showEditBadgeDialog } from "../editor/badge-editor/show-edit-badge-dialog";
|
||||
import { showCreateCardDialog } from "../editor/card-editor/show-create-card-dialog";
|
||||
import { showEditCardDialog } from "../editor/card-editor/show-edit-card-dialog";
|
||||
import { addCard, replaceCard } from "../editor/config-util";
|
||||
import { getCardSectionConfig } from "../editor/config-util";
|
||||
import {
|
||||
type DeleteBadgeParams,
|
||||
performDeleteBadge,
|
||||
@@ -41,8 +42,15 @@ import {
|
||||
type DeleteCardParams,
|
||||
performDeleteCard,
|
||||
} from "../editor/delete-card";
|
||||
import type { LovelaceCardPath } from "../editor/lovelace-path";
|
||||
import { parseLovelaceCardPath } from "../editor/lovelace-path";
|
||||
import type { LovelacePath } from "../editor/lovelace-path";
|
||||
import {
|
||||
appendAtPath,
|
||||
getAtPath,
|
||||
getParentPath,
|
||||
getPathTarget,
|
||||
normalizeCardPath,
|
||||
setAtPath,
|
||||
} from "../editor/lovelace-path";
|
||||
import { createErrorSectionConfig } from "../sections/hui-error-section";
|
||||
import "../sections/hui-section";
|
||||
import type { HuiSection } from "../sections/hui-section";
|
||||
@@ -56,13 +64,13 @@ import { getViewType } from "./get-view-type";
|
||||
declare global {
|
||||
// for fire event
|
||||
interface HASSDomEvents {
|
||||
"ll-create-card": { suggested?: string[] } | undefined;
|
||||
"ll-edit-card": { path: LovelaceCardPath };
|
||||
"ll-create-card": { path: LovelacePath; suggested?: string[] };
|
||||
"ll-edit-card": { path: LovelacePath };
|
||||
"ll-delete-card": DeleteCardParams;
|
||||
"ll-duplicate-card": { path: LovelaceCardPath };
|
||||
"ll-copy-card": { path: LovelaceCardPath };
|
||||
"ll-create-badge": undefined;
|
||||
"ll-edit-badge": { path: LovelaceCardPath };
|
||||
"ll-duplicate-card": { path: LovelacePath };
|
||||
"ll-copy-card": { path: LovelacePath };
|
||||
"ll-create-badge": { path: LovelacePath };
|
||||
"ll-edit-badge": { path: LovelacePath };
|
||||
"ll-delete-badge": DeleteBadgeParams;
|
||||
}
|
||||
interface HTMLElementEventMap {
|
||||
@@ -108,6 +116,8 @@ export class HUIView extends ReactiveElement {
|
||||
@consume({ context: childPanelReadyContext })
|
||||
private _registerChildPanelReady?: RegisterChildPanelReady;
|
||||
|
||||
private _path = memoizeOne((index: number): LovelacePath => ["views", index]);
|
||||
|
||||
@storage({
|
||||
key: "dashboardCardClipboard",
|
||||
state: false,
|
||||
@@ -116,6 +126,132 @@ export class HUIView extends ReactiveElement {
|
||||
})
|
||||
protected _clipboard?: LovelaceCardConfig;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.addEventListener(
|
||||
"ll-create-card",
|
||||
(ev: HASSDomEvent<HASSDomEvents["ll-create-card"]>) => {
|
||||
// Temporary compatibility: custom view layouts still fire this event without a path
|
||||
const detail = ev.detail as HASSDomEvents["ll-create-card"] | undefined;
|
||||
const path = detail?.path ?? [...this._path(this.index), "cards"];
|
||||
showCreateCardDialog(this, {
|
||||
lovelaceConfig: this.lovelace.config,
|
||||
saveConfig: this.lovelace.saveConfig,
|
||||
path,
|
||||
suggestedCards: detail?.suggested,
|
||||
});
|
||||
}
|
||||
);
|
||||
this.addEventListener(
|
||||
"ll-edit-card",
|
||||
(ev: HASSDomEvent<HASSDomEvents["ll-edit-card"]>) => {
|
||||
const path = normalizeCardPath(ev.detail.path);
|
||||
const cardConfig = this._getCardConfig(path);
|
||||
if (!cardConfig) {
|
||||
return;
|
||||
}
|
||||
showEditCardDialog(this, {
|
||||
lovelaceConfig: this.lovelace.config,
|
||||
saveCardConfig: async (newCardConfig) => {
|
||||
const newConfig = setAtPath(
|
||||
this.lovelace.config,
|
||||
path,
|
||||
newCardConfig
|
||||
);
|
||||
await this.lovelace.saveConfig(newConfig);
|
||||
},
|
||||
sectionConfig: getCardSectionConfig(this.lovelace.config, path),
|
||||
cardConfig,
|
||||
});
|
||||
}
|
||||
);
|
||||
this.addEventListener(
|
||||
"ll-delete-card",
|
||||
(ev: HASSDomEvent<HASSDomEvents["ll-delete-card"]>) => {
|
||||
if (!this.lovelace) return;
|
||||
performDeleteCard(this.hass, this.lovelace, {
|
||||
...ev.detail,
|
||||
path: normalizeCardPath(ev.detail.path),
|
||||
});
|
||||
}
|
||||
);
|
||||
this.addEventListener(
|
||||
"ll-duplicate-card",
|
||||
(ev: HASSDomEvent<HASSDomEvents["ll-duplicate-card"]>) => {
|
||||
const path = normalizeCardPath(ev.detail.path);
|
||||
if (getPathTarget(path) !== "item") {
|
||||
return;
|
||||
}
|
||||
const cardConfig = this._getCardConfig(path);
|
||||
if (!cardConfig) {
|
||||
return;
|
||||
}
|
||||
showEditCardDialog(this, {
|
||||
lovelaceConfig: this.lovelace.config,
|
||||
saveCardConfig: async (newCardConfig) => {
|
||||
const cardsPath = getParentPath(path);
|
||||
const newConfig = appendAtPath(
|
||||
this.lovelace.config,
|
||||
cardsPath,
|
||||
newCardConfig
|
||||
);
|
||||
await this.lovelace.saveConfig(newConfig);
|
||||
},
|
||||
sectionConfig: getCardSectionConfig(this.lovelace.config, path),
|
||||
cardConfig,
|
||||
isNew: true,
|
||||
});
|
||||
}
|
||||
);
|
||||
this.addEventListener(
|
||||
"ll-copy-card",
|
||||
(ev: HASSDomEvent<HASSDomEvents["ll-copy-card"]>) => {
|
||||
if (!this.lovelace) return;
|
||||
const cardConfig = this._getCardConfig(
|
||||
normalizeCardPath(ev.detail.path)
|
||||
);
|
||||
if (!cardConfig) {
|
||||
return;
|
||||
}
|
||||
this._clipboard = deepClone(cardConfig);
|
||||
}
|
||||
);
|
||||
this.addEventListener(
|
||||
"ll-create-badge",
|
||||
(ev: HASSDomEvent<HASSDomEvents["ll-create-badge"]>) => {
|
||||
showCreateBadgeDialog(this, {
|
||||
lovelaceConfig: this.lovelace.config,
|
||||
saveConfig: this.lovelace.saveConfig,
|
||||
path: ev.detail.path,
|
||||
});
|
||||
}
|
||||
);
|
||||
this.addEventListener(
|
||||
"ll-edit-badge",
|
||||
(ev: HASSDomEvent<HASSDomEvents["ll-edit-badge"]>) => {
|
||||
showEditBadgeDialog(this, {
|
||||
lovelaceConfig: this.lovelace.config,
|
||||
saveConfig: this.lovelace.saveConfig,
|
||||
path: ev.detail.path,
|
||||
});
|
||||
}
|
||||
);
|
||||
this.addEventListener(
|
||||
"ll-delete-badge",
|
||||
(ev: HASSDomEvent<HASSDomEvents["ll-delete-badge"]>) => {
|
||||
if (!this.lovelace) return;
|
||||
performDeleteBadge(this.hass, this.lovelace, ev.detail);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private _getCardConfig(path: LovelacePath): LovelaceCardConfig | undefined {
|
||||
if (isStrategyView(this.lovelace.config.views[this.index])) {
|
||||
return undefined;
|
||||
}
|
||||
return getAtPath<LovelaceCardConfig>(this.lovelace.config, path);
|
||||
}
|
||||
|
||||
private _createCardElement(cardConfig: LovelaceCardConfig) {
|
||||
const element = document.createElement("hui-card");
|
||||
element.hass = this.hass;
|
||||
@@ -148,7 +284,6 @@ export class HUIView extends ReactiveElement {
|
||||
element.hass = this.hass;
|
||||
element.lovelace = this.lovelace;
|
||||
element.config = sectionConfig;
|
||||
element.viewIndex = this.index;
|
||||
element.preview = this.lovelace.editMode;
|
||||
element.addEventListener(
|
||||
"ll-rebuild",
|
||||
@@ -323,6 +458,7 @@ export class HUIView extends ReactiveElement {
|
||||
this._layoutElement!.narrow = this.narrow;
|
||||
this._layoutElement!.lovelace = this.lovelace;
|
||||
this._layoutElement!.index = this.index;
|
||||
this._layoutElement!.path = this._path(this.index);
|
||||
this._layoutElement!.cards = this._cards;
|
||||
this._layoutElement!.badges = this._badges;
|
||||
this._layoutElement!.sections = this._sections;
|
||||
@@ -364,89 +500,6 @@ export class HUIView extends ReactiveElement {
|
||||
},
|
||||
{ once: true }
|
||||
);
|
||||
this._layoutElement.addEventListener("ll-create-card", (ev) => {
|
||||
showCreateCardDialog(this, {
|
||||
lovelaceConfig: this.lovelace.config,
|
||||
saveConfig: this.lovelace.saveConfig,
|
||||
path: [this.index],
|
||||
suggestedCards: ev.detail?.suggested,
|
||||
});
|
||||
});
|
||||
this._layoutElement.addEventListener("ll-edit-card", (ev) => {
|
||||
const { cardIndex } = parseLovelaceCardPath(ev.detail.path);
|
||||
const viewConfig = this.lovelace!.config.views[this.index];
|
||||
if (isStrategyView(viewConfig)) {
|
||||
return;
|
||||
}
|
||||
const cardConfig = viewConfig.cards![cardIndex];
|
||||
showEditCardDialog(this, {
|
||||
lovelaceConfig: this.lovelace.config,
|
||||
saveCardConfig: async (newCardConfig) => {
|
||||
const newConfig = replaceCard(
|
||||
this.lovelace!.config,
|
||||
[this.index, cardIndex],
|
||||
newCardConfig
|
||||
);
|
||||
await this.lovelace.saveConfig(newConfig);
|
||||
},
|
||||
cardConfig,
|
||||
});
|
||||
});
|
||||
this._layoutElement.addEventListener("ll-delete-card", (ev) => {
|
||||
if (!this.lovelace) return;
|
||||
performDeleteCard(this.hass, this.lovelace, ev.detail);
|
||||
});
|
||||
this._layoutElement.addEventListener("ll-create-badge", async () => {
|
||||
showCreateBadgeDialog(this, {
|
||||
lovelaceConfig: this.lovelace.config,
|
||||
saveConfig: this.lovelace.saveConfig,
|
||||
path: [this.index],
|
||||
});
|
||||
});
|
||||
this._layoutElement.addEventListener("ll-edit-badge", (ev) => {
|
||||
const { cardIndex } = parseLovelaceCardPath(ev.detail.path);
|
||||
showEditBadgeDialog(this, {
|
||||
lovelaceConfig: this.lovelace.config,
|
||||
saveConfig: this.lovelace.saveConfig,
|
||||
path: [this.index],
|
||||
badgeIndex: cardIndex,
|
||||
});
|
||||
});
|
||||
this._layoutElement.addEventListener("ll-delete-badge", async (ev) => {
|
||||
if (!this.lovelace) return;
|
||||
performDeleteBadge(this.hass, this.lovelace, ev.detail);
|
||||
});
|
||||
this._layoutElement.addEventListener("ll-duplicate-card", (ev) => {
|
||||
const { cardIndex } = parseLovelaceCardPath(ev.detail.path);
|
||||
const viewConfig = this.lovelace!.config.views[this.index];
|
||||
if (isStrategyView(viewConfig)) {
|
||||
return;
|
||||
}
|
||||
const cardConfig = viewConfig.cards![cardIndex];
|
||||
showEditCardDialog(this, {
|
||||
lovelaceConfig: this.lovelace!.config,
|
||||
saveCardConfig: async (newCardConfig) => {
|
||||
const newConfig = addCard(
|
||||
this.lovelace!.config,
|
||||
[this.index],
|
||||
newCardConfig
|
||||
);
|
||||
await this.lovelace!.saveConfig(newConfig);
|
||||
},
|
||||
cardConfig,
|
||||
isNew: true,
|
||||
});
|
||||
});
|
||||
this._layoutElement.addEventListener("ll-copy-card", (ev) => {
|
||||
if (!this.lovelace) return;
|
||||
const { cardIndex } = parseLovelaceCardPath(ev.detail.path);
|
||||
const viewConfig = this.lovelace!.config.views[this.index];
|
||||
if (isStrategyView(viewConfig)) {
|
||||
return;
|
||||
}
|
||||
const cardConfig = viewConfig.cards![cardIndex];
|
||||
this._clipboard = deepClone(cardConfig);
|
||||
});
|
||||
}
|
||||
|
||||
private _createBadges(config: LovelaceViewConfig): void {
|
||||
@@ -482,7 +535,7 @@ export class HUIView extends ReactiveElement {
|
||||
|
||||
this._sections = config.sections.map((sectionConfig, index) => {
|
||||
const element = this.createSectionElement(sectionConfig);
|
||||
element.index = index;
|
||||
element.path = [...this._path(this.index), "sections", index];
|
||||
return element;
|
||||
});
|
||||
}
|
||||
@@ -492,7 +545,7 @@ export class HUIView extends ReactiveElement {
|
||||
config: LovelaceSectionConfig
|
||||
): void {
|
||||
const newSectionEl = this.createSectionElement(config);
|
||||
newSectionEl.index = sectionElToReplace.index;
|
||||
newSectionEl.path = sectionElToReplace.path;
|
||||
if (sectionElToReplace.parentElement) {
|
||||
sectionElToReplace.parentElement!.replaceChild(
|
||||
newSectionEl,
|
||||
|
||||
@@ -18,10 +18,7 @@ import {
|
||||
subscribeFrontendUserData,
|
||||
} from "../data/frontend";
|
||||
import { forwardHaptic } from "../data/haptics";
|
||||
import {
|
||||
getServiceCallEntityIds,
|
||||
serviceCallWillDisconnect,
|
||||
} from "../data/service";
|
||||
import { serviceCallWillDisconnect } from "../data/service";
|
||||
import {
|
||||
DateFormat,
|
||||
FirstWeekday,
|
||||
@@ -35,12 +32,7 @@ import { preserveUnchangedRecord } from "../common/util/preserve-unchanged-recor
|
||||
import { subscribeFloorRegistry } from "../data/ws-floor_registry";
|
||||
import { subscribePanels } from "../data/ws-panels";
|
||||
import { translationMetadata } from "../resources/translations-metadata";
|
||||
import type {
|
||||
Constructor,
|
||||
HomeAssistant,
|
||||
ServiceCallRequest,
|
||||
ServiceCallResponse,
|
||||
} from "../types";
|
||||
import type { Constructor, HomeAssistant, ServiceCallResponse } from "../types";
|
||||
import {
|
||||
addBrandsAuth,
|
||||
clearBrandsTokenRefresh,
|
||||
@@ -122,7 +114,7 @@ export const connectionMixin = <T extends Constructor<HassBaseEl>>(
|
||||
);
|
||||
}
|
||||
try {
|
||||
const response = (await callService(
|
||||
return (await callService(
|
||||
conn,
|
||||
domain,
|
||||
service,
|
||||
@@ -130,24 +122,11 @@ export const connectionMixin = <T extends Constructor<HassBaseEl>>(
|
||||
target,
|
||||
returnResponse
|
||||
)) as ServiceCallResponse;
|
||||
this._reportEntityControlToExternalApp(
|
||||
domain,
|
||||
service,
|
||||
serviceData,
|
||||
target
|
||||
);
|
||||
return response;
|
||||
} catch (err: any) {
|
||||
if (
|
||||
err.error?.code === ERR_CONNECTION_LOST &&
|
||||
serviceCallWillDisconnect(domain, service, serviceData)
|
||||
) {
|
||||
this._reportEntityControlToExternalApp(
|
||||
domain,
|
||||
service,
|
||||
serviceData,
|
||||
target
|
||||
);
|
||||
return { context: { id: "" } };
|
||||
}
|
||||
if (this.hass?.debugConnection) {
|
||||
@@ -426,28 +405,4 @@ export const connectionMixin = <T extends Constructor<HassBaseEl>>(
|
||||
this._updateHass({});
|
||||
}
|
||||
}
|
||||
|
||||
private _reportEntityControlToExternalApp(
|
||||
domain: string,
|
||||
service: string,
|
||||
serviceData?: ServiceCallRequest["serviceData"],
|
||||
target?: ServiceCallRequest["target"]
|
||||
) {
|
||||
const external = this.hass?.auth.external;
|
||||
if (!external) {
|
||||
return;
|
||||
}
|
||||
const entityIds = getServiceCallEntityIds(serviceData, target);
|
||||
if (!entityIds.length) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
external.fireMessage({
|
||||
type: "entity/controlled",
|
||||
payload: { entity_ids: entityIds, domain, service },
|
||||
});
|
||||
} catch (_err) {
|
||||
// Reporting is best effort and must not fail the service call.
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -5866,8 +5866,8 @@
|
||||
"missing_triggers": "This condition references a trigger that no longer exists. Uncheck it to clear it.",
|
||||
"duplicate_ids": "Some triggers share the same trigger ID. Avoid reusing a trigger ID to group triggers and select multiple triggers here instead.",
|
||||
"duplicate_ids_fix": "Fix",
|
||||
"assign_unique_ids_title": "Fix duplicate trigger IDs?",
|
||||
"assign_unique_ids_description": "Triggers that share a used ID get a unique ID, and conditions using it are updated to match. IDs that nothing uses are removed. Templates or actions that use trigger.id directly may need to be updated.",
|
||||
"assign_unique_ids_title": "Assign unique trigger IDs?",
|
||||
"assign_unique_ids_description": "Each trigger sharing an ID gets a unique one, and conditions using it are updated to match. Templates or actions that use trigger.id directly may need to be updated.",
|
||||
"description": {
|
||||
"picker": "Tests if the automation has been triggered by a specific trigger.",
|
||||
"full": "If triggered by {id}",
|
||||
|
||||
@@ -1,78 +1,11 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import {
|
||||
localizeStateMessage,
|
||||
localizeTriggerSource,
|
||||
parseTriggerSource,
|
||||
} from "../../src/data/logbook";
|
||||
import type { HomeAssistant } from "../../src/types";
|
||||
|
||||
const fakeLocalize = ((key: string) => `<${key}>`) as any;
|
||||
|
||||
const fakeHass = {
|
||||
localize: fakeLocalize,
|
||||
formatEntityState: (_stateObj, state: string) => `<state:${state}>`,
|
||||
} as unknown as HomeAssistant;
|
||||
|
||||
const fakeStateObj = {
|
||||
entity_id: "button.restart",
|
||||
state: "unknown",
|
||||
attributes: {},
|
||||
} as HassEntity;
|
||||
|
||||
describe("localizeStateMessage", () => {
|
||||
it.each(["unknown", "unavailable"])(
|
||||
"preserves the %s lifecycle state for timestamp entities",
|
||||
(state) => {
|
||||
expect(
|
||||
localizeStateMessage(fakeHass, state, fakeStateObj, "button")
|
||||
).toBe(`<state:${state}>`);
|
||||
}
|
||||
);
|
||||
|
||||
it("uses the action label for a timestamp state", () => {
|
||||
expect(
|
||||
localizeStateMessage(
|
||||
fakeHass,
|
||||
"2026-09-15T11:05:05+00:00",
|
||||
fakeStateObj,
|
||||
"button",
|
||||
{
|
||||
name: "Restart",
|
||||
when: Date.parse("2026-09-15T11:05:05+00:00") / 1000,
|
||||
}
|
||||
)
|
||||
).toBe("<ui.components.logbook.messages.pressed>");
|
||||
});
|
||||
|
||||
it("does not treat a restored timestamp with inherited context as a new action", () => {
|
||||
const restoredState = "2026-09-15T10:55:25+00:00";
|
||||
expect(
|
||||
localizeStateMessage(fakeHass, restoredState, fakeStateObj, "button", {
|
||||
name: "Restart",
|
||||
when: Date.parse("2026-09-15T11:05:05+00:00") / 1000,
|
||||
context_domain: "homeassistant",
|
||||
context_service: "reload_config_entry",
|
||||
})
|
||||
).toBe(`<state:${restoredState}>`);
|
||||
});
|
||||
|
||||
it("keeps the action label for a producer timestamp", () => {
|
||||
expect(
|
||||
localizeStateMessage(
|
||||
fakeHass,
|
||||
"2026-09-15T10:55:25+00:00",
|
||||
fakeStateObj,
|
||||
"image",
|
||||
{
|
||||
name: "Satellite image",
|
||||
when: Date.parse("2026-09-15T11:05:05+00:00") / 1000,
|
||||
}
|
||||
)
|
||||
).toBe("<ui.components.logbook.messages.updated>");
|
||||
});
|
||||
});
|
||||
|
||||
describe("localizeTriggerSource", () => {
|
||||
it("replaces a known phrase with the bare translation", () => {
|
||||
expect(localizeTriggerSource(fakeLocalize, "Home Assistant starting")).toBe(
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { getServiceCallEntityIds } from "../../src/data/service";
|
||||
|
||||
describe("getServiceCallEntityIds", () => {
|
||||
it("returns an empty list when no entities are targeted", () => {
|
||||
expect(getServiceCallEntityIds()).toEqual([]);
|
||||
expect(getServiceCallEntityIds({ brightness: 50 }, {})).toEqual([]);
|
||||
expect(getServiceCallEntityIds({}, { area_id: "kitchen" })).toEqual([]);
|
||||
});
|
||||
|
||||
it("reads a single entity from target or service data", () => {
|
||||
expect(getServiceCallEntityIds({}, { entity_id: "light.a" })).toEqual([
|
||||
"light.a",
|
||||
]);
|
||||
expect(getServiceCallEntityIds({ entity_id: "light.a" })).toEqual([
|
||||
"light.a",
|
||||
]);
|
||||
});
|
||||
|
||||
it("prefers the target over the legacy service data entity ids", () => {
|
||||
expect(
|
||||
getServiceCallEntityIds(
|
||||
{ entity_id: ["light.a", "light.b"] },
|
||||
{ entity_id: ["light.b", "light.c"] }
|
||||
)
|
||||
).toEqual(["light.b", "light.c"]);
|
||||
});
|
||||
|
||||
it("deduplicates entity ids", () => {
|
||||
expect(
|
||||
getServiceCallEntityIds({}, { entity_id: ["light.a", "light.a"] })
|
||||
).toEqual(["light.a"]);
|
||||
});
|
||||
|
||||
it("splits comma separated ids and lowercases them like Core does", () => {
|
||||
expect(
|
||||
getServiceCallEntityIds({}, { entity_id: "Light.A, light.b ,light.a" })
|
||||
).toEqual(["light.a", "light.b"]);
|
||||
});
|
||||
|
||||
it("ignores wildcard, malformed, and non-string entity ids", () => {
|
||||
expect(getServiceCallEntityIds({ entity_id: "all" })).toEqual([]);
|
||||
expect(
|
||||
getServiceCallEntityIds(
|
||||
{},
|
||||
{ entity_id: ["all", "none", "", "light", 5] as unknown as string[] }
|
||||
)
|
||||
).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -290,25 +290,6 @@ describe("automation trigger IDs", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("strips unreferenced duplicate IDs instead of generating new ones", () => {
|
||||
const config: AutomationConfig = {
|
||||
triggers: [
|
||||
{ trigger: "state", entity_id: "light.kitchen", id: "motion" },
|
||||
{ trigger: "time", at: "12:00:00", id: "motion" },
|
||||
],
|
||||
conditions: [{ condition: "trigger", id: "" }],
|
||||
actions: [],
|
||||
};
|
||||
|
||||
const updated = makeDuplicateTriggerIdsUnique(config);
|
||||
|
||||
expect(updated.triggers).toEqual([
|
||||
{ trigger: "state", entity_id: "light.kitchen" },
|
||||
{ trigger: "time", at: "12:00:00" },
|
||||
]);
|
||||
expect(updated.conditions).toBe(config.conditions);
|
||||
});
|
||||
|
||||
it("removes generated trigger IDs that no condition or action references", () => {
|
||||
const generatedA = `${GENERATED_TRIGGER_ID_PREFIX}aB3x`;
|
||||
const generatedB = `${GENERATED_TRIGGER_ID_PREFIX}yZ7w`;
|
||||
|
||||
@@ -1,119 +0,0 @@
|
||||
/**
|
||||
* Protects the energy-distribution home ring from overflowing when hourly
|
||||
* allocation yields more used_solar/used_battery/used_grid than net used_total
|
||||
* (https://github.com/home-assistant/frontend/issues/54185).
|
||||
*/
|
||||
import { assert, describe, it } from "vitest";
|
||||
|
||||
import {
|
||||
ENERGY_DISTRIBUTION_HOME_CIRCLE_CIRCUMFERENCE as CIRCLE,
|
||||
computeEnergyDistributionHomeCircleArcs,
|
||||
} from "../../../../../src/panels/lovelace/cards/energy/energy-distribution-home-circle";
|
||||
|
||||
const sumDefinedArcs = (
|
||||
arcs: ReturnType<typeof computeEnergyDistributionHomeCircleArcs>
|
||||
): number =>
|
||||
(arcs.solar ?? 0) +
|
||||
(arcs.battery ?? 0) +
|
||||
(arcs.lowCarbon ?? 0) +
|
||||
(arcs.grid ?? 0);
|
||||
|
||||
describe("computeEnergyDistributionHomeCircleArcs", () => {
|
||||
it("sizes arcs from allocated home flows so they fill the circle", () => {
|
||||
const arcs = computeEnergyDistributionHomeCircleArcs({
|
||||
usedSolar: 8.5,
|
||||
usedBattery: 4.6,
|
||||
usedGrid: 0.12,
|
||||
hasSolar: true,
|
||||
hasGrid: true,
|
||||
});
|
||||
|
||||
assert.approximately(arcs.solar!, CIRCLE * (8.5 / 13.22), 1e-6);
|
||||
assert.approximately(arcs.battery!, CIRCLE * (4.6 / 13.22), 1e-6);
|
||||
assert.approximately(arcs.grid!, CIRCLE * (0.12 / 13.22), 1e-6);
|
||||
assert.approximately(sumDefinedArcs(arcs), CIRCLE, 1e-6);
|
||||
assert.isAtLeast(arcs.grid!, 0);
|
||||
});
|
||||
|
||||
it("does not paint a full grid ring when used_solar exceeds net home energy", () => {
|
||||
// Screenshot totals from #54185 with solar and export in different hours:
|
||||
// used_solar 77.1, used_battery 0, used_grid 0, net used_total 13.22.
|
||||
const arcs = computeEnergyDistributionHomeCircleArcs({
|
||||
usedSolar: 77.1,
|
||||
usedBattery: 0,
|
||||
usedGrid: 0,
|
||||
hasSolar: true,
|
||||
hasGrid: true,
|
||||
});
|
||||
|
||||
assert.approximately(arcs.solar!, CIRCLE, 1e-6);
|
||||
assert.isUndefined(arcs.battery);
|
||||
assert.equal(arcs.grid, 0);
|
||||
assert.approximately(sumDefinedArcs(arcs), CIRCLE, 1e-6);
|
||||
});
|
||||
|
||||
it("keeps a zero-consumption ring from producing NaN or negative dashes", () => {
|
||||
const arcs = computeEnergyDistributionHomeCircleArcs({
|
||||
usedSolar: 0,
|
||||
usedBattery: 0,
|
||||
usedGrid: 0,
|
||||
hasSolar: true,
|
||||
hasGrid: true,
|
||||
});
|
||||
|
||||
assert.deepEqual(arcs, {});
|
||||
});
|
||||
|
||||
it("splits grid into low-carbon and high-carbon without exceeding the grid share", () => {
|
||||
const arcs = computeEnergyDistributionHomeCircleArcs({
|
||||
usedSolar: 4,
|
||||
usedBattery: 0,
|
||||
usedGrid: 6,
|
||||
hasSolar: true,
|
||||
hasGrid: true,
|
||||
highCarbonConsumption: 2,
|
||||
});
|
||||
|
||||
const ringTotal = 10;
|
||||
assert.approximately(arcs.solar!, CIRCLE * (4 / ringTotal), 1e-6);
|
||||
assert.approximately(arcs.lowCarbon!, CIRCLE * (4 / ringTotal), 1e-6);
|
||||
assert.approximately(arcs.grid!, CIRCLE * (2 / ringTotal), 1e-6);
|
||||
assert.approximately(sumDefinedArcs(arcs), CIRCLE, 1e-6);
|
||||
assert.isAtLeast(arcs.lowCarbon!, 0);
|
||||
assert.isAtLeast(arcs.grid!, 0);
|
||||
});
|
||||
|
||||
it("clamps high-carbon consumption to the grid share", () => {
|
||||
const arcs = computeEnergyDistributionHomeCircleArcs({
|
||||
usedSolar: 5,
|
||||
usedBattery: 0,
|
||||
usedGrid: 5,
|
||||
hasSolar: true,
|
||||
hasGrid: true,
|
||||
highCarbonConsumption: 50,
|
||||
});
|
||||
|
||||
assert.equal(arcs.lowCarbon, 0);
|
||||
assert.approximately(arcs.grid!, CIRCLE * 0.5, 1e-6);
|
||||
assert.approximately(sumDefinedArcs(arcs), CIRCLE, 1e-6);
|
||||
});
|
||||
|
||||
it("keeps a defined zero low-carbon arc so a grid-only high-carbon ring still renders", () => {
|
||||
// The card only mounts the home SVG when solar or lowCarbon is defined.
|
||||
// Omitting 0 would drop battery/grid arcs in a no-solar 100% fossil grid.
|
||||
const arcs = computeEnergyDistributionHomeCircleArcs({
|
||||
usedSolar: 0,
|
||||
usedBattery: 4,
|
||||
usedGrid: 6,
|
||||
hasSolar: false,
|
||||
hasGrid: true,
|
||||
highCarbonConsumption: 6,
|
||||
});
|
||||
|
||||
assert.isUndefined(arcs.solar);
|
||||
assert.equal(arcs.lowCarbon, 0);
|
||||
assert.approximately(arcs.battery!, CIRCLE * 0.4, 1e-6);
|
||||
assert.approximately(arcs.grid!, CIRCLE * 0.6, 1e-6);
|
||||
assert.approximately(sumDefinedArcs(arcs), CIRCLE, 1e-6);
|
||||
});
|
||||
});
|
||||
@@ -20,7 +20,11 @@ describe("moveCardToContainer", () => {
|
||||
],
|
||||
};
|
||||
|
||||
const result = moveCardToContainer(config, [1, 0], [0]);
|
||||
const result = moveCardToContainer(
|
||||
config,
|
||||
["views", 1, "cards", 0],
|
||||
["views", 0]
|
||||
);
|
||||
const expected: LovelaceConfig = {
|
||||
views: [
|
||||
{
|
||||
@@ -46,7 +50,11 @@ describe("moveCardToContainer", () => {
|
||||
],
|
||||
};
|
||||
|
||||
const result = moveCardToContainer(config, [1, 0], [0]);
|
||||
const result = moveCardToContainer(
|
||||
config,
|
||||
["views", 1, "cards", 0],
|
||||
["views", 0]
|
||||
);
|
||||
const expected: LovelaceConfig = {
|
||||
views: [
|
||||
{
|
||||
@@ -73,7 +81,7 @@ describe("moveCardToContainer", () => {
|
||||
};
|
||||
|
||||
const result = () => {
|
||||
moveCardToContainer(config, [1, 0], [1]);
|
||||
moveCardToContainer(config, ["views", 1, "cards", 0], ["views", 1]);
|
||||
};
|
||||
assert.throws(
|
||||
result,
|
||||
@@ -158,7 +166,7 @@ describe("duplicateSection", () => {
|
||||
],
|
||||
};
|
||||
|
||||
const result = duplicateSection(config, 0, 0);
|
||||
const result = duplicateSection(config, ["views", 0, "sections", 0]);
|
||||
|
||||
const expected: LovelaceConfig = {
|
||||
views: [
|
||||
@@ -189,7 +197,7 @@ describe("duplicateSection", () => {
|
||||
],
|
||||
};
|
||||
|
||||
const result = duplicateSection(config, 0, 0);
|
||||
const result = duplicateSection(config, ["views", 0, "sections", 0]);
|
||||
const view = result.views[0] as LovelaceViewConfig;
|
||||
|
||||
assert.equal(view.sections!.length, 2);
|
||||
@@ -211,7 +219,7 @@ describe("duplicateSection", () => {
|
||||
],
|
||||
};
|
||||
|
||||
const result = duplicateSection(config, 0, 0);
|
||||
const result = duplicateSection(config, ["views", 0, "sections", 0]);
|
||||
const resultSections = (result.views[0] as LovelaceViewConfig).sections!;
|
||||
|
||||
assert.equal(resultSections.length, 2);
|
||||
|
||||
@@ -0,0 +1,537 @@
|
||||
import { assert, describe, it } from "vitest";
|
||||
|
||||
import type { LovelaceCardConfig } from "../../../../src/data/lovelace/config/card";
|
||||
import type { LovelaceConfig } from "../../../../src/data/lovelace/config/types";
|
||||
import type { LovelacePath } from "../../../../src/panels/lovelace/editor/lovelace-path";
|
||||
import {
|
||||
appendAtPath,
|
||||
deleteAtPath,
|
||||
getAtPath,
|
||||
getItemKind,
|
||||
getParentPath,
|
||||
getViewPath,
|
||||
insertAtPath,
|
||||
isAncestorPath,
|
||||
getPathTarget,
|
||||
moveAtPath,
|
||||
normalizeCardPath,
|
||||
parsePath,
|
||||
pathEquals,
|
||||
setAtPath,
|
||||
stringifyPath,
|
||||
} from "../../../../src/panels/lovelace/editor/lovelace-path";
|
||||
|
||||
const createConfig = (): LovelaceConfig => ({
|
||||
views: [
|
||||
{
|
||||
title: "Home",
|
||||
badges: ["sensor.badge0", { type: "entity", entity: "sensor.badge1" }],
|
||||
cards: [{ type: "v0-c0" }, { type: "v0-c1" }],
|
||||
},
|
||||
{
|
||||
title: "Areas",
|
||||
sections: [
|
||||
{
|
||||
type: "grid",
|
||||
cards: [{ type: "s0-c0" }, { type: "s0-c1" }, { type: "s0-c2" }],
|
||||
},
|
||||
{ type: "grid", cards: [{ type: "s1-c0" }] },
|
||||
{ strategy: { type: "areas" } },
|
||||
],
|
||||
},
|
||||
{ strategy: { type: "original-states" } },
|
||||
],
|
||||
});
|
||||
|
||||
describe("stringifyPath / parsePath", () => {
|
||||
it("round trips a nested card path", () => {
|
||||
const path: LovelacePath = ["views", 0, "sections", 1, "cards", 2];
|
||||
assert.strictEqual(stringifyPath(path), "views/0/sections/1/cards/2");
|
||||
assert.deepEqual(parsePath("views/0/sections/1/cards/2"), path);
|
||||
});
|
||||
|
||||
it("round trips a slot path", () => {
|
||||
const path: LovelacePath = ["views", 0, "header", "card"];
|
||||
assert.strictEqual(stringifyPath(path), "views/0/header/card");
|
||||
assert.deepEqual(parsePath("views/0/header/card"), path);
|
||||
});
|
||||
|
||||
it("parses numeric segments as numbers", () => {
|
||||
const parsed = parsePath("views/12");
|
||||
assert.deepEqual(parsed, ["views", 12]);
|
||||
assert.strictEqual(typeof parsed[1], "number");
|
||||
});
|
||||
|
||||
it("handles the root path", () => {
|
||||
assert.strictEqual(stringifyPath([]), "");
|
||||
assert.deepEqual(parsePath(""), []);
|
||||
});
|
||||
});
|
||||
|
||||
describe("path helpers", () => {
|
||||
it("normalizes legacy card index tuples", () => {
|
||||
assert.deepEqual(normalizeCardPath([0, 2]), ["views", 0, "cards", 2]);
|
||||
assert.deepEqual(normalizeCardPath([0, 1, 2]), [
|
||||
"views",
|
||||
0,
|
||||
"sections",
|
||||
1,
|
||||
"cards",
|
||||
2,
|
||||
]);
|
||||
assert.deepEqual(normalizeCardPath(["views", 0, "cards", 2]), [
|
||||
"views",
|
||||
0,
|
||||
"cards",
|
||||
2,
|
||||
]);
|
||||
});
|
||||
|
||||
it("resolves the item kind from the last string segment", () => {
|
||||
assert.strictEqual(getItemKind(["views", 0, "header", "card"]), "card");
|
||||
assert.strictEqual(getItemKind(["views", 0, "cards", 1]), "card");
|
||||
assert.strictEqual(getItemKind(["views", 0, "badges", 1]), "badge");
|
||||
assert.strictEqual(getItemKind(["views", 0]), "view");
|
||||
assert.strictEqual(
|
||||
getItemKind(["views", 0, "sidebar", "sections", 0]),
|
||||
"section"
|
||||
);
|
||||
assert.strictEqual(getItemKind(["views", 0, "header"]), undefined);
|
||||
assert.strictEqual(getItemKind([]), undefined);
|
||||
});
|
||||
|
||||
it("detects slot paths", () => {
|
||||
assert.strictEqual(getPathTarget(["views", 0, "header", "card"]), "slot");
|
||||
assert.strictEqual(getPathTarget(["views", 0, "cards"]), "list");
|
||||
assert.strictEqual(getPathTarget(["views", 0, "cards", 1]), "item");
|
||||
assert.strictEqual(getPathTarget(["views", 0, "header"]), "node");
|
||||
assert.strictEqual(getPathTarget(["views", 0]), "item");
|
||||
});
|
||||
|
||||
it("returns the parent path", () => {
|
||||
assert.deepEqual(getParentPath(["views", 0, "cards", 1]), [
|
||||
"views",
|
||||
0,
|
||||
"cards",
|
||||
]);
|
||||
assert.deepEqual(getParentPath([]), []);
|
||||
});
|
||||
|
||||
it("returns the view path", () => {
|
||||
assert.deepEqual(getViewPath(["views", 1, "sections", 0, "cards", 2]), [
|
||||
"views",
|
||||
1,
|
||||
]);
|
||||
assert.deepEqual(getViewPath(["views", 1]), ["views", 1]);
|
||||
});
|
||||
|
||||
it("compares paths", () => {
|
||||
assert.strictEqual(pathEquals(["views", 0], ["views", 0]), true);
|
||||
assert.strictEqual(pathEquals(["views", 0], ["views", 1]), false);
|
||||
assert.strictEqual(pathEquals(["views", 0], ["views", 0, "cards"]), false);
|
||||
});
|
||||
|
||||
it("does not consider a path its own ancestor", () => {
|
||||
assert.strictEqual(
|
||||
isAncestorPath(["views", 0], ["views", 0, "cards", 1]),
|
||||
true
|
||||
);
|
||||
assert.strictEqual(isAncestorPath(["views", 0], ["views", 0]), false);
|
||||
assert.strictEqual(
|
||||
isAncestorPath(["views", 0, "cards", 1], ["views", 0]),
|
||||
false
|
||||
);
|
||||
assert.strictEqual(
|
||||
isAncestorPath(["views", 1], ["views", 0, "cards", 1]),
|
||||
false
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getAtPath", () => {
|
||||
it("reads a nested card", () => {
|
||||
const config = createConfig();
|
||||
assert.deepEqual(
|
||||
getAtPath(config, ["views", 1, "sections", 0, "cards", 1]),
|
||||
{
|
||||
type: "s0-c1",
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it("returns undefined for a missing leaf", () => {
|
||||
const config = createConfig();
|
||||
assert.strictEqual(getAtPath(config, ["views", 0, "cards", 5]), undefined);
|
||||
});
|
||||
|
||||
it("returns undefined for a missing intermediate", () => {
|
||||
const config = createConfig();
|
||||
assert.strictEqual(
|
||||
getAtPath(config, ["views", 0, "sidebar", "sections", 0]),
|
||||
undefined
|
||||
);
|
||||
});
|
||||
|
||||
it("throws when descending through a strategy view", () => {
|
||||
const config = createConfig();
|
||||
assert.throws(
|
||||
() => getAtPath(config, ["views", 2, "cards"]),
|
||||
"Cannot edit inside a strategy: views/2"
|
||||
);
|
||||
});
|
||||
|
||||
it("throws when descending through a strategy section", () => {
|
||||
const config = createConfig();
|
||||
assert.throws(
|
||||
() => getAtPath(config, ["views", 1, "sections", 2, "cards"]),
|
||||
"Cannot edit inside a strategy: views/1/sections/2"
|
||||
);
|
||||
});
|
||||
|
||||
it("returns a strategy view when it is the final target", () => {
|
||||
const config = createConfig();
|
||||
assert.deepEqual(getAtPath(config, ["views", 2]), {
|
||||
strategy: { type: "original-states" },
|
||||
});
|
||||
});
|
||||
|
||||
it("throws for any non-empty path on a strategy dashboard", () => {
|
||||
const config = {
|
||||
strategy: { type: "original-states" },
|
||||
} as unknown as LovelaceConfig;
|
||||
assert.throws(
|
||||
() => getAtPath(config, ["views"]),
|
||||
"Cannot edit inside a strategy: "
|
||||
);
|
||||
assert.deepEqual(getAtPath(config, []), config);
|
||||
});
|
||||
});
|
||||
|
||||
describe("setAtPath", () => {
|
||||
it("replaces a list item", () => {
|
||||
const config = createConfig();
|
||||
const result = setAtPath(config, ["views", 0, "cards", 1], {
|
||||
type: "new-card",
|
||||
});
|
||||
assert.deepEqual(getAtPath(result, ["views", 0, "cards"]), [
|
||||
{ type: "v0-c0" },
|
||||
{ type: "new-card" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("replaces a slot", () => {
|
||||
const config: LovelaceConfig = {
|
||||
views: [{ header: { card: { type: "old" } } }],
|
||||
};
|
||||
const result = setAtPath(config, ["views", 0, "header", "card"], {
|
||||
type: "new",
|
||||
});
|
||||
assert.deepEqual(getAtPath(result, ["views", 0, "header"]), {
|
||||
card: { type: "new" },
|
||||
});
|
||||
});
|
||||
|
||||
it("refuses to create a missing list item", () => {
|
||||
const config = createConfig();
|
||||
assert.throws(
|
||||
() => setAtPath(config, ["views", 9, "header", "card"], { type: "x" }),
|
||||
"Cannot edit missing item: views/9"
|
||||
);
|
||||
assert.throws(
|
||||
() => setAtPath(config, ["views", 0, "cards", 5], { type: "x" }),
|
||||
"Cannot edit missing item: views/0/cards/5"
|
||||
);
|
||||
});
|
||||
|
||||
it("creates a missing header object", () => {
|
||||
const config = createConfig();
|
||||
const result = setAtPath(config, ["views", 0, "header", "card"], {
|
||||
type: "heading",
|
||||
});
|
||||
assert.deepEqual(getAtPath(result, ["views", 0, "header"]), {
|
||||
card: { type: "heading" },
|
||||
});
|
||||
});
|
||||
|
||||
it("does not mutate the input config", () => {
|
||||
const config = createConfig();
|
||||
setAtPath(config, ["views", 1, "sections", 0, "cards", 0], {
|
||||
type: "new-card",
|
||||
});
|
||||
assert.deepEqual(config, createConfig());
|
||||
});
|
||||
|
||||
it("keeps untouched branches identical", () => {
|
||||
const config = createConfig();
|
||||
const result = setAtPath(config, ["views", 1, "sections", 0, "cards", 0], {
|
||||
type: "new-card",
|
||||
});
|
||||
// Renderers key repeat() on config object identity, so untouched nodes must keep their reference.
|
||||
assert.strictEqual(
|
||||
getAtPath(result, ["views", 1, "sections", 0, "cards", 1]),
|
||||
getAtPath(config, ["views", 1, "sections", 0, "cards", 1])
|
||||
);
|
||||
assert.strictEqual(
|
||||
getAtPath(result, ["views", 1, "sections", 1]),
|
||||
getAtPath(config, ["views", 1, "sections", 1])
|
||||
);
|
||||
assert.strictEqual(
|
||||
getAtPath(result, ["views", 0]),
|
||||
getAtPath(config, ["views", 0])
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("insertAtPath", () => {
|
||||
it("splices in the middle", () => {
|
||||
const config = createConfig();
|
||||
const result = insertAtPath(config, ["views", 0, "cards", 1], {
|
||||
type: "inserted",
|
||||
});
|
||||
assert.deepEqual(getAtPath(result, ["views", 0, "cards"]), [
|
||||
{ type: "v0-c0" },
|
||||
{ type: "inserted" },
|
||||
{ type: "v0-c1" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("clamps an index beyond the length to the end", () => {
|
||||
const config = createConfig();
|
||||
const result = insertAtPath(config, ["views", 0, "cards", 99], {
|
||||
type: "inserted",
|
||||
});
|
||||
assert.deepEqual(getAtPath(result, ["views", 0, "cards"]), [
|
||||
{ type: "v0-c0" },
|
||||
{ type: "v0-c1" },
|
||||
{ type: "inserted" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("clamps a negative index to the start", () => {
|
||||
const config = createConfig();
|
||||
const result = insertAtPath(config, ["views", 0, "cards", -1], {
|
||||
type: "inserted",
|
||||
});
|
||||
assert.deepEqual(getAtPath(result, ["views", 0, "cards"]), [
|
||||
{ type: "inserted" },
|
||||
{ type: "v0-c0" },
|
||||
{ type: "v0-c1" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("creates a missing cards list", () => {
|
||||
const config: LovelaceConfig = { views: [{ title: "Empty" }] };
|
||||
const result = insertAtPath(config, ["views", 0, "cards", 0], {
|
||||
type: "first",
|
||||
});
|
||||
assert.deepEqual(result, {
|
||||
views: [{ title: "Empty", cards: [{ type: "first" }] }],
|
||||
});
|
||||
});
|
||||
|
||||
it("behaves like set when the last segment is a string", () => {
|
||||
const config = createConfig();
|
||||
const card: LovelaceCardConfig = { type: "heading" };
|
||||
assert.deepEqual(
|
||||
insertAtPath(config, ["views", 0, "header", "card"], card),
|
||||
setAtPath(config, ["views", 0, "header", "card"], card)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("appendAtPath", () => {
|
||||
it("appends to an existing list", () => {
|
||||
const config = createConfig();
|
||||
const result = appendAtPath(config, ["views", 0, "cards"], {
|
||||
type: "appended",
|
||||
});
|
||||
assert.deepEqual(getAtPath(result, ["views", 0, "cards"]), [
|
||||
{ type: "v0-c0" },
|
||||
{ type: "v0-c1" },
|
||||
{ type: "appended" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("creates a missing collection", () => {
|
||||
const config: LovelaceConfig = { views: [{ title: "Empty" }] };
|
||||
const result = appendAtPath(config, ["views", 0, "sidebar", "sections"], {
|
||||
type: "grid",
|
||||
});
|
||||
assert.deepEqual(result, {
|
||||
views: [{ title: "Empty", sidebar: { sections: [{ type: "grid" }] } }],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("deleteAtPath", () => {
|
||||
it("removes a list item", () => {
|
||||
const config = createConfig();
|
||||
const result = deleteAtPath(config, ["views", 0, "cards", 0]);
|
||||
assert.deepEqual(getAtPath(result, ["views", 0, "cards"]), [
|
||||
{ type: "v0-c1" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps an emptied list", () => {
|
||||
const config = createConfig();
|
||||
const result = deleteAtPath(config, [
|
||||
"views",
|
||||
1,
|
||||
"sections",
|
||||
1,
|
||||
"cards",
|
||||
0,
|
||||
]);
|
||||
assert.deepEqual(getAtPath(result, ["views", 1, "sections", 1]), {
|
||||
type: "grid",
|
||||
cards: [],
|
||||
});
|
||||
});
|
||||
|
||||
it("removes a slot key", () => {
|
||||
const config: LovelaceConfig = {
|
||||
views: [{ header: { layout: "start", card: { type: "old" } } }],
|
||||
};
|
||||
const result = deleteAtPath(config, ["views", 0, "header", "card"]);
|
||||
assert.deepEqual(getAtPath(result, ["views", 0, "header"]), {
|
||||
layout: "start",
|
||||
});
|
||||
});
|
||||
|
||||
it("returns the same config when the target is missing", () => {
|
||||
const config = createConfig();
|
||||
assert.strictEqual(deleteAtPath(config, ["views", 0, "cards", 9]), config);
|
||||
assert.strictEqual(deleteAtPath(config, []), config);
|
||||
});
|
||||
});
|
||||
|
||||
describe("moveAtPath", () => {
|
||||
it("moves forward inside the same list", () => {
|
||||
const config: LovelaceConfig = {
|
||||
views: [{ cards: [{ type: "a" }, { type: "b" }, { type: "c" }] }],
|
||||
};
|
||||
const result = moveAtPath(
|
||||
config,
|
||||
["views", 0, "cards", 0],
|
||||
["views", 0, "cards", 2]
|
||||
);
|
||||
assert.deepEqual(getAtPath(result, ["views", 0, "cards"]), [
|
||||
{ type: "b" },
|
||||
{ type: "c" },
|
||||
{ type: "a" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("moves backward inside the same list", () => {
|
||||
const config: LovelaceConfig = {
|
||||
views: [{ cards: [{ type: "a" }, { type: "b" }, { type: "c" }] }],
|
||||
};
|
||||
const result = moveAtPath(
|
||||
config,
|
||||
["views", 0, "cards", 2],
|
||||
["views", 0, "cards", 0]
|
||||
);
|
||||
assert.deepEqual(getAtPath(result, ["views", 0, "cards"]), [
|
||||
{ type: "c" },
|
||||
{ type: "a" },
|
||||
{ type: "b" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("moves a card between two sections", () => {
|
||||
const config = createConfig();
|
||||
const result = moveAtPath(
|
||||
config,
|
||||
["views", 1, "sections", 0, "cards", 0],
|
||||
["views", 1, "sections", 1, "cards", 0]
|
||||
);
|
||||
assert.deepEqual(getAtPath(result, ["views", 1, "sections", 0, "cards"]), [
|
||||
{ type: "s0-c1" },
|
||||
{ type: "s0-c2" },
|
||||
]);
|
||||
assert.deepEqual(getAtPath(result, ["views", 1, "sections", 1, "cards"]), [
|
||||
{ type: "s0-c0" },
|
||||
{ type: "s1-c0" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("moves a view card into the header slot", () => {
|
||||
const config = createConfig();
|
||||
const result = moveAtPath(
|
||||
config,
|
||||
["views", 0, "cards", 0],
|
||||
["views", 0, "header", "card"]
|
||||
);
|
||||
assert.deepEqual(getAtPath(result, ["views", 0, "cards"]), [
|
||||
{ type: "v0-c1" },
|
||||
]);
|
||||
assert.deepEqual(getAtPath(result, ["views", 0, "header"]), {
|
||||
card: { type: "v0-c0" },
|
||||
});
|
||||
});
|
||||
|
||||
it("targets the shifted sibling when moving into a later sibling", () => {
|
||||
const config: LovelaceConfig = {
|
||||
views: [
|
||||
{
|
||||
sections: [
|
||||
{ type: "grid", cards: [{ type: "s0-c0" }] },
|
||||
{ type: "grid", cards: [{ type: "s1-c0" }] },
|
||||
{ type: "grid", cards: [{ type: "s2-c0" }] },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
const result = moveAtPath(
|
||||
config,
|
||||
["views", 0, "sections", 0],
|
||||
["views", 0, "sections", 2, "cards", 0]
|
||||
);
|
||||
assert.deepEqual(result, {
|
||||
views: [
|
||||
{
|
||||
sections: [
|
||||
{ type: "grid", cards: [{ type: "s1-c0" }] },
|
||||
{
|
||||
type: "grid",
|
||||
cards: [
|
||||
{ type: "grid", cards: [{ type: "s0-c0" }] },
|
||||
{ type: "s2-c0" },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("returns the same config when source and target are equal", () => {
|
||||
const config = createConfig();
|
||||
assert.strictEqual(
|
||||
moveAtPath(config, ["views", 0, "cards", 0], ["views", 0, "cards", 0]),
|
||||
config
|
||||
);
|
||||
});
|
||||
|
||||
it("throws when moving into its own descendant", () => {
|
||||
const config = createConfig();
|
||||
assert.throws(
|
||||
() =>
|
||||
moveAtPath(
|
||||
config,
|
||||
["views", 1, "sections", 0],
|
||||
["views", 1, "sections", 0, "cards", 0]
|
||||
),
|
||||
"Cannot move views/1/sections/0 into itself: views/1/sections/0/cards/0"
|
||||
);
|
||||
});
|
||||
|
||||
it("throws when the source does not exist", () => {
|
||||
const config = createConfig();
|
||||
assert.throws(
|
||||
() =>
|
||||
moveAtPath(config, ["views", 0, "cards", 9], ["views", 0, "cards", 0]),
|
||||
"Nothing to move at views/0/cards/9"
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user