Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add visibility option to dashboard cards #20840

Merged
merged 9 commits into from
May 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 5 additions & 9 deletions src/data/lovelace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,13 @@ import {
getCollection,
HassEventBase,
} from "home-assistant-js-websocket";
import { HuiErrorCard } from "../panels/lovelace/cards/hui-error-card";
import {
Lovelace,
LovelaceBadge,
LovelaceCard,
} from "../panels/lovelace/types";
import type { HuiCard } from "../panels/lovelace/cards/hui-card";
import type { HuiSection } from "../panels/lovelace/sections/hui-section";
import { Lovelace, LovelaceBadge } from "../panels/lovelace/types";
import { HomeAssistant } from "../types";
import { LovelaceSectionConfig } from "./lovelace/config/section";
import { fetchConfig, LegacyLovelaceConfig } from "./lovelace/config/types";
import { LovelaceViewConfig } from "./lovelace/config/view";
import { HuiSection } from "../panels/lovelace/sections/hui-section";

export interface LovelacePanelConfig {
mode: "yaml" | "storage";
Expand All @@ -24,7 +20,7 @@ export interface LovelaceViewElement extends HTMLElement {
lovelace?: Lovelace;
narrow?: boolean;
index?: number;
cards?: Array<LovelaceCard | HuiErrorCard>;
cards?: HuiCard[];
badges?: LovelaceBadge[];
sections?: HuiSection[];
isStrategy: boolean;
Expand All @@ -36,7 +32,7 @@ export interface LovelaceSectionElement extends HTMLElement {
lovelace?: Lovelace;
viewIndex?: number;
index?: number;
cards?: Array<LovelaceCard | HuiErrorCard>;
cards?: HuiCard[];
isStrategy: boolean;
setConfig(config: LovelaceSectionConfig): void;
}
Expand Down
4 changes: 3 additions & 1 deletion src/data/lovelace/config/card.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { LovelaceLayoutOptions } from "../../../panels/lovelace/types";
import type { Condition } from "../../../panels/lovelace/common/validate-condition";
import type { LovelaceLayoutOptions } from "../../../panels/lovelace/types";

export interface LovelaceCardConfig {
index?: number;
Expand All @@ -7,4 +8,5 @@ export interface LovelaceCardConfig {
layout_options?: LovelaceLayoutOptions;
type: string;
[key: string]: any;
visibility?: Condition[];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Specify a more appropriate type for view_layout.

-  view_layout?: any,
+  view_layout?: LovelaceLayoutOptions,

Committable suggestion was skipped due low confidence.

}
141 changes: 141 additions & 0 deletions src/panels/lovelace/cards/hui-card.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import { PropertyValues, ReactiveElement } from "lit";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optimize imports by using import type for type-only imports to reduce runtime overhead and improve build performance.

- import { PropertyValues, ReactiveElement } from "lit";
- import { customElement, property, state } from "lit/decorators";
- import { MediaQueriesListener } from "../../../common/dom/media_query";
+ import type { PropertyValues, ReactiveElement } from "lit";
+ import type { customElement, property, state } from "lit/decorators";
+ import type { MediaQueriesListener } from "../../../common/dom/media_query";

Also applies to: 2-3, 4-5


Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation.

Suggested change
import { PropertyValues, ReactiveElement } from "lit";
import type { PropertyValues, ReactiveElement } from "lit";
import type { customElement, property, state } from "lit/decorators";
import type { MediaQueriesListener } from "../../../common/dom/media_query";

import { customElement, property, state } from "lit/decorators";
import { MediaQueriesListener } from "../../../common/dom/media_query";
import "../../../components/ha-svg-icon";
import { LovelaceCardConfig } from "../../../data/lovelace/config/card";
import type { HomeAssistant } from "../../../types";
import { computeCardSize } from "../common/compute-card-size";
import {
attachConditionMediaQueriesListeners,
checkConditionsMet,
} from "../common/validate-condition";
import { createCardElement } from "../create-element/create-card-element";
import type { Lovelace, LovelaceCard, LovelaceLayoutOptions } from "../types";

@customElement("hui-card")
export class HuiCard extends ReactiveElement {
@property({ attribute: false }) public hass!: HomeAssistant;

@property({ attribute: false }) public lovelace!: Lovelace;

@state() public _config?: LovelaceCardConfig;

private _element?: LovelaceCard;

private _listeners: MediaQueriesListener[] = [];

protected createRenderRoot() {
return this;
}

public disconnectedCallback() {
super.disconnectedCallback();
this._clearMediaQueries();
}

public connectedCallback() {
super.connectedCallback();
this._listenMediaQueries();
this._updateElement();
}

public getCardSize(): number | Promise<number> {
if (this._element) {
const size = computeCardSize(this._element);
return size;
}
return 1;
}

public getLayoutOptions(): LovelaceLayoutOptions {
const configOptions = this._config?.layout_options ?? {};
if (this._element) {
const cardOptions = this._element.getLayoutOptions?.() ?? {};
return {
...cardOptions,
...configOptions,
};
}
return configOptions;
}

public setConfig(config: LovelaceCardConfig): void {
if (this._config === config) {
return;
}
this._config = config;
this._element = createCardElement(config);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hui-card should probably also handle the rebuild logic, we can then remove it anywhere else.

this._element.hass = this.hass;
this._element.editMode = this.lovelace.editMode;

while (this.lastChild) {
this.removeChild(this.lastChild);
}
this.appendChild(this._element!);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Avoid using non-null assertions as they bypass TypeScript's strict null checks.

- this.appendChild(this._element!);
+ if (this._element) {
+   this.appendChild(this._element);
+ }

Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation.

Suggested change
this.appendChild(this._element!);
if (this._element) {
this.appendChild(this._element);
}

}

protected update(changedProperties: PropertyValues<typeof this>) {
super.update(changedProperties);

if (this._element) {
if (changedProperties.has("hass")) {
this._element.hass = this.hass;
}
if (changedProperties.has("lovelace")) {
this._element.editMode = this.lovelace.editMode;
}
if (changedProperties.has("hass") || changedProperties.has("lovelace")) {
this._updateElement();
}
}
}

private _clearMediaQueries() {
this._listeners.forEach((unsub) => unsub());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prefer using for...of for iterating over arrays for better readability and performance.

- this._listeners.forEach((unsub) => unsub());
+ for (const unsub of this._listeners) {
+   unsub();
+ }

Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation.

Suggested change
this._listeners.forEach((unsub) => unsub());
for (const unsub of this._listeners) {
unsub();
}

this._listeners = [];
}

private _listenMediaQueries() {
this._clearMediaQueries();
if (!this._config?.visibility) {
return;
}
const conditions = this._config.visibility;
const hasOnlyMediaQuery =
conditions.length === 1 &&
conditions[0].condition === "screen" &&
!!conditions[0].media_query;

this._listeners = attachConditionMediaQueriesListeners(
this._config.visibility,
(matches) => {
this._updateElement(hasOnlyMediaQuery && matches);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here

}
);
}

private _updateElement(forceVisible?: boolean) {
if (!this._element) {
return;
}
const visible =
forceVisible ||
this.lovelace.editMode ||
!this._config?.visibility ||
checkConditionsMet(this._config.visibility, this.hass);

this.style.setProperty("display", visible ? "" : "none");
this.toggleAttribute("hidden", !visible);
if (!visible && this._element.parentElement) {
this.removeChild(this._element);
} else if (visible && !this._element.parentElement) {
this.appendChild(this._element);
}
}
}

declare global {
interface HTMLElementTagNameMap {
"hui-card": HuiCard;
}
}
3 changes: 2 additions & 1 deletion src/panels/lovelace/common/compute-card-size.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { promiseTimeout } from "../../../common/util/promise-timeout";
import { HuiCard } from "../cards/hui-card";
import { LovelaceCard, LovelaceHeaderFooter } from "../types";
Comment on lines 1 to 3
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optimize imports to use type-only imports where appropriate.

- import { HuiCard } from "../cards/hui-card";
- import { LovelaceCard, LovelaceHeaderFooter } from "../types";
+ import type { HuiCard } from "../cards/hui-card";
+ import type { LovelaceCard, LovelaceHeaderFooter } from "../types";

Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation.

Suggested change
import { promiseTimeout } from "../../../common/util/promise-timeout";
import { HuiCard } from "../cards/hui-card";
import { LovelaceCard, LovelaceHeaderFooter } from "../types";
import { promiseTimeout } from "../../../common/util/promise-timeout";
import type { HuiCard } from "../cards/hui-card";
import type { LovelaceCard, LovelaceHeaderFooter } from "../types";


export const computeCardSize = (
card: LovelaceCard | LovelaceHeaderFooter
card: LovelaceCard | LovelaceHeaderFooter | HuiCard
): number | Promise<number> => {
if (typeof card.getCardSize === "function") {
try {
Expand Down
18 changes: 2 additions & 16 deletions src/panels/lovelace/common/validate-condition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -327,27 +327,13 @@ export function extractMediaQueries(conditions: Condition[]): string[] {

export function attachConditionMediaQueriesListeners(
conditions: Condition[],
hass: HomeAssistant,
onChange: (visibility: boolean) => void
): MediaQueriesListener[] {
// For performance, if there is only one condition and it's a screen condition, set the visibility directly
if (
conditions.length === 1 &&
conditions[0].condition === "screen" &&
conditions[0].media_query
) {
const listener = listenMediaQuery(conditions[0].media_query, (matches) => {
onChange(matches);
});
return [listener];
}

const mediaQueries = extractMediaQueries(conditions);

const listeners = mediaQueries.map((query) => {
const listener = listenMediaQuery(query, () => {
const visibility = checkConditionsMet(conditions, hass);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I moved checkConditionsMet outside of this function because hass value wasn't updated.

onChange(visibility);
const listener = listenMediaQuery(query, (matches) => {
onChange(matches);
});
return listener;
});
Expand Down
19 changes: 15 additions & 4 deletions src/panels/lovelace/components/hui-conditional-base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,11 +84,21 @@ export class HuiConditionalBase extends ReactiveElement {

this._clearMediaQueries();

const conditions = this._config.conditions;
const hasOnlyMediaQuery =
conditions.length === 1 &&
"condition" in conditions[0] &&
conditions[0].condition === "screen" &&
!!conditions[0].media_query;

this._listeners = attachConditionMediaQueriesListeners(
supportedConditions,
this.hass,
(visibility) => {
this._setVisibility(visibility);
(matches) => {
if (hasOnlyMediaQuery) {
this._setVisibility(matches);
return;
}
this._updateVisibility();
}
);
}
Expand All @@ -99,7 +109,8 @@ export class HuiConditionalBase extends ReactiveElement {
if (
changed.has("_element") ||
changed.has("_config") ||
changed.has("hass")
changed.has("hass") ||
changed.has("editMode")
) {
this._listenMediaQueries();
this._updateVisibility();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@ import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-yaml-editor";

import type { HaYamlEditor } from "../../../../components/ha-yaml-editor";
import { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
import { LovelaceSectionConfig } from "../../../../data/lovelace/config/section";
import { LovelaceConfig } from "../../../../data/lovelace/config/types";
import { isStrategyView } from "../../../../data/lovelace/config/view";
import { haStyleDialog } from "../../../../resources/styles";
import { HomeAssistant } from "../../../../types";
import { showSaveSuccessToast } from "../../../../util/toast-saved-success";
import "../../sections/hui-section";
import { addCards, addSection } from "../config-util";
import {
LovelaceContainerPath,
Expand All @@ -18,7 +21,6 @@ import {
import "./hui-card-preview";
import { showCreateCardDialog } from "./show-create-card-dialog";
import { SuggestCardDialogParams } from "./show-suggest-card-dialog";
import { LovelaceConfig } from "../../../../data/lovelace/config/types";

@customElement("hui-dialog-suggest-card")
export class HuiDialogSuggestCard extends LitElement {
Expand Down
1 change: 1 addition & 0 deletions src/panels/lovelace/editor/structs/base-card-struct.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ export const baseLovelaceCardConfig = object({
type: string(),
view_layout: any(),
layout_options: any(),
visibility: any(),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Specify a more appropriate type for visibility.

-  visibility: any(),
+  visibility: array(Condition),

Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation.

Suggested change
visibility: any(),
visibility: array(Condition),

});
27 changes: 7 additions & 20 deletions src/panels/lovelace/sections/hui-grid-section.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ import { LovelaceCardConfig } from "../../../data/lovelace/config/card";
import type { LovelaceSectionConfig } from "../../../data/lovelace/config/section";
import { haStyle } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types";
import { HuiErrorCard } from "../cards/hui-error-card";
import "../components/hui-card-edit-mode";
import { moveCard } from "../editor/config-util";
import type { Lovelace, LovelaceCard, LovelaceLayoutOptions } from "../types";
import type { Lovelace } from "../types";
import { HuiCard } from "../cards/hui-card";

const CARD_SORTABLE_OPTIONS: HaSortableOptions = {
delay: 100,
Expand All @@ -34,9 +34,7 @@ export class GridSection extends LitElement implements LovelaceSectionElement {

@property({ type: Boolean }) public isStrategy = false;

@property({ attribute: false }) public cards: Array<
LovelaceCard | HuiErrorCard
> = [];
@property({ attribute: false }) public cards: HuiCard[] = [];

@state() _config?: LovelaceSectionConfig;

Expand Down Expand Up @@ -95,27 +93,16 @@ export class GridSection extends LitElement implements LovelaceSectionElement {
(cardConfig) => this._getKey(cardConfig),
(_cardConfig, idx) => {
const card = this.cards![idx];
(card as any).editMode = editMode;
(card as any).lovelace = this.lovelace;

const configOptions = _cardConfig.layout_options;
const cardOptions = (card as any)?.getLayoutOptions?.() as
| LovelaceLayoutOptions
| undefined;

const options = {
...cardOptions,
...configOptions,
} as LovelaceLayoutOptions;
const layoutOptions = card.getLayoutOptions();

return html`
<div
style=${styleMap({
"--column-size": options.grid_columns,
"--row-size": options.grid_rows,
"--column-size": layoutOptions.grid_columns,
"--row-size": layoutOptions.grid_rows,
})}
class="card ${classMap({
"fit-rows": typeof options?.grid_rows === "number",
"fit-rows": typeof layoutOptions?.grid_rows === "number",
})}"
>
${editMode
Expand Down
Loading
Loading