Show card preview inside section in card editor (#21065)

* Show card inside section in card editor

* Replace edit mode by preview

* Add backward compatibility for custom cards

* Re-order props
This commit is contained in:
Paul Bottein 2024-06-21 11:12:18 +02:00 committed by GitHub
parent eb0579ddc5
commit f78946447f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 101 additions and 45 deletions

View File

@ -25,7 +25,7 @@ declare global {
export class HuiCard extends ReactiveElement { export class HuiCard extends ReactiveElement {
@property({ attribute: false }) public hass?: HomeAssistant; @property({ attribute: false }) public hass?: HomeAssistant;
@property({ type: Boolean }) public editMode = false; @property({ type: Boolean }) public preview = false;
@property({ type: Boolean }) public isPanel = false; @property({ type: Boolean }) public isPanel = false;
@ -89,7 +89,9 @@ export class HuiCard extends ReactiveElement {
private _createElement(config: LovelaceCardConfig) { private _createElement(config: LovelaceCardConfig) {
const element = createCardElement(config); const element = createCardElement(config);
element.hass = this.hass; element.hass = this.hass;
element.editMode = this.editMode; element.preview = this.preview;
// For backwards compatibility
(element as any).editMode = this.preview;
// Update element when the visibility of the card changes (e.g. conditional card or filter card) // Update element when the visibility of the card changes (e.g. conditional card or filter card)
element.addEventListener("card-visibility-changed", (ev: Event) => { element.addEventListener("card-visibility-changed", (ev: Event) => {
ev.stopPropagation(); ev.stopPropagation();
@ -135,9 +137,11 @@ export class HuiCard extends ReactiveElement {
this._buildElement(createErrorCardConfig(e.message, null)); this._buildElement(createErrorCardConfig(e.message, null));
} }
} }
if (changedProps.has("editMode")) { if (changedProps.has("preview")) {
try { try {
this._element.editMode = this.editMode; this._element.preview = this.preview;
// For backwards compatibility
(this._element as any).editMode = this.preview;
} catch (e: any) { } catch (e: any) {
this._buildElement(createErrorCardConfig(e.message, null)); this._buildElement(createErrorCardConfig(e.message, null));
} }
@ -192,7 +196,7 @@ export class HuiCard extends ReactiveElement {
const visible = const visible =
forceVisible || forceVisible ||
this.editMode || this.preview ||
!this.config?.visibility || !this.config?.visibility ||
checkConditionsMet(this.config.visibility, this.hass); checkConditionsMet(this.config.visibility, this.hass);
this._setElementVisibility(visible); this._setElementVisibility(visible);

View File

@ -39,13 +39,13 @@ class HuiConditionalCard extends HuiConditionalBase implements LovelaceCard {
private _createCardElement(cardConfig: LovelaceCardConfig) { private _createCardElement(cardConfig: LovelaceCardConfig) {
const element = document.createElement("hui-card"); const element = document.createElement("hui-card");
element.hass = this.hass; element.hass = this.hass;
element.editMode = this.editMode; element.preview = this.preview;
element.config = cardConfig; element.config = cardConfig;
return element; return element;
} }
protected setVisibility(conditionMet: boolean): void { protected setVisibility(conditionMet: boolean): void {
const visible = this.editMode || conditionMet; const visible = this.preview || conditionMet;
const previouslyHidden = this.hidden; const previouslyHidden = this.hidden;
super.setVisibility(conditionMet); super.setVisibility(conditionMet);
if (previouslyHidden !== this.hidden) { if (previouslyHidden !== this.hidden) {

View File

@ -55,7 +55,7 @@ export class HuiEntityFilterCard
@property({ type: Boolean }) public isPanel = false; @property({ type: Boolean }) public isPanel = false;
@property({ type: Boolean }) public editMode = false; @property({ type: Boolean }) public preview = false;
@state() private _config?: EntityFilterCardConfig; @state() private _config?: EntityFilterCardConfig;
@ -117,7 +117,7 @@ export class HuiEntityFilterCard
protected shouldUpdate(changedProps: PropertyValues): boolean { protected shouldUpdate(changedProps: PropertyValues): boolean {
if (this._element) { if (this._element) {
this._element.hass = this.hass; this._element.hass = this.hass;
this._element.editMode = this.editMode; this._element.preview = this.preview;
this._element.isPanel = this.isPanel; this._element.isPanel = this.isPanel;
} }
@ -247,7 +247,7 @@ export class HuiEntityFilterCard
private _createCardElement(cardConfig: LovelaceCardConfig) { private _createCardElement(cardConfig: LovelaceCardConfig) {
const element = document.createElement("hui-card"); const element = document.createElement("hui-card");
element.hass = this.hass; element.hass = this.hass;
element.editMode = this.editMode; element.preview = this.preview;
element.config = cardConfig; element.config = cardConfig;
return element; return element;
} }

View File

@ -10,7 +10,7 @@ import { ErrorCardConfig } from "./types";
export class HuiErrorCard extends LitElement implements LovelaceCard { export class HuiErrorCard extends LitElement implements LovelaceCard {
public hass?: HomeAssistant; public hass?: HomeAssistant;
@property({ attribute: false }) public editMode = false; @property({ attribute: false }) public preview = false;
@state() private _config?: ErrorCardConfig; @state() private _config?: ErrorCardConfig;

View File

@ -38,7 +38,7 @@ export class HuiMarkdownCard extends LitElement implements LovelaceCard {
@property({ attribute: false }) public hass?: HomeAssistant; @property({ attribute: false }) public hass?: HomeAssistant;
@property({ type: Boolean }) public editMode = false; @property({ type: Boolean }) public preview = false;
@state() private _config?: MarkdownCardConfig; @state() private _config?: MarkdownCardConfig;
@ -163,12 +163,12 @@ export class HuiMarkdownCard extends LitElement implements LovelaceCard {
user: this.hass.user!.name, user: this.hass.user!.name,
}, },
strict: true, strict: true,
report_errors: this.editMode, report_errors: this.preview,
} }
); );
await this._unsubRenderTemplate; await this._unsubRenderTemplate;
} catch (e: any) { } catch (e: any) {
if (this.editMode) { if (this.preview) {
this._error = e.message; this._error = e.message;
this._errorLevel = undefined; this._errorLevel = undefined;
} }

View File

@ -23,7 +23,7 @@ export abstract class HuiStackCard<T extends StackCardConfig = StackCardConfig>
@property({ attribute: false }) public hass?: HomeAssistant; @property({ attribute: false }) public hass?: HomeAssistant;
@property({ type: Boolean }) public editMode = false; @property({ type: Boolean }) public preview = false;
@state() protected _cards?: HuiCard[]; @state() protected _cards?: HuiCard[];
@ -58,7 +58,7 @@ export abstract class HuiStackCard<T extends StackCardConfig = StackCardConfig>
} }
if (changedProperties.has("editMode")) { if (changedProperties.has("editMode")) {
this._cards.forEach((card) => { this._cards.forEach((card) => {
card.editMode = this.editMode; card.preview = this.preview;
}); });
} }
} }
@ -67,7 +67,7 @@ export abstract class HuiStackCard<T extends StackCardConfig = StackCardConfig>
private _createCardElement(cardConfig: LovelaceCardConfig) { private _createCardElement(cardConfig: LovelaceCardConfig) {
const element = document.createElement("hui-card"); const element = document.createElement("hui-card");
element.hass = this.hass; element.hass = this.hass;
element.editMode = this.editMode; element.preview = this.preview;
element.config = cardConfig; element.config = cardConfig;
return element; return element;
} }

View File

@ -24,7 +24,7 @@ declare global {
export class HuiConditionalBase extends ReactiveElement { export class HuiConditionalBase extends ReactiveElement {
@property({ attribute: false }) public hass?: HomeAssistant; @property({ attribute: false }) public hass?: HomeAssistant;
@property({ type: Boolean }) public editMode = false; @property({ type: Boolean }) public preview = false;
@state() protected _config?: ConditionalCardConfig | ConditionalRowConfig; @state() protected _config?: ConditionalCardConfig | ConditionalRowConfig;
@ -116,7 +116,7 @@ export class HuiConditionalBase extends ReactiveElement {
changed.has("_element") || changed.has("_element") ||
changed.has("_config") || changed.has("_config") ||
changed.has("hass") || changed.has("hass") ||
changed.has("editMode") changed.has("preview")
) { ) {
this._listenMediaQueries(); this._listenMediaQueries();
this._updateVisibility(); this._updateVisibility();
@ -128,7 +128,7 @@ export class HuiConditionalBase extends ReactiveElement {
return; return;
} }
this._element.editMode = this.editMode; this._element.preview = this.preview;
const conditionMet = checkConditionsMet( const conditionMet = checkConditionsMet(
this._config!.conditions, this._config!.conditions,
@ -142,7 +142,7 @@ export class HuiConditionalBase extends ReactiveElement {
if (!this._element || !this.hass) { if (!this._element || !this.hass) {
return; return;
} }
const visible = this.editMode || conditionMet; const visible = this.preview || conditionMet;
if (this.hidden !== !visible) { if (this.hidden !== !visible) {
this.toggleAttribute("hidden", !visible); this.toggleAttribute("hidden", !visible);
this.style.setProperty("display", visible ? "" : "none"); this.style.setProperty("display", visible ? "" : "none");

View File

@ -48,7 +48,7 @@ export class HuiDialogDeleteCard extends LitElement {
<hui-card <hui-card
.hass=${this.hass} .hass=${this.hass}
.config=${this._cardConfig} .config=${this._cardConfig}
editMode preview
></hui-card> ></hui-card>
</div> </div>
` `

View File

@ -9,6 +9,7 @@ import {
nothing, nothing,
} from "lit"; } from "lit";
import { customElement, property, query, state } from "lit/decorators"; import { customElement, property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import type { HASSDomEvent } from "../../../../common/dom/fire_event"; import type { HASSDomEvent } from "../../../../common/dom/fire_event";
import { fireEvent } from "../../../../common/dom/fire_event"; import { fireEvent } from "../../../../common/dom/fire_event";
import { computeRTLDirection } from "../../../../common/util/compute_rtl"; import { computeRTLDirection } from "../../../../common/util/compute_rtl";
@ -29,6 +30,7 @@ import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
import { haStyleDialog } from "../../../../resources/styles"; import { haStyleDialog } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types"; import type { HomeAssistant } from "../../../../types";
import { showSaveSuccessToast } from "../../../../util/toast-saved-success"; import { showSaveSuccessToast } from "../../../../util/toast-saved-success";
import "../../sections/hui-section";
import { addCard, replaceCard } from "../config-util"; import { addCard, replaceCard } from "../config-util";
import { getCardDocumentationURL } from "../get-card-documentation-url"; import { getCardDocumentationURL } from "../get-card-documentation-url";
import type { ConfigChangedEvent } from "../hui-element-editor"; import type { ConfigChangedEvent } from "../hui-element-editor";
@ -245,12 +247,23 @@ export class HuiDialogEditCard
></hui-card-element-editor> ></hui-card-element-editor>
</div> </div>
<div class="element-preview"> <div class="element-preview">
<hui-card ${this._isInSection
.hass=${this.hass} ? html`
.config=${this._cardConfig} <hui-section
editMode .hass=${this.hass}
class=${this._error ? "blur" : ""} .config=${this._cardConfigInSection(this._cardConfig)}
></hui-card> preview
class=${this._error ? "blur" : ""}
></hui-section>
`
: html`
<hui-card
.hass=${this.hass}
.config=${this._cardConfig}
preview
class=${this._error ? "blur" : ""}
></hui-card>
`}
${this._error ${this._error
? html` ? html`
<ha-circular-progress <ha-circular-progress
@ -335,6 +348,22 @@ export class HuiDialogEditCard
this._cardEditorEl?.focusYamlEditor(); this._cardEditorEl?.focusYamlEditor();
} }
private get _isInSection() {
return this._params!.path.length === 2;
}
private _cardConfigInSection = memoizeOne(
(cardConfig?: LovelaceCardConfig) => {
const { cards, title, ...containerConfig } = this
._containerConfig as LovelaceSectionConfig;
return {
...containerConfig,
cards: cardConfig ? [cardConfig] : [],
};
}
);
private get _canSave(): boolean { private get _canSave(): boolean {
if (this._saving) { if (this._saving) {
return false; return false;
@ -454,9 +483,17 @@ export class HuiDialogEditCard
} }
.content hui-card { .content hui-card {
margin: 4px auto; display: block;
padding: 4px;
margin: 0 auto;
max-width: 390px; max-width: 390px;
} }
.content hui-section {
display: block;
padding: 4px;
margin: 0 auto;
max-width: var(--ha-view-sections-column-max-width, 500px);
}
.content .element-editor { .content .element-editor {
margin: 0 10px; margin: 0 10px;
} }
@ -476,6 +513,11 @@ export class HuiDialogEditCard
margin: auto 0px; margin: auto 0px;
max-width: 500px; max-width: 500px;
} }
.content hui-section {
padding: 8px 10px;
margin: auto 0px;
max-width: var(--ha-view-sections-column-max-width, 500px);
}
} }
.hidden { .hidden {
display: none; display: none;

View File

@ -75,6 +75,7 @@ export class HuiDialogSuggestCard extends LitElement {
<hui-section <hui-section
.hass=${this.hass} .hass=${this.hass}
.config=${this._sectionConfig} .config=${this._sectionConfig}
preview
></hui-section> ></hui-section>
</div> </div>
`; `;
@ -84,7 +85,11 @@ export class HuiDialogSuggestCard extends LitElement {
<div class="element-preview"> <div class="element-preview">
${this._cardConfig.map( ${this._cardConfig.map(
(cardConfig) => html` (cardConfig) => html`
<hui-card .hass=${this.hass} .config=${cardConfig}></hui-card> <hui-card
.hass=${this.hass}
.config=${cardConfig}
preview
></hui-card>
` `
)} )}
</div> </div>

View File

@ -81,7 +81,7 @@ export type LovelaceRowConfig =
export interface LovelaceRow extends HTMLElement { export interface LovelaceRow extends HTMLElement {
hass?: HomeAssistant; hass?: HomeAssistant;
editMode?: boolean; preview?: boolean;
setConfig(config: LovelaceRowConfig); setConfig(config: LovelaceRowConfig);
} }

View File

@ -41,6 +41,8 @@ export class HuiSection extends ReactiveElement {
@property({ attribute: false }) public lovelace?: Lovelace; @property({ attribute: false }) public lovelace?: Lovelace;
@property({ type: Boolean, reflect: true }) public preview = 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;
@ -56,7 +58,7 @@ export class HuiSection extends ReactiveElement {
private _createCardElement(cardConfig: LovelaceCardConfig) { private _createCardElement(cardConfig: LovelaceCardConfig) {
const element = document.createElement("hui-card"); const element = document.createElement("hui-card");
element.hass = this.hass; element.hass = this.hass;
element.editMode = this.lovelace?.editMode || false; element.preview = this.preview;
element.config = cardConfig; element.config = cardConfig;
element.addEventListener("card-updated", (ev: Event) => { element.addEventListener("card-updated", (ev: Event) => {
ev.stopPropagation(); ev.stopPropagation();
@ -118,8 +120,10 @@ export class HuiSection extends ReactiveElement {
} }
if (changedProperties.has("lovelace")) { if (changedProperties.has("lovelace")) {
this._layoutElement.lovelace = this.lovelace; this._layoutElement.lovelace = this.lovelace;
}
if (changedProperties.has("preview")) {
this._cards.forEach((element) => { this._cards.forEach((element) => {
element.editMode = this.lovelace?.editMode || false; element.preview = this.preview;
}); });
} }
if (changedProperties.has("_cards")) { if (changedProperties.has("_cards")) {
@ -205,7 +209,7 @@ export class HuiSection extends ReactiveElement {
} }
const visible = const visible =
forceVisible || forceVisible ||
this.lovelace?.editMode || this.preview ||
!this.config.visibility || !this.config.visibility ||
checkConditionsMet(this.config.visibility, this.hass); checkConditionsMet(this.config.visibility, this.hass);

View File

@ -48,7 +48,7 @@ export type LovelaceLayoutOptions = {
export interface LovelaceCard extends HTMLElement { export interface LovelaceCard extends HTMLElement {
hass?: HomeAssistant; hass?: HomeAssistant;
isPanel?: boolean; isPanel?: boolean;
editMode?: boolean; preview?: boolean;
getCardSize(): number | Promise<number>; getCardSize(): number | Promise<number>;
getLayoutOptions?(): LovelaceLayoutOptions; getLayoutOptions?(): LovelaceLayoutOptions;
setConfig(config: LovelaceCardConfig): void; setConfig(config: LovelaceCardConfig): void;

View File

@ -248,17 +248,17 @@ export class MasonryView extends LitElement implements LovelaceViewElement {
}); });
} }
private _addCardToColumn(columnEl, index, editMode) { private _addCardToColumn(columnEl, index, preview) {
const card: HuiCard = this.cards[index]; const card: HuiCard = this.cards[index];
if (!editMode || this.isStrategy) { if (!preview || this.isStrategy) {
card.editMode = false; card.preview = false;
columnEl.appendChild(card); columnEl.appendChild(card);
} else { } else {
const wrapper = document.createElement("hui-card-options"); const wrapper = document.createElement("hui-card-options");
wrapper.hass = this.hass; wrapper.hass = this.hass;
wrapper.lovelace = this.lovelace; wrapper.lovelace = this.lovelace;
wrapper.path = [this.index!, index]; wrapper.path = [this.index!, index];
card.editMode = true; card.preview = true;
wrapper.appendChild(card); wrapper.appendChild(card);
columnEl.appendChild(wrapper); columnEl.appendChild(wrapper);
} }

View File

@ -108,7 +108,7 @@ export class PanelView extends LitElement implements LovelaceViewElement {
card.isPanel = true; card.isPanel = true;
if (this.isStrategy || !this.lovelace?.editMode) { if (this.isStrategy || !this.lovelace?.editMode) {
card.editMode = false; card.preview = false;
this._card = card; this._card = card;
return; return;
} }
@ -118,7 +118,7 @@ export class PanelView extends LitElement implements LovelaceViewElement {
wrapper.lovelace = this.lovelace; wrapper.lovelace = this.lovelace;
wrapper.path = [this.index!, 0]; wrapper.path = [this.index!, 0];
wrapper.hidePosition = true; wrapper.hidePosition = true;
card.editMode = true; card.preview = true;
wrapper.appendChild(card); wrapper.appendChild(card);
this._card = wrapper; this._card = wrapper;
} }

View File

@ -144,14 +144,14 @@ export class SideBarView extends LitElement implements LovelaceViewElement {
const cardConfig = this._config?.cards?.[idx]; const cardConfig = this._config?.cards?.[idx];
let element: HuiCard | HuiCardOptions; let element: HuiCard | HuiCardOptions;
if (this.isStrategy || !this.lovelace?.editMode) { if (this.isStrategy || !this.lovelace?.editMode) {
card.editMode = false; card.preview = false;
element = card; element = card;
} else { } else {
element = document.createElement("hui-card-options"); element = document.createElement("hui-card-options");
element.hass = this.hass; element.hass = this.hass;
element.lovelace = this.lovelace; element.lovelace = this.lovelace;
element.path = [this.index!, idx]; element.path = [this.index!, idx];
card.editMode = true; card.preview = true;
const movePositionButton = document.createElement("ha-icon-button"); const movePositionButton = document.createElement("ha-icon-button");
movePositionButton.slot = "buttons"; movePositionButton.slot = "buttons";
const moveIcon = document.createElement("ha-svg-icon"); const moveIcon = document.createElement("ha-svg-icon");

View File

@ -76,7 +76,7 @@ export class HUIView extends ReactiveElement {
private _createCardElement(cardConfig: LovelaceCardConfig) { private _createCardElement(cardConfig: LovelaceCardConfig) {
const element = document.createElement("hui-card"); const element = document.createElement("hui-card");
element.hass = this.hass; element.hass = this.hass;
element.editMode = this.lovelace.editMode; element.preview = this.lovelace.editMode;
element.config = cardConfig; element.config = cardConfig;
element.addEventListener("card-updated", (ev: Event) => { element.addEventListener("card-updated", (ev: Event) => {
ev.stopPropagation(); ev.stopPropagation();
@ -109,6 +109,7 @@ export class HUIView extends ReactiveElement {
element.lovelace = this.lovelace; element.lovelace = this.lovelace;
element.config = sectionConfig; element.config = sectionConfig;
element.viewIndex = this.index; element.viewIndex = this.index;
element.preview = this.lovelace.editMode;
element.addEventListener( element.addEventListener(
"ll-rebuild", "ll-rebuild",
(ev: Event) => { (ev: Event) => {
@ -214,7 +215,7 @@ export class HUIView extends ReactiveElement {
} }
}); });
this._cards.forEach((element) => { this._cards.forEach((element) => {
element.editMode = this.lovelace.editMode; element.preview = this.lovelace.editMode;
}); });
} }
if (changedProperties.has("_cards")) { if (changedProperties.has("_cards")) {