mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-21 16:26:43 +00:00
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:
parent
51f89b00c1
commit
bc11c0b3ac
@ -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;
|
||||
}
|
@ -32,7 +32,6 @@ export class HaActionSelector extends LitElement {
|
||||
.disabled=${this.disabled}
|
||||
.actions=${this._actions(this.value)}
|
||||
.hass=${this.hass}
|
||||
.path=${this.selector.action?.path}
|
||||
></ha-automation-action>
|
||||
`;
|
||||
}
|
||||
|
@ -24,7 +24,6 @@ export class HaConditionSelector extends LitElement {
|
||||
.disabled=${this.disabled}
|
||||
.conditions=${this.value || []}
|
||||
.hass=${this.hass}
|
||||
.path=${this.selector.condition?.path}
|
||||
></ha-automation-condition>
|
||||
`;
|
||||
}
|
||||
|
@ -32,7 +32,6 @@ export class HaTriggerSelector extends LitElement {
|
||||
.disabled=${this.disabled}
|
||||
.triggers=${this._triggers(this.value)}
|
||||
.hass=${this.hass}
|
||||
.path=${this.selector.trigger?.path}
|
||||
></ha-automation-trigger>
|
||||
`;
|
||||
}
|
||||
|
@ -19,7 +19,6 @@ import { fireEvent } from "../common/dom/fire_event";
|
||||
import { computeDomain } from "../common/entity/compute_domain";
|
||||
import { computeObjectId } from "../common/entity/compute_object_id";
|
||||
import { supportsFeature } from "../common/entity/supports-feature";
|
||||
import { nestedArrayMove } from "../common/util/array-move";
|
||||
import {
|
||||
fetchIntegrationManifest,
|
||||
IntegrationManifest,
|
||||
@ -597,15 +596,6 @@ export class HaServiceControl extends LitElement {
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
@ -646,7 +636,7 @@ export class HaServiceControl extends LitElement {
|
||||
(!this._value?.data ||
|
||||
this._value.data[dataField.key] === undefined))}
|
||||
.hass=${this.hass}
|
||||
.selector=${enhancedSelector}
|
||||
.selector=${selector}
|
||||
.key=${dataField.key}
|
||||
@value-changed=${this._serviceDataChanged}
|
||||
.value=${this._value?.data
|
||||
@ -654,7 +644,6 @@ export class HaServiceControl extends LitElement {
|
||||
: undefined}
|
||||
.placeholder=${dataField.default}
|
||||
.localizeValue=${this._localizeValueCallback}
|
||||
@item-moved=${this._itemMoved}
|
||||
></ha-selector>
|
||||
</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) {
|
||||
ev.stopPropagation();
|
||||
if (!ev.detail.isValid) {
|
||||
|
@ -4,15 +4,19 @@ import { customElement, property } from "lit/decorators";
|
||||
import type { SortableEvent } from "sortablejs";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import type { SortableInstance } from "../resources/sortable";
|
||||
import { ItemPath } from "../types";
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
"item-moved": {
|
||||
oldIndex: number;
|
||||
newIndex: number;
|
||||
oldPath?: ItemPath;
|
||||
newPath?: ItemPath;
|
||||
};
|
||||
"item-added": {
|
||||
index: number;
|
||||
data: any;
|
||||
};
|
||||
"item-removed": {
|
||||
index: number;
|
||||
};
|
||||
"drag-start": undefined;
|
||||
"drag-end": undefined;
|
||||
@ -21,7 +25,7 @@ declare global {
|
||||
|
||||
export type HaSortableOptions = Omit<
|
||||
SortableInstance.SortableOptions,
|
||||
"onStart" | "onChoose" | "onEnd"
|
||||
"onStart" | "onChoose" | "onEnd" | "onUpdate" | "onAdd" | "onRemove"
|
||||
>;
|
||||
|
||||
@customElement("ha-sortable")
|
||||
@ -31,9 +35,6 @@ export class HaSortable extends LitElement {
|
||||
@property({ type: Boolean })
|
||||
public disabled = false;
|
||||
|
||||
@property({ type: Array })
|
||||
public path?: ItemPath;
|
||||
|
||||
@property({ type: Boolean, attribute: "no-style" })
|
||||
public noStyle: boolean = false;
|
||||
|
||||
@ -138,6 +139,9 @@ export class HaSortable extends LitElement {
|
||||
onChoose: this._handleChoose,
|
||||
onStart: this._handleStart,
|
||||
onEnd: this._handleEnd,
|
||||
onUpdate: this._handleUpdate,
|
||||
onAdd: this._handleAdd,
|
||||
onRemove: this._handleRemove,
|
||||
};
|
||||
|
||||
if (this.draggableSelector) {
|
||||
@ -159,33 +163,31 @@ export class HaSortable extends LitElement {
|
||||
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");
|
||||
// put back in original location
|
||||
if (this.rollback && (evt.item as any).placeholder) {
|
||||
(evt.item as any).placeholder.replaceWith(evt.item);
|
||||
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 = () => {
|
||||
|
@ -33,7 +33,7 @@ import { LabelRegistryEntry } from "../../data/label_registry";
|
||||
import { LogbookEntry } from "../../data/logbook";
|
||||
import {
|
||||
ChooseAction,
|
||||
ChooseActionChoice,
|
||||
Option,
|
||||
IfAction,
|
||||
ParallelAction,
|
||||
RepeatAction,
|
||||
@ -413,7 +413,7 @@ class ActionRenderer {
|
||||
: undefined;
|
||||
const choiceConfig = this._getDataFromPath(
|
||||
`${this.keys[index]}/choose/${chooseTrace.result.choice}`
|
||||
) as ChooseActionChoice | undefined;
|
||||
) as Option | undefined;
|
||||
const choiceName = choiceConfig
|
||||
? `${
|
||||
choiceConfig.alias ||
|
||||
|
@ -224,13 +224,14 @@ export interface ForEachRepeat extends BaseRepeat {
|
||||
for_each: string | any[];
|
||||
}
|
||||
|
||||
export interface ChooseActionChoice extends BaseAction {
|
||||
export interface Option {
|
||||
alias?: string;
|
||||
conditions: string | Condition[];
|
||||
sequence: Action | Action[];
|
||||
}
|
||||
|
||||
export interface ChooseAction extends BaseAction {
|
||||
choose: ChooseActionChoice | ChooseActionChoice[] | null;
|
||||
choose: Option | Option[] | null;
|
||||
default?: Action | Action[];
|
||||
}
|
||||
|
||||
|
@ -5,7 +5,7 @@ import { supportsFeature } from "../common/entity/supports-feature";
|
||||
import type { CropOptions } from "../dialogs/image-cropper-dialog/show-image-cropper-dialog";
|
||||
import { isHelperDomain } from "../panels/config/helpers/const";
|
||||
import { UiAction } from "../panels/lovelace/components/hui-action-editor";
|
||||
import { HomeAssistant, ItemPath } from "../types";
|
||||
import { HomeAssistant } from "../types";
|
||||
import {
|
||||
DeviceRegistryEntry,
|
||||
getDeviceIntegrationLookup,
|
||||
@ -68,9 +68,8 @@ export type Selector =
|
||||
| UiStateContentSelector;
|
||||
|
||||
export interface ActionSelector {
|
||||
action: {
|
||||
path?: ItemPath;
|
||||
} | null;
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
action: {} | null;
|
||||
}
|
||||
|
||||
export interface AddonSelector {
|
||||
@ -121,9 +120,8 @@ export interface ColorTempSelector {
|
||||
}
|
||||
|
||||
export interface ConditionSelector {
|
||||
condition: {
|
||||
path?: ItemPath;
|
||||
} | null;
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
condition: {} | null;
|
||||
}
|
||||
|
||||
export interface ConversationAgentSelector {
|
||||
@ -432,9 +430,8 @@ export interface TimeSelector {
|
||||
}
|
||||
|
||||
export interface TriggerSelector {
|
||||
trigger: {
|
||||
path?: ItemPath;
|
||||
} | null;
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
trigger: {} | null;
|
||||
}
|
||||
|
||||
export interface TTSSelector {
|
||||
|
@ -9,12 +9,13 @@ import {
|
||||
import {
|
||||
CSSResultGroup,
|
||||
LitElement,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
css,
|
||||
html,
|
||||
nothing,
|
||||
} from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { formatListWithAnds } from "../../../common/string/format-list";
|
||||
@ -49,7 +50,7 @@ import {
|
||||
} from "./show-dialog-area-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 };
|
||||
|
||||
@ -63,9 +64,11 @@ export class HaConfigAreasDashboard extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public route!: Route;
|
||||
|
||||
@state() private _areas: AreaRegistryEntry[] = [];
|
||||
|
||||
private _processAreas = memoizeOne(
|
||||
(
|
||||
areas: HomeAssistant["areas"],
|
||||
areas: AreaRegistryEntry[],
|
||||
devices: HomeAssistant["devices"],
|
||||
entities: HomeAssistant["entities"],
|
||||
floors: HomeAssistant["floors"]
|
||||
@ -99,8 +102,8 @@ export class HaConfigAreasDashboard extends LitElement {
|
||||
};
|
||||
};
|
||||
|
||||
const floorAreaLookup = getFloorAreaLookup(Object.values(areas));
|
||||
const unassisgnedAreas = Object.values(areas).filter(
|
||||
const floorAreaLookup = getFloorAreaLookup(areas);
|
||||
const unassignedAreas = areas.filter(
|
||||
(area) => !area.floor_id || !floorAreaLookup[area.floor_id]
|
||||
);
|
||||
return {
|
||||
@ -108,11 +111,21 @@ export class HaConfigAreasDashboard extends LitElement {
|
||||
...floor,
|
||||
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 {
|
||||
const areasAndFloors =
|
||||
!this.hass.areas ||
|
||||
@ -121,7 +134,7 @@ export class HaConfigAreasDashboard extends LitElement {
|
||||
!this.hass.floors
|
||||
? undefined
|
||||
: this._processAreas(
|
||||
this.hass.areas,
|
||||
this._areas,
|
||||
this.hass.devices,
|
||||
this.hass.entities,
|
||||
this.hass.floors
|
||||
@ -183,10 +196,10 @@ export class HaConfigAreasDashboard extends LitElement {
|
||||
<ha-sortable
|
||||
handle-selector="a"
|
||||
draggable-selector="a"
|
||||
@item-moved=${this._areaMoved}
|
||||
@item-added=${this._areaAdded}
|
||||
group="floor"
|
||||
.options=${SORT_OPTIONS}
|
||||
.path=${[floor.floor_id]}
|
||||
.floor=${floor.floor_id}
|
||||
>
|
||||
<div class="areas">
|
||||
${floor.areas.map((area) => this._renderArea(area))}
|
||||
@ -194,7 +207,7 @@ export class HaConfigAreasDashboard extends LitElement {
|
||||
</ha-sortable>
|
||||
</div>`
|
||||
)}
|
||||
${areasAndFloors?.unassisgnedAreas.length
|
||||
${areasAndFloors?.unassignedAreas.length
|
||||
? html`<div class="floor">
|
||||
<div class="header">
|
||||
<h2>
|
||||
@ -206,13 +219,13 @@ export class HaConfigAreasDashboard extends LitElement {
|
||||
<ha-sortable
|
||||
handle-selector="a"
|
||||
draggable-selector="a"
|
||||
@item-moved=${this._areaMoved}
|
||||
@item-added=${this._areaAdded}
|
||||
group="floor"
|
||||
.options=${SORT_OPTIONS}
|
||||
.path=${UNASSIGNED_PATH}
|
||||
.floor=${UNASSIGNED_FLOOR}
|
||||
>
|
||||
<div class="areas">
|
||||
${areasAndFloors?.unassisgnedAreas.map((area) =>
|
||||
${areasAndFloors?.unassignedAreas.map((area) =>
|
||||
this._renderArea(area)
|
||||
)}
|
||||
</div>
|
||||
@ -246,7 +259,10 @@ export class HaConfigAreasDashboard extends LitElement {
|
||||
}
|
||||
|
||||
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>
|
||||
<div
|
||||
style=${styleMap({
|
||||
@ -309,26 +325,23 @@ export class HaConfigAreasDashboard extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private async _areaMoved(ev) {
|
||||
const areasAndFloors = this._processAreas(
|
||||
this.hass.areas,
|
||||
this.hass.devices,
|
||||
this.hass.entities,
|
||||
this.hass.floors
|
||||
);
|
||||
let area: AreaRegistryEntry;
|
||||
if (ev.detail.oldPath === UNASSIGNED_PATH) {
|
||||
area = areasAndFloors.unassisgnedAreas[ev.detail.oldIndex];
|
||||
} else {
|
||||
const oldFloor = areasAndFloors.floors!.find(
|
||||
(floor) => floor.floor_id === ev.detail.oldPath[0]
|
||||
);
|
||||
area = oldFloor!.areas[ev.detail.oldIndex];
|
||||
}
|
||||
private async _areaAdded(ev) {
|
||||
ev.stopPropagation();
|
||||
const { floor } = ev.currentTarget;
|
||||
|
||||
const newFloorId = floor === UNASSIGNED_FLOOR ? null : floor;
|
||||
|
||||
const { data: area } = ev.detail;
|
||||
|
||||
this._areas = this._areas.map<AreaRegistryEntry>((a) => {
|
||||
if (a.area_id === area.area_id) {
|
||||
return { ...a, floor_id: newFloorId };
|
||||
}
|
||||
return a;
|
||||
});
|
||||
|
||||
await updateAreaRegistryEntry(this.hass, area.area_id, {
|
||||
floor_id:
|
||||
ev.detail.newPath === UNASSIGNED_PATH ? null : ev.detail.newPath[0],
|
||||
floor_id: newFloorId,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -65,7 +65,7 @@ import {
|
||||
showPromptDialog,
|
||||
} from "../../../../dialogs/generic/show-dialog-box";
|
||||
import { haStyle } from "../../../../resources/styles";
|
||||
import type { HomeAssistant, ItemPath } from "../../../../types";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import { showToast } from "../../../../util/toast";
|
||||
import "./types/ha-automation-action-activate_scene";
|
||||
import "./types/ha-automation-action-choose";
|
||||
@ -137,8 +137,6 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ type: Array }) public path?: ItemPath;
|
||||
|
||||
@property({ type: Boolean }) public first?: boolean;
|
||||
|
||||
@property({ type: Boolean }) public last?: boolean;
|
||||
@ -432,7 +430,6 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
action: this.action,
|
||||
narrow: this.narrow,
|
||||
disabled: this.disabled,
|
||||
path: this.path,
|
||||
})}
|
||||
</div>
|
||||
`}
|
||||
|
@ -13,14 +13,14 @@ 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 { nestedArrayMove } from "../../../../common/util/array-move";
|
||||
import { nextRender } from "../../../../common/util/render-status";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-sortable";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import { getService, isService } from "../../../../data/action";
|
||||
import type { AutomationClipboard } from "../../../../data/automation";
|
||||
import { Action } from "../../../../data/script";
|
||||
import { HomeAssistant, ItemPath } from "../../../../types";
|
||||
import { HomeAssistant } from "../../../../types";
|
||||
import {
|
||||
PASTE_VALUE,
|
||||
showAddAutomationElementDialog,
|
||||
@ -36,8 +36,6 @@ export default class HaAutomationAction extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ type: Array }) public path?: ItemPath;
|
||||
|
||||
@property({ attribute: false }) public actions!: Action[];
|
||||
|
||||
@state() private _showReorder: boolean = false;
|
||||
@ -69,20 +67,17 @@ export default class HaAutomationAction extends LitElement {
|
||||
this._unsubMql = undefined;
|
||||
}
|
||||
|
||||
private get nested() {
|
||||
return this.path !== undefined;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-sortable
|
||||
handle-selector=".handle"
|
||||
draggable-selector="ha-automation-action-row"
|
||||
.disabled=${!this._showReorder || this.disabled}
|
||||
@item-moved=${this._actionMoved}
|
||||
group="actions"
|
||||
.path=${this.path}
|
||||
invert-swap
|
||||
@item-moved=${this._actionMoved}
|
||||
@item-added=${this._actionAdded}
|
||||
@item-removed=${this._actionRemoved}
|
||||
>
|
||||
<div class="actions">
|
||||
${repeat(
|
||||
@ -90,7 +85,7 @@ export default class HaAutomationAction extends LitElement {
|
||||
(action) => this._getKey(action),
|
||||
(action, idx) => html`
|
||||
<ha-automation-action-row
|
||||
.path=${[...(this.path ?? []), idx]}
|
||||
.sortableData=${action}
|
||||
.index=${idx}
|
||||
.first=${idx === 0}
|
||||
.last=${idx === this.actions.length - 1}
|
||||
@ -225,28 +220,44 @@ export default class HaAutomationAction extends LitElement {
|
||||
this._move(index, newIndex);
|
||||
}
|
||||
|
||||
private _move(
|
||||
oldIndex: number,
|
||||
newIndex: number,
|
||||
oldPath?: ItemPath,
|
||||
newPath?: ItemPath
|
||||
) {
|
||||
const actions = nestedArrayMove(
|
||||
this.actions,
|
||||
oldIndex,
|
||||
newIndex,
|
||||
oldPath,
|
||||
newPath
|
||||
);
|
||||
|
||||
private _move(oldIndex: number, newIndex: number) {
|
||||
const actions = this.actions.concat();
|
||||
const item = actions.splice(oldIndex, 1)[0];
|
||||
actions.splice(newIndex, 0, item);
|
||||
this.actions = actions;
|
||||
fireEvent(this, "value-changed", { value: actions });
|
||||
}
|
||||
|
||||
private _actionMoved(ev: CustomEvent): void {
|
||||
if (this.nested) return;
|
||||
ev.stopPropagation();
|
||||
const { oldIndex, newIndex, oldPath, newPath } = ev.detail;
|
||||
this._move(oldIndex, newIndex, oldPath, newPath);
|
||||
const { oldIndex, newIndex } = ev.detail;
|
||||
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) {
|
||||
|
@ -1,299 +1,40 @@
|
||||
import { consume } from "@lit-labs/context";
|
||||
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 { CSSResultGroup, LitElement, css, html } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import { ensureArray } from "../../../../../common/array/ensure-array";
|
||||
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-menu";
|
||||
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 { Action, ChooseAction, Option } from "../../../../../data/script";
|
||||
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";
|
||||
|
||||
const preventDefault = (ev) => ev.preventDefault();
|
||||
|
||||
@customElement("ha-automation-action-choose")
|
||||
export class HaChooseAction extends LitElement implements ActionElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ attribute: false }) public path?: ItemPath;
|
||||
|
||||
@property({ attribute: false }) public action!: ChooseAction;
|
||||
|
||||
@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 {
|
||||
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() {
|
||||
const action = this.action;
|
||||
|
||||
const options = action.choose ? ensureArray(action.choose) : [];
|
||||
|
||||
return html`
|
||||
<ha-sortable
|
||||
handle-selector=".handle"
|
||||
draggable-selector=".option"
|
||||
.disabled=${!this._showReorder || this.disabled}
|
||||
group="choose-options"
|
||||
.path=${[...(this.path ?? []), "choose"]}
|
||||
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>
|
||||
<ha-automation-option
|
||||
.options=${options}
|
||||
.disabled=${this.disabled}
|
||||
@value-changed=${this._optionsChanged}
|
||||
.hass=${this.hass}
|
||||
></ha-automation-option>
|
||||
|
||||
${this._showDefault || action.default
|
||||
? html`
|
||||
@ -303,190 +44,39 @@ export class HaChooseAction extends LitElement implements ActionElement {
|
||||
)}:
|
||||
</h2>
|
||||
<ha-automation-action
|
||||
.path=${[...(this.path ?? []), "default"]}
|
||||
.actions=${ensureArray(action.default) || []}
|
||||
.disabled=${this.disabled}
|
||||
@value-changed=${this._defaultChanged}
|
||||
.hass=${this.hass}
|
||||
></ha-automation-action>
|
||||
`
|
||||
: html`<div class="link-button-row">
|
||||
<button
|
||||
class="link"
|
||||
@click=${this._addDefault}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.type.choose.add_default"
|
||||
)}
|
||||
</button>
|
||||
</div>`}
|
||||
: html`
|
||||
<div class="link-button-row">
|
||||
<button
|
||||
class="link"
|
||||
@click=${this._addDefault}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.type.choose.add_default"
|
||||
)}
|
||||
</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() {
|
||||
this._showDefault = true;
|
||||
}
|
||||
|
||||
private _conditionChanged(ev: CustomEvent) {
|
||||
private _optionsChanged(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
const value = ev.detail.value as Condition[];
|
||||
const index = (ev.target as any).idx;
|
||||
const choose = this.action.choose
|
||||
? [...ensureArray(this.action.choose)]
|
||||
: [];
|
||||
choose[index].conditions = value;
|
||||
const value = ev.detail.value as Option[];
|
||||
fireEvent(this, "value-changed", {
|
||||
value: { ...this.action, choose },
|
||||
});
|
||||
}
|
||||
|
||||
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 },
|
||||
});
|
||||
value: {
|
||||
...this.action,
|
||||
choose: value,
|
||||
},
|
||||
});
|
||||
}
|
||||
@ -509,68 +99,9 @@ export class HaChooseAction extends LitElement implements ActionElement {
|
||||
return [
|
||||
haStyle,
|
||||
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 {
|
||||
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;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@ -4,7 +4,7 @@ import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import "../../../../../components/ha-textfield";
|
||||
import { Action, IfAction } from "../../../../../data/script";
|
||||
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 "../ha-automation-action";
|
||||
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({ attribute: false }) public path?: ItemPath;
|
||||
|
||||
@property({ attribute: false }) public action!: IfAction;
|
||||
|
||||
@state() private _showElse = false;
|
||||
@ -38,7 +36,6 @@ export class HaIfAction extends LitElement implements ActionElement {
|
||||
)}*:
|
||||
</h3>
|
||||
<ha-automation-condition
|
||||
.path=${[...(this.path ?? []), "if"]}
|
||||
.conditions=${action.if}
|
||||
.disabled=${this.disabled}
|
||||
@value-changed=${this._ifChanged}
|
||||
@ -51,7 +48,6 @@ export class HaIfAction extends LitElement implements ActionElement {
|
||||
)}*:
|
||||
</h3>
|
||||
<ha-automation-action
|
||||
.path=${[...(this.path ?? []), "then"]}
|
||||
.actions=${action.then}
|
||||
.disabled=${this.disabled}
|
||||
@value-changed=${this._thenChanged}
|
||||
@ -65,7 +61,6 @@ export class HaIfAction extends LitElement implements ActionElement {
|
||||
)}:
|
||||
</h3>
|
||||
<ha-automation-action
|
||||
.path=${[...(this.path ?? []), "else"]}
|
||||
.actions=${action.else || []}
|
||||
.disabled=${this.disabled}
|
||||
@value-changed=${this._elseChanged}
|
||||
|
@ -4,7 +4,7 @@ import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import "../../../../../components/ha-textfield";
|
||||
import { Action, ParallelAction } from "../../../../../data/script";
|
||||
import { haStyle } from "../../../../../resources/styles";
|
||||
import type { HomeAssistant, ItemPath } from "../../../../../types";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
import "../ha-automation-action";
|
||||
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({ attribute: false }) public path?: ItemPath;
|
||||
|
||||
@property({ attribute: false }) public action!: ParallelAction;
|
||||
|
||||
public static get defaultConfig(): ParallelAction {
|
||||
@ -29,7 +27,6 @@ export class HaParallelAction extends LitElement implements ActionElement {
|
||||
|
||||
return html`
|
||||
<ha-automation-action
|
||||
.path=${[...(this.path ?? []), "parallel"]}
|
||||
.actions=${action.parallel}
|
||||
.disabled=${this.disabled}
|
||||
@value-changed=${this._actionsChanged}
|
||||
|
@ -5,7 +5,7 @@ import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import "../../../../../components/ha-textfield";
|
||||
import { RepeatAction } from "../../../../../data/script";
|
||||
import { haStyle } from "../../../../../resources/styles";
|
||||
import type { HomeAssistant, ItemPath } from "../../../../../types";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
import "../ha-automation-action";
|
||||
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({ type: Array }) public path?: ItemPath;
|
||||
|
||||
public static get defaultConfig(): RepeatAction {
|
||||
return { repeat: { count: 2, sequence: [] } };
|
||||
}
|
||||
|
||||
private _schema = memoizeOne(
|
||||
(
|
||||
localize: LocalizeFunc,
|
||||
type: string,
|
||||
template: boolean,
|
||||
path?: ItemPath
|
||||
) =>
|
||||
(localize: LocalizeFunc, type: string, template: boolean) =>
|
||||
[
|
||||
{
|
||||
name: "type",
|
||||
@ -73,9 +66,7 @@ export class HaRepeatAction extends LitElement implements ActionElement {
|
||||
{
|
||||
name: type,
|
||||
selector: {
|
||||
condition: {
|
||||
path: [...(path ?? []), "repeat", type],
|
||||
},
|
||||
condition: {},
|
||||
},
|
||||
},
|
||||
] as const satisfies readonly HaFormSchema[])
|
||||
@ -92,9 +83,7 @@ export class HaRepeatAction extends LitElement implements ActionElement {
|
||||
{
|
||||
name: "sequence",
|
||||
selector: {
|
||||
action: {
|
||||
path: [...(path ?? []), "repeat", "sequence"],
|
||||
},
|
||||
action: {},
|
||||
},
|
||||
},
|
||||
] as const satisfies readonly HaFormSchema[]
|
||||
@ -108,8 +97,7 @@ export class HaRepeatAction extends LitElement implements ActionElement {
|
||||
type ?? "count",
|
||||
"count" in action && typeof action.count === "string"
|
||||
? isTemplate(action.count)
|
||||
: false,
|
||||
this.path
|
||||
: false
|
||||
);
|
||||
|
||||
const data = { ...action, type };
|
||||
|
@ -1,11 +1,10 @@
|
||||
import { CSSResultGroup, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import "../../../../../components/ha-textfield";
|
||||
import { Action, SequenceAction } from "../../../../../data/script";
|
||||
import { haStyle } from "../../../../../resources/styles";
|
||||
import type { HomeAssistant, ItemPath } from "../../../../../types";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
import "../ha-automation-action";
|
||||
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({ attribute: false }) public path?: ItemPath;
|
||||
|
||||
@property({ attribute: false }) public action!: 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() {
|
||||
const { action } = this;
|
||||
|
||||
return html`
|
||||
<ha-automation-action
|
||||
.path=${this._getMemoizedPath(this.path)}
|
||||
.actions=${action.sequence}
|
||||
.disabled=${this.disabled}
|
||||
@value-changed=${this._actionsChanged}
|
||||
|
@ -8,7 +8,7 @@ import "../../../../../components/ha-duration-input";
|
||||
import "../../../../../components/ha-formfield";
|
||||
import "../../../../../components/ha-textfield";
|
||||
import { WaitForTriggerAction } from "../../../../../data/script";
|
||||
import { HomeAssistant, ItemPath } from "../../../../../types";
|
||||
import { HomeAssistant } from "../../../../../types";
|
||||
import "../../trigger/ha-automation-trigger";
|
||||
import { ActionElement, handleChangeEvent } from "../ha-automation-action-row";
|
||||
|
||||
@ -23,8 +23,6 @@ export class HaWaitForTriggerAction
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ attribute: false }) public path?: ItemPath;
|
||||
|
||||
public static get defaultConfig(): WaitForTriggerAction {
|
||||
return { wait_for_trigger: [] };
|
||||
}
|
||||
@ -55,7 +53,6 @@ export class HaWaitForTriggerAction
|
||||
></ha-switch>
|
||||
</ha-formfield>
|
||||
<ha-automation-trigger
|
||||
.path=${[...(this.path ?? []), "wait_for_trigger"]}
|
||||
.triggers=${ensureArray(this.action.wait_for_trigger)}
|
||||
.hass=${this.hass}
|
||||
.disabled=${this.disabled}
|
||||
|
@ -7,7 +7,7 @@ import "../../../../components/ha-yaml-editor";
|
||||
import type { Condition } from "../../../../data/automation";
|
||||
import { expandConditionWithShorthand } from "../../../../data/automation";
|
||||
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-device";
|
||||
import "./types/ha-automation-condition-not";
|
||||
@ -30,8 +30,6 @@ export default class HaAutomationConditionEditor extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public yamlMode = false;
|
||||
|
||||
@property({ type: Array }) public path?: ItemPath;
|
||||
|
||||
private _processedCondition = memoizeOne((condition) =>
|
||||
expandConditionWithShorthand(condition)
|
||||
);
|
||||
@ -68,7 +66,6 @@ export default class HaAutomationConditionEditor extends LitElement {
|
||||
hass: this.hass,
|
||||
condition: condition,
|
||||
disabled: this.disabled,
|
||||
path: this.path,
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
|
@ -41,7 +41,7 @@ import {
|
||||
showPromptDialog,
|
||||
} from "../../../../dialogs/generic/show-dialog-box";
|
||||
import { haStyle } from "../../../../resources/styles";
|
||||
import { HomeAssistant, ItemPath } from "../../../../types";
|
||||
import { HomeAssistant } from "../../../../types";
|
||||
import "./ha-automation-condition-editor";
|
||||
|
||||
export interface ConditionElement extends LitElement {
|
||||
@ -83,8 +83,6 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ type: Array }) public path?: ItemPath;
|
||||
|
||||
@property({ type: Boolean }) public first?: boolean;
|
||||
|
||||
@property({ type: Boolean }) public last?: boolean;
|
||||
@ -302,7 +300,6 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
.disabled=${this.disabled}
|
||||
.hass=${this.hass}
|
||||
.condition=${this.condition}
|
||||
.path=${this.path}
|
||||
></ha-automation-condition-editor>
|
||||
</div>
|
||||
</ha-expansion-panel>
|
||||
|
@ -13,7 +13,7 @@ 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 { nestedArrayMove } from "../../../../common/util/array-move";
|
||||
import { nextRender } from "../../../../common/util/render-status";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-button-menu";
|
||||
import "../../../../components/ha-sortable";
|
||||
@ -22,7 +22,7 @@ import type {
|
||||
AutomationClipboard,
|
||||
Condition,
|
||||
} from "../../../../data/automation";
|
||||
import type { HomeAssistant, ItemPath } from "../../../../types";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import {
|
||||
PASTE_VALUE,
|
||||
showAddAutomationElementDialog,
|
||||
@ -38,8 +38,6 @@ export default class HaAutomationCondition extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ type: Array }) public path?: ItemPath;
|
||||
|
||||
@state() private _showReorder: boolean = false;
|
||||
|
||||
@storage({
|
||||
@ -115,10 +113,6 @@ export default class HaAutomationCondition extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private get nested() {
|
||||
return this.path !== undefined;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!Array.isArray(this.conditions)) {
|
||||
return nothing;
|
||||
@ -128,10 +122,11 @@ export default class HaAutomationCondition extends LitElement {
|
||||
handle-selector=".handle"
|
||||
draggable-selector="ha-automation-condition-row"
|
||||
.disabled=${!this._showReorder || this.disabled}
|
||||
@item-moved=${this._conditionMoved}
|
||||
group="conditions"
|
||||
.path=${this.path}
|
||||
invert-swap
|
||||
@item-moved=${this._conditionMoved}
|
||||
@item-added=${this._conditionAdded}
|
||||
@item-removed=${this._conditionRemoved}
|
||||
>
|
||||
<div class="conditions">
|
||||
${repeat(
|
||||
@ -139,7 +134,7 @@ export default class HaAutomationCondition extends LitElement {
|
||||
(condition) => this._getKey(condition),
|
||||
(cond, idx) => html`
|
||||
<ha-automation-condition-row
|
||||
.path=${[...(this.path ?? []), idx]}
|
||||
.sortableData=${cond}
|
||||
.index=${idx}
|
||||
.first=${idx === 0}
|
||||
.last=${idx === this.conditions.length - 1}
|
||||
@ -248,28 +243,44 @@ export default class HaAutomationCondition extends LitElement {
|
||||
this._move(index, newIndex);
|
||||
}
|
||||
|
||||
private _move(
|
||||
oldIndex: number,
|
||||
newIndex: number,
|
||||
oldPath?: ItemPath,
|
||||
newPath?: ItemPath
|
||||
) {
|
||||
const conditions = nestedArrayMove(
|
||||
this.conditions,
|
||||
oldIndex,
|
||||
newIndex,
|
||||
oldPath,
|
||||
newPath
|
||||
);
|
||||
|
||||
private _move(oldIndex: number, newIndex: number) {
|
||||
const conditions = this.conditions.concat();
|
||||
const item = conditions.splice(oldIndex, 1)[0];
|
||||
conditions.splice(newIndex, 0, item);
|
||||
this.conditions = conditions;
|
||||
fireEvent(this, "value-changed", { value: conditions });
|
||||
}
|
||||
|
||||
private _conditionMoved(ev: CustomEvent): void {
|
||||
if (this.nested) return;
|
||||
ev.stopPropagation();
|
||||
const { oldIndex, newIndex, oldPath, newPath } = ev.detail;
|
||||
this._move(oldIndex, newIndex, oldPath, newPath);
|
||||
const { oldIndex, newIndex } = ev.detail;
|
||||
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) {
|
||||
|
@ -2,7 +2,7 @@ import { html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import type { LogicalCondition } from "../../../../../data/automation";
|
||||
import type { HomeAssistant, ItemPath } from "../../../../../types";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
import "../ha-automation-condition";
|
||||
import type { ConditionElement } from "../ha-automation-condition-row";
|
||||
|
||||
@ -17,12 +17,9 @@ export abstract class HaLogicalCondition
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ attribute: false }) public path?: ItemPath;
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-automation-condition
|
||||
.path=${[...(this.path ?? []), "conditions"]}
|
||||
.conditions=${this.condition.conditions || []}
|
||||
@value-changed=${this._valueChanged}
|
||||
.hass=${this.hass}
|
||||
|
@ -12,7 +12,6 @@ import {
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { ensureArray } from "../../../common/array/ensure-array";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { nestedArrayMove } from "../../../common/util/array-move";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-markdown";
|
||||
@ -132,7 +131,6 @@ export class HaManualAutomationEditor extends LitElement {
|
||||
.triggers=${this.config.triggers || []}
|
||||
.path=${["triggers"]}
|
||||
@value-changed=${this._triggerChanged}
|
||||
@item-moved=${this._itemMoved}
|
||||
.hass=${this.hass}
|
||||
.disabled=${this.disabled}
|
||||
></ha-automation-trigger>
|
||||
@ -174,7 +172,6 @@ export class HaManualAutomationEditor extends LitElement {
|
||||
.conditions=${this.config.conditions || []}
|
||||
.path=${["conditions"]}
|
||||
@value-changed=${this._conditionChanged}
|
||||
@item-moved=${this._itemMoved}
|
||||
.hass=${this.hass}
|
||||
.disabled=${this.disabled}
|
||||
></ha-automation-condition>
|
||||
@ -214,7 +211,6 @@ export class HaManualAutomationEditor extends LitElement {
|
||||
.actions=${this.config.actions || []}
|
||||
.path=${["actions"]}
|
||||
@value-changed=${this._actionChanged}
|
||||
@item-moved=${this._itemMoved}
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.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> {
|
||||
if (!this.hass || !this.stateObj) {
|
||||
return;
|
||||
|
335
src/panels/config/automation/option/ha-automation-option-row.ts
Normal file
335
src/panels/config/automation/option/ha-automation-option-row.ts
Normal 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;
|
||||
}
|
||||
}
|
290
src/panels/config/automation/option/ha-automation-option.ts
Normal file
290
src/panels/config/automation/option/ha-automation-option.ts
Normal 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;
|
||||
}
|
||||
}
|
@ -58,7 +58,7 @@ import {
|
||||
showPromptDialog,
|
||||
} from "../../../../dialogs/generic/show-dialog-box";
|
||||
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-conversation";
|
||||
import "./types/ha-automation-trigger-device";
|
||||
@ -112,8 +112,6 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ type: Array }) public path?: ItemPath;
|
||||
|
||||
@property({ type: Boolean }) public first?: boolean;
|
||||
|
||||
@property({ type: Boolean }) public last?: boolean;
|
||||
@ -383,7 +381,6 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
hass: this.hass,
|
||||
trigger: this.trigger,
|
||||
disabled: this.disabled,
|
||||
path: this.path,
|
||||
})}
|
||||
</div>
|
||||
`}
|
||||
|
@ -13,7 +13,7 @@ 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 { nestedArrayMove } from "../../../../common/util/array-move";
|
||||
import { nextRender } from "../../../../common/util/render-status";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-button-menu";
|
||||
import "../../../../components/ha-sortable";
|
||||
@ -23,14 +23,14 @@ import {
|
||||
Trigger,
|
||||
TriggerList,
|
||||
} from "../../../../data/automation";
|
||||
import { HomeAssistant, ItemPath } from "../../../../types";
|
||||
import { isTriggerList } from "../../../../data/trigger";
|
||||
import { HomeAssistant } from "../../../../types";
|
||||
import {
|
||||
PASTE_VALUE,
|
||||
showAddAutomationElementDialog,
|
||||
} from "../show-add-automation-element-dialog";
|
||||
import "./ha-automation-trigger-row";
|
||||
import type HaAutomationTriggerRow from "./ha-automation-trigger-row";
|
||||
import { isTriggerList } from "../../../../data/trigger";
|
||||
|
||||
@customElement("ha-automation-trigger")
|
||||
export default class HaAutomationTrigger extends LitElement {
|
||||
@ -40,8 +40,6 @@ export default class HaAutomationTrigger extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ type: Array }) public path?: ItemPath;
|
||||
|
||||
@state() private _showReorder: boolean = false;
|
||||
|
||||
@storage({
|
||||
@ -71,20 +69,17 @@ export default class HaAutomationTrigger extends LitElement {
|
||||
this._unsubMql = undefined;
|
||||
}
|
||||
|
||||
private get nested() {
|
||||
return this.path !== undefined;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-sortable
|
||||
handle-selector=".handle"
|
||||
draggable-selector="ha-automation-trigger-row"
|
||||
.disabled=${!this._showReorder || this.disabled}
|
||||
@item-moved=${this._triggerMoved}
|
||||
group="triggers"
|
||||
.path=${this.path}
|
||||
invert-swap
|
||||
@item-moved=${this._triggerMoved}
|
||||
@item-added=${this._triggerAdded}
|
||||
@item-removed=${this._triggerRemoved}
|
||||
>
|
||||
<div class="triggers">
|
||||
${repeat(
|
||||
@ -92,7 +87,7 @@ export default class HaAutomationTrigger extends LitElement {
|
||||
(trigger) => this._getKey(trigger),
|
||||
(trg, idx) => html`
|
||||
<ha-automation-trigger-row
|
||||
.path=${[...(this.path ?? []), idx]}
|
||||
.sortableData=${trg}
|
||||
.index=${idx}
|
||||
.first=${idx === 0}
|
||||
.last=${idx === this.triggers.length - 1}
|
||||
@ -210,28 +205,44 @@ export default class HaAutomationTrigger extends LitElement {
|
||||
this._move(index, newIndex);
|
||||
}
|
||||
|
||||
private _move(
|
||||
oldIndex: number,
|
||||
newIndex: number,
|
||||
oldPath?: ItemPath,
|
||||
newPath?: ItemPath
|
||||
) {
|
||||
const triggers = nestedArrayMove(
|
||||
this.triggers,
|
||||
oldIndex,
|
||||
newIndex,
|
||||
oldPath,
|
||||
newPath
|
||||
);
|
||||
|
||||
private _move(oldIndex: number, newIndex: number) {
|
||||
const triggers = this.triggers.concat();
|
||||
const item = triggers.splice(oldIndex, 1)[0];
|
||||
triggers.splice(newIndex, 0, item);
|
||||
this.triggers = triggers;
|
||||
fireEvent(this, "value-changed", { value: triggers });
|
||||
}
|
||||
|
||||
private _triggerMoved(ev: CustomEvent): void {
|
||||
if (this.nested) return;
|
||||
ev.stopPropagation();
|
||||
const { oldIndex, newIndex, oldPath, newPath } = ev.detail;
|
||||
this._move(oldIndex, newIndex, oldPath, newPath);
|
||||
const { oldIndex, newIndex } = ev.detail;
|
||||
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) {
|
||||
|
@ -2,7 +2,7 @@ import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { ensureArray } from "../../../../../common/array/ensure-array";
|
||||
import type { TriggerList } from "../../../../../data/automation";
|
||||
import type { HomeAssistant, ItemPath } from "../../../../../types";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
import "../ha-automation-trigger";
|
||||
import {
|
||||
handleChangeEvent,
|
||||
@ -15,8 +15,6 @@ export class HaTriggerList extends LitElement implements TriggerElement {
|
||||
|
||||
@property({ attribute: false }) public trigger!: TriggerList;
|
||||
|
||||
@property({ attribute: false }) public path?: ItemPath;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
public static get defaultConfig(): TriggerList {
|
||||
@ -30,7 +28,6 @@ export class HaTriggerList extends LitElement implements TriggerElement {
|
||||
|
||||
return html`
|
||||
<ha-automation-trigger
|
||||
.path=${[...(this.path ?? []), "triggers"]}
|
||||
.triggers=${triggers}
|
||||
.hass=${this.hass}
|
||||
.disabled=${this.disabled}
|
||||
|
@ -2,7 +2,6 @@ import "@material/mwc-button/mwc-button";
|
||||
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { nestedArrayMove } from "../../../common/util/array-move";
|
||||
import "../../../components/ha-blueprint-picker";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-circular-progress";
|
||||
@ -158,15 +157,6 @@ export abstract class HaBlueprintGenericEditor extends LitElement {
|
||||
border: boolean
|
||||
) {
|
||||
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
|
||||
.narrow=${this.narrow}
|
||||
class=${border ? "border" : ""}
|
||||
@ -180,7 +170,7 @@ export abstract class HaBlueprintGenericEditor extends LitElement {
|
||||
></ha-markdown>
|
||||
${html`<ha-selector
|
||||
.hass=${this.hass}
|
||||
.selector=${enhancedSelector}
|
||||
.selector=${selector}
|
||||
.key=${key}
|
||||
.disabled=${this.disabled}
|
||||
.required=${value?.default === undefined}
|
||||
@ -190,7 +180,6 @@ export abstract class HaBlueprintGenericEditor extends LitElement {
|
||||
? this._config.use_blueprint.input[key]
|
||||
: value?.default}
|
||||
@value-changed=${this._inputChanged}
|
||||
@item-moved=${this._itemMoved}
|
||||
></ha-selector>`}
|
||||
</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 {
|
||||
return [
|
||||
haStyle,
|
||||
|
@ -15,7 +15,6 @@ import {
|
||||
extractSearchParam,
|
||||
removeSearchParam,
|
||||
} from "../../../common/url/search-params";
|
||||
import { nestedArrayMove } from "../../../common/util/array-move";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-markdown";
|
||||
@ -163,7 +162,6 @@ export class HaManualScriptEditor extends LitElement {
|
||||
.actions=${this.config.sequence || []}
|
||||
.path=${["sequence"]}
|
||||
@value-changed=${this._sequenceChanged}
|
||||
@item-moved=${this._itemMoved}
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.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 {
|
||||
return [
|
||||
haStyle,
|
||||
|
@ -83,11 +83,11 @@ export class HuiViewBadges extends LitElement {
|
||||
|
||||
private _badgeMoved(ev) {
|
||||
ev.stopPropagation();
|
||||
const { oldIndex, newIndex, oldPath, newPath } = ev.detail;
|
||||
const { oldIndex, newIndex } = ev.detail;
|
||||
const newConfig = moveBadge(
|
||||
this.lovelace!.config,
|
||||
[...oldPath, oldIndex] as [number, number, number],
|
||||
[...newPath, newIndex] as [number, number, number]
|
||||
[this.viewIndex!, oldIndex],
|
||||
[this.viewIndex!, newIndex]
|
||||
);
|
||||
this.lovelace!.saveConfig(newConfig);
|
||||
}
|
||||
@ -121,7 +121,6 @@ export class HuiViewBadges extends LitElement {
|
||||
@drag-end=${this._dragEnd}
|
||||
group="badge"
|
||||
draggable-selector="[data-sortable]"
|
||||
.path=${[this.viewIndex]}
|
||||
.rollback=${false}
|
||||
.options=${BADGE_SORTABLE_OPTIONS}
|
||||
invert-swap
|
||||
|
@ -4,8 +4,8 @@ import { property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import "../../../components/ha-ripple";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import "../../../components/ha-ripple";
|
||||
import type { HaSortableOptions } from "../../../components/ha-sortable";
|
||||
import { LovelaceSectionElement } from "../../../data/lovelace";
|
||||
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 "../components/hui-card-edit-mode";
|
||||
import { moveCard } from "../editor/config-util";
|
||||
import { LovelaceCardPath } from "../editor/lovelace-path";
|
||||
import type { Lovelace } from "../types";
|
||||
|
||||
const CARD_SORTABLE_OPTIONS: HaSortableOptions = {
|
||||
@ -68,14 +69,15 @@ export class GridSection extends LitElement implements LovelaceSectionElement {
|
||||
return html`
|
||||
<ha-sortable
|
||||
.disabled=${!editMode}
|
||||
@item-moved=${this._cardMoved}
|
||||
@drag-start=${this._dragStart}
|
||||
@drag-end=${this._dragEnd}
|
||||
group="card"
|
||||
draggable-selector=".card"
|
||||
.path=${[this.viewIndex, this.index]}
|
||||
.rollback=${false}
|
||||
.options=${CARD_SORTABLE_OPTIONS}
|
||||
@item-moved=${this._cardMoved}
|
||||
@item-added=${this._cardAdded}
|
||||
@item-removed=${this._cardRemoved}
|
||||
invert-swap
|
||||
>
|
||||
<div class="container ${classMap({ "edit-mode": editMode })}">
|
||||
@ -89,6 +91,11 @@ export class GridSection extends LitElement implements LovelaceSectionElement {
|
||||
|
||||
const { rows, columns } = computeCardGridSize(layoutOptions);
|
||||
|
||||
const cardPath: LovelaceCardPath = [
|
||||
this.viewIndex!,
|
||||
this.index!,
|
||||
idx,
|
||||
];
|
||||
return html`
|
||||
<div
|
||||
style=${styleMap({
|
||||
@ -100,13 +107,14 @@ export class GridSection extends LitElement implements LovelaceSectionElement {
|
||||
"fit-rows": typeof layoutOptions?.grid_rows === "number",
|
||||
"full-width": columns === "full",
|
||||
})}"
|
||||
.sortableData=${cardPath}
|
||||
>
|
||||
${editMode
|
||||
? html`
|
||||
<hui-card-edit-mode
|
||||
.hass=${this.hass}
|
||||
.lovelace=${this.lovelace}
|
||||
.path=${[this.viewIndex, this.index, idx]}
|
||||
.lovelace=${this.lovelace!}
|
||||
.path=${cardPath}
|
||||
.hiddenOverlay=${this._dragging}
|
||||
>
|
||||
${card}
|
||||
@ -141,15 +149,28 @@ export class GridSection extends LitElement implements LovelaceSectionElement {
|
||||
|
||||
private _cardMoved(ev) {
|
||||
ev.stopPropagation();
|
||||
const { oldIndex, newIndex, oldPath, newPath } = ev.detail;
|
||||
const { oldIndex, newIndex } = ev.detail;
|
||||
const newConfig = moveCard(
|
||||
this.lovelace!.config,
|
||||
[...oldPath, oldIndex] as [number, number, number],
|
||||
[...newPath, newIndex] as [number, number, number]
|
||||
[this.viewIndex!, this.index!, oldIndex],
|
||||
[this.viewIndex!, this.index!, newIndex]
|
||||
);
|
||||
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() {
|
||||
this._dragging = true;
|
||||
}
|
||||
|
@ -175,8 +175,6 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
|
||||
|
||||
const rowSpan = sectionConfig?.row_span || 1;
|
||||
|
||||
(section as any).itemPath = [idx];
|
||||
|
||||
return html`
|
||||
<div
|
||||
class="section"
|
||||
|
@ -307,5 +307,3 @@ export type AsyncReturnType<T extends (...args: any) => any> = T extends (
|
||||
: never;
|
||||
|
||||
export type Entries<T> = [keyof T, T[keyof T]][];
|
||||
|
||||
export type ItemPath = (number | string)[];
|
||||
|
Loading…
x
Reference in New Issue
Block a user