1
0
mirror of https://github.com/home-assistant/frontend.git synced 2026-04-02 00:27:49 +01:00

Add ability to duplicate a section (#30265)

* Add ability to duplicate a section

* Move section edit mode buttons to overflow menu

* Fix typing for concat and push parameters

* Fix incorrect clipboard typing for badges
This commit is contained in:
Maarten Lakerveld
2026-03-30 14:59:27 +02:00
committed by GitHub
parent 10c90d222d
commit bbda7affdc
9 changed files with 175 additions and 23 deletions

View File

@@ -198,7 +198,7 @@ export default class HaAutomationAction extends AutomationSortableListMixin<Acti
private _addAction = (action: string, target?: HassServiceTarget) => {
let actions: Action[];
if (action === PASTE_VALUE) {
actions = this.actions.concat(deepClone(this._clipboard!.action));
actions = this.actions.concat(deepClone(this._clipboard!.action!));
} else if (action in VIRTUAL_ACTIONS) {
actions = this.actions.concat(VIRTUAL_ACTIONS[action]);
} else if (isDynamic(action)) {

View File

@@ -287,7 +287,7 @@ export default class HaAutomationCondition extends AutomationSortableListMixin<C
let conditions: Condition[];
if (value === PASTE_VALUE) {
conditions = this.conditions.concat(
deepClone(this._clipboard!.condition)
deepClone(this._clipboard!.condition!)
);
} else if (isDynamic(value)) {
conditions = this.conditions.concat({

View File

@@ -203,7 +203,7 @@ export default class HaAutomationTrigger extends AutomationSortableListMixin<Tri
private _addTrigger = (value: string, target?: HassServiceTarget) => {
let triggers: Trigger[];
if (value === PASTE_VALUE) {
triggers = this.triggers.concat(deepClone(this._clipboard!.trigger));
triggers = this.triggers.concat(deepClone(this._clipboard!.trigger!));
} else if (isDynamic(value)) {
triggers = this.triggers.concat({
trigger: getValueFromDynamic(value),

View File

@@ -19,8 +19,10 @@ import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
import "../../../components/ha-dropdown-item";
import "../../../components/ha-icon-button";
import "../../../components/ha-svg-icon";
import { ensureBadgeConfig } from "../../../data/lovelace/config/badge";
import type { LovelaceCardConfig } from "../../../data/lovelace/config/card";
import {
ensureBadgeConfig,
type LovelaceBadgeConfig,
} from "../../../data/lovelace/config/badge";
import { haStyle } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types";
import { showEditBadgeDialog } from "../editor/badge-editor/show-edit-badge-dialog";
@@ -58,7 +60,7 @@ export class HuiBadgeEditMode extends LitElement {
subscribe: false,
storage: "sessionStorage",
})
protected _clipboard?: LovelaceCardConfig;
protected _clipboard?: string | Partial<LovelaceBadgeConfig>;
private get _badges() {
const containerPath = getLovelaceContainerPath(this.path!);

View File

@@ -1,13 +1,23 @@
import { mdiDelete, mdiDragHorizontalVariant, mdiPencil } from "@mdi/js";
import "@home-assistant/webawesome/dist/components/divider/divider";
import {
mdiDelete,
mdiDotsVertical,
mdiDragHorizontalVariant,
mdiPencil,
mdiPlusCircleMultipleOutline,
} from "@mdi/js";
import type { CSSResultGroup, TemplateResult } from "lit";
import { LitElement, css, html } from "lit";
import { customElement, property } from "lit/decorators";
import "../../../components/ha-dropdown";
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
import "../../../components/ha-dropdown-item";
import "../../../components/ha-icon-button";
import "../../../components/ha-svg-icon";
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
import { haStyle } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types";
import { deleteSection } from "../editor/config-util";
import { deleteSection, duplicateSection } from "../editor/config-util";
import { findLovelaceContainer } from "../editor/lovelace-path";
import { showEditSectionDialog } from "../editor/section-editor/show-edit-section-dialog";
import type { Lovelace } from "../types";
@@ -31,16 +41,32 @@ export class HuiSectionEditMode extends LitElement {
class="handle"
.path=${mdiDragHorizontalVariant}
></ha-svg-icon>
<ha-icon-button
.label=${this.hass.localize("ui.common.edit")}
@click=${this._editSection}
.path=${mdiPencil}
></ha-icon-button>
<ha-icon-button
.label=${this.hass.localize("ui.common.delete")}
@click=${this._deleteSection}
.path=${mdiDelete}
></ha-icon-button>
<ha-dropdown
placement="bottom-end"
@wa-select=${this._handleDropdownSelect}
>
<ha-icon-button
slot="trigger"
.label=${this.hass.localize("ui.common.menu")}
.path=${mdiDotsVertical}
></ha-icon-button>
<ha-dropdown-item value="edit">
<ha-svg-icon slot="icon" .path=${mdiPencil}></ha-svg-icon>
${this.hass.localize("ui.common.edit")}
</ha-dropdown-item>
<ha-dropdown-item value="duplicate">
<ha-svg-icon
slot="icon"
.path=${mdiPlusCircleMultipleOutline}
></ha-svg-icon>
${this.hass.localize("ui.common.duplicate")}
</ha-dropdown-item>
<wa-divider></wa-divider>
<ha-dropdown-item value="delete" variant="danger">
<ha-svg-icon slot="icon" .path=${mdiDelete}></ha-svg-icon>
${this.hass.localize("ui.common.delete")}
</ha-dropdown-item>
</ha-dropdown>
</div>
</div>
<div class="section-wrapper">
@@ -49,8 +75,23 @@ export class HuiSectionEditMode extends LitElement {
`;
}
private async _editSection(ev) {
ev.stopPropagation();
private _handleDropdownSelect(ev: HaDropdownSelectEvent): void {
const action = ev.detail?.item?.value;
if (!action) return;
switch (action) {
case "edit":
this._editSection();
break;
case "duplicate":
this._duplicateSection();
break;
case "delete":
this._deleteSection();
break;
}
}
private async _editSection() {
showEditSectionDialog(this, {
lovelace: this.lovelace!,
lovelaceConfig: this.lovelace!.config,
@@ -62,8 +103,16 @@ export class HuiSectionEditMode extends LitElement {
});
}
private async _deleteSection(ev) {
ev.stopPropagation();
private _duplicateSection(): void {
const newConfig = duplicateSection(
this.lovelace!.config,
this.viewIndex,
this.index
);
this.lovelace!.saveConfig(newConfig);
}
private async _deleteSection() {
const path = [this.viewIndex, this.index] as [number, number];
const section = findLovelaceContainer(this.lovelace!.config, path);

View File

@@ -153,7 +153,7 @@ export class HaCardConditionsEditor extends LitElement {
}
if (condition === "paste") {
const newCondition = deepClone(this._clipboard);
const newCondition = deepClone(this._clipboard!);
conditions.push(newCondition);
} else {
const elClass = customElements.get(`ha-card-condition-${condition}`) as

View File

@@ -1,3 +1,4 @@
import deepClone from "deep-clone-simple";
import type { LovelaceBadgeConfig } from "../../../data/lovelace/config/badge";
import { ensureBadgeConfig } from "../../../data/lovelace/config/badge";
import type { LovelaceCardConfig } from "../../../data/lovelace/config/card";
@@ -303,6 +304,19 @@ export const deleteSection = (
return newConfig;
};
export const duplicateSection = (
config: LovelaceConfig,
viewIndex: number,
sectionIndex: number
): LovelaceConfig => {
const view = findLovelaceContainer(config, [viewIndex]);
if (isStrategyView(view)) {
throw new Error("Duplicating sections in a strategy is not supported.");
}
const clone = deepClone(view.sections![sectionIndex]);
return insertSection(config, viewIndex, sectionIndex + 1, clone);
};
export const insertSection = (
config: LovelaceConfig,
viewIndex: number,

3
src/types/deep-clone-simple.d.ts vendored Normal file
View File

@@ -0,0 +1,3 @@
declare module "deep-clone-simple" {
export default function deepClone<T>(data: T): T;
}