Handle automation and dashboard drag and drop at the element level (#22300)

* Handle drag and drop at action, condition, trigger level

* Clean item path

* Clean item path

* Fix selectors

* Clean selector config

* Remove enhancedSelector

* Add option row component

* Fix DnD inside option sequence or condition

* Add comments

* Remove item path logic from the dashboard too

* Fix floor/area drag and drop

* Avoid UI jump in area dashboard

* Remove unused import

* Add comment
This commit is contained in:
Paul Bottein 2024-10-30 09:44:38 +01:00 committed by GitHub
parent 51f89b00c1
commit bc11c0b3ac
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
34 changed files with 909 additions and 907 deletions

View File

@ -1,66 +0,0 @@
import { ItemPath } from "../../types";
function findNestedItem(
obj: any,
path: ItemPath,
createNonExistingPath?: boolean
): any {
return path.reduce((ac, p, index, array) => {
if (ac === undefined) return undefined;
if (!ac[p] && createNonExistingPath) {
const nextP = array[index + 1];
// Create object or array depending on next path
if (nextP === undefined || typeof nextP === "number") {
ac[p] = [];
} else {
ac[p] = {};
}
}
return ac[p];
}, obj);
}
function updateNestedItem(obj: any, path: ItemPath): any {
const lastKey = path.pop()!;
const parent = findNestedItem(obj, path);
parent[lastKey] = Array.isArray(parent[lastKey])
? [...parent[lastKey]]
: [parent[lastKey]];
return obj;
}
export function nestedArrayMove<A>(
obj: A,
oldIndex: number,
newIndex: number,
oldPath?: ItemPath,
newPath?: ItemPath
): A {
let newObj = (Array.isArray(obj) ? [...obj] : { ...obj }) as A;
if (oldPath) {
newObj = updateNestedItem(newObj, [...oldPath]);
}
if (newPath) {
newObj = updateNestedItem(newObj, [...newPath]);
}
const from = oldPath ? findNestedItem(newObj, oldPath) : newObj;
const to = newPath ? findNestedItem(newObj, newPath, true) : newObj;
const item = from.splice(oldIndex, 1)[0];
to.splice(newIndex, 0, item);
return newObj;
}
export function arrayMove<T = any>(
array: T[],
oldIndex: number,
newIndex: number
): T[] {
const newArray = [...array];
const [item] = newArray.splice(oldIndex, 1);
newArray.splice(newIndex, 0, item);
return newArray;
}

View File

@ -32,7 +32,6 @@ export class HaActionSelector extends LitElement {
.disabled=${this.disabled} .disabled=${this.disabled}
.actions=${this._actions(this.value)} .actions=${this._actions(this.value)}
.hass=${this.hass} .hass=${this.hass}
.path=${this.selector.action?.path}
></ha-automation-action> ></ha-automation-action>
`; `;
} }

View File

@ -24,7 +24,6 @@ export class HaConditionSelector extends LitElement {
.disabled=${this.disabled} .disabled=${this.disabled}
.conditions=${this.value || []} .conditions=${this.value || []}
.hass=${this.hass} .hass=${this.hass}
.path=${this.selector.condition?.path}
></ha-automation-condition> ></ha-automation-condition>
`; `;
} }

View File

@ -32,7 +32,6 @@ export class HaTriggerSelector extends LitElement {
.disabled=${this.disabled} .disabled=${this.disabled}
.triggers=${this._triggers(this.value)} .triggers=${this._triggers(this.value)}
.hass=${this.hass} .hass=${this.hass}
.path=${this.selector.trigger?.path}
></ha-automation-trigger> ></ha-automation-trigger>
`; `;
} }

View File

@ -19,7 +19,6 @@ import { fireEvent } from "../common/dom/fire_event";
import { computeDomain } from "../common/entity/compute_domain"; import { computeDomain } from "../common/entity/compute_domain";
import { computeObjectId } from "../common/entity/compute_object_id"; import { computeObjectId } from "../common/entity/compute_object_id";
import { supportsFeature } from "../common/entity/supports-feature"; import { supportsFeature } from "../common/entity/supports-feature";
import { nestedArrayMove } from "../common/util/array-move";
import { import {
fetchIntegrationManifest, fetchIntegrationManifest,
IntegrationManifest, IntegrationManifest,
@ -597,15 +596,6 @@ export class HaServiceControl extends LitElement {
} }
const selector = dataField?.selector ?? { text: undefined }; const selector = dataField?.selector ?? { text: undefined };
const type = Object.keys(selector)[0];
const enhancedSelector = ["action", "condition", "trigger"].includes(type)
? {
[type]: {
...selector[type],
path: [dataField.key],
},
}
: selector;
const showOptional = showOptionalToggle(dataField); const showOptional = showOptionalToggle(dataField);
@ -646,7 +636,7 @@ export class HaServiceControl extends LitElement {
(!this._value?.data || (!this._value?.data ||
this._value.data[dataField.key] === undefined))} this._value.data[dataField.key] === undefined))}
.hass=${this.hass} .hass=${this.hass}
.selector=${enhancedSelector} .selector=${selector}
.key=${dataField.key} .key=${dataField.key}
@value-changed=${this._serviceDataChanged} @value-changed=${this._serviceDataChanged}
.value=${this._value?.data .value=${this._value?.data
@ -654,7 +644,6 @@ export class HaServiceControl extends LitElement {
: undefined} : undefined}
.placeholder=${dataField.default} .placeholder=${dataField.default}
.localizeValue=${this._localizeValueCallback} .localizeValue=${this._localizeValueCallback}
@item-moved=${this._itemMoved}
></ha-selector> ></ha-selector>
</ha-settings-row>` </ha-settings-row>`
: ""; : "";
@ -856,22 +845,6 @@ export class HaServiceControl extends LitElement {
}); });
} }
private _itemMoved(ev) {
ev.stopPropagation();
const { oldIndex, newIndex, oldPath, newPath } = ev.detail;
const data = this.value?.data ?? {};
const newData = nestedArrayMove(data, oldIndex, newIndex, oldPath, newPath);
fireEvent(this, "value-changed", {
value: {
...this.value,
data: newData,
},
});
}
private _dataChanged(ev: CustomEvent) { private _dataChanged(ev: CustomEvent) {
ev.stopPropagation(); ev.stopPropagation();
if (!ev.detail.isValid) { if (!ev.detail.isValid) {

View File

@ -4,15 +4,19 @@ import { customElement, property } from "lit/decorators";
import type { SortableEvent } from "sortablejs"; import type { SortableEvent } from "sortablejs";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
import type { SortableInstance } from "../resources/sortable"; import type { SortableInstance } from "../resources/sortable";
import { ItemPath } from "../types";
declare global { declare global {
interface HASSDomEvents { interface HASSDomEvents {
"item-moved": { "item-moved": {
oldIndex: number; oldIndex: number;
newIndex: number; newIndex: number;
oldPath?: ItemPath; };
newPath?: ItemPath; "item-added": {
index: number;
data: any;
};
"item-removed": {
index: number;
}; };
"drag-start": undefined; "drag-start": undefined;
"drag-end": undefined; "drag-end": undefined;
@ -21,7 +25,7 @@ declare global {
export type HaSortableOptions = Omit< export type HaSortableOptions = Omit<
SortableInstance.SortableOptions, SortableInstance.SortableOptions,
"onStart" | "onChoose" | "onEnd" "onStart" | "onChoose" | "onEnd" | "onUpdate" | "onAdd" | "onRemove"
>; >;
@customElement("ha-sortable") @customElement("ha-sortable")
@ -31,9 +35,6 @@ export class HaSortable extends LitElement {
@property({ type: Boolean }) @property({ type: Boolean })
public disabled = false; public disabled = false;
@property({ type: Array })
public path?: ItemPath;
@property({ type: Boolean, attribute: "no-style" }) @property({ type: Boolean, attribute: "no-style" })
public noStyle: boolean = false; public noStyle: boolean = false;
@ -138,6 +139,9 @@ export class HaSortable extends LitElement {
onChoose: this._handleChoose, onChoose: this._handleChoose,
onStart: this._handleStart, onStart: this._handleStart,
onEnd: this._handleEnd, onEnd: this._handleEnd,
onUpdate: this._handleUpdate,
onAdd: this._handleAdd,
onRemove: this._handleRemove,
}; };
if (this.draggableSelector) { if (this.draggableSelector) {
@ -159,33 +163,31 @@ export class HaSortable extends LitElement {
this._sortable = new Sortable(container, options); this._sortable = new Sortable(container, options);
} }
private _handleEnd = async (evt: SortableEvent) => { private _handleUpdate = (evt) => {
fireEvent(this, "item-moved", {
newIndex: evt.newIndex,
oldIndex: evt.oldIndex,
});
};
private _handleAdd = (evt) => {
fireEvent(this, "item-added", {
index: evt.newIndex,
data: evt.item.sortableData,
});
};
private _handleRemove = (evt) => {
fireEvent(this, "item-removed", { index: evt.oldIndex });
};
private _handleEnd = async (evt) => {
fireEvent(this, "drag-end"); fireEvent(this, "drag-end");
// put back in original location // put back in original location
if (this.rollback && (evt.item as any).placeholder) { if (this.rollback && (evt.item as any).placeholder) {
(evt.item as any).placeholder.replaceWith(evt.item); (evt.item as any).placeholder.replaceWith(evt.item);
delete (evt.item as any).placeholder; delete (evt.item as any).placeholder;
} }
const oldIndex = evt.oldIndex;
const oldPath = (evt.from.parentElement as HaSortable).path;
const newIndex = evt.newIndex;
const newPath = (evt.to.parentElement as HaSortable).path;
if (
oldIndex === undefined ||
newIndex === undefined ||
(oldIndex === newIndex && oldPath?.join(".") === newPath?.join("."))
) {
return;
}
fireEvent(this, "item-moved", {
oldIndex,
newIndex,
oldPath,
newPath,
});
}; };
private _handleStart = () => { private _handleStart = () => {

View File

@ -33,7 +33,7 @@ import { LabelRegistryEntry } from "../../data/label_registry";
import { LogbookEntry } from "../../data/logbook"; import { LogbookEntry } from "../../data/logbook";
import { import {
ChooseAction, ChooseAction,
ChooseActionChoice, Option,
IfAction, IfAction,
ParallelAction, ParallelAction,
RepeatAction, RepeatAction,
@ -413,7 +413,7 @@ class ActionRenderer {
: undefined; : undefined;
const choiceConfig = this._getDataFromPath( const choiceConfig = this._getDataFromPath(
`${this.keys[index]}/choose/${chooseTrace.result.choice}` `${this.keys[index]}/choose/${chooseTrace.result.choice}`
) as ChooseActionChoice | undefined; ) as Option | undefined;
const choiceName = choiceConfig const choiceName = choiceConfig
? `${ ? `${
choiceConfig.alias || choiceConfig.alias ||

View File

@ -224,13 +224,14 @@ export interface ForEachRepeat extends BaseRepeat {
for_each: string | any[]; for_each: string | any[];
} }
export interface ChooseActionChoice extends BaseAction { export interface Option {
alias?: string;
conditions: string | Condition[]; conditions: string | Condition[];
sequence: Action | Action[]; sequence: Action | Action[];
} }
export interface ChooseAction extends BaseAction { export interface ChooseAction extends BaseAction {
choose: ChooseActionChoice | ChooseActionChoice[] | null; choose: Option | Option[] | null;
default?: Action | Action[]; default?: Action | Action[];
} }

View File

@ -5,7 +5,7 @@ import { supportsFeature } from "../common/entity/supports-feature";
import type { CropOptions } from "../dialogs/image-cropper-dialog/show-image-cropper-dialog"; import type { CropOptions } from "../dialogs/image-cropper-dialog/show-image-cropper-dialog";
import { isHelperDomain } from "../panels/config/helpers/const"; import { isHelperDomain } from "../panels/config/helpers/const";
import { UiAction } from "../panels/lovelace/components/hui-action-editor"; import { UiAction } from "../panels/lovelace/components/hui-action-editor";
import { HomeAssistant, ItemPath } from "../types"; import { HomeAssistant } from "../types";
import { import {
DeviceRegistryEntry, DeviceRegistryEntry,
getDeviceIntegrationLookup, getDeviceIntegrationLookup,
@ -68,9 +68,8 @@ export type Selector =
| UiStateContentSelector; | UiStateContentSelector;
export interface ActionSelector { export interface ActionSelector {
action: { // eslint-disable-next-line @typescript-eslint/ban-types
path?: ItemPath; action: {} | null;
} | null;
} }
export interface AddonSelector { export interface AddonSelector {
@ -121,9 +120,8 @@ export interface ColorTempSelector {
} }
export interface ConditionSelector { export interface ConditionSelector {
condition: { // eslint-disable-next-line @typescript-eslint/ban-types
path?: ItemPath; condition: {} | null;
} | null;
} }
export interface ConversationAgentSelector { export interface ConversationAgentSelector {
@ -432,9 +430,8 @@ export interface TimeSelector {
} }
export interface TriggerSelector { export interface TriggerSelector {
trigger: { // eslint-disable-next-line @typescript-eslint/ban-types
path?: ItemPath; trigger: {} | null;
} | null;
} }
export interface TTSSelector { export interface TTSSelector {

View File

@ -9,12 +9,13 @@ import {
import { import {
CSSResultGroup, CSSResultGroup,
LitElement, LitElement,
PropertyValues,
TemplateResult, TemplateResult,
css, css,
html, html,
nothing, nothing,
} from "lit"; } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map"; import { styleMap } from "lit/directives/style-map";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { formatListWithAnds } from "../../../common/string/format-list"; import { formatListWithAnds } from "../../../common/string/format-list";
@ -49,7 +50,7 @@ import {
} from "./show-dialog-area-registry-detail"; } from "./show-dialog-area-registry-detail";
import { showFloorRegistryDetailDialog } from "./show-dialog-floor-registry-detail"; import { showFloorRegistryDetailDialog } from "./show-dialog-floor-registry-detail";
const UNASSIGNED_PATH = ["__unassigned__"]; const UNASSIGNED_FLOOR = "__unassigned__";
const SORT_OPTIONS = { sort: false, delay: 500, delayOnTouchOnly: true }; const SORT_OPTIONS = { sort: false, delay: 500, delayOnTouchOnly: true };
@ -63,9 +64,11 @@ export class HaConfigAreasDashboard extends LitElement {
@property({ attribute: false }) public route!: Route; @property({ attribute: false }) public route!: Route;
@state() private _areas: AreaRegistryEntry[] = [];
private _processAreas = memoizeOne( private _processAreas = memoizeOne(
( (
areas: HomeAssistant["areas"], areas: AreaRegistryEntry[],
devices: HomeAssistant["devices"], devices: HomeAssistant["devices"],
entities: HomeAssistant["entities"], entities: HomeAssistant["entities"],
floors: HomeAssistant["floors"] floors: HomeAssistant["floors"]
@ -99,8 +102,8 @@ export class HaConfigAreasDashboard extends LitElement {
}; };
}; };
const floorAreaLookup = getFloorAreaLookup(Object.values(areas)); const floorAreaLookup = getFloorAreaLookup(areas);
const unassisgnedAreas = Object.values(areas).filter( const unassignedAreas = areas.filter(
(area) => !area.floor_id || !floorAreaLookup[area.floor_id] (area) => !area.floor_id || !floorAreaLookup[area.floor_id]
); );
return { return {
@ -108,11 +111,21 @@ export class HaConfigAreasDashboard extends LitElement {
...floor, ...floor,
areas: (floorAreaLookup[floor.floor_id] || []).map(processArea), areas: (floorAreaLookup[floor.floor_id] || []).map(processArea),
})), })),
unassisgnedAreas: unassisgnedAreas.map(processArea), unassignedAreas: unassignedAreas.map(processArea),
}; };
} }
); );
protected willUpdate(changedProperties: PropertyValues<this>): void {
super.willUpdate(changedProperties);
if (changedProperties.has("hass")) {
const oldHass = changedProperties.get("hass");
if (this.hass.areas !== oldHass?.areas) {
this._areas = Object.values(this.hass.areas);
}
}
}
protected render(): TemplateResult { protected render(): TemplateResult {
const areasAndFloors = const areasAndFloors =
!this.hass.areas || !this.hass.areas ||
@ -121,7 +134,7 @@ export class HaConfigAreasDashboard extends LitElement {
!this.hass.floors !this.hass.floors
? undefined ? undefined
: this._processAreas( : this._processAreas(
this.hass.areas, this._areas,
this.hass.devices, this.hass.devices,
this.hass.entities, this.hass.entities,
this.hass.floors this.hass.floors
@ -183,10 +196,10 @@ export class HaConfigAreasDashboard extends LitElement {
<ha-sortable <ha-sortable
handle-selector="a" handle-selector="a"
draggable-selector="a" draggable-selector="a"
@item-moved=${this._areaMoved} @item-added=${this._areaAdded}
group="floor" group="floor"
.options=${SORT_OPTIONS} .options=${SORT_OPTIONS}
.path=${[floor.floor_id]} .floor=${floor.floor_id}
> >
<div class="areas"> <div class="areas">
${floor.areas.map((area) => this._renderArea(area))} ${floor.areas.map((area) => this._renderArea(area))}
@ -194,7 +207,7 @@ export class HaConfigAreasDashboard extends LitElement {
</ha-sortable> </ha-sortable>
</div>` </div>`
)} )}
${areasAndFloors?.unassisgnedAreas.length ${areasAndFloors?.unassignedAreas.length
? html`<div class="floor"> ? html`<div class="floor">
<div class="header"> <div class="header">
<h2> <h2>
@ -206,13 +219,13 @@ export class HaConfigAreasDashboard extends LitElement {
<ha-sortable <ha-sortable
handle-selector="a" handle-selector="a"
draggable-selector="a" draggable-selector="a"
@item-moved=${this._areaMoved} @item-added=${this._areaAdded}
group="floor" group="floor"
.options=${SORT_OPTIONS} .options=${SORT_OPTIONS}
.path=${UNASSIGNED_PATH} .floor=${UNASSIGNED_FLOOR}
> >
<div class="areas"> <div class="areas">
${areasAndFloors?.unassisgnedAreas.map((area) => ${areasAndFloors?.unassignedAreas.map((area) =>
this._renderArea(area) this._renderArea(area)
)} )}
</div> </div>
@ -246,7 +259,10 @@ export class HaConfigAreasDashboard extends LitElement {
} }
private _renderArea(area) { private _renderArea(area) {
return html`<a href=${`/config/areas/area/${area.area_id}`}> return html`<a
href=${`/config/areas/area/${area.area_id}`}
.sortableData=${area}
>
<ha-card outlined> <ha-card outlined>
<div <div
style=${styleMap({ style=${styleMap({
@ -309,26 +325,23 @@ export class HaConfigAreasDashboard extends LitElement {
}); });
} }
private async _areaMoved(ev) { private async _areaAdded(ev) {
const areasAndFloors = this._processAreas( ev.stopPropagation();
this.hass.areas, const { floor } = ev.currentTarget;
this.hass.devices,
this.hass.entities, const newFloorId = floor === UNASSIGNED_FLOOR ? null : floor;
this.hass.floors
); const { data: area } = ev.detail;
let area: AreaRegistryEntry;
if (ev.detail.oldPath === UNASSIGNED_PATH) { this._areas = this._areas.map<AreaRegistryEntry>((a) => {
area = areasAndFloors.unassisgnedAreas[ev.detail.oldIndex]; if (a.area_id === area.area_id) {
} else { return { ...a, floor_id: newFloorId };
const oldFloor = areasAndFloors.floors!.find( }
(floor) => floor.floor_id === ev.detail.oldPath[0] return a;
); });
area = oldFloor!.areas[ev.detail.oldIndex];
}
await updateAreaRegistryEntry(this.hass, area.area_id, { await updateAreaRegistryEntry(this.hass, area.area_id, {
floor_id: floor_id: newFloorId,
ev.detail.newPath === UNASSIGNED_PATH ? null : ev.detail.newPath[0],
}); });
} }

View File

@ -65,7 +65,7 @@ import {
showPromptDialog, showPromptDialog,
} from "../../../../dialogs/generic/show-dialog-box"; } from "../../../../dialogs/generic/show-dialog-box";
import { haStyle } from "../../../../resources/styles"; import { haStyle } from "../../../../resources/styles";
import type { HomeAssistant, ItemPath } from "../../../../types"; import type { HomeAssistant } from "../../../../types";
import { showToast } from "../../../../util/toast"; import { showToast } from "../../../../util/toast";
import "./types/ha-automation-action-activate_scene"; import "./types/ha-automation-action-activate_scene";
import "./types/ha-automation-action-choose"; import "./types/ha-automation-action-choose";
@ -137,8 +137,6 @@ export default class HaAutomationActionRow extends LitElement {
@property({ type: Boolean }) public disabled = false; @property({ type: Boolean }) public disabled = false;
@property({ type: Array }) public path?: ItemPath;
@property({ type: Boolean }) public first?: boolean; @property({ type: Boolean }) public first?: boolean;
@property({ type: Boolean }) public last?: boolean; @property({ type: Boolean }) public last?: boolean;
@ -432,7 +430,6 @@ export default class HaAutomationActionRow extends LitElement {
action: this.action, action: this.action,
narrow: this.narrow, narrow: this.narrow,
disabled: this.disabled, disabled: this.disabled,
path: this.path,
})} })}
</div> </div>
`} `}

View File

@ -13,14 +13,14 @@ import { repeat } from "lit/directives/repeat";
import { storage } from "../../../../common/decorators/storage"; import { storage } from "../../../../common/decorators/storage";
import { fireEvent } from "../../../../common/dom/fire_event"; import { fireEvent } from "../../../../common/dom/fire_event";
import { listenMediaQuery } from "../../../../common/dom/media_query"; import { listenMediaQuery } from "../../../../common/dom/media_query";
import { nestedArrayMove } from "../../../../common/util/array-move"; import { nextRender } from "../../../../common/util/render-status";
import "../../../../components/ha-button"; import "../../../../components/ha-button";
import "../../../../components/ha-sortable"; import "../../../../components/ha-sortable";
import "../../../../components/ha-svg-icon"; import "../../../../components/ha-svg-icon";
import { getService, isService } from "../../../../data/action"; import { getService, isService } from "../../../../data/action";
import type { AutomationClipboard } from "../../../../data/automation"; import type { AutomationClipboard } from "../../../../data/automation";
import { Action } from "../../../../data/script"; import { Action } from "../../../../data/script";
import { HomeAssistant, ItemPath } from "../../../../types"; import { HomeAssistant } from "../../../../types";
import { import {
PASTE_VALUE, PASTE_VALUE,
showAddAutomationElementDialog, showAddAutomationElementDialog,
@ -36,8 +36,6 @@ export default class HaAutomationAction extends LitElement {
@property({ type: Boolean }) public disabled = false; @property({ type: Boolean }) public disabled = false;
@property({ type: Array }) public path?: ItemPath;
@property({ attribute: false }) public actions!: Action[]; @property({ attribute: false }) public actions!: Action[];
@state() private _showReorder: boolean = false; @state() private _showReorder: boolean = false;
@ -69,20 +67,17 @@ export default class HaAutomationAction extends LitElement {
this._unsubMql = undefined; this._unsubMql = undefined;
} }
private get nested() {
return this.path !== undefined;
}
protected render() { protected render() {
return html` return html`
<ha-sortable <ha-sortable
handle-selector=".handle" handle-selector=".handle"
draggable-selector="ha-automation-action-row" draggable-selector="ha-automation-action-row"
.disabled=${!this._showReorder || this.disabled} .disabled=${!this._showReorder || this.disabled}
@item-moved=${this._actionMoved}
group="actions" group="actions"
.path=${this.path}
invert-swap invert-swap
@item-moved=${this._actionMoved}
@item-added=${this._actionAdded}
@item-removed=${this._actionRemoved}
> >
<div class="actions"> <div class="actions">
${repeat( ${repeat(
@ -90,7 +85,7 @@ export default class HaAutomationAction extends LitElement {
(action) => this._getKey(action), (action) => this._getKey(action),
(action, idx) => html` (action, idx) => html`
<ha-automation-action-row <ha-automation-action-row
.path=${[...(this.path ?? []), idx]} .sortableData=${action}
.index=${idx} .index=${idx}
.first=${idx === 0} .first=${idx === 0}
.last=${idx === this.actions.length - 1} .last=${idx === this.actions.length - 1}
@ -225,28 +220,44 @@ export default class HaAutomationAction extends LitElement {
this._move(index, newIndex); this._move(index, newIndex);
} }
private _move( private _move(oldIndex: number, newIndex: number) {
oldIndex: number, const actions = this.actions.concat();
newIndex: number, const item = actions.splice(oldIndex, 1)[0];
oldPath?: ItemPath, actions.splice(newIndex, 0, item);
newPath?: ItemPath this.actions = actions;
) {
const actions = nestedArrayMove(
this.actions,
oldIndex,
newIndex,
oldPath,
newPath
);
fireEvent(this, "value-changed", { value: actions }); fireEvent(this, "value-changed", { value: actions });
} }
private _actionMoved(ev: CustomEvent): void { private _actionMoved(ev: CustomEvent): void {
if (this.nested) return;
ev.stopPropagation(); ev.stopPropagation();
const { oldIndex, newIndex, oldPath, newPath } = ev.detail; const { oldIndex, newIndex } = ev.detail;
this._move(oldIndex, newIndex, oldPath, newPath); this._move(oldIndex, newIndex);
}
private async _actionAdded(ev: CustomEvent): Promise<void> {
ev.stopPropagation();
const { index, data } = ev.detail;
const actions = [
...this.actions.slice(0, index),
data,
...this.actions.slice(index),
];
// Add action locally to avoid UI jump
this.actions = actions;
await nextRender();
fireEvent(this, "value-changed", { value: this.actions });
}
private async _actionRemoved(ev: CustomEvent): Promise<void> {
ev.stopPropagation();
const { index } = ev.detail;
const action = this.actions[index];
// Remove action locally to avoid UI jump
this.actions = this.actions.filter((a) => a !== action);
await nextRender();
// Ensure action is removed even after update
const actions = this.actions.filter((a) => a !== action);
fireEvent(this, "value-changed", { value: actions });
} }
private _actionChanged(ev: CustomEvent) { private _actionChanged(ev: CustomEvent) {

View File

@ -1,299 +1,40 @@
import { consume } from "@lit-labs/context"; import { CSSResultGroup, LitElement, css, html } from "lit";
import type { ActionDetail } from "@material/mwc-list";
import {
mdiArrowDown,
mdiArrowUp,
mdiContentDuplicate,
mdiDelete,
mdiDotsVertical,
mdiDrag,
mdiPlus,
mdiRenameBox,
} from "@mdi/js";
import deepClone from "deep-clone-simple";
import {
CSSResultGroup,
LitElement,
PropertyValues,
css,
html,
nothing,
} from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
import { ensureArray } from "../../../../../common/array/ensure-array"; import { ensureArray } from "../../../../../common/array/ensure-array";
import { fireEvent } from "../../../../../common/dom/fire_event"; import { fireEvent } from "../../../../../common/dom/fire_event";
import { stopPropagation } from "../../../../../common/dom/stop_propagation";
import { listenMediaQuery } from "../../../../../common/dom/media_query";
import { capitalizeFirstLetter } from "../../../../../common/string/capitalize-first-letter";
import "../../../../../components/ha-button"; import "../../../../../components/ha-button";
import "../../../../../components/ha-button-menu"; import { Action, ChooseAction, Option } from "../../../../../data/script";
import "../../../../../components/ha-icon-button";
import "../../../../../components/ha-sortable";
import { Condition } from "../../../../../data/automation";
import { describeCondition } from "../../../../../data/automation_i18n";
import { fullEntitiesContext } from "../../../../../data/context";
import { EntityRegistryEntry } from "../../../../../data/entity_registry";
import {
Action,
ChooseAction,
ChooseActionChoice,
} from "../../../../../data/script";
import {
showConfirmationDialog,
showPromptDialog,
} from "../../../../../dialogs/generic/show-dialog-box";
import { haStyle } from "../../../../../resources/styles"; import { haStyle } from "../../../../../resources/styles";
import { HomeAssistant, ItemPath } from "../../../../../types"; import { HomeAssistant } from "../../../../../types";
import "../../option/ha-automation-option";
import { ActionElement } from "../ha-automation-action-row"; import { ActionElement } from "../ha-automation-action-row";
const preventDefault = (ev) => ev.preventDefault();
@customElement("ha-automation-action-choose") @customElement("ha-automation-action-choose")
export class HaChooseAction extends LitElement implements ActionElement { export class HaChooseAction extends LitElement implements ActionElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean }) public disabled = false; @property({ type: Boolean }) public disabled = false;
@property({ attribute: false }) public path?: ItemPath;
@property({ attribute: false }) public action!: ChooseAction; @property({ attribute: false }) public action!: ChooseAction;
@state() private _showDefault = false; @state() private _showDefault = false;
@state() private _expandedStates: boolean[] = [];
@state()
@consume({ context: fullEntitiesContext, subscribe: true })
_entityReg!: EntityRegistryEntry[];
@state() private _showReorder: boolean = false;
private _expandLast = false;
private _unsubMql?: () => void;
public connectedCallback() {
super.connectedCallback();
this._unsubMql = listenMediaQuery("(min-width: 600px)", (matches) => {
this._showReorder = matches;
});
}
public disconnectedCallback() {
super.disconnectedCallback();
this._unsubMql?.();
this._unsubMql = undefined;
}
public static get defaultConfig(): ChooseAction { public static get defaultConfig(): ChooseAction {
return { choose: [{ conditions: [], sequence: [] }] }; return { choose: [{ conditions: [], sequence: [] }] };
} }
private _expandedChanged(ev) {
this._expandedStates = this._expandedStates.concat();
this._expandedStates[ev.target!.index] = ev.detail.expanded;
}
private _getDescription(option) {
const conditions = ensureArray(option.conditions);
if (!conditions || conditions.length === 0) {
return this.hass.localize(
"ui.panel.config.automation.editor.actions.type.choose.no_conditions"
);
}
let str = "";
if (typeof conditions[0] === "string") {
str += conditions[0];
} else {
str += describeCondition(conditions[0], this.hass, this._entityReg);
}
if (conditions.length > 1) {
str += this.hass.localize(
"ui.panel.config.automation.editor.actions.type.choose.option_description_additional",
{ numberOfAdditionalConditions: conditions.length - 1 }
);
}
return str;
}
protected render() { protected render() {
const action = this.action; const action = this.action;
const options = action.choose ? ensureArray(action.choose) : [];
return html` return html`
<ha-sortable <ha-automation-option
handle-selector=".handle" .options=${options}
draggable-selector=".option" .disabled=${this.disabled}
.disabled=${!this._showReorder || this.disabled} @value-changed=${this._optionsChanged}
group="choose-options" .hass=${this.hass}
.path=${[...(this.path ?? []), "choose"]} ></ha-automation-option>
invert-swap
>
<div class="options">
${repeat(
action.choose ? ensureArray(action.choose) : [],
(option) => option,
(option, idx) => html`
<div class="option">
<ha-card>
<ha-expansion-panel
.index=${idx}
leftChevron
@expanded-changed=${this._expandedChanged}
>
<h3 slot="header">
${this.hass.localize(
"ui.panel.config.automation.editor.actions.type.choose.option",
{ number: idx + 1 }
)}:
${option.alias ||
(this._expandedStates[idx]
? ""
: this._getDescription(option))}
</h3>
${this._showReorder && !this.disabled
? html`
<div class="handle" slot="icons">
<ha-svg-icon .path=${mdiDrag}></ha-svg-icon>
</div>
`
: nothing}
<ha-button-menu
slot="icons"
.idx=${idx}
@action=${this._handleAction}
@click=${preventDefault}
@closed=${stopPropagation}
fixed
>
<ha-icon-button
slot="trigger"
.label=${this.hass.localize("ui.common.menu")}
.path=${mdiDotsVertical}
></ha-icon-button>
<mwc-list-item graphic="icon" .disabled=${this.disabled}>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.rename"
)}
<ha-svg-icon
slot="graphic"
.path=${mdiRenameBox}
></ha-svg-icon>
</mwc-list-item>
<mwc-list-item graphic="icon" .disabled=${this.disabled}>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.duplicate"
)}
<ha-svg-icon
slot="graphic"
.path=${mdiContentDuplicate}
></ha-svg-icon>
</mwc-list-item>
<mwc-list-item
graphic="icon"
.disabled=${this.disabled || idx === 0}
>
${this.hass.localize(
"ui.panel.config.automation.editor.move_up"
)}
<ha-svg-icon
slot="graphic"
.path=${mdiArrowUp}
></ha-svg-icon>
</mwc-list-item>
<mwc-list-item
graphic="icon"
.disabled=${this.disabled ||
idx === ensureArray(this.action.choose).length - 1}
>
${this.hass.localize(
"ui.panel.config.automation.editor.move_down"
)}
<ha-svg-icon
slot="graphic"
.path=${mdiArrowDown}
></ha-svg-icon>
</mwc-list-item>
<mwc-list-item
class="warning"
graphic="icon"
.disabled=${this.disabled}
>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.type.choose.remove_option"
)}
<ha-svg-icon
class="warning"
slot="graphic"
.path=${mdiDelete}
></ha-svg-icon>
</mwc-list-item>
</ha-button-menu>
<div class="card-content">
<h4>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.type.choose.conditions"
)}:
</h4>
<ha-automation-condition
.path=${[
...(this.path ?? []),
"choose",
idx,
"conditions",
]}
.conditions=${ensureArray<string | Condition>(
option.conditions
)}
.disabled=${this.disabled}
.hass=${this.hass}
.idx=${idx}
@value-changed=${this._conditionChanged}
></ha-automation-condition>
<h4>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.type.choose.sequence"
)}:
</h4>
<ha-automation-action
.path=${[
...(this.path ?? []),
"choose",
idx,
"sequence",
]}
.actions=${ensureArray(option.sequence) || []}
.disabled=${this.disabled}
.hass=${this.hass}
.idx=${idx}
@value-changed=${this._actionChanged}
></ha-automation-action>
</div>
</ha-expansion-panel>
</ha-card>
</div>
`
)}
<div class="buttons">
<ha-button
outlined
.label=${this.hass.localize(
"ui.panel.config.automation.editor.actions.type.choose.add_option"
)}
.disabled=${this.disabled}
@click=${this._addOption}
>
<ha-svg-icon .path=${mdiPlus} slot="icon"></ha-svg-icon>
</ha-button>
</div>
</div>
</ha-sortable>
${this._showDefault || action.default ${this._showDefault || action.default
? html` ? html`
@ -303,190 +44,39 @@ export class HaChooseAction extends LitElement implements ActionElement {
)}: )}:
</h2> </h2>
<ha-automation-action <ha-automation-action
.path=${[...(this.path ?? []), "default"]}
.actions=${ensureArray(action.default) || []} .actions=${ensureArray(action.default) || []}
.disabled=${this.disabled} .disabled=${this.disabled}
@value-changed=${this._defaultChanged} @value-changed=${this._defaultChanged}
.hass=${this.hass} .hass=${this.hass}
></ha-automation-action> ></ha-automation-action>
` `
: html`<div class="link-button-row"> : html`
<button <div class="link-button-row">
class="link" <button
@click=${this._addDefault} class="link"
.disabled=${this.disabled} @click=${this._addDefault}
> .disabled=${this.disabled}
${this.hass.localize( >
"ui.panel.config.automation.editor.actions.type.choose.add_default" ${this.hass.localize(
)} "ui.panel.config.automation.editor.actions.type.choose.add_default"
</button> )}
</div>`} </button>
</div>
`}
`; `;
} }
private async _handleAction(ev: CustomEvent<ActionDetail>) {
switch (ev.detail.index) {
case 0:
await this._renameAction(ev);
break;
case 1:
this._duplicateOption(ev);
break;
case 2:
this._moveUp(ev);
break;
case 3:
this._moveDown(ev);
break;
case 4:
this._removeOption(ev);
break;
}
}
private async _renameAction(ev: CustomEvent<ActionDetail>): Promise<void> {
const index = (ev.target as any).idx;
const choose = this.action.choose
? [...ensureArray(this.action.choose)]
: [];
const choice = choose[index];
const alias = await showPromptDialog(this, {
title: this.hass.localize(
"ui.panel.config.automation.editor.actions.type.choose.change_alias"
),
inputLabel: this.hass.localize(
"ui.panel.config.automation.editor.actions.type.choose.alias"
),
inputType: "string",
placeholder: capitalizeFirstLetter(this._getDescription(choice)),
defaultValue: choice.alias,
confirmText: this.hass.localize("ui.common.submit"),
});
if (alias !== null) {
if (alias === "") {
delete choose[index].alias;
} else {
choose[index].alias = alias;
}
fireEvent(this, "value-changed", {
value: { ...this.action, choose },
});
}
}
private _duplicateOption(ev) {
const index = (ev.target as any).idx;
this._createOption(deepClone(ensureArray(this.action.choose)[index]));
}
protected firstUpdated() {
ensureArray(this.action.choose).forEach(() =>
this._expandedStates.push(false)
);
}
protected updated(changedProps: PropertyValues) {
super.updated(changedProps);
if (this._expandLast) {
const nodes = this.shadowRoot!.querySelectorAll("ha-expansion-panel");
nodes[nodes.length - 1].expanded = true;
this._expandLast = false;
}
}
private _addDefault() { private _addDefault() {
this._showDefault = true; this._showDefault = true;
} }
private _conditionChanged(ev: CustomEvent) { private _optionsChanged(ev: CustomEvent) {
ev.stopPropagation(); ev.stopPropagation();
const value = ev.detail.value as Condition[]; const value = ev.detail.value as Option[];
const index = (ev.target as any).idx;
const choose = this.action.choose
? [...ensureArray(this.action.choose)]
: [];
choose[index].conditions = value;
fireEvent(this, "value-changed", { fireEvent(this, "value-changed", {
value: { ...this.action, choose }, value: {
}); ...this.action,
} choose: value,
private _actionChanged(ev: CustomEvent) {
ev.stopPropagation();
const value = ev.detail.value as Action[];
const index = (ev.target as any).idx;
const choose = this.action.choose
? [...ensureArray(this.action.choose)]
: [];
choose[index].sequence = value;
fireEvent(this, "value-changed", {
value: { ...this.action, choose },
});
}
private _addOption() {
this._createOption({ conditions: [], sequence: [] });
}
private _createOption(opt: ChooseActionChoice) {
const choose = this.action.choose
? [...ensureArray(this.action.choose)]
: [];
choose.push(opt);
fireEvent(this, "value-changed", {
value: { ...this.action, choose },
});
this._expandLast = true;
this._expandedStates[choose.length - 1] = true;
}
private _moveUp(ev) {
const index = (ev.target as any).idx;
const newIndex = index - 1;
this._move(index, newIndex);
}
private _moveDown(ev) {
const index = (ev.target as any).idx;
const newIndex = index + 1;
this._move(index, newIndex);
}
private _move(index: number, newIndex: number) {
const options = ensureArray(this.action.choose)!.concat();
const item = options.splice(index, 1)[0];
options.splice(newIndex, 0, item);
const expanded = this._expandedStates.splice(index, 1)[0];
this._expandedStates.splice(newIndex, 0, expanded);
fireEvent(this, "value-changed", {
value: { ...this.action, choose: options },
});
}
private _removeOption(ev: CustomEvent) {
const index = (ev.target as any).idx;
showConfirmationDialog(this, {
title: this.hass.localize(
"ui.panel.config.automation.editor.actions.type.choose.delete_confirm_title"
),
text: this.hass.localize(
"ui.panel.config.automation.editor.actions.delete_confirm_text"
),
dismissText: this.hass.localize("ui.common.cancel"),
confirmText: this.hass.localize("ui.common.delete"),
destructive: true,
confirm: () => {
const choose = this.action.choose
? [...ensureArray(this.action.choose)]
: [];
choose.splice(index, 1);
this._expandedStates.splice(index, 1);
fireEvent(this, "value-changed", {
value: { ...this.action, choose },
});
}, },
}); });
} }
@ -509,68 +99,9 @@ export class HaChooseAction extends LitElement implements ActionElement {
return [ return [
haStyle, haStyle,
css` css`
.options {
padding: 16px;
margin: -16px;
display: flex;
flex-direction: column;
gap: 16px;
}
.sortable-ghost {
background: none;
border-radius: var(--ha-card-border-radius, 12px);
}
.sortable-drag {
background: none;
}
.add-card mwc-button {
display: block;
text-align: center;
}
ha-expansion-panel {
--expansion-panel-summary-padding: 0 0 0 8px;
--expansion-panel-content-padding: 0;
}
mwc-list-item[disabled] {
--mdc-theme-text-primary-on-background: var(--disabled-text-color);
}
mwc-list-item.hidden {
display: none;
}
h3 {
margin: 0;
font-size: inherit;
font-weight: inherit;
}
ha-icon-button {
inset-inline-start: initial;
inset-inline-end: 0;
direction: var(--direction);
}
ha-svg-icon {
height: 20px;
}
.link-button-row { .link-button-row {
padding: 14px 14px 0 14px; padding: 14px 14px 0 14px;
} }
.card-content {
padding: 0 16px 16px 16px;
}
.handle {
padding: 12px;
cursor: move; /* fallback if grab cursor is unsupported */
cursor: grab;
}
.handle ha-svg-icon {
pointer-events: none;
height: 24px;
}
.buttons {
display: flex;
flex-wrap: wrap;
gap: 8px;
order: 1;
}
`, `,
]; ];
} }

View File

@ -4,7 +4,7 @@ import { fireEvent } from "../../../../../common/dom/fire_event";
import "../../../../../components/ha-textfield"; import "../../../../../components/ha-textfield";
import { Action, IfAction } from "../../../../../data/script"; import { Action, IfAction } from "../../../../../data/script";
import { haStyle } from "../../../../../resources/styles"; import { haStyle } from "../../../../../resources/styles";
import type { HomeAssistant, ItemPath } from "../../../../../types"; import type { HomeAssistant } from "../../../../../types";
import type { Condition } from "../../../../lovelace/common/validate-condition"; import type { Condition } from "../../../../lovelace/common/validate-condition";
import "../ha-automation-action"; import "../ha-automation-action";
import type { ActionElement } from "../ha-automation-action-row"; import type { ActionElement } from "../ha-automation-action-row";
@ -15,8 +15,6 @@ export class HaIfAction extends LitElement implements ActionElement {
@property({ type: Boolean }) public disabled = false; @property({ type: Boolean }) public disabled = false;
@property({ attribute: false }) public path?: ItemPath;
@property({ attribute: false }) public action!: IfAction; @property({ attribute: false }) public action!: IfAction;
@state() private _showElse = false; @state() private _showElse = false;
@ -38,7 +36,6 @@ export class HaIfAction extends LitElement implements ActionElement {
)}*: )}*:
</h3> </h3>
<ha-automation-condition <ha-automation-condition
.path=${[...(this.path ?? []), "if"]}
.conditions=${action.if} .conditions=${action.if}
.disabled=${this.disabled} .disabled=${this.disabled}
@value-changed=${this._ifChanged} @value-changed=${this._ifChanged}
@ -51,7 +48,6 @@ export class HaIfAction extends LitElement implements ActionElement {
)}*: )}*:
</h3> </h3>
<ha-automation-action <ha-automation-action
.path=${[...(this.path ?? []), "then"]}
.actions=${action.then} .actions=${action.then}
.disabled=${this.disabled} .disabled=${this.disabled}
@value-changed=${this._thenChanged} @value-changed=${this._thenChanged}
@ -65,7 +61,6 @@ export class HaIfAction extends LitElement implements ActionElement {
)}: )}:
</h3> </h3>
<ha-automation-action <ha-automation-action
.path=${[...(this.path ?? []), "else"]}
.actions=${action.else || []} .actions=${action.else || []}
.disabled=${this.disabled} .disabled=${this.disabled}
@value-changed=${this._elseChanged} @value-changed=${this._elseChanged}

View File

@ -4,7 +4,7 @@ import { fireEvent } from "../../../../../common/dom/fire_event";
import "../../../../../components/ha-textfield"; import "../../../../../components/ha-textfield";
import { Action, ParallelAction } from "../../../../../data/script"; import { Action, ParallelAction } from "../../../../../data/script";
import { haStyle } from "../../../../../resources/styles"; import { haStyle } from "../../../../../resources/styles";
import type { HomeAssistant, ItemPath } from "../../../../../types"; import type { HomeAssistant } from "../../../../../types";
import "../ha-automation-action"; import "../ha-automation-action";
import type { ActionElement } from "../ha-automation-action-row"; import type { ActionElement } from "../ha-automation-action-row";
@ -14,8 +14,6 @@ export class HaParallelAction extends LitElement implements ActionElement {
@property({ type: Boolean }) public disabled = false; @property({ type: Boolean }) public disabled = false;
@property({ attribute: false }) public path?: ItemPath;
@property({ attribute: false }) public action!: ParallelAction; @property({ attribute: false }) public action!: ParallelAction;
public static get defaultConfig(): ParallelAction { public static get defaultConfig(): ParallelAction {
@ -29,7 +27,6 @@ export class HaParallelAction extends LitElement implements ActionElement {
return html` return html`
<ha-automation-action <ha-automation-action
.path=${[...(this.path ?? []), "parallel"]}
.actions=${action.parallel} .actions=${action.parallel}
.disabled=${this.disabled} .disabled=${this.disabled}
@value-changed=${this._actionsChanged} @value-changed=${this._actionsChanged}

View File

@ -5,7 +5,7 @@ import { fireEvent } from "../../../../../common/dom/fire_event";
import "../../../../../components/ha-textfield"; import "../../../../../components/ha-textfield";
import { RepeatAction } from "../../../../../data/script"; import { RepeatAction } from "../../../../../data/script";
import { haStyle } from "../../../../../resources/styles"; import { haStyle } from "../../../../../resources/styles";
import type { HomeAssistant, ItemPath } from "../../../../../types"; import type { HomeAssistant } from "../../../../../types";
import "../ha-automation-action"; import "../ha-automation-action";
import type { ActionElement } from "../ha-automation-action-row"; import type { ActionElement } from "../ha-automation-action-row";
@ -29,19 +29,12 @@ export class HaRepeatAction extends LitElement implements ActionElement {
@property({ attribute: false }) public action!: RepeatAction; @property({ attribute: false }) public action!: RepeatAction;
@property({ type: Array }) public path?: ItemPath;
public static get defaultConfig(): RepeatAction { public static get defaultConfig(): RepeatAction {
return { repeat: { count: 2, sequence: [] } }; return { repeat: { count: 2, sequence: [] } };
} }
private _schema = memoizeOne( private _schema = memoizeOne(
( (localize: LocalizeFunc, type: string, template: boolean) =>
localize: LocalizeFunc,
type: string,
template: boolean,
path?: ItemPath
) =>
[ [
{ {
name: "type", name: "type",
@ -73,9 +66,7 @@ export class HaRepeatAction extends LitElement implements ActionElement {
{ {
name: type, name: type,
selector: { selector: {
condition: { condition: {},
path: [...(path ?? []), "repeat", type],
},
}, },
}, },
] as const satisfies readonly HaFormSchema[]) ] as const satisfies readonly HaFormSchema[])
@ -92,9 +83,7 @@ export class HaRepeatAction extends LitElement implements ActionElement {
{ {
name: "sequence", name: "sequence",
selector: { selector: {
action: { action: {},
path: [...(path ?? []), "repeat", "sequence"],
},
}, },
}, },
] as const satisfies readonly HaFormSchema[] ] as const satisfies readonly HaFormSchema[]
@ -108,8 +97,7 @@ export class HaRepeatAction extends LitElement implements ActionElement {
type ?? "count", type ?? "count",
"count" in action && typeof action.count === "string" "count" in action && typeof action.count === "string"
? isTemplate(action.count) ? isTemplate(action.count)
: false, : false
this.path
); );
const data = { ...action, type }; const data = { ...action, type };

View File

@ -1,11 +1,10 @@
import { CSSResultGroup, html, LitElement } from "lit"; import { CSSResultGroup, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../../common/dom/fire_event"; import { fireEvent } from "../../../../../common/dom/fire_event";
import "../../../../../components/ha-textfield"; import "../../../../../components/ha-textfield";
import { Action, SequenceAction } from "../../../../../data/script"; import { Action, SequenceAction } from "../../../../../data/script";
import { haStyle } from "../../../../../resources/styles"; import { haStyle } from "../../../../../resources/styles";
import type { HomeAssistant, ItemPath } from "../../../../../types"; import type { HomeAssistant } from "../../../../../types";
import "../ha-automation-action"; import "../ha-automation-action";
import type { ActionElement } from "../ha-automation-action-row"; import type { ActionElement } from "../ha-automation-action-row";
@ -15,8 +14,6 @@ export class HaSequenceAction extends LitElement implements ActionElement {
@property({ type: Boolean }) public disabled = false; @property({ type: Boolean }) public disabled = false;
@property({ attribute: false }) public path?: ItemPath;
@property({ attribute: false }) public action!: SequenceAction; @property({ attribute: false }) public action!: SequenceAction;
public static get defaultConfig(): SequenceAction { public static get defaultConfig(): SequenceAction {
@ -25,17 +22,11 @@ export class HaSequenceAction extends LitElement implements ActionElement {
}; };
} }
private _getMemoizedPath = memoizeOne((path: ItemPath | undefined) => [
...(path ?? []),
"sequence",
]);
protected render() { protected render() {
const { action } = this; const { action } = this;
return html` return html`
<ha-automation-action <ha-automation-action
.path=${this._getMemoizedPath(this.path)}
.actions=${action.sequence} .actions=${action.sequence}
.disabled=${this.disabled} .disabled=${this.disabled}
@value-changed=${this._actionsChanged} @value-changed=${this._actionsChanged}

View File

@ -8,7 +8,7 @@ import "../../../../../components/ha-duration-input";
import "../../../../../components/ha-formfield"; import "../../../../../components/ha-formfield";
import "../../../../../components/ha-textfield"; import "../../../../../components/ha-textfield";
import { WaitForTriggerAction } from "../../../../../data/script"; import { WaitForTriggerAction } from "../../../../../data/script";
import { HomeAssistant, ItemPath } from "../../../../../types"; import { HomeAssistant } from "../../../../../types";
import "../../trigger/ha-automation-trigger"; import "../../trigger/ha-automation-trigger";
import { ActionElement, handleChangeEvent } from "../ha-automation-action-row"; import { ActionElement, handleChangeEvent } from "../ha-automation-action-row";
@ -23,8 +23,6 @@ export class HaWaitForTriggerAction
@property({ type: Boolean }) public disabled = false; @property({ type: Boolean }) public disabled = false;
@property({ attribute: false }) public path?: ItemPath;
public static get defaultConfig(): WaitForTriggerAction { public static get defaultConfig(): WaitForTriggerAction {
return { wait_for_trigger: [] }; return { wait_for_trigger: [] };
} }
@ -55,7 +53,6 @@ export class HaWaitForTriggerAction
></ha-switch> ></ha-switch>
</ha-formfield> </ha-formfield>
<ha-automation-trigger <ha-automation-trigger
.path=${[...(this.path ?? []), "wait_for_trigger"]}
.triggers=${ensureArray(this.action.wait_for_trigger)} .triggers=${ensureArray(this.action.wait_for_trigger)}
.hass=${this.hass} .hass=${this.hass}
.disabled=${this.disabled} .disabled=${this.disabled}

View File

@ -7,7 +7,7 @@ import "../../../../components/ha-yaml-editor";
import type { Condition } from "../../../../data/automation"; import type { Condition } from "../../../../data/automation";
import { expandConditionWithShorthand } from "../../../../data/automation"; import { expandConditionWithShorthand } from "../../../../data/automation";
import { haStyle } from "../../../../resources/styles"; import { haStyle } from "../../../../resources/styles";
import type { HomeAssistant, ItemPath } from "../../../../types"; import type { HomeAssistant } from "../../../../types";
import "./types/ha-automation-condition-and"; import "./types/ha-automation-condition-and";
import "./types/ha-automation-condition-device"; import "./types/ha-automation-condition-device";
import "./types/ha-automation-condition-not"; import "./types/ha-automation-condition-not";
@ -30,8 +30,6 @@ export default class HaAutomationConditionEditor extends LitElement {
@property({ type: Boolean }) public yamlMode = false; @property({ type: Boolean }) public yamlMode = false;
@property({ type: Array }) public path?: ItemPath;
private _processedCondition = memoizeOne((condition) => private _processedCondition = memoizeOne((condition) =>
expandConditionWithShorthand(condition) expandConditionWithShorthand(condition)
); );
@ -68,7 +66,6 @@ export default class HaAutomationConditionEditor extends LitElement {
hass: this.hass, hass: this.hass,
condition: condition, condition: condition,
disabled: this.disabled, disabled: this.disabled,
path: this.path,
} }
)} )}
</div> </div>

View File

@ -41,7 +41,7 @@ import {
showPromptDialog, showPromptDialog,
} from "../../../../dialogs/generic/show-dialog-box"; } from "../../../../dialogs/generic/show-dialog-box";
import { haStyle } from "../../../../resources/styles"; import { haStyle } from "../../../../resources/styles";
import { HomeAssistant, ItemPath } from "../../../../types"; import { HomeAssistant } from "../../../../types";
import "./ha-automation-condition-editor"; import "./ha-automation-condition-editor";
export interface ConditionElement extends LitElement { export interface ConditionElement extends LitElement {
@ -83,8 +83,6 @@ export default class HaAutomationConditionRow extends LitElement {
@property({ type: Boolean }) public disabled = false; @property({ type: Boolean }) public disabled = false;
@property({ type: Array }) public path?: ItemPath;
@property({ type: Boolean }) public first?: boolean; @property({ type: Boolean }) public first?: boolean;
@property({ type: Boolean }) public last?: boolean; @property({ type: Boolean }) public last?: boolean;
@ -302,7 +300,6 @@ export default class HaAutomationConditionRow extends LitElement {
.disabled=${this.disabled} .disabled=${this.disabled}
.hass=${this.hass} .hass=${this.hass}
.condition=${this.condition} .condition=${this.condition}
.path=${this.path}
></ha-automation-condition-editor> ></ha-automation-condition-editor>
</div> </div>
</ha-expansion-panel> </ha-expansion-panel>

View File

@ -13,7 +13,7 @@ import { repeat } from "lit/directives/repeat";
import { storage } from "../../../../common/decorators/storage"; import { storage } from "../../../../common/decorators/storage";
import { fireEvent } from "../../../../common/dom/fire_event"; import { fireEvent } from "../../../../common/dom/fire_event";
import { listenMediaQuery } from "../../../../common/dom/media_query"; import { listenMediaQuery } from "../../../../common/dom/media_query";
import { nestedArrayMove } from "../../../../common/util/array-move"; import { nextRender } from "../../../../common/util/render-status";
import "../../../../components/ha-button"; import "../../../../components/ha-button";
import "../../../../components/ha-button-menu"; import "../../../../components/ha-button-menu";
import "../../../../components/ha-sortable"; import "../../../../components/ha-sortable";
@ -22,7 +22,7 @@ import type {
AutomationClipboard, AutomationClipboard,
Condition, Condition,
} from "../../../../data/automation"; } from "../../../../data/automation";
import type { HomeAssistant, ItemPath } from "../../../../types"; import type { HomeAssistant } from "../../../../types";
import { import {
PASTE_VALUE, PASTE_VALUE,
showAddAutomationElementDialog, showAddAutomationElementDialog,
@ -38,8 +38,6 @@ export default class HaAutomationCondition extends LitElement {
@property({ type: Boolean }) public disabled = false; @property({ type: Boolean }) public disabled = false;
@property({ type: Array }) public path?: ItemPath;
@state() private _showReorder: boolean = false; @state() private _showReorder: boolean = false;
@storage({ @storage({
@ -115,10 +113,6 @@ export default class HaAutomationCondition extends LitElement {
}); });
} }
private get nested() {
return this.path !== undefined;
}
protected render() { protected render() {
if (!Array.isArray(this.conditions)) { if (!Array.isArray(this.conditions)) {
return nothing; return nothing;
@ -128,10 +122,11 @@ export default class HaAutomationCondition extends LitElement {
handle-selector=".handle" handle-selector=".handle"
draggable-selector="ha-automation-condition-row" draggable-selector="ha-automation-condition-row"
.disabled=${!this._showReorder || this.disabled} .disabled=${!this._showReorder || this.disabled}
@item-moved=${this._conditionMoved}
group="conditions" group="conditions"
.path=${this.path}
invert-swap invert-swap
@item-moved=${this._conditionMoved}
@item-added=${this._conditionAdded}
@item-removed=${this._conditionRemoved}
> >
<div class="conditions"> <div class="conditions">
${repeat( ${repeat(
@ -139,7 +134,7 @@ export default class HaAutomationCondition extends LitElement {
(condition) => this._getKey(condition), (condition) => this._getKey(condition),
(cond, idx) => html` (cond, idx) => html`
<ha-automation-condition-row <ha-automation-condition-row
.path=${[...(this.path ?? []), idx]} .sortableData=${cond}
.index=${idx} .index=${idx}
.first=${idx === 0} .first=${idx === 0}
.last=${idx === this.conditions.length - 1} .last=${idx === this.conditions.length - 1}
@ -248,28 +243,44 @@ export default class HaAutomationCondition extends LitElement {
this._move(index, newIndex); this._move(index, newIndex);
} }
private _move( private _move(oldIndex: number, newIndex: number) {
oldIndex: number, const conditions = this.conditions.concat();
newIndex: number, const item = conditions.splice(oldIndex, 1)[0];
oldPath?: ItemPath, conditions.splice(newIndex, 0, item);
newPath?: ItemPath this.conditions = conditions;
) {
const conditions = nestedArrayMove(
this.conditions,
oldIndex,
newIndex,
oldPath,
newPath
);
fireEvent(this, "value-changed", { value: conditions }); fireEvent(this, "value-changed", { value: conditions });
} }
private _conditionMoved(ev: CustomEvent): void { private _conditionMoved(ev: CustomEvent): void {
if (this.nested) return;
ev.stopPropagation(); ev.stopPropagation();
const { oldIndex, newIndex, oldPath, newPath } = ev.detail; const { oldIndex, newIndex } = ev.detail;
this._move(oldIndex, newIndex, oldPath, newPath); this._move(oldIndex, newIndex);
}
private async _conditionAdded(ev: CustomEvent): Promise<void> {
ev.stopPropagation();
const { index, data } = ev.detail;
const conditions = [
...this.conditions.slice(0, index),
data,
...this.conditions.slice(index),
];
// Add condition locally to avoid UI jump
this.conditions = conditions;
await nextRender();
fireEvent(this, "value-changed", { value: this.conditions });
}
private async _conditionRemoved(ev: CustomEvent): Promise<void> {
ev.stopPropagation();
const { index } = ev.detail;
const condition = this.conditions[index];
// Remove condition locally to avoid UI jump
this.conditions = this.conditions.filter((c) => c !== condition);
await nextRender();
// Ensure condition is removed even after update
const conditions = this.conditions.filter((c) => c !== condition);
fireEvent(this, "value-changed", { value: conditions });
} }
private _conditionChanged(ev: CustomEvent) { private _conditionChanged(ev: CustomEvent) {

View File

@ -2,7 +2,7 @@ import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../../../../common/dom/fire_event"; import { fireEvent } from "../../../../../common/dom/fire_event";
import type { LogicalCondition } from "../../../../../data/automation"; import type { LogicalCondition } from "../../../../../data/automation";
import type { HomeAssistant, ItemPath } from "../../../../../types"; import type { HomeAssistant } from "../../../../../types";
import "../ha-automation-condition"; import "../ha-automation-condition";
import type { ConditionElement } from "../ha-automation-condition-row"; import type { ConditionElement } from "../ha-automation-condition-row";
@ -17,12 +17,9 @@ export abstract class HaLogicalCondition
@property({ type: Boolean }) public disabled = false; @property({ type: Boolean }) public disabled = false;
@property({ attribute: false }) public path?: ItemPath;
protected render() { protected render() {
return html` return html`
<ha-automation-condition <ha-automation-condition
.path=${[...(this.path ?? []), "conditions"]}
.conditions=${this.condition.conditions || []} .conditions=${this.condition.conditions || []}
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
.hass=${this.hass} .hass=${this.hass}

View File

@ -12,7 +12,6 @@ import {
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { ensureArray } from "../../../common/array/ensure-array"; import { ensureArray } from "../../../common/array/ensure-array";
import { fireEvent } from "../../../common/dom/fire_event"; import { fireEvent } from "../../../common/dom/fire_event";
import { nestedArrayMove } from "../../../common/util/array-move";
import "../../../components/ha-card"; import "../../../components/ha-card";
import "../../../components/ha-icon-button"; import "../../../components/ha-icon-button";
import "../../../components/ha-markdown"; import "../../../components/ha-markdown";
@ -132,7 +131,6 @@ export class HaManualAutomationEditor extends LitElement {
.triggers=${this.config.triggers || []} .triggers=${this.config.triggers || []}
.path=${["triggers"]} .path=${["triggers"]}
@value-changed=${this._triggerChanged} @value-changed=${this._triggerChanged}
@item-moved=${this._itemMoved}
.hass=${this.hass} .hass=${this.hass}
.disabled=${this.disabled} .disabled=${this.disabled}
></ha-automation-trigger> ></ha-automation-trigger>
@ -174,7 +172,6 @@ export class HaManualAutomationEditor extends LitElement {
.conditions=${this.config.conditions || []} .conditions=${this.config.conditions || []}
.path=${["conditions"]} .path=${["conditions"]}
@value-changed=${this._conditionChanged} @value-changed=${this._conditionChanged}
@item-moved=${this._itemMoved}
.hass=${this.hass} .hass=${this.hass}
.disabled=${this.disabled} .disabled=${this.disabled}
></ha-automation-condition> ></ha-automation-condition>
@ -214,7 +211,6 @@ export class HaManualAutomationEditor extends LitElement {
.actions=${this.config.actions || []} .actions=${this.config.actions || []}
.path=${["actions"]} .path=${["actions"]}
@value-changed=${this._actionChanged} @value-changed=${this._actionChanged}
@item-moved=${this._itemMoved}
.hass=${this.hass} .hass=${this.hass}
.narrow=${this.narrow} .narrow=${this.narrow}
.disabled=${this.disabled} .disabled=${this.disabled}
@ -246,21 +242,6 @@ export class HaManualAutomationEditor extends LitElement {
}); });
} }
private _itemMoved(ev: CustomEvent): void {
ev.stopPropagation();
const { oldIndex, newIndex, oldPath, newPath } = ev.detail;
const updatedConfig = nestedArrayMove(
this.config,
oldIndex,
newIndex,
oldPath,
newPath
);
fireEvent(this, "value-changed", {
value: updatedConfig,
});
}
private async _enable(): Promise<void> { private async _enable(): Promise<void> {
if (!this.hass || !this.stateObj) { if (!this.hass || !this.stateObj) {
return; return;

View File

@ -0,0 +1,335 @@
import { consume } from "@lit-labs/context";
import { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
import "@material/mwc-list/mwc-list-item";
import {
mdiArrowDown,
mdiArrowUp,
mdiContentDuplicate,
mdiDelete,
mdiDotsVertical,
mdiRenameBox,
} from "@mdi/js";
import { CSSResultGroup, LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { ensureArray } from "../../../../common/array/ensure-array";
import { fireEvent } from "../../../../common/dom/fire_event";
import { preventDefault } from "../../../../common/dom/prevent_default";
import { stopPropagation } from "../../../../common/dom/stop_propagation";
import { capitalizeFirstLetter } from "../../../../common/string/capitalize-first-letter";
import "../../../../components/ha-button-menu";
import "../../../../components/ha-card";
import "../../../../components/ha-expansion-panel";
import "../../../../components/ha-icon-button";
import { Condition } from "../../../../data/automation";
import { describeCondition } from "../../../../data/automation_i18n";
import { fullEntitiesContext } from "../../../../data/context";
import { EntityRegistryEntry } from "../../../../data/entity_registry";
import { Action, Option } from "../../../../data/script";
import {
showConfirmationDialog,
showPromptDialog,
} from "../../../../dialogs/generic/show-dialog-box";
import { haStyle } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types";
@customElement("ha-automation-option-row")
export default class HaAutomationOptionRow extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public option!: Option;
@property({ type: Boolean }) public narrow = false;
@property({ type: Boolean }) public disabled = false;
@property({ type: Number }) public index!: number;
@property({ type: Boolean }) public first = false;
@property({ type: Boolean }) public last = false;
@state() private _expanded = false;
@state()
@consume({ context: fullEntitiesContext, subscribe: true })
_entityReg!: EntityRegistryEntry[];
private _expandedChanged(ev) {
if (ev.currentTarget.id !== "option") {
return;
}
this._expanded = ev.detail.expanded;
}
private _getDescription() {
const conditions = ensureArray<Condition | string>(this.option.conditions);
if (!conditions || conditions.length === 0) {
return this.hass.localize(
"ui.panel.config.automation.editor.actions.type.choose.no_conditions"
);
}
let str = "";
if (typeof conditions[0] === "string") {
str += conditions[0];
} else {
str += describeCondition(conditions[0], this.hass, this._entityReg);
}
if (conditions.length > 1) {
str += this.hass.localize(
"ui.panel.config.automation.editor.actions.type.choose.option_description_additional",
{ numberOfAdditionalConditions: conditions.length - 1 }
);
}
return str;
}
protected render() {
if (!this.option) return nothing;
return html`
<ha-card outlined>
<ha-expansion-panel
leftChevron
@expanded-changed=${this._expandedChanged}
id="option"
>
<h3 slot="header">
${this.hass.localize(
"ui.panel.config.automation.editor.actions.type.choose.option",
{ number: this.index + 1 }
)}:
${this.option.alias ||
(this._expanded ? "" : this._getDescription())}
</h3>
<slot name="icons" slot="icons"></slot>
<ha-button-menu
slot="icons"
@action=${this._handleAction}
@click=${preventDefault}
@closed=${stopPropagation}
fixed
>
<ha-icon-button
slot="trigger"
.label=${this.hass.localize("ui.common.menu")}
.path=${mdiDotsVertical}
></ha-icon-button>
<mwc-list-item graphic="icon" .disabled=${this.disabled}>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.rename"
)}
<ha-svg-icon slot="graphic" .path=${mdiRenameBox}></ha-svg-icon>
</mwc-list-item>
<mwc-list-item graphic="icon" .disabled=${this.disabled}>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.duplicate"
)}
<ha-svg-icon
slot="graphic"
.path=${mdiContentDuplicate}
></ha-svg-icon>
</mwc-list-item>
<mwc-list-item
graphic="icon"
.disabled=${this.disabled || this.first}
>
${this.hass.localize("ui.panel.config.automation.editor.move_up")}
<ha-svg-icon slot="graphic" .path=${mdiArrowUp}></ha-svg-icon>
</mwc-list-item>
<mwc-list-item
graphic="icon"
.disabled=${this.disabled || this.last}
>
${this.hass.localize(
"ui.panel.config.automation.editor.move_down"
)}
<ha-svg-icon slot="graphic" .path=${mdiArrowDown}></ha-svg-icon>
</mwc-list-item>
<mwc-list-item
class="warning"
graphic="icon"
.disabled=${this.disabled}
>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.type.choose.remove_option"
)}
<ha-svg-icon
class="warning"
slot="graphic"
.path=${mdiDelete}
></ha-svg-icon>
</mwc-list-item>
</ha-button-menu>
<div class="card-content">
<h4>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.type.choose.conditions"
)}:
</h4>
<ha-automation-condition
.conditions=${ensureArray<string | Condition>(
this.option.conditions
)}
.disabled=${this.disabled}
.hass=${this.hass}
@value-changed=${this._conditionChanged}
></ha-automation-condition>
<h4>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.type.choose.sequence"
)}:
</h4>
<ha-automation-action
.actions=${ensureArray(this.option.sequence) || []}
.disabled=${this.disabled}
.hass=${this.hass}
@value-changed=${this._actionChanged}
></ha-automation-action>
</div>
</ha-expansion-panel>
</ha-card>
`;
}
private async _handleAction(ev: CustomEvent<ActionDetail>) {
switch (ev.detail.index) {
case 0:
await this._renameOption();
break;
case 1:
fireEvent(this, "duplicate");
break;
case 2:
fireEvent(this, "move-up");
break;
case 3:
fireEvent(this, "move-down");
break;
case 4:
this._removeOption();
break;
}
}
private _removeOption() {
showConfirmationDialog(this, {
title: this.hass.localize(
"ui.panel.config.automation.editor.actions.type.choose.delete_confirm_title"
),
text: this.hass.localize(
"ui.panel.config.automation.editor.actions.delete_confirm_text"
),
dismissText: this.hass.localize("ui.common.cancel"),
confirmText: this.hass.localize("ui.common.delete"),
destructive: true,
confirm: () =>
fireEvent(this, "value-changed", {
value: null,
}),
});
}
private async _renameOption(): Promise<void> {
const alias = await showPromptDialog(this, {
title: this.hass.localize(
"ui.panel.config.automation.editor.actions.type.choose.change_alias"
),
inputLabel: this.hass.localize(
"ui.panel.config.automation.editor.actions.type.choose.alias"
),
inputType: "string",
placeholder: capitalizeFirstLetter(this._getDescription()),
defaultValue: this.option.alias,
confirmText: this.hass.localize("ui.common.submit"),
});
if (alias !== null) {
const value = { ...this.option };
if (alias === "") {
delete value.alias;
} else {
value.alias = alias;
}
fireEvent(this, "value-changed", {
value,
});
}
}
private _conditionChanged(ev: CustomEvent) {
ev.stopPropagation();
const conditions = ev.detail.value as Condition[];
const value = { ...this.option, conditions: conditions };
fireEvent(this, "value-changed", {
value,
});
}
private _actionChanged(ev: CustomEvent) {
ev.stopPropagation();
const actions = ev.detail.value as Action[];
const value = { ...this.option, sequence: actions };
fireEvent(this, "value-changed", {
value,
});
}
public expand() {
this.updateComplete.then(() => {
this.shadowRoot!.querySelector("ha-expansion-panel")!.expanded = true;
});
}
static get styles(): CSSResultGroup {
return [
haStyle,
css`
ha-button-menu,
ha-icon-button {
--mdc-theme-text-primary-on-background: var(--primary-text-color);
}
.disabled {
opacity: 0.5;
pointer-events: none;
}
ha-expansion-panel {
--expansion-panel-summary-padding: 0 0 0 8px;
--expansion-panel-content-padding: 0;
}
h3 {
margin: 0;
font-size: inherit;
font-weight: inherit;
}
.card-content {
padding: 16px;
}
mwc-list-item[disabled] {
--mdc-theme-text-primary-on-background: var(--disabled-text-color);
}
mwc-list-item.hidden {
display: none;
}
.warning ul {
margin: 4px 0;
}
li[role="separator"] {
border-bottom-color: var(--divider-color);
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-automation-option-row": HaAutomationOptionRow;
}
}

View File

@ -0,0 +1,290 @@
import { mdiDrag, mdiPlus } from "@mdi/js";
import deepClone from "deep-clone-simple";
import {
CSSResultGroup,
LitElement,
PropertyValues,
css,
html,
nothing,
} from "lit";
import { customElement, property, state } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
import { storage } from "../../../../common/decorators/storage";
import { fireEvent } from "../../../../common/dom/fire_event";
import { listenMediaQuery } from "../../../../common/dom/media_query";
import { nextRender } from "../../../../common/util/render-status";
import "../../../../components/ha-button";
import "../../../../components/ha-sortable";
import "../../../../components/ha-svg-icon";
import type { AutomationClipboard } from "../../../../data/automation";
import { Option } from "../../../../data/script";
import { HomeAssistant } from "../../../../types";
import "./ha-automation-option-row";
import type HaAutomationOptionRow from "./ha-automation-option-row";
@customElement("ha-automation-option")
export default class HaAutomationOption extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean }) public narrow = false;
@property({ type: Boolean }) public disabled = false;
@property({ attribute: false }) public options!: Option[];
@state() private _showReorder: boolean = false;
@storage({
key: "automationClipboard",
state: true,
subscribe: true,
storage: "sessionStorage",
})
public _clipboard?: AutomationClipboard;
private _focusLastOptionOnChange = false;
private _optionsKeys = new WeakMap<Option, string>();
private _unsubMql?: () => void;
public connectedCallback() {
super.connectedCallback();
this._unsubMql = listenMediaQuery("(min-width: 600px)", (matches) => {
this._showReorder = matches;
});
}
public disconnectedCallback() {
super.disconnectedCallback();
this._unsubMql?.();
this._unsubMql = undefined;
}
protected render() {
return html`
<ha-sortable
handle-selector=".handle"
draggable-selector="ha-automation-option-row"
.disabled=${!this._showReorder || this.disabled}
group="options"
invert-swap
@item-moved=${this._optionMoved}
@item-added=${this._optionAdded}
@item-removed=${this._optionRemoved}
>
<div class="options">
${repeat(
this.options,
(option) => this._getKey(option),
(option, idx) => html`
<ha-automation-option-row
.sortableData=${option}
.index=${idx}
.first=${idx === 0}
.last=${idx === this.options.length - 1}
.option=${option}
.narrow=${this.narrow}
.disabled=${this.disabled}
@duplicate=${this._duplicateOption}
@move-down=${this._moveDown}
@move-up=${this._moveUp}
@value-changed=${this._optionChanged}
.hass=${this.hass}
>
${this._showReorder && !this.disabled
? html`
<div class="handle" slot="icons">
<ha-svg-icon .path=${mdiDrag}></ha-svg-icon>
</div>
`
: nothing}
</ha-automation-option-row>
`
)}
<div class="buttons">
<ha-button
outlined
.disabled=${this.disabled}
.label=${this.hass.localize(
"ui.panel.config.automation.editor.actions.type.choose.add_option"
)}
@click=${this._addOption}
>
<ha-svg-icon .path=${mdiPlus} slot="icon"></ha-svg-icon>
</ha-button>
</div>
</div>
</ha-sortable>
`;
}
protected updated(changedProps: PropertyValues) {
super.updated(changedProps);
if (changedProps.has("options") && this._focusLastOptionOnChange) {
this._focusLastOptionOnChange = false;
const row = this.shadowRoot!.querySelector<HaAutomationOptionRow>(
"ha-automation-option-row:last-of-type"
)!;
row.updateComplete.then(() => {
row.expand();
row.scrollIntoView();
row.focus();
});
}
}
public expandAll() {
const rows = this.shadowRoot!.querySelectorAll<HaAutomationOptionRow>(
"ha-automation-option-row"
)!;
rows.forEach((row) => {
row.expand();
});
}
private _addOption = () => {
const options = this.options.concat({ conditions: [], sequence: [] });
this._focusLastOptionOnChange = true;
fireEvent(this, "value-changed", { value: options });
};
private _getKey(option: Option) {
if (!this._optionsKeys.has(option)) {
this._optionsKeys.set(option, Math.random().toString());
}
return this._optionsKeys.get(option)!;
}
private _moveUp(ev) {
ev.stopPropagation();
const index = (ev.target as any).index;
const newIndex = index - 1;
this._move(index, newIndex);
}
private _moveDown(ev) {
ev.stopPropagation();
const index = (ev.target as any).index;
const newIndex = index + 1;
this._move(index, newIndex);
}
private _move(oldIndex: number, newIndex: number) {
const options = this.options.concat();
const item = options.splice(oldIndex, 1)[0];
options.splice(newIndex, 0, item);
this.options = options;
fireEvent(this, "value-changed", { value: options });
}
private _optionMoved(ev: CustomEvent): void {
ev.stopPropagation();
const { oldIndex, newIndex } = ev.detail;
this._move(oldIndex, newIndex);
}
private async _optionAdded(ev: CustomEvent): Promise<void> {
ev.stopPropagation();
const { index, data } = ev.detail;
const options = [
...this.options.slice(0, index),
data,
...this.options.slice(index),
];
// Add option locally to avoid UI jump
this.options = options;
await nextRender();
fireEvent(this, "value-changed", { value: this.options });
}
private async _optionRemoved(ev: CustomEvent): Promise<void> {
ev.stopPropagation();
const { index } = ev.detail;
const option = this.options[index];
// Remove option locally to avoid UI jump
this.options = this.options.filter((o) => o !== option);
await nextRender();
// Ensure option is removed even after update
const options = this.options.filter((o) => o !== option);
fireEvent(this, "value-changed", { value: options });
}
private _optionChanged(ev: CustomEvent) {
ev.stopPropagation();
const options = [...this.options];
const newValue = ev.detail.value;
const index = (ev.target as any).index;
if (newValue === null) {
options.splice(index, 1);
} else {
// Store key on new value.
const key = this._getKey(options[index]);
this._optionsKeys.set(newValue, key);
options[index] = newValue;
}
fireEvent(this, "value-changed", { value: options });
}
private _duplicateOption(ev: CustomEvent) {
ev.stopPropagation();
const index = (ev.target as any).index;
fireEvent(this, "value-changed", {
value: this.options.concat(deepClone(this.options[index])),
});
}
static get styles(): CSSResultGroup {
return css`
.options {
padding: 16px;
margin: -16px;
display: flex;
flex-direction: column;
gap: 16px;
}
.sortable-ghost {
background: none;
border-radius: var(--ha-card-border-radius, 12px);
}
.sortable-drag {
background: none;
}
ha-automation-option-row {
display: block;
scroll-margin-top: 48px;
}
ha-svg-icon {
height: 20px;
}
.handle {
padding: 12px;
cursor: move; /* fallback if grab cursor is unsupported */
cursor: grab;
}
.handle ha-svg-icon {
pointer-events: none;
height: 24px;
}
.buttons {
display: flex;
flex-wrap: wrap;
gap: 8px;
order: 1;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-automation-option": HaAutomationOption;
}
}

View File

@ -58,7 +58,7 @@ import {
showPromptDialog, showPromptDialog,
} from "../../../../dialogs/generic/show-dialog-box"; } from "../../../../dialogs/generic/show-dialog-box";
import { haStyle } from "../../../../resources/styles"; import { haStyle } from "../../../../resources/styles";
import type { HomeAssistant, ItemPath } from "../../../../types"; import type { HomeAssistant } from "../../../../types";
import "./types/ha-automation-trigger-calendar"; import "./types/ha-automation-trigger-calendar";
import "./types/ha-automation-trigger-conversation"; import "./types/ha-automation-trigger-conversation";
import "./types/ha-automation-trigger-device"; import "./types/ha-automation-trigger-device";
@ -112,8 +112,6 @@ export default class HaAutomationTriggerRow extends LitElement {
@property({ type: Boolean }) public disabled = false; @property({ type: Boolean }) public disabled = false;
@property({ type: Array }) public path?: ItemPath;
@property({ type: Boolean }) public first?: boolean; @property({ type: Boolean }) public first?: boolean;
@property({ type: Boolean }) public last?: boolean; @property({ type: Boolean }) public last?: boolean;
@ -383,7 +381,6 @@ export default class HaAutomationTriggerRow extends LitElement {
hass: this.hass, hass: this.hass,
trigger: this.trigger, trigger: this.trigger,
disabled: this.disabled, disabled: this.disabled,
path: this.path,
})} })}
</div> </div>
`} `}

View File

@ -13,7 +13,7 @@ import { repeat } from "lit/directives/repeat";
import { storage } from "../../../../common/decorators/storage"; import { storage } from "../../../../common/decorators/storage";
import { fireEvent } from "../../../../common/dom/fire_event"; import { fireEvent } from "../../../../common/dom/fire_event";
import { listenMediaQuery } from "../../../../common/dom/media_query"; import { listenMediaQuery } from "../../../../common/dom/media_query";
import { nestedArrayMove } from "../../../../common/util/array-move"; import { nextRender } from "../../../../common/util/render-status";
import "../../../../components/ha-button"; import "../../../../components/ha-button";
import "../../../../components/ha-button-menu"; import "../../../../components/ha-button-menu";
import "../../../../components/ha-sortable"; import "../../../../components/ha-sortable";
@ -23,14 +23,14 @@ import {
Trigger, Trigger,
TriggerList, TriggerList,
} from "../../../../data/automation"; } from "../../../../data/automation";
import { HomeAssistant, ItemPath } from "../../../../types"; import { isTriggerList } from "../../../../data/trigger";
import { HomeAssistant } from "../../../../types";
import { import {
PASTE_VALUE, PASTE_VALUE,
showAddAutomationElementDialog, showAddAutomationElementDialog,
} from "../show-add-automation-element-dialog"; } from "../show-add-automation-element-dialog";
import "./ha-automation-trigger-row"; import "./ha-automation-trigger-row";
import type HaAutomationTriggerRow from "./ha-automation-trigger-row"; import type HaAutomationTriggerRow from "./ha-automation-trigger-row";
import { isTriggerList } from "../../../../data/trigger";
@customElement("ha-automation-trigger") @customElement("ha-automation-trigger")
export default class HaAutomationTrigger extends LitElement { export default class HaAutomationTrigger extends LitElement {
@ -40,8 +40,6 @@ export default class HaAutomationTrigger extends LitElement {
@property({ type: Boolean }) public disabled = false; @property({ type: Boolean }) public disabled = false;
@property({ type: Array }) public path?: ItemPath;
@state() private _showReorder: boolean = false; @state() private _showReorder: boolean = false;
@storage({ @storage({
@ -71,20 +69,17 @@ export default class HaAutomationTrigger extends LitElement {
this._unsubMql = undefined; this._unsubMql = undefined;
} }
private get nested() {
return this.path !== undefined;
}
protected render() { protected render() {
return html` return html`
<ha-sortable <ha-sortable
handle-selector=".handle" handle-selector=".handle"
draggable-selector="ha-automation-trigger-row" draggable-selector="ha-automation-trigger-row"
.disabled=${!this._showReorder || this.disabled} .disabled=${!this._showReorder || this.disabled}
@item-moved=${this._triggerMoved}
group="triggers" group="triggers"
.path=${this.path}
invert-swap invert-swap
@item-moved=${this._triggerMoved}
@item-added=${this._triggerAdded}
@item-removed=${this._triggerRemoved}
> >
<div class="triggers"> <div class="triggers">
${repeat( ${repeat(
@ -92,7 +87,7 @@ export default class HaAutomationTrigger extends LitElement {
(trigger) => this._getKey(trigger), (trigger) => this._getKey(trigger),
(trg, idx) => html` (trg, idx) => html`
<ha-automation-trigger-row <ha-automation-trigger-row
.path=${[...(this.path ?? []), idx]} .sortableData=${trg}
.index=${idx} .index=${idx}
.first=${idx === 0} .first=${idx === 0}
.last=${idx === this.triggers.length - 1} .last=${idx === this.triggers.length - 1}
@ -210,28 +205,44 @@ export default class HaAutomationTrigger extends LitElement {
this._move(index, newIndex); this._move(index, newIndex);
} }
private _move( private _move(oldIndex: number, newIndex: number) {
oldIndex: number, const triggers = this.triggers.concat();
newIndex: number, const item = triggers.splice(oldIndex, 1)[0];
oldPath?: ItemPath, triggers.splice(newIndex, 0, item);
newPath?: ItemPath this.triggers = triggers;
) {
const triggers = nestedArrayMove(
this.triggers,
oldIndex,
newIndex,
oldPath,
newPath
);
fireEvent(this, "value-changed", { value: triggers }); fireEvent(this, "value-changed", { value: triggers });
} }
private _triggerMoved(ev: CustomEvent): void { private _triggerMoved(ev: CustomEvent): void {
if (this.nested) return;
ev.stopPropagation(); ev.stopPropagation();
const { oldIndex, newIndex, oldPath, newPath } = ev.detail; const { oldIndex, newIndex } = ev.detail;
this._move(oldIndex, newIndex, oldPath, newPath); this._move(oldIndex, newIndex);
}
private async _triggerAdded(ev: CustomEvent): Promise<void> {
ev.stopPropagation();
const { index, data } = ev.detail;
const triggers = [
...this.triggers.slice(0, index),
data,
...this.triggers.slice(index),
];
// Add trigger locally to avoid UI jump
this.triggers = triggers;
await nextRender();
fireEvent(this, "value-changed", { value: this.triggers });
}
private async _triggerRemoved(ev: CustomEvent): Promise<void> {
ev.stopPropagation();
const { index } = ev.detail;
const trigger = this.triggers[index];
// Remove trigger locally to avoid UI jump
this.triggers = this.triggers.filter((t) => t !== trigger);
await nextRender();
// Ensure trigger is removed even after update
const triggers = this.triggers.filter((t) => t !== trigger);
fireEvent(this, "value-changed", { value: triggers });
} }
private _triggerChanged(ev: CustomEvent) { private _triggerChanged(ev: CustomEvent) {

View File

@ -2,7 +2,7 @@ import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { ensureArray } from "../../../../../common/array/ensure-array"; import { ensureArray } from "../../../../../common/array/ensure-array";
import type { TriggerList } from "../../../../../data/automation"; import type { TriggerList } from "../../../../../data/automation";
import type { HomeAssistant, ItemPath } from "../../../../../types"; import type { HomeAssistant } from "../../../../../types";
import "../ha-automation-trigger"; import "../ha-automation-trigger";
import { import {
handleChangeEvent, handleChangeEvent,
@ -15,8 +15,6 @@ export class HaTriggerList extends LitElement implements TriggerElement {
@property({ attribute: false }) public trigger!: TriggerList; @property({ attribute: false }) public trigger!: TriggerList;
@property({ attribute: false }) public path?: ItemPath;
@property({ type: Boolean }) public disabled = false; @property({ type: Boolean }) public disabled = false;
public static get defaultConfig(): TriggerList { public static get defaultConfig(): TriggerList {
@ -30,7 +28,6 @@ export class HaTriggerList extends LitElement implements TriggerElement {
return html` return html`
<ha-automation-trigger <ha-automation-trigger
.path=${[...(this.path ?? []), "triggers"]}
.triggers=${triggers} .triggers=${triggers}
.hass=${this.hass} .hass=${this.hass}
.disabled=${this.disabled} .disabled=${this.disabled}

View File

@ -2,7 +2,6 @@ import "@material/mwc-button/mwc-button";
import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event"; import { fireEvent } from "../../../common/dom/fire_event";
import { nestedArrayMove } from "../../../common/util/array-move";
import "../../../components/ha-blueprint-picker"; import "../../../components/ha-blueprint-picker";
import "../../../components/ha-card"; import "../../../components/ha-card";
import "../../../components/ha-circular-progress"; import "../../../components/ha-circular-progress";
@ -158,15 +157,6 @@ export abstract class HaBlueprintGenericEditor extends LitElement {
border: boolean border: boolean
) { ) {
const selector = value?.selector ?? { text: undefined }; const selector = value?.selector ?? { text: undefined };
const type = Object.keys(selector)[0];
const enhancedSelector = ["action", "condition", "trigger"].includes(type)
? {
[type]: {
...selector[type],
path: [key],
},
}
: selector;
return html`<ha-settings-row return html`<ha-settings-row
.narrow=${this.narrow} .narrow=${this.narrow}
class=${border ? "border" : ""} class=${border ? "border" : ""}
@ -180,7 +170,7 @@ export abstract class HaBlueprintGenericEditor extends LitElement {
></ha-markdown> ></ha-markdown>
${html`<ha-selector ${html`<ha-selector
.hass=${this.hass} .hass=${this.hass}
.selector=${enhancedSelector} .selector=${selector}
.key=${key} .key=${key}
.disabled=${this.disabled} .disabled=${this.disabled}
.required=${value?.default === undefined} .required=${value?.default === undefined}
@ -190,7 +180,6 @@ export abstract class HaBlueprintGenericEditor extends LitElement {
? this._config.use_blueprint.input[key] ? this._config.use_blueprint.input[key]
: value?.default} : value?.default}
@value-changed=${this._inputChanged} @value-changed=${this._inputChanged}
@item-moved=${this._itemMoved}
></ha-selector>`} ></ha-selector>`}
</ha-settings-row>`; </ha-settings-row>`;
} }
@ -237,29 +226,6 @@ export abstract class HaBlueprintGenericEditor extends LitElement {
}); });
} }
private _itemMoved(ev) {
ev.stopPropagation();
const { oldIndex, newIndex, oldPath, newPath } = ev.detail;
const input = nestedArrayMove(
this._config.use_blueprint.input,
oldIndex,
newIndex,
oldPath,
newPath
);
fireEvent(this, "value-changed", {
value: {
...this._config,
use_blueprint: {
...this._config.use_blueprint,
input,
},
},
});
}
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return [ return [
haStyle, haStyle,

View File

@ -15,7 +15,6 @@ import {
extractSearchParam, extractSearchParam,
removeSearchParam, removeSearchParam,
} from "../../../common/url/search-params"; } from "../../../common/url/search-params";
import { nestedArrayMove } from "../../../common/util/array-move";
import "../../../components/ha-card"; import "../../../components/ha-card";
import "../../../components/ha-icon-button"; import "../../../components/ha-icon-button";
import "../../../components/ha-markdown"; import "../../../components/ha-markdown";
@ -163,7 +162,6 @@ export class HaManualScriptEditor extends LitElement {
.actions=${this.config.sequence || []} .actions=${this.config.sequence || []}
.path=${["sequence"]} .path=${["sequence"]}
@value-changed=${this._sequenceChanged} @value-changed=${this._sequenceChanged}
@item-moved=${this._itemMoved}
.hass=${this.hass} .hass=${this.hass}
.narrow=${this.narrow} .narrow=${this.narrow}
.disabled=${this.disabled} .disabled=${this.disabled}
@ -185,21 +183,6 @@ export class HaManualScriptEditor extends LitElement {
}); });
} }
private _itemMoved(ev: CustomEvent): void {
ev.stopPropagation();
const { oldIndex, newIndex, oldPath, newPath } = ev.detail;
const updatedConfig = nestedArrayMove(
this.config,
oldIndex,
newIndex,
oldPath,
newPath
);
fireEvent(this, "value-changed", {
value: updatedConfig,
});
}
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return [ return [
haStyle, haStyle,

View File

@ -83,11 +83,11 @@ export class HuiViewBadges extends LitElement {
private _badgeMoved(ev) { private _badgeMoved(ev) {
ev.stopPropagation(); ev.stopPropagation();
const { oldIndex, newIndex, oldPath, newPath } = ev.detail; const { oldIndex, newIndex } = ev.detail;
const newConfig = moveBadge( const newConfig = moveBadge(
this.lovelace!.config, this.lovelace!.config,
[...oldPath, oldIndex] as [number, number, number], [this.viewIndex!, oldIndex],
[...newPath, newIndex] as [number, number, number] [this.viewIndex!, newIndex]
); );
this.lovelace!.saveConfig(newConfig); this.lovelace!.saveConfig(newConfig);
} }
@ -121,7 +121,6 @@ export class HuiViewBadges extends LitElement {
@drag-end=${this._dragEnd} @drag-end=${this._dragEnd}
group="badge" group="badge"
draggable-selector="[data-sortable]" draggable-selector="[data-sortable]"
.path=${[this.viewIndex]}
.rollback=${false} .rollback=${false}
.options=${BADGE_SORTABLE_OPTIONS} .options=${BADGE_SORTABLE_OPTIONS}
invert-swap invert-swap

View File

@ -4,8 +4,8 @@ import { property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map"; import { classMap } from "lit/directives/class-map";
import { repeat } from "lit/directives/repeat"; import { repeat } from "lit/directives/repeat";
import { styleMap } from "lit/directives/style-map"; import { styleMap } from "lit/directives/style-map";
import "../../../components/ha-ripple";
import { fireEvent } from "../../../common/dom/fire_event"; import { fireEvent } from "../../../common/dom/fire_event";
import "../../../components/ha-ripple";
import type { HaSortableOptions } from "../../../components/ha-sortable"; import type { HaSortableOptions } from "../../../components/ha-sortable";
import { LovelaceSectionElement } from "../../../data/lovelace"; import { LovelaceSectionElement } from "../../../data/lovelace";
import { LovelaceCardConfig } from "../../../data/lovelace/config/card"; import { LovelaceCardConfig } from "../../../data/lovelace/config/card";
@ -16,6 +16,7 @@ import { HuiCard } from "../cards/hui-card";
import { computeCardGridSize } from "../common/compute-card-grid-size"; import { computeCardGridSize } from "../common/compute-card-grid-size";
import "../components/hui-card-edit-mode"; import "../components/hui-card-edit-mode";
import { moveCard } from "../editor/config-util"; import { moveCard } from "../editor/config-util";
import { LovelaceCardPath } from "../editor/lovelace-path";
import type { Lovelace } from "../types"; import type { Lovelace } from "../types";
const CARD_SORTABLE_OPTIONS: HaSortableOptions = { const CARD_SORTABLE_OPTIONS: HaSortableOptions = {
@ -68,14 +69,15 @@ export class GridSection extends LitElement implements LovelaceSectionElement {
return html` return html`
<ha-sortable <ha-sortable
.disabled=${!editMode} .disabled=${!editMode}
@item-moved=${this._cardMoved}
@drag-start=${this._dragStart} @drag-start=${this._dragStart}
@drag-end=${this._dragEnd} @drag-end=${this._dragEnd}
group="card" group="card"
draggable-selector=".card" draggable-selector=".card"
.path=${[this.viewIndex, this.index]}
.rollback=${false} .rollback=${false}
.options=${CARD_SORTABLE_OPTIONS} .options=${CARD_SORTABLE_OPTIONS}
@item-moved=${this._cardMoved}
@item-added=${this._cardAdded}
@item-removed=${this._cardRemoved}
invert-swap invert-swap
> >
<div class="container ${classMap({ "edit-mode": editMode })}"> <div class="container ${classMap({ "edit-mode": editMode })}">
@ -89,6 +91,11 @@ export class GridSection extends LitElement implements LovelaceSectionElement {
const { rows, columns } = computeCardGridSize(layoutOptions); const { rows, columns } = computeCardGridSize(layoutOptions);
const cardPath: LovelaceCardPath = [
this.viewIndex!,
this.index!,
idx,
];
return html` return html`
<div <div
style=${styleMap({ style=${styleMap({
@ -100,13 +107,14 @@ export class GridSection extends LitElement implements LovelaceSectionElement {
"fit-rows": typeof layoutOptions?.grid_rows === "number", "fit-rows": typeof layoutOptions?.grid_rows === "number",
"full-width": columns === "full", "full-width": columns === "full",
})}" })}"
.sortableData=${cardPath}
> >
${editMode ${editMode
? html` ? html`
<hui-card-edit-mode <hui-card-edit-mode
.hass=${this.hass} .hass=${this.hass}
.lovelace=${this.lovelace} .lovelace=${this.lovelace!}
.path=${[this.viewIndex, this.index, idx]} .path=${cardPath}
.hiddenOverlay=${this._dragging} .hiddenOverlay=${this._dragging}
> >
${card} ${card}
@ -141,15 +149,28 @@ export class GridSection extends LitElement implements LovelaceSectionElement {
private _cardMoved(ev) { private _cardMoved(ev) {
ev.stopPropagation(); ev.stopPropagation();
const { oldIndex, newIndex, oldPath, newPath } = ev.detail; const { oldIndex, newIndex } = ev.detail;
const newConfig = moveCard( const newConfig = moveCard(
this.lovelace!.config, this.lovelace!.config,
[...oldPath, oldIndex] as [number, number, number], [this.viewIndex!, this.index!, oldIndex],
[...newPath, newIndex] as [number, number, number] [this.viewIndex!, this.index!, newIndex]
); );
this.lovelace!.saveConfig(newConfig); this.lovelace!.saveConfig(newConfig);
} }
private _cardAdded(ev) {
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);
this.lovelace!.saveConfig(newConfig);
}
private _cardRemoved(ev) {
ev.stopPropagation();
// Do nothing, it's handle by the "card-added" event from the new parent.
}
private _dragStart() { private _dragStart() {
this._dragging = true; this._dragging = true;
} }

View File

@ -175,8 +175,6 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
const rowSpan = sectionConfig?.row_span || 1; const rowSpan = sectionConfig?.row_span || 1;
(section as any).itemPath = [idx];
return html` return html`
<div <div
class="section" class="section"

View File

@ -307,5 +307,3 @@ export type AsyncReturnType<T extends (...args: any) => any> = T extends (
: never; : never;
export type Entries<T> = [keyof T, T[keyof T]][]; export type Entries<T> = [keyof T, T[keyof T]][];
export type ItemPath = (number | string)[];