mirror of
https://github.com/home-assistant/frontend.git
synced 2026-10-23 22:24:24 +00:00
Compare commits
6
Commits
dev
...
card-paste-menu
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fcbf5c1d22 | ||
|
|
672d144f43 | ||
|
|
fd4a26d3c8 | ||
|
|
57a42b420b | ||
|
|
41adc02408 | ||
|
|
46291451d7 |
No files matched your search
@@ -185,6 +185,7 @@ export function storage(options: {
|
||||
if (options.subscribe) {
|
||||
const connectedCallback = proto.connectedCallback;
|
||||
const disconnectedCallback = proto.disconnectedCallback;
|
||||
const disconnectedValues = new WeakMap<ReactiveElement, unknown>();
|
||||
|
||||
proto.connectedCallback = function () {
|
||||
connectedCallback.call(this);
|
||||
@@ -192,12 +193,20 @@ export function storage(options: {
|
||||
if (!el.__unbsubLocalStorage) {
|
||||
el.__unbsubLocalStorage = subscribeChanges?.(this);
|
||||
}
|
||||
if (disconnectedValues.has(el)) {
|
||||
const oldValue = disconnectedValues.get(el);
|
||||
disconnectedValues.delete(el);
|
||||
if (oldValue !== storageInstance.getValue(storageKey)) {
|
||||
el.requestUpdate(propertyKey, oldValue);
|
||||
}
|
||||
}
|
||||
};
|
||||
proto.disconnectedCallback = function () {
|
||||
disconnectedCallback.call(this);
|
||||
const el = this as unknown as ReactiveStorageElement;
|
||||
el.__unbsubLocalStorage?.();
|
||||
el.__unbsubLocalStorage = undefined;
|
||||
disconnectedValues.set(el, storageInstance.getValue(storageKey));
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import "@home-assistant/webawesome/dist/components/divider/divider";
|
||||
import {
|
||||
mdiContentCopy,
|
||||
mdiContentCut,
|
||||
mdiContentPaste,
|
||||
mdiCursorMove,
|
||||
mdiDelete,
|
||||
mdiDotsVertical,
|
||||
@@ -13,6 +14,7 @@ import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { consumeLocalize } from "../../../common/decorators/consume-context-entry";
|
||||
import { storage } from "../../../common/decorators/storage";
|
||||
import type { LocalizeFunc } from "../../../common/translations/localize";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import "../../../components/ha-dropdown";
|
||||
@@ -20,6 +22,7 @@ import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
|
||||
import "../../../components/ha-dropdown-item";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import type { LovelaceCardConfig } from "../../../data/lovelace/config/card";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import type { LovelacePath } from "../editor/lovelace-path";
|
||||
import type { Lovelace } from "../types";
|
||||
@@ -46,6 +49,15 @@ export class HuiCardEditMode extends LitElement {
|
||||
@consumeLocalize()
|
||||
private _localize!: LocalizeFunc;
|
||||
|
||||
@state()
|
||||
@storage({
|
||||
key: "dashboardCardClipboard",
|
||||
state: true,
|
||||
subscribe: true,
|
||||
storage: "sessionStorage",
|
||||
})
|
||||
private _clipboard?: LovelaceCardConfig;
|
||||
|
||||
@state()
|
||||
public _hover = false;
|
||||
|
||||
@@ -175,6 +187,21 @@ export class HuiCardEditMode extends LitElement {
|
||||
></ha-svg-icon>
|
||||
${this._localize("ui.panel.lovelace.editor.edit_card.cut")}
|
||||
</ha-dropdown-item>
|
||||
${
|
||||
this._clipboard && !this.noDuplicate
|
||||
? html`
|
||||
<ha-dropdown-item value="paste">
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiContentPaste}
|
||||
></ha-svg-icon>
|
||||
${this._localize(
|
||||
"ui.panel.lovelace.editor.edit_card.paste"
|
||||
)}
|
||||
</ha-dropdown-item>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
`
|
||||
}
|
||||
${
|
||||
@@ -227,6 +254,9 @@ export class HuiCardEditMode extends LitElement {
|
||||
case "cut":
|
||||
this._cutCard();
|
||||
break;
|
||||
case "paste":
|
||||
this._pasteCard();
|
||||
break;
|
||||
case "delete":
|
||||
this._deleteCard();
|
||||
break;
|
||||
@@ -249,6 +279,10 @@ export class HuiCardEditMode extends LitElement {
|
||||
fireEvent(this, "ll-copy-card", { path: this.path! });
|
||||
}
|
||||
|
||||
private _pasteCard(): void {
|
||||
fireEvent(this, "ll-paste-card", { path: this.path! });
|
||||
}
|
||||
|
||||
private _deleteCard(): void {
|
||||
fireEvent(this, "ll-delete-card", { path: this.path!, silent: false });
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import "@home-assistant/webawesome/dist/components/divider/divider";
|
||||
import {
|
||||
mdiContentCopy,
|
||||
mdiContentCut,
|
||||
mdiContentPaste,
|
||||
mdiDelete,
|
||||
mdiDotsVertical,
|
||||
mdiFileMoveOutline,
|
||||
@@ -11,7 +12,12 @@ import {
|
||||
} from "@mdi/js";
|
||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, queryAssignedElements } from "lit/decorators";
|
||||
import {
|
||||
customElement,
|
||||
property,
|
||||
queryAssignedElements,
|
||||
state,
|
||||
} from "lit/decorators";
|
||||
import { storage } from "../../../common/decorators/storage";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import "../../../components/ha-button";
|
||||
@@ -61,13 +67,14 @@ export class HuiCardOptions extends LitElement {
|
||||
@property({ attribute: "hide-position", type: Boolean })
|
||||
public hidePosition = false;
|
||||
|
||||
@state()
|
||||
@storage({
|
||||
key: "dashboardCardClipboard",
|
||||
state: false,
|
||||
subscribe: false,
|
||||
state: true,
|
||||
subscribe: true,
|
||||
storage: "sessionStorage",
|
||||
})
|
||||
protected _clipboard?: LovelaceCardConfig;
|
||||
private _clipboard?: LovelaceCardConfig;
|
||||
|
||||
public getCardSize() {
|
||||
return this._assignedElements
|
||||
@@ -189,6 +196,21 @@ export class HuiCardOptions extends LitElement {
|
||||
<ha-svg-icon slot="icon" .path=${mdiContentCut}></ha-svg-icon>
|
||||
${this.hass!.localize("ui.panel.lovelace.editor.edit_card.cut")}
|
||||
</ha-dropdown-item>
|
||||
${
|
||||
this._clipboard
|
||||
? html`
|
||||
<ha-dropdown-item value="paste">
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiContentPaste}
|
||||
></ha-svg-icon>
|
||||
${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.edit_card.paste"
|
||||
)}
|
||||
</ha-dropdown-item>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
<wa-divider></wa-divider>
|
||||
<ha-dropdown-item value="delete" variant="danger">
|
||||
<ha-svg-icon
|
||||
@@ -283,6 +305,9 @@ export class HuiCardOptions extends LitElement {
|
||||
case "cut":
|
||||
this._cutCard();
|
||||
break;
|
||||
case "paste":
|
||||
this._pasteCard();
|
||||
break;
|
||||
case "delete":
|
||||
this._deleteCard();
|
||||
break;
|
||||
@@ -305,6 +330,10 @@ export class HuiCardOptions extends LitElement {
|
||||
fireEvent(this, "ll-copy-card", { path: this.path! });
|
||||
}
|
||||
|
||||
private _pasteCard(): void {
|
||||
fireEvent(this, "ll-paste-card", { path: this.path! });
|
||||
}
|
||||
|
||||
private _deleteCard(): void {
|
||||
fireEvent(this, "ll-delete-card", { path: this.path!, silent: false });
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import { LOVELACE_EDIT_TOAST_ID } from "../types";
|
||||
import { performDeleteBadge } from "./delete-badge";
|
||||
import { performDeleteCard } from "./delete-card";
|
||||
import type { LovelacePath } from "./lovelace-path";
|
||||
import { getParentPath, insertAtPath, setAtPath } from "./lovelace-path";
|
||||
|
||||
interface ClipboardItemConfigs {
|
||||
card: LovelaceCardConfig;
|
||||
@@ -53,6 +54,50 @@ export function performCopy<K extends ClipboardItemType>(
|
||||
});
|
||||
}
|
||||
|
||||
export interface PasteParams<K extends ClipboardItemType> {
|
||||
type: K;
|
||||
path: LovelacePath;
|
||||
config: ClipboardItemConfigs[K];
|
||||
replace?: boolean;
|
||||
}
|
||||
|
||||
export async function performPaste<K extends ClipboardItemType>(
|
||||
localize: LocalizeFunc,
|
||||
lovelace: Lovelace,
|
||||
{ type, path, config, replace }: PasteParams<K>
|
||||
): Promise<void> {
|
||||
++lastVersion;
|
||||
lovelace.showToast({ id: LOVELACE_EDIT_TOAST_ID, message: "", duration: 0 });
|
||||
try {
|
||||
const index = path[path.length - 1];
|
||||
if (typeof index !== "number") {
|
||||
throw new Error("Paste target is not an item");
|
||||
}
|
||||
const pastedPath = replace ? path : [...getParentPath(path), index + 1];
|
||||
const newConfig = replace
|
||||
? setAtPath(lovelace.config, path, deepClone(config))
|
||||
: insertAtPath(lovelace.config, pastedPath, deepClone(config));
|
||||
await lovelace.saveConfig(newConfig);
|
||||
} catch (_err: unknown) {
|
||||
lovelace.showToast({
|
||||
id: LOVELACE_EDIT_TOAST_ID,
|
||||
message: localize(`ui.panel.lovelace.editor.edit_${type}.paste_error`),
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (replace) {
|
||||
lovelace.showToast({
|
||||
id: LOVELACE_EDIT_TOAST_ID,
|
||||
message: localize(`ui.panel.lovelace.editor.edit_${type}.replaced`),
|
||||
duration: 8000,
|
||||
action: {
|
||||
action: () => fireEvent(window, "undo-change"),
|
||||
text: localize("ui.common.undo"),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function performCut<K extends ClipboardItemType>(
|
||||
localize: LocalizeFunc,
|
||||
lovelace: Lovelace,
|
||||
@@ -70,6 +115,9 @@ export async function performCut<K extends ClipboardItemType>(
|
||||
}
|
||||
clipboardVersions[type] = version;
|
||||
setClipboard(deepClone(config));
|
||||
if (lastVersion !== version) {
|
||||
return;
|
||||
}
|
||||
lovelace.showToast({
|
||||
id: LOVELACE_EDIT_TOAST_ID,
|
||||
message: localize(`ui.panel.lovelace.editor.edit_${type}.cut_to_clipboard`),
|
||||
|
||||
@@ -75,6 +75,16 @@ export const moveCardToContainer = (
|
||||
return deleteAtPath(newConfig, cardPath);
|
||||
};
|
||||
|
||||
export const adoptViewLayout = (
|
||||
card: LovelaceCardConfig,
|
||||
target: LovelaceCardConfig
|
||||
): LovelaceCardConfig => {
|
||||
const { view_layout: _viewLayout, ...rest } = card;
|
||||
return target.view_layout === undefined
|
||||
? rest
|
||||
: { ...rest, view_layout: target.view_layout };
|
||||
};
|
||||
|
||||
export const getCardSectionConfig = (
|
||||
config: LovelaceConfig,
|
||||
path: LovelacePath
|
||||
|
||||
@@ -32,9 +32,13 @@ import { showCreateBadgeDialog } from "../editor/badge-editor/show-create-badge-
|
||||
import { showEditBadgeDialog } from "../editor/badge-editor/show-edit-badge-dialog";
|
||||
import { showCreateCardDialog } from "../editor/card-editor/show-create-card-dialog";
|
||||
import { showEditCardDialog } from "../editor/card-editor/show-edit-card-dialog";
|
||||
import { getCardSectionConfig } from "../editor/config-util";
|
||||
import { adoptViewLayout, getCardSectionConfig } from "../editor/config-util";
|
||||
import type { ClipboardActionParams } from "../editor/clipboard-actions";
|
||||
import { performCopy, performCut } from "../editor/clipboard-actions";
|
||||
import {
|
||||
performCopy,
|
||||
performCut,
|
||||
performPaste,
|
||||
} from "../editor/clipboard-actions";
|
||||
import {
|
||||
type DeleteBadgeParams,
|
||||
performDeleteBadge,
|
||||
@@ -47,8 +51,10 @@ import type { LovelacePath } from "../editor/lovelace-path";
|
||||
import {
|
||||
appendAtPath,
|
||||
getAtPath,
|
||||
getItemKind,
|
||||
getParentPath,
|
||||
getPathTarget,
|
||||
getViewPath,
|
||||
normalizeCardPath,
|
||||
setAtPath,
|
||||
} from "../editor/lovelace-path";
|
||||
@@ -60,6 +66,7 @@ import {
|
||||
generateLovelaceViewStrategy,
|
||||
} from "../strategies/get-strategy";
|
||||
import type { Lovelace } from "../types";
|
||||
import { PANEL_VIEW_LAYOUT, SIDEBAR_VIEW_LAYOUT } from "./const";
|
||||
import { getViewType } from "./get-view-type";
|
||||
|
||||
declare global {
|
||||
@@ -71,6 +78,7 @@ declare global {
|
||||
"ll-duplicate-card": { path: LovelacePath };
|
||||
"ll-copy-card": { path: LovelacePath };
|
||||
"ll-cut-card": { path: LovelacePath };
|
||||
"ll-paste-card": { path: LovelacePath };
|
||||
"ll-create-badge": { path: LovelacePath };
|
||||
"ll-edit-badge": { path: LovelacePath };
|
||||
"ll-delete-badge": DeleteBadgeParams;
|
||||
@@ -84,6 +92,7 @@ declare global {
|
||||
"ll-duplicate-card": HASSDomEvent<HASSDomEvents["ll-duplicate-card"]>;
|
||||
"ll-copy-card": HASSDomEvent<HASSDomEvents["ll-copy-card"]>;
|
||||
"ll-cut-card": HASSDomEvent<HASSDomEvents["ll-cut-card"]>;
|
||||
"ll-paste-card": HASSDomEvent<HASSDomEvents["ll-paste-card"]>;
|
||||
"ll-create-badge": HASSDomEvent<HASSDomEvents["ll-create-badge"]>;
|
||||
"ll-edit-badge": HASSDomEvent<HASSDomEvents["ll-edit-badge"]>;
|
||||
"ll-delete-badge": HASSDomEvent<HASSDomEvents["ll-delete-badge"]>;
|
||||
@@ -238,6 +247,32 @@ export class HUIView extends ReactiveElement {
|
||||
}
|
||||
}
|
||||
);
|
||||
this.addEventListener(
|
||||
"ll-paste-card",
|
||||
(ev: HASSDomEvent<HASSDomEvents["ll-paste-card"]>) => {
|
||||
const config = this._clipboard;
|
||||
const path = normalizeCardPath(ev.detail.path);
|
||||
const target =
|
||||
getPathTarget(path) === "item" && getItemKind(path) === "card"
|
||||
? this._getCardConfig(path)
|
||||
: undefined;
|
||||
if (!config || !target) {
|
||||
return;
|
||||
}
|
||||
const viewType = getViewType(
|
||||
getAtPath<LovelaceViewConfig>(this.lovelace.config, getViewPath(path))
|
||||
);
|
||||
performPaste(this.hass.localize, this.lovelace, {
|
||||
type: "card",
|
||||
path,
|
||||
config:
|
||||
viewType === SIDEBAR_VIEW_LAYOUT
|
||||
? adoptViewLayout(config, target)
|
||||
: config,
|
||||
replace: viewType === PANEL_VIEW_LAYOUT,
|
||||
});
|
||||
}
|
||||
);
|
||||
this.addEventListener(
|
||||
"ll-create-badge",
|
||||
(ev: HASSDomEvent<HASSDomEvents["ll-create-badge"]>) => {
|
||||
|
||||
@@ -10293,6 +10293,9 @@
|
||||
"cut": "Cut",
|
||||
"copied_to_clipboard": "Card copied to clipboard",
|
||||
"cut_to_clipboard": "Card cut to clipboard",
|
||||
"paste": "Paste",
|
||||
"paste_error": "Error while pasting card",
|
||||
"replaced": "Card replaced",
|
||||
"duplicate": "Duplicate",
|
||||
"move": "Move to view",
|
||||
"move_before": "Move card before",
|
||||
@@ -10337,6 +10340,8 @@
|
||||
"cut": "[%key:ui::panel::lovelace::editor::edit_card::cut%]",
|
||||
"copied_to_clipboard": "Badge copied to clipboard",
|
||||
"cut_to_clipboard": "Badge cut to clipboard",
|
||||
"paste_error": "Error while pasting badge",
|
||||
"replaced": "Badge replaced",
|
||||
"duplicate": "[%key:ui::panel::lovelace::editor::edit_card::duplicate%]",
|
||||
"tab_config": "[%key:ui::panel::lovelace::editor::edit_card::tab_config%]",
|
||||
"tab_visibility": "[%key:ui::panel::lovelace::editor::edit_card::tab_visibility%]"
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import { ReactiveElement } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { getStorage, storage } from "../../../src/common/decorators/storage";
|
||||
|
||||
const KEY = "test-storage-subscriber-value";
|
||||
|
||||
@customElement("test-storage-subscriber")
|
||||
class TestStorageSubscriber extends ReactiveElement {
|
||||
@storage({
|
||||
key: KEY,
|
||||
state: true,
|
||||
subscribe: true,
|
||||
storage: "sessionStorage",
|
||||
})
|
||||
public value?: string;
|
||||
|
||||
public updates = 0;
|
||||
|
||||
protected update(changedProperties) {
|
||||
super.update(changedProperties);
|
||||
this.updates++;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"test-storage-subscriber": TestStorageSubscriber;
|
||||
}
|
||||
}
|
||||
|
||||
describe("storage decorator", () => {
|
||||
afterEach(() => {
|
||||
document.body.replaceChildren();
|
||||
getStorage("sessionStorage").setValue(KEY, undefined);
|
||||
});
|
||||
|
||||
it("renders a value that changed while the element was disconnected", async () => {
|
||||
const element = document.createElement("test-storage-subscriber");
|
||||
document.body.append(element);
|
||||
await element.updateComplete;
|
||||
expect(element.updates).toBe(1);
|
||||
|
||||
element.remove();
|
||||
document.body.append(element);
|
||||
await element.updateComplete;
|
||||
expect(element.updates).toBe(1);
|
||||
|
||||
element.remove();
|
||||
getStorage("sessionStorage").setValue(KEY, "changed");
|
||||
document.body.append(element);
|
||||
await element.updateComplete;
|
||||
expect(element.updates).toBe(2);
|
||||
expect(element.value).toBe("changed");
|
||||
});
|
||||
});
|
||||
@@ -5,7 +5,10 @@ import type { LovelaceConfig } from "../../../../src/data/lovelace/config/types"
|
||||
import {
|
||||
performCopy,
|
||||
performCut,
|
||||
performPaste,
|
||||
} from "../../../../src/panels/lovelace/editor/clipboard-actions";
|
||||
import type { LovelacePath } from "../../../../src/panels/lovelace/editor/lovelace-path";
|
||||
import { getAtPath } from "../../../../src/panels/lovelace/editor/lovelace-path";
|
||||
import type { Lovelace } from "../../../../src/panels/lovelace/types";
|
||||
|
||||
const CARD_A: LovelaceCardConfig = { type: "card-a" };
|
||||
@@ -118,4 +121,134 @@ describe("performCut", () => {
|
||||
|
||||
expect(clipboard).toEqual(CARD_A);
|
||||
});
|
||||
it("keeps the clipboard but not the toast when a paste finished first", async () => {
|
||||
let finishSave!: () => void;
|
||||
const lovelace = createLovelace(
|
||||
() =>
|
||||
new Promise<void>((resolve) => {
|
||||
finishSave = resolve;
|
||||
})
|
||||
);
|
||||
let clipboard: LovelaceCardConfig | undefined;
|
||||
const setClipboard = (config: LovelaceCardConfig) => {
|
||||
clipboard = config;
|
||||
};
|
||||
|
||||
const cut = performCut(
|
||||
localize,
|
||||
lovelace,
|
||||
cardParams(CARD_A, 0, setClipboard)
|
||||
);
|
||||
await performPaste(
|
||||
localize,
|
||||
createLovelace(() => Promise.resolve()),
|
||||
{
|
||||
type: "card",
|
||||
path: ["views", 0, "cards", 1],
|
||||
config: CARD_B,
|
||||
}
|
||||
);
|
||||
vi.mocked(lovelace.showToast).mockClear();
|
||||
finishSave();
|
||||
await cut;
|
||||
|
||||
expect(clipboard).toEqual(CARD_A);
|
||||
expect(lovelace.showToast).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("performPaste", () => {
|
||||
const CLIPBOARD: LovelaceCardConfig = {
|
||||
type: "clipboard",
|
||||
entities: ["light.a"],
|
||||
};
|
||||
|
||||
const pasteInto = async (
|
||||
config: LovelaceConfig,
|
||||
params: Omit<Parameters<typeof performPaste<"card">>[2], "type" | "config">
|
||||
) => {
|
||||
const saveConfig = vi.fn<Lovelace["saveConfig"]>(() => Promise.resolve());
|
||||
const lovelace = {
|
||||
config,
|
||||
rawConfig: config,
|
||||
saveConfig,
|
||||
showToast: vi.fn(),
|
||||
} as Partial<Lovelace> as Lovelace;
|
||||
await performPaste(localize, lovelace, {
|
||||
type: "card",
|
||||
config: CLIPBOARD,
|
||||
...params,
|
||||
});
|
||||
const saved = saveConfig.mock.calls[0][0] as LovelaceConfig;
|
||||
return {
|
||||
cardsAt: (cardsPath: LovelacePath) =>
|
||||
getAtPath<LovelaceCardConfig[]>(saved, cardsPath)!,
|
||||
showToast: lovelace.showToast,
|
||||
};
|
||||
};
|
||||
|
||||
it("inserts a copy of the clipboard card after the target card", async () => {
|
||||
const { cardsAt } = await pasteInto(
|
||||
{ views: [{ cards: [CARD_A, CARD_B] }] },
|
||||
{ path: ["views", 0, "cards", 0] }
|
||||
);
|
||||
expect(cardsAt(["views", 0, "cards"])).toEqual([CARD_A, CLIPBOARD, CARD_B]);
|
||||
|
||||
const { cardsAt: nestedCardsAt } = await pasteInto(
|
||||
{ views: [{ sections: [{ cards: [CARD_A, CARD_B] }] }] },
|
||||
{ path: ["views", 0, "sections", 0, "cards", 1] }
|
||||
);
|
||||
expect(nestedCardsAt(["views", 0, "sections", 0, "cards"])).toEqual([
|
||||
CARD_A,
|
||||
CARD_B,
|
||||
CLIPBOARD,
|
||||
]);
|
||||
});
|
||||
|
||||
it("pastes an independent copy", async () => {
|
||||
const { cardsAt } = await pasteInto(
|
||||
{ views: [{ cards: [CARD_A] }] },
|
||||
{ path: ["views", 0, "cards", 0] }
|
||||
);
|
||||
const pasted = cardsAt(["views", 0, "cards"])[1];
|
||||
expect(pasted).not.toBe(CLIPBOARD);
|
||||
expect(pasted.entities).not.toBe(CLIPBOARD.entities);
|
||||
});
|
||||
|
||||
it("replaces the target card in panel views", async () => {
|
||||
const { cardsAt, showToast } = await pasteInto(
|
||||
{ views: [{ panel: true, cards: [CARD_A] }] },
|
||||
{ path: ["views", 0, "cards", 0], replace: true }
|
||||
);
|
||||
expect(cardsAt(["views", 0, "cards"])).toEqual([CLIPBOARD]);
|
||||
expect(showToast).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
message: "ui.panel.lovelace.editor.edit_card.replaced",
|
||||
action: expect.objectContaining({ text: "ui.common.undo" }),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("shows an error toast when saving fails", async () => {
|
||||
vi.spyOn(console, "error").mockImplementation(() => undefined);
|
||||
const config: LovelaceConfig = { views: [{ cards: [CARD_A] }] };
|
||||
const lovelace = {
|
||||
config,
|
||||
rawConfig: config,
|
||||
saveConfig: () => Promise.reject(new Error("save failed")),
|
||||
showToast: vi.fn(),
|
||||
} as Partial<Lovelace> as Lovelace;
|
||||
|
||||
await performPaste(localize, lovelace, {
|
||||
type: "card",
|
||||
path: ["views", 0, "cards", 0],
|
||||
config: CLIPBOARD,
|
||||
});
|
||||
|
||||
expect(lovelace.showToast).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
message: "ui.panel.lovelace.editor.edit_card.paste_error",
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -4,6 +4,7 @@ import type { LovelaceConfig } from "../../../../src/data/lovelace/config/types"
|
||||
import type { LovelaceSectionConfig } from "../../../../src/data/lovelace/config/section";
|
||||
import type { LovelaceViewConfig } from "../../../../src/data/lovelace/config/view";
|
||||
import {
|
||||
adoptViewLayout,
|
||||
duplicateSection,
|
||||
moveCardToContainer,
|
||||
swapView,
|
||||
@@ -233,3 +234,23 @@ describe("duplicateSection", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("adoptViewLayout", () => {
|
||||
it("takes the target card's view layout", () => {
|
||||
const card = { type: "card1", view_layout: { position: "sidebar" } };
|
||||
const target = { type: "card2", view_layout: { position: "main" } };
|
||||
|
||||
assert.deepEqual(adoptViewLayout(card, target), {
|
||||
type: "card1",
|
||||
view_layout: { position: "main" },
|
||||
});
|
||||
});
|
||||
|
||||
it("drops the card's own view layout when the target has none", () => {
|
||||
const card = { type: "card1", view_layout: { position: "sidebar" } };
|
||||
|
||||
assert.deepEqual(adoptViewLayout(card, { type: "card2" }), {
|
||||
type: "card1",
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user