diff --git a/src/data/lovelace.ts b/src/data/lovelace.ts index 06c1294053..fadce4b31b 100644 --- a/src/data/lovelace.ts +++ b/src/data/lovelace.ts @@ -53,6 +53,7 @@ interface LovelaceGenericDashboard { show_in_sidebar: boolean; icon?: string; title: string; + hide_header?: boolean; } export interface LovelaceYamlDashboard extends LovelaceGenericDashboard { @@ -69,6 +70,7 @@ export interface LovelaceDashboardMutableParams { show_in_sidebar: boolean; icon?: string; title: string; + hide_header?: boolean; } export interface LovelaceDashboardCreateParams diff --git a/src/panels/config/lovelace/dashboards/dialog-lovelace-dashboard-detail.ts b/src/panels/config/lovelace/dashboards/dialog-lovelace-dashboard-detail.ts index f48ef1f439..982405ea23 100644 --- a/src/panels/config/lovelace/dashboards/dialog-lovelace-dashboard-detail.ts +++ b/src/panels/config/lovelace/dashboards/dialog-lovelace-dashboard-detail.ts @@ -45,6 +45,7 @@ export class DialogLovelaceDashboardDetail extends LitElement { title: "", require_admin: false, mode: "storage", + hide_header: false, }; } } @@ -194,6 +195,13 @@ export class DialogLovelaceDashboardDetail extends LitElement { boolean: {}, }, }, + { + name: "hide_header", + required: true, + selector: { + boolean: {}, + }, + }, ].filter(Boolean) ); @@ -270,6 +278,7 @@ export class DialogLovelaceDashboardDetail extends LitElement { show_in_sidebar: this._data!.show_in_sidebar, icon: this._data!.icon || undefined, title: this._data!.title, + hide_header: this._data!.hide_header || false, }; await this._params!.updateDashboard(values); } else { diff --git a/src/panels/config/lovelace/dashboards/ha-config-lovelace-dashboards.ts b/src/panels/config/lovelace/dashboards/ha-config-lovelace-dashboards.ts index b17cc9cbd1..b4fd422f1f 100644 --- a/src/panels/config/lovelace/dashboards/ha-config-lovelace-dashboards.ts +++ b/src/panels/config/lovelace/dashboards/ha-config-lovelace-dashboards.ts @@ -3,6 +3,7 @@ import { mdiCheckCircleOutline, mdiDotsVertical, mdiOpenInNew, + mdiPencil, mdiPlus, } from "@mdi/js"; import "@polymer/paper-tooltip/paper-tooltip"; @@ -13,10 +14,8 @@ import memoize from "memoize-one"; import { isComponentLoaded } from "../../../../common/config/is_component_loaded"; import { navigate } from "../../../../common/navigate"; import { stringCompare } from "../../../../common/string/compare"; -import { - DataTableColumnContainer, - RowClickedEvent, -} from "../../../../components/data-table/ha-data-table"; +import { addSearchParam } from "../../../../common/url/search-params"; +import { DataTableColumnContainer } from "../../../../components/data-table/ha-data-table"; import "../../../../components/ha-clickable-list-item"; import "../../../../components/ha-fab"; import "../../../../components/ha-icon"; @@ -31,7 +30,10 @@ import { LovelacePanelConfig, updateDashboard, } from "../../../../data/lovelace"; -import { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog-box"; +import { + showAlertDialog, + showConfirmationDialog, +} from "../../../../dialogs/generic/show-dialog-box"; import "../../../../layouts/hass-loading-screen"; import "../../../../layouts/hass-tabs-subpage-data-table"; import { HomeAssistant, Route } from "../../../../types"; @@ -42,11 +44,11 @@ import { showDashboardDetailDialog } from "./show-dialog-lovelace-dashboard-deta export class HaConfigLovelaceDashboards extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; - @property() public isWide!: boolean; + @property({ type: Boolean }) public isWide!: boolean; - @property() public narrow!: boolean; + @property({ type: Boolean }) public narrow!: boolean; - @property() public route!: Route; + @property({ attribute: false }) public route!: Route; @state() private _dashboards: LovelaceDashboard[] = []; @@ -192,6 +194,37 @@ export class HaConfigLovelaceDashboards extends LitElement { `, }; + columns.edit_path = { + title: "", + label: this.hass.localize( + "ui.panel.config.lovelace.dashboards.picker.headers.edit" + ), + width: "100px", + template: (edit_path, dashboard) => + narrow + ? html` + + ` + : html` + ${this.hass.localize( + "ui.panel.config.lovelace.dashboards.picker.edit" + )} + `, + }; + return columns; } ); @@ -210,6 +243,7 @@ export class HaConfigLovelaceDashboards extends LitElement { show_in_sidebar: isDefault, require_admin: false, url_path: "lovelace", + edit_path: "lovelace?edit=1", mode: defaultMode, filename: defaultMode === "yaml" ? "ui-lovelace.yaml" : "", iconColor: "var(--primary-color)", @@ -222,6 +256,7 @@ export class HaConfigLovelaceDashboards extends LitElement { show_in_sidebar: true, mode: "storage", url_path: "energy", + edit_path: "config/energy", filename: "", iconColor: "var(--label-badge-yellow)", }); @@ -232,6 +267,7 @@ export class HaConfigLovelaceDashboards extends LitElement { .sort((a, b) => stringCompare(a.title, b.title)) .map((dashboard) => ({ filename: "", + edit_path: `${dashboard.url_path}?${addSearchParam({ edit: "1" })}`, ...dashboard, default: defaultUrlPath === dashboard.url_path, })) @@ -257,10 +293,7 @@ export class HaConfigLovelaceDashboards extends LitElement { this._dashboards )} .data=${this._getItems(this._dashboards)} - @row-click=${this._editDashboard} - id="url_path" hasFab - clickable > ${this.hass.userData?.showAdvanced ? html` @@ -317,14 +350,13 @@ export class HaConfigLovelaceDashboards extends LitElement { } private _editDashboard(ev: CustomEvent) { - const urlPath = (ev.detail as RowClickedEvent).id; - - if (urlPath === "energy") { - navigate("/config/energy"); + if ((ev.detail as any).mode === "yaml") { + showAlertDialog(this, { + text: "The edit UI is not available when in YAML mode.", + }); return; } - const dashboard = this._dashboards.find((res) => res.url_path === urlPath); - this._openDialog(dashboard, urlPath); + navigate(`/${(ev.target as any).urlPath}`); } private _addDashboard() { @@ -387,3 +419,9 @@ export class HaConfigLovelaceDashboards extends LitElement { ev.currentTarget.blur(); } } + +declare global { + interface HTMLElementTagNameMap { + "ha-config-lovelace-dashboards": HaConfigLovelaceDashboards; + } +} diff --git a/src/panels/lovelace/editor/lovelace-editor/hui-dialog-edit-lovelace.ts b/src/panels/lovelace/editor/lovelace-editor/hui-dialog-edit-lovelace.ts index 5974cddd1d..c8d946e853 100644 --- a/src/panels/lovelace/editor/lovelace-editor/hui-dialog-edit-lovelace.ts +++ b/src/panels/lovelace/editor/lovelace-editor/hui-dialog-edit-lovelace.ts @@ -1,14 +1,25 @@ import "@material/mwc-button"; import { CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property, state } from "lit/decorators"; +import memoizeOne from "memoize-one"; import { fireEvent } from "../../../../common/dom/fire_event"; +import { slugify } from "../../../../common/string/slugify"; import "../../../../components/ha-circular-progress"; -import type { LovelaceConfig } from "../../../../data/lovelace"; +import "../../../../components/ha-dialog"; +import "../../../../components/ha-form/ha-form"; +import { HaFormSchema } from "../../../../components/ha-form/types"; +import { CoreFrontendUserData } from "../../../../data/frontend"; +import { + LovelaceConfig, + LovelaceDashboard, + LovelaceDashboardMutableParams, + updateDashboard, +} from "../../../../data/lovelace"; import { haStyleDialog } from "../../../../resources/styles"; import type { HomeAssistant } from "../../../../types"; +import { LovelaceDashboardDetailsDialogParams } from "../../../config/lovelace/dashboards/show-dialog-lovelace-dashboard-detail"; import type { Lovelace } from "../../types"; import "./hui-lovelace-editor"; -import "../../../../components/ha-dialog"; @customElement("hui-dialog-edit-lovelace") export class HuiDialogEditLovelace extends LitElement { @@ -18,7 +29,15 @@ export class HuiDialogEditLovelace extends LitElement { @state() private _config?: LovelaceConfig; - private _saving = false; + @state() private _params?: LovelaceDashboardDetailsDialogParams; + + @state() private _urlPathChanged = false; + + @state() private _data?: Partial; + + @state() private _error?: Record; + + @state() private _submitting = false; public showDialog(lovelace: Lovelace): void { this._lovelace = lovelace; @@ -32,9 +51,10 @@ export class HuiDialogEditLovelace extends LitElement { } protected render(): TemplateResult { - if (!this._config) { + if (!this._config || !this.hass) { return html``; } + return html` + + ${this.hass!.localize("ui.common.cancel")} - ${this._saving + ${this._submitting ? html` + [ + { + name: "title", + required: true, + selector: { + text: {}, + }, + }, + { + name: "icon", + required: true, + selector: { + icon: {}, + }, + }, + !params.dashboard && + userData?.showAdvanced && { + name: "url_path", + required: true, + selector: { text: {} }, + }, + { + name: "require_admin", + required: true, + selector: { + boolean: {}, + }, + }, + { + name: "show_in_sidebar", + required: true, + selector: { + boolean: {}, + }, + }, + { + name: "hide_header", + required: true, + selector: { + boolean: {}, + }, + }, + ].filter(Boolean) + ); + + private _computeLabel = (entry: HaFormSchema): string => + this.hass!.localize( + `ui.panel.config.lovelace.dashboards.detail.${ + entry.name === "show_in_sidebar" + ? "show_sidebar" + : entry.name === "url_path" + ? "url" + : entry.name + }` + ); + + private _valueChanged(ev: CustomEvent) { + this._error = undefined; + const value = ev.detail.value; + if (value.url_path !== this._data?.url_path) { + this._urlPathChanged = true; + if ( + !value.url_path || + value.url_path === "lovelace" || + !/^[a-zA-Z0-9_-]+-[a-zA-Z0-9_-]+$/.test(value.url_path) + ) { + this._error = { + url_path: this.hass!.localize( + "ui.panel.config.lovelace.dashboards.detail.url_error_msg" + ), + }; + } + } + if (value.title !== this._data?.title) { + this._data = value; + this._fillUrlPath(value.title); + } else { + this._data = value; + } } + + private _fillUrlPath(title: string) { + if ((this.hass!.userData?.showAdvanced && this._urlPathChanged) || !title) { + return; + } + + const slugifyTitle = slugify(title, "-"); + this._data = { + ...this._data, + url_path: slugifyTitle.includes("-") + ? slugifyTitle + : `lovelace-${slugifyTitle}`, + }; + } + + private async _updateDashboard() { + if (this._params?.urlPath && !this._params.dashboard?.id) { + this.closeDialog(); + } + this._submitting = true; + try { + const values: Partial = { + require_admin: this._data!.require_admin, + show_in_sidebar: this._data!.show_in_sidebar, + icon: this._data!.icon || undefined, + title: this._data!.title, + hide_header: this._data!.hide_header || false, + }; + await updateDashboard(this.hass!, "", values); + + this.closeDialog(); + } catch (err: any) { + this._error = { base: err?.message || "Unknown error" }; + } finally { + this._submitting = false; + } + } + + static styles: CSSResultGroup = haStyleDialog; } declare global { diff --git a/src/panels/lovelace/editor/lovelace-editor/hui-lovelace-editor.ts b/src/panels/lovelace/editor/lovelace-editor/hui-lovelace-editor.ts index cfe3627398..8bdfe47e1d 100644 --- a/src/panels/lovelace/editor/lovelace-editor/hui-lovelace-editor.ts +++ b/src/panels/lovelace/editor/lovelace-editor/hui-lovelace-editor.ts @@ -1,7 +1,7 @@ -import "../../../../components/ha-textfield"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property } from "lit/decorators"; import { fireEvent } from "../../../../common/dom/fire_event"; +import "../../../../components/ha-textfield"; import { LovelaceConfig } from "../../../../data/lovelace"; import { HomeAssistant } from "../../../../types"; import { EditorTarget } from "../types"; @@ -18,7 +18,7 @@ declare global { export class HuiLovelaceEditor extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; - @property() public config?: LovelaceConfig; + @property({ attribute: false }) public config?: LovelaceConfig; get _title(): string { if (!this.config) { diff --git a/src/panels/lovelace/hui-root.ts b/src/panels/lovelace/hui-root.ts index 83e2ebdfd6..32ddab216b 100644 --- a/src/panels/lovelace/hui-root.ts +++ b/src/panels/lovelace/hui-root.ts @@ -57,10 +57,7 @@ import type { LovelacePanelConfig, LovelaceViewConfig, } from "../../data/lovelace"; -import { - showAlertDialog, - showConfirmationDialog, -} from "../../dialogs/generic/show-dialog-box"; +import { showConfirmationDialog } from "../../dialogs/generic/show-dialog-box"; import { showQuickBar } from "../../dialogs/quick-bar/show-dialog-quick-bar"; import { showVoiceCommandDialog } from "../../dialogs/voice-command-dialog/show-ha-voice-command-dialog"; import "../../layouts/ha-app-layout"; @@ -108,6 +105,8 @@ class HUIRoot extends LitElement { } protected render(): TemplateResult { + const currentPanel = this.hass.panels[this.route!.prefix.substring(1)]!; + return html` - - ${this._editMode - ? html` - -
- ${this.config.title || - this.hass!.localize("ui.panel.lovelace.editor.header")} - -
- - - - - - - ${__DEMO__ /* No unused entities available in the demo */ - ? "" - : html` - - - - ${this.hass!.localize( - "ui.panel.lovelace.unused_entities.title" - )} - - `} - - - ${this.hass!.localize( - "ui.panel.lovelace.editor.menu.raw_editor" - )} - - ${__DEMO__ /* No config available in the demo */ - ? "" - : html` - - ${this.hass!.localize( - "ui.panel.lovelace.editor.menu.manage_dashboards" - )} - - ${this.hass.userData?.showAdvanced - ? html` - - ${this.hass!.localize( - "ui.panel.lovelace.editor.menu.manage_resources" - )} - ` - : ""} `} - -
- ` - : html` - - - ${this.lovelace!.config.views.length > 1 - ? html` - - ${this.lovelace!.config.views.map( - (view) => html` - e.user === this.hass!.user!.id - )) || - view.visible === false) - ), - })} - > - ${view.icon - ? html` - - ` - : view.title || "Unnamed view"} - - ` + ${this._editMode || !currentPanel.hide_header + ? html` + + ${this._editMode + ? html` + +
+ ${this.config.title || + this.hass!.localize( + "ui.panel.lovelace.editor.header" )} - - ` - : html`
${this.config.title}
`} - ${!this.narrow - ? html` - - ` - : ""} - ${!this.narrow && - this._conversation(this.hass.config.components) - ? html` - +
+ - ` - : ""} - ${this._showButtonMenu - ? html` + @click=${this._editModeDisable} + > + + + - - ${this.narrow - ? html` + ${__DEMO__ /* No unused entities available in the demo */ + ? "" + : html` - ${this.hass!.localize( - "ui.panel.lovelace.menu.search" - )} - - - ` - : ""} - ${this.narrow && - this._conversation(this.hass.config.components) - ? html` - - ${this.hass!.localize( - "ui.panel.lovelace.menu.start_conversation" - )} - - - ` - : ""} - ${this._yamlMode - ? html` - - ${this.hass!.localize( - "ui.common.refresh" - )} - - - - ${this.hass!.localize( - "ui.panel.lovelace.unused_entities.title" - )} - - - ` - : ""} - ${( - this.hass.panels.lovelace - ?.config as LovelacePanelConfig - )?.mode === "yaml" - ? html` - + ${this.hass!.localize( - "ui.panel.lovelace.menu.reload_resources" + "ui.panel.lovelace.unused_entities.title" )} - - ` - : ""} - ${this.hass!.user?.is_admin && - !this.hass!.config.safe_mode - ? html` - - ${this.hass!.localize( - "ui.panel.lovelace.menu.configure_ui" - )} - - - ` - : ""} - ${this._editMode - ? html` - - - ${this.hass!.localize( - "ui.panel.lovelace.menu.help" - )} - - - - ` - : ""} - - ` - : ""} -
- `} - ${this._editMode - ? html` -
- - ${this.lovelace!.config.views.map( - (view) => html` - e.user === this.hass!.user!.id - )) || - view.visible === false) - ), - })} - > - ${this._editMode - ? html` - - ` - : ""} - ${view.icon - ? html` - - ` - : view.title || "Unnamed view"} - ${this._editMode - ? html` - - - ` - : ""} - - ` - )} - ${this._editMode - ? html` - + + ${this.hass!.localize( + "ui.panel.lovelace.editor.menu.raw_editor" )} - .path=${mdiPlus} - > - ` - : ""} - -
- ` - : ""} -
+ + ${__DEMO__ /* No config available in the demo */ + ? "" + : html` + + ${this.hass!.localize( + "ui.panel.lovelace.editor.menu.manage_dashboards" + )} + + ${this.hass.userData?.showAdvanced + ? html` + + ${this.hass!.localize( + "ui.panel.lovelace.editor.menu.manage_resources" + )} + ` + : ""} `} + +
+ ` + : html` + + + ${this.lovelace!.config.views.length > 1 + ? html` + + ${this.lovelace!.config.views.map( + (view) => html` + + e.user === this.hass!.user!.id + )) || + view.visible === false) + ), + })} + > + ${view.icon + ? html` + + ` + : view.title || "Unnamed view"} + + ` + )} + + ` + : html`
${this.config.title}
`} + ${!this.narrow + ? html` + + ` + : ""} + ${!this.narrow && + this._conversation(this.hass.config.components) + ? html` + + ` + : ""} + ${this._showButtonMenu + ? html` + + + + ${this.narrow + ? html` + + ${this.hass!.localize( + "ui.panel.lovelace.menu.search" + )} + + + ` + : ""} + ${this.narrow && + this._conversation(this.hass.config.components) + ? html` + + ${this.hass!.localize( + "ui.panel.lovelace.menu.start_conversation" + )} + + + ` + : ""} + ${this._yamlMode + ? html` + + ${this.hass!.localize( + "ui.common.refresh" + )} + + + + ${this.hass!.localize( + "ui.panel.lovelace.unused_entities.title" + )} + + + ` + : ""} + ${( + this.hass.panels.lovelace + ?.config as LovelacePanelConfig + )?.mode === "yaml" + ? html` + + ${this.hass!.localize( + "ui.panel.lovelace.menu.reload_resources" + )} + + + ` + : ""} + ${this._editMode + ? html` + + + ${this.hass!.localize( + "ui.panel.lovelace.menu.help" + )} + + + + ` + : ""} + + ` + : ""} +
+ `} + ${this._editMode + ? html` +
+ + ${this.lovelace!.config.views.map( + (view) => html` + e.user === this.hass!.user!.id + )) || + view.visible === false) + ), + })} + > + ${this._editMode + ? html` + + ` + : ""} + ${view.icon + ? html` + + ` + : view.title || "Unnamed view"} + ${this._editMode + ? html` + + + ` + : ""} + + ` + )} + ${this._editMode + ? html` + + ` + : ""} + +
+ ` + : ""} +
+ ` + : ""} +
`; @@ -674,7 +664,6 @@ class HUIRoot extends LitElement { return ( (this.narrow && this._conversation(this.hass.config.components)) || this._editMode || - (this.hass!.user?.is_admin && !this.hass!.config.safe_mode) || (this.hass.panels.lovelace?.config as LovelacePanelConfig)?.mode === "yaml" || this._yamlMode @@ -747,19 +736,6 @@ class HUIRoot extends LitElement { showVoiceCommandDialog(this); } - private _handleEnableEditMode(ev: CustomEvent): void { - if (!shouldHandleRequestSelectedEvent(ev)) { - return; - } - if (this._yamlMode) { - showAlertDialog(this, { - text: "The edit UI is not available when in YAML mode.", - }); - return; - } - this.lovelace!.setEditMode(true); - } - private _editModeDisable(): void { this.lovelace!.setEditMode(false); } diff --git a/src/translations/en.json b/src/translations/en.json index 649aea8f90..6cbac8b7df 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -1621,9 +1621,11 @@ "require_admin": "Admin only", "sidebar": "Show in sidebar", "filename": "Filename", - "url": "Open" + "url": "Open", + "edit": "Edit" }, "open": "Open", + "edit": "Edit", "add_dashboard": "Add dashboard" }, "confirm_delete_title": "Delete {dashboard_title}?", @@ -1644,6 +1646,7 @@ "delete": "Delete", "update": "Update", "create": "Create", + "hide_header": "Hide header", "set_default": "Set as default on this device", "remove_default": "Remove as default on this device" } diff --git a/src/types.ts b/src/types.ts index b6bab9a982..ee424466b1 100644 --- a/src/types.ts +++ b/src/types.ts @@ -103,6 +103,7 @@ export interface PanelInfo | null> { icon: string | null; title: string | null; url_path: string; + hide_header: boolean; } export interface Panels {