Support cut/copy/paste in dashboard UI editor (#16707)

This commit is contained in:
karwosts 2023-06-26 02:14:14 -07:00 committed by GitHub
parent 2929bf5b1a
commit 9bcbb6f914
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 178 additions and 15 deletions

View File

@ -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<LovelaceCard>;
@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"
)}</mwc-list-item
>
<mwc-list-item
>${this.hass!.localize(
"ui.panel.lovelace.editor.edit_card.copy"
)}</mwc-list-item
>
<mwc-list-item
>${this.hass!.localize(
"ui.panel.lovelace.editor.edit_card.cut"
)}</mwc-list-item
>
<mwc-list-item class="delete-item">
${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 });
}
}

View File

@ -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 {
})}
>
<div class="cards-container">
${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`
<div class="card spinner">
<ha-circular-progress
active
alt="Loading"
></ha-circular-progress>
</div>
`
)}
`
: 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<TemplateResult> {
private async _renderCardElement(
card: Card,
config?: LovelaceCardConfig
): Promise<TemplateResult> {
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 {

View File

@ -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"
)}
</mwc-button>
<ha-icon-button
.label=${this.hass!.localize(
"ui.panel.lovelace.editor.edit_card.copy"
)}
.path=${mdiContentCopy}
@click=${this._handleCopyCard}
></ha-icon-button>
<mwc-button @click=${this._handleReplaceCard}
>${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<ConfigChangedEvent>): void {
ev.stopPropagation();
if (!this._config) {

View File

@ -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}
></ha-icon-button>
<ha-icon-button
.label=${this.hass!.localize(
"ui.panel.lovelace.editor.edit_card.copy"
)}
.path=${mdiContentCopy}
@click=${this._handleCopyCard}
></ha-icon-button>
<ha-icon-button
.label=${this.hass!.localize(
"ui.panel.lovelace.editor.edit_card.cut"
)}
.path=${mdiContentCut}
@click=${this._handleCutCard}
></ha-icon-button>
<ha-icon-button
.label=${this.hass!.localize(
"ui.panel.lovelace.editor.edit_card.delete"
@ -191,6 +224,18 @@ export class HuiStackCardEditor
fireEvent(this, "config-changed", { config: this._config });
}
protected _handleCopyCard() {
if (!this._config) {
return;
}
this._clipboard = deepClone(this._config.cards[this._selectedCard]);
}
protected _handleCutCard() {
this._handleCopyCard();
this._handleDeleteCard();
}
protected _handleDeleteCard() {
if (!this._config) {
return;

View File

@ -26,6 +26,7 @@ import { createViewElement } from "../create-element/create-view-element";
import { showCreateCardDialog } from "../editor/card-editor/show-create-card-dialog";
import { showEditCardDialog } from "../editor/card-editor/show-edit-card-dialog";
import { confDeleteCard } from "../editor/delete-card";
import { deleteCard } from "../editor/config-util";
import { generateLovelaceViewStrategy } from "../strategies/get-strategy";
import type { Lovelace, LovelaceBadge, LovelaceCard } from "../types";
import { PANEL_VIEW_LAYOUT, DEFAULT_VIEW_LAYOUT } from "./const";
@ -35,7 +36,7 @@ declare global {
interface HASSDomEvents {
"ll-create-card": undefined;
"ll-edit-card": { path: [number] | [number, number] };
"ll-delete-card": { path: [number] | [number, number] };
"ll-delete-card": { path: [number] | [number, number]; confirm: boolean };
}
}
@ -251,7 +252,12 @@ export class HUIView extends ReactiveElement {
});
});
this._layoutElement.addEventListener("ll-delete-card", (ev) => {
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);
}
});
}

View File

@ -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?",