Automation nested drag and drop (#19338)

* Add nested drag and drop for actions

* Add nested drag and drop for triggers, conditions and options

* Update src/panels/config/automation/action/types/ha-automation-action-choose.ts

Co-authored-by: Bram Kragten <mail@bramkragten.nl>

* Move object at the root level

* Add support for choose option

* Fix undefined container (e.g else action)

* Add common nested array move function

* Move item at root level for manual automation

* Fix array move

* Don't fallback on body

* migrate blueprint and script

* Add drag and drop to service control

* Use context for reorder mode

* Rename reorder mode functions

* Fix hide menu props

* Fix drag and drop for choose action

---------

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
This commit is contained in:
Paul Bottein 2024-01-18 14:26:17 +01:00 committed by GitHub
parent 4046534fa8
commit 7398c6ab3e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
27 changed files with 770 additions and 247 deletions

View File

@ -0,0 +1,53 @@
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);
}
export function nestedArrayMove<T>(
obj: T | T[],
oldIndex: number,
newIndex: number,
oldPath?: ItemPath,
newPath?: ItemPath
): T | T[] {
const newObj = Array.isArray(obj) ? [...obj] : { ...obj };
const from = oldPath ? findNestedItem(newObj, oldPath) : newObj;
const to = newPath ? findNestedItem(newObj, newPath, true) : newObj;
if (!Array.isArray(from) || !Array.isArray(to)) {
return obj;
}
const item = from.splice(oldIndex, 1)[0];
to.splice(newIndex, 0, item);
return newObj;
}
export function arrayMove<T = any>(
array: T[],
oldIndex: number,
newIndex: number
): T[] {
const newArray = [...array];
const [item] = newArray.splice(oldIndex, 1);
newArray.splice(newIndex, 0, item);
return newArray;
}

View File

@ -24,8 +24,7 @@ export class HaActionSelector extends LitElement {
.disabled=${this.disabled} .disabled=${this.disabled}
.actions=${this.value || []} .actions=${this.value || []}
.hass=${this.hass} .hass=${this.hass}
.nested=${this.selector.action?.nested} .path=${this.selector.action?.path}
.reOrderMode=${this.selector.action?.reorder_mode}
></ha-automation-action> ></ha-automation-action>
`; `;
} }

View File

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

View File

@ -24,8 +24,7 @@ export class HaTriggerSelector extends LitElement {
.disabled=${this.disabled} .disabled=${this.disabled}
.triggers=${this.value || []} .triggers=${this.value || []}
.hass=${this.hass} .hass=${this.hass}
.nested=${this.selector.trigger?.nested} .path=${this.selector.trigger?.path}
.reOrderMode=${this.selector.trigger?.reorder_mode}
></ha-automation-trigger> ></ha-automation-trigger>
`; `;
} }

View File

@ -40,6 +40,8 @@ import "./ha-service-picker";
import "./ha-settings-row"; import "./ha-settings-row";
import "./ha-yaml-editor"; import "./ha-yaml-editor";
import type { HaYamlEditor } from "./ha-yaml-editor"; import type { HaYamlEditor } from "./ha-yaml-editor";
import { nestedArrayMove } from "../common/util/array-move";
import { ReorderModeMixin } from "../state/reorder-mode-mixin";
const attributeFilter = (values: any[], attribute: any) => { const attributeFilter = (values: any[], attribute: any) => {
if (typeof attribute === "object") { if (typeof attribute === "object") {
@ -75,7 +77,7 @@ interface ExtHassService extends Omit<HassService, "fields"> {
} }
@customElement("ha-service-control") @customElement("ha-service-control")
export class HaServiceControl extends LitElement { export class HaServiceControl extends ReorderModeMixin(LitElement) {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public value?: { @property({ attribute: false }) public value?: {
@ -439,6 +441,7 @@ export class HaServiceControl extends LitElement {
allow-custom-entity allow-custom-entity
></ha-entity-picker>` ></ha-entity-picker>`
: ""} : ""}
${this._renderReorderModeAlert()}
${shouldRenderServiceDataYaml ${shouldRenderServiceDataYaml
? html`<ha-yaml-editor ? html`<ha-yaml-editor
.hass=${this.hass} .hass=${this.hass}
@ -449,7 +452,23 @@ export class HaServiceControl extends LitElement {
@value-changed=${this._dataChanged} @value-changed=${this._dataChanged}
></ha-yaml-editor>` ></ha-yaml-editor>`
: filteredFields?.map((dataField) => { : filteredFields?.map((dataField) => {
const selector = dataField?.selector ?? { text: undefined };
const type = Object.keys(selector)[0];
const enhancedSelector = [
"action",
"condition",
"trigger",
].includes(type)
? {
[type]: {
...selector[type],
path: [dataField.key],
},
}
: selector;
const showOptional = showOptionalToggle(dataField); const showOptional = showOptionalToggle(dataField);
return dataField.selector && return dataField.selector &&
(!dataField.advanced || (!dataField.advanced ||
this.showAdvanced || this.showAdvanced ||
@ -488,7 +507,7 @@ export class HaServiceControl extends LitElement {
(!this._value?.data || (!this._value?.data ||
this._value.data[dataField.key] === undefined))} this._value.data[dataField.key] === undefined))}
.hass=${this.hass} .hass=${this.hass}
.selector=${dataField.selector} .selector=${enhancedSelector}
.key=${dataField.key} .key=${dataField.key}
@value-changed=${this._serviceDataChanged} @value-changed=${this._serviceDataChanged}
.value=${this._value?.data .value=${this._value?.data
@ -496,12 +515,41 @@ export class HaServiceControl extends LitElement {
: undefined} : undefined}
.placeholder=${dataField.default} .placeholder=${dataField.default}
.localizeValue=${this._localizeValueCallback} .localizeValue=${this._localizeValueCallback}
@item-moved=${this._itemMoved}
></ha-selector> ></ha-selector>
</ha-settings-row>` </ha-settings-row>`
: ""; : "";
})}`; })}`;
} }
private _renderReorderModeAlert() {
if (!this._reorderMode.active) {
return nothing;
}
return html`
<ha-alert
class="re-order"
alert-type="info"
.title=${this.hass.localize(
"ui.panel.config.automation.editor.re_order_mode.title"
)}
>
${this.hass.localize(
"ui.panel.config.automation.editor.re_order_mode.description_all"
)}
<ha-button slot="action" @click=${this._exitReOrderMode}>
${this.hass.localize(
"ui.panel.config.automation.editor.re_order_mode.exit"
)}
</ha-button>
</ha-alert>
`;
}
private async _exitReOrderMode() {
this._reorderMode.exit();
}
private _localizeValueCallback = (key: string) => { private _localizeValueCallback = (key: string) => {
if (!this._value?.service) { if (!this._value?.service) {
return ""; return "";
@ -697,6 +745,22 @@ export class HaServiceControl extends LitElement {
}); });
} }
private _itemMoved(ev) {
ev.stopPropagation();
const { oldIndex, newIndex, oldPath, newPath } = ev.detail;
const data = this.value?.data ?? {};
const newData = nestedArrayMove(data, oldIndex, newIndex, oldPath, newPath);
fireEvent(this, "value-changed", {
value: {
...this.value,
data: newData,
},
});
}
private _dataChanged(ev: CustomEvent) { private _dataChanged(ev: CustomEvent) {
ev.stopPropagation(); ev.stopPropagation();
if (!ev.detail.isValid) { if (!ev.detail.isValid) {

View File

@ -4,12 +4,15 @@ import { customElement, property } from "lit/decorators";
import type { SortableEvent } from "sortablejs"; import type { SortableEvent } from "sortablejs";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
import type { SortableInstance } from "../resources/sortable"; import type { SortableInstance } from "../resources/sortable";
import { ItemPath } from "../types";
declare global { declare global {
interface HASSDomEvents { interface HASSDomEvents {
"item-moved": { "item-moved": {
oldIndex: number; oldIndex: number;
newIndex: number; newIndex: number;
oldPath?: ItemPath;
newPath?: ItemPath;
}; };
} }
} }
@ -21,6 +24,9 @@ export class HaSortable extends LitElement {
@property({ type: Boolean }) @property({ type: Boolean })
public disabled = false; public disabled = false;
@property({ type: Boolean })
public path?: ItemPath;
@property({ type: Boolean, attribute: "no-style" }) @property({ type: Boolean, attribute: "no-style" })
public noStyle: boolean = false; public noStyle: boolean = false;
@ -30,6 +36,9 @@ export class HaSortable extends LitElement {
@property({ type: String, attribute: "handle-selector" }) @property({ type: String, attribute: "handle-selector" })
public handleSelector?: string; public handleSelector?: string;
@property({ type: String, attribute: "group" })
public group?: string;
protected updated(changedProperties: PropertyValues<this>) { protected updated(changedProperties: PropertyValues<this>) {
if (changedProperties.has("disabled")) { if (changedProperties.has("disabled")) {
if (this.disabled) { if (this.disabled) {
@ -100,6 +109,7 @@ export class HaSortable extends LitElement {
const options: SortableInstance.Options = { const options: SortableInstance.Options = {
animation: 150, animation: 150,
swapThreshold: 0.75,
onChoose: this._handleChoose, onChoose: this._handleChoose,
onEnd: this._handleEnd, onEnd: this._handleEnd,
}; };
@ -110,27 +120,41 @@ export class HaSortable extends LitElement {
if (this.handleSelector) { if (this.handleSelector) {
options.handle = this.handleSelector; options.handle = this.handleSelector;
} }
if (this.draggableSelector) {
options.draggable = this.draggableSelector;
}
if (this.group) {
options.group = this.group;
}
this._sortable = new Sortable(container, options); this._sortable = new Sortable(container, options);
} }
private _handleEnd = (evt: SortableEvent) => { private _handleEnd = async (evt: SortableEvent) => {
// put back in original location // put back in original location
if ((evt.item as any).placeholder) { if ((evt.item as any).placeholder) {
(evt.item as any).placeholder.replaceWith(evt.item); (evt.item as any).placeholder.replaceWith(evt.item);
delete (evt.item as any).placeholder; delete (evt.item as any).placeholder;
} }
// if item was not moved, ignore
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 ( if (
evt.oldIndex === undefined || oldIndex === undefined ||
evt.newIndex === undefined || newIndex === undefined ||
evt.oldIndex === evt.newIndex (oldIndex === newIndex && oldPath?.join(".") === newPath?.join("."))
) { ) {
return; return;
} }
fireEvent(this, "item-moved", { fireEvent(this, "item-moved", {
oldIndex: evt.oldIndex!, oldIndex,
newIndex: evt.newIndex!, newIndex,
oldPath,
newPath,
}); });
}; };

View File

@ -3,7 +3,7 @@ import { ensureArray } from "../common/array/ensure-array";
import { computeStateDomain } from "../common/entity/compute_state_domain"; import { computeStateDomain } from "../common/entity/compute_state_domain";
import { supportsFeature } from "../common/entity/supports-feature"; import { supportsFeature } from "../common/entity/supports-feature";
import { UiAction } from "../panels/lovelace/components/hui-action-editor"; import { UiAction } from "../panels/lovelace/components/hui-action-editor";
import { HomeAssistant } from "../types"; import { HomeAssistant, ItemPath } from "../types";
import { import {
DeviceRegistryEntry, DeviceRegistryEntry,
getDeviceIntegrationLookup, getDeviceIntegrationLookup,
@ -59,8 +59,7 @@ export type Selector =
export interface ActionSelector { export interface ActionSelector {
action: { action: {
reorder_mode?: boolean; path?: ItemPath;
nested?: boolean;
} | null; } | null;
} }
@ -113,8 +112,7 @@ export interface ColorTempSelector {
export interface ConditionSelector { export interface ConditionSelector {
condition: { condition: {
reorder_mode?: boolean; path?: ItemPath;
nested?: boolean;
} | null; } | null;
} }
@ -392,8 +390,7 @@ export interface TimeSelector {
export interface TriggerSelector { export interface TriggerSelector {
trigger: { trigger: {
reorder_mode?: boolean; path?: ItemPath;
nested?: boolean;
} | null; } | null;
} }

View File

@ -57,7 +57,11 @@ import {
showPromptDialog, showPromptDialog,
} from "../../../../dialogs/generic/show-dialog-box"; } from "../../../../dialogs/generic/show-dialog-box";
import { haStyle } from "../../../../resources/styles"; import { haStyle } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types"; import {
ReorderMode,
reorderModeContext,
} from "../../../../state/reorder-mode-mixin";
import type { HomeAssistant, ItemPath } from "../../../../types";
import { showToast } from "../../../../util/toast"; import { showToast } from "../../../../util/toast";
import "./types/ha-automation-action-activate_scene"; import "./types/ha-automation-action-activate_scene";
import "./types/ha-automation-action-choose"; import "./types/ha-automation-action-choose";
@ -129,7 +133,7 @@ export default class HaAutomationActionRow extends LitElement {
@property({ type: Boolean }) public hideMenu = false; @property({ type: Boolean }) public hideMenu = false;
@property({ type: Boolean }) public reOrderMode = false; @property() public path?: ItemPath;
@storage({ @storage({
key: "automationClipboard", key: "automationClipboard",
@ -143,6 +147,10 @@ export default class HaAutomationActionRow extends LitElement {
@consume({ context: fullEntitiesContext, subscribe: true }) @consume({ context: fullEntitiesContext, subscribe: true })
_entityReg!: EntityRegistryEntry[]; _entityReg!: EntityRegistryEntry[];
@state()
@consume({ context: reorderModeContext, subscribe: true })
private _reorderMode?: ReorderMode;
@state() private _warnings?: string[]; @state() private _warnings?: string[];
@state() private _uiModeAvailable = true; @state() private _uiModeAvailable = true;
@ -176,9 +184,13 @@ export default class HaAutomationActionRow extends LitElement {
} }
protected render() { protected render() {
if (!this.action) return nothing;
const type = getType(this.action); const type = getType(this.action);
const yamlMode = this._yamlMode; const yamlMode = this._yamlMode;
const noReorderModeAvailable = this._reorderMode === undefined;
return html` return html`
<ha-card outlined> <ha-card outlined>
${this.action.enabled === false ${this.action.enabled === false
@ -247,7 +259,12 @@ export default class HaAutomationActionRow extends LitElement {
.path=${mdiRenameBox} .path=${mdiRenameBox}
></ha-svg-icon> ></ha-svg-icon>
</mwc-list-item> </mwc-list-item>
<mwc-list-item graphic="icon" .disabled=${this.disabled}> <mwc-list-item
graphic="icon"
.disabled=${this.disabled}
class=${classMap({ hidden: noReorderModeAvailable })}
?aria-hidden=${noReorderModeAvailable}
>
${this.hass.localize( ${this.hass.localize(
"ui.panel.config.automation.editor.actions.re_order" "ui.panel.config.automation.editor.actions.re_order"
)} )}
@ -405,8 +422,8 @@ export default class HaAutomationActionRow extends LitElement {
hass: this.hass, hass: this.hass,
action: this.action, action: this.action,
narrow: this.narrow, narrow: this.narrow,
reOrderMode: this.reOrderMode,
disabled: this.disabled, disabled: this.disabled,
path: this.path,
})} })}
</div> </div>
`} `}
@ -435,7 +452,7 @@ export default class HaAutomationActionRow extends LitElement {
await this._renameAction(); await this._renameAction();
break; break;
case 2: case 2:
fireEvent(this, "re-order"); this._reorderMode?.enter();
break; break;
case 3: case 3:
fireEvent(this, "duplicate"); fireEvent(this, "duplicate");
@ -640,6 +657,9 @@ export default class HaAutomationActionRow extends LitElement {
mwc-list-item[disabled] { mwc-list-item[disabled] {
--mdc-theme-text-primary-on-background: var(--disabled-text-color); --mdc-theme-text-primary-on-background: var(--disabled-text-color);
} }
mwc-list-item.hidden {
display: none;
}
.warning ul { .warning ul {
margin: 4px 0; margin: 4px 0;
} }

View File

@ -1,17 +1,23 @@
import { consume } from "@lit-labs/context";
import { mdiArrowDown, mdiArrowUp, mdiDrag, mdiPlus } from "@mdi/js"; import { mdiArrowDown, mdiArrowUp, mdiDrag, mdiPlus } from "@mdi/js";
import deepClone from "deep-clone-simple"; import deepClone from "deep-clone-simple";
import { CSSResultGroup, LitElement, PropertyValues, css, html } from "lit"; import { CSSResultGroup, LitElement, PropertyValues, css, html } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { repeat } from "lit/directives/repeat"; import { repeat } from "lit/directives/repeat";
import { storage } from "../../../../common/decorators/storage"; import { storage } from "../../../../common/decorators/storage";
import { fireEvent } from "../../../../common/dom/fire_event"; import { fireEvent } from "../../../../common/dom/fire_event";
import { nestedArrayMove } from "../../../../common/util/array-move";
import "../../../../components/ha-button"; import "../../../../components/ha-button";
import "../../../../components/ha-svg-icon";
import "../../../../components/ha-sortable"; import "../../../../components/ha-sortable";
import "../../../../components/ha-svg-icon";
import { getService, isService } from "../../../../data/action"; import { getService, isService } from "../../../../data/action";
import type { AutomationClipboard } from "../../../../data/automation"; import type { AutomationClipboard } from "../../../../data/automation";
import { Action } from "../../../../data/script"; import { Action } from "../../../../data/script";
import { HomeAssistant } from "../../../../types"; import {
ReorderMode,
reorderModeContext,
} from "../../../../state/reorder-mode-mixin";
import { HomeAssistant, ItemPath } from "../../../../types";
import { import {
PASTE_VALUE, PASTE_VALUE,
showAddAutomationElementDialog, showAddAutomationElementDialog,
@ -27,11 +33,13 @@ export default class HaAutomationAction extends LitElement {
@property({ type: Boolean }) public disabled = false; @property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public nested = false; @property() public path?: ItemPath;
@property() public actions!: Action[]; @property() public actions!: Action[];
@property({ type: Boolean }) public reOrderMode = false; @state()
@consume({ context: reorderModeContext, subscribe: true })
private _reorderMode?: ReorderMode;
@storage({ @storage({
key: "automationClipboard", key: "automationClipboard",
@ -45,31 +53,18 @@ export default class HaAutomationAction extends LitElement {
private _actionKeys = new WeakMap<Action, string>(); private _actionKeys = new WeakMap<Action, string>();
private get nested() {
return this.path !== undefined;
}
protected render() { protected render() {
return html` return html`
${this.reOrderMode && !this.nested
? html`
<ha-alert
alert-type="info"
.title=${this.hass.localize(
"ui.panel.config.automation.editor.re_order_mode.title"
)}
>
${this.hass.localize(
"ui.panel.config.automation.editor.re_order_mode.description_actions"
)}
<ha-button slot="action" @click=${this._exitReOrderMode}>
${this.hass.localize(
"ui.panel.config.automation.editor.re_order_mode.exit"
)}
</ha-button>
</ha-alert>
`
: null}
<ha-sortable <ha-sortable
handle-selector=".handle" handle-selector=".handle"
.disabled=${!this.reOrderMode} .disabled=${!this._reorderMode?.active}
@item-moved=${this._actionMoved} @item-moved=${this._actionMoved}
group="actions"
.path=${this.path}
> >
<div class="actions"> <div class="actions">
${repeat( ${repeat(
@ -77,18 +72,17 @@ export default class HaAutomationAction extends LitElement {
(action) => this._getKey(action), (action) => this._getKey(action),
(action, idx) => html` (action, idx) => html`
<ha-automation-action-row <ha-automation-action-row
.path=${[...(this.path ?? []), idx]}
.index=${idx} .index=${idx}
.action=${action} .action=${action}
.narrow=${this.narrow} .narrow=${this.narrow}
.disabled=${this.disabled} .disabled=${this.disabled}
.hideMenu=${this.reOrderMode} .hideMenu=${Boolean(this._reorderMode?.active)}
.reOrderMode=${this.reOrderMode}
@duplicate=${this._duplicateAction} @duplicate=${this._duplicateAction}
@value-changed=${this._actionChanged} @value-changed=${this._actionChanged}
@re-order=${this._enterReOrderMode}
.hass=${this.hass} .hass=${this.hass}
> >
${this.reOrderMode ${this._reorderMode?.active
? html` ? html`
<ha-icon-button <ha-icon-button
.index=${idx} .index=${idx}
@ -199,16 +193,6 @@ export default class HaAutomationAction extends LitElement {
fireEvent(this, "value-changed", { value: actions }); fireEvent(this, "value-changed", { value: actions });
}; };
private async _enterReOrderMode(ev: CustomEvent) {
if (this.nested) return;
ev.stopPropagation();
this.reOrderMode = true;
}
private async _exitReOrderMode() {
this.reOrderMode = false;
}
private _getKey(action: Action) { private _getKey(action: Action) {
if (!this._actionKeys.has(action)) { if (!this._actionKeys.has(action)) {
this._actionKeys.set(action, Math.random().toString()); this._actionKeys.set(action, Math.random().toString());
@ -229,17 +213,28 @@ export default class HaAutomationAction extends LitElement {
this._move(index, newIndex); this._move(index, newIndex);
} }
private _move(index: number, newIndex: number) { private _move(
const actions = this.actions.concat(); oldIndex: number,
const action = actions.splice(index, 1)[0]; newIndex: number,
actions.splice(newIndex, 0, action); oldPath?: ItemPath,
newPath?: ItemPath
) {
const actions = nestedArrayMove(
this.actions,
oldIndex,
newIndex,
oldPath,
newPath
);
fireEvent(this, "value-changed", { value: actions }); fireEvent(this, "value-changed", { value: actions });
} }
private _actionMoved(ev: CustomEvent): void { private _actionMoved(ev: CustomEvent): void {
if (this.nested) return;
ev.stopPropagation(); ev.stopPropagation();
const { oldIndex, newIndex } = ev.detail; const { oldIndex, newIndex, oldPath, newPath } = ev.detail;
this._move(oldIndex, newIndex); this._move(oldIndex, newIndex, oldPath, newPath);
} }
private _actionChanged(ev: CustomEvent) { private _actionChanged(ev: CustomEvent) {

View File

@ -14,6 +14,7 @@ import {
import deepClone from "deep-clone-simple"; import deepClone from "deep-clone-simple";
import { CSSResultGroup, LitElement, PropertyValues, css, html } from "lit"; import { CSSResultGroup, LitElement, PropertyValues, css, html } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { repeat } from "lit/directives/repeat"; import { repeat } from "lit/directives/repeat";
import { ensureArray } from "../../../../../common/array/ensure-array"; import { ensureArray } from "../../../../../common/array/ensure-array";
import { fireEvent } from "../../../../../common/dom/fire_event"; import { fireEvent } from "../../../../../common/dom/fire_event";
@ -36,7 +37,11 @@ import {
showPromptDialog, showPromptDialog,
} from "../../../../../dialogs/generic/show-dialog-box"; } from "../../../../../dialogs/generic/show-dialog-box";
import { haStyle } from "../../../../../resources/styles"; import { haStyle } from "../../../../../resources/styles";
import { HomeAssistant } from "../../../../../types"; import {
ReorderMode,
reorderModeContext,
} from "../../../../../state/reorder-mode-mixin";
import { HomeAssistant, ItemPath } from "../../../../../types";
import { ActionElement } from "../ha-automation-action-row"; import { ActionElement } from "../ha-automation-action-row";
const preventDefault = (ev) => ev.preventDefault(); const preventDefault = (ev) => ev.preventDefault();
@ -47,9 +52,9 @@ export class HaChooseAction extends LitElement implements ActionElement {
@property({ type: Boolean }) public disabled = false; @property({ type: Boolean }) public disabled = false;
@property() public action!: ChooseAction; @property({ attribute: false }) public path?: ItemPath;
@property({ type: Boolean }) public reOrderMode = false; @property() public action!: ChooseAction;
@state() private _showDefault = false; @state() private _showDefault = false;
@ -59,6 +64,10 @@ export class HaChooseAction extends LitElement implements ActionElement {
@consume({ context: fullEntitiesContext, subscribe: true }) @consume({ context: fullEntitiesContext, subscribe: true })
_entityReg!: EntityRegistryEntry[]; _entityReg!: EntityRegistryEntry[];
@state()
@consume({ context: reorderModeContext, subscribe: true })
private _reorderMode?: ReorderMode;
private _expandLast = false; private _expandLast = false;
public static get defaultConfig() { public static get defaultConfig() {
@ -95,11 +104,14 @@ export class HaChooseAction extends LitElement implements ActionElement {
protected render() { protected render() {
const action = this.action; const action = this.action;
const noReorderModeAvailable = this._reorderMode === undefined;
return html` return html`
<ha-sortable <ha-sortable
handle-selector=".handle" handle-selector=".handle"
.disabled=${!this.reOrderMode} .disabled=${!this._reorderMode?.active}
@item-moved=${this._optionMoved} group="choose-options"
.path=${[...(this.path ?? []), "choose"]}
> >
<div class="options"> <div class="options">
${repeat( ${repeat(
@ -123,7 +135,7 @@ export class HaChooseAction extends LitElement implements ActionElement {
? "" ? ""
: this._getDescription(option))} : this._getDescription(option))}
</h3> </h3>
${this.reOrderMode ${this._reorderMode?.active
? html` ? html`
<ha-icon-button <ha-icon-button
.index=${idx} .index=${idx}
@ -178,6 +190,10 @@ export class HaChooseAction extends LitElement implements ActionElement {
<mwc-list-item <mwc-list-item
graphic="icon" graphic="icon"
.disabled=${this.disabled} .disabled=${this.disabled}
class=${classMap({
hidden: noReorderModeAvailable,
})}
?aria-hidden=${noReorderModeAvailable}
> >
${this.hass.localize( ${this.hass.localize(
"ui.panel.config.automation.editor.actions.re_order" "ui.panel.config.automation.editor.actions.re_order"
@ -224,11 +240,15 @@ export class HaChooseAction extends LitElement implements ActionElement {
)}: )}:
</h4> </h4>
<ha-automation-condition <ha-automation-condition
nested .path=${[
...(this.path ?? []),
"choose",
idx,
"conditions",
]}
.conditions=${ensureArray<string | Condition>( .conditions=${ensureArray<string | Condition>(
option.conditions option.conditions
)} )}
.reOrderMode=${this.reOrderMode}
.disabled=${this.disabled} .disabled=${this.disabled}
.hass=${this.hass} .hass=${this.hass}
.idx=${idx} .idx=${idx}
@ -240,9 +260,13 @@ export class HaChooseAction extends LitElement implements ActionElement {
)}: )}:
</h4> </h4>
<ha-automation-action <ha-automation-action
nested .path=${[
...(this.path ?? []),
"choose",
idx,
"sequence",
]}
.actions=${ensureArray(option.sequence) || []} .actions=${ensureArray(option.sequence) || []}
.reOrderMode=${this.reOrderMode}
.disabled=${this.disabled} .disabled=${this.disabled}
.hass=${this.hass} .hass=${this.hass}
.idx=${idx} .idx=${idx}
@ -274,9 +298,8 @@ export class HaChooseAction extends LitElement implements ActionElement {
)}: )}:
</h2> </h2>
<ha-automation-action <ha-automation-action
nested .path=${[...(this.path ?? []), "choose", "default"]}
.actions=${ensureArray(action.default) || []} .actions=${ensureArray(action.default) || []}
.reOrderMode=${this.reOrderMode}
.disabled=${this.disabled} .disabled=${this.disabled}
@value-changed=${this._defaultChanged} @value-changed=${this._defaultChanged}
.hass=${this.hass} .hass=${this.hass}
@ -302,7 +325,7 @@ export class HaChooseAction extends LitElement implements ActionElement {
await this._renameAction(ev); await this._renameAction(ev);
break; break;
case 1: case 1:
fireEvent(this, "re-order"); this._reorderMode?.enter();
break; break;
case 2: case 2:
this._duplicateOption(ev); this._duplicateOption(ev);
@ -435,12 +458,6 @@ export class HaChooseAction extends LitElement implements ActionElement {
}); });
} }
private _optionMoved(ev: CustomEvent): void {
ev.stopPropagation();
const { oldIndex, newIndex } = ev.detail;
this._move(oldIndex, newIndex);
}
private _removeOption(ev: CustomEvent) { private _removeOption(ev: CustomEvent) {
const index = (ev.target as any).idx; const index = (ev.target as any).idx;
showConfirmationDialog(this, { showConfirmationDialog(this, {
@ -495,6 +512,12 @@ export class HaChooseAction extends LitElement implements ActionElement {
--expansion-panel-summary-padding: 0 0 0 8px; --expansion-panel-summary-padding: 0 0 0 8px;
--expansion-panel-content-padding: 0; --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 { h3 {
margin: 0; margin: 0;
font-size: inherit; font-size: inherit;

View File

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

View File

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

View File

@ -5,14 +5,17 @@ import { fireEvent } from "../../../../../common/dom/fire_event";
import "../../../../../components/ha-textfield"; import "../../../../../components/ha-textfield";
import { RepeatAction } from "../../../../../data/script"; import { RepeatAction } from "../../../../../data/script";
import { haStyle } from "../../../../../resources/styles"; import { haStyle } from "../../../../../resources/styles";
import type { HomeAssistant } from "../../../../../types"; import type { HomeAssistant, ItemPath } from "../../../../../types";
import "../ha-automation-action"; import "../ha-automation-action";
import type { ActionElement } from "../ha-automation-action-row"; import type { ActionElement } from "../ha-automation-action-row";
import { isTemplate } from "../../../../../common/string/has-template"; import { isTemplate } from "../../../../../common/string/has-template";
import type { LocalizeFunc } from "../../../../../common/translations/localize"; import type { LocalizeFunc } from "../../../../../common/translations/localize";
import "../../../../../components/ha-form/ha-form"; import "../../../../../components/ha-form/ha-form";
import type { SchemaUnion } from "../../../../../components/ha-form/types"; import type {
HaFormSchema,
SchemaUnion,
} from "../../../../../components/ha-form/types";
const OPTIONS = ["count", "while", "until", "for_each"] as const; const OPTIONS = ["count", "while", "until", "for_each"] as const;
@ -26,7 +29,7 @@ export class HaRepeatAction extends LitElement implements ActionElement {
@property({ attribute: false }) public action!: RepeatAction; @property({ attribute: false }) public action!: RepeatAction;
@property({ type: Boolean }) public reOrderMode = false; @property() public path?: ItemPath;
public static get defaultConfig() { public static get defaultConfig() {
return { repeat: { count: 2, sequence: [] } }; return { repeat: { count: 2, sequence: [] } };
@ -36,8 +39,8 @@ export class HaRepeatAction extends LitElement implements ActionElement {
( (
localize: LocalizeFunc, localize: LocalizeFunc,
type: string, type: string,
reOrderMode: boolean, template: boolean,
template: boolean path?: ItemPath
) => ) =>
[ [
{ {
@ -60,20 +63,22 @@ export class HaRepeatAction extends LitElement implements ActionElement {
name: "count", name: "count",
required: true, required: true,
selector: template selector: template
? ({ template: {} } as const) ? { template: {} }
: ({ number: { mode: "box", min: 1 } } as const), : { number: { mode: "box", min: 1 } },
}, },
] as const) ] as const satisfies readonly HaFormSchema[])
: []), : []),
...(type === "until" || type === "while" ...(type === "until" || type === "while"
? ([ ? ([
{ {
name: type, name: type,
selector: { selector: {
condition: { nested: true, reorder_mode: reOrderMode }, condition: {
path: [...(path ?? []), "repeat", type],
},
}, },
}, },
] as const) ] as const satisfies readonly HaFormSchema[])
: []), : []),
...(type === "for_each" ...(type === "for_each"
? ([ ? ([
@ -82,13 +87,17 @@ export class HaRepeatAction extends LitElement implements ActionElement {
required: true, required: true,
selector: { object: {} }, selector: { object: {} },
}, },
] as const) ] as const satisfies readonly HaFormSchema[])
: []), : []),
{ {
name: "sequence", name: "sequence",
selector: { action: { nested: true, reorder_mode: reOrderMode } }, selector: {
action: {
path: [...(path ?? []), "repeat", "sequence"],
},
},
}, },
] as const ] as const satisfies readonly HaFormSchema[]
); );
protected render() { protected render() {
@ -97,11 +106,12 @@ export class HaRepeatAction extends LitElement implements ActionElement {
const schema = this._schema( const schema = this._schema(
this.hass.localize, this.hass.localize,
type ?? "count", type ?? "count",
this.reOrderMode,
"count" in action && typeof action.count === "string" "count" in action && typeof action.count === "string"
? isTemplate(action.count) ? isTemplate(action.count)
: false : false,
this.path
); );
const data = { ...action, type }; const data = { ...action, type };
return html`<ha-form return html`<ha-form
.hass=${this.hass} .hass=${this.hass}

View File

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

View File

@ -1,15 +1,16 @@
import "@material/mwc-button/mwc-button"; import "@material/mwc-button/mwc-button";
import { HassEntity } from "home-assistant-js-websocket"; import { HassEntity } from "home-assistant-js-websocket";
import { css, CSSResultGroup, html, LitElement } from "lit"; import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event"; import { fireEvent } from "../../../common/dom/fire_event";
import { nestedArrayMove } from "../../../common/util/array-move";
import "../../../components/ha-alert";
import "../../../components/ha-blueprint-picker"; import "../../../components/ha-blueprint-picker";
import "../../../components/ha-card"; import "../../../components/ha-card";
import "../../../components/ha-circular-progress"; import "../../../components/ha-circular-progress";
import "../../../components/ha-markdown"; import "../../../components/ha-markdown";
import "../../../components/ha-selector/ha-selector"; import "../../../components/ha-selector/ha-selector";
import "../../../components/ha-settings-row"; import "../../../components/ha-settings-row";
import "../../../components/ha-alert";
import { BlueprintAutomationConfig } from "../../../data/automation"; import { BlueprintAutomationConfig } from "../../../data/automation";
import { import {
BlueprintOrError, BlueprintOrError,
@ -17,11 +18,12 @@ import {
fetchBlueprints, fetchBlueprints,
} from "../../../data/blueprint"; } from "../../../data/blueprint";
import { haStyle } from "../../../resources/styles"; import { haStyle } from "../../../resources/styles";
import { ReorderModeMixin } from "../../../state/reorder-mode-mixin";
import { HomeAssistant } from "../../../types"; import { HomeAssistant } from "../../../types";
import "../ha-config-section"; import "../ha-config-section";
@customElement("blueprint-automation-editor") @customElement("blueprint-automation-editor")
export class HaBlueprintAutomationEditor extends LitElement { export class HaBlueprintAutomationEditor extends ReorderModeMixin(LitElement) {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean }) public isWide = false; @property({ type: Boolean }) public isWide = false;
@ -76,6 +78,7 @@ export class HaBlueprintAutomationEditor extends LitElement {
${this.config.description ${this.config.description
? html`<p class="description">${this.config.description}</p>` ? html`<p class="description">${this.config.description}</p>`
: ""} : ""}
${this._renderReorderModeAlert()}
<ha-card <ha-card
outlined outlined
class="blueprint" class="blueprint"
@ -119,8 +122,23 @@ export class HaBlueprintAutomationEditor extends LitElement {
${blueprint?.metadata?.input && ${blueprint?.metadata?.input &&
Object.keys(blueprint.metadata.input).length Object.keys(blueprint.metadata.input).length
? Object.entries(blueprint.metadata.input).map( ? Object.entries(blueprint.metadata.input).map(
([key, value]) => ([key, value]) => {
html`<ha-settings-row .narrow=${this.narrow}> 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}>
<span slot="heading">${value?.name || key}</span> <span slot="heading">${value?.name || key}</span>
<ha-markdown <ha-markdown
slot="description" slot="description"
@ -130,7 +148,7 @@ export class HaBlueprintAutomationEditor extends LitElement {
></ha-markdown> ></ha-markdown>
${html`<ha-selector ${html`<ha-selector
.hass=${this.hass} .hass=${this.hass}
.selector=${value?.selector ?? { text: undefined }} .selector=${enhancedSelector}
.key=${key} .key=${key}
.disabled=${this.disabled} .disabled=${this.disabled}
.required=${value?.default === undefined} .required=${value?.default === undefined}
@ -140,8 +158,10 @@ export class HaBlueprintAutomationEditor extends LitElement {
? this.config.use_blueprint.input[key] ? this.config.use_blueprint.input[key]
: value?.default} : value?.default}
@value-changed=${this._inputChanged} @value-changed=${this._inputChanged}
@item-moved=${this._itemMoved}
></ha-selector>`} ></ha-selector>`}
</ha-settings-row>` </ha-settings-row>`;
}
) )
: html`<p class="padding"> : html`<p class="padding">
${this.hass.localize( ${this.hass.localize(
@ -153,6 +173,34 @@ export class HaBlueprintAutomationEditor extends LitElement {
`; `;
} }
private _renderReorderModeAlert() {
if (!this._reorderMode.active) {
return nothing;
}
return html`
<ha-alert
class="re-order"
alert-type="info"
.title=${this.hass.localize(
"ui.panel.config.automation.editor.re_order_mode.title"
)}
>
${this.hass.localize(
"ui.panel.config.automation.editor.re_order_mode.description_all"
)}
<ha-button slot="action" @click=${this._exitReOrderMode}>
${this.hass.localize(
"ui.panel.config.automation.editor.re_order_mode.exit"
)}
</ha-button>
</ha-alert>
`;
}
private async _exitReOrderMode() {
this._reorderMode.exit();
}
private async _getBlueprints() { private async _getBlueprints() {
this._blueprints = await fetchBlueprints(this.hass, "automation"); this._blueprints = await fetchBlueprints(this.hass, "automation");
} }
@ -197,6 +245,29 @@ export class HaBlueprintAutomationEditor 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,
},
},
});
}
private async _enable(): Promise<void> { private async _enable(): Promise<void> {
if (!this.hass || !this.stateObj) { if (!this.hass || !this.stateObj) {
return; return;
@ -259,6 +330,10 @@ export class HaBlueprintAutomationEditor extends LitElement {
margin-bottom: 16px; margin-bottom: 16px;
display: block; display: block;
} }
ha-alert.re-order {
border-radius: var(--ha-card-border-radius, 12px);
overflow: hidden;
}
`, `,
]; ];
} }

View File

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

View File

@ -39,8 +39,12 @@ import {
showPromptDialog, showPromptDialog,
} from "../../../../dialogs/generic/show-dialog-box"; } from "../../../../dialogs/generic/show-dialog-box";
import { haStyle } from "../../../../resources/styles"; import { haStyle } from "../../../../resources/styles";
import { HomeAssistant } from "../../../../types"; import { HomeAssistant, ItemPath } from "../../../../types";
import "./ha-automation-condition-editor"; import "./ha-automation-condition-editor";
import {
ReorderMode,
reorderModeContext,
} from "../../../../state/reorder-mode-mixin";
export interface ConditionElement extends LitElement { export interface ConditionElement extends LitElement {
condition: Condition; condition: Condition;
@ -81,10 +85,10 @@ export default class HaAutomationConditionRow extends LitElement {
@property({ type: Boolean }) public hideMenu = false; @property({ type: Boolean }) public hideMenu = false;
@property({ type: Boolean }) public reOrderMode = false;
@property({ type: Boolean }) public disabled = false; @property({ type: Boolean }) public disabled = false;
@property() public path?: ItemPath;
@storage({ @storage({
key: "automationClipboard", key: "automationClipboard",
state: false, state: false,
@ -105,10 +109,17 @@ export default class HaAutomationConditionRow extends LitElement {
@consume({ context: fullEntitiesContext, subscribe: true }) @consume({ context: fullEntitiesContext, subscribe: true })
_entityReg!: EntityRegistryEntry[]; _entityReg!: EntityRegistryEntry[];
@state()
@consume({ context: reorderModeContext, subscribe: true })
private _reorderMode?: ReorderMode;
protected render() { protected render() {
if (!this.condition) { if (!this.condition) {
return nothing; return nothing;
} }
const noReorderModeAvailable = this._reorderMode === undefined;
return html` return html`
<ha-card outlined> <ha-card outlined>
${this.condition.enabled === false ${this.condition.enabled === false
@ -163,7 +174,12 @@ export default class HaAutomationConditionRow extends LitElement {
></ha-svg-icon> ></ha-svg-icon>
</mwc-list-item> </mwc-list-item>
<mwc-list-item graphic="icon" .disabled=${this.disabled}> <mwc-list-item
graphic="icon"
.disabled=${this.disabled}
class=${classMap({ hidden: noReorderModeAvailable })}
?aria-hidden=${noReorderModeAvailable}
>
${this.hass.localize( ${this.hass.localize(
"ui.panel.config.automation.editor.conditions.re_order" "ui.panel.config.automation.editor.conditions.re_order"
)} )}
@ -297,7 +313,7 @@ export default class HaAutomationConditionRow extends LitElement {
.disabled=${this.disabled} .disabled=${this.disabled}
.hass=${this.hass} .hass=${this.hass}
.condition=${this.condition} .condition=${this.condition}
.reOrderMode=${this.reOrderMode} .path=${this.path}
></ha-automation-condition-editor> ></ha-automation-condition-editor>
</div> </div>
</ha-expansion-panel> </ha-expansion-panel>
@ -344,7 +360,7 @@ export default class HaAutomationConditionRow extends LitElement {
await this._renameCondition(); await this._renameCondition();
break; break;
case 2: case 2:
fireEvent(this, "re-order"); this._reorderMode?.enter();
break; break;
case 3: case 3:
fireEvent(this, "duplicate"); fireEvent(this, "duplicate");
@ -547,6 +563,9 @@ export default class HaAutomationConditionRow extends LitElement {
mwc-list-item[disabled] { mwc-list-item[disabled] {
--mdc-theme-text-primary-on-background: var(--disabled-text-color); --mdc-theme-text-primary-on-background: var(--disabled-text-color);
} }
mwc-list-item.hidden {
display: none;
}
.testing { .testing {
position: absolute; position: absolute;
top: 0px; top: 0px;

View File

@ -1,3 +1,4 @@
import { consume } from "@lit-labs/context";
import { mdiArrowDown, mdiArrowUp, mdiDrag, mdiPlus } from "@mdi/js"; import { mdiArrowDown, mdiArrowUp, mdiDrag, mdiPlus } from "@mdi/js";
import deepClone from "deep-clone-simple"; import deepClone from "deep-clone-simple";
import { import {
@ -8,10 +9,11 @@ import {
html, html,
nothing, nothing,
} from "lit"; } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { repeat } from "lit/directives/repeat"; import { repeat } from "lit/directives/repeat";
import { storage } from "../../../../common/decorators/storage"; import { storage } from "../../../../common/decorators/storage";
import { fireEvent } from "../../../../common/dom/fire_event"; import { fireEvent } from "../../../../common/dom/fire_event";
import { nestedArrayMove } from "../../../../common/util/array-move";
import "../../../../components/ha-button"; import "../../../../components/ha-button";
import "../../../../components/ha-button-menu"; import "../../../../components/ha-button-menu";
import "../../../../components/ha-sortable"; import "../../../../components/ha-sortable";
@ -20,7 +22,11 @@ import type {
AutomationClipboard, AutomationClipboard,
Condition, Condition,
} from "../../../../data/automation"; } from "../../../../data/automation";
import type { HomeAssistant } from "../../../../types"; import {
ReorderMode,
reorderModeContext,
} from "../../../../state/reorder-mode-mixin";
import type { HomeAssistant, ItemPath } from "../../../../types";
import { import {
PASTE_VALUE, PASTE_VALUE,
showAddAutomationElementDialog, showAddAutomationElementDialog,
@ -36,9 +42,11 @@ export default class HaAutomationCondition extends LitElement {
@property({ type: Boolean }) public disabled = false; @property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public nested = false; @property() public path?: ItemPath;
@property({ type: Boolean }) public reOrderMode = false; @state()
@consume({ context: reorderModeContext, subscribe: true })
private _reorderMode?: ReorderMode;
@storage({ @storage({
key: "automationClipboard", key: "automationClipboard",
@ -89,35 +97,21 @@ export default class HaAutomationCondition extends LitElement {
} }
} }
private get nested() {
return this.path !== undefined;
}
protected render() { protected render() {
if (!Array.isArray(this.conditions)) { if (!Array.isArray(this.conditions)) {
return nothing; return nothing;
} }
return html` return html`
${this.reOrderMode && !this.nested
? html`
<ha-alert
alert-type="info"
.title=${this.hass.localize(
"ui.panel.config.automation.editor.re_order_mode.title"
)}
>
${this.hass.localize(
"ui.panel.config.automation.editor.re_order_mode.description_conditions"
)}
<ha-button slot="action" @click=${this._exitReOrderMode}>
${this.hass.localize(
"ui.panel.config.automation.editor.re_order_mode.exit"
)}
</ha-button>
</ha-alert>
`
: null}
<ha-sortable <ha-sortable
handle-selector=".handle" handle-selector=".handle"
.disabled=${!this.reOrderMode} .disabled=${!this._reorderMode?.active}
@item-moved=${this._conditionMoved} @item-moved=${this._conditionMoved}
group="conditions"
.path=${this.path}
> >
<div class="conditions"> <div class="conditions">
${repeat( ${repeat(
@ -125,19 +119,18 @@ export default class HaAutomationCondition extends LitElement {
(condition) => this._getKey(condition), (condition) => this._getKey(condition),
(cond, idx) => html` (cond, idx) => html`
<ha-automation-condition-row <ha-automation-condition-row
.path=${[...(this.path ?? []), idx]}
.index=${idx} .index=${idx}
.totalConditions=${this.conditions.length} .totalConditions=${this.conditions.length}
.condition=${cond} .condition=${cond}
.hideMenu=${this.reOrderMode} .hideMenu=${Boolean(this._reorderMode?.active)}
.reOrderMode=${this.reOrderMode}
.disabled=${this.disabled} .disabled=${this.disabled}
@duplicate=${this._duplicateCondition} @duplicate=${this._duplicateCondition}
@move-condition=${this._move} @move-condition=${this._move}
@value-changed=${this._conditionChanged} @value-changed=${this._conditionChanged}
@re-order=${this._enterReOrderMode}
.hass=${this.hass} .hass=${this.hass}
> >
${this.reOrderMode ${this._reorderMode?.active
? html` ? html`
<ha-icon-button <ha-icon-button
.index=${idx} .index=${idx}
@ -232,16 +225,6 @@ export default class HaAutomationCondition extends LitElement {
fireEvent(this, "value-changed", { value: conditions }); fireEvent(this, "value-changed", { value: conditions });
}; };
private async _enterReOrderMode(ev: CustomEvent) {
if (this.nested) return;
ev.stopPropagation();
this.reOrderMode = true;
}
private async _exitReOrderMode() {
this.reOrderMode = false;
}
private _getKey(condition: Condition) { private _getKey(condition: Condition) {
if (!this._conditionKeys.has(condition)) { if (!this._conditionKeys.has(condition)) {
this._conditionKeys.set(condition, Math.random().toString()); this._conditionKeys.set(condition, Math.random().toString());
@ -262,17 +245,28 @@ export default class HaAutomationCondition extends LitElement {
this._move(index, newIndex); this._move(index, newIndex);
} }
private _move(index: number, newIndex: number) { private _move(
const conditions = this.conditions.concat(); oldIndex: number,
const condition = conditions.splice(index, 1)[0]; newIndex: number,
conditions.splice(newIndex, 0, condition); oldPath?: ItemPath,
newPath?: ItemPath
) {
const conditions = nestedArrayMove(
this.conditions,
oldIndex,
newIndex,
oldPath,
newPath
);
fireEvent(this, "value-changed", { value: conditions }); fireEvent(this, "value-changed", { value: conditions });
} }
private _conditionMoved(ev: CustomEvent): void { private _conditionMoved(ev: CustomEvent): void {
if (this.nested) return;
ev.stopPropagation(); ev.stopPropagation();
const { oldIndex, newIndex } = ev.detail; const { oldIndex, newIndex, oldPath, newPath } = ev.detail;
this._move(oldIndex, newIndex); this._move(oldIndex, newIndex, oldPath, newPath);
} }
private _conditionChanged(ev: CustomEvent) { private _conditionChanged(ev: CustomEvent) {

View File

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

View File

@ -5,6 +5,7 @@ import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { ensureArray } from "../../../common/array/ensure-array"; import { ensureArray } from "../../../common/array/ensure-array";
import { fireEvent } from "../../../common/dom/fire_event"; import { fireEvent } from "../../../common/dom/fire_event";
import { nestedArrayMove } from "../../../common/util/array-move";
import "../../../components/ha-card"; import "../../../components/ha-card";
import "../../../components/ha-icon-button"; import "../../../components/ha-icon-button";
import "../../../components/ha-markdown"; import "../../../components/ha-markdown";
@ -15,6 +16,7 @@ import {
} from "../../../data/automation"; } from "../../../data/automation";
import { Action } from "../../../data/script"; import { Action } from "../../../data/script";
import { haStyle } from "../../../resources/styles"; import { haStyle } from "../../../resources/styles";
import { ReorderModeMixin } from "../../../state/reorder-mode-mixin";
import type { HomeAssistant } from "../../../types"; import type { HomeAssistant } from "../../../types";
import { documentationUrl } from "../../../util/documentation-url"; import { documentationUrl } from "../../../util/documentation-url";
import "./action/ha-automation-action"; import "./action/ha-automation-action";
@ -22,7 +24,7 @@ import "./condition/ha-automation-condition";
import "./trigger/ha-automation-trigger"; import "./trigger/ha-automation-trigger";
@customElement("manual-automation-editor") @customElement("manual-automation-editor")
export class HaManualAutomationEditor extends LitElement { export class HaManualAutomationEditor extends ReorderModeMixin(LitElement) {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean }) public isWide!: boolean; @property({ type: Boolean }) public isWide!: boolean;
@ -44,7 +46,7 @@ export class HaManualAutomationEditor extends LitElement {
${this.hass.localize("ui.panel.config.automation.editor.migrate")} ${this.hass.localize("ui.panel.config.automation.editor.migrate")}
</mwc-button> </mwc-button>
</ha-alert>` </ha-alert>`
: ""} : nothing}
${this.stateObj?.state === "off" ${this.stateObj?.state === "off"
? html` ? html`
<ha-alert alert-type="info"> <ha-alert alert-type="info">
@ -92,12 +94,15 @@ export class HaManualAutomationEditor extends LitElement {
)} )}
</p>` </p>`
: nothing} : nothing}
${this._renderReorderModeAlert("triggers")}
<ha-automation-trigger <ha-automation-trigger
role="region" role="region"
aria-labelledby="triggers-heading" aria-labelledby="triggers-heading"
.triggers=${this.config.trigger} .triggers=${this.config.trigger}
.path=${["trigger"]}
@value-changed=${this._triggerChanged} @value-changed=${this._triggerChanged}
@item-moved=${this._itemMoved}
.hass=${this.hass} .hass=${this.hass}
.disabled=${this.disabled} .disabled=${this.disabled}
></ha-automation-trigger> ></ha-automation-trigger>
@ -132,12 +137,15 @@ export class HaManualAutomationEditor extends LitElement {
)} )}
</p>` </p>`
: nothing} : nothing}
${this._renderReorderModeAlert("conditions")}
<ha-automation-condition <ha-automation-condition
role="region" role="region"
aria-labelledby="conditions-heading" aria-labelledby="conditions-heading"
.conditions=${this.config.condition || []} .conditions=${this.config.condition || []}
.path=${["condition"]}
@value-changed=${this._conditionChanged} @value-changed=${this._conditionChanged}
@item-moved=${this._itemMoved}
.hass=${this.hass} .hass=${this.hass}
.disabled=${this.disabled} .disabled=${this.disabled}
></ha-automation-condition> ></ha-automation-condition>
@ -170,12 +178,15 @@ export class HaManualAutomationEditor extends LitElement {
)} )}
</p>` </p>`
: nothing} : nothing}
${this._renderReorderModeAlert("actions")}
<ha-automation-action <ha-automation-action
role="region" role="region"
aria-labelledby="actions-heading" aria-labelledby="actions-heading"
.actions=${this.config.action} .actions=${this.config.action}
.path=${["action"]}
@value-changed=${this._actionChanged} @value-changed=${this._actionChanged}
@item-moved=${this._itemMoved}
.hass=${this.hass} .hass=${this.hass}
.narrow=${this.narrow} .narrow=${this.narrow}
.disabled=${this.disabled} .disabled=${this.disabled}
@ -183,6 +194,34 @@ export class HaManualAutomationEditor extends LitElement {
`; `;
} }
private _renderReorderModeAlert(type: "conditions" | "actions" | "triggers") {
if (!this._reorderMode.active) {
return nothing;
}
return html`
<ha-alert
class="re-order"
alert-type="info"
.title=${this.hass.localize(
"ui.panel.config.automation.editor.re_order_mode.title"
)}
>
${this.hass.localize(
`ui.panel.config.automation.editor.re_order_mode.description_${type}`
)}
<ha-button slot="action" @click=${this._exitReOrderMode}>
${this.hass.localize(
"ui.panel.config.automation.editor.re_order_mode.exit"
)}
</ha-button>
</ha-alert>
`;
}
private async _exitReOrderMode() {
this._reorderMode.exit();
}
private _triggerChanged(ev: CustomEvent): void { private _triggerChanged(ev: CustomEvent): void {
ev.stopPropagation(); ev.stopPropagation();
fireEvent(this, "value-changed", { fireEvent(this, "value-changed", {
@ -207,6 +246,21 @@ export class HaManualAutomationEditor extends LitElement {
}); });
} }
private _itemMoved(ev: CustomEvent): void {
ev.stopPropagation();
const { oldIndex, newIndex, oldPath, newPath } = ev.detail;
const updatedConfig = nestedArrayMove(
this.config,
oldIndex,
newIndex,
oldPath,
newPath
);
fireEvent(this, "value-changed", {
value: updatedConfig,
});
}
private async _enable(): Promise<void> { private async _enable(): Promise<void> {
if (!this.hass || !this.stateObj) { if (!this.hass || !this.stateObj) {
return; return;
@ -258,6 +312,12 @@ export class HaManualAutomationEditor extends LitElement {
font-weight: normal; font-weight: normal;
line-height: 0; line-height: 0;
} }
ha-alert.re-order {
display: block;
margin-bottom: 16px;
border-radius: var(--ha-card-border-radius, 12px);
overflow: hidden;
}
`, `,
]; ];
} }

View File

@ -15,7 +15,14 @@ import {
mdiStopCircleOutline, mdiStopCircleOutline,
} from "@mdi/js"; } from "@mdi/js";
import type { UnsubscribeFunc } from "home-assistant-js-websocket"; import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import { CSSResultGroup, LitElement, PropertyValues, css, html } from "lit"; import {
CSSResultGroup,
LitElement,
PropertyValues,
css,
html,
nothing,
} from "lit";
import { customElement, property, query, state } from "lit/decorators"; import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map"; import { classMap } from "lit/directives/class-map";
import { storage } from "../../../../common/decorators/storage"; import { storage } from "../../../../common/decorators/storage";
@ -44,7 +51,7 @@ import {
showPromptDialog, showPromptDialog,
} from "../../../../dialogs/generic/show-dialog-box"; } from "../../../../dialogs/generic/show-dialog-box";
import { haStyle } from "../../../../resources/styles"; import { haStyle } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types"; import type { HomeAssistant, ItemPath } from "../../../../types";
import "./types/ha-automation-trigger-calendar"; import "./types/ha-automation-trigger-calendar";
import "./types/ha-automation-trigger-device"; import "./types/ha-automation-trigger-device";
import "./types/ha-automation-trigger-event"; import "./types/ha-automation-trigger-event";
@ -62,6 +69,10 @@ import "./types/ha-automation-trigger-time";
import "./types/ha-automation-trigger-time_pattern"; import "./types/ha-automation-trigger-time_pattern";
import "./types/ha-automation-trigger-webhook"; import "./types/ha-automation-trigger-webhook";
import "./types/ha-automation-trigger-zone"; import "./types/ha-automation-trigger-zone";
import {
ReorderMode,
reorderModeContext,
} from "../../../../state/reorder-mode-mixin";
export interface TriggerElement extends LitElement { export interface TriggerElement extends LitElement {
trigger: Trigger; trigger: Trigger;
@ -101,6 +112,8 @@ export default class HaAutomationTriggerRow extends LitElement {
@property({ type: Boolean }) public disabled = false; @property({ type: Boolean }) public disabled = false;
@property() public path?: ItemPath;
@state() private _warnings?: string[]; @state() private _warnings?: string[];
@state() private _yamlMode = false; @state() private _yamlMode = false;
@ -125,9 +138,17 @@ export default class HaAutomationTriggerRow extends LitElement {
@consume({ context: fullEntitiesContext, subscribe: true }) @consume({ context: fullEntitiesContext, subscribe: true })
_entityReg!: EntityRegistryEntry[]; _entityReg!: EntityRegistryEntry[];
@state()
@consume({ context: reorderModeContext, subscribe: true })
private _reorderMode?: ReorderMode;
private _triggerUnsub?: Promise<UnsubscribeFunc>; private _triggerUnsub?: Promise<UnsubscribeFunc>;
protected render() { protected render() {
if (!this.trigger) return nothing;
const noReorderModeAvailable = this._reorderMode === undefined;
const supported = const supported =
customElements.get(`ha-automation-trigger-${this.trigger.platform}`) !== customElements.get(`ha-automation-trigger-${this.trigger.platform}`) !==
undefined; undefined;
@ -181,7 +202,12 @@ export default class HaAutomationTriggerRow extends LitElement {
></ha-svg-icon> ></ha-svg-icon>
</mwc-list-item> </mwc-list-item>
<mwc-list-item graphic="icon" .disabled=${this.disabled}> <mwc-list-item
graphic="icon"
.disabled=${this.disabled}
class=${classMap({ hidden: noReorderModeAvailable })}
?aria-hidden=${noReorderModeAvailable}
>
${this.hass.localize( ${this.hass.localize(
"ui.panel.config.automation.editor.triggers.re_order" "ui.panel.config.automation.editor.triggers.re_order"
)} )}
@ -357,6 +383,7 @@ export default class HaAutomationTriggerRow extends LitElement {
hass: this.hass, hass: this.hass,
trigger: this.trigger, trigger: this.trigger,
disabled: this.disabled, disabled: this.disabled,
path: this.path,
} }
)} )}
</div> </div>
@ -470,7 +497,7 @@ export default class HaAutomationTriggerRow extends LitElement {
await this._renameTrigger(); await this._renameTrigger();
break; break;
case 1: case 1:
fireEvent(this, "re-order"); this._reorderMode?.enter();
break; break;
case 2: case 2:
this._requestShowId = true; this._requestShowId = true;
@ -702,6 +729,9 @@ export default class HaAutomationTriggerRow extends LitElement {
mwc-list-item[disabled] { mwc-list-item[disabled] {
--mdc-theme-text-primary-on-background: var(--disabled-text-color); --mdc-theme-text-primary-on-background: var(--disabled-text-color);
} }
mwc-list-item.hidden {
display: none;
}
ha-textfield { ha-textfield {
display: block; display: block;
margin-bottom: 24px; margin-bottom: 24px;

View File

@ -1,16 +1,22 @@
import { consume } from "@lit-labs/context";
import { mdiArrowDown, mdiArrowUp, mdiDrag, mdiPlus } from "@mdi/js"; import { mdiArrowDown, mdiArrowUp, mdiDrag, mdiPlus } from "@mdi/js";
import deepClone from "deep-clone-simple"; import deepClone from "deep-clone-simple";
import { CSSResultGroup, LitElement, PropertyValues, css, html } from "lit"; import { CSSResultGroup, LitElement, PropertyValues, css, html } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { repeat } from "lit/directives/repeat"; import { repeat } from "lit/directives/repeat";
import { storage } from "../../../../common/decorators/storage"; import { storage } from "../../../../common/decorators/storage";
import { fireEvent } from "../../../../common/dom/fire_event"; import { fireEvent } from "../../../../common/dom/fire_event";
import { nestedArrayMove } from "../../../../common/util/array-move";
import "../../../../components/ha-button"; import "../../../../components/ha-button";
import "../../../../components/ha-button-menu"; import "../../../../components/ha-button-menu";
import "../../../../components/ha-sortable"; import "../../../../components/ha-sortable";
import "../../../../components/ha-svg-icon"; import "../../../../components/ha-svg-icon";
import { AutomationClipboard, Trigger } from "../../../../data/automation"; import { AutomationClipboard, Trigger } from "../../../../data/automation";
import { HomeAssistant } from "../../../../types"; import {
ReorderMode,
reorderModeContext,
} from "../../../../state/reorder-mode-mixin";
import { HomeAssistant, ItemPath } from "../../../../types";
import { import {
PASTE_VALUE, PASTE_VALUE,
showAddAutomationElementDialog, showAddAutomationElementDialog,
@ -26,9 +32,11 @@ export default class HaAutomationTrigger extends LitElement {
@property({ type: Boolean }) public disabled = false; @property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public nested = false; @property() public path?: ItemPath;
@property({ type: Boolean }) public reOrderMode = false; @state()
@consume({ context: reorderModeContext, subscribe: true })
private _reorderMode?: ReorderMode;
@storage({ @storage({
key: "automationClipboard", key: "automationClipboard",
@ -42,31 +50,18 @@ export default class HaAutomationTrigger extends LitElement {
private _triggerKeys = new WeakMap<Trigger, string>(); private _triggerKeys = new WeakMap<Trigger, string>();
private get nested() {
return this.path !== undefined;
}
protected render() { protected render() {
return html` return html`
${this.reOrderMode && !this.nested
? html`
<ha-alert
alert-type="info"
.title=${this.hass.localize(
"ui.panel.config.automation.editor.re_order_mode.title"
)}
>
${this.hass.localize(
"ui.panel.config.automation.editor.re_order_mode.description_triggers"
)}
<ha-button slot="action" @click=${this._exitReOrderMode}>
${this.hass.localize(
"ui.panel.config.automation.editor.re_order_mode.exit"
)}
</ha-button>
</ha-alert>
`
: null}
<ha-sortable <ha-sortable
handle-selector=".handle" handle-selector=".handle"
.disabled=${!this.reOrderMode} .disabled=${!this._reorderMode?.active}
@item-moved=${this._triggerMoved} @item-moved=${this._triggerMoved}
group="triggers"
.path=${this.path}
> >
<div class="triggers"> <div class="triggers">
${repeat( ${repeat(
@ -74,16 +69,16 @@ export default class HaAutomationTrigger extends LitElement {
(trigger) => this._getKey(trigger), (trigger) => this._getKey(trigger),
(trg, idx) => html` (trg, idx) => html`
<ha-automation-trigger-row <ha-automation-trigger-row
.path=${[...(this.path ?? []), idx]}
.index=${idx} .index=${idx}
.trigger=${trg} .trigger=${trg}
.hideMenu=${this.reOrderMode} .hideMenu=${Boolean(this._reorderMode?.active)}
@duplicate=${this._duplicateTrigger} @duplicate=${this._duplicateTrigger}
@value-changed=${this._triggerChanged} @value-changed=${this._triggerChanged}
.hass=${this.hass} .hass=${this.hass}
.disabled=${this.disabled} .disabled=${this.disabled}
@re-order=${this._enterReOrderMode}
> >
${this.reOrderMode ${this._reorderMode?.active
? html` ? html`
<ha-icon-button <ha-icon-button
.index=${idx} .index=${idx}
@ -173,16 +168,6 @@ export default class HaAutomationTrigger extends LitElement {
} }
} }
private async _enterReOrderMode(ev: CustomEvent) {
if (this.nested) return;
ev.stopPropagation();
this.reOrderMode = true;
}
private async _exitReOrderMode() {
this.reOrderMode = false;
}
private _getKey(action: Trigger) { private _getKey(action: Trigger) {
if (!this._triggerKeys.has(action)) { if (!this._triggerKeys.has(action)) {
this._triggerKeys.set(action, Math.random().toString()); this._triggerKeys.set(action, Math.random().toString());
@ -203,17 +188,28 @@ export default class HaAutomationTrigger extends LitElement {
this._move(index, newIndex); this._move(index, newIndex);
} }
private _move(index: number, newIndex: number) { private _move(
const triggers = this.triggers.concat(); oldIndex: number,
const trigger = triggers.splice(index, 1)[0]; newIndex: number,
triggers.splice(newIndex, 0, trigger); oldPath?: ItemPath,
newPath?: ItemPath
) {
const triggers = nestedArrayMove(
this.triggers,
oldIndex,
newIndex,
oldPath,
newPath
);
fireEvent(this, "value-changed", { value: triggers }); fireEvent(this, "value-changed", { value: triggers });
} }
private _triggerMoved(ev: CustomEvent): void { private _triggerMoved(ev: CustomEvent): void {
if (this.nested) return;
ev.stopPropagation(); ev.stopPropagation();
const { oldIndex, newIndex } = ev.detail; const { oldIndex, newIndex, oldPath, newPath } = ev.detail;
this._move(oldIndex, newIndex); this._move(oldIndex, newIndex, oldPath, newPath);
} }
private _triggerChanged(ev: CustomEvent) { private _triggerChanged(ev: CustomEvent) {

View File

@ -1,13 +1,14 @@
import { css, CSSResultGroup, html, LitElement } from "lit"; import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event"; import { fireEvent } from "../../../common/dom/fire_event";
import { nestedArrayMove } from "../../../common/util/array-move";
import "../../../components/ha-alert";
import "../../../components/ha-blueprint-picker"; import "../../../components/ha-blueprint-picker";
import "../../../components/ha-card"; import "../../../components/ha-card";
import "../../../components/ha-circular-progress"; import "../../../components/ha-circular-progress";
import "../../../components/ha-markdown"; import "../../../components/ha-markdown";
import "../../../components/ha-selector/ha-selector"; import "../../../components/ha-selector/ha-selector";
import "../../../components/ha-settings-row"; import "../../../components/ha-settings-row";
import { import {
BlueprintOrError, BlueprintOrError,
Blueprints, Blueprints,
@ -17,9 +18,10 @@ import { BlueprintScriptConfig } from "../../../data/script";
import { haStyle } from "../../../resources/styles"; import { haStyle } from "../../../resources/styles";
import { HomeAssistant } from "../../../types"; import { HomeAssistant } from "../../../types";
import "../ha-config-section"; import "../ha-config-section";
import { ReorderModeMixin } from "../../../state/reorder-mode-mixin";
@customElement("blueprint-script-editor") @customElement("blueprint-script-editor")
export class HaBlueprintScriptEditor extends LitElement { export class HaBlueprintScriptEditor extends ReorderModeMixin(LitElement) {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean }) public isWide!: boolean; @property({ type: Boolean }) public isWide!: boolean;
@ -55,6 +57,7 @@ export class HaBlueprintScriptEditor extends LitElement {
</mwc-button> </mwc-button>
</ha-alert>` </ha-alert>`
: ""} : ""}
${this._renderReorderModeAlert()}
<ha-card <ha-card
outlined outlined
class="blueprint" class="blueprint"
@ -82,7 +85,6 @@ export class HaBlueprintScriptEditor extends LitElement {
) )
: html`<ha-circular-progress indeterminate></ha-circular-progress>`} : html`<ha-circular-progress indeterminate></ha-circular-progress>`}
</div> </div>
${this.config.use_blueprint.path ${this.config.use_blueprint.path
? blueprint && "error" in blueprint ? blueprint && "error" in blueprint
? html`<p class="warning padding"> ? html`<p class="warning padding">
@ -98,8 +100,23 @@ export class HaBlueprintScriptEditor extends LitElement {
${blueprint?.metadata?.input && ${blueprint?.metadata?.input &&
Object.keys(blueprint.metadata.input).length Object.keys(blueprint.metadata.input).length
? Object.entries(blueprint.metadata.input).map( ? Object.entries(blueprint.metadata.input).map(
([key, value]) => ([key, value]) => {
html`<ha-settings-row .narrow=${this.narrow}> 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}>
<span slot="heading">${value?.name || key}</span> <span slot="heading">${value?.name || key}</span>
<ha-markdown <ha-markdown
slot="description" slot="description"
@ -109,7 +126,7 @@ export class HaBlueprintScriptEditor extends LitElement {
></ha-markdown> ></ha-markdown>
${html`<ha-selector ${html`<ha-selector
.hass=${this.hass} .hass=${this.hass}
.selector=${value?.selector ?? { text: undefined }} .selector=${enhancedSelector}
.key=${key} .key=${key}
.disabled=${this.disabled} .disabled=${this.disabled}
.required=${value?.default === undefined} .required=${value?.default === undefined}
@ -119,8 +136,10 @@ export class HaBlueprintScriptEditor extends LitElement {
? this.config.use_blueprint.input[key] ? this.config.use_blueprint.input[key]
: value?.default} : value?.default}
@value-changed=${this._inputChanged} @value-changed=${this._inputChanged}
@item-moved=${this._itemMoved}
></ha-selector>`} ></ha-selector>`}
</ha-settings-row>` </ha-settings-row>`;
}
) )
: html`<p class="padding"> : html`<p class="padding">
${this.hass.localize( ${this.hass.localize(
@ -132,6 +151,34 @@ export class HaBlueprintScriptEditor extends LitElement {
`; `;
} }
private _renderReorderModeAlert() {
if (!this._reorderMode.active) {
return nothing;
}
return html`
<ha-alert
class="re-order"
alert-type="info"
.title=${this.hass.localize(
"ui.panel.config.automation.editor.re_order_mode.title"
)}
>
${this.hass.localize(
"ui.panel.config.automation.editor.re_order_mode.description_all"
)}
<ha-button slot="action" @click=${this._exitReOrderMode}>
${this.hass.localize(
"ui.panel.config.automation.editor.re_order_mode.exit"
)}
</ha-button>
</ha-alert>
`;
}
private async _exitReOrderMode() {
this._reorderMode.exit();
}
private async _getBlueprints() { private async _getBlueprints() {
this._blueprints = await fetchBlueprints(this.hass, "script"); this._blueprints = await fetchBlueprints(this.hass, "script");
} }
@ -176,6 +223,29 @@ export class HaBlueprintScriptEditor 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,
},
},
});
}
private _duplicate() { private _duplicate() {
fireEvent(this, "duplicate"); fireEvent(this, "duplicate");
} }
@ -229,6 +299,10 @@ export class HaBlueprintScriptEditor extends LitElement {
margin-bottom: 16px; margin-bottom: 16px;
display: block; display: block;
} }
ha-alert.re-order {
border-radius: var(--ha-card-border-radius, 12px);
overflow: hidden;
}
`, `,
]; ];
} }

View File

@ -3,10 +3,12 @@ import { mdiHelpCircle } from "@mdi/js";
import { CSSResultGroup, LitElement, css, html, nothing } from "lit"; import { CSSResultGroup, LitElement, css, html, nothing } from "lit";
import { customElement, property, query } from "lit/decorators"; import { customElement, property, query } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event"; import { fireEvent } from "../../../common/dom/fire_event";
import { nestedArrayMove } from "../../../common/util/array-move";
import "../../../components/ha-card"; import "../../../components/ha-card";
import "../../../components/ha-icon-button"; import "../../../components/ha-icon-button";
import { Action, Fields, ScriptConfig } from "../../../data/script"; import { Action, Fields, ScriptConfig } from "../../../data/script";
import { haStyle } from "../../../resources/styles"; import { haStyle } from "../../../resources/styles";
import { ReorderModeMixin } from "../../../state/reorder-mode-mixin";
import type { HomeAssistant } from "../../../types"; import type { HomeAssistant } from "../../../types";
import { documentationUrl } from "../../../util/documentation-url"; import { documentationUrl } from "../../../util/documentation-url";
import "../automation/action/ha-automation-action"; import "../automation/action/ha-automation-action";
@ -14,7 +16,7 @@ import "./ha-script-fields";
import type HaScriptFields from "./ha-script-fields"; import type HaScriptFields from "./ha-script-fields";
@customElement("manual-script-editor") @customElement("manual-script-editor")
export class HaManualScriptEditor extends LitElement { export class HaManualScriptEditor extends ReorderModeMixin(LitElement) {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean }) public isWide!: boolean; @property({ type: Boolean }) public isWide!: boolean;
@ -118,11 +120,15 @@ export class HaManualScriptEditor extends LitElement {
</a> </a>
</div> </div>
${this._renderReorderModeAlert()}
<ha-automation-action <ha-automation-action
role="region" role="region"
aria-labelledby="sequence-heading" aria-labelledby="sequence-heading"
.actions=${this.config.sequence} .actions=${this.config.sequence}
.path=${["sequence"]}
@value-changed=${this._sequenceChanged} @value-changed=${this._sequenceChanged}
@item-moved=${this._itemMoved}
.hass=${this.hass} .hass=${this.hass}
.narrow=${this.narrow} .narrow=${this.narrow}
.disabled=${this.disabled} .disabled=${this.disabled}
@ -130,6 +136,34 @@ export class HaManualScriptEditor extends LitElement {
`; `;
} }
private _renderReorderModeAlert() {
if (!this._reorderMode.active) {
return nothing;
}
return html`
<ha-alert
class="re-order"
alert-type="info"
.title=${this.hass.localize(
"ui.panel.config.automation.editor.re_order_mode.title"
)}
>
${this.hass.localize(
"ui.panel.config.automation.editor.re_order_mode.description_all"
)}
<ha-button slot="action" @click=${this._exitReOrderMode}>
${this.hass.localize(
"ui.panel.config.automation.editor.re_order_mode.exit"
)}
</ha-button>
</ha-alert>
`;
}
private async _exitReOrderMode() {
this._reorderMode.exit();
}
private _fieldsChanged(ev: CustomEvent): void { private _fieldsChanged(ev: CustomEvent): void {
ev.stopPropagation(); ev.stopPropagation();
fireEvent(this, "value-changed", { fireEvent(this, "value-changed", {
@ -144,6 +178,21 @@ 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,
});
}
private _duplicate() { private _duplicate() {
fireEvent(this, "duplicate"); fireEvent(this, "duplicate");
} }
@ -179,6 +228,12 @@ export class HaManualScriptEditor extends LitElement {
.header a { .header a {
color: var(--secondary-text-color); color: var(--secondary-text-color);
} }
ha-alert.re-order {
display: block;
margin-bottom: 16px;
border-radius: var(--ha-card-border-radius, 12px);
overflow: hidden;
}
`, `,
]; ];
} }

View File

@ -0,0 +1,40 @@
import { ContextProvider, createContext } from "@lit-labs/context";
import { LitElement } from "lit";
import { Constructor } from "../types";
export type ReorderMode = {
active: boolean;
enter: () => void;
exit: () => void;
};
export const reorderModeContext = createContext<ReorderMode>("reorder-mode");
export const ReorderModeMixin = <T extends Constructor<LitElement>>(
superClass: T
) =>
class extends superClass {
private _reorderModeProvider = new ContextProvider(this, {
context: reorderModeContext,
initialValue: {
active: false,
enter: () => {
this._reorderModeProvider.setValue({
...this._reorderModeProvider.value,
active: true,
});
this.requestUpdate("_reorderMode");
},
exit: () => {
this._reorderModeProvider.setValue({
...this._reorderModeProvider.value,
active: false,
});
this.requestUpdate("_reorderMode");
},
},
});
get _reorderMode() {
return this._reorderModeProvider.value;
}
};

View File

@ -2444,6 +2444,7 @@
"description_triggers": "You are in re-order mode, you can re-order your triggers.", "description_triggers": "You are in re-order mode, you can re-order your triggers.",
"description_conditions": "You are in re-order mode, you can re-order your conditions.", "description_conditions": "You are in re-order mode, you can re-order your conditions.",
"description_actions": "You are in re-order mode, you can re-order your actions.", "description_actions": "You are in re-order mode, you can re-order your actions.",
"description_all": "You are in re-order mode, you can re-order your triggers, conditions and actions.",
"exit": "Exit" "exit": "Exit"
}, },
"description": { "description": {

View File

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