mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-25 18:26:35 +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,
|
TemplateResult,
|
||||||
} from "lit";
|
} from "lit";
|
||||||
import { customElement, property, queryAssignedNodes } from "lit/decorators";
|
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 { fireEvent } from "../../../common/dom/fire_event";
|
||||||
import "../../../components/ha-button-menu";
|
import "../../../components/ha-button-menu";
|
||||||
import "../../../components/ha-icon-button";
|
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 { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||||
import { HomeAssistant } from "../../../types";
|
import { HomeAssistant } from "../../../types";
|
||||||
import { showSaveSuccessToast } from "../../../util/toast-saved-success";
|
import { showSaveSuccessToast } from "../../../util/toast-saved-success";
|
||||||
@ -34,6 +36,14 @@ export class HuiCardOptions extends LitElement {
|
|||||||
|
|
||||||
@queryAssignedNodes() private _assignedNodes?: NodeListOf<LovelaceCard>;
|
@queryAssignedNodes() private _assignedNodes?: NodeListOf<LovelaceCard>;
|
||||||
|
|
||||||
|
@storage({
|
||||||
|
key: "lovelaceClipboard",
|
||||||
|
state: false,
|
||||||
|
subscribe: false,
|
||||||
|
storage: "sessionStorage",
|
||||||
|
})
|
||||||
|
protected _clipboard?: LovelaceCardConfig;
|
||||||
|
|
||||||
public getCardSize() {
|
public getCardSize() {
|
||||||
return this._assignedNodes ? computeCardSize(this._assignedNodes[0]) : 1;
|
return this._assignedNodes ? computeCardSize(this._assignedNodes[0]) : 1;
|
||||||
}
|
}
|
||||||
@ -98,6 +108,16 @@ export class HuiCardOptions extends LitElement {
|
|||||||
"ui.panel.lovelace.editor.edit_card.duplicate"
|
"ui.panel.lovelace.editor.edit_card.duplicate"
|
||||||
)}</mwc-list-item
|
)}</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">
|
<mwc-list-item class="delete-item">
|
||||||
${this.hass!.localize(
|
${this.hass!.localize(
|
||||||
"ui.panel.lovelace.editor.edit_card.delete"
|
"ui.panel.lovelace.editor.edit_card.delete"
|
||||||
@ -163,7 +183,13 @@ export class HuiCardOptions extends LitElement {
|
|||||||
this._duplicateCard();
|
this._duplicateCard();
|
||||||
break;
|
break;
|
||||||
case 2:
|
case 2:
|
||||||
this._deleteCard();
|
this._copyCard();
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
this._cutCard();
|
||||||
|
break;
|
||||||
|
case 4:
|
||||||
|
this._deleteCard(true);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -183,6 +209,17 @@ export class HuiCardOptions extends LitElement {
|
|||||||
fireEvent(this, "ll-edit-card", { path: this.path! });
|
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 {
|
private _cardUp(): void {
|
||||||
const lovelace = this.lovelace!;
|
const lovelace = this.lovelace!;
|
||||||
const path = this.path!;
|
const path = this.path!;
|
||||||
@ -236,8 +273,8 @@ export class HuiCardOptions extends LitElement {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private _deleteCard(): void {
|
private _deleteCard(confirm: boolean): void {
|
||||||
fireEvent(this, "ll-delete-card", { path: this.path! });
|
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 { styleMap } from "lit/directives/style-map";
|
||||||
import { until } from "lit/directives/until";
|
import { until } from "lit/directives/until";
|
||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
|
import { storage } from "../../../../common/decorators/storage";
|
||||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||||
import "../../../../components/ha-circular-progress";
|
import "../../../../components/ha-circular-progress";
|
||||||
import "../../../../components/search-input";
|
import "../../../../components/search-input";
|
||||||
@ -49,6 +50,14 @@ interface CardElement {
|
|||||||
export class HuiCardPicker extends LitElement {
|
export class HuiCardPicker extends LitElement {
|
||||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||||
|
|
||||||
|
@storage({
|
||||||
|
key: "lovelaceClipboard",
|
||||||
|
state: true,
|
||||||
|
subscribe: true,
|
||||||
|
storage: "sessionStorage",
|
||||||
|
})
|
||||||
|
private _clipboard?: LovelaceCardConfig;
|
||||||
|
|
||||||
@state() private _cards: CardElement[] = [];
|
@state() private _cards: CardElement[] = [];
|
||||||
|
|
||||||
public lovelace?: LovelaceConfig;
|
public lovelace?: LovelaceConfig;
|
||||||
@ -114,6 +123,37 @@ export class HuiCardPicker extends LitElement {
|
|||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<div class="cards-container">
|
<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(
|
${this._filterCards(this._cards, this._filter).map(
|
||||||
(cardElement: CardElement) => cardElement.element
|
(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;
|
let { type } = card;
|
||||||
const { showElement, isCustom, name, description } = card;
|
const { showElement, isCustom, name, description } = card;
|
||||||
const customCard = isCustom ? getCustomCardEntry(type) : undefined;
|
const customCard = isCustom ? getCustomCardEntry(type) : undefined;
|
||||||
@ -281,15 +324,17 @@ export class HuiCardPicker extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let element: LovelaceCard | undefined;
|
let element: LovelaceCard | undefined;
|
||||||
let cardConfig: LovelaceCardConfig = { type };
|
let cardConfig: LovelaceCardConfig = config ?? { type };
|
||||||
|
|
||||||
if (this.hass && this.lovelace) {
|
if (this.hass && this.lovelace) {
|
||||||
cardConfig = await getCardStubConfig(
|
if (!config) {
|
||||||
this.hass,
|
cardConfig = await getCardStubConfig(
|
||||||
type,
|
this.hass,
|
||||||
this._unusedEntities!,
|
type,
|
||||||
this._usedEntities!
|
this._unusedEntities!,
|
||||||
);
|
this._usedEntities!
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (showElement) {
|
if (showElement) {
|
||||||
try {
|
try {
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import "@material/mwc-list/mwc-list-item";
|
import "@material/mwc-list/mwc-list-item";
|
||||||
import "@material/mwc-tab-bar/mwc-tab-bar";
|
import "@material/mwc-tab-bar/mwc-tab-bar";
|
||||||
import "@material/mwc-tab/mwc-tab";
|
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 type { MDCTabBarActivatedEvent } from "@material/tab-bar";
|
||||||
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
||||||
import { customElement, property, query, state } from "lit/decorators";
|
import { customElement, property, query, state } from "lit/decorators";
|
||||||
@ -13,6 +15,7 @@ import {
|
|||||||
optional,
|
optional,
|
||||||
string,
|
string,
|
||||||
} from "superstruct";
|
} from "superstruct";
|
||||||
|
import { storage } from "../../../../common/decorators/storage";
|
||||||
import { fireEvent, HASSDomEvent } from "../../../../common/dom/fire_event";
|
import { fireEvent, HASSDomEvent } from "../../../../common/dom/fire_event";
|
||||||
import { stopPropagation } from "../../../../common/dom/stop_propagation";
|
import { stopPropagation } from "../../../../common/dom/stop_propagation";
|
||||||
import "../../../../components/entity/ha-entity-picker";
|
import "../../../../components/entity/ha-entity-picker";
|
||||||
@ -56,6 +59,14 @@ export class HuiConditionalCardEditor
|
|||||||
|
|
||||||
@property({ attribute: false }) public lovelace?: LovelaceConfig;
|
@property({ attribute: false }) public lovelace?: LovelaceConfig;
|
||||||
|
|
||||||
|
@storage({
|
||||||
|
key: "lovelaceClipboard",
|
||||||
|
state: false,
|
||||||
|
subscribe: false,
|
||||||
|
storage: "sessionStorage",
|
||||||
|
})
|
||||||
|
protected _clipboard?: LovelaceCardConfig;
|
||||||
|
|
||||||
@state() private _config?: ConditionalCardConfig;
|
@state() private _config?: ConditionalCardConfig;
|
||||||
|
|
||||||
@state() private _GUImode = true;
|
@state() private _GUImode = true;
|
||||||
@ -114,6 +125,14 @@ export class HuiConditionalCardEditor
|
|||||||
: "ui.panel.lovelace.editor.edit_card.show_visual_editor"
|
: "ui.panel.lovelace.editor.edit_card.show_visual_editor"
|
||||||
)}
|
)}
|
||||||
</mwc-button>
|
</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}
|
<mwc-button @click=${this._handleReplaceCard}
|
||||||
>${this.hass!.localize(
|
>${this.hass!.localize(
|
||||||
"ui.panel.lovelace.editor.card.conditional.change_type"
|
"ui.panel.lovelace.editor.card.conditional.change_type"
|
||||||
@ -238,6 +257,13 @@ export class HuiConditionalCardEditor
|
|||||||
fireEvent(this, "config-changed", { config: this._config });
|
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 {
|
private _handleCardChanged(ev: HASSDomEvent<ConfigChangedEvent>): void {
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
if (!this._config) {
|
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";
|
||||||
import "@polymer/paper-tabs/paper-tab";
|
import "@polymer/paper-tabs/paper-tab";
|
||||||
|
import deepClone from "deep-clone-simple";
|
||||||
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
||||||
import { customElement, property, query, state } from "lit/decorators";
|
import { customElement, property, query, state } from "lit/decorators";
|
||||||
import {
|
import {
|
||||||
@ -12,6 +20,7 @@ import {
|
|||||||
optional,
|
optional,
|
||||||
string,
|
string,
|
||||||
} from "superstruct";
|
} from "superstruct";
|
||||||
|
import { storage } from "../../../../common/decorators/storage";
|
||||||
import { fireEvent, HASSDomEvent } from "../../../../common/dom/fire_event";
|
import { fireEvent, HASSDomEvent } from "../../../../common/dom/fire_event";
|
||||||
import "../../../../components/ha-icon-button";
|
import "../../../../components/ha-icon-button";
|
||||||
import { LovelaceCardConfig, LovelaceConfig } from "../../../../data/lovelace";
|
import { LovelaceCardConfig, LovelaceConfig } from "../../../../data/lovelace";
|
||||||
@ -43,6 +52,14 @@ export class HuiStackCardEditor
|
|||||||
|
|
||||||
@property({ attribute: false }) public lovelace?: LovelaceConfig;
|
@property({ attribute: false }) public lovelace?: LovelaceConfig;
|
||||||
|
|
||||||
|
@storage({
|
||||||
|
key: "lovelaceClipboard",
|
||||||
|
state: false,
|
||||||
|
subscribe: false,
|
||||||
|
storage: "sessionStorage",
|
||||||
|
})
|
||||||
|
protected _clipboard?: LovelaceCardConfig;
|
||||||
|
|
||||||
@state() protected _config?: StackCardConfig;
|
@state() protected _config?: StackCardConfig;
|
||||||
|
|
||||||
@state() protected _selectedCard = 0;
|
@state() protected _selectedCard = 0;
|
||||||
@ -129,6 +146,22 @@ export class HuiStackCardEditor
|
|||||||
.move=${1}
|
.move=${1}
|
||||||
></ha-icon-button>
|
></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
|
<ha-icon-button
|
||||||
.label=${this.hass!.localize(
|
.label=${this.hass!.localize(
|
||||||
"ui.panel.lovelace.editor.edit_card.delete"
|
"ui.panel.lovelace.editor.edit_card.delete"
|
||||||
@ -191,6 +224,18 @@ export class HuiStackCardEditor
|
|||||||
fireEvent(this, "config-changed", { config: this._config });
|
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() {
|
protected _handleDeleteCard() {
|
||||||
if (!this._config) {
|
if (!this._config) {
|
||||||
return;
|
return;
|
||||||
|
@ -26,6 +26,7 @@ import { createViewElement } from "../create-element/create-view-element";
|
|||||||
import { showCreateCardDialog } from "../editor/card-editor/show-create-card-dialog";
|
import { showCreateCardDialog } from "../editor/card-editor/show-create-card-dialog";
|
||||||
import { showEditCardDialog } from "../editor/card-editor/show-edit-card-dialog";
|
import { showEditCardDialog } from "../editor/card-editor/show-edit-card-dialog";
|
||||||
import { confDeleteCard } from "../editor/delete-card";
|
import { confDeleteCard } from "../editor/delete-card";
|
||||||
|
import { deleteCard } from "../editor/config-util";
|
||||||
import { generateLovelaceViewStrategy } from "../strategies/get-strategy";
|
import { generateLovelaceViewStrategy } from "../strategies/get-strategy";
|
||||||
import type { Lovelace, LovelaceBadge, LovelaceCard } from "../types";
|
import type { Lovelace, LovelaceBadge, LovelaceCard } from "../types";
|
||||||
import { PANEL_VIEW_LAYOUT, DEFAULT_VIEW_LAYOUT } from "./const";
|
import { PANEL_VIEW_LAYOUT, DEFAULT_VIEW_LAYOUT } from "./const";
|
||||||
@ -35,7 +36,7 @@ declare global {
|
|||||||
interface HASSDomEvents {
|
interface HASSDomEvents {
|
||||||
"ll-create-card": undefined;
|
"ll-create-card": undefined;
|
||||||
"ll-edit-card": { path: [number] | [number, number] };
|
"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) => {
|
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",
|
"edit": "Edit",
|
||||||
"clear": "Clear",
|
"clear": "Clear",
|
||||||
"delete": "Delete card",
|
"delete": "Delete card",
|
||||||
|
"copy": "Copy card",
|
||||||
|
"cut": "Cut card",
|
||||||
"duplicate": "Duplicate card",
|
"duplicate": "Duplicate card",
|
||||||
"move": "Move to view",
|
"move": "Move to view",
|
||||||
"move_up": "Move card up",
|
"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?",
|
"manual_description": "Need to add a custom card or just want to manually write the YAML?",
|
||||||
"minimum": "Minimum",
|
"minimum": "Minimum",
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
|
"paste": "Paste from Clipboard",
|
||||||
|
"paste_description": "Paste a {type} card from the clipboard",
|
||||||
"refresh_interval": "Refresh Interval",
|
"refresh_interval": "Refresh Interval",
|
||||||
"show_icon": "Show Icon?",
|
"show_icon": "Show Icon?",
|
||||||
"show_name": "Show Name?",
|
"show_name": "Show Name?",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user