From 9bcbb6f9146dd549d666df139d636bf5abdbe83b Mon Sep 17 00:00:00 2001 From: karwosts <32912880+karwosts@users.noreply.github.com> Date: Mon, 26 Jun 2023 02:14:14 -0700 Subject: [PATCH] Support cut/copy/paste in dashboard UI editor (#16707) --- .../lovelace/components/hui-card-options.ts | 45 ++++++++++++-- .../editor/card-editor/hui-card-picker.ts | 61 ++++++++++++++++--- .../hui-conditional-card-editor.ts | 26 ++++++++ .../config-elements/hui-stack-card-editor.ts | 47 +++++++++++++- src/panels/lovelace/views/hui-view.ts | 10 ++- src/translations/en.json | 4 ++ 6 files changed, 178 insertions(+), 15 deletions(-) diff --git a/src/panels/lovelace/components/hui-card-options.ts b/src/panels/lovelace/components/hui-card-options.ts index 18c32eafa4..6a39d8b590 100644 --- a/src/panels/lovelace/components/hui-card-options.ts +++ b/src/panels/lovelace/components/hui-card-options.ts @@ -11,10 +11,12 @@ import { TemplateResult, } from "lit"; import { customElement, property, queryAssignedNodes } from "lit/decorators"; +import deepClone from "deep-clone-simple"; +import { storage } from "../../../common/decorators/storage"; import { fireEvent } from "../../../common/dom/fire_event"; import "../../../components/ha-button-menu"; import "../../../components/ha-icon-button"; -import { saveConfig } from "../../../data/lovelace"; +import { saveConfig, LovelaceCardConfig } from "../../../data/lovelace"; import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box"; import { HomeAssistant } from "../../../types"; import { showSaveSuccessToast } from "../../../util/toast-saved-success"; @@ -34,6 +36,14 @@ export class HuiCardOptions extends LitElement { @queryAssignedNodes() private _assignedNodes?: NodeListOf; + @storage({ + key: "lovelaceClipboard", + state: false, + subscribe: false, + storage: "sessionStorage", + }) + protected _clipboard?: LovelaceCardConfig; + public getCardSize() { return this._assignedNodes ? computeCardSize(this._assignedNodes[0]) : 1; } @@ -98,6 +108,16 @@ export class HuiCardOptions extends LitElement { "ui.panel.lovelace.editor.edit_card.duplicate" )} + ${this.hass!.localize( + "ui.panel.lovelace.editor.edit_card.copy" + )} + ${this.hass!.localize( + "ui.panel.lovelace.editor.edit_card.cut" + )} ${this.hass!.localize( "ui.panel.lovelace.editor.edit_card.delete" @@ -163,7 +183,13 @@ export class HuiCardOptions extends LitElement { this._duplicateCard(); break; case 2: - this._deleteCard(); + this._copyCard(); + break; + case 3: + this._cutCard(); + break; + case 4: + this._deleteCard(true); break; } } @@ -183,6 +209,17 @@ export class HuiCardOptions extends LitElement { fireEvent(this, "ll-edit-card", { path: this.path! }); } + private _cutCard(): void { + this._copyCard(); + this._deleteCard(false); + } + + private _copyCard(): void { + const cardConfig = + this.lovelace!.config.views[this.path![0]].cards![this.path![1]]; + this._clipboard = deepClone(cardConfig); + } + private _cardUp(): void { const lovelace = this.lovelace!; const path = this.path!; @@ -236,8 +273,8 @@ export class HuiCardOptions extends LitElement { }); } - private _deleteCard(): void { - fireEvent(this, "ll-delete-card", { path: this.path! }); + private _deleteCard(confirm: boolean): void { + fireEvent(this, "ll-delete-card", { path: this.path!, confirm }); } } diff --git a/src/panels/lovelace/editor/card-editor/hui-card-picker.ts b/src/panels/lovelace/editor/card-editor/hui-card-picker.ts index 16f2a4cead..415b89b993 100644 --- a/src/panels/lovelace/editor/card-editor/hui-card-picker.ts +++ b/src/panels/lovelace/editor/card-editor/hui-card-picker.ts @@ -15,6 +15,7 @@ import { classMap } from "lit/directives/class-map"; import { styleMap } from "lit/directives/style-map"; import { until } from "lit/directives/until"; import memoizeOne from "memoize-one"; +import { storage } from "../../../../common/decorators/storage"; import { fireEvent } from "../../../../common/dom/fire_event"; import "../../../../components/ha-circular-progress"; import "../../../../components/search-input"; @@ -49,6 +50,14 @@ interface CardElement { export class HuiCardPicker extends LitElement { @property({ attribute: false }) public hass?: HomeAssistant; + @storage({ + key: "lovelaceClipboard", + state: true, + subscribe: true, + storage: "sessionStorage", + }) + private _clipboard?: LovelaceCardConfig; + @state() private _cards: CardElement[] = []; public lovelace?: LovelaceConfig; @@ -114,6 +123,37 @@ export class HuiCardPicker extends LitElement { })} >
+ ${this._clipboard + ? html` + ${until( + this._renderCardElement( + { + type: this._clipboard.type, + showElement: true, + isCustom: false, + name: this.hass!.localize( + "ui.panel.lovelace.editor.card.generic.paste" + ), + description: `${this.hass!.localize( + "ui.panel.lovelace.editor.card.generic.paste_description", + { + type: this._clipboard.type, + } + )}`, + }, + this._clipboard + ), + html` +
+ +
+ ` + )} + ` + : nothing} ${this._filterCards(this._cards, this._filter).map( (cardElement: CardElement) => cardElement.element )} @@ -272,7 +312,10 @@ export class HuiCardPicker extends LitElement { } } - private async _renderCardElement(card: Card): Promise { + private async _renderCardElement( + card: Card, + config?: LovelaceCardConfig + ): Promise { let { type } = card; const { showElement, isCustom, name, description } = card; const customCard = isCustom ? getCustomCardEntry(type) : undefined; @@ -281,15 +324,17 @@ export class HuiCardPicker extends LitElement { } let element: LovelaceCard | undefined; - let cardConfig: LovelaceCardConfig = { type }; + let cardConfig: LovelaceCardConfig = config ?? { type }; if (this.hass && this.lovelace) { - cardConfig = await getCardStubConfig( - this.hass, - type, - this._unusedEntities!, - this._usedEntities! - ); + if (!config) { + cardConfig = await getCardStubConfig( + this.hass, + type, + this._unusedEntities!, + this._usedEntities! + ); + } if (showElement) { try { diff --git a/src/panels/lovelace/editor/config-elements/hui-conditional-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-conditional-card-editor.ts index 69c48fb105..b2c8566af6 100644 --- a/src/panels/lovelace/editor/config-elements/hui-conditional-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-conditional-card-editor.ts @@ -1,6 +1,8 @@ import "@material/mwc-list/mwc-list-item"; import "@material/mwc-tab-bar/mwc-tab-bar"; import "@material/mwc-tab/mwc-tab"; +import { mdiContentCopy } from "@mdi/js"; +import deepClone from "deep-clone-simple"; import type { MDCTabBarActivatedEvent } from "@material/tab-bar"; import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; import { customElement, property, query, state } from "lit/decorators"; @@ -13,6 +15,7 @@ import { optional, string, } from "superstruct"; +import { storage } from "../../../../common/decorators/storage"; import { fireEvent, HASSDomEvent } from "../../../../common/dom/fire_event"; import { stopPropagation } from "../../../../common/dom/stop_propagation"; import "../../../../components/entity/ha-entity-picker"; @@ -56,6 +59,14 @@ export class HuiConditionalCardEditor @property({ attribute: false }) public lovelace?: LovelaceConfig; + @storage({ + key: "lovelaceClipboard", + state: false, + subscribe: false, + storage: "sessionStorage", + }) + protected _clipboard?: LovelaceCardConfig; + @state() private _config?: ConditionalCardConfig; @state() private _GUImode = true; @@ -114,6 +125,14 @@ export class HuiConditionalCardEditor : "ui.panel.lovelace.editor.edit_card.show_visual_editor" )} + + ${this.hass!.localize( "ui.panel.lovelace.editor.card.conditional.change_type" @@ -238,6 +257,13 @@ export class HuiConditionalCardEditor fireEvent(this, "config-changed", { config: this._config }); } + protected _handleCopyCard() { + if (!this._config) { + return; + } + this._clipboard = deepClone(this._config.card); + } + private _handleCardChanged(ev: HASSDomEvent): void { ev.stopPropagation(); if (!this._config) { diff --git a/src/panels/lovelace/editor/config-elements/hui-stack-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-stack-card-editor.ts index 9809a3ac6a..79dafd0aaf 100644 --- a/src/panels/lovelace/editor/config-elements/hui-stack-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-stack-card-editor.ts @@ -1,6 +1,14 @@ -import { mdiArrowLeft, mdiArrowRight, mdiDelete, mdiPlus } from "@mdi/js"; +import { + mdiArrowLeft, + mdiArrowRight, + mdiDelete, + mdiContentCut, + mdiContentCopy, + mdiPlus, +} from "@mdi/js"; import "@polymer/paper-tabs"; import "@polymer/paper-tabs/paper-tab"; +import deepClone from "deep-clone-simple"; import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; import { customElement, property, query, state } from "lit/decorators"; import { @@ -12,6 +20,7 @@ import { optional, string, } from "superstruct"; +import { storage } from "../../../../common/decorators/storage"; import { fireEvent, HASSDomEvent } from "../../../../common/dom/fire_event"; import "../../../../components/ha-icon-button"; import { LovelaceCardConfig, LovelaceConfig } from "../../../../data/lovelace"; @@ -43,6 +52,14 @@ export class HuiStackCardEditor @property({ attribute: false }) public lovelace?: LovelaceConfig; + @storage({ + key: "lovelaceClipboard", + state: false, + subscribe: false, + storage: "sessionStorage", + }) + protected _clipboard?: LovelaceCardConfig; + @state() protected _config?: StackCardConfig; @state() protected _selectedCard = 0; @@ -129,6 +146,22 @@ export class HuiStackCardEditor .move=${1} > + + + + { - confDeleteCard(this, this.hass!, this.lovelace!, ev.detail.path); + if (ev.detail.confirm) { + confDeleteCard(this, this.hass!, this.lovelace!, ev.detail.path); + } else { + const newLovelace = deleteCard(this.lovelace!.config, ev.detail.path); + this.lovelace.saveConfig(newLovelace); + } }); } diff --git a/src/translations/en.json b/src/translations/en.json index 6ed1039023..883659695f 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -4454,6 +4454,8 @@ "edit": "Edit", "clear": "Clear", "delete": "Delete card", + "copy": "Copy card", + "cut": "Cut card", "duplicate": "Duplicate card", "move": "Move to view", "move_up": "Move card up", @@ -4708,6 +4710,8 @@ "manual_description": "Need to add a custom card or just want to manually write the YAML?", "minimum": "Minimum", "name": "Name", + "paste": "Paste from Clipboard", + "paste_description": "Paste a {type} card from the clipboard", "refresh_interval": "Refresh Interval", "show_icon": "Show Icon?", "show_name": "Show Name?",