Files
frontend/src/panels/lovelace/hui-editor.ts
2023-01-04 17:19:00 -05:00

321 lines
8.3 KiB
TypeScript

import { undoDepth } from "@codemirror/commands";
import "@material/mwc-button";
import { mdiClose } from "@mdi/js";
import "@polymer/app-layout/app-header/app-header";
import "@polymer/app-layout/app-toolbar/app-toolbar";
import { dump, load } from "js-yaml";
import {
css,
CSSResultGroup,
html,
LitElement,
PropertyValues,
TemplateResult,
} from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { array, assert, object, optional, string, type } from "superstruct";
import { deepEqual } from "../../common/util/deep-equal";
import "../../components/ha-circular-progress";
import "../../components/ha-code-editor";
import type { HaCodeEditor } from "../../components/ha-code-editor";
import "../../components/ha-icon-button";
import type { LovelaceConfig } from "../../data/lovelace";
import {
showAlertDialog,
showConfirmationDialog,
} from "../../dialogs/generic/show-dialog-box";
import "../../layouts/ha-app-layout";
import { haStyle } from "../../resources/styles";
import type { HomeAssistant } from "../../types";
import { showToast } from "../../util/toast";
import type { Lovelace } from "./types";
const lovelaceStruct = type({
title: optional(string()),
views: array(object()),
});
@customElement("hui-editor")
class LovelaceFullConfigEditor extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public lovelace?: Lovelace;
@property() public closeEditor?: () => void;
@state() private _saving?: boolean;
@state() private _changed?: boolean;
protected render(): TemplateResult | void {
return html`
<ha-app-layout>
<app-header slot="header">
<app-toolbar>
<ha-icon-button
.path=${mdiClose}
@click=${this._closeEditor}
.label=${this.hass!.localize("ui.common.close")}
></ha-icon-button>
<div main-title>
${this.hass!.localize(
"ui.panel.lovelace.editor.raw_editor.header"
)}
</div>
<div
class="save-button
${classMap({
saved: this._saving! === false || this._changed === true,
})}"
>
${this._changed
? this.hass!.localize(
"ui.panel.lovelace.editor.raw_editor.unsaved_changes"
)
: this.hass!.localize(
"ui.panel.lovelace.editor.raw_editor.saved"
)}
</div>
<mwc-button
raised
@click=${this._handleSave}
.disabled=${!this._changed}
>${this.hass!.localize(
"ui.panel.lovelace.editor.raw_editor.save"
)}</mwc-button
>
</app-toolbar>
</app-header>
<div class="content">
<ha-code-editor
mode="yaml"
autofocus
autocomplete-entities
autocomplete-icons
.hass=${this.hass}
@value-changed=${this._yamlChanged}
@editor-save=${this._handleSave}
dir="ltr"
>
</ha-code-editor>
</div>
</ha-app-layout>
`;
}
protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps);
this.yamlEditor.value = dump(this.lovelace!.rawConfig);
}
protected updated(changedProps: PropertyValues) {
const oldLovelace = changedProps.get("lovelace") as Lovelace | undefined;
if (
!this._saving &&
oldLovelace &&
this.lovelace &&
oldLovelace.rawConfig !== this.lovelace.rawConfig &&
!deepEqual(oldLovelace.rawConfig, this.lovelace.rawConfig)
) {
showToast(this, {
message: this.hass!.localize(
"ui.panel.lovelace.editor.raw_editor.lovelace_changed"
),
action: {
action: () => {
this.yamlEditor.value = dump(this.lovelace!.rawConfig);
},
text: this.hass!.localize(
"ui.panel.lovelace.editor.raw_editor.reload"
),
},
duration: 0,
dismissable: false,
});
}
}
static get styles(): CSSResultGroup {
return [
haStyle,
css`
:host {
--code-mirror-height: 100%;
}
ha-app-layout {
height: 100vh;
}
app-toolbar {
background-color: var(--app-header-edit-background-color, #455a64);
color: var(--app-header-edit-text-color, #fff);
}
mwc-button[disabled] {
background-color: var(--mdc-theme-on-primary);
border-radius: 4px;
}
.comments {
font-size: 16px;
}
.content {
height: calc(100vh - var(--header-height));
}
.save-button {
opacity: 0;
font-size: 14px;
padding: 0px 10px;
}
.saved {
opacity: 1;
}
`,
];
}
private _yamlChanged() {
this._changed = undoDepth(this.yamlEditor.codemirror!.state) > 0;
if (this._changed && !window.onbeforeunload) {
window.onbeforeunload = () => true;
} else if (!this._changed && window.onbeforeunload) {
window.onbeforeunload = null;
}
}
private async _closeEditor() {
if (
this._changed &&
!(await showConfirmationDialog(this, {
text: this.hass.localize(
"ui.panel.lovelace.editor.raw_editor.confirm_unsaved_changes"
),
dismissText: this.hass!.localize("ui.common.stay"),
confirmText: this.hass!.localize("ui.common.leave"),
}))
) {
return;
}
window.onbeforeunload = null;
if (this.closeEditor) {
this.closeEditor();
}
}
private async _removeConfig() {
try {
await this.lovelace!.deleteConfig();
} catch (err: any) {
showAlertDialog(this, {
text: this.hass.localize(
"ui.panel.lovelace.editor.raw_editor.error_remove",
"error",
err
),
});
}
window.onbeforeunload = null;
if (this.closeEditor) {
this.closeEditor();
}
}
private async _handleSave() {
this._saving = true;
const value = this.yamlEditor.value;
if (!value) {
showConfirmationDialog(this, {
title: this.hass.localize(
"ui.panel.lovelace.editor.raw_editor.confirm_remove_config_title"
),
text: this.hass.localize(
"ui.panel.lovelace.editor.raw_editor.confirm_remove_config_text"
),
confirmText: this.hass.localize("ui.common.remove"),
dismissText: this.hass.localize("ui.common.cancel"),
confirm: () => this._removeConfig(),
});
return;
}
if (this.yamlEditor.hasComments) {
if (
!confirm(
this.hass.localize(
"ui.panel.lovelace.editor.raw_editor.confirm_unsaved_comments"
)
)
) {
return;
}
}
let config: LovelaceConfig;
try {
config = load(value) as LovelaceConfig;
} catch (err: any) {
showAlertDialog(this, {
text: this.hass.localize(
"ui.panel.lovelace.editor.raw_editor.error_parse_yaml",
"error",
err
),
});
this._saving = false;
return;
}
try {
assert(config, lovelaceStruct);
} catch (err: any) {
showAlertDialog(this, {
text: this.hass.localize(
"ui.panel.lovelace.editor.raw_editor.error_invalid_config",
"error",
err
),
});
return;
}
// @ts-ignore
if (config.resources) {
showAlertDialog(this, {
text: this.hass.localize(
"ui.panel.lovelace.editor.raw_editor.resources_moved"
),
});
}
try {
await this.lovelace!.saveConfig(config);
} catch (err: any) {
showAlertDialog(this, {
text: this.hass.localize(
"ui.panel.lovelace.editor.raw_editor.error_save_yaml",
"error",
err
),
});
}
window.onbeforeunload = null;
this._changed = false;
this._saving = false;
}
private get yamlEditor(): HaCodeEditor {
return this.shadowRoot!.querySelector("ha-code-editor")! as HaCodeEditor;
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-editor": LovelaceFullConfigEditor;
}
}