Allow to convert a view to sections view (#22594)

* Add imported cards in section view

* Add convert logic

* Improve editor

* Fix type

* Use imported container for individual card move

* Fix type import

* Add missing translations

* Feedback
This commit is contained in:
Paul Bottein 2024-10-30 13:27:12 +01:00 committed by GitHub
parent 3c8da03d66
commit 29d9b61319
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 358 additions and 154 deletions

View File

@ -34,6 +34,7 @@ export interface LovelaceSectionElement extends HTMLElement {
index?: number; index?: number;
cards?: HuiCard[]; cards?: HuiCard[];
isStrategy: boolean; isStrategy: boolean;
importOnly?: boolean;
setConfig(config: LovelaceSectionConfig): void; setConfig(config: LovelaceSectionConfig): void;
} }

View File

@ -1,8 +1,8 @@
import "@material/mwc-button"; import "@material/mwc-button";
import type { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
import { import {
mdiContentCopy, mdiContentCopy,
mdiContentCut, mdiContentCut,
mdiCursorMove,
mdiDelete, mdiDelete,
mdiDotsVertical, mdiDotsVertical,
mdiPencil, mdiPencil,
@ -10,7 +10,7 @@ import {
} from "@mdi/js"; } from "@mdi/js";
import deepClone from "deep-clone-simple"; import deepClone from "deep-clone-simple";
import type { CSSResultGroup, TemplateResult } from "lit"; import type { CSSResultGroup, TemplateResult } from "lit";
import { LitElement, css, html } from "lit"; import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map"; import { classMap } from "lit/directives/class-map";
import { storage } from "../../../common/decorators/storage"; import { storage } from "../../../common/decorators/storage";
@ -39,7 +39,14 @@ export class HuiCardEditMode extends LitElement {
@property({ type: Array }) public path!: LovelaceCardPath; @property({ type: Array }) public path!: LovelaceCardPath;
@property({ type: Boolean }) public hiddenOverlay = false; @property({ type: Boolean, attribute: "hidden-overlay" })
public hiddenOverlay = false;
@property({ type: Boolean, attribute: "no-edit" })
public noEdit = false;
@property({ type: Boolean, attribute: "no-duplicate" })
public noDuplicate = false;
@state() @state()
public _menuOpened: boolean = false; public _menuOpened: boolean = false;
@ -110,15 +117,24 @@ export class HuiCardEditMode extends LitElement {
return html` return html`
<div class="card-wrapper" inert><slot></slot></div> <div class="card-wrapper" inert><slot></slot></div>
<div class="card-overlay ${classMap({ visible: showOverlay })}"> <div class="card-overlay ${classMap({ visible: showOverlay })}">
<div ${this.noEdit
class="edit" ? html`
@click=${this._handleOverlayClick} <div class="control">
@keydown=${this._handleOverlayClick} <div class="control-overlay"></div>
tabindex="0" <ha-svg-icon .path=${mdiCursorMove}> </ha-svg-icon>
> </div>
<div class="edit-overlay"></div> `
<ha-svg-icon class="edit" .path=${mdiPencil}> </ha-svg-icon> : html`
</div> <div
class="control"
@click=${this._handleOverlayClick}
@keydown=${this._handleOverlayClick}
tabindex="0"
>
<div class="control-overlay"></div>
<ha-svg-icon .path=${mdiPencil}> </ha-svg-icon>
</div>
`}
<ha-button-menu <ha-button-menu
class="more" class="more"
corner="BOTTOM_END" corner="BOTTOM_END"
@ -130,29 +146,60 @@ export class HuiCardEditMode extends LitElement {
> >
<ha-icon-button slot="trigger" .path=${mdiDotsVertical}> <ha-icon-button slot="trigger" .path=${mdiDotsVertical}>
</ha-icon-button> </ha-icon-button>
<ha-list-item graphic="icon"> ${this.noEdit
<ha-svg-icon slot="graphic" .path=${mdiPencil}></ha-svg-icon> ? nothing
${this.hass.localize("ui.panel.lovelace.editor.edit_card.edit")} : html`
</ha-list-item> <ha-list-item
<ha-list-item graphic="icon"> graphic="icon"
<ha-svg-icon @click=${this._handleAction}
slot="graphic" .action=${"edit"}
.path=${mdiPlusCircleMultipleOutline} >
></ha-svg-icon> <ha-svg-icon slot="graphic" .path=${mdiPencil}></ha-svg-icon>
${this.hass.localize( ${this.hass.localize(
"ui.panel.lovelace.editor.edit_card.duplicate" "ui.panel.lovelace.editor.edit_card.edit"
)} )}
</ha-list-item> </ha-list-item>
<ha-list-item graphic="icon"> `}
${this.noDuplicate
? nothing
: html`
<ha-list-item
graphic="icon"
@click=${this._handleAction}
.action=${"duplicate"}
>
<ha-svg-icon
slot="graphic"
.path=${mdiPlusCircleMultipleOutline}
></ha-svg-icon>
${this.hass.localize(
"ui.panel.lovelace.editor.edit_card.duplicate"
)}
</ha-list-item>
`}
<ha-list-item
graphic="icon"
@click=${this._handleAction}
.action=${"copy"}
>
<ha-svg-icon slot="graphic" .path=${mdiContentCopy}></ha-svg-icon> <ha-svg-icon slot="graphic" .path=${mdiContentCopy}></ha-svg-icon>
${this.hass.localize("ui.panel.lovelace.editor.edit_card.copy")} ${this.hass.localize("ui.panel.lovelace.editor.edit_card.copy")}
</ha-list-item> </ha-list-item>
<ha-list-item graphic="icon"> <ha-list-item
graphic="icon"
@click=${this._handleAction}
.action=${"cut"}
>
<ha-svg-icon slot="graphic" .path=${mdiContentCut}></ha-svg-icon> <ha-svg-icon slot="graphic" .path=${mdiContentCut}></ha-svg-icon>
${this.hass.localize("ui.panel.lovelace.editor.edit_card.cut")} ${this.hass.localize("ui.panel.lovelace.editor.edit_card.cut")}
</ha-list-item> </ha-list-item>
<li divider role="separator"></li> <li divider role="separator"></li>
<ha-list-item graphic="icon" class="warning"> <ha-list-item
graphic="icon"
class="warning"
@click=${this._handleAction}
.action=${"delete"}
>
${this.hass.localize("ui.panel.lovelace.editor.edit_card.delete")} ${this.hass.localize("ui.panel.lovelace.editor.edit_card.delete")}
<ha-svg-icon <ha-svg-icon
class="warning" class="warning"
@ -185,21 +232,21 @@ export class HuiCardEditMode extends LitElement {
this._editCard(); this._editCard();
} }
private _handleAction(ev: CustomEvent<ActionDetail>) { private _handleAction(ev) {
switch (ev.detail.index) { switch (ev.target.action) {
case 0: case "edit":
this._editCard(); this._editCard();
break; break;
case 1: case "duplicate":
this._duplicateCard(); this._duplicateCard();
break; break;
case 2: case "copy":
this._copyCard(); this._copyCard();
break; break;
case 3: case "cut":
this._cutCard(); this._cutCard();
break; break;
case 4: case "delete":
this._deleteCard(); this._deleteCard();
break; break;
} }
@ -262,7 +309,7 @@ export class HuiCardEditMode extends LitElement {
z-index: 0; z-index: 0;
} }
.edit { .control {
outline: none !important; outline: none !important;
cursor: pointer; cursor: pointer;
position: absolute; position: absolute;
@ -273,7 +320,7 @@ export class HuiCardEditMode extends LitElement {
border-radius: var(--ha-card-border-radius, 12px); border-radius: var(--ha-card-border-radius, 12px);
z-index: 0; z-index: 0;
} }
.edit-overlay { .control-overlay {
position: absolute; position: absolute;
inset: 0; inset: 0;
opacity: 0.8; opacity: 0.8;
@ -282,7 +329,7 @@ export class HuiCardEditMode extends LitElement {
border-radius: var(--ha-card-border-radius, 12px); border-radius: var(--ha-card-border-radius, 12px);
z-index: 0; z-index: 0;
} }
.edit ha-svg-icon { .control ha-svg-icon {
display: flex; display: flex;
position: relative; position: relative;
color: var(--primary-text-color); color: var(--primary-text-color);

View File

@ -21,22 +21,17 @@ import "../../../components/ha-icon-button";
import "../../../components/ha-list-item"; import "../../../components/ha-list-item";
import type { LovelaceCardConfig } from "../../../data/lovelace/config/card"; import type { LovelaceCardConfig } from "../../../data/lovelace/config/card";
import { saveConfig } from "../../../data/lovelace/config/types"; import { saveConfig } from "../../../data/lovelace/config/types";
import { import { isStrategyView } from "../../../data/lovelace/config/view";
isStrategyView,
type LovelaceViewConfig,
} from "../../../data/lovelace/config/view";
import { import {
showAlertDialog, showAlertDialog,
showPromptDialog, showPromptDialog,
} from "../../../dialogs/generic/show-dialog-box"; } from "../../../dialogs/generic/show-dialog-box";
import { haStyle } from "../../../resources/styles"; import { haStyle } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types"; import type { HomeAssistant } from "../../../types";
import { showSaveSuccessToast } from "../../../util/toast-saved-success";
import { computeCardSize } from "../common/compute-card-size"; import { computeCardSize } from "../common/compute-card-size";
import { showEditCardDialog } from "../editor/card-editor/show-edit-card-dialog"; import { showEditCardDialog } from "../editor/card-editor/show-edit-card-dialog";
import { import {
addCard, addCard,
addSection,
deleteCard, deleteCard,
moveCardToContainer, moveCardToContainer,
moveCardToIndex, moveCardToIndex,
@ -50,8 +45,6 @@ import {
} from "../editor/lovelace-path"; } from "../editor/lovelace-path";
import { showSelectViewDialog } from "../editor/select-view/show-select-view-dialog"; import { showSelectViewDialog } from "../editor/select-view/show-select-view-dialog";
import type { Lovelace, LovelaceCard } from "../types"; import type { Lovelace, LovelaceCard } from "../types";
import { SECTIONS_VIEW_LAYOUT } from "../views/const";
import type { LovelaceSectionConfig } from "../../../data/lovelace/config/section";
@customElement("hui-card-options") @customElement("hui-card-options")
export class HuiCardOptions extends LitElement { export class HuiCardOptions extends LitElement {
@ -352,9 +345,13 @@ export class HuiCardOptions extends LitElement {
allowDashboardChange: true, allowDashboardChange: true,
header: this.hass!.localize("ui.panel.lovelace.editor.move_card.header"), header: this.hass!.localize("ui.panel.lovelace.editor.move_card.header"),
viewSelectedCallback: async (urlPath, selectedDashConfig, viewIndex) => { viewSelectedCallback: async (urlPath, selectedDashConfig, viewIndex) => {
const fromView = selectedDashConfig.views[this.path![0]]; if (!this.lovelace) return;
let toView = selectedDashConfig.views[viewIndex]; const toView = selectedDashConfig.views[viewIndex];
let newConfig = selectedDashConfig; const newConfig = selectedDashConfig;
const undoAction = async () => {
this.lovelace!.saveConfig(selectedDashConfig);
};
if (isStrategyView(toView)) { if (isStrategyView(toView)) {
showAlertDialog(this, { showAlertDialog(this, {
@ -369,53 +366,22 @@ export class HuiCardOptions extends LitElement {
return; return;
} }
const isSectionsView = toView.type === SECTIONS_VIEW_LAYOUT; const toPath: LovelaceContainerPath = [viewIndex];
let toPath: LovelaceContainerPath = [viewIndex];
// If the view is a section view and has no "imported cards" section, adds a default section.
if (isSectionsView) {
const importedCardHeading = fromView.title
? this.hass!.localize(
"ui.panel.lovelace.editor.section.imported_card_section_title_view",
{ view_title: fromView.title }
)
: this.hass!.localize(
"ui.panel.lovelace.editor.section.imported_card_section_title_default"
);
let sectionIndex = toView.sections
? toView.sections.findIndex(
(s) =>
"cards" in s &&
s.cards?.some(
(c) =>
c.type === "heading" && c.heading === importedCardHeading
)
)
: -1;
if (sectionIndex === -1) {
const newSection: LovelaceSectionConfig = {
type: "grid",
cards: [
{
type: "heading",
heading: importedCardHeading,
},
],
};
newConfig = addSection(selectedDashConfig, viewIndex, newSection);
toView = newConfig.views[viewIndex] as LovelaceViewConfig;
sectionIndex = toView.sections!.length - 1;
}
toPath = [viewIndex, sectionIndex];
}
if (urlPath === this.lovelace!.urlPath) { if (urlPath === this.lovelace!.urlPath) {
this.lovelace!.saveConfig( this.lovelace!.saveConfig(
moveCardToContainer(newConfig, this.path!, toPath) moveCardToContainer(newConfig, this.path!, toPath)
); );
showSaveSuccessToast(this, this.hass!); this.lovelace.showToast({
message: this.hass!.localize(
"ui.panel.lovelace.editor.move_card.success"
),
duration: 4000,
action: {
action: undoAction,
text: this.hass!.localize("ui.common.undo"),
},
});
return; return;
} }
try { try {
@ -429,10 +395,22 @@ export class HuiCardOptions extends LitElement {
this.lovelace!.saveConfig( this.lovelace!.saveConfig(
deleteCard(this.lovelace!.config, this.path!) deleteCard(this.lovelace!.config, this.path!)
); );
showSaveSuccessToast(this, this.hass!);
this.lovelace.showToast({
message: this.hass!.localize(
"ui.panel.lovelace.editor.move_card.success"
),
duration: 4000,
action: {
action: undoAction,
text: this.hass!.localize("ui.common.undo"),
},
});
} catch (err: any) { } catch (err: any) {
showAlertDialog(this, { this.lovelace.showToast({
text: `Moving failed: ${err.message}`, message: this.hass!.localize(
"ui.panel.lovelace.editor.move_card.error"
),
}); });
} }
}, },

View File

@ -13,6 +13,7 @@ import { stopPropagation } from "../../../../common/dom/stop_propagation";
import { navigate } from "../../../../common/navigate"; import { navigate } from "../../../../common/navigate";
import { deepEqual } from "../../../../common/util/deep-equal"; import { deepEqual } from "../../../../common/util/deep-equal";
import "../../../../components/ha-alert"; import "../../../../components/ha-alert";
import "../../../../components/ha-button";
import "../../../../components/ha-circular-progress"; import "../../../../components/ha-circular-progress";
import "../../../../components/ha-dialog"; import "../../../../components/ha-dialog";
import "../../../../components/ha-dialog-header"; import "../../../../components/ha-dialog-header";
@ -57,8 +58,10 @@ export class HuiDialogEditView extends LitElement {
@query("ha-yaml-editor") private _editor?: HaYamlEditor; @query("ha-yaml-editor") private _editor?: HaYamlEditor;
@state() private _currentType = getViewType();
get _type(): string { get _type(): string {
return getViewType(this._config!); return getViewType(this._config);
} }
protected updated(changedProperties: PropertyValues) { protected updated(changedProperties: PropertyValues) {
@ -76,7 +79,6 @@ export class HuiDialogEditView extends LitElement {
if (this._params.viewIndex === undefined) { if (this._params.viewIndex === undefined) {
this._config = { this._config = {
type: SECTIONS_VIEW_LAYOUT, type: SECTIONS_VIEW_LAYOUT,
sections: [generateDefaultSection(this.hass!.localize)],
}; };
this._dirty = false; this._dirty = false;
return; return;
@ -89,6 +91,7 @@ export class HuiDialogEditView extends LitElement {
return; return;
} }
this._config = view; this._config = view;
this._currentType = this._type;
} }
public closeDialog(): void { public closeDialog(): void {
@ -159,12 +162,14 @@ export class HuiDialogEditView extends LitElement {
} }
} }
const isCompatibleViewType = const convertToSection =
this._config?.type === SECTIONS_VIEW_LAYOUT this._type === SECTIONS_VIEW_LAYOUT &&
? this._config?.type === SECTIONS_VIEW_LAYOUT && this._currentType !== SECTIONS_VIEW_LAYOUT &&
!this._config?.cards?.length this._config?.cards?.length;
: this._config?.type !== SECTIONS_VIEW_LAYOUT && const convertNotSupported =
!this._config?.sections?.length; this._type !== SECTIONS_VIEW_LAYOUT &&
this._currentType === SECTIONS_VIEW_LAYOUT &&
this._config?.sections?.length;
return html` return html`
<ha-dialog <ha-dialog
@ -224,16 +229,29 @@ export class HuiDialogEditView extends LitElement {
: ``} : ``}
</mwc-list-item> </mwc-list-item>
</ha-button-menu> </ha-button-menu>
${!isCompatibleViewType ${convertToSection
? html` ? html`
<ha-alert class="incompatible" alert-type="warning"> <ha-alert alert-type="info">
${this._config?.type === SECTIONS_VIEW_LAYOUT ${this.hass!.localize(
? this.hass!.localize( "ui.panel.lovelace.editor.edit_view.card_to_section_convert"
"ui.panel.lovelace.editor.edit_view.type_warning_sections" )}
) <ha-button
: this.hass!.localize( slot="action"
"ui.panel.lovelace.editor.edit_view.type_warning_others" .label=${this.hass!.localize(
)} "ui.panel.lovelace.editor.edit_view.convert_view"
)}
@click=${this._convertToSection}
>
</ha-button>
</ha-alert>
`
: nothing}
${convertNotSupported
? html`
<ha-alert alert-type="warning">
${this.hass!.localize(
"ui.panel.lovelace.editor.edit_view.section_to_card_not_supported"
)}
</ha-alert> </ha-alert>
` `
: nothing} : nothing}
@ -258,7 +276,7 @@ export class HuiDialogEditView extends LitElement {
${content} ${content}
${this._params.viewIndex !== undefined ${this._params.viewIndex !== undefined
? html` ? html`
<mwc-button <ha-button
class="warning" class="warning"
slot="secondaryAction" slot="secondaryAction"
@click=${this._deleteConfirm} @click=${this._deleteConfirm}
@ -266,15 +284,16 @@ export class HuiDialogEditView extends LitElement {
${this.hass!.localize( ${this.hass!.localize(
"ui.panel.lovelace.editor.edit_view.delete" "ui.panel.lovelace.editor.edit_view.delete"
)} )}
</mwc-button> </ha-button>
` `
: nothing} : nothing}
<mwc-button <ha-button
slot="primaryAction" slot="primaryAction"
?disabled=${!this._config || ?disabled=${!this._config ||
this._saving || this._saving ||
!this._dirty || !this._dirty ||
!isCompatibleViewType} convertToSection ||
convertNotSupported}
@click=${this._save} @click=${this._save}
> >
${this._saving ${this._saving
@ -284,7 +303,7 @@ export class HuiDialogEditView extends LitElement {
aria-label="Saving" aria-label="Saving"
></ha-circular-progress>` ></ha-circular-progress>`
: nothing} : nothing}
${this.hass!.localize("ui.common.save")}</mwc-button ${this.hass!.localize("ui.common.save")}</ha-button
> >
</ha-dialog> </ha-dialog>
`; `;
@ -303,6 +322,54 @@ export class HuiDialogEditView extends LitElement {
} }
} }
private async _convertToSection() {
if (!this._params || !this._config) {
return;
}
const confirm = await showConfirmationDialog(this, {
title: this.hass!.localize(
"ui.panel.lovelace.editor.edit_view.convert_view_title"
),
text: this.hass!.localize(
"ui.panel.lovelace.editor.edit_view.convert_view_text"
),
confirmText: this.hass!.localize(
"ui.panel.lovelace.editor.edit_view.convert_view_action"
),
dismissText: this.hass!.localize("ui.common.cancel"),
});
if (!confirm) {
return;
}
const newConfig = {
...this._config,
};
newConfig.type = SECTIONS_VIEW_LAYOUT;
newConfig.sections = [generateDefaultSection(this.hass!.localize)];
newConfig.path = undefined;
const lovelace = this._params!.lovelace!;
try {
await lovelace.saveConfig(
addView(this.hass!, lovelace.config, newConfig)
);
if (this._params.saveCallback) {
this._params.saveCallback(lovelace.config.views.length, newConfig);
}
this.closeDialog();
} catch (err: any) {
showAlertDialog(this, {
text: `${this.hass!.localize(
"ui.panel.lovelace.editor.edit_view.saving_failed"
)}: ${err.message}`,
});
} finally {
this._saving = false;
}
}
private async _delete(): Promise<void> { private async _delete(): Promise<void> {
if (!this._params) { if (!this._params) {
return; return;
@ -366,7 +433,7 @@ export class HuiDialogEditView extends LitElement {
const viewConf = { const viewConf = {
...this._config, ...this._config,
}; };
// Ensure we have at least one section if we are in sections view
if (viewConf.type === SECTIONS_VIEW_LAYOUT && !viewConf.sections?.length) { if (viewConf.type === SECTIONS_VIEW_LAYOUT && !viewConf.sections?.length) {
viewConf.sections = [generateDefaultSection(this.hass!.localize)]; viewConf.sections = [generateDefaultSection(this.hass!.localize)];
} else if (!viewConf.cards?.length) { } else if (!viewConf.cards?.length) {
@ -386,7 +453,7 @@ export class HuiDialogEditView extends LitElement {
viewConf viewConf
) )
); );
if (this._params.saveCallback) { if (this._params.saveCallback && this._creatingView) {
this._params.saveCallback( this._params.saveCallback(
this._params.viewIndex || lovelace.config.views.length, this._params.viewIndex || lovelace.config.views.length,
viewConf viewConf
@ -479,7 +546,7 @@ export class HuiDialogEditView extends LitElement {
text-transform: uppercase; text-transform: uppercase;
padding: 0 20px; padding: 0 20px;
} }
mwc-button.warning { ha-button.warning {
margin-right: auto; margin-right: auto;
margin-inline-end: auto; margin-inline-end: auto;
margin-inline-start: initial; margin-inline-start: initial;
@ -494,7 +561,10 @@ export class HuiDialogEditView extends LitElement {
color: var(--error-color); color: var(--error-color);
border-bottom: 1px solid var(--error-color); border-bottom: 1px solid var(--error-color);
} }
.incompatible { ha-alert {
display: block;
}
ha-alert ha-button {
display: block; display: block;
} }

View File

@ -40,6 +40,26 @@ export class HuiViewEditor extends LitElement {
private _schema = memoizeOne( private _schema = memoizeOne(
(localize: LocalizeFunc, viewType: string) => (localize: LocalizeFunc, viewType: string) =>
[ [
{
name: "type",
selector: {
select: {
options: (
[
SECTIONS_VIEW_LAYOUT,
MASONRY_VIEW_LAYOUT,
SIDEBAR_VIEW_LAYOUT,
PANEL_VIEW_LAYOUT,
] as const
).map((type) => ({
value: type,
label: localize(
`ui.panel.lovelace.editor.edit_view.types.${type}`
),
})),
},
},
},
{ name: "title", selector: { text: {} } }, { name: "title", selector: { text: {} } },
{ {
name: "icon", name: "icon",
@ -55,26 +75,6 @@ export class HuiViewEditor extends LitElement {
boolean: {}, boolean: {},
}, },
}, },
{
name: "type",
selector: {
select: {
options: (
[
SECTIONS_VIEW_LAYOUT,
SIDEBAR_VIEW_LAYOUT,
PANEL_VIEW_LAYOUT,
MASONRY_VIEW_LAYOUT,
] as const
).map((type) => ({
value: type,
label: localize(
`ui.panel.lovelace.editor.edit_view.types.${type}`
),
})),
},
},
},
...(viewType === SECTIONS_VIEW_LAYOUT ...(viewType === SECTIONS_VIEW_LAYOUT
? ([ ? ([
{ {

View File

@ -833,6 +833,10 @@ class HUIRoot extends LitElement {
showEditViewDialog(this, { showEditViewDialog(this, {
lovelace: this.lovelace!, lovelace: this.lovelace!,
viewIndex: this._curView as number, viewIndex: this._curView as number,
saveCallback: (viewIndex: number, viewConfig: LovelaceViewConfig) => {
const path = viewConfig.path || viewIndex;
this._navigateToView(path);
},
}); });
} }

View File

@ -25,8 +25,18 @@ const CARD_SORTABLE_OPTIONS: HaSortableOptions = {
delayOnTouchOnly: true, delayOnTouchOnly: true,
direction: "vertical", direction: "vertical",
invertedSwapThreshold: 0.7, invertedSwapThreshold: 0.7,
group: "card",
} as HaSortableOptions; } as HaSortableOptions;
const IMPORT_MODE_CARD_SORTABLE_OPTIONS: HaSortableOptions = {
...CARD_SORTABLE_OPTIONS,
sort: false,
group: {
name: "card",
put: false,
},
};
export class GridSection extends LitElement implements LovelaceSectionElement { export class GridSection extends LitElement implements LovelaceSectionElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@ -40,6 +50,8 @@ export class GridSection extends LitElement implements LovelaceSectionElement {
@property({ attribute: false }) public cards: HuiCard[] = []; @property({ attribute: false }) public cards: HuiCard[] = [];
@property({ attribute: false }) public importOnly = false;
@state() _config?: LovelaceSectionConfig; @state() _config?: LovelaceSectionConfig;
@state() _dragging = false; @state() _dragging = false;
@ -67,21 +79,29 @@ export class GridSection extends LitElement implements LovelaceSectionElement {
const editMode = Boolean(this.lovelace?.editMode && !this.isStrategy); const editMode = Boolean(this.lovelace?.editMode && !this.isStrategy);
const sortableOptions = this.importOnly
? IMPORT_MODE_CARD_SORTABLE_OPTIONS
: CARD_SORTABLE_OPTIONS;
return html` return html`
<ha-sortable <ha-sortable
.disabled=${!editMode} .disabled=${!editMode}
@drag-start=${this._dragStart} @drag-start=${this._dragStart}
@drag-end=${this._dragEnd} @drag-end=${this._dragEnd}
group="card"
draggable-selector=".card" draggable-selector=".card"
.rollback=${false} .rollback=${false}
.options=${CARD_SORTABLE_OPTIONS} .options=${sortableOptions}
@item-moved=${this._cardMoved} @item-moved=${this._cardMoved}
@item-added=${this._cardAdded} @item-added=${this._cardAdded}
@item-removed=${this._cardRemoved} @item-removed=${this._cardRemoved}
invert-swap invert-swap
> >
<div class="container ${classMap({ "edit-mode": editMode })}"> <div
class="container ${classMap({
"edit-mode": editMode,
"import-only": this.importOnly,
})}"
>
${repeat( ${repeat(
cardsConfig, cardsConfig,
(cardConfig) => this._getKey(cardConfig), (cardConfig) => this._getKey(cardConfig),
@ -117,6 +137,8 @@ export class GridSection extends LitElement implements LovelaceSectionElement {
.lovelace=${this.lovelace!} .lovelace=${this.lovelace!}
.path=${cardPath} .path=${cardPath}
.hiddenOverlay=${this._dragging} .hiddenOverlay=${this._dragging}
.noEdit=${this.importOnly}
.noDuplicate=${this.importOnly}
> >
${card} ${card}
</hui-card-edit-mode> </hui-card-edit-mode>
@ -126,7 +148,7 @@ export class GridSection extends LitElement implements LovelaceSectionElement {
`; `;
} }
)} )}
${editMode ${editMode && !this.importOnly
? html` ? html`
<button <button
class="add" class="add"
@ -216,11 +238,14 @@ export class GridSection extends LitElement implements LovelaceSectionElement {
.container.edit-mode { .container.edit-mode {
padding: 8px; padding: 8px;
border-radius: var(--ha-card-border-radius, 12px); border-radius: var(--ha-card-border-radius, 12px);
border-start-end-radius: 0px;
border: 2px dashed var(--divider-color); border: 2px dashed var(--divider-color);
min-height: var(--row-height); min-height: var(--row-height);
} }
.container.edit-mode:not(.import-only) {
border-start-end-radius: 0px;
}
.card { .card {
border-radius: var(--ha-card-border-radius, 12px); border-radius: var(--ha-card-border-radius, 12px);
position: relative; position: relative;

View File

@ -43,6 +43,9 @@ export class HuiSection extends ReactiveElement {
@property({ type: Boolean, reflect: true }) public preview = false; @property({ type: Boolean, reflect: true }) public preview = false;
@property({ type: Boolean, attribute: "import-only" })
public importOnly = false;
@property({ type: Number }) public index!: number; @property({ type: Number }) public index!: number;
@property({ type: Number }) public viewIndex!: number; @property({ type: Number }) public viewIndex!: number;
@ -128,6 +131,9 @@ export class HuiSection extends ReactiveElement {
element.preview = this.preview; element.preview = this.preview;
}); });
} }
if (changedProperties.has("importOnly")) {
this._layoutElement.importOnly = this.importOnly;
}
if (changedProperties.has("_cards")) { if (changedProperties.has("_cards")) {
this._layoutElement.cards = this._cards; this._layoutElement.cards = this._cards;
} }

View File

@ -6,17 +6,20 @@ import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map"; import { classMap } from "lit/directives/class-map";
import { repeat } from "lit/directives/repeat"; import { repeat } from "lit/directives/repeat";
import { styleMap } from "lit/directives/style-map"; import { styleMap } from "lit/directives/style-map";
import memoizeOne from "memoize-one";
import { clamp } from "../../../common/number/clamp"; import { clamp } from "../../../common/number/clamp";
import "../../../components/ha-icon-button"; import "../../../components/ha-icon-button";
import "../../../components/ha-ripple";
import "../../../components/ha-sortable"; import "../../../components/ha-sortable";
import "../../../components/ha-svg-icon"; import "../../../components/ha-svg-icon";
import "../../../components/ha-ripple";
import type { LovelaceViewElement } from "../../../data/lovelace"; import type { LovelaceViewElement } from "../../../data/lovelace";
import type { LovelaceCardConfig } from "../../../data/lovelace/config/card";
import type { LovelaceViewConfig } from "../../../data/lovelace/config/view"; import type { LovelaceViewConfig } from "../../../data/lovelace/config/view";
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box"; import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
import type { HomeAssistant } from "../../../types"; import type { HomeAssistant } from "../../../types";
import type { HuiBadge } from "../badges/hui-badge"; import type { HuiBadge } from "../badges/hui-badge";
import "../badges/hui-view-badges"; import "../badges/hui-view-badges";
import type { HuiCard } from "../cards/hui-card";
import "../components/hui-badge-edit-mode"; import "../components/hui-badge-edit-mode";
import { addSection, deleteSection, moveSection } from "../editor/config-util"; import { addSection, deleteSection, moveSection } from "../editor/config-util";
import { findLovelaceContainer } from "../editor/lovelace-path"; import { findLovelaceContainer } from "../editor/lovelace-path";
@ -42,6 +45,8 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
@property({ attribute: false }) public badges: HuiBadge[] = []; @property({ attribute: false }) public badges: HuiBadge[] = [];
@property({ attribute: false }) public cards: HuiCard[] = [];
@state() private _config?: LovelaceViewConfig; @state() private _config?: LovelaceViewConfig;
@state() private _sectionColumnCount = 0; @state() private _sectionColumnCount = 0;
@ -235,11 +240,46 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
</button> </button>
` `
: nothing} : nothing}
${editMode && this._config?.cards?.length
? html`
<div class="section imported-cards">
<div class="imported-card-header">
<p class="title">
${this.hass.localize(
"ui.panel.lovelace.editor.section.imported_cards_title"
)}
</p>
<p class="subtitle">
${this.hass.localize(
"ui.panel.lovelace.editor.section.imported_cards_description"
)}
</p>
</div>
<hui-section
.lovelace=${this.lovelace}
.hass=${this.hass}
.config=${this._importedCardSectionConfig(
this._config.cards
)}
.viewIndex=${this.index}
preview
import-only
></hui-section>
</div>
`
: nothing}
</div> </div>
</ha-sortable> </ha-sortable>
`; `;
} }
private _importedCardSectionConfig = memoizeOne(
(cards: LovelaceCardConfig[]) => ({
type: "grid",
cards,
})
);
private _createSection(): void { private _createSection(): void {
const newConfig = addSection(this.lovelace!.config, this.index!, { const newConfig = addSection(this.lovelace!.config, this.index!, {
type: "grid", type: "grid",
@ -432,6 +472,33 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
--mdc-icon-size: 20px; --mdc-icon-size: 20px;
color: var(--primary-text-color); color: var(--primary-text-color);
} }
.imported-cards {
--column-span: var(--column-count);
--row-span: 1;
order: 2;
}
.imported-card-header {
margin-top: 24px;
padding: 16px 8px;
border-top: 2px dashed var(--divider-color);
}
.imported-card-header .title {
margin: 0;
color: var(--primary-text-color);
font-size: 16px;
font-weight: 400;
line-height: 24px;
}
.imported-card-header .subtitle {
margin: 0;
color: var(--secondary-text-color);
font-size: 14px;
font-weight: 400;
line-height: 20px;
}
`; `;
} }
} }

View File

@ -5611,14 +5611,18 @@
"visibility": { "visibility": {
"select_users": "Select which users should see this view in the navigation" "select_users": "Select which users should see this view in the navigation"
}, },
"type": "View type", "type": "Layout",
"type_warning_sections": "You can not change your view to use the 'sections' view type because migration is not supported yet. Start from scratch with a new view if you want to experiment with the 'sections' view.", "convert_view": "Convert",
"type_warning_others": "You can not change your view to an other type because migration is not supported yet. Start from scratch with a new view if you want to use another view type.", "convert_view_title": "Convert view layout",
"convert_view_text": "It will create a new view using sections. This current view will stay untouched. All your cards will be imported so you can rearrange them freely.",
"convert_view_action": "Create",
"card_to_section_convert": "Convert your view to a section view.",
"section_to_card_not_supported": "You can not change your section view to an other type. Start from scratch with a new view if you want to use another view type.",
"types": { "types": {
"sections": "Sections (default)",
"masonry": "Masonry", "masonry": "Masonry",
"sidebar": "Sidebar", "sidebar": "Sidebar",
"panel": "Panel (single card)", "panel": "Panel (single card)"
"sections": "Sections"
}, },
"subview": "Subview", "subview": "Subview",
"max_columns": "Max number of sections wide", "max_columns": "Max number of sections wide",
@ -5704,7 +5708,9 @@
"move_card": { "move_card": {
"header": "Choose a view to move the card to", "header": "Choose a view to move the card to",
"strategy_error_title": "Impossible to move the card", "strategy_error_title": "Impossible to move the card",
"strategy_error_text_strategy": "Moving a card to an auto generated view is not supported." "strategy_error_text_strategy": "Moving a card to an auto generated view is not supported.",
"success": "Card moved successfully",
"error": "Error while moving card"
}, },
"change_position": { "change_position": {
"title": "Change card position", "title": "Change card position",
@ -5723,8 +5729,8 @@
"add_card": "[%key:ui::panel::lovelace::editor::edit_card::add%]", "add_card": "[%key:ui::panel::lovelace::editor::edit_card::add%]",
"create_section": "Create section", "create_section": "Create section",
"default_section_title": "New section", "default_section_title": "New section",
"imported_card_section_title_view": "Imported cards from ''{view_title}'' view", "imported_cards_title": "Imported cards",
"imported_card_section_title_default": "Imported cards from another view" "imported_cards_description": "These cards are imported from another view. They will only be displayed in edit mode. Move them into sections to display them in your view."
}, },
"delete_section": { "delete_section": {
"title": "Delete section", "title": "Delete section",