From 3b1f542dd9efe7afe4ace40269206e45bb87e2c0 Mon Sep 17 00:00:00 2001 From: surfingbytes Date: Mon, 30 Oct 2023 09:19:42 +0000 Subject: [PATCH 1/9] Handle sequences under parallel actions --- src/data/script.ts | 6 +- .../types/ha-automation-action-parallel.ts | 115 +++++++++++++++--- src/translations/en.json | 3 + 3 files changed, 109 insertions(+), 15 deletions(-) diff --git a/src/data/script.ts b/src/data/script.ts index 0f585cf1e003..9d8e8c4a2897 100644 --- a/src/data/script.ts +++ b/src/data/script.ts @@ -244,8 +244,12 @@ export interface StopAction extends BaseAction { error?: boolean; } +export interface SequenceAction extends BaseAction { + sequence: ManualScriptConfig | Action | (ManualScriptConfig | Action)[]; +} + export interface ParallelAction extends BaseAction { - parallel: ManualScriptConfig | Action | (ManualScriptConfig | Action)[]; + parallel: SequenceAction | SequenceAction[]; } export interface SetConversationResponseAction extends BaseAction { diff --git a/src/panels/config/automation/action/types/ha-automation-action-parallel.ts b/src/panels/config/automation/action/types/ha-automation-action-parallel.ts index 6ccec60232b0..6c1a8bc6c838 100644 --- a/src/panels/config/automation/action/types/ha-automation-action-parallel.ts +++ b/src/panels/config/automation/action/types/ha-automation-action-parallel.ts @@ -1,5 +1,6 @@ -import { CSSResultGroup, html, LitElement } from "lit"; +import { css, CSSResultGroup, html, LitElement } from "lit"; import { customElement, property } from "lit/decorators"; +import { mdiDelete, mdiPlus } from "@mdi/js"; import { fireEvent } from "../../../../../common/dom/fire_event"; import "../../../../../components/ha-textfield"; import { Action, ParallelAction } from "../../../../../data/script"; @@ -7,6 +8,7 @@ import { haStyle } from "../../../../../resources/styles"; import type { HomeAssistant, ItemPath } from "../../../../../types"; import "../ha-automation-action"; import type { ActionElement } from "../ha-automation-action-row"; +import { ensureArray } from "../../../../../common/array/ensure-array"; @customElement("ha-automation-action-parallel") export class HaParallelAction extends LitElement implements ActionElement { @@ -20,37 +22,122 @@ export class HaParallelAction extends LitElement implements ActionElement { public static get defaultConfig() { return { - parallel: [], + parallel: [{ sequence: [] }], }; } protected render() { const action = this.action; + action.parallel = (action.parallel ? ensureArray(action.parallel) : []).map( + (sequenceAction) => + sequenceAction.sequence + ? sequenceAction + : { sequence: [sequenceAction] } + ); + return html` - + html` + +
+

+ ${this.hass.localize( + "ui.panel.config.automation.editor.actions.type.parallel.sequence" + )}: +

+ +
+
` + )} +
+ @click=${this._addSequence} + > + + `; } - private _actionsChanged(ev: CustomEvent) { + private _actionChanged(ev: CustomEvent) { ev.stopPropagation(); const value = ev.detail.value as Action[]; + const index = (ev.target as any).idx; + const parallel = this.action.parallel + ? [...ensureArray(this.action.parallel)] + : []; + parallel[index].sequence = value; + fireEvent(this, "value-changed", { + value: { ...this.action, parallel }, + }); + } + + private _addSequence() { + const parallel = this.action.parallel + ? [...ensureArray(this.action.parallel)] + : []; + parallel.push({ sequence: [] }); + fireEvent(this, "value-changed", { + value: { ...this.action, parallel }, + }); + } + + private _removeSequence(ev: CustomEvent) { + const index = (ev.currentTarget as any).idx; + const parallel = this.action.parallel + ? [...ensureArray(this.action.parallel)] + : []; + parallel.splice(index, 1); fireEvent(this, "value-changed", { - value: { - ...this.action, - parallel: value, - }, + value: { ...this.action, parallel }, }); } static get styles(): CSSResultGroup { - return haStyle; + return [ + haStyle, + css` + ha-card { + margin: 16px 0; + } + .add-card mwc-button { + display: block; + text-align: center; + } + ha-icon-button { + position: absolute; + right: 0; + padding: 4px; + } + ha-svg-icon { + height: 20px; + } + .link-button-row { + padding: 14px; + } + `, + ]; } } diff --git a/src/translations/en.json b/src/translations/en.json index d7eddeb14004..54682c8d021b 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -3099,6 +3099,9 @@ }, "parallel": { "label": "Run in parallel", + "add_sequence": "Add parallel actions", + "remove_sequence": "Remove actions", + "sequence": "Actions", "description": { "picker": "Perform a sequence of actions in parallel.", "full": "Run {number} {number, plural,\n one {action}\n other {actions}\n} in parallel" From 0bd4adc07d281783635da435142511e9f2fed2c0 Mon Sep 17 00:00:00 2001 From: surfingbytes Date: Wed, 17 Jan 2024 09:21:31 +0000 Subject: [PATCH 2/9] Better descriptions, sequences are collapsible --- .../types/ha-automation-action-parallel.ts | 397 ++++++++++++++++-- src/translations/en.json | 15 +- 2 files changed, 367 insertions(+), 45 deletions(-) diff --git a/src/panels/config/automation/action/types/ha-automation-action-parallel.ts b/src/panels/config/automation/action/types/ha-automation-action-parallel.ts index 6c1a8bc6c838..cf2c17e5caf5 100644 --- a/src/panels/config/automation/action/types/ha-automation-action-parallel.ts +++ b/src/panels/config/automation/action/types/ha-automation-action-parallel.ts @@ -1,14 +1,44 @@ -import { css, CSSResultGroup, html, LitElement } from "lit"; -import { customElement, property } from "lit/decorators"; -import { mdiDelete, mdiPlus } from "@mdi/js"; +import { consume } from "@lit-labs/context"; +import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import { + mdiDotsVertical, + mdiRenameBox, + mdiSort, + mdiContentDuplicate, + mdiDelete, + mdiPlus, + mdiArrowUp, + mdiArrowDown, + mdiDrag, +} from "@mdi/js"; +import deepClone from "deep-clone-simple"; +import type { ActionDetail } from "@material/mwc-list"; +import type { SortableEvent } from "sortablejs"; +import type { SortableInstance } from "../../../../../resources/sortable"; +import { describeAction } from "../../../../../data/script_i18n"; import { fireEvent } from "../../../../../common/dom/fire_event"; +import { capitalizeFirstLetter } from "../../../../../common/string/capitalize-first-letter"; import "../../../../../components/ha-textfield"; -import { Action, ParallelAction } from "../../../../../data/script"; +import { + Action, + ParallelAction, + SequenceAction, +} from "../../../../../data/script"; +import { + showConfirmationDialog, + showPromptDialog, +} from "../../../../../dialogs/generic/show-dialog-box"; import { haStyle } from "../../../../../resources/styles"; import type { HomeAssistant, ItemPath } from "../../../../../types"; import "../ha-automation-action"; import type { ActionElement } from "../ha-automation-action-row"; import { ensureArray } from "../../../../../common/array/ensure-array"; +import { fullEntitiesContext } from "../../../../../data/context"; +import { EntityRegistryEntry } from "../../../../../data/entity_registry"; +import { sortableStyles } from "../../../../../resources/ha-sortable-style"; + +const preventDefault = (ev) => ev.preventDefault(); @customElement("ha-automation-action-parallel") export class HaParallelAction extends LitElement implements ActionElement { @@ -20,12 +50,44 @@ export class HaParallelAction extends LitElement implements ActionElement { @property({ attribute: false }) public action!: ParallelAction; + @state() private _expandedStates: boolean[] = []; + + @state() + @consume({ context: fullEntitiesContext, subscribe: true }) + _entityReg!: EntityRegistryEntry[]; + + private _expandLast = false; + + private _sortable?: SortableInstance; + public static get defaultConfig() { return { parallel: [{ sequence: [] }], }; } + private _expandedChanged(ev) { + this._expandedStates = this._expandedStates.concat(); + this._expandedStates[ev.target!.index] = ev.detail.expanded; + } + + private _getDescription(sequence) { + const actions = ensureArray(sequence.sequence); + if (!actions || actions.length === 0) { + return this.hass.localize( + "ui.panel.config.automation.editor.actions.type.parallel.no_actions" + ); + } + if (actions.length === 1) { + return describeAction(this.hass, this._entityReg, actions[0]); + } + + return this.hass.localize( + "ui.panel.config.automation.editor.actions.type.parallel.actions", + { number: actions.length } + ); + } + protected render() { const action = this.action; @@ -39,32 +101,117 @@ export class HaParallelAction extends LitElement implements ActionElement { return html` ${action.parallel.map( (sequence, idx) => - html` - -
-

+ html` + +

${this.hass.localize( - "ui.panel.config.automation.editor.actions.type.parallel.sequence" + "ui.panel.config.automation.editor.actions.type.parallel.sequence", + { number: idx + 1 } )}: -

- -

+ ${sequence.alias || this._getDescription(sequence)} + + ${this.reOrderMode + ? html` + + +
+ +
+ ` + : html` + + + + ${this.hass.localize( + "ui.panel.config.automation.editor.actions.rename" + )} + + + + ${this.hass.localize( + "ui.panel.config.automation.editor.actions.re_order" + )} + + + + + ${this.hass.localize( + "ui.panel.config.automation.editor.actions.duplicate" + )} + + + + + ${this.hass.localize( + "ui.panel.config.automation.editor.actions.type.parallel.remove_sequence" + )} + + + + `} +
+

 

+ +
+
` )} ) { + switch (ev.detail.index) { + case 0: + await this._renameAction(ev); + break; + case 1: + fireEvent(this, "re-order"); + break; + case 2: + this._duplicateParallel(ev); + break; + case 3: + this._removeSequence(ev); + break; + } + } + + private async _renameAction(ev: CustomEvent): Promise { + const index = (ev.target as any).idx; + const parallel = this.action.parallel + ? [...ensureArray(this.action.parallel)] + : []; + const current = parallel[index]; + const alias = await showPromptDialog(this, { + title: this.hass.localize( + "ui.panel.config.automation.editor.actions.type.parallel.change_alias" + ), + inputLabel: this.hass.localize( + "ui.panel.config.automation.editor.actions.type.parallel.alias" + ), + inputType: "string", + placeholder: capitalizeFirstLetter(this._getDescription(current)), + defaultValue: current.alias, + confirmText: this.hass.localize("ui.common.submit"), + }); + if (alias !== null) { + if (alias === "") { + delete parallel[index].alias; + } else { + parallel[index].alias = alias; + } + fireEvent(this, "value-changed", { + value: { ...this.action, parallel }, + }); + } + } + + private _duplicateParallel(ev) { + const index = (ev.target as any).idx; + this._createSequence(deepClone(ensureArray(this.action.parallel)[index])); + } + private _actionChanged(ev: CustomEvent) { ev.stopPropagation(); const value = ev.detail.value as Action[]; @@ -93,30 +292,136 @@ export class HaParallelAction extends LitElement implements ActionElement { }); } - private _addSequence() { - const parallel = this.action.parallel - ? [...ensureArray(this.action.parallel)] - : []; - parallel.push({ sequence: [] }); + protected firstUpdated() { + ensureArray(this.action.parallel).forEach(() => + this._expandedStates.push(false) + ); + } + + protected updated(changedProps: PropertyValues) { + super.updated(changedProps); + + if (changedProps.has("reOrderMode")) { + if (this.reOrderMode) { + this._createSortable(); + } else { + this._destroySortable(); + } + } + + if (this._expandLast) { + const nodes = this.shadowRoot!.querySelectorAll("ha-expansion-panel"); + nodes[nodes.length - 1].expanded = true; + this._expandLast = false; + } + } + + 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 parallel = ensureArray(this.action.parallel)!.concat(); + const item = parallel.splice(index, 1)[0]; + parallel.splice(newIndex, 0, item); + + const expanded = this._expandedStates.splice(index, 1)[0]; + this._expandedStates.splice(newIndex, 0, expanded); + fireEvent(this, "value-changed", { value: { ...this.action, parallel }, }); } - private _removeSequence(ev: CustomEvent) { - const index = (ev.currentTarget as any).idx; + private _addSequence() { + this._createSequence({ sequence: [] }); + } + + private _createSequence(sequence: SequenceAction) { const parallel = this.action.parallel ? [...ensureArray(this.action.parallel)] : []; - parallel.splice(index, 1); + parallel.push(sequence); fireEvent(this, "value-changed", { value: { ...this.action, parallel }, }); + this._expandLast = true; + this._expandedStates[parallel.length - 1] = true; + } + + private _removeSequence(ev: CustomEvent) { + const index = (ev.target as any).idx; + showConfirmationDialog(this, { + title: this.hass.localize( + "ui.panel.config.automation.editor.actions.type.parallel.delete_confirm_title" + ), + text: this.hass.localize( + "ui.panel.config.automation.editor.actions.delete_confirm_text" + ), + dismissText: this.hass.localize("ui.common.cancel"), + confirmText: this.hass.localize("ui.common.delete"), + destructive: true, + confirm: () => { + const parallel = this.action.parallel + ? [...ensureArray(this.action.parallel)] + : []; + parallel.splice(index, 1); + this._expandedStates.splice(index, 1); + fireEvent(this, "value-changed", { + value: { ...this.action, parallel }, + }); + }, + }); + } + + private async _createSortable() { + const Sortable = (await import("../../../../../resources/sortable")) + .default; + this._sortable = new Sortable( + this.shadowRoot!.querySelector(".parallel")!, + { + 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; } static get styles(): CSSResultGroup { return [ haStyle, + sortableStyles, css` ha-card { margin: 16px 0; @@ -125,16 +430,28 @@ export class HaParallelAction extends LitElement implements ActionElement { display: block; text-align: center; } + ha-expansion-panel { + --expansion-panel-summary-padding: 0 0 0 8px; + --expansion-panel-content-padding: 0; + } + h3 { + margin: 0; + font-size: inherit; + font-weight: inherit; + } ha-icon-button { - position: absolute; - right: 0; - padding: 4px; + inset-inline-start: initial; + inset-inline-end: 0; + direction: var(--direction); } ha-svg-icon { height: 20px; } .link-button-row { - padding: 14px; + padding: 14px 14px 0 14px; + } + .card-content { + padding: 0 16px 16px 16px; } `, ]; diff --git a/src/translations/en.json b/src/translations/en.json index 54682c8d021b..f18e2a88aab2 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -3099,12 +3099,17 @@ }, "parallel": { "label": "Run in parallel", - "add_sequence": "Add parallel actions", - "remove_sequence": "Remove actions", - "sequence": "Actions", + "add_sequence": "Add parallel sequence", + "remove_sequence": "Remove sequence", + "sequence": "Sequence {number}", + "change_alias": "Rename sequence", + "alias": "Sequence name", + "delete_confirm_title": "Remove sequence?", + "actions": "Run {number} {number, plural,\n one {action}\n other {actions}\n}", + "no_actions": "No actions", "description": { - "picker": "Perform a sequence of actions in parallel.", - "full": "Run {number} {number, plural,\n one {action}\n other {actions}\n} in parallel" + "picker": "Perform sequences of actions in parallel.", + "full": "Run {number} {number, plural,\n one {sequence}\n other {sequences}\n} in parallel" } }, "variables": { From 0f30f56df6d87a4129d583a14e876086694e548b Mon Sep 17 00:00:00 2001 From: surfingbytes Date: Fri, 19 Jan 2024 12:53:39 +0000 Subject: [PATCH 3/9] Use of ha-sortable component --- .../types/ha-automation-action-parallel.ts | 305 ++++++++---------- 1 file changed, 140 insertions(+), 165 deletions(-) diff --git a/src/panels/config/automation/action/types/ha-automation-action-parallel.ts b/src/panels/config/automation/action/types/ha-automation-action-parallel.ts index cf2c17e5caf5..9f2798b6408b 100644 --- a/src/panels/config/automation/action/types/ha-automation-action-parallel.ts +++ b/src/panels/config/automation/action/types/ha-automation-action-parallel.ts @@ -14,11 +14,10 @@ import { } from "@mdi/js"; import deepClone from "deep-clone-simple"; import type { ActionDetail } from "@material/mwc-list"; -import type { SortableEvent } from "sortablejs"; -import type { SortableInstance } from "../../../../../resources/sortable"; import { describeAction } from "../../../../../data/script_i18n"; import { fireEvent } from "../../../../../common/dom/fire_event"; import { capitalizeFirstLetter } from "../../../../../common/string/capitalize-first-letter"; +import "../../../../../components/ha-sortable"; import "../../../../../components/ha-textfield"; import { Action, @@ -36,7 +35,6 @@ import type { ActionElement } from "../ha-automation-action-row"; import { ensureArray } from "../../../../../common/array/ensure-array"; import { fullEntitiesContext } from "../../../../../data/context"; import { EntityRegistryEntry } from "../../../../../data/entity_registry"; -import { sortableStyles } from "../../../../../resources/ha-sortable-style"; const preventDefault = (ev) => ev.preventDefault(); @@ -58,8 +56,6 @@ export class HaParallelAction extends LitElement implements ActionElement { private _expandLast = false; - private _sortable?: SortableInstance; - public static get defaultConfig() { return { parallel: [{ sequence: [] }], @@ -99,121 +95,139 @@ export class HaParallelAction extends LitElement implements ActionElement { ); return html` - ${action.parallel.map( - (sequence, idx) => - html` - -

- ${this.hass.localize( - "ui.panel.config.automation.editor.actions.type.parallel.sequence", - { number: idx + 1 } - )}: - ${sequence.alias || this._getDescription(sequence)} -

- ${this.reOrderMode - ? html` - - -
- -
- ` - : html` - - - - ${this.hass.localize( - "ui.panel.config.automation.editor.actions.rename" - )} - - - - ${this.hass.localize( - "ui.panel.config.automation.editor.actions.re_order" - )} - - - - - ${this.hass.localize( - "ui.panel.config.automation.editor.actions.duplicate" - )} - - - - +
+ ${action.parallel.map( + (sequence, idx) => + html`
+ + +

+ ${this.hass.localize( + "ui.panel.config.automation.editor.actions.type.parallel.sequence", + { number: idx + 1 } + )}: + ${sequence.alias || this._getDescription(sequence)} +

+ ${this.reOrderMode + ? html` + + +
+ +
+ ` + : html` + + + + ${this.hass.localize( + "ui.panel.config.automation.editor.actions.rename" + )} + + + + ${this.hass.localize( + "ui.panel.config.automation.editor.actions.re_order" + )} + + + + + ${this.hass.localize( + "ui.panel.config.automation.editor.actions.duplicate" + )} + + + + + ${this.hass.localize( + "ui.panel.config.automation.editor.actions.type.parallel.remove_sequence" + )} + + + + `} +
+ - ${this.hass.localize( - "ui.panel.config.automation.editor.actions.type.parallel.remove_sequence" - )} - - - - `} -
-

 

- -
- - ` - )} + @value-changed=${this._actionChanged} + .path=${[...(this.path ?? []), "parallel"]} + .hass=${this.hass} + .idx=${idx} + >
+
+
+
+
` + )} +
+ { - (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; - } - static get styles(): CSSResultGroup { return [ haStyle, - sortableStyles, css` ha-card { margin: 16px 0; @@ -451,7 +426,7 @@ export class HaParallelAction extends LitElement implements ActionElement { padding: 14px 14px 0 14px; } .card-content { - padding: 0 16px 16px 16px; + padding: 16px; } `, ]; From 8c0f67e53785cc46384b0687cd3c381fc1bd2a88 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 30 Jan 2024 15:25:33 +0100 Subject: [PATCH 4/9] rebase and fix --- .../types/ha-automation-action-parallel.ts | 222 ++++++++++-------- 1 file changed, 118 insertions(+), 104 deletions(-) diff --git a/src/panels/config/automation/action/types/ha-automation-action-parallel.ts b/src/panels/config/automation/action/types/ha-automation-action-parallel.ts index 9f2798b6408b..6cdef9962200 100644 --- a/src/panels/config/automation/action/types/ha-automation-action-parallel.ts +++ b/src/panels/config/automation/action/types/ha-automation-action-parallel.ts @@ -1,10 +1,16 @@ import { consume } from "@lit-labs/context"; -import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit"; +import { + css, + CSSResultGroup, + html, + LitElement, + nothing, + PropertyValues, +} from "lit"; import { customElement, property, state } from "lit/decorators"; import { mdiDotsVertical, mdiRenameBox, - mdiSort, mdiContentDuplicate, mdiDelete, mdiPlus, @@ -35,6 +41,7 @@ import type { ActionElement } from "../ha-automation-action-row"; import { ensureArray } from "../../../../../common/array/ensure-array"; import { fullEntitiesContext } from "../../../../../data/context"; import { EntityRegistryEntry } from "../../../../../data/entity_registry"; +import { listenMediaQuery } from "../../../../../common/dom/media_query"; const preventDefault = (ev) => ev.preventDefault(); @@ -50,18 +57,35 @@ export class HaParallelAction extends LitElement implements ActionElement { @state() private _expandedStates: boolean[] = []; + @state() private _showReorder: boolean = false; + @state() @consume({ context: fullEntitiesContext, subscribe: true }) _entityReg!: EntityRegistryEntry[]; private _expandLast = false; + private _unsubMql?: () => void; + public static get defaultConfig() { return { parallel: [{ sequence: [] }], }; } + public connectedCallback() { + super.connectedCallback(); + this._unsubMql = listenMediaQuery("(min-width: 600px)", (matches) => { + this._showReorder = matches; + }); + } + + public disconnectedCallback() { + super.disconnectedCallback(); + this._unsubMql?.(); + this._unsubMql = undefined; + } + private _expandedChanged(ev) { this._expandedStates = this._expandedStates.concat(); this._expandedStates[ev.target!.index] = ev.detail.expanded; @@ -97,8 +121,9 @@ export class HaParallelAction extends LitElement implements ActionElement { return html`
${action.parallel.map( @@ -117,107 +142,99 @@ export class HaParallelAction extends LitElement implements ActionElement { )}: ${sequence.alias || this._getDescription(sequence)} - ${this.reOrderMode + ${this._showReorder ? html` - -
` - : html` - - - - ${this.hass.localize( - "ui.panel.config.automation.editor.actions.rename" - )} - - - - ${this.hass.localize( - "ui.panel.config.automation.editor.actions.re_order" - )} - - - - - ${this.hass.localize( - "ui.panel.config.automation.editor.actions.duplicate" - )} - - - - - ${this.hass.localize( - "ui.panel.config.automation.editor.actions.type.parallel.remove_sequence" - )} - - - - `} + : nothing} + + + + ${this.hass.localize( + "ui.panel.config.automation.editor.actions.rename" + )} + + + + + ${this.hass.localize( + "ui.panel.config.automation.editor.actions.duplicate" + )} + + + + + ${this.hass.localize( + "ui.panel.config.automation.editor.move_up" + )} + + + + + ${this.hass.localize( + "ui.panel.config.automation.editor.move_down" + )} + + + + + ${this.hass.localize( + "ui.panel.config.automation.editor.actions.type.parallel.remove_sequence" + )} + + +
@@ -247,12 +264,15 @@ export class HaParallelAction extends LitElement implements ActionElement { await this._renameAction(ev); break; case 1: - fireEvent(this, "re-order"); + this._duplicateParallel(ev); break; case 2: - this._duplicateParallel(ev); + this._moveUp(ev); break; case 3: + this._moveDown(ev); + break; + case 4: this._removeSequence(ev); break; } @@ -322,20 +342,14 @@ export class HaParallelAction extends LitElement implements ActionElement { } } - private _sequenceMoved(ev: CustomEvent): void { - ev.stopPropagation(); - const { oldIndex, newIndex } = ev.detail; - this._move(oldIndex, newIndex); - } - private _moveUp(ev) { - const index = (ev.target as any).index; + const index = (ev.target as any).idx; const newIndex = index - 1; this._move(index, newIndex); } private _moveDown(ev) { - const index = (ev.target as any).index; + const index = (ev.target as any).idx; const newIndex = index + 1; this._move(index, newIndex); } From 9f3eafe6a71ee7fd52998d185c543dc188b1294e Mon Sep 17 00:00:00 2001 From: surfingbytes Date: Tue, 2 Apr 2024 12:04:00 +0000 Subject: [PATCH 5/9] New "Sequence" action type. Parallel almost reverted. --- src/data/action.ts | 2 + src/data/script.ts | 8 +- src/data/script_i18n.ts | 10 + .../action/ha-automation-action-row.ts | 1 + .../types/ha-automation-action-parallel.ts | 435 ++---------------- .../types/ha-automation-action-sequence.ts | 61 +++ src/translations/en.json | 20 +- 7 files changed, 128 insertions(+), 409 deletions(-) create mode 100644 src/panels/config/automation/action/types/ha-automation-action-sequence.ts diff --git a/src/data/action.ts b/src/data/action.ts index c85d5226c889..114a2b27d4a1 100644 --- a/src/data/action.ts +++ b/src/data/action.ts @@ -8,6 +8,7 @@ import { mdiDevices, mdiDotsHorizontal, mdiExcavator, + mdiFormatListNumbered, mdiGestureDoubleTap, mdiHandBackRight, mdiPalette, @@ -36,6 +37,7 @@ export const ACTION_ICONS = { device_id: mdiDevices, stop: mdiHandBackRight, parallel: mdiShuffleDisabled, + sequence: mdiFormatListNumbered, variables: mdiApplicationVariableOutline, set_conversation_response: mdiBullhorn, } as const; diff --git a/src/data/script.ts b/src/data/script.ts index 401e5c24b96b..90009644a9ee 100644 --- a/src/data/script.ts +++ b/src/data/script.ts @@ -248,11 +248,11 @@ export interface StopAction extends BaseAction { } export interface SequenceAction extends BaseAction { - sequence: ManualScriptConfig | Action | (ManualScriptConfig | Action)[]; + sequence: (ManualScriptConfig | Action)[]; } export interface ParallelAction extends BaseAction { - parallel: SequenceAction | SequenceAction[]; + parallel: SequenceAction[]; } export interface SetConversationResponseAction extends BaseAction { @@ -303,6 +303,7 @@ export interface ActionTypes { play_media: PlayMediaAction; stop: StopAction; parallel: ParallelAction; + sequence: SequenceAction; set_conversation_response: SetConversationResponseAction; unknown: UnknownAction; } @@ -395,6 +396,9 @@ export const getActionType = (action: Action): ActionType => { if ("parallel" in action) { return "parallel"; } + if ("sequence" in action) { + return "sequence"; + } if ("set_conversation_response" in action) { return "set_conversation_response"; } diff --git a/src/data/script_i18n.ts b/src/data/script_i18n.ts index 22d5e3c0a7bf..2c283a4220b2 100644 --- a/src/data/script_i18n.ts +++ b/src/data/script_i18n.ts @@ -27,6 +27,7 @@ import { PlayMediaAction, RepeatAction, SceneAction, + SequenceAction, SetConversationResponseAction, StopAction, VariablesAction, @@ -444,6 +445,15 @@ const tryDescribeAction = ( ); } + if (actionType === "sequence") { + const config = action as SequenceAction; + const numActions = ensureArray(config.sequence).length; + return hass.localize( + `${actionTranslationBaseKey}.sequence.description.full`, + { number: numActions } + ); + } + if (actionType === "set_conversation_response") { const config = action as SetConversationResponseAction; return hass.localize( diff --git a/src/panels/config/automation/action/ha-automation-action-row.ts b/src/panels/config/automation/action/ha-automation-action-row.ts index 6986aba97464..d20e67e3f05b 100644 --- a/src/panels/config/automation/action/ha-automation-action-row.ts +++ b/src/panels/config/automation/action/ha-automation-action-row.ts @@ -69,6 +69,7 @@ import "./types/ha-automation-action-if"; import "./types/ha-automation-action-parallel"; import "./types/ha-automation-action-play_media"; import "./types/ha-automation-action-repeat"; +import "./types/ha-automation-action-sequence"; import "./types/ha-automation-action-service"; import "./types/ha-automation-action-set_conversation_response"; import "./types/ha-automation-action-stop"; diff --git a/src/panels/config/automation/action/types/ha-automation-action-parallel.ts b/src/panels/config/automation/action/types/ha-automation-action-parallel.ts index 6cdef9962200..c4fbcdb9e54b 100644 --- a/src/panels/config/automation/action/types/ha-automation-action-parallel.ts +++ b/src/panels/config/automation/action/types/ha-automation-action-parallel.ts @@ -1,49 +1,14 @@ -import { consume } from "@lit-labs/context"; -import { - css, - CSSResultGroup, - html, - LitElement, - nothing, - PropertyValues, -} from "lit"; -import { customElement, property, state } from "lit/decorators"; -import { - mdiDotsVertical, - mdiRenameBox, - mdiContentDuplicate, - mdiDelete, - mdiPlus, - mdiArrowUp, - mdiArrowDown, - mdiDrag, -} from "@mdi/js"; -import deepClone from "deep-clone-simple"; -import type { ActionDetail } from "@material/mwc-list"; -import { describeAction } from "../../../../../data/script_i18n"; +import { mdiPlus } from "@mdi/js"; +import { CSSResultGroup, css, html, LitElement } from "lit"; +import { customElement, property } from "lit/decorators"; import { fireEvent } from "../../../../../common/dom/fire_event"; -import { capitalizeFirstLetter } from "../../../../../common/string/capitalize-first-letter"; -import "../../../../../components/ha-sortable"; import "../../../../../components/ha-textfield"; -import { - Action, - ParallelAction, - SequenceAction, -} from "../../../../../data/script"; -import { - showConfirmationDialog, - showPromptDialog, -} from "../../../../../dialogs/generic/show-dialog-box"; +import { Action, ParallelAction } from "../../../../../data/script"; import { haStyle } from "../../../../../resources/styles"; import type { HomeAssistant, ItemPath } from "../../../../../types"; import "../ha-automation-action"; import type { ActionElement } from "../ha-automation-action-row"; -import { ensureArray } from "../../../../../common/array/ensure-array"; -import { fullEntitiesContext } from "../../../../../data/context"; -import { EntityRegistryEntry } from "../../../../../data/entity_registry"; -import { listenMediaQuery } from "../../../../../common/dom/media_query"; - -const preventDefault = (ev) => ev.preventDefault(); +import { HaSequenceAction } from "./ha-automation-action-sequence"; @customElement("ha-automation-action-parallel") export class HaParallelAction extends LitElement implements ActionElement { @@ -55,355 +20,58 @@ export class HaParallelAction extends LitElement implements ActionElement { @property({ attribute: false }) public action!: ParallelAction; - @state() private _expandedStates: boolean[] = []; - - @state() private _showReorder: boolean = false; - - @state() - @consume({ context: fullEntitiesContext, subscribe: true }) - _entityReg!: EntityRegistryEntry[]; - - private _expandLast = false; - - private _unsubMql?: () => void; - public static get defaultConfig() { return { - parallel: [{ sequence: [] }], + parallel: [], }; } - public connectedCallback() { - super.connectedCallback(); - this._unsubMql = listenMediaQuery("(min-width: 600px)", (matches) => { - this._showReorder = matches; - }); - } - - public disconnectedCallback() { - super.disconnectedCallback(); - this._unsubMql?.(); - this._unsubMql = undefined; - } - - private _expandedChanged(ev) { - this._expandedStates = this._expandedStates.concat(); - this._expandedStates[ev.target!.index] = ev.detail.expanded; - } - - private _getDescription(sequence) { - const actions = ensureArray(sequence.sequence); - if (!actions || actions.length === 0) { - return this.hass.localize( - "ui.panel.config.automation.editor.actions.type.parallel.no_actions" - ); - } - if (actions.length === 1) { - return describeAction(this.hass, this._entityReg, actions[0]); - } - - return this.hass.localize( - "ui.panel.config.automation.editor.actions.type.parallel.actions", - { number: actions.length } - ); - } - protected render() { const action = this.action; - action.parallel = (action.parallel ? ensureArray(action.parallel) : []).map( - (sequenceAction) => - sequenceAction.sequence - ? sequenceAction - : { sequence: [sequenceAction] } - ); - return html` - -
- ${action.parallel.map( - (sequence, idx) => - html`
- - -

- ${this.hass.localize( - "ui.panel.config.automation.editor.actions.type.parallel.sequence", - { number: idx + 1 } - )}: - ${sequence.alias || this._getDescription(sequence)} -

- ${this._showReorder - ? html` -
- -
- ` - : nothing} - - - - ${this.hass.localize( - "ui.panel.config.automation.editor.actions.rename" - )} - - - - - ${this.hass.localize( - "ui.panel.config.automation.editor.actions.duplicate" - )} - - - - - ${this.hass.localize( - "ui.panel.config.automation.editor.move_up" - )} - - - - - ${this.hass.localize( - "ui.panel.config.automation.editor.move_down" - )} - - - - - ${this.hass.localize( - "ui.panel.config.automation.editor.actions.type.parallel.remove_sequence" - )} - - - -
- -
-
-
-
` - )} -
-
- - - + @value-changed=${this._actionsChanged} + .hass=${this.hass} + > +
+ + + +
`; } - private async _handleAction(ev: CustomEvent) { - switch (ev.detail.index) { - case 0: - await this._renameAction(ev); - break; - case 1: - this._duplicateParallel(ev); - break; - case 2: - this._moveUp(ev); - break; - case 3: - this._moveDown(ev); - break; - case 4: - this._removeSequence(ev); - break; - } - } - - private async _renameAction(ev: CustomEvent): Promise { - const index = (ev.target as any).idx; - const parallel = this.action.parallel - ? [...ensureArray(this.action.parallel)] - : []; - const current = parallel[index]; - const alias = await showPromptDialog(this, { - title: this.hass.localize( - "ui.panel.config.automation.editor.actions.type.parallel.change_alias" - ), - inputLabel: this.hass.localize( - "ui.panel.config.automation.editor.actions.type.parallel.alias" - ), - inputType: "string", - placeholder: capitalizeFirstLetter(this._getDescription(current)), - defaultValue: current.alias, - confirmText: this.hass.localize("ui.common.submit"), - }); - if (alias !== null) { - if (alias === "") { - delete parallel[index].alias; - } else { - parallel[index].alias = alias; - } - fireEvent(this, "value-changed", { - value: { ...this.action, parallel }, - }); - } - } - - private _duplicateParallel(ev) { - const index = (ev.target as any).idx; - this._createSequence(deepClone(ensureArray(this.action.parallel)[index])); - } - - private _actionChanged(ev: CustomEvent) { - ev.stopPropagation(); - const value = ev.detail.value as Action[]; - const index = (ev.target as any).idx; - const parallel = this.action.parallel - ? [...ensureArray(this.action.parallel)] - : []; - parallel[index].sequence = value; - fireEvent(this, "value-changed", { - value: { ...this.action, parallel }, + private _addSequenceAction() { + const actions = this.action.parallel.concat({ + ...HaSequenceAction.defaultConfig, }); - } - - protected firstUpdated() { - ensureArray(this.action.parallel).forEach(() => - this._expandedStates.push(false) - ); - } - - protected updated(changedProps: PropertyValues) { - super.updated(changedProps); - - if (this._expandLast) { - const nodes = this.shadowRoot!.querySelectorAll("ha-expansion-panel"); - nodes[nodes.length - 1].expanded = true; - this._expandLast = false; - } - } - - private _moveUp(ev) { - const index = (ev.target as any).idx; - const newIndex = index - 1; - this._move(index, newIndex); - } - - private _moveDown(ev) { - const index = (ev.target as any).idx; - const newIndex = index + 1; - this._move(index, newIndex); - } - - private _move(index: number, newIndex: number) { - const parallel = ensureArray(this.action.parallel)!.concat(); - const item = parallel.splice(index, 1)[0]; - parallel.splice(newIndex, 0, item); - - const expanded = this._expandedStates.splice(index, 1)[0]; - this._expandedStates.splice(newIndex, 0, expanded); fireEvent(this, "value-changed", { - value: { ...this.action, parallel }, + value: { + ...this.action, + parallel: actions, + }, }); } - private _addSequence() { - this._createSequence({ sequence: [] }); - } - - private _createSequence(sequence: SequenceAction) { - const parallel = this.action.parallel - ? [...ensureArray(this.action.parallel)] - : []; - parallel.push(sequence); + private _actionsChanged(ev: CustomEvent) { + ev.stopPropagation(); + const value = ev.detail.value as Action[]; fireEvent(this, "value-changed", { - value: { ...this.action, parallel }, - }); - this._expandLast = true; - this._expandedStates[parallel.length - 1] = true; - } - - private _removeSequence(ev: CustomEvent) { - const index = (ev.target as any).idx; - showConfirmationDialog(this, { - title: this.hass.localize( - "ui.panel.config.automation.editor.actions.type.parallel.delete_confirm_title" - ), - text: this.hass.localize( - "ui.panel.config.automation.editor.actions.delete_confirm_text" - ), - dismissText: this.hass.localize("ui.common.cancel"), - confirmText: this.hass.localize("ui.common.delete"), - destructive: true, - confirm: () => { - const parallel = this.action.parallel - ? [...ensureArray(this.action.parallel)] - : []; - parallel.splice(index, 1); - this._expandedStates.splice(index, 1); - fireEvent(this, "value-changed", { - value: { ...this.action, parallel }, - }); + value: { + ...this.action, + parallel: value, }, }); } @@ -412,35 +80,8 @@ export class HaParallelAction extends LitElement implements ActionElement { return [ haStyle, css` - ha-card { - margin: 16px 0; - } - .add-card mwc-button { - display: block; - text-align: center; - } - ha-expansion-panel { - --expansion-panel-summary-padding: 0 0 0 8px; - --expansion-panel-content-padding: 0; - } - h3 { - margin: 0; - font-size: inherit; - font-weight: inherit; - } - ha-icon-button { - inset-inline-start: initial; - inset-inline-end: 0; - direction: var(--direction); - } - ha-svg-icon { - height: 20px; - } - .link-button-row { - padding: 14px 14px 0 14px; - } - .card-content { - padding: 16px; + .buttons { + padding-top: 16px; } `, ]; diff --git a/src/panels/config/automation/action/types/ha-automation-action-sequence.ts b/src/panels/config/automation/action/types/ha-automation-action-sequence.ts new file mode 100644 index 000000000000..1fc810750b8e --- /dev/null +++ b/src/panels/config/automation/action/types/ha-automation-action-sequence.ts @@ -0,0 +1,61 @@ +import { CSSResultGroup, html, LitElement } from "lit"; +import { customElement, property } from "lit/decorators"; +import { fireEvent } from "../../../../../common/dom/fire_event"; +import "../../../../../components/ha-textfield"; +import { Action, SequenceAction } from "../../../../../data/script"; +import { haStyle } from "../../../../../resources/styles"; +import type { HomeAssistant, ItemPath } from "../../../../../types"; +import "../ha-automation-action"; +import type { ActionElement } from "../ha-automation-action-row"; + +@customElement("ha-automation-action-sequence") +export class HaSequenceAction extends LitElement implements ActionElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ type: Boolean }) public disabled = false; + + @property({ attribute: false }) public path?: ItemPath; + + @property({ attribute: false }) public action!: SequenceAction; + + public static get defaultConfig() { + return { + sequence: [], + }; + } + + protected render() { + const action = this.action; + + return html` + + `; + } + + private _actionsChanged(ev: CustomEvent) { + ev.stopPropagation(); + const value = ev.detail.value as Action[]; + fireEvent(this, "value-changed", { + value: { + ...this.action, + sequence: value, + }, + }); + } + + static get styles(): CSSResultGroup { + return haStyle; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-automation-action-sequence": HaSequenceAction; + } +} diff --git a/src/translations/en.json b/src/translations/en.json index 17dd96a64e9c..8abdccaf5a8b 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -3392,17 +3392,17 @@ }, "parallel": { "label": "Run in parallel", - "add_sequence": "Add parallel sequence", - "remove_sequence": "Remove sequence", - "sequence": "Sequence {number}", - "change_alias": "Rename sequence", - "alias": "Sequence name", - "delete_confirm_title": "Remove sequence?", - "actions": "Run {number} {number, plural,\n one {action}\n other {actions}\n}", - "no_actions": "No actions", + "add_sequence": "Add sequence of actions", "description": { - "picker": "Perform sequences of actions in parallel.", - "full": "Run {number} {number, plural,\n one {sequence}\n other {sequences}\n} in parallel" + "picker": "Perform actions in parallel.", + "full": "Run {number} {number, plural,\n one {action}\n other {actions}\n} in parallel" + } + }, + "sequence": { + "label": "Run in sequence", + "description": { + "picker": "Perform actions in sequence.", + "full": "Run {number} {number, plural,\n one {action}\n other {actions}\n} in sequence" } }, "variables": { From a187b1c31379ab3127657500f1b946109fe3477c Mon Sep 17 00:00:00 2001 From: surfingbytes Date: Mon, 22 Apr 2024 08:58:28 +0000 Subject: [PATCH 6/9] Button moved into slot --- .../automation/action/ha-automation-action.ts | 1 + .../types/ha-automation-action-parallel.ts | 23 +++++++++---------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/panels/config/automation/action/ha-automation-action.ts b/src/panels/config/automation/action/ha-automation-action.ts index 2722574ac0d9..9bdbe59c9731 100644 --- a/src/panels/config/automation/action/ha-automation-action.ts +++ b/src/panels/config/automation/action/ha-automation-action.ts @@ -133,6 +133,7 @@ export default class HaAutomationAction extends LitElement { > +
diff --git a/src/panels/config/automation/action/types/ha-automation-action-parallel.ts b/src/panels/config/automation/action/types/ha-automation-action-parallel.ts index c4fbcdb9e54b..3fdaf22ae2c4 100644 --- a/src/panels/config/automation/action/types/ha-automation-action-parallel.ts +++ b/src/panels/config/automation/action/types/ha-automation-action-parallel.ts @@ -37,18 +37,17 @@ export class HaParallelAction extends LitElement implements ActionElement { @value-changed=${this._actionsChanged} .hass=${this.hass} > -
- - - -
+ + + `; } From 531137081468a6ab247ed1808412eaa4932d98d3 Mon Sep 17 00:00:00 2001 From: surfingbytes Date: Mon, 22 Apr 2024 17:09:19 +0000 Subject: [PATCH 7/9] Moved button to the same row with other buttons --- .../types/ha-automation-action-parallel.ts | 37 +++++++------------ 1 file changed, 13 insertions(+), 24 deletions(-) diff --git a/src/panels/config/automation/action/types/ha-automation-action-parallel.ts b/src/panels/config/automation/action/types/ha-automation-action-parallel.ts index 3fdaf22ae2c4..8f93896088b0 100644 --- a/src/panels/config/automation/action/types/ha-automation-action-parallel.ts +++ b/src/panels/config/automation/action/types/ha-automation-action-parallel.ts @@ -1,10 +1,9 @@ import { mdiPlus } from "@mdi/js"; -import { CSSResultGroup, css, html, LitElement } from "lit"; +import { html, LitElement } from "lit"; import { customElement, property } from "lit/decorators"; import { fireEvent } from "../../../../../common/dom/fire_event"; import "../../../../../components/ha-textfield"; import { Action, ParallelAction } from "../../../../../data/script"; -import { haStyle } from "../../../../../resources/styles"; import type { HomeAssistant, ItemPath } from "../../../../../types"; import "../ha-automation-action"; import type { ActionElement } from "../ha-automation-action-row"; @@ -36,18 +35,19 @@ export class HaParallelAction extends LitElement implements ActionElement { .disabled=${this.disabled} @value-changed=${this._actionsChanged} .hass=${this.hass} - > - - - + + + + `; } @@ -74,17 +74,6 @@ export class HaParallelAction extends LitElement implements ActionElement { }, }); } - - static get styles(): CSSResultGroup { - return [ - haStyle, - css` - .buttons { - padding-top: 16px; - } - `, - ]; - } } declare global { From 0136918b4b67aaee987944b6fe411a7e963a3a2c Mon Sep 17 00:00:00 2001 From: surfingbytes Date: Thu, 2 May 2024 10:57:06 +0000 Subject: [PATCH 8/9] Revert ParallelAction interface --- src/data/script.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/data/script.ts b/src/data/script.ts index 90009644a9ee..1a2185bea3a7 100644 --- a/src/data/script.ts +++ b/src/data/script.ts @@ -252,7 +252,7 @@ export interface SequenceAction extends BaseAction { } export interface ParallelAction extends BaseAction { - parallel: SequenceAction[]; + parallel: ManualScriptConfig | Action | (ManualScriptConfig | Action)[]; } export interface SetConversationResponseAction extends BaseAction { From cd794313c6e94dbeb12cbb82a560761d1c86c0c1 Mon Sep 17 00:00:00 2001 From: surfingbytes Date: Thu, 2 May 2024 20:57:55 +0000 Subject: [PATCH 9/9] Fixed compilation error --- .../action/types/ha-automation-action-parallel.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/panels/config/automation/action/types/ha-automation-action-parallel.ts b/src/panels/config/automation/action/types/ha-automation-action-parallel.ts index 8f93896088b0..0ee2cf73784d 100644 --- a/src/panels/config/automation/action/types/ha-automation-action-parallel.ts +++ b/src/panels/config/automation/action/types/ha-automation-action-parallel.ts @@ -3,7 +3,11 @@ import { html, LitElement } from "lit"; import { customElement, property } from "lit/decorators"; import { fireEvent } from "../../../../../common/dom/fire_event"; import "../../../../../components/ha-textfield"; -import { Action, ParallelAction } from "../../../../../data/script"; +import { + Action, + ManualScriptConfig, + ParallelAction, +} from "../../../../../data/script"; import type { HomeAssistant, ItemPath } from "../../../../../types"; import "../ha-automation-action"; import type { ActionElement } from "../ha-automation-action-row"; @@ -52,7 +56,11 @@ export class HaParallelAction extends LitElement implements ActionElement { } private _addSequenceAction() { - const actions = this.action.parallel.concat({ + const currentAction = (this.action.parallel as ( + | ManualScriptConfig + | Action + )[]) ?? [this.action.parallel as ManualScriptConfig | Action]; + const actions = currentAction.concat({ ...HaSequenceAction.defaultConfig, });