mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-24 09:46:36 +00:00
Support cut/copy/paste in dashboard UI editor (#16707)
This commit is contained in:
parent
2929bf5b1a
commit
9bcbb6f914
@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -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?",
|
||||
|
Loading…
x
Reference in New Issue
Block a user