Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 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
52 changes: 39 additions & 13 deletions src/panels/lovelace/cards/hui-glance-card.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,23 @@ import {
import { TemplateResult } from "lit-html";
import { classMap } from "lit-html/directives/classMap";

import computeStateDisplay from "../../../common/entity/compute_state_display";
import computeStateName from "../../../common/entity/compute_state_name";
import processConfigEntities from "../common/process-config-entities";
import applyThemesOnElement from "../../../common/dom/apply_themes_on_element";
import { fireEvent } from "../../../common/dom/fire_event.js";

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Just as a future note; these cleanups should be done in a separate PR. Each time I review this PR (and it's been a few times!), I see all these changes that are actually just noise of the actual code/features.

import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin";
import { HomeAssistant } from "../../../types.js";
import { LovelaceCard, LovelaceConfig, LovelaceCardEditor } from "../types.js";
import { longPress } from "../common/directives/long-press-directive";

import toggleEntity from "../common/entity/toggle-entity";
import computeStateDisplay from "../../../common/entity/compute_state_display.js";
import computeStateName from "../../../common/entity/compute_state_name.js";
import processConfigEntities from "../common/process-config-entities";
import applyThemesOnElement from "../../../common/dom/apply_themes_on_element.js";
import toggleEntity from "../common/entity/toggle-entity.js";

import "../../../components/entity/state-badge";
import "../../../components/ha-card";
import "../../../components/ha-icon";

import { fireEvent } from "../../../common/dom/fire_event";
import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin";
import { HomeAssistant } from "../../../types";
import { LovelaceCard, LovelaceConfig } from "../types";
import { longPress } from "../common/directives/long-press-directive";

interface EntityConfig {
export interface EntityConfig {
name: string;
icon: string;
entity: string;
Expand All @@ -34,7 +33,7 @@ interface EntityConfig {
service_data?: object;
}

interface Config extends LovelaceConfig {
export interface Config extends LovelaceConfig {
show_name?: boolean;
show_state?: boolean;
title?: string;
Expand All @@ -45,6 +44,11 @@ interface Config extends LovelaceConfig {

export class HuiGlanceCard extends hassLocalizeLitMixin(LitElement)
implements LovelaceCard {
public static async getConfigElement(): Promise<LovelaceCardEditor> {
await import("../editor/hui-glance-card-editor");
return document.createElement("hui-glance-card-editor");
}

public hass?: HomeAssistant;
private _config?: Config;
private _configEntities?: EntityConfig[];
Expand Down Expand Up @@ -89,6 +93,28 @@ export class HuiGlanceCard extends hassLocalizeLitMixin(LitElement)
}
}

public getElementConfig(config: any, hass: HomeAssistant): TemplateResult {
Comment thread
zsarnett marked this conversation as resolved.
Outdated
if (!config || !hass) {
Comment thread
zsarnett marked this conversation as resolved.
Outdated
return html``;
}
return html`
<paper-input label="Title" value="${config.title}"></paper-input>
${config.entities.map((entityConf) => {
return html`
<ha-entity-picker
hass="${hass}"
value="${entityConf.entity || entityConf}"
allow-custom-entity
></ha-entity-picker>
`;
})}
<paper-checkbox ?checked="${config.show_name !==
false}">Show Entity's Name?</paper-checkbox>
<paper-checkbox ?checked="${config.show_state !==
false}">Show Entity's state-text?</paper-checkbox>
`;
}

protected shouldUpdate(changedProps: PropertyValues): boolean {
if (changedProps.has("_config")) {
return true;
Expand Down
118 changes: 93 additions & 25 deletions src/panels/lovelace/editor/hui-dialog-edit-card.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,34 @@
import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element";
import { fireEvent } from "../../../common/dom/fire_event";
import yaml from "js-yaml";
Comment thread
zsarnett marked this conversation as resolved.
import { when } from "lit-html/directives/when";

import "@polymer/paper-button/paper-button";
import "@polymer/paper-input/paper-textarea";
import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable";
import "@polymer/paper-dialog/paper-dialog";
import "@polymer/paper-button/paper-button.js";
Comment thread
zsarnett marked this conversation as resolved.
Outdated
import "@polymer/paper-input/paper-textarea.js";
import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable.js";
import "@polymer/paper-dialog/paper-dialog.js";
// This is not a duplicate import, one is for types, one is for element.
// tslint:disable-next-line
import { PaperDialogElement } from "@polymer/paper-dialog/paper-dialog";
import { HomeAssistant } from "../../../types";
import { getCardConfig, updateCardConfig } from "../common/data";
import { fireEvent } from "../../../common/dom/fire_event.js";

import "./hui-yaml-editor";
import "./hui-yaml-card-preview";
// This is not a duplicate import, one is for types, one is for element.
// tslint:disable-next-line
import { HuiYAMLCardPreview } from "./hui-yaml-card-preview";
import { LovelaceCardEditor, LovelaceConfig } from "../types";
import { TemplateResult } from "lit-html";
Comment thread
zsarnett marked this conversation as resolved.
Outdated

export class HuiDialogEditCard extends LitElement {
protected hass?: HomeAssistant;
private _cardId?: string;
private _cardConfig?: string;
private _elementConfig?: LovelaceCardEditor | null;
private _reloadLovelace?: () => void;
private _editorToggle?: boolean;
private _newConfigYaml?: string;

static get properties(): PropertyDeclarations {
return {
Expand All @@ -31,6 +38,8 @@ export class HuiDialogEditCard extends LitElement {
},
_cardConfig: {},
_dialogClosedCallback: {},
_elementConfig: {},
_editorToggle: {},
};
}

Expand All @@ -39,7 +48,9 @@ export class HuiDialogEditCard extends LitElement {
this._cardId = cardId;
this._reloadLovelace = reloadLovelace;
this._cardConfig = "";
this._loadConfig();
this._editorToggle = true;
this._elementConfig = undefined;
this._loadConfig().then(() => this._loadElementConfig());
// Wait till dialog is rendered.
await this.updateComplete;
this._dialog.open();
Expand All @@ -53,58 +64,115 @@ export class HuiDialogEditCard extends LitElement {
return this.shadowRoot!.querySelector("hui-yaml-card-preview")!;
}

protected render() {
protected render(): TemplateResult {
return html`
<style>
paper-dialog {
width: 650px;
}
.element-editor {
margin-bottom: 16px;
}
</style>
<paper-dialog with-backdrop>
<h2>Card Configuration</h2>
<paper-dialog-scrollable>
<hui-yaml-editor
.yaml="${this._cardConfig}"
@yaml-changed="${this._handleYamlChanged}"
></hui-yaml-editor>
${
this._editorToggle && this._elementConfig !== null
? html`<div class="element-editor">${when(
this._elementConfig,
() => this._elementConfig,
() => html`Loading...`
)}</div>`
: html`
<hui-yaml-editor
.yaml="${this._cardConfig}"
@yaml-changed="${this._handleYamlChanged}"
></hui-yaml-editor>`
}
<hui-yaml-card-preview
.hass="${this.hass}"
.yaml="${this._cardConfig}"
></hui-yaml-card-preview>
</paper-dialog-scrollable>
<div class="paper-dialog-buttons">
<paper-button @click="${this._closeDialog}">Cancel</paper-button>
<paper-button @click="${this._updateConfig}">Save</paper-button>
<paper-button
@click="${this._toggleEditor}"
>Toggle Editor</paper-button>
<paper-button
@click="${this._closeDialog}"
>Cancel</paper-button>
<paper-button
@click="${this._updateConfig}"'
>Save</paper-button>
</div>
</paper-dialog>
`;
}

private _handleYamlChanged(ev) {
this._previewEl.yaml = ev.detail.yaml;
protected updated(): void {
// This will center the dialog with the updated config
fireEvent(this._dialog, "iron-resize");
Comment thread
zsarnett marked this conversation as resolved.
Outdated
}

private _handleYamlChanged(ev: MouseEvent): void {
Comment thread
zsarnett marked this conversation as resolved.
Outdated
this._handleConfigChanged("yaml", (ev.detail as any).yaml);
}

private _closeDialog() {
private _handleConfigChanged(
format: string,
value: LovelaceConfig | string
): void {
if (!this._previewEl) {
return;
}

if (format === "js") {
this._newConfigYaml = yaml.safeDump(value);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Two things are weird here:

  • why is this only set when it's JS and not when YAML ? The name _handleConfigChanged seems like the method that should always set it, no other methods.
  • We should convert every change to YAML. Do less and be lazy: we only need it as YAML when the user presses thet save button. We should store the exact same format as that we pass the preview element.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This removes the need for configValue. I can just always update the YAML and send the Yaml to the backend and the review. So now it knows its always YAML

} else if (this._elementConfig) {
this._elementConfig.setConfig(yaml.safeLoad(value));
}

this._previewEl.value = { format, value };
}

private _closeDialog(): void {
this._dialog.close();
}

private async _loadConfig() {
private _toggleEditor(): void {
this._editorToggle = !this._editorToggle;
}

private async _loadConfig(): Promise<void> {
this._cardConfig = await getCardConfig(this.hass!, this._cardId!);
await this.updateComplete;
// This will center the dialog with the updated config
fireEvent(this._dialog, "iron-resize");
}

private async _updateConfig() {
const newCardConfig = this.shadowRoot!.querySelector("hui-yaml-editor")!
.yaml;
private async _loadElementConfig(): Promise<void> {
Comment thread
zsarnett marked this conversation as resolved.
Outdated
const conf = yaml.safeLoad(this._cardConfig);
const elClass = customElements.get(`hui-${conf.type}-card`);
Comment thread
zsarnett marked this conversation as resolved.
Outdated
let elementConfig;

try {
elementConfig = await elClass.getConfigElement();
elementConfig.setConfig(conf);
elementConfig.hass = this.hass;
elementConfig.addEventListener("config-changed", (ev) =>
this._handleConfigChanged("js", ev.detail.config)
);
this._elementConfig = elementConfig;
} catch (err) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Which things can raise here? We should make sure we only wrap those in our try … catch

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

catch (err) {
      if (!(err instanceof TypeError)) {
        // tslint:disable-next-line:no-console
        console.error(err);
      }
      this._configElement = null;
    }

Something like this so we can log the errors we are unexpected?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

we shouldn't wrap anything that can raise a type error.

Whenever you see try…catch, we need to know exactly why the code inside can fail and guard for those clauses. I think in this case just the fetching of config element is something we want to guard for ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

ah gotcha. I missunderstood.

this._elementConfig = null;
}
}

if (this._cardConfig === newCardConfig) {
private async _updateConfig(): Promise<void> {
Comment thread
zsarnett marked this conversation as resolved.
Outdated
if (this._cardConfig === this._newConfigYaml) {
this._dialog.close();
return;
}
try {
await updateCardConfig(this.hass!, this._cardId!, newCardConfig);
await updateCardConfig(this.hass!, this._cardId!, this._newConfigYaml);
this._dialog.close();
this._reloadLovelace!();
} catch (err) {
Expand Down
100 changes: 100 additions & 0 deletions src/panels/lovelace/editor/hui-glance-card-editor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element";
Comment thread
zsarnett marked this conversation as resolved.
import "@polymer/paper-checkbox/paper-checkbox.js";

import processConfigEntities from "../common/process-config-entities";
import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin";
import { HomeAssistant } from "../../../types.js";
import { LovelaceCardEditor } from "../types.js";
import { fireEvent } from "../../../common/dom/fire_event.js";
import { Config, EntityConfig } from "../cards/hui-glance-card";

import "../../../components/entity/state-badge.js";
import "../../../components/entity/ha-entity-picker";
import "../../../components/ha-card.js";
import "../../../components/ha-icon.js";
import { TemplateResult } from "lit-html";
Comment thread
zsarnett marked this conversation as resolved.
Outdated

export class HuiGlanceCardEditor extends hassLocalizeLitMixin(LitElement)
implements LovelaceCardEditor {
public hass?: HomeAssistant;
private _config?: Config;
private _configEntities?: EntityConfig[];

static get properties(): PropertyDeclarations {
return {
hass: {},
_config: {},
};
}

public setConfig(config: Config): void {
this._config = config;
const entities = processConfigEntities(config.entities);

this._configEntities = entities;
}

protected render(): TemplateResult {
if (!this._config || !this.hass) {
Comment thread
zsarnett marked this conversation as resolved.
Outdated
return html``;
}

return html`
<paper-input
id="title"
@value-changed="${this._valueChanged}"
label="Title"
value="${this._config.title}"
></paper-input>
${this._configEntities!.map((entityConf) =>
this.renderEntity(entityConf)
)}<br>
<paper-checkbox
id="show_name"
@change="${this._valueChanged}"
?checked="${this._config.show_name !== false}"
>Show Entity's Name?</paper-checkbox><br><br>
<paper-checkbox
id="show_state"
@change="${this._valueChanged}"
?checked="${this._config.show_state !== false}"
>Show Entity's State Text?</paper-checkbox><br>
`;
}

private renderEntity(entityConf: EntityConfig): TemplateResult {
return html`
<ha-entity-picker
hass="${this.hass}"
value="${entityConf.entity || entityConf}"
allow-custom-entity
></ha-entity-picker>
`;
}

private _valueChanged(ev: MouseEvent): void {
if (!this._config || !this.hass) {
return;
}

const target = ev.target! as any;

const newValue =
target.checked !== undefined ? target.checked : target.value;

// Unsure why I could not grab a propery value ".configValue='show_name'" and ev.target.configValue wasnt working
this._config[target.id] = newValue;
Comment thread
zsarnett marked this conversation as resolved.
Outdated

fireEvent(this, "config-changed", {
config: this._config,
});
}
}

declare global {
interface HTMLElementTagNameMap {
"hui-glance-card-editor": HuiGlanceCardEditor;
}
}

customElements.define("hui-glance-card-editor", HuiGlanceCardEditor);
Loading