Reorder automation elements (#13548)

This commit is contained in:
Paul Bottein 2022-09-05 14:19:38 +02:00 committed by GitHub
parent 02d608b704
commit ab745f6e8e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 819 additions and 431 deletions

View File

@ -1,8 +1,6 @@
import { ActionDetail } from "@material/mwc-list/mwc-list-foundation"; import { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
import "@material/mwc-list/mwc-list-item"; import "@material/mwc-list/mwc-list-item";
import { import {
mdiArrowDown,
mdiArrowUp,
mdiCheck, mdiCheck,
mdiContentDuplicate, mdiContentDuplicate,
mdiDelete, mdiDelete,
@ -17,13 +15,15 @@ import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map"; import { classMap } from "lit/directives/class-map";
import { dynamicElement } from "../../../../common/dom/dynamic-element-directive"; import { dynamicElement } from "../../../../common/dom/dynamic-element-directive";
import { fireEvent } from "../../../../common/dom/fire_event"; import { fireEvent } from "../../../../common/dom/fire_event";
import { capitalizeFirstLetter } from "../../../../common/string/capitalize-first-letter";
import { handleStructError } from "../../../../common/structs/handle-errors"; import { handleStructError } from "../../../../common/structs/handle-errors";
import "../../../../components/ha-alert"; import "../../../../components/ha-alert";
import "../../../../components/ha-button-menu"; import "../../../../components/ha-button-menu";
import "../../../../components/ha-card"; import "../../../../components/ha-card";
import "../../../../components/ha-icon-button";
import "../../../../components/ha-expansion-panel"; import "../../../../components/ha-expansion-panel";
import "../../../../components/ha-icon-button";
import type { HaYamlEditor } from "../../../../components/ha-yaml-editor"; import type { HaYamlEditor } from "../../../../components/ha-yaml-editor";
import { ACTION_TYPES } from "../../../../data/action";
import { validateConfig } from "../../../../data/config"; import { validateConfig } from "../../../../data/config";
import { Action, getActionType } from "../../../../data/script"; import { Action, getActionType } from "../../../../data/script";
import { describeAction } from "../../../../data/script_i18n"; import { describeAction } from "../../../../data/script_i18n";
@ -50,8 +50,6 @@ import "./types/ha-automation-action-service";
import "./types/ha-automation-action-stop"; import "./types/ha-automation-action-stop";
import "./types/ha-automation-action-wait_for_trigger"; import "./types/ha-automation-action-wait_for_trigger";
import "./types/ha-automation-action-wait_template"; import "./types/ha-automation-action-wait_template";
import { ACTION_TYPES } from "../../../../data/action";
import { capitalizeFirstLetter } from "../../../../common/string/capitalize-first-letter";
const getType = (action: Action | undefined) => { const getType = (action: Action | undefined) => {
if (!action) { if (!action) {
@ -66,13 +64,6 @@ const getType = (action: Action | undefined) => {
return Object.keys(ACTION_TYPES).find((option) => option in action); return Object.keys(ACTION_TYPES).find((option) => option in action);
}; };
declare global {
// for fire event
interface HASSDomEvents {
"move-action": { direction: "up" | "down" };
}
}
export interface ActionElement extends LitElement { export interface ActionElement extends LitElement {
action: Action; action: Action;
} }
@ -107,12 +98,12 @@ export default class HaAutomationActionRow extends LitElement {
@property() public action!: Action; @property() public action!: Action;
@property() public index!: number;
@property() public totalActions!: number;
@property({ type: Boolean }) public narrow = false; @property({ type: Boolean }) public narrow = false;
@property({ type: Boolean }) public hideMenu = false;
@property({ type: Boolean }) public reOrderMode = false;
@state() private _warnings?: string[]; @state() private _warnings?: string[];
@state() private _uiModeAvailable = true; @state() private _uiModeAvailable = true;
@ -165,30 +156,10 @@ export default class HaAutomationActionRow extends LitElement {
${capitalizeFirstLetter(describeAction(this.hass, this.action))} ${capitalizeFirstLetter(describeAction(this.hass, this.action))}
</h3> </h3>
${this.index !== 0 <slot name="icons" slot="icons"></slot>
? html` ${this.hideMenu
<ha-icon-button ? ""
slot="icons" : html`
.label=${this.hass.localize(
"ui.panel.config.automation.editor.move_up"
)}
.path=${mdiArrowUp}
@click=${this._moveUp}
></ha-icon-button>
`
: ""}
${this.index !== this.totalActions - 1
? html`
<ha-icon-button
slot="icons"
.label=${this.hass.localize(
"ui.panel.config.automation.editor.move_down"
)}
.path=${mdiArrowDown}
@click=${this._moveDown}
></ha-icon-button>
`
: ""}
<ha-button-menu <ha-button-menu
slot="icons" slot="icons"
fixed fixed
@ -212,7 +183,10 @@ export default class HaAutomationActionRow extends LitElement {
${this.hass.localize( ${this.hass.localize(
"ui.panel.config.automation.editor.actions.rename" "ui.panel.config.automation.editor.actions.rename"
)} )}
<ha-svg-icon slot="graphic" .path=${mdiRenameBox}></ha-svg-icon> <ha-svg-icon
slot="graphic"
.path=${mdiRenameBox}
></ha-svg-icon>
</mwc-list-item> </mwc-list-item>
<mwc-list-item graphic="icon"> <mwc-list-item graphic="icon">
${this.hass.localize( ${this.hass.localize(
@ -226,8 +200,13 @@ export default class HaAutomationActionRow extends LitElement {
<li divider role="separator"></li> <li divider role="separator"></li>
<mwc-list-item .disabled=${!this._uiModeAvailable} graphic="icon"> <mwc-list-item
${this.hass.localize("ui.panel.config.automation.editor.edit_ui")} .disabled=${!this._uiModeAvailable}
graphic="icon"
>
${this.hass.localize(
"ui.panel.config.automation.editor.edit_ui"
)}
${!yamlMode ${!yamlMode
? html`<ha-svg-icon ? html`<ha-svg-icon
class="selected_menu_item" class="selected_menu_item"
@ -237,7 +216,10 @@ export default class HaAutomationActionRow extends LitElement {
: ``} : ``}
</mwc-list-item> </mwc-list-item>
<mwc-list-item .disabled=${!this._uiModeAvailable} graphic="icon"> <mwc-list-item
.disabled=${!this._uiModeAvailable}
graphic="icon"
>
${this.hass.localize( ${this.hass.localize(
"ui.panel.config.automation.editor.edit_yaml" "ui.panel.config.automation.editor.edit_yaml"
)} )}
@ -278,6 +260,8 @@ export default class HaAutomationActionRow extends LitElement {
></ha-svg-icon> ></ha-svg-icon>
</mwc-list-item> </mwc-list-item>
</ha-button-menu> </ha-button-menu>
`}
<div <div
class=${classMap({ class=${classMap({
"card-content": true, "card-content": true,
@ -327,6 +311,7 @@ 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,
})} })}
</div> </div>
`} `}
@ -346,16 +331,6 @@ export default class HaAutomationActionRow extends LitElement {
} }
} }
private _moveUp(ev) {
ev.preventDefault();
fireEvent(this, "move-action", { direction: "up" });
}
private _moveDown(ev) {
ev.preventDefault();
fireEvent(this, "move-action", { direction: "down" });
}
private async _handleAction(ev: CustomEvent<ActionDetail>) { private async _handleAction(ev: CustomEvent<ActionDetail>) {
switch (ev.detail.index) { switch (ev.detail.index) {
case 0: case 0:

View File

@ -1,15 +1,25 @@
import { repeat } from "lit/directives/repeat";
import { mdiPlus } from "@mdi/js";
import deepClone from "deep-clone-simple";
import "@material/mwc-button"; import "@material/mwc-button";
import type { ActionDetail } from "@material/mwc-list"; import type { ActionDetail } from "@material/mwc-list";
import memoizeOne from "memoize-one"; import { mdiArrowDown, mdiArrowUp, mdiDrag, mdiPlus } from "@mdi/js";
import deepClone from "deep-clone-simple";
import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit"; import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
import memoizeOne from "memoize-one";
import type { SortableEvent } from "sortablejs";
import { fireEvent } from "../../../../common/dom/fire_event"; import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-svg-icon"; import { stringCompare } from "../../../../common/string/compare";
import { LocalizeFunc } from "../../../../common/translations/localize";
import "../../../../components/ha-button-menu"; import "../../../../components/ha-button-menu";
import type { HaSelect } from "../../../../components/ha-select";
import "../../../../components/ha-svg-icon";
import { ACTION_TYPES } from "../../../../data/action";
import { Action } from "../../../../data/script"; import { Action } from "../../../../data/script";
import { sortableStyles } from "../../../../resources/ha-sortable-style";
import {
loadSortable,
SortableInstance,
} from "../../../../resources/sortable.ondemand";
import { HomeAssistant } from "../../../../types"; import { HomeAssistant } from "../../../../types";
import "./ha-automation-action-row"; import "./ha-automation-action-row";
import type HaAutomationActionRow from "./ha-automation-action-row"; import type HaAutomationActionRow from "./ha-automation-action-row";
@ -27,10 +37,6 @@ import "./types/ha-automation-action-service";
import "./types/ha-automation-action-stop"; import "./types/ha-automation-action-stop";
import "./types/ha-automation-action-wait_for_trigger"; import "./types/ha-automation-action-wait_for_trigger";
import "./types/ha-automation-action-wait_template"; import "./types/ha-automation-action-wait_template";
import { ACTION_TYPES } from "../../../../data/action";
import { stringCompare } from "../../../../common/string/compare";
import { LocalizeFunc } from "../../../../common/translations/localize";
import type { HaSelect } from "../../../../components/ha-select";
@customElement("ha-automation-action") @customElement("ha-automation-action")
export default class HaAutomationAction extends LitElement { export default class HaAutomationAction extends LitElement {
@ -40,28 +46,62 @@ export default class HaAutomationAction extends LitElement {
@property() public actions!: Action[]; @property() public actions!: Action[];
@property({ type: Boolean }) public reOrderMode = false;
private _focusLastActionOnChange = false; private _focusLastActionOnChange = false;
private _actionKeys = new WeakMap<Action, string>(); private _actionKeys = new WeakMap<Action, string>();
private _sortable?: SortableInstance;
protected render() { protected render() {
return html` return html`
<div class="actions">
${repeat( ${repeat(
this.actions, this.actions,
(action) => this._getKey(action), (action) => this._getKey(action),
(action, idx) => html` (action, idx) => html`
<ha-automation-action-row <ha-automation-action-row
.index=${idx} .index=${idx}
.totalActions=${this.actions.length}
.action=${action} .action=${action}
.narrow=${this.narrow} .narrow=${this.narrow}
.hideMenu=${this.reOrderMode}
.reOrderMode=${this.reOrderMode}
@duplicate=${this._duplicateAction} @duplicate=${this._duplicateAction}
@move-action=${this._move}
@value-changed=${this._actionChanged} @value-changed=${this._actionChanged}
.hass=${this.hass} .hass=${this.hass}
></ha-automation-action-row> >
${this.reOrderMode
? html`
<ha-icon-button
.index=${idx}
slot="icons"
.label=${this.hass.localize(
"ui.panel.config.automation.editor.move_up"
)}
.path=${mdiArrowUp}
@click=${this._moveUp}
.disabled=${idx === 0}
></ha-icon-button>
<ha-icon-button
.index=${idx}
slot="icons"
.label=${this.hass.localize(
"ui.panel.config.automation.editor.move_down"
)}
.path=${mdiArrowDown}
@click=${this._moveDown}
.disabled=${idx === this.actions.length - 1}
></ha-icon-button>
<div class="handle" slot="icons">
<ha-svg-icon .path=${mdiDrag}></ha-svg-icon>
</div>
`
: ""}
</ha-automation-action-row>
` `
)} )}
</div>
<ha-button-menu fixed @action=${this._addAction}> <ha-button-menu fixed @action=${this._addAction}>
<mwc-button <mwc-button
slot="trigger" slot="trigger"
@ -86,6 +126,13 @@ export default class HaAutomationAction extends LitElement {
protected updated(changedProps: PropertyValues) { protected updated(changedProps: PropertyValues) {
super.updated(changedProps); super.updated(changedProps);
if (changedProps.has("reOrderMode")) {
if (this.reOrderMode) {
this._createSortable();
} else {
this._destroySortable();
}
}
if (changedProps.has("actions") && this._focusLastActionOnChange) { if (changedProps.has("actions") && this._focusLastActionOnChange) {
this._focusLastActionOnChange = false; this._focusLastActionOnChange = false;
@ -100,6 +147,33 @@ export default class HaAutomationAction extends LitElement {
} }
} }
private async _createSortable() {
const Sortable = await loadSortable();
this._sortable = new Sortable(this.shadowRoot!.querySelector(".actions")!, {
animation: 150,
fallbackClass: "sortable-fallback",
handle: ".handle",
onChoose: (evt: SortableEvent) => {
(evt.item as any).placeholder =
document.createComment("sort-placeholder");
evt.item.after((evt.item as any).placeholder);
},
onEnd: (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;
}
this._dragged(evt);
},
});
}
private _destroySortable() {
this._sortable?.destroy();
this._sortable = undefined;
}
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());
@ -121,12 +195,24 @@ export default class HaAutomationAction extends LitElement {
fireEvent(this, "value-changed", { value: actions }); fireEvent(this, "value-changed", { value: actions });
} }
private _move(ev: CustomEvent) { private _moveUp(ev) {
// Prevent possible parent action-row from also moving
ev.stopPropagation();
const index = (ev.target as any).index; const index = (ev.target as any).index;
const newIndex = ev.detail.direction === "up" ? index - 1 : index + 1; const newIndex = index - 1;
this._move(index, newIndex);
}
private _moveDown(ev) {
const index = (ev.target as any).index;
const newIndex = index + 1;
this._move(index, newIndex);
}
private _dragged(ev: SortableEvent): void {
if (ev.oldIndex === ev.newIndex) return;
this._move(ev.oldIndex!, ev.newIndex!);
}
private _move(index: number, newIndex: number) {
const actions = this.actions.concat(); const actions = this.actions.concat();
const action = actions.splice(index, 1)[0]; const action = actions.splice(index, 1)[0];
actions.splice(newIndex, 0, action); actions.splice(newIndex, 0, action);
@ -177,7 +263,9 @@ export default class HaAutomationAction extends LitElement {
); );
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return css` return [
sortableStyles,
css`
ha-automation-action-row { ha-automation-action-row {
display: block; display: block;
margin-bottom: 16px; margin-bottom: 16px;
@ -186,7 +274,16 @@ export default class HaAutomationAction extends LitElement {
ha-svg-icon { ha-svg-icon {
height: 20px; height: 20px;
} }
`; .handle {
cursor: move;
padding: 12px;
}
.handle ha-svg-icon {
pointer-events: none;
height: 24px;
}
`,
];
} }
} }

View File

@ -17,6 +17,8 @@ export class HaChooseAction extends LitElement implements ActionElement {
@property() public action!: ChooseAction; @property() public action!: ChooseAction;
@property({ type: Boolean }) public reOrderMode = false;
@state() private _showDefault = false; @state() private _showDefault = false;
public static get defaultConfig() { public static get defaultConfig() {
@ -52,6 +54,7 @@ export class HaChooseAction extends LitElement implements ActionElement {
</h3> </h3>
<ha-automation-condition <ha-automation-condition
.conditions=${option.conditions} .conditions=${option.conditions}
.reOrderMode=${this.reOrderMode}
.hass=${this.hass} .hass=${this.hass}
.idx=${idx} .idx=${idx}
@value-changed=${this._conditionChanged} @value-changed=${this._conditionChanged}
@ -89,6 +92,7 @@ export class HaChooseAction extends LitElement implements ActionElement {
</h2> </h2>
<ha-automation-action <ha-automation-action
.actions=${action.default || []} .actions=${action.default || []}
.reOrderMode=${this.reOrderMode}
@value-changed=${this._defaultChanged} @value-changed=${this._defaultChanged}
.hass=${this.hass} .hass=${this.hass}
></ha-automation-action> ></ha-automation-action>

View File

@ -15,6 +15,8 @@ export class HaIfAction extends LitElement implements ActionElement {
@property({ attribute: false }) public action!: IfAction; @property({ attribute: false }) public action!: IfAction;
@property({ type: Boolean }) public reOrderMode = false;
@state() private _showElse = false; @state() private _showElse = false;
public static get defaultConfig() { public static get defaultConfig() {
@ -35,8 +37,9 @@ export class HaIfAction extends LitElement implements ActionElement {
</h3> </h3>
<ha-automation-condition <ha-automation-condition
.conditions=${action.if} .conditions=${action.if}
.hass=${this.hass} .reOrderMode=${this.reOrderMode}
@value-changed=${this._ifChanged} @value-changed=${this._ifChanged}
.hass=${this.hass}
></ha-automation-condition> ></ha-automation-condition>
<h3> <h3>
@ -46,6 +49,7 @@ export class HaIfAction extends LitElement implements ActionElement {
</h3> </h3>
<ha-automation-action <ha-automation-action
.actions=${action.then} .actions=${action.then}
.reOrderMode=${this.reOrderMode}
@value-changed=${this._thenChanged} @value-changed=${this._thenChanged}
.hass=${this.hass} .hass=${this.hass}
></ha-automation-action> ></ha-automation-action>
@ -58,6 +62,7 @@ export class HaIfAction extends LitElement implements ActionElement {
</h3> </h3>
<ha-automation-action <ha-automation-action
.actions=${action.else || []} .actions=${action.else || []}
.reOrderMode=${this.reOrderMode}
@value-changed=${this._elseChanged} @value-changed=${this._elseChanged}
.hass=${this.hass} .hass=${this.hass}
></ha-automation-action> ></ha-automation-action>

View File

@ -14,6 +14,8 @@ export class HaParallelAction extends LitElement implements ActionElement {
@property({ attribute: false }) public action!: ParallelAction; @property({ attribute: false }) public action!: ParallelAction;
@property({ type: Boolean }) public reOrderMode = false;
public static get defaultConfig() { public static get defaultConfig() {
return { return {
parallel: [], parallel: [],
@ -26,6 +28,7 @@ export class HaParallelAction extends LitElement implements ActionElement {
return html` return html`
<ha-automation-action <ha-automation-action
.actions=${action.parallel} .actions=${action.parallel}
.reOrderMode=${this.reOrderMode}
@value-changed=${this._actionsChanged} @value-changed=${this._actionsChanged}
.hass=${this.hass} .hass=${this.hass}
></ha-automation-action> ></ha-automation-action>

View File

@ -25,6 +25,8 @@ 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;
public static get defaultConfig() { public static get defaultConfig() {
return { repeat: { count: 2, sequence: [] } }; return { repeat: { count: 2, sequence: [] } };
} }
@ -95,6 +97,7 @@ export class HaRepeatAction extends LitElement implements ActionElement {
</h3> </h3>
<ha-automation-action <ha-automation-action
.actions=${action.sequence} .actions=${action.sequence}
.reOrderMode=${this.reOrderMode}
@value-changed=${this._actionChanged} @value-changed=${this._actionChanged}
.hass=${this.hass} .hass=${this.hass}
></ha-automation-action> ></ha-automation-action>

View File

@ -28,6 +28,8 @@ export default class HaAutomationConditionEditor extends LitElement {
@property({ type: Boolean }) public yamlMode = false; @property({ type: Boolean }) public yamlMode = false;
@property({ type: Boolean }) public reOrderMode = false;
private _processedCondition = memoizeOne((condition) => private _processedCondition = memoizeOne((condition) =>
expandConditionWithShorthand(condition) expandConditionWithShorthand(condition)
); );
@ -60,7 +62,11 @@ export default class HaAutomationConditionEditor extends LitElement {
<div> <div>
${dynamicElement( ${dynamicElement(
`ha-automation-condition-${condition.condition}`, `ha-automation-condition-${condition.condition}`,
{ hass: this.hass, condition: condition } {
hass: this.hass,
condition: condition,
reOrderMode: this.reOrderMode,
}
)} )}
</div> </div>
`} `}

View File

@ -70,6 +70,10 @@ export default class HaAutomationConditionRow extends LitElement {
@property() public condition!: Condition; @property() public condition!: Condition;
@property({ type: Boolean }) public hideMenu = false;
@property({ type: Boolean }) public reOrderMode = false;
@state() private _yamlMode = false; @state() private _yamlMode = false;
@state() private _warnings?: string[]; @state() private _warnings?: string[];
@ -103,6 +107,10 @@ export default class HaAutomationConditionRow extends LitElement {
)} )}
</h3> </h3>
<slot name="icons" slot="icons"></slot>
${this.hideMenu
? ""
: html`
<ha-button-menu <ha-button-menu
slot="icons" slot="icons"
fixed fixed
@ -127,7 +135,10 @@ export default class HaAutomationConditionRow extends LitElement {
${this.hass.localize( ${this.hass.localize(
"ui.panel.config.automation.editor.conditions.rename" "ui.panel.config.automation.editor.conditions.rename"
)} )}
<ha-svg-icon slot="graphic" .path=${mdiRenameBox}></ha-svg-icon> <ha-svg-icon
slot="graphic"
.path=${mdiRenameBox}
></ha-svg-icon>
</mwc-list-item> </mwc-list-item>
<mwc-list-item graphic="icon"> <mwc-list-item graphic="icon">
${this.hass.localize( ${this.hass.localize(
@ -142,7 +153,9 @@ export default class HaAutomationConditionRow extends LitElement {
<li divider role="separator"></li> <li divider role="separator"></li>
<mwc-list-item graphic="icon"> <mwc-list-item graphic="icon">
${this.hass.localize("ui.panel.config.automation.editor.edit_ui")} ${this.hass.localize(
"ui.panel.config.automation.editor.edit_ui"
)}
${!this._yamlMode ${!this._yamlMode
? html`<ha-svg-icon ? html`<ha-svg-icon
class="selected_menu_item" class="selected_menu_item"
@ -193,6 +206,7 @@ export default class HaAutomationConditionRow extends LitElement {
></ha-svg-icon> ></ha-svg-icon>
</mwc-list-item> </mwc-list-item>
</ha-button-menu> </ha-button-menu>
`}
<div <div
class=${classMap({ class=${classMap({
@ -226,6 +240,7 @@ export default class HaAutomationConditionRow extends LitElement {
.yamlMode=${this._yamlMode} .yamlMode=${this._yamlMode}
.hass=${this.hass} .hass=${this.hass}
.condition=${this.condition} .condition=${this.condition}
.reOrderMode=${this.reOrderMode}
></ha-automation-condition-editor> ></ha-automation-condition-editor>
</div> </div>
</ha-expansion-panel> </ha-expansion-panel>

View File

@ -1,14 +1,15 @@
import { mdiPlus } from "@mdi/js";
import { repeat } from "lit/directives/repeat";
import deepClone from "deep-clone-simple";
import "@material/mwc-button"; import "@material/mwc-button";
import type { ActionDetail } from "@material/mwc-list";
import { mdiArrowDown, mdiArrowUp, mdiDrag, mdiPlus } from "@mdi/js";
import deepClone from "deep-clone-simple";
import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit"; import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import type { ActionDetail } from "@material/mwc-list"; import type { SortableEvent } from "sortablejs";
import { fireEvent } from "../../../../common/dom/fire_event"; import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-svg-icon";
import "../../../../components/ha-button-menu"; import "../../../../components/ha-button-menu";
import "../../../../components/ha-svg-icon";
import type { Condition } from "../../../../data/automation"; import type { Condition } from "../../../../data/automation";
import type { HomeAssistant } from "../../../../types"; import type { HomeAssistant } from "../../../../types";
import "./ha-automation-condition-row"; import "./ha-automation-condition-row";
@ -16,6 +17,14 @@ import type HaAutomationConditionRow from "./ha-automation-condition-row";
// Uncommenting these and this element doesn't load // Uncommenting these and this element doesn't load
// import "./types/ha-automation-condition-not"; // import "./types/ha-automation-condition-not";
// import "./types/ha-automation-condition-or"; // import "./types/ha-automation-condition-or";
import { stringCompare } from "../../../../common/string/compare";
import type { LocalizeFunc } from "../../../../common/translations/localize";
import type { HaSelect } from "../../../../components/ha-select";
import { CONDITION_TYPES } from "../../../../data/condition";
import {
loadSortable,
SortableInstance,
} from "../../../../resources/sortable.ondemand";
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-numeric_state"; import "./types/ha-automation-condition-numeric_state";
@ -25,10 +34,7 @@ import "./types/ha-automation-condition-template";
import "./types/ha-automation-condition-time"; import "./types/ha-automation-condition-time";
import "./types/ha-automation-condition-trigger"; import "./types/ha-automation-condition-trigger";
import "./types/ha-automation-condition-zone"; import "./types/ha-automation-condition-zone";
import { CONDITION_TYPES } from "../../../../data/condition"; import { sortableStyles } from "../../../../resources/ha-sortable-style";
import { stringCompare } from "../../../../common/string/compare";
import type { LocalizeFunc } from "../../../../common/translations/localize";
import type { HaSelect } from "../../../../components/ha-select";
@customElement("ha-automation-condition") @customElement("ha-automation-condition")
export default class HaAutomationCondition extends LitElement { export default class HaAutomationCondition extends LitElement {
@ -36,11 +42,23 @@ export default class HaAutomationCondition extends LitElement {
@property() public conditions!: Condition[]; @property() public conditions!: Condition[];
@property({ type: Boolean }) public reOrderMode = false;
private _focusLastConditionOnChange = false; private _focusLastConditionOnChange = false;
private _conditionKeys = new WeakMap<Condition, string>(); private _conditionKeys = new WeakMap<Condition, string>();
private _sortable?: SortableInstance;
protected updated(changedProperties: PropertyValues) { protected updated(changedProperties: PropertyValues) {
if (changedProperties.has("reOrderMode")) {
if (this.reOrderMode) {
this._createSortable();
} else {
this._destroySortable();
}
}
if (!changedProperties.has("conditions")) { if (!changedProperties.has("conditions")) {
return; return;
} }
@ -82,19 +100,53 @@ export default class HaAutomationCondition extends LitElement {
return html``; return html``;
} }
return html` return html`
<div class="conditions">
${repeat( ${repeat(
this.conditions, this.conditions,
(condition) => this._getKey(condition), (condition) => this._getKey(condition),
(cond, idx) => html` (cond, idx) => html`
<ha-automation-condition-row <ha-automation-condition-row
.index=${idx} .index=${idx}
.totalConditions=${this.conditions.length}
.condition=${cond} .condition=${cond}
.hideMenu=${this.reOrderMode}
.reOrderMode=${this.reOrderMode}
@duplicate=${this._duplicateCondition} @duplicate=${this._duplicateCondition}
@move-condition=${this._move}
@value-changed=${this._conditionChanged} @value-changed=${this._conditionChanged}
.hass=${this.hass} .hass=${this.hass}
></ha-automation-condition-row> >
${this.reOrderMode
? html`
<ha-icon-button
.index=${idx}
slot="icons"
.label=${this.hass.localize(
"ui.panel.config.automation.editor.move_up"
)}
.path=${mdiArrowUp}
@click=${this._moveUp}
.disabled=${idx === 0}
></ha-icon-button>
<ha-icon-button
.index=${idx}
slot="icons"
.label=${this.hass.localize(
"ui.panel.config.automation.editor.move_down"
)}
.path=${mdiArrowDown}
@click=${this._moveDown}
.disabled=${idx === this.conditions.length - 1}
></ha-icon-button>
<div class="handle" slot="icons">
<ha-svg-icon .path=${mdiDrag}></ha-svg-icon>
</div>
`
: ""}
</ha-automation-condition-row>
` `
)} )}
</div>
<ha-button-menu fixed @action=${this._addCondition}> <ha-button-menu fixed @action=${this._addCondition}>
<mwc-button <mwc-button
slot="trigger" slot="trigger"
@ -116,6 +168,36 @@ export default class HaAutomationCondition extends LitElement {
`; `;
} }
private async _createSortable() {
const Sortable = await loadSortable();
this._sortable = new Sortable(
this.shadowRoot!.querySelector(".conditions")!,
{
animation: 150,
fallbackClass: "sortable-fallback",
handle: ".handle",
onChoose: (evt: SortableEvent) => {
(evt.item as any).placeholder =
document.createComment("sort-placeholder");
evt.item.after((evt.item as any).placeholder);
},
onEnd: (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;
}
this._dragged(evt);
},
}
);
}
private _destroySortable() {
this._sortable?.destroy();
this._sortable = undefined;
}
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());
@ -142,6 +224,30 @@ export default class HaAutomationCondition extends LitElement {
fireEvent(this, "value-changed", { value: conditions }); fireEvent(this, "value-changed", { value: conditions });
} }
private _moveUp(ev) {
const index = (ev.target as any).index;
const newIndex = index - 1;
this._move(index, newIndex);
}
private _moveDown(ev) {
const index = (ev.target as any).index;
const newIndex = index + 1;
this._move(index, newIndex);
}
private _dragged(ev: SortableEvent): void {
if (ev.oldIndex === ev.newIndex) return;
this._move(ev.oldIndex!, ev.newIndex!);
}
private _move(index: number, newIndex: number) {
const conditions = this.conditions.concat();
const condition = conditions.splice(index, 1)[0];
conditions.splice(newIndex, 0, condition);
fireEvent(this, "value-changed", { value: conditions });
}
private _conditionChanged(ev: CustomEvent) { private _conditionChanged(ev: CustomEvent) {
ev.stopPropagation(); ev.stopPropagation();
const conditions = [...this.conditions]; const conditions = [...this.conditions];
@ -186,7 +292,9 @@ export default class HaAutomationCondition extends LitElement {
); );
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return css` return [
sortableStyles,
css`
ha-automation-condition-row { ha-automation-condition-row {
display: block; display: block;
margin-bottom: 16px; margin-bottom: 16px;
@ -195,7 +303,16 @@ export default class HaAutomationCondition extends LitElement {
ha-svg-icon { ha-svg-icon {
height: 20px; height: 20px;
} }
`; .handle {
cursor: move;
padding: 12px;
}
.handle ha-svg-icon {
pointer-events: none;
height: 24px;
}
`,
];
} }
} }

View File

@ -12,6 +12,8 @@ export class HaLogicalCondition extends LitElement implements ConditionElement {
@property({ attribute: false }) public condition!: LogicalCondition; @property({ attribute: false }) public condition!: LogicalCondition;
@property({ type: Boolean }) public reOrderMode = false;
public static get defaultConfig() { public static get defaultConfig() {
return { return {
conditions: [], conditions: [],
@ -24,6 +26,7 @@ export class HaLogicalCondition extends LitElement implements ConditionElement {
.conditions=${this.condition.conditions || []} .conditions=${this.condition.conditions || []}
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
.hass=${this.hass} .hass=${this.hass}
.reOrderMode=${this.reOrderMode}
></ha-automation-condition> ></ha-automation-condition>
`; `;
} }

View File

@ -1,8 +1,8 @@
import "@material/mwc-button/mwc-button"; import "@material/mwc-button/mwc-button";
import { mdiHelpCircle, mdiRobot } from "@mdi/js"; import { mdiHelpCircle, mdiRobot, mdiSort, mdiTextBoxEdit } from "@mdi/js";
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 } from "lit";
import { customElement, property } 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 "../../../components/entity/ha-entity-toggle"; import "../../../components/entity/ha-entity-toggle";
import "../../../components/ha-card"; import "../../../components/ha-card";
@ -35,6 +35,12 @@ export class HaManualAutomationEditor extends LitElement {
@property({ attribute: false }) public stateObj?: HassEntity; @property({ attribute: false }) public stateObj?: HassEntity;
@state() private _reOrderMode = false;
private _toggleReOrderMode() {
this._reOrderMode = !this._reOrderMode;
}
protected render() { protected render() {
return html` return html`
<ha-card outlined> <ha-card outlined>
@ -108,6 +114,13 @@ export class HaManualAutomationEditor extends LitElement {
"ui.panel.config.automation.editor.triggers.header" "ui.panel.config.automation.editor.triggers.header"
)} )}
</h2> </h2>
<ha-icon-button
.path=${this._reOrderMode ? mdiTextBoxEdit : mdiSort}
.label=${this.hass.localize(
"ui.panel.config.automation.editor.actions.re_order"
)}
@click=${this._toggleReOrderMode}
></ha-icon-button>
<a <a
href=${documentationUrl(this.hass, "/docs/automation/trigger/")} href=${documentationUrl(this.hass, "/docs/automation/trigger/")}
target="_blank" target="_blank"
@ -128,6 +141,7 @@ export class HaManualAutomationEditor extends LitElement {
.triggers=${this.config.trigger} .triggers=${this.config.trigger}
@value-changed=${this._triggerChanged} @value-changed=${this._triggerChanged}
.hass=${this.hass} .hass=${this.hass}
.reOrderMode=${this._reOrderMode}
></ha-automation-trigger> ></ha-automation-trigger>
<div class="header"> <div class="header">
@ -136,6 +150,13 @@ export class HaManualAutomationEditor extends LitElement {
"ui.panel.config.automation.editor.conditions.header" "ui.panel.config.automation.editor.conditions.header"
)} )}
</h2> </h2>
<ha-icon-button
.path=${this._reOrderMode ? mdiTextBoxEdit : mdiSort}
.label=${this.hass.localize(
"ui.panel.config.automation.editor.actions.re_order"
)}
@click=${this._toggleReOrderMode}
></ha-icon-button>
<a <a
href=${documentationUrl(this.hass, "/docs/automation/condition/")} href=${documentationUrl(this.hass, "/docs/automation/condition/")}
target="_blank" target="_blank"
@ -156,6 +177,7 @@ export class HaManualAutomationEditor extends LitElement {
.conditions=${this.config.condition || []} .conditions=${this.config.condition || []}
@value-changed=${this._conditionChanged} @value-changed=${this._conditionChanged}
.hass=${this.hass} .hass=${this.hass}
.reOrderMode=${this._reOrderMode}
></ha-automation-condition> ></ha-automation-condition>
<div class="header"> <div class="header">
@ -164,6 +186,14 @@ export class HaManualAutomationEditor extends LitElement {
"ui.panel.config.automation.editor.actions.header" "ui.panel.config.automation.editor.actions.header"
)} )}
</h2> </h2>
<div>
<ha-icon-button
.path=${this._reOrderMode ? mdiTextBoxEdit : mdiSort}
.label=${this.hass.localize(
"ui.panel.config.automation.editor.actions.re_order"
)}
@click=${this._toggleReOrderMode}
></ha-icon-button>
<a <a
href=${documentationUrl(this.hass, "/docs/automation/action/")} href=${documentationUrl(this.hass, "/docs/automation/action/")}
target="_blank" target="_blank"
@ -177,6 +207,7 @@ export class HaManualAutomationEditor extends LitElement {
></ha-icon-button> ></ha-icon-button>
</a> </a>
</div> </div>
</div>
<ha-automation-action <ha-automation-action
role="region" role="region"
@ -185,6 +216,7 @@ export class HaManualAutomationEditor extends LitElement {
@value-changed=${this._actionChanged} @value-changed=${this._actionChanged}
.hass=${this.hass} .hass=${this.hass}
.narrow=${this.narrow} .narrow=${this.narrow}
.reOrderMode=${this._reOrderMode}
></ha-automation-action> ></ha-automation-action>
`; `;
} }

View File

@ -87,6 +87,8 @@ export default class HaAutomationTriggerRow extends LitElement {
@property({ attribute: false }) public trigger!: Trigger; @property({ attribute: false }) public trigger!: Trigger;
@property({ type: Boolean }) public hideMenu = false;
@state() private _warnings?: string[]; @state() private _warnings?: string[];
@state() private _yamlMode = false; @state() private _yamlMode = false;
@ -128,6 +130,11 @@ export default class HaAutomationTriggerRow extends LitElement {
></ha-svg-icon> ></ha-svg-icon>
${capitalizeFirstLetter(describeTrigger(this.trigger, this.hass))} ${capitalizeFirstLetter(describeTrigger(this.trigger, this.hass))}
</h3> </h3>
<slot name="icons" slot="icons"></slot>
${this.hideMenu
? ""
: html`
<ha-button-menu <ha-button-menu
slot="icons" slot="icons"
fixed fixed
@ -145,7 +152,10 @@ export default class HaAutomationTriggerRow extends LitElement {
${this.hass.localize( ${this.hass.localize(
"ui.panel.config.automation.editor.triggers.rename" "ui.panel.config.automation.editor.triggers.rename"
)} )}
<ha-svg-icon slot="graphic" .path=${mdiRenameBox}></ha-svg-icon> <ha-svg-icon
slot="graphic"
.path=${mdiRenameBox}
></ha-svg-icon>
</mwc-list-item> </mwc-list-item>
<mwc-list-item graphic="icon"> <mwc-list-item graphic="icon">
${this.hass.localize( ${this.hass.localize(
@ -161,13 +171,18 @@ export default class HaAutomationTriggerRow extends LitElement {
${this.hass.localize( ${this.hass.localize(
"ui.panel.config.automation.editor.triggers.edit_id" "ui.panel.config.automation.editor.triggers.edit_id"
)} )}
<ha-svg-icon slot="graphic" .path=${mdiIdentifier}></ha-svg-icon> <ha-svg-icon
slot="graphic"
.path=${mdiIdentifier}
></ha-svg-icon>
</mwc-list-item> </mwc-list-item>
<li divider role="separator"></li> <li divider role="separator"></li>
<mwc-list-item .disabled=${!supported} graphic="icon"> <mwc-list-item .disabled=${!supported} graphic="icon">
${this.hass.localize("ui.panel.config.automation.editor.edit_ui")} ${this.hass.localize(
"ui.panel.config.automation.editor.edit_ui"
)}
${!yamlMode ${!yamlMode
? html`<ha-svg-icon ? html`<ha-svg-icon
class="selected_menu_item" class="selected_menu_item"
@ -218,7 +233,7 @@ export default class HaAutomationTriggerRow extends LitElement {
></ha-svg-icon> ></ha-svg-icon>
</mwc-list-item> </mwc-list-item>
</ha-button-menu> </ha-button-menu>
`}
<div <div
class=${classMap({ class=${classMap({
"card-content": true, "card-content": true,

View File

@ -1,22 +1,26 @@
import { repeat } from "lit/directives/repeat";
import { mdiPlus } from "@mdi/js";
import deepClone from "deep-clone-simple";
import memoizeOne from "memoize-one";
import "@material/mwc-button"; import "@material/mwc-button";
import type { ActionDetail } from "@material/mwc-list";
import { mdiArrowDown, mdiArrowUp, mdiDrag, mdiPlus } from "@mdi/js";
import deepClone from "deep-clone-simple";
import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit"; import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import type { ActionDetail } from "@material/mwc-list"; import { repeat } from "lit/directives/repeat";
import memoizeOne from "memoize-one";
import type { SortableEvent } from "sortablejs";
import { fireEvent } from "../../../../common/dom/fire_event"; import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-svg-icon"; import { stringCompare } from "../../../../common/string/compare";
import type { LocalizeFunc } from "../../../../common/translations/localize";
import "../../../../components/ha-button-menu"; import "../../../../components/ha-button-menu";
import type { HaSelect } from "../../../../components/ha-select";
import "../../../../components/ha-svg-icon";
import { Trigger } from "../../../../data/automation"; import { Trigger } from "../../../../data/automation";
import { TRIGGER_TYPES } from "../../../../data/trigger"; import { TRIGGER_TYPES } from "../../../../data/trigger";
import { sortableStyles } from "../../../../resources/ha-sortable-style";
import { SortableInstance } from "../../../../resources/sortable";
import { loadSortable } from "../../../../resources/sortable.ondemand";
import { HomeAssistant } from "../../../../types"; import { HomeAssistant } from "../../../../types";
import "./ha-automation-trigger-row"; import "./ha-automation-trigger-row";
import type HaAutomationTriggerRow from "./ha-automation-trigger-row"; import type HaAutomationTriggerRow from "./ha-automation-trigger-row";
import type { LocalizeFunc } from "../../../../common/translations/localize";
import { stringCompare } from "../../../../common/string/compare";
import type { HaSelect } from "../../../../components/ha-select";
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";
@ -39,12 +43,17 @@ export default class HaAutomationTrigger extends LitElement {
@property() public triggers!: Trigger[]; @property() public triggers!: Trigger[];
@property({ type: Boolean }) public reOrderMode = false;
private _focusLastTriggerOnChange = false; private _focusLastTriggerOnChange = false;
private _triggerKeys = new WeakMap<Trigger, string>(); private _triggerKeys = new WeakMap<Trigger, string>();
private _sortable?: SortableInstance;
protected render() { protected render() {
return html` return html`
<div class="triggers">
${repeat( ${repeat(
this.triggers, this.triggers,
(trigger) => this._getKey(trigger), (trigger) => this._getKey(trigger),
@ -52,12 +61,42 @@ export default class HaAutomationTrigger extends LitElement {
<ha-automation-trigger-row <ha-automation-trigger-row
.index=${idx} .index=${idx}
.trigger=${trg} .trigger=${trg}
.hideMenu=${this.reOrderMode}
@duplicate=${this._duplicateTrigger} @duplicate=${this._duplicateTrigger}
@value-changed=${this._triggerChanged} @value-changed=${this._triggerChanged}
.hass=${this.hass} .hass=${this.hass}
></ha-automation-trigger-row> >
${this.reOrderMode
? html`
<ha-icon-button
.index=${idx}
slot="icons"
.label=${this.hass.localize(
"ui.panel.config.automation.editor.move_up"
)}
.path=${mdiArrowUp}
@click=${this._moveUp}
.disabled=${idx === 0}
></ha-icon-button>
<ha-icon-button
.index=${idx}
slot="icons"
.label=${this.hass.localize(
"ui.panel.config.automation.editor.move_down"
)}
.path=${mdiArrowDown}
@click=${this._moveDown}
.disabled=${idx === this.triggers.length - 1}
></ha-icon-button>
<div class="handle" slot="icons">
<ha-svg-icon .path=${mdiDrag}></ha-svg-icon>
</div>
`
: ""}
</ha-automation-trigger-row>
` `
)} )}
</div>
<ha-button-menu @action=${this._addTrigger}> <ha-button-menu @action=${this._addTrigger}>
<mwc-button <mwc-button
slot="trigger" slot="trigger"
@ -76,12 +115,21 @@ export default class HaAutomationTrigger extends LitElement {
` `
)} )}
</ha-button-menu> </ha-button-menu>
</div>
`; `;
} }
protected updated(changedProps: PropertyValues) { protected updated(changedProps: PropertyValues) {
super.updated(changedProps); super.updated(changedProps);
if (changedProps.has("reOrderMode")) {
if (this.reOrderMode) {
this._createSortable();
} else {
this._destroySortable();
}
}
if (changedProps.has("triggers") && this._focusLastTriggerOnChange) { if (changedProps.has("triggers") && this._focusLastTriggerOnChange) {
this._focusLastTriggerOnChange = false; this._focusLastTriggerOnChange = false;
@ -96,6 +144,36 @@ export default class HaAutomationTrigger extends LitElement {
} }
} }
private async _createSortable() {
const Sortable = await loadSortable();
this._sortable = new Sortable(
this.shadowRoot!.querySelector(".triggers")!,
{
animation: 150,
fallbackClass: "sortable-fallback",
handle: ".handle",
onChoose: (evt: SortableEvent) => {
(evt.item as any).placeholder =
document.createComment("sort-placeholder");
evt.item.after((evt.item as any).placeholder);
},
onEnd: (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;
}
this._dragged(evt);
},
}
);
}
private _destroySortable() {
this._sortable?.destroy();
this._sortable = undefined;
}
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());
@ -122,6 +200,30 @@ export default class HaAutomationTrigger extends LitElement {
fireEvent(this, "value-changed", { value: triggers }); fireEvent(this, "value-changed", { value: triggers });
} }
private _moveUp(ev) {
const index = (ev.target as any).index;
const newIndex = index - 1;
this._move(index, newIndex);
}
private _moveDown(ev) {
const index = (ev.target as any).index;
const newIndex = index + 1;
this._move(index, newIndex);
}
private _dragged(ev: SortableEvent): void {
if (ev.oldIndex === ev.newIndex) return;
this._move(ev.oldIndex!, ev.newIndex!);
}
private _move(index: number, newIndex: number) {
const triggers = this.triggers.concat();
const trigger = triggers.splice(index, 1)[0];
triggers.splice(newIndex, 0, trigger);
fireEvent(this, "value-changed", { value: triggers });
}
private _triggerChanged(ev: CustomEvent) { private _triggerChanged(ev: CustomEvent) {
ev.stopPropagation(); ev.stopPropagation();
const triggers = [...this.triggers]; const triggers = [...this.triggers];
@ -166,7 +268,9 @@ export default class HaAutomationTrigger extends LitElement {
); );
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return css` return [
sortableStyles,
css`
ha-automation-trigger-row { ha-automation-trigger-row {
display: block; display: block;
margin-bottom: 16px; margin-bottom: 16px;
@ -175,7 +279,16 @@ export default class HaAutomationTrigger extends LitElement {
ha-svg-icon { ha-svg-icon {
height: 20px; height: 20px;
} }
`; .handle {
cursor: move;
padding: 12px;
}
.handle ha-svg-icon {
pointer-events: none;
height: 24px;
}
`,
];
} }
} }