Add edit/add/delete view (#2172)

* Add edit/add/delete view

* Add delete

* Comments

* Lint

* Fix delete with numeric ids

* fix translations

* add translations
This commit is contained in:
Bram Kragten 2018-12-04 16:49:12 +01:00 committed by Paulus Schoutsen
parent 77711ea711
commit f680832f78
11 changed files with 535 additions and 25 deletions

View File

@ -12,6 +12,7 @@ export interface LovelaceViewConfig {
cards?: LovelaceCardConfig[];
id?: string;
icon?: string;
theme?: string;
}
export interface LovelaceCardConfig {
@ -84,3 +85,36 @@ export const addCard = (
card_config: config,
format,
});
export const updateViewConfig = (
hass: HomeAssistant,
viewId: string,
config: LovelaceViewConfig | string,
format: "json" | "yaml"
): Promise<void> =>
hass.callWS({
type: "lovelace/config/view/update",
view_id: viewId,
view_config: config,
format,
});
export const deleteView = (
hass: HomeAssistant,
viewId: string
): Promise<void> =>
hass.callWS({
type: "lovelace/config/view/delete",
view_id: viewId,
});
export const addView = (
hass: HomeAssistant,
config: LovelaceViewConfig | string,
format: "json" | "yaml"
): Promise<void> =>
hass.callWS({
type: "lovelace/config/view/add",
view_config: config,
format,
});

View File

@ -3,6 +3,7 @@ import "@polymer/paper-button/paper-button";
import { fireEvent } from "../../../common/dom/fire_event";
import { showEditCardDialog } from "../editor/hui-dialog-edit-card";
import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin";
import { confDeleteCard } from "../editor/delete-card";
import { HomeAssistant } from "../../../types";
import { LovelaceCardConfig } from "../../../data/lovelace";
@ -19,7 +20,7 @@ declare global {
}
}
export class HuiCardOptions extends LitElement {
export class HuiCardOptions extends hassLocalizeLitMixin(LitElement) {
public cardConfig?: LovelaceCardConfig;
protected hass?: HomeAssistant;
@ -50,8 +51,14 @@ export class HuiCardOptions extends LitElement {
<slot></slot>
<div>
<paper-button class="warning" @click="${this._deleteCard}"
>DELETE</paper-button
><paper-button @click="${this._editCard}">EDIT</paper-button>
>${
this.localize("ui.panel.lovelace.editor.edit_card.delete")
}</paper-button
><paper-button @click="${this._editCard}"
>${
this.localize("ui.panel.lovelace.editor.edit_card.edit")
}</paper-button
>
</div>
`;
}

View File

@ -10,7 +10,7 @@ export async function confDeleteCard(
return;
}
try {
await deleteCard(hass, cardId);
await deleteCard(hass, String(cardId));
reloadLovelace();
} catch (err) {
alert(`Deleting failed: ${err.message}`);

View File

@ -0,0 +1,18 @@
import { deleteView } from "../../../data/lovelace";
import { HomeAssistant } from "../../../types";
export async function confDeleteView(
hass: HomeAssistant,
viewId: string,
reloadLovelace: () => void
): Promise<void> {
if (!confirm("Are you sure you want to delete this view?")) {
return;
}
try {
await deleteView(hass, String(viewId));
reloadLovelace();
} catch (err) {
alert(`Deleting failed: ${err.message}`);
}
}

View File

@ -7,6 +7,8 @@ import { fireEvent } from "../../../common/dom/fire_event";
import { LovelaceCardConfig } from "../../../data/lovelace";
import { getCardElementTag } from "../common/get-card-element-tag";
import { CardPickTarget } from "./types";
import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin";
import { uid } from "../../../common/util/uid";
declare global {
@ -44,13 +46,13 @@ const cards = [
{ name: "Weather Forecast", type: "weather-forecast" },
];
export class HuiCardPicker extends LitElement {
export class HuiCardPicker extends hassLocalizeLitMixin(LitElement) {
protected hass?: HomeAssistant;
protected render(): TemplateResult {
return html`
${this.renderStyle()}
<h3>Pick the card you want to add:</h3>
<h3>${this.localize("ui.panel.lovelace.editor.edit_card.pick_card")}</h3>
<div class="cards-container">
${
cards.map((card) => {

View File

@ -0,0 +1,101 @@
import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element";
import { TemplateResult } from "lit-html";
import { HomeAssistant } from "../../../types";
import { fireEvent, HASSDomEvent } from "../../../common/dom/fire_event";
import { LovelaceViewConfig } from "../../../data/lovelace";
import "./hui-edit-view";
import "./hui-migrate-config";
declare global {
// for fire event
interface HASSDomEvents {
"reload-lovelace": undefined;
"show-edit-view": EditViewDialogParams;
}
// for add event listener
interface HTMLElementEventMap {
"reload-lovelace": HASSDomEvent<undefined>;
}
}
let registeredDialog = false;
const dialogShowEvent = "show-edit-view";
const dialogTag = "hui-dialog-edit-view";
export interface EditViewDialogParams {
viewConfig?: LovelaceViewConfig;
add?: boolean;
reloadLovelace: () => void;
}
const registerEditViewDialog = (element: HTMLElement) =>
fireEvent(element, "register-dialog", {
dialogShowEvent,
dialogTag,
dialogImport: () => import("./hui-dialog-edit-view"),
});
export const showEditViewDialog = (
element: HTMLElement,
editViewDialogParams: EditViewDialogParams
) => {
if (!registeredDialog) {
registeredDialog = true;
registerEditViewDialog(element);
}
fireEvent(element, dialogShowEvent, editViewDialogParams);
};
export class HuiDialogEditView extends LitElement {
protected hass?: HomeAssistant;
private _params?: EditViewDialogParams;
static get properties(): PropertyDeclarations {
return {
hass: {},
_params: {},
};
}
public async showDialog(params: EditViewDialogParams): Promise<void> {
this._params = params;
await this.updateComplete;
(this.shadowRoot!.children[0] as any).showDialog();
}
protected render(): TemplateResult {
if (!this._params) {
return html``;
}
if (
!this._params.add &&
this._params.viewConfig &&
!this._params.viewConfig.id
) {
return html`
<hui-migrate-config
.hass="${this.hass}"
@reload-lovelace="${this._params.reloadLovelace}"
></hui-migrate-config>
`;
}
return html`
<hui-edit-view
.hass="${this.hass}"
.viewConfig="${this._params.viewConfig}"
.add="${this._params.add}"
.reloadLovelace="${this._params.reloadLovelace}"
>
</hui-edit-view>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-dialog-edit-view": HuiDialogEditView;
}
}
customElements.define(dialogTag, HuiDialogEditView);

View File

@ -14,7 +14,6 @@ import "@polymer/paper-dialog/paper-dialog";
// tslint:disable-next-line
import { PaperDialogElement } from "@polymer/paper-dialog/paper-dialog";
import "@polymer/paper-button/paper-button";
import "@polymer/paper-input/paper-textarea";
import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable";
import { HomeAssistant } from "../../../types";
import {
@ -108,7 +107,7 @@ export class HuiEditCard extends hassLocalizeLitMixin(LitElement) {
this._dialog.open();
}
protected async updated(changedProperties: PropertyValues): Promise<void> {
protected updated(changedProperties: PropertyValues): void {
super.updated(changedProperties);
if (
!changedProperties.has("cardConfig") &&
@ -170,7 +169,7 @@ export class HuiEditCard extends hassLocalizeLitMixin(LitElement) {
return html`
${this.renderStyle()}
<paper-dialog with-backdrop>
<h2>${this.localize("ui.panel.lovelace.editor.edit.header")}</h2>
<h2>${this.localize("ui.panel.lovelace.editor.edit_card.header")}</h2>
<paper-spinner
?active="${this._loading}"
alt="Loading"
@ -200,7 +199,7 @@ export class HuiEditCard extends hassLocalizeLitMixin(LitElement) {
@click="${this._toggleEditor}"
>${
this.localize(
"ui.panel.lovelace.editor.edit.toggle_editor"
"ui.panel.lovelace.editor.edit_card.toggle_editor"
)
}</paper-button
>
@ -216,9 +215,7 @@ export class HuiEditCard extends hassLocalizeLitMixin(LitElement) {
?active="${this._saving}"
alt="Saving"
></paper-spinner>
${
this.localize("ui.panel.lovelace.editor.edit.save")
}</paper-button
${this.localize("ui.common.save")}</paper-button
>
</div>
`
@ -331,6 +328,7 @@ export class HuiEditCard extends hassLocalizeLitMixin(LitElement) {
this._configValue!.format
);
}
fireEvent(this, "reload-lovelace");
this._closeDialog();
this._saveDone();
} catch (err) {

View File

@ -0,0 +1,264 @@
import {
html,
LitElement,
PropertyDeclarations,
PropertyValues,
} from "@polymer/lit-element";
import { TemplateResult } from "lit-html";
import "@polymer/paper-spinner/paper-spinner";
import "@polymer/paper-dialog/paper-dialog";
// 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 "@polymer/paper-button/paper-button";
import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable";
import "../components/hui-theme-select-editor";
import { HomeAssistant } from "../../../types";
import {
addView,
updateViewConfig,
LovelaceViewConfig,
} from "../../../data/lovelace";
import { fireEvent } from "../../../common/dom/fire_event";
import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin";
import { EditorTarget } from "./types";
export class HuiEditView extends hassLocalizeLitMixin(LitElement) {
static get properties(): PropertyDeclarations {
return {
hass: {},
viewConfig: {},
add: {},
_config: {},
_saving: {},
};
}
public viewConfig?: LovelaceViewConfig;
public add?: boolean;
public reloadLovelace?: () => {};
protected hass?: HomeAssistant;
private _config?: LovelaceViewConfig;
private _saving: boolean;
protected constructor() {
super();
this._saving = false;
}
public async showDialog(): Promise<void> {
// Wait till dialog is rendered.
if (this._dialog == null) {
await this.updateComplete;
}
this._dialog.open();
}
protected updated(changedProperties: PropertyValues): void {
super.updated(changedProperties);
if (!changedProperties.has("viewConfig") && !changedProperties.has("add")) {
return;
}
if (
this.viewConfig &&
(!changedProperties.get("viewConfig") ||
this.viewConfig.id !==
(changedProperties.get("viewConfig") as LovelaceViewConfig).id)
) {
this._config = this.viewConfig;
} else if (changedProperties.has("add")) {
this._config = { cards: [] };
}
this._resizeDialog();
}
private get _dialog(): PaperDialogElement {
return this.shadowRoot!.querySelector("paper-dialog")!;
}
get _id(): string {
if (!this._config) {
return "";
}
return this._config.id || "";
}
get _title(): string {
if (!this._config) {
return "";
}
return this._config.title || "";
}
get _icon(): string {
if (!this._config) {
return "";
}
return this._config.icon || "";
}
get _theme(): string {
if (!this._config) {
return "";
}
return this._config.theme || "Backend-selected";
}
protected render(): TemplateResult {
return html`
${this.renderStyle()}
<paper-dialog with-backdrop>
<h2>${this.localize("ui.panel.lovelace.editor.edit_view.header")}</h2>
<paper-dialog-scrollable>
<div class="card-config">
<paper-input
label="ID"
value="${this._id}"
.configValue="${"id"}"
@value-changed="${this._valueChanged}"
></paper-input>
<paper-input
label="Title"
value="${this._title}"
.configValue="${"title"}"
@value-changed="${this._valueChanged}"
></paper-input>
<paper-input
label="Icon"
value="${this._icon}"
.configValue="${"icon"}"
@value-changed="${this._valueChanged}"
></paper-input>
<hui-theme-select-editor
.hass="${this.hass}"
.value="${this._theme}"
.configValue="${"theme"}"
@theme-changed="${this._valueChanged}"
></hui-theme-select-editor>
</div>
</paper-dialog-scrollable>
<div class="paper-dialog-buttons">
<paper-button @click="${this._closeDialog}"
>${this.localize("ui.common.cancel")}</paper-button
>
<paper-button
?disabled="${!this._config || this._saving}"
@click="${this._save}"
>
<paper-spinner
?active="${this._saving}"
alt="Saving"
></paper-spinner>
${this.localize("ui.common.save")}</paper-button
>
</div>
</paper-dialog>
`;
}
private renderStyle(): TemplateResult {
return html`
<style>
paper-dialog {
width: 650px;
}
paper-button paper-spinner {
width: 14px;
height: 14px;
margin-right: 20px;
}
paper-spinner {
display: none;
}
paper-spinner[active] {
display: block;
}
.hidden {
display: none;
}
.error {
color: #ef5350;
border-bottom: 1px solid #ef5350;
}
</style>
`;
}
private _save(): void {
this._saving = true;
this._updateConfigInBackend();
}
private async _resizeDialog(): Promise<void> {
await this.updateComplete;
fireEvent(this._dialog, "iron-resize");
}
private _closeDialog(): void {
this._config = { cards: [] };
this.viewConfig = undefined;
this._dialog.close();
}
private async _updateConfigInBackend(): Promise<void> {
if (!this._isConfigChanged()) {
this._closeDialog();
this._saving = false;
return;
}
try {
if (this.add) {
await addView(this.hass!, this._config!, "json");
} else {
await updateViewConfig(
this.hass!,
this.viewConfig!.id!,
this._config!,
"json"
);
}
this.reloadLovelace!();
this._closeDialog();
this._saving = false;
} catch (err) {
alert(`Saving failed: ${err.message}`);
this._saving = false;
}
}
private _valueChanged(ev: Event): void {
if (!this._config || !this.hass) {
return;
}
const target = ev.currentTarget! as EditorTarget;
if (this[`_${target.configValue}`] === target.value) {
return;
}
if (target.configValue) {
this._config = {
...this._config,
[target.configValue]: target.value,
};
}
}
private _isConfigChanged(): boolean {
if (!this.add) {
return true;
}
return JSON.stringify(this._config) !== JSON.stringify(this.viewConfig);
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-edit-view": HuiEditView;
}
}
customElements.define("hui-edit-view", HuiEditView);

View File

@ -4,6 +4,7 @@ import "@polymer/app-layout/app-scroll-effects/effects/waterfall";
import "@polymer/app-layout/app-toolbar/app-toolbar";
import "@polymer/app-route/app-route";
import "@polymer/paper-icon-button/paper-icon-button";
import "@polymer/paper-button/paper-button";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
import "@polymer/paper-menu-button/paper-menu-button";
@ -16,6 +17,7 @@ import { PolymerElement } from "@polymer/polymer/polymer-element";
import scrollToTarget from "../../common/dom/scroll-to-target";
import EventsMixin from "../../mixins/events-mixin";
import localizeMixin from "../../mixins/localize-mixin";
import NavigateMixin from "../../mixins/navigate-mixin";
import "../../layouts/ha-app-layout";
@ -31,12 +33,16 @@ import "./hui-view";
import debounce from "../../common/util/debounce";
import createCardElement from "./common/create-card-element";
import { showSaveDialog } from "./editor/hui-dialog-save-config";
import { showEditViewDialog } from "./editor/hui-dialog-edit-view";
import { confDeleteView } from "./editor/delete-view";
// CSS and JS should only be imported once. Modules and HTML are safe.
const CSS_CACHE = {};
const JS_CACHE = {};
class HUIRoot extends NavigateMixin(EventsMixin(PolymerElement)) {
class HUIRoot extends NavigateMixin(
EventsMixin(localizeMixin(PolymerElement))
) {
static get template() {
return html`
<style include='ha-style'>
@ -54,9 +60,24 @@ class HUIRoot extends NavigateMixin(EventsMixin(PolymerElement)) {
--paper-tabs-selection-bar-color: var(--text-primary-color, #FFF);
text-transform: uppercase;
}
#add-view {
background: var(--paper-fab-background, var(--accent-color));
position: absolute;
height: 44px;
}
app-toolbar a {
color: var(--text-primary-color, white);
}
paper-button.warning:not([disabled]) {
color: var(--google-red-500);
}
app-toolbar.secondary {
background-color: var(--light-primary-color);
color: var(--primary-text-color, #333);
font-size: 14px;
font-weight: 500;
height: auto;
}
#view {
min-height: calc(100vh - 112px);
/**
@ -103,7 +124,7 @@ class HUIRoot extends NavigateMixin(EventsMixin(PolymerElement)) {
<paper-listbox on-iron-select="_deselect" slot="dropdown-content">
<paper-item on-click="_handleRefresh">Refresh</paper-item>
<paper-item on-click="_handleUnusedEntities">Unused entities</paper-item>
<paper-item on-click="_editModeEnable">Configure UI (alpha)</paper-item>
<paper-item on-click="_editModeEnable">[[localize("ui.panel.lovelace.editor.configure_ui")]] (alpha)</paper-item>
<paper-item on-click="_handleHelp">Help</paper-item>
</paper-listbox>
</paper-menu-button>
@ -115,7 +136,7 @@ class HUIRoot extends NavigateMixin(EventsMixin(PolymerElement)) {
icon='hass:close'
on-click='_editModeDisable'
></paper-icon-button>
<div main-title>Edit UI</div>
<div main-title>[[localize("ui.panel.lovelace.editor.header")]]</div>
</app-toolbar>
</template>
@ -131,10 +152,20 @@ class HUIRoot extends NavigateMixin(EventsMixin(PolymerElement)) {
</template>
</paper-tab>
</template>
<template is='dom-if' if="[[_editMode]]">
<paper-button id="add-view" on-click="_addView">
<ha-icon title=[[localize("ui.panel.lovelace.editor.edit_view.add")]] icon="hass:plus"></ha-icon>
</paper-button>
</template>
</paper-tabs>
</div>
</app-header>
<template is='dom-if' if="[[_editMode]]">
<app-toolbar class="secondary">
<paper-button on-click="_editView">[[localize("ui.panel.lovelace.editor.edit_view.edit")]]</paper-button>
<paper-button class="warning" on-click="_deleteView">[[localize("ui.panel.lovelace.editor.edit_view.delete")]]</paper-button>
</app-toolbar>
</template>
<div id='view' on-rebuild-view='_debouncedConfigChanged'></div>
</app-header-layout>
`;
@ -295,10 +326,52 @@ class HUIRoot extends NavigateMixin(EventsMixin(PolymerElement)) {
this._selectView(this._curView);
}
_editView() {
const { cards, badges, ...viewConfig } = this.config.views[this._curView];
showEditViewDialog(this, {
viewConfig,
add: false,
reloadLovelace: () => {
this.fire("config-refresh");
},
});
}
_addView() {
showEditViewDialog(this, {
add: true,
reloadLovelace: () => {
this.fire("config-refresh");
},
});
}
_deleteView() {
const viewConfig = this.config.views[this._curView];
if (viewConfig.cards && viewConfig.cards.length > 0) {
alert(
"You can't delete a view that has card in them. Remove the cards first."
);
return;
}
if (!viewConfig.id) {
this._editView();
return;
}
confDeleteView(this.hass, viewConfig.id, () => {
this.fire("config-refresh");
this._navigateView(0);
});
}
_handleViewSelected(ev) {
const index = ev.detail.selected;
if (index !== this._curView) {
const id = this.config.views[index].id || index;
this._navigateView(index);
}
_navigateView(viewIndex) {
if (viewIndex !== this._curView) {
const id = this.config.views[viewIndex].id || viewIndex;
this.navigate(`/lovelace/${id}`);
}
scrollToTarget(this, this.$.layout.header.scrollTarget);

View File

@ -8,11 +8,12 @@ import "./components/hui-card-options";
import applyThemesOnElement from "../../common/dom/apply_themes_on_element";
import EventsMixin from "../../mixins/events-mixin";
import localizeMixin from "../../mixins/localize-mixin";
import createCardElement from "./common/create-card-element";
import { computeCardSize } from "./common/compute-card-size";
import { showEditCardDialog } from "./editor/hui-dialog-edit-card";
class HUIView extends EventsMixin(PolymerElement) {
class HUIView extends localizeMixin(EventsMixin(PolymerElement)) {
static get template() {
return html`
<style>
@ -83,7 +84,7 @@ class HUIView extends EventsMixin(PolymerElement) {
hidden$="{{!editMode}}"
elevated="2"
icon="hass:plus"
title="Add Card"
title=[[localize("ui.panel.lovelace.editor.edit_card.add")]]
on-click="_addCard"
></paper-fab>
`;

View File

@ -442,7 +442,8 @@
},
"common": {
"loading": "Loading",
"cancel": "Cancel"
"cancel": "Cancel",
"save": "Save"
},
"components": {
"entity": {
@ -767,10 +768,21 @@
}
},
"editor": {
"edit": {
"header": "Edit UI",
"configure_ui": "Configure UI",
"edit_view": {
"header": "View Configuration",
"add": "Add view",
"edit": "Edit view",
"delete": "Delete view"
},
"edit_card": {
"header": "Card Configuration",
"save": "Save",
"toggle_editor": "Toggle Editor"
"pick_card": "Pick the card you want to add.",
"toggle_editor": "Toggle Editor",
"add": "Add Card",
"edit": "Edit",
"delete": "Delete"
},
"save_config": {
"header": "Take control of your Lovelace UI",