mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-24 01:36:49 +00:00
Various changes to card editor. (#3265)
* Various changes to card editor. * Avoid crashing on bad yaml when creating a new card * Address review comments * Revert interface change * Avoid config loops. Nicer error behavior.
This commit is contained in:
parent
ef3892de92
commit
c15629b81b
281
src/panels/lovelace/editor/card-editor/hui-card-editor.ts
Normal file
281
src/panels/lovelace/editor/card-editor/hui-card-editor.ts
Normal file
@ -0,0 +1,281 @@
|
||||
import {
|
||||
html,
|
||||
css,
|
||||
LitElement,
|
||||
TemplateResult,
|
||||
CSSResult,
|
||||
customElement,
|
||||
property,
|
||||
} from "lit-element";
|
||||
|
||||
import yaml from "js-yaml";
|
||||
|
||||
import "@material/mwc-button";
|
||||
import { HomeAssistant } from "../../../../types";
|
||||
import { LovelaceCardConfig } from "../../../../data/lovelace";
|
||||
import { LovelaceCardEditor } from "../../types";
|
||||
import { getCardElementTag } from "../../common/get-card-element-tag";
|
||||
|
||||
import "../../components/hui-yaml-editor";
|
||||
// This is not a duplicate import, one is for types, one is for element.
|
||||
// tslint:disable-next-line
|
||||
import { HuiYamlEditor } from "../../components/hui-yaml-editor";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { EntityConfig } from "../../entity-rows/types";
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
"entities-changed": {
|
||||
entities: EntityConfig[];
|
||||
};
|
||||
"config-changed": {
|
||||
config: LovelaceCardConfig;
|
||||
error?: string;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export interface UIConfigChangedEvent extends Event {
|
||||
detail: {
|
||||
config: LovelaceCardConfig;
|
||||
};
|
||||
}
|
||||
|
||||
@customElement("hui-card-editor")
|
||||
export class HuiCardEditor extends LitElement {
|
||||
@property() public hass?: HomeAssistant;
|
||||
|
||||
@property() private _yaml?: string;
|
||||
@property() private _config?: LovelaceCardConfig;
|
||||
@property() private _configElement?: LovelaceCardEditor;
|
||||
@property() private _configElType?: string;
|
||||
@property() private _GUImode: boolean = true;
|
||||
// Error: Configuration broken - do not save
|
||||
@property() private _error?: string;
|
||||
// Warning: GUI editor can't handle configuration - ok to save
|
||||
@property() private _warning?: string;
|
||||
@property() private _loading: boolean = false;
|
||||
|
||||
public get yaml(): string {
|
||||
return this._yaml || "";
|
||||
}
|
||||
public set yaml(_yaml: string) {
|
||||
this._yaml = _yaml;
|
||||
try {
|
||||
this._config = yaml.safeLoad(this.yaml);
|
||||
this._updateConfigElement();
|
||||
setTimeout(() => {
|
||||
if (this._yamlEditor) {
|
||||
this._yamlEditor.codemirror.refresh();
|
||||
}
|
||||
}, 1);
|
||||
this._error = undefined;
|
||||
} catch (err) {
|
||||
this._error = err.message;
|
||||
}
|
||||
fireEvent(this, "config-changed", {
|
||||
config: this.value!,
|
||||
error: this._error,
|
||||
});
|
||||
}
|
||||
|
||||
public get value(): LovelaceCardConfig | undefined {
|
||||
return this._config;
|
||||
}
|
||||
public set value(config: LovelaceCardConfig | undefined) {
|
||||
if (JSON.stringify(config) !== JSON.stringify(this._config || {})) {
|
||||
this.yaml = yaml.safeDump(config);
|
||||
}
|
||||
}
|
||||
|
||||
public get hasError(): boolean {
|
||||
return this._error !== undefined;
|
||||
}
|
||||
|
||||
private get _yamlEditor(): HuiYamlEditor {
|
||||
return this.shadowRoot!.querySelector("hui-yaml-editor")!;
|
||||
}
|
||||
|
||||
public toggleMode() {
|
||||
this._GUImode = !this._GUImode;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<div class="wrapper">
|
||||
${this._GUImode
|
||||
? html`
|
||||
<div class="gui-editor">
|
||||
${this._loading
|
||||
? html`
|
||||
<paper-spinner
|
||||
active
|
||||
alt="Loading"
|
||||
class="center margin-bot"
|
||||
></paper-spinner>
|
||||
`
|
||||
: this._configElement}
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
<div class="yaml-editor">
|
||||
<hui-yaml-editor
|
||||
.hass=${this.hass}
|
||||
.value=${this.yaml}
|
||||
@yaml-changed=${this._handleYAMLChanged}
|
||||
></hui-yaml-editor>
|
||||
</div>
|
||||
`}
|
||||
${this._error
|
||||
? html`
|
||||
<div class="error">
|
||||
${this._error}
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
${this._warning
|
||||
? html`
|
||||
<div class="warning">
|
||||
${this._warning}
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
<div class="buttons">
|
||||
<mwc-button
|
||||
@click=${this.toggleMode}
|
||||
?disabled=${this._warning || this._error}
|
||||
?unelevated=${this._GUImode === false}
|
||||
>
|
||||
<ha-icon icon="mdi:code-braces"></ha-icon>
|
||||
</mwc-button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
protected updated(changedProperties) {
|
||||
super.updated(changedProperties);
|
||||
|
||||
if (changedProperties.has("_GUImode")) {
|
||||
if (this._GUImode === false) {
|
||||
// Refresh code editor when switching to yaml mode
|
||||
this._yamlEditor.codemirror.refresh();
|
||||
this._yamlEditor.codemirror.focus();
|
||||
}
|
||||
fireEvent(this as HTMLElement, "iron-resize");
|
||||
}
|
||||
}
|
||||
|
||||
private _handleUIConfigChanged(ev: UIConfigChangedEvent) {
|
||||
ev.stopPropagation();
|
||||
const config = ev.detail.config;
|
||||
this.value = config;
|
||||
}
|
||||
private _handleYAMLChanged(ev) {
|
||||
ev.stopPropagation();
|
||||
const newYaml = ev.detail.value;
|
||||
if (newYaml !== this.yaml) {
|
||||
this.yaml = newYaml;
|
||||
}
|
||||
}
|
||||
|
||||
private async _updateConfigElement(): Promise<void> {
|
||||
if (!this.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cardType = this.value.type;
|
||||
let configElement = this._configElement;
|
||||
try {
|
||||
this._error = undefined;
|
||||
this._warning = undefined;
|
||||
|
||||
if (this._configElType !== cardType) {
|
||||
// If the card type has changed, we need to load a new GUI editor
|
||||
if (!this.value.type) {
|
||||
throw new Error("No card type defined");
|
||||
}
|
||||
|
||||
const tag = getCardElementTag(cardType);
|
||||
|
||||
// Check if the card type exists
|
||||
const elClass = customElements.get(tag);
|
||||
if (!elClass) {
|
||||
throw new Error(`Unknown card type encountered: ${cardType}.`);
|
||||
}
|
||||
|
||||
this._loading = true;
|
||||
// Check if a GUI editor exists
|
||||
if (elClass && elClass.getConfigElement) {
|
||||
configElement = await elClass.getConfigElement();
|
||||
} else {
|
||||
configElement = undefined;
|
||||
throw Error(`WARNING: No GUI editor available for: ${cardType}`);
|
||||
}
|
||||
|
||||
this._configElement = configElement;
|
||||
this._configElType = cardType;
|
||||
}
|
||||
|
||||
// Setup GUI editor and check that it can handle the current config
|
||||
try {
|
||||
this._configElement!.setConfig(this.value);
|
||||
} catch (err) {
|
||||
throw Error(`WARNING: ${err.message}`);
|
||||
}
|
||||
|
||||
// Perform final setup
|
||||
this._configElement!.hass = this.hass;
|
||||
this._configElement!.addEventListener("config-changed", (ev) =>
|
||||
this._handleUIConfigChanged(ev as UIConfigChangedEvent)
|
||||
);
|
||||
|
||||
return;
|
||||
} catch (err) {
|
||||
if (err.message.startsWith("WARNING:")) {
|
||||
this._warning = err.message.substr(8);
|
||||
} else {
|
||||
this._error = err;
|
||||
}
|
||||
this._GUImode = false;
|
||||
} finally {
|
||||
this._loading = false;
|
||||
fireEvent(this, "iron-resize");
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
:host {
|
||||
display: flex;
|
||||
}
|
||||
.wrapper {
|
||||
width: 100%;
|
||||
}
|
||||
.gui-editor,
|
||||
.yaml-editor {
|
||||
padding: 8px 0px;
|
||||
}
|
||||
.error {
|
||||
color: #ef5350;
|
||||
}
|
||||
.warning {
|
||||
color: #ffa726;
|
||||
}
|
||||
.buttons {
|
||||
text-align: right;
|
||||
padding: 8px 0px;
|
||||
}
|
||||
paper-spinner {
|
||||
display: block;
|
||||
margin: auto;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-card-editor": HuiCardEditor;
|
||||
}
|
||||
}
|
@ -12,6 +12,7 @@ import { HomeAssistant } from "../../../../types";
|
||||
import { LovelaceCardConfig } from "../../../../data/lovelace";
|
||||
import { getCardElementTag } from "../../common/get-card-element-tag";
|
||||
import { CardPickTarget } from "../types";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
|
||||
const cards = [
|
||||
{ name: "Alarm panel", type: "alarm-panel" },
|
||||
@ -60,6 +61,9 @@ export class HuiCardPicker extends LitElement {
|
||||
`;
|
||||
})}
|
||||
</div>
|
||||
<div class="cards-container">
|
||||
<mwc-button @click="${this._manualPicked}">MANUAL CARD</mwc-button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@ -85,6 +89,12 @@ export class HuiCardPicker extends LitElement {
|
||||
];
|
||||
}
|
||||
|
||||
private _manualPicked(): void {
|
||||
fireEvent(this, "config-changed", {
|
||||
config: { type: "" },
|
||||
});
|
||||
}
|
||||
|
||||
private _cardPicked(ev: Event): void {
|
||||
const type = (ev.currentTarget! as CardPickTarget).type;
|
||||
const tag = getCardElementTag(type);
|
||||
@ -97,7 +107,7 @@ export class HuiCardPicker extends LitElement {
|
||||
config = { ...config, ...cardConfig };
|
||||
}
|
||||
|
||||
this.cardPicked!(config);
|
||||
fireEvent(this, "config-changed", { config });
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,9 @@
|
||||
import {
|
||||
css,
|
||||
html,
|
||||
LitElement,
|
||||
TemplateResult,
|
||||
CSSResultArray,
|
||||
customElement,
|
||||
property,
|
||||
} from "lit-element";
|
||||
@ -9,11 +11,17 @@ import {
|
||||
import { HomeAssistant } from "../../../../types";
|
||||
import { HASSDomEvent } from "../../../../common/dom/fire_event";
|
||||
import { LovelaceCardConfig } from "../../../../data/lovelace";
|
||||
import "./hui-edit-card";
|
||||
import "./hui-dialog-pick-card";
|
||||
import "./hui-card-editor";
|
||||
// tslint:disable-next-line
|
||||
import { HuiCardEditor } from "./hui-card-editor";
|
||||
import "./hui-card-preview";
|
||||
import "./hui-card-picker";
|
||||
import { EditCardDialogParams } from "./show-edit-card-dialog";
|
||||
import { addCard, replaceCard } from "../config-util";
|
||||
|
||||
import "../../../../components/dialog/ha-paper-dialog";
|
||||
import { haStyleDialog } from "../../../../resources/styles";
|
||||
|
||||
declare global {
|
||||
// for fire event
|
||||
interface HASSDomEvents {
|
||||
@ -33,72 +41,227 @@ export class HuiDialogEditCard extends LitElement {
|
||||
|
||||
@property() private _cardConfig?: LovelaceCardConfig;
|
||||
|
||||
@property() private _newCard?: boolean;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this._cardPicked = this._cardPicked.bind(this);
|
||||
this._cancel = this._cancel.bind(this);
|
||||
this._save = this._save.bind(this);
|
||||
}
|
||||
@property() private _saving: boolean = false;
|
||||
@property() private _error?: string;
|
||||
|
||||
public async showDialog(params: EditCardDialogParams): Promise<void> {
|
||||
this._params = params;
|
||||
const [view, card] = params.path;
|
||||
this._newCard = card !== undefined ? false : true;
|
||||
this._cardConfig =
|
||||
card !== undefined
|
||||
? params.lovelace.config.views[view].cards![card]
|
||||
: undefined;
|
||||
}
|
||||
|
||||
private get _cardEditorEl(): HuiCardEditor | null {
|
||||
return this.shadowRoot!.querySelector("hui-card-editor");
|
||||
}
|
||||
|
||||
protected render(): TemplateResult | void {
|
||||
if (!this._params) {
|
||||
return html``;
|
||||
}
|
||||
if (!this._cardConfig) {
|
||||
// Card picker
|
||||
return html`
|
||||
<hui-dialog-pick-card
|
||||
.hass="${this.hass}"
|
||||
.cardPicked="${this._cardPicked}"
|
||||
.closeDialog="${this._cancel}"
|
||||
></hui-dialog-pick-card>
|
||||
`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<hui-edit-card
|
||||
.hass="${this.hass}"
|
||||
.lovelace="${this._params.lovelace}"
|
||||
.cardConfig="${this._cardConfig}"
|
||||
.closeDialog="${this._cancel}"
|
||||
.saveCard="${this._save}"
|
||||
.newCard="${this._newCard}"
|
||||
>
|
||||
</hui-edit-card>
|
||||
<ha-paper-dialog with-backdrop opened modal>
|
||||
<h2>
|
||||
${this.hass!.localize("ui.panel.lovelace.editor.edit_card.header")}
|
||||
</h2>
|
||||
<paper-dialog-scrollable>
|
||||
${this._cardConfig === undefined
|
||||
? html`
|
||||
<hui-card-picker
|
||||
.hass="${this.hass}"
|
||||
@config-changed="${this._handleConfigChanged}"
|
||||
></hui-card-picker>
|
||||
`
|
||||
: html`
|
||||
<div class="content">
|
||||
<div class="element-editor">
|
||||
<hui-card-editor
|
||||
.hass="${this.hass}"
|
||||
.value="${this._cardConfig}"
|
||||
@config-changed="${this._handleConfigChanged}"
|
||||
></hui-card-editor>
|
||||
</div>
|
||||
<div class="element-preview">
|
||||
<hui-card-preview
|
||||
.hass="${this.hass}"
|
||||
.config="${this._cardConfig}"
|
||||
class=${this._error ? "blur" : ""}
|
||||
></hui-card-preview>
|
||||
${this._error
|
||||
? html`
|
||||
<paper-spinner
|
||||
active
|
||||
alt="Can't update card"
|
||||
></paper-spinner>
|
||||
`
|
||||
: ``}
|
||||
</div>
|
||||
</div>
|
||||
`}
|
||||
</paper-dialog-scrollable>
|
||||
<div class="paper-dialog-buttons">
|
||||
<mwc-button @click="${this._close}">
|
||||
${this.hass!.localize("ui.common.cancel")}
|
||||
</mwc-button>
|
||||
<mwc-button
|
||||
?disabled="${!this._canSave || this._saving}"
|
||||
@click="${this._save}"
|
||||
>
|
||||
${this._saving
|
||||
? html`
|
||||
<paper-spinner active alt="Saving"></paper-spinner>
|
||||
`
|
||||
: this.hass!.localize("ui.common.save")}
|
||||
</mwc-button>
|
||||
</div>
|
||||
</ha-paper-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private _cardPicked(cardConf: LovelaceCardConfig): void {
|
||||
this._cardConfig = cardConf;
|
||||
static get styles(): CSSResultArray {
|
||||
return [
|
||||
haStyleDialog,
|
||||
css`
|
||||
:host {
|
||||
--code-mirror-max-height: calc(100vh - 176px);
|
||||
}
|
||||
|
||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||
/* overrule the ha-style-dialog max-height on small screens */
|
||||
ha-paper-dialog {
|
||||
max-height: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media all and (min-width: 660px) {
|
||||
ha-paper-dialog {
|
||||
width: 845px;
|
||||
}
|
||||
}
|
||||
|
||||
ha-paper-dialog {
|
||||
max-width: 845px;
|
||||
}
|
||||
|
||||
.center {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 0 -10px;
|
||||
}
|
||||
.content hui-card-preview {
|
||||
margin: 4px auto;
|
||||
max-width: 390px;
|
||||
}
|
||||
.content .element-editor {
|
||||
margin: 0 10px;
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
ha-paper-dialog {
|
||||
max-width: none;
|
||||
width: 1000px;
|
||||
}
|
||||
|
||||
.content {
|
||||
flex-direction: row;
|
||||
}
|
||||
.content > * {
|
||||
flex-basis: 0;
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.content hui-card-preview {
|
||||
padding: 8px 0;
|
||||
margin: auto 10px;
|
||||
max-width: 500px;
|
||||
}
|
||||
}
|
||||
|
||||
mwc-button paper-spinner {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
margin-right: 20px;
|
||||
}
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
.element-editor {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.blur {
|
||||
filter: blur(2px) grayscale(100%);
|
||||
}
|
||||
.element-preview {
|
||||
position: relative;
|
||||
}
|
||||
.element-preview paper-spinner {
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
}
|
||||
hui-card-preview {
|
||||
padding-top: 8px;
|
||||
margin-bottom: 4px;
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
private _cancel(): void {
|
||||
private _handleConfigChanged(ev) {
|
||||
this._cardConfig = ev.detail.config;
|
||||
this._error = ev.detail.error;
|
||||
}
|
||||
|
||||
private _close(): void {
|
||||
this._params = undefined;
|
||||
this._cardConfig = undefined;
|
||||
this._error = undefined;
|
||||
}
|
||||
|
||||
private async _save(cardConf: LovelaceCardConfig): Promise<void> {
|
||||
private get _canSave(): boolean {
|
||||
if (this._saving) {
|
||||
return false;
|
||||
}
|
||||
if (this._cardConfig === undefined) {
|
||||
return false;
|
||||
}
|
||||
if (this._cardEditorEl && this._cardEditorEl.hasError) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private async _save(): Promise<void> {
|
||||
const lovelace = this._params!.lovelace;
|
||||
this._saving = true;
|
||||
await lovelace.saveConfig(
|
||||
this._params!.path.length === 1
|
||||
? addCard(lovelace.config, this._params!.path as [number], cardConf)
|
||||
? addCard(
|
||||
lovelace.config,
|
||||
this._params!.path as [number],
|
||||
this._cardConfig!
|
||||
)
|
||||
: replaceCard(
|
||||
lovelace.config,
|
||||
this._params!.path as [number, number],
|
||||
cardConf
|
||||
this._cardConfig!
|
||||
)
|
||||
);
|
||||
this._saving = false;
|
||||
this._close();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,88 +0,0 @@
|
||||
import {
|
||||
html,
|
||||
css,
|
||||
LitElement,
|
||||
TemplateResult,
|
||||
CSSResult,
|
||||
customElement,
|
||||
} from "lit-element";
|
||||
import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable";
|
||||
|
||||
import "../../../../components/dialog/ha-paper-dialog";
|
||||
|
||||
import { haStyleDialog } from "../../../../resources/styles";
|
||||
|
||||
import "./hui-card-picker";
|
||||
import { HomeAssistant } from "../../../../types";
|
||||
import { LovelaceCardConfig } from "../../../../data/lovelace";
|
||||
|
||||
@customElement("hui-dialog-pick-card")
|
||||
export class HuiDialogPickCard extends LitElement {
|
||||
public hass?: HomeAssistant;
|
||||
public cardPicked?: (cardConf: LovelaceCardConfig) => void;
|
||||
public closeDialog?: () => void;
|
||||
|
||||
protected render(): TemplateResult | void {
|
||||
return html`
|
||||
<ha-paper-dialog
|
||||
with-backdrop
|
||||
opened
|
||||
@opened-changed="${this._openedChanged}"
|
||||
>
|
||||
<h2>
|
||||
${this.hass!.localize("ui.panel.lovelace.editor.edit_card.header")}
|
||||
</h2>
|
||||
<paper-dialog-scrollable>
|
||||
<hui-card-picker
|
||||
.hass="${this.hass}"
|
||||
.cardPicked="${this.cardPicked}"
|
||||
></hui-card-picker>
|
||||
</paper-dialog-scrollable>
|
||||
<div class="paper-dialog-buttons">
|
||||
<mwc-button @click="${this._skipPick}">MANUAL CARD</mwc-button>
|
||||
</div>
|
||||
</ha-paper-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private _openedChanged(ev): void {
|
||||
if (!ev.detail.value) {
|
||||
this.closeDialog!();
|
||||
}
|
||||
}
|
||||
|
||||
private _skipPick() {
|
||||
this.cardPicked!({ type: "" });
|
||||
}
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
haStyleDialog,
|
||||
css`
|
||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||
/* overrule the ha-style-dialog max-height on small screens */
|
||||
ha-paper-dialog {
|
||||
max-height: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media all and (min-width: 660px) {
|
||||
ha-paper-dialog {
|
||||
width: 650px;
|
||||
}
|
||||
}
|
||||
|
||||
ha-paper-dialog {
|
||||
max-width: 650px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-dialog-pick-card": HuiDialogPickCard;
|
||||
}
|
||||
}
|
@ -1,493 +0,0 @@
|
||||
import {
|
||||
html,
|
||||
css,
|
||||
LitElement,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
CSSResult,
|
||||
customElement,
|
||||
property,
|
||||
} from "lit-element";
|
||||
import { classMap } from "lit-html/directives/class-map";
|
||||
import yaml from "js-yaml";
|
||||
|
||||
import { haStyleDialog } from "../../../../resources/styles";
|
||||
|
||||
import "@polymer/paper-spinner/paper-spinner";
|
||||
import "@polymer/paper-dialog/paper-dialog";
|
||||
import "../../../../components/dialog/ha-paper-dialog";
|
||||
// This is not a duplicate import, one is for types, one is for element.
|
||||
// tslint:disable-next-line
|
||||
import { HaPaperDialog } from "../../../../components/dialog/ha-paper-dialog";
|
||||
import "@material/mwc-button";
|
||||
import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable";
|
||||
import { HomeAssistant } from "../../../../types";
|
||||
import { LovelaceCardConfig } from "../../../../data/lovelace";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
|
||||
import "../../components/hui-yaml-editor";
|
||||
// This is not a duplicate import, one is for types, one is for element.
|
||||
// tslint:disable-next-line
|
||||
import { HuiYamlEditor } from "../../components/hui-yaml-editor";
|
||||
import "./hui-card-preview";
|
||||
// This is not a duplicate import, one is for types, one is for element.
|
||||
// tslint:disable-next-line
|
||||
import { HuiCardPreview } from "./hui-card-preview";
|
||||
import { LovelaceCardEditor, Lovelace } from "../../types";
|
||||
import { ConfigError } from "../types";
|
||||
import { EntityConfig } from "../../entity-rows/types";
|
||||
import { getCardElementTag } from "../../common/get-card-element-tag";
|
||||
import { afterNextRender } from "../../../../common/util/render-status";
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
"entities-changed": {
|
||||
entities: EntityConfig[];
|
||||
};
|
||||
"config-changed": {
|
||||
config: LovelaceCardConfig;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@customElement("hui-edit-card")
|
||||
export class HuiEditCard extends LitElement {
|
||||
@property() public hass?: HomeAssistant;
|
||||
|
||||
@property() public cardConfig?: LovelaceCardConfig;
|
||||
|
||||
public lovelace?: Lovelace;
|
||||
|
||||
public closeDialog?: () => void;
|
||||
|
||||
public saveCard?: (cardConf: LovelaceCardConfig) => Promise<void>;
|
||||
|
||||
public newCard?: boolean;
|
||||
|
||||
@property() private _configElement?: LovelaceCardEditor | null;
|
||||
|
||||
@property() private _uiEditor?: boolean;
|
||||
|
||||
@property() private _cardConfig?: LovelaceCardConfig;
|
||||
|
||||
@property() private _configState?: string;
|
||||
|
||||
@property() private _loading?: boolean;
|
||||
|
||||
@property() private _saving: boolean;
|
||||
|
||||
@property() private _errorMsg?: TemplateResult;
|
||||
|
||||
private get _dialog(): HaPaperDialog {
|
||||
return this.shadowRoot!.querySelector("ha-paper-dialog")!;
|
||||
}
|
||||
|
||||
private get _previewEl(): HuiCardPreview {
|
||||
return this.shadowRoot!.querySelector("hui-card-preview")!;
|
||||
}
|
||||
|
||||
// tslint:disable-next-line
|
||||
private __cardYaml: string | undefined;
|
||||
|
||||
private get _cardYaml(): string | undefined {
|
||||
if (!this.__cardYaml) {
|
||||
this.__cardYaml = yaml.safeDump(this._cardConfig);
|
||||
}
|
||||
return this.__cardYaml;
|
||||
}
|
||||
|
||||
private set _cardYaml(yml: string | undefined) {
|
||||
this.__cardYaml = yml;
|
||||
}
|
||||
|
||||
public constructor() {
|
||||
super();
|
||||
this._saving = false;
|
||||
}
|
||||
|
||||
protected updated(changedProperties: PropertyValues): void {
|
||||
super.updated(changedProperties);
|
||||
|
||||
if (!changedProperties.has("cardConfig")) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._cardConfig = undefined;
|
||||
this._cardYaml = undefined;
|
||||
this._configState = "OK";
|
||||
this._uiEditor = true;
|
||||
this._errorMsg = undefined;
|
||||
this._configElement = undefined;
|
||||
|
||||
this._loading = true;
|
||||
this._loadConfigElement(this.cardConfig!);
|
||||
}
|
||||
|
||||
protected render(): TemplateResult | void {
|
||||
let content;
|
||||
let preview;
|
||||
if (this._configElement !== undefined) {
|
||||
content = html`
|
||||
<div class="element-editor">
|
||||
${this._uiEditor
|
||||
? this._configElement
|
||||
: html`
|
||||
<hui-yaml-editor
|
||||
.hass="${this.hass}"
|
||||
.value="${this._cardYaml}"
|
||||
@yaml-changed="${this._handleYamlChanged}"
|
||||
@yaml-save="${this._save}"
|
||||
></hui-yaml-editor>
|
||||
`}
|
||||
</div>
|
||||
`;
|
||||
|
||||
preview = html`
|
||||
<hui-card-preview .hass="${this.hass}"> </hui-card-preview>
|
||||
`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-paper-dialog
|
||||
with-backdrop
|
||||
opened
|
||||
modal
|
||||
@opened-changed="${this._openedChanged}"
|
||||
>
|
||||
<h2>
|
||||
${this.hass!.localize("ui.panel.lovelace.editor.edit_card.header")}
|
||||
</h2>
|
||||
<paper-spinner
|
||||
?active="${this._loading}"
|
||||
alt="Loading"
|
||||
class="center margin-bot"
|
||||
></paper-spinner>
|
||||
<paper-dialog-scrollable
|
||||
class="${classMap({ hidden: this._loading! })}"
|
||||
>
|
||||
${this._errorMsg
|
||||
? html`
|
||||
<div class="error">${this._errorMsg}</div>
|
||||
`
|
||||
: ""}
|
||||
<div class="content">${content}${preview}</div>
|
||||
</paper-dialog-scrollable>
|
||||
${!this._loading
|
||||
? html`
|
||||
<div class="paper-dialog-buttons">
|
||||
<mwc-button
|
||||
class="toggle-button"
|
||||
?disabled="${this._configElement === null ||
|
||||
this._configState !== "OK"}"
|
||||
@click="${this._toggleEditor}"
|
||||
>${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.edit_card.toggle_editor"
|
||||
)}</mwc-button
|
||||
>
|
||||
<mwc-button @click="${this.closeDialog}"
|
||||
>${this.hass!.localize("ui.common.cancel")}</mwc-button
|
||||
>
|
||||
<mwc-button
|
||||
?disabled="${this._saving || this._configState !== "OK"}"
|
||||
@click="${this._save}"
|
||||
>
|
||||
<paper-spinner
|
||||
?active="${this._saving}"
|
||||
alt="Saving"
|
||||
></paper-spinner>
|
||||
${this.hass!.localize("ui.common.save")}
|
||||
</mwc-button>
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
</ha-paper-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _loadedDialog(): Promise<void> {
|
||||
await this.updateComplete;
|
||||
this._loading = false;
|
||||
this._resizeDialog();
|
||||
if (!this._uiEditor) {
|
||||
afterNextRender(() => {
|
||||
this.yamlEditor.codemirror.refresh();
|
||||
this._resizeDialog();
|
||||
this.yamlEditor.codemirror.focus();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async _resizeDialog(): Promise<void> {
|
||||
await this.updateComplete;
|
||||
fireEvent(this._dialog as HTMLElement, "iron-resize");
|
||||
}
|
||||
|
||||
private async _save(): Promise<void> {
|
||||
if (!this._isConfigValid()) {
|
||||
alert("Your config is not valid, please fix your config before saving.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this._isConfigChanged()) {
|
||||
this.closeDialog!();
|
||||
return;
|
||||
}
|
||||
|
||||
this._saving = true;
|
||||
|
||||
try {
|
||||
await this.saveCard!(this._cardConfig!);
|
||||
this._cardYaml = undefined;
|
||||
this.closeDialog!();
|
||||
} catch (err) {
|
||||
alert(`Saving failed: ${err.message}`);
|
||||
} finally {
|
||||
this._saving = false;
|
||||
}
|
||||
}
|
||||
|
||||
private _handleYamlChanged(ev: CustomEvent): void {
|
||||
try {
|
||||
this._cardConfig = yaml.safeLoad(ev.detail.value);
|
||||
this._updatePreview(this._cardConfig!);
|
||||
this._configState = "OK";
|
||||
} catch (err) {
|
||||
this._configState = "YAML_ERROR";
|
||||
this._setPreviewError({
|
||||
type: "YAML Error",
|
||||
message: err,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private _handleUIConfigChanged(value: LovelaceCardConfig): void {
|
||||
this._cardConfig = value;
|
||||
this._updatePreview(value);
|
||||
}
|
||||
|
||||
private async _updatePreview(config: LovelaceCardConfig): Promise<void> {
|
||||
await this.updateComplete;
|
||||
|
||||
if (!this._previewEl) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._previewEl.config = config;
|
||||
|
||||
if (this._loading) {
|
||||
this._loadedDialog();
|
||||
} else {
|
||||
this._resizeDialog();
|
||||
}
|
||||
}
|
||||
|
||||
private _setPreviewError(error: ConfigError): void {
|
||||
if (!this._previewEl) {
|
||||
return;
|
||||
}
|
||||
this._previewEl.error = error;
|
||||
|
||||
this._resizeDialog();
|
||||
}
|
||||
|
||||
private async _toggleEditor(): Promise<void> {
|
||||
this._cardYaml = undefined;
|
||||
if (this._uiEditor) {
|
||||
this._uiEditor = false;
|
||||
} else if (this._configElement) {
|
||||
const success = await this._loadConfigElement(this._cardConfig!);
|
||||
if (!success) {
|
||||
this._loadedDialog();
|
||||
} else {
|
||||
this._uiEditor = true;
|
||||
this._configElement.setConfig(this._cardConfig!);
|
||||
}
|
||||
}
|
||||
this._resizeDialog();
|
||||
}
|
||||
|
||||
private _isConfigValid(): boolean {
|
||||
if (!this._cardConfig) {
|
||||
return false;
|
||||
}
|
||||
if (this._configState === "OK") {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private _isConfigChanged(): boolean {
|
||||
if (this.newCard) {
|
||||
return true;
|
||||
}
|
||||
return JSON.stringify(this._cardConfig) !== JSON.stringify(this.cardConfig);
|
||||
}
|
||||
|
||||
private async _loadConfigElement(conf: LovelaceCardConfig): Promise<boolean> {
|
||||
if (!conf) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this._errorMsg = undefined;
|
||||
this._loading = true;
|
||||
this._configElement = undefined;
|
||||
|
||||
const tag = getCardElementTag(conf.type);
|
||||
|
||||
const elClass = customElements.get(tag);
|
||||
let configElement;
|
||||
|
||||
this._cardConfig = conf;
|
||||
|
||||
if (elClass && elClass.getConfigElement) {
|
||||
configElement = await elClass.getConfigElement();
|
||||
} else {
|
||||
this._updatePreview(conf);
|
||||
this._uiEditor = false;
|
||||
this._configElement = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
configElement.setConfig(conf);
|
||||
} catch (err) {
|
||||
this._errorMsg = html`
|
||||
Your config is not supported by the UI editor:<br /><b>${err.message}</b
|
||||
><br />Falling back to YAML editor.
|
||||
`;
|
||||
this._updatePreview(conf);
|
||||
this._uiEditor = false;
|
||||
this._configElement = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
configElement.hass = this.hass;
|
||||
configElement.addEventListener("config-changed", (ev) =>
|
||||
this._handleUIConfigChanged(ev.detail.config)
|
||||
);
|
||||
this._configElement = configElement;
|
||||
await this.updateComplete;
|
||||
this._updatePreview(conf);
|
||||
return true;
|
||||
}
|
||||
|
||||
private _openedChanged(ev): void {
|
||||
if (!ev.detail.value) {
|
||||
this.closeDialog!();
|
||||
}
|
||||
}
|
||||
|
||||
private get yamlEditor(): HuiYamlEditor {
|
||||
return this.shadowRoot!.querySelector("hui-yaml-editor")!;
|
||||
}
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
haStyleDialog,
|
||||
css`
|
||||
:host {
|
||||
--code-mirror-max-height: calc(100vh - 176px);
|
||||
}
|
||||
|
||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||
/* overrule the ha-style-dialog max-height on small screens */
|
||||
ha-paper-dialog {
|
||||
max-height: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media all and (min-width: 660px) {
|
||||
ha-paper-dialog {
|
||||
width: 845px;
|
||||
}
|
||||
}
|
||||
|
||||
ha-paper-dialog {
|
||||
max-width: 845px;
|
||||
}
|
||||
|
||||
.center {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 0 -10px;
|
||||
}
|
||||
.content hui-card-preview {
|
||||
margin-top: 16px;
|
||||
margin: 0 auto;
|
||||
max-width: 390px;
|
||||
}
|
||||
.content .element-editor {
|
||||
margin: 0 10px;
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
ha-paper-dialog {
|
||||
max-width: none;
|
||||
width: 1000px;
|
||||
}
|
||||
|
||||
.content {
|
||||
flex-direction: row;
|
||||
}
|
||||
.content > * {
|
||||
flex-basis: 0;
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.content hui-card-preview {
|
||||
padding-top: 0;
|
||||
margin: 0 10px;
|
||||
max-width: 490px;
|
||||
}
|
||||
}
|
||||
|
||||
.margin-bot {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
mwc-button paper-spinner {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
margin-right: 20px;
|
||||
}
|
||||
paper-spinner {
|
||||
display: none;
|
||||
}
|
||||
paper-spinner[active] {
|
||||
display: block;
|
||||
}
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
.element-editor {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.error {
|
||||
color: #ef5350;
|
||||
border-bottom: 1px solid #ef5350;
|
||||
}
|
||||
hui-card-preview {
|
||||
padding-top: 8px;
|
||||
margin-bottom: 4px;
|
||||
display: block;
|
||||
}
|
||||
.toggle-button {
|
||||
margin-right: auto;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-edit-card": HuiEditCard;
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user