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}
.actions=${this.value || []}
.hass=${this.hass}
.nested=${this.selector.action?.nested}
.reOrderMode=${this.selector.action?.reorder_mode}
.path=${this.selector.action?.path}
></ha-automation-action>
`;
}

View File

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

View File

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

View File

@ -40,6 +40,8 @@ import "./ha-service-picker";
import "./ha-settings-row";
import "./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) => {
if (typeof attribute === "object") {
@ -75,7 +77,7 @@ interface ExtHassService extends Omit<HassService, "fields"> {
}
@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 value?: {
@ -439,6 +441,7 @@ export class HaServiceControl extends LitElement {
allow-custom-entity
></ha-entity-picker>`
: ""}
${this._renderReorderModeAlert()}
${shouldRenderServiceDataYaml
? html`<ha-yaml-editor
.hass=${this.hass}
@ -449,7 +452,23 @@ export class HaServiceControl extends LitElement {
@value-changed=${this._dataChanged}
></ha-yaml-editor>`
: 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);
return dataField.selector &&
(!dataField.advanced ||
this.showAdvanced ||
@ -488,7 +507,7 @@ export class HaServiceControl extends LitElement {
(!this._value?.data ||
this._value.data[dataField.key] === undefined))}
.hass=${this.hass}
.selector=${dataField.selector}
.selector=${enhancedSelector}
.key=${dataField.key}
@value-changed=${this._serviceDataChanged}
.value=${this._value?.data
@ -496,12 +515,41 @@ export class HaServiceControl extends LitElement {
: undefined}
.placeholder=${dataField.default}
.localizeValue=${this._localizeValueCallback}
@item-moved=${this._itemMoved}
></ha-selector>
</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) => {
if (!this._value?.service) {
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) {
ev.stopPropagation();
if (!ev.detail.isValid) {

View File

@ -4,12 +4,15 @@ 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;
};
}
}
@ -21,6 +24,9 @@ export class HaSortable extends LitElement {
@property({ type: Boolean })
public disabled = false;
@property({ type: Boolean })
public path?: ItemPath;
@property({ type: Boolean, attribute: "no-style" })
public noStyle: boolean = false;
@ -30,6 +36,9 @@ export class HaSortable extends LitElement {
@property({ type: String, attribute: "handle-selector" })
public handleSelector?: string;
@property({ type: String, attribute: "group" })
public group?: string;
protected updated(changedProperties: PropertyValues<this>) {
if (changedProperties.has("disabled")) {
if (this.disabled) {
@ -100,6 +109,7 @@ export class HaSortable extends LitElement {
const options: SortableInstance.Options = {
animation: 150,
swapThreshold: 0.75,
onChoose: this._handleChoose,
onEnd: this._handleEnd,
};
@ -110,27 +120,41 @@ export class HaSortable extends LitElement {
if (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);
}
private _handleEnd = (evt: SortableEvent) => {
private _handleEnd = async (evt: SortableEvent) => {
// put back in original location
if ((evt.item as any).placeholder) {
(evt.item as any).placeholder.replaceWith(evt.item);
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 (
evt.oldIndex === undefined ||
evt.newIndex === undefined ||
evt.oldIndex === evt.newIndex
oldIndex === undefined ||
newIndex === undefined ||
(oldIndex === newIndex && oldPath?.join(".") === newPath?.join("."))
) {
return;
}
fireEvent(this, "item-moved", {
oldIndex: evt.oldIndex!,
newIndex: evt.newIndex!,
oldIndex,
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 { supportsFeature } from "../common/entity/supports-feature";
import { UiAction } from "../panels/lovelace/components/hui-action-editor";
import { HomeAssistant } from "../types";
import { HomeAssistant, ItemPath } from "../types";
import {
DeviceRegistryEntry,
getDeviceIntegrationLookup,
@ -59,8 +59,7 @@ export type Selector =
export interface ActionSelector {
action: {
reorder_mode?: boolean;
nested?: boolean;
path?: ItemPath;
} | null;
}
@ -113,8 +112,7 @@ export interface ColorTempSelector {
export interface ConditionSelector {
condition: {
reorder_mode?: boolean;
nested?: boolean;
path?: ItemPath;
} | null;
}
@ -392,8 +390,7 @@ export interface TimeSelector {
export interface TriggerSelector {
trigger: {
reorder_mode?: boolean;
nested?: boolean;
path?: ItemPath;
} | null;
}

View File

@ -57,7 +57,11 @@ import {
showPromptDialog,
} from "../../../../dialogs/generic/show-dialog-box";
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 "./types/ha-automation-action-activate_scene";
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 reOrderMode = false;
@property() public path?: ItemPath;
@storage({
key: "automationClipboard",
@ -143,6 +147,10 @@ export default class HaAutomationActionRow extends LitElement {
@consume({ context: fullEntitiesContext, subscribe: true })
_entityReg!: EntityRegistryEntry[];
@state()
@consume({ context: reorderModeContext, subscribe: true })
private _reorderMode?: ReorderMode;
@state() private _warnings?: string[];
@state() private _uiModeAvailable = true;
@ -176,9 +184,13 @@ export default class HaAutomationActionRow extends LitElement {
}
protected render() {
if (!this.action) return nothing;
const type = getType(this.action);
const yamlMode = this._yamlMode;
const noReorderModeAvailable = this._reorderMode === undefined;
return html`
<ha-card outlined>
${this.action.enabled === false
@ -247,7 +259,12 @@ export default class HaAutomationActionRow extends LitElement {
.path=${mdiRenameBox}
></ha-svg-icon>
</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(
"ui.panel.config.automation.editor.actions.re_order"
)}
@ -405,8 +422,8 @@ export default class HaAutomationActionRow extends LitElement {
hass: this.hass,
action: this.action,
narrow: this.narrow,
reOrderMode: this.reOrderMode,
disabled: this.disabled,
path: this.path,
})}
</div>
`}
@ -435,7 +452,7 @@ export default class HaAutomationActionRow extends LitElement {
await this._renameAction();
break;
case 2:
fireEvent(this, "re-order");
this._reorderMode?.enter();
break;
case 3:
fireEvent(this, "duplicate");
@ -640,6 +657,9 @@ export default class HaAutomationActionRow extends LitElement {
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;
}

View File

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

View File

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

View File

@ -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 } from "../../../../../types";
import type { HomeAssistant, ItemPath } from "../../../../../types";
import type { Condition } from "../../../../lovelace/common/validate-condition";
import "../ha-automation-action";
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({ 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;
@ -38,9 +38,8 @@ export class HaIfAction extends LitElement implements ActionElement {
)}*:
</h3>
<ha-automation-condition
nested
.path=${[...(this.path ?? []), "if"]}
.conditions=${action.if}
.reOrderMode=${this.reOrderMode}
.disabled=${this.disabled}
@value-changed=${this._ifChanged}
.hass=${this.hass}
@ -52,9 +51,8 @@ export class HaIfAction extends LitElement implements ActionElement {
)}*:
</h3>
<ha-automation-action
nested
.path=${[...(this.path ?? []), "then"]}
.actions=${action.then}
.reOrderMode=${this.reOrderMode}
.disabled=${this.disabled}
@value-changed=${this._thenChanged}
.hass=${this.hass}
@ -67,9 +65,8 @@ export class HaIfAction extends LitElement implements ActionElement {
)}:
</h3>
<ha-automation-action
nested
.path=${[...(this.path ?? []), "else"]}
.actions=${action.else || []}
.reOrderMode=${this.reOrderMode}
.disabled=${this.disabled}
@value-changed=${this._elseChanged}
.hass=${this.hass}

View File

@ -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 } from "../../../../../types";
import type { HomeAssistant, ItemPath } from "../../../../../types";
import "../ha-automation-action";
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({ 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() {
return {
@ -29,9 +29,8 @@ export class HaParallelAction extends LitElement implements ActionElement {
return html`
<ha-automation-action
nested
.path=${[...(this.path ?? []), "parallel"]}
.actions=${action.parallel}
.reOrderMode=${this.reOrderMode}
.disabled=${this.disabled}
@value-changed=${this._actionsChanged}
.hass=${this.hass}

View File

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

View File

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

View File

@ -1,15 +1,16 @@
import "@material/mwc-button/mwc-button";
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 { 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-card";
import "../../../components/ha-circular-progress";
import "../../../components/ha-markdown";
import "../../../components/ha-selector/ha-selector";
import "../../../components/ha-settings-row";
import "../../../components/ha-alert";
import { BlueprintAutomationConfig } from "../../../data/automation";
import {
BlueprintOrError,
@ -17,11 +18,12 @@ import {
fetchBlueprints,
} from "../../../data/blueprint";
import { haStyle } from "../../../resources/styles";
import { ReorderModeMixin } from "../../../state/reorder-mode-mixin";
import { HomeAssistant } from "../../../types";
import "../ha-config-section";
@customElement("blueprint-automation-editor")
export class HaBlueprintAutomationEditor extends LitElement {
export class HaBlueprintAutomationEditor extends ReorderModeMixin(LitElement) {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean }) public isWide = false;
@ -76,6 +78,7 @@ export class HaBlueprintAutomationEditor extends LitElement {
${this.config.description
? html`<p class="description">${this.config.description}</p>`
: ""}
${this._renderReorderModeAlert()}
<ha-card
outlined
class="blueprint"
@ -119,8 +122,23 @@ export class HaBlueprintAutomationEditor extends LitElement {
${blueprint?.metadata?.input &&
Object.keys(blueprint.metadata.input).length
? Object.entries(blueprint.metadata.input).map(
([key, value]) =>
html`<ha-settings-row .narrow=${this.narrow}>
([key, value]) => {
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>
<ha-markdown
slot="description"
@ -130,7 +148,7 @@ export class HaBlueprintAutomationEditor extends LitElement {
></ha-markdown>
${html`<ha-selector
.hass=${this.hass}
.selector=${value?.selector ?? { text: undefined }}
.selector=${enhancedSelector}
.key=${key}
.disabled=${this.disabled}
.required=${value?.default === undefined}
@ -140,8 +158,10 @@ export class HaBlueprintAutomationEditor extends LitElement {
? this.config.use_blueprint.input[key]
: value?.default}
@value-changed=${this._inputChanged}
@item-moved=${this._itemMoved}
></ha-selector>`}
</ha-settings-row>`
</ha-settings-row>`;
}
)
: html`<p class="padding">
${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() {
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> {
if (!this.hass || !this.stateObj) {
return;
@ -259,6 +330,10 @@ export class HaBlueprintAutomationEditor extends LitElement {
margin-bottom: 16px;
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 { expandConditionWithShorthand } from "../../../../data/automation";
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-device";
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 reOrderMode = false;
@property() public path?: ItemPath;
private _processedCondition = memoizeOne((condition) =>
expandConditionWithShorthand(condition)
@ -67,8 +67,8 @@ export default class HaAutomationConditionEditor extends LitElement {
{
hass: this.hass,
condition: condition,
reOrderMode: this.reOrderMode,
disabled: this.disabled,
path: this.path,
}
)}
</div>

View File

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

View File

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

View File

@ -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 } from "../../../../../types";
import type { HomeAssistant, ItemPath } from "../../../../../types";
import "../ha-automation-condition";
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 reOrderMode = false;
@property({ attribute: false }) public path?: ItemPath;
public static get defaultConfig() {
return {
@ -25,12 +25,11 @@ export class HaLogicalCondition extends LitElement implements ConditionElement {
protected render() {
return html`
<ha-automation-condition
nested
.path=${[...(this.path ?? []), "conditions"]}
.conditions=${this.condition.conditions || []}
@value-changed=${this._valueChanged}
.hass=${this.hass}
.disabled=${this.disabled}
.reOrderMode=${this.reOrderMode}
></ha-automation-condition>
`;
}

View File

@ -5,6 +5,7 @@ import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
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";
@ -15,6 +16,7 @@ import {
} from "../../../data/automation";
import { Action } from "../../../data/script";
import { haStyle } from "../../../resources/styles";
import { ReorderModeMixin } from "../../../state/reorder-mode-mixin";
import type { HomeAssistant } from "../../../types";
import { documentationUrl } from "../../../util/documentation-url";
import "./action/ha-automation-action";
@ -22,7 +24,7 @@ import "./condition/ha-automation-condition";
import "./trigger/ha-automation-trigger";
@customElement("manual-automation-editor")
export class HaManualAutomationEditor extends LitElement {
export class HaManualAutomationEditor extends ReorderModeMixin(LitElement) {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean }) public isWide!: boolean;
@ -44,7 +46,7 @@ export class HaManualAutomationEditor extends LitElement {
${this.hass.localize("ui.panel.config.automation.editor.migrate")}
</mwc-button>
</ha-alert>`
: ""}
: nothing}
${this.stateObj?.state === "off"
? html`
<ha-alert alert-type="info">
@ -92,12 +94,15 @@ export class HaManualAutomationEditor extends LitElement {
)}
</p>`
: nothing}
${this._renderReorderModeAlert("triggers")}
<ha-automation-trigger
role="region"
aria-labelledby="triggers-heading"
.triggers=${this.config.trigger}
.path=${["trigger"]}
@value-changed=${this._triggerChanged}
@item-moved=${this._itemMoved}
.hass=${this.hass}
.disabled=${this.disabled}
></ha-automation-trigger>
@ -132,12 +137,15 @@ export class HaManualAutomationEditor extends LitElement {
)}
</p>`
: nothing}
${this._renderReorderModeAlert("conditions")}
<ha-automation-condition
role="region"
aria-labelledby="conditions-heading"
.conditions=${this.config.condition || []}
.path=${["condition"]}
@value-changed=${this._conditionChanged}
@item-moved=${this._itemMoved}
.hass=${this.hass}
.disabled=${this.disabled}
></ha-automation-condition>
@ -170,12 +178,15 @@ export class HaManualAutomationEditor extends LitElement {
)}
</p>`
: nothing}
${this._renderReorderModeAlert("actions")}
<ha-automation-action
role="region"
aria-labelledby="actions-heading"
.actions=${this.config.action}
.path=${["action"]}
@value-changed=${this._actionChanged}
@item-moved=${this._itemMoved}
.hass=${this.hass}
.narrow=${this.narrow}
.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 {
ev.stopPropagation();
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> {
if (!this.hass || !this.stateObj) {
return;
@ -258,6 +312,12 @@ export class HaManualAutomationEditor extends LitElement {
font-weight: normal;
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,
} from "@mdi/js";
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 { classMap } from "lit/directives/class-map";
import { storage } from "../../../../common/decorators/storage";
@ -44,7 +51,7 @@ import {
showPromptDialog,
} from "../../../../dialogs/generic/show-dialog-box";
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-device";
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-webhook";
import "./types/ha-automation-trigger-zone";
import {
ReorderMode,
reorderModeContext,
} from "../../../../state/reorder-mode-mixin";
export interface TriggerElement extends LitElement {
trigger: Trigger;
@ -101,6 +112,8 @@ export default class HaAutomationTriggerRow extends LitElement {
@property({ type: Boolean }) public disabled = false;
@property() public path?: ItemPath;
@state() private _warnings?: string[];
@state() private _yamlMode = false;
@ -125,9 +138,17 @@ export default class HaAutomationTriggerRow extends LitElement {
@consume({ context: fullEntitiesContext, subscribe: true })
_entityReg!: EntityRegistryEntry[];
@state()
@consume({ context: reorderModeContext, subscribe: true })
private _reorderMode?: ReorderMode;
private _triggerUnsub?: Promise<UnsubscribeFunc>;
protected render() {
if (!this.trigger) return nothing;
const noReorderModeAvailable = this._reorderMode === undefined;
const supported =
customElements.get(`ha-automation-trigger-${this.trigger.platform}`) !==
undefined;
@ -181,7 +202,12 @@ export default class HaAutomationTriggerRow extends LitElement {
></ha-svg-icon>
</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(
"ui.panel.config.automation.editor.triggers.re_order"
)}
@ -357,6 +383,7 @@ export default class HaAutomationTriggerRow extends LitElement {
hass: this.hass,
trigger: this.trigger,
disabled: this.disabled,
path: this.path,
}
)}
</div>
@ -470,7 +497,7 @@ export default class HaAutomationTriggerRow extends LitElement {
await this._renameTrigger();
break;
case 1:
fireEvent(this, "re-order");
this._reorderMode?.enter();
break;
case 2:
this._requestShowId = true;
@ -702,6 +729,9 @@ export default class HaAutomationTriggerRow extends LitElement {
mwc-list-item[disabled] {
--mdc-theme-text-primary-on-background: var(--disabled-text-color);
}
mwc-list-item.hidden {
display: none;
}
ha-textfield {
display: block;
margin-bottom: 24px;

View File

@ -1,16 +1,22 @@
import { consume } from "@lit-labs/context";
import { mdiArrowDown, mdiArrowUp, mdiDrag, mdiPlus } from "@mdi/js";
import deepClone from "deep-clone-simple";
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 { storage } from "../../../../common/decorators/storage";
import { fireEvent } from "../../../../common/dom/fire_event";
import { nestedArrayMove } from "../../../../common/util/array-move";
import "../../../../components/ha-button";
import "../../../../components/ha-button-menu";
import "../../../../components/ha-sortable";
import "../../../../components/ha-svg-icon";
import { AutomationClipboard, Trigger } from "../../../../data/automation";
import { HomeAssistant } from "../../../../types";
import {
ReorderMode,
reorderModeContext,
} from "../../../../state/reorder-mode-mixin";
import { HomeAssistant, ItemPath } from "../../../../types";
import {
PASTE_VALUE,
showAddAutomationElementDialog,
@ -26,9 +32,11 @@ export default class HaAutomationTrigger extends LitElement {
@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({
key: "automationClipboard",
@ -42,31 +50,18 @@ export default class HaAutomationTrigger extends LitElement {
private _triggerKeys = new WeakMap<Trigger, string>();
private get nested() {
return this.path !== undefined;
}
protected render() {
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
handle-selector=".handle"
.disabled=${!this.reOrderMode}
.disabled=${!this._reorderMode?.active}
@item-moved=${this._triggerMoved}
group="triggers"
.path=${this.path}
>
<div class="triggers">
${repeat(
@ -74,16 +69,16 @@ export default class HaAutomationTrigger extends LitElement {
(trigger) => this._getKey(trigger),
(trg, idx) => html`
<ha-automation-trigger-row
.path=${[...(this.path ?? []), idx]}
.index=${idx}
.trigger=${trg}
.hideMenu=${this.reOrderMode}
.hideMenu=${Boolean(this._reorderMode?.active)}
@duplicate=${this._duplicateTrigger}
@value-changed=${this._triggerChanged}
.hass=${this.hass}
.disabled=${this.disabled}
@re-order=${this._enterReOrderMode}
>
${this.reOrderMode
${this._reorderMode?.active
? html`
<ha-icon-button
.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) {
if (!this._triggerKeys.has(action)) {
this._triggerKeys.set(action, Math.random().toString());
@ -203,17 +188,28 @@ export default class HaAutomationTrigger extends LitElement {
this._move(index, newIndex);
}
private _move(index: number, newIndex: number) {
const triggers = this.triggers.concat();
const trigger = triggers.splice(index, 1)[0];
triggers.splice(newIndex, 0, trigger);
private _move(
oldIndex: number,
newIndex: number,
oldPath?: ItemPath,
newPath?: ItemPath
) {
const triggers = nestedArrayMove(
this.triggers,
oldIndex,
newIndex,
oldPath,
newPath
);
fireEvent(this, "value-changed", { value: triggers });
}
private _triggerMoved(ev: CustomEvent): void {
if (this.nested) return;
ev.stopPropagation();
const { oldIndex, newIndex } = ev.detail;
this._move(oldIndex, newIndex);
const { oldIndex, newIndex, oldPath, newPath } = ev.detail;
this._move(oldIndex, newIndex, oldPath, newPath);
}
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 { 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-card";
import "../../../components/ha-circular-progress";
import "../../../components/ha-markdown";
import "../../../components/ha-selector/ha-selector";
import "../../../components/ha-settings-row";
import {
BlueprintOrError,
Blueprints,
@ -17,9 +18,10 @@ import { BlueprintScriptConfig } from "../../../data/script";
import { haStyle } from "../../../resources/styles";
import { HomeAssistant } from "../../../types";
import "../ha-config-section";
import { ReorderModeMixin } from "../../../state/reorder-mode-mixin";
@customElement("blueprint-script-editor")
export class HaBlueprintScriptEditor extends LitElement {
export class HaBlueprintScriptEditor extends ReorderModeMixin(LitElement) {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean }) public isWide!: boolean;
@ -55,6 +57,7 @@ export class HaBlueprintScriptEditor extends LitElement {
</mwc-button>
</ha-alert>`
: ""}
${this._renderReorderModeAlert()}
<ha-card
outlined
class="blueprint"
@ -82,7 +85,6 @@ export class HaBlueprintScriptEditor extends LitElement {
)
: html`<ha-circular-progress indeterminate></ha-circular-progress>`}
</div>
${this.config.use_blueprint.path
? blueprint && "error" in blueprint
? html`<p class="warning padding">
@ -98,8 +100,23 @@ export class HaBlueprintScriptEditor extends LitElement {
${blueprint?.metadata?.input &&
Object.keys(blueprint.metadata.input).length
? Object.entries(blueprint.metadata.input).map(
([key, value]) =>
html`<ha-settings-row .narrow=${this.narrow}>
([key, value]) => {
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>
<ha-markdown
slot="description"
@ -109,7 +126,7 @@ export class HaBlueprintScriptEditor extends LitElement {
></ha-markdown>
${html`<ha-selector
.hass=${this.hass}
.selector=${value?.selector ?? { text: undefined }}
.selector=${enhancedSelector}
.key=${key}
.disabled=${this.disabled}
.required=${value?.default === undefined}
@ -119,8 +136,10 @@ export class HaBlueprintScriptEditor extends LitElement {
? this.config.use_blueprint.input[key]
: value?.default}
@value-changed=${this._inputChanged}
@item-moved=${this._itemMoved}
></ha-selector>`}
</ha-settings-row>`
</ha-settings-row>`;
}
)
: html`<p class="padding">
${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() {
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() {
fireEvent(this, "duplicate");
}
@ -229,6 +299,10 @@ export class HaBlueprintScriptEditor extends LitElement {
margin-bottom: 16px;
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 { customElement, property, query } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event";
import { nestedArrayMove } from "../../../common/util/array-move";
import "../../../components/ha-card";
import "../../../components/ha-icon-button";
import { Action, Fields, ScriptConfig } from "../../../data/script";
import { haStyle } from "../../../resources/styles";
import { ReorderModeMixin } from "../../../state/reorder-mode-mixin";
import type { HomeAssistant } from "../../../types";
import { documentationUrl } from "../../../util/documentation-url";
import "../automation/action/ha-automation-action";
@ -14,7 +16,7 @@ import "./ha-script-fields";
import type HaScriptFields from "./ha-script-fields";
@customElement("manual-script-editor")
export class HaManualScriptEditor extends LitElement {
export class HaManualScriptEditor extends ReorderModeMixin(LitElement) {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean }) public isWide!: boolean;
@ -118,11 +120,15 @@ export class HaManualScriptEditor extends LitElement {
</a>
</div>
${this._renderReorderModeAlert()}
<ha-automation-action
role="region"
aria-labelledby="sequence-heading"
.actions=${this.config.sequence}
.path=${["sequence"]}
@value-changed=${this._sequenceChanged}
@item-moved=${this._itemMoved}
.hass=${this.hass}
.narrow=${this.narrow}
.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 {
ev.stopPropagation();
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() {
fireEvent(this, "duplicate");
}
@ -179,6 +228,12 @@ export class HaManualScriptEditor extends LitElement {
.header a {
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_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_all": "You are in re-order mode, you can re-order your triggers, conditions and actions.",
"exit": "Exit"
},
"description": {

View File

@ -294,3 +294,5 @@ 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)[];