From b82f1128fecacaa0e71dd8137d2c50509f7bd2a0 Mon Sep 17 00:00:00 2001 From: karwosts <32912880+karwosts@users.noreply.github.com> Date: Fri, 12 Apr 2024 11:51:23 -0700 Subject: [PATCH] Location/zone editor updates (#19994) --- .../ha-selector/ha-selector-location.ts | 86 +++++++++- src/data/selector.ts | 6 +- src/data/zone.ts | 5 + .../config/core/ha-config-section-general.ts | 40 ++--- .../config/zone/dialog-home-zone-detail.ts | 150 ++++++++++++++++++ src/panels/config/zone/dialog-zone-detail.ts | 34 +--- src/panels/config/zone/ha-config-zone.ts | 22 ++- .../zone/show-dialog-home-zone-detail.ts | 20 +++ src/translations/en.json | 12 +- 9 files changed, 308 insertions(+), 67 deletions(-) create mode 100644 src/panels/config/zone/dialog-home-zone-detail.ts create mode 100644 src/panels/config/zone/show-dialog-home-zone-detail.ts diff --git a/src/components/ha-selector/ha-selector-location.ts b/src/components/ha-selector/ha-selector-location.ts index 96d9d6629802..01fc052cb266 100644 --- a/src/components/ha-selector/ha-selector-location.ts +++ b/src/components/ha-selector/ha-selector-location.ts @@ -7,8 +7,10 @@ import type { LocationSelectorValue, } from "../../data/selector"; import type { HomeAssistant } from "../../types"; +import type { SchemaUnion } from "../ha-form/types"; import type { MarkerLocation } from "../map/ha-locations-editor"; import "../map/ha-locations-editor"; +import "../ha-form/ha-form"; @customElement("ha-selector-location") export class HaLocationSelector extends LitElement { @@ -24,6 +26,49 @@ export class HaLocationSelector extends LitElement { @property({ type: Boolean, reflect: true }) public disabled = false; + private _schema = memoizeOne( + (radius?: boolean, radius_readonly?: boolean) => + [ + { + name: "", + type: "grid", + schema: [ + { + name: "latitude", + required: true, + selector: { number: { step: "any" } }, + }, + { + name: "longitude", + required: true, + selector: { number: { step: "any" } }, + }, + ], + }, + ...(radius + ? [ + { + name: "radius", + required: true, + default: 1000, + disabled: !!radius_readonly, + selector: { number: { min: 0, step: 1, mode: "box" } as const }, + } as const, + ] + : []), + ] as const + ); + + protected willUpdate() { + if (!this.value) { + this.value = { + latitude: this.hass.config.latitude, + longitude: this.hass.config.longitude, + radius: this.selector.location?.radius ? 1000 : undefined, + }; + } + } + protected render() { return html`

${this.label ? this.label : ""}

@@ -35,6 +80,17 @@ export class HaLocationSelector extends LitElement { @location-updated=${this._locationChanged} @radius-updated=${this._radiusChanged} > + `; } @@ -66,7 +122,8 @@ export class HaLocationSelector extends LitElement { ? "mdi:map-marker-radius" : "mdi:map-marker", location_editable: true, - radius_editable: true, + radius_editable: + !!selector.location?.radius && !selector.location?.radius_readonly, }, ]; } @@ -80,14 +137,39 @@ export class HaLocationSelector extends LitElement { } private _radiusChanged(ev: CustomEvent) { - const radius = ev.detail.radius; + const radius = Math.round(ev.detail.radius); fireEvent(this, "value-changed", { value: { ...this.value, radius } }); } + private _valueChanged(ev: CustomEvent) { + ev.stopPropagation(); + const value = ev.detail.value; + const radius = Math.round(ev.detail.value.radius); + + fireEvent(this, "value-changed", { + value: { + latitude: value.latitude, + longitude: value.longitude, + ...(this.selector.location?.radius && + !this.selector.location?.radius_readonly + ? { + radius, + } + : {}), + }, + }); + } + + private _computeLabel = ( + entry: SchemaUnion> + ): string => + this.hass.localize(`ui.components.selectors.location.${entry.name}`); + static styles = css` ha-locations-editor { display: block; height: 400px; + margin-bottom: 16px; } p { margin-top: 0; diff --git a/src/data/selector.ts b/src/data/selector.ts index 557650635a12..f49eae11a1c3 100644 --- a/src/data/selector.ts +++ b/src/data/selector.ts @@ -270,7 +270,11 @@ export interface LanguageSelector { } export interface LocationSelector { - location: { radius?: boolean; icon?: string } | null; + location: { + radius?: boolean; + radius_readonly?: boolean; + icon?: string; + } | null; } export interface LocationSelectorValue { diff --git a/src/data/zone.ts b/src/data/zone.ts index 52ea733317b4..d59e33e87f6f 100644 --- a/src/data/zone.ts +++ b/src/data/zone.ts @@ -11,6 +11,11 @@ export interface Zone { radius?: number; } +export interface HomeZoneMutableParams { + latitude: number; + longitude: number; +} + export interface ZoneMutableParams { name: string; icon?: string; diff --git a/src/panels/config/core/ha-config-section-general.ts b/src/panels/config/core/ha-config-section-general.ts index 1a8b4a7263ae..438ff479b2c0 100644 --- a/src/panels/config/core/ha-config-section-general.ts +++ b/src/panels/config/core/ha-config-section-general.ts @@ -18,18 +18,20 @@ import "../../../components/ha-language-picker"; import "../../../components/ha-radio"; import type { HaRadio } from "../../../components/ha-radio"; import "../../../components/ha-select"; +import "../../../components/ha-selector/ha-selector-location"; +import type { LocationSelectorValue } from "../../../data/selector"; import "../../../components/ha-settings-row"; import "../../../components/ha-textfield"; import type { HaTextField } from "../../../components/ha-textfield"; import "../../../components/ha-timezone-picker"; -import "../../../components/map/ha-locations-editor"; -import type { MarkerLocation } from "../../../components/map/ha-locations-editor"; import { ConfigUpdateValues, saveCoreConfig } from "../../../data/core"; import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box"; import "../../../layouts/hass-subpage"; import { haStyle } from "../../../resources/styles"; import type { HomeAssistant, ValueChangedEvent } from "../../../types"; +const LOCATION_SELECTOR = { location: {} }; + @customElement("ha-config-section-general") class HaConfigSectionGeneral extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; @@ -244,15 +246,16 @@ class HaConfigSectionGeneral extends LitElement { ${this.narrow ? html` - + @value-changed=${this._locationChanged} + > ` : html` @@ -320,7 +323,7 @@ class HaConfigSectionGeneral extends LitElement { } private _locationChanged(ev: CustomEvent) { - this._location = ev.detail.location; + this._location = [ev.detail.value.latitude, ev.detail.value.longitude]; } private async _updateEntry(ev: CustomEvent) { @@ -381,19 +384,15 @@ class HaConfigSectionGeneral extends LitElement { } } - private _markerLocation = memoizeOne( + private _selectorLocation = memoizeOne( ( - lat: number, - lng: number, + latDefault: number, + lngDefault: number, location?: [number, number] - ): MarkerLocation[] => [ - { - id: "location", - latitude: location ? location[0] : lat, - longitude: location ? location[1] : lng, - location_editable: true, - }, - ] + ): LocationSelectorValue => ({ + latitude: location != null ? location[0] : latDefault, + longitude: location != null ? location[1] : lngDefault, + }) ); private _editLocation() { @@ -441,11 +440,6 @@ class HaConfigSectionGeneral extends LitElement { margin-top: 8px; display: inline-block; } - ha-locations-editor { - display: block; - height: 400px; - padding: 16px; - } `, ]; } diff --git a/src/panels/config/zone/dialog-home-zone-detail.ts b/src/panels/config/zone/dialog-home-zone-detail.ts new file mode 100644 index 000000000000..2786cca3cb53 --- /dev/null +++ b/src/panels/config/zone/dialog-home-zone-detail.ts @@ -0,0 +1,150 @@ +import "@material/mwc-button"; +import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; +import { property, state } from "lit/decorators"; +import memoizeOne from "memoize-one"; +import { fireEvent } from "../../../common/dom/fire_event"; +import { createCloseHeading } from "../../../components/ha-dialog"; +import "../../../components/ha-form/ha-form"; +import { HomeZoneMutableParams } from "../../../data/zone"; +import { haStyleDialog } from "../../../resources/styles"; +import { HomeAssistant } from "../../../types"; +import { HomeZoneDetailDialogParams } from "./show-dialog-home-zone-detail"; + +const SCHEMA = [ + { + name: "location", + required: true, + selector: { location: { radius: true, radius_readonly: true } }, + }, +]; + +class DialogHomeZoneDetail extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @state() private _error?: Record; + + @state() private _data?: HomeZoneMutableParams; + + @state() private _params?: HomeZoneDetailDialogParams; + + @state() private _submitting = false; + + public showDialog(params: HomeZoneDetailDialogParams): void { + this._params = params; + this._error = undefined; + this._data = { + latitude: this.hass.config.latitude, + longitude: this.hass.config.longitude, + }; + } + + public closeDialog(): void { + this._params = undefined; + this._data = undefined; + fireEvent(this, "dialog-closed", { dialog: this.localName }); + } + + protected render() { + if (!this._params || !this._data) { + return nothing; + } + const latInvalid = String(this._data.latitude) === ""; + const lngInvalid = String(this._data.longitude) === ""; + + const valid = !latInvalid && !lngInvalid; + + return html` + +
+ +

+ ${this.hass!.localize( + "ui.panel.config.zone.detail.no_edit_home_zone_radius" + )} +

+
+ + ${this.hass!.localize("ui.panel.config.zone.detail.update")} + +
+ `; + } + + private _formData = memoizeOne((data: HomeZoneMutableParams) => ({ + ...data, + location: { + latitude: data.latitude, + longitude: data.longitude, + radius: this.hass.states["zone.home"]?.attributes?.radius || 100, + }, + })); + + private _valueChanged(ev: CustomEvent) { + this._error = undefined; + const value = { ...ev.detail.value }; + value.latitude = value.location.latitude; + value.longitude = value.location.longitude; + delete value.location; + this._data = value; + } + + private _computeLabel = (): string => ""; + + private async _updateEntry() { + this._submitting = true; + try { + await this._params!.updateEntry!(this._data!); + this.closeDialog(); + } catch (err: any) { + this._error = { base: err ? err.message : "Unknown error" }; + } finally { + this._submitting = false; + } + } + + static get styles(): CSSResultGroup { + return [ + haStyleDialog, + css` + ha-dialog { + --mdc-dialog-min-width: min(600px, 95vw); + } + @media all and (max-width: 450px), all and (max-height: 500px) { + ha-dialog { + --mdc-dialog-min-width: calc( + 100vw - env(safe-area-inset-right) - env(safe-area-inset-left) + ); + } + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "dialog-home-zone-detail": DialogHomeZoneDetail; + } +} + +customElements.define("dialog-home-zone-detail", DialogHomeZoneDetail); diff --git a/src/panels/config/zone/dialog-zone-detail.ts b/src/panels/config/zone/dialog-zone-detail.ts index 37432c6b8f89..064bf61fa798 100644 --- a/src/panels/config/zone/dialog-zone-detail.ts +++ b/src/panels/config/zone/dialog-zone-detail.ts @@ -145,30 +145,8 @@ class DialogZoneDetail extends LitElement { required: true, selector: { location: { radius: true, icon } }, }, - { - name: "", - type: "grid", - schema: [ - { - name: "latitude", - required: true, - selector: { number: {} }, - }, - { - name: "longitude", - required: true, - - selector: { number: {} }, - }, - ], - }, { name: "passive_note", type: "constant" }, { name: "passive", selector: { boolean: {} } }, - { - name: "radius", - required: false, - selector: { number: { min: 0, max: 999999, mode: "box" } }, - }, ] as const ); @@ -184,15 +162,9 @@ class DialogZoneDetail extends LitElement { private _valueChanged(ev: CustomEvent) { this._error = undefined; const value = { ...ev.detail.value }; - if ( - value.location.latitude !== this._data!.latitude || - value.location.longitude !== this._data!.longitude || - value.location.radius !== this._data!.radius - ) { - value.latitude = value.location.latitude; - value.longitude = value.location.longitude; - value.radius = Math.round(value.location.radius); - } + value.latitude = value.location.latitude; + value.longitude = value.location.longitude; + value.radius = value.location.radius; delete value.location; if (!value.icon) { delete value.icon; diff --git a/src/panels/config/zone/ha-config-zone.ts b/src/panels/config/zone/ha-config-zone.ts index 2455bf2b4818..46c9ea5c719c 100644 --- a/src/panels/config/zone/ha-config-zone.ts +++ b/src/panels/config/zone/ha-config-zone.ts @@ -1,4 +1,4 @@ -import { mdiCog, mdiPencil, mdiPencilOff, mdiPlus } from "@mdi/js"; +import { mdiPencil, mdiPencilOff, mdiPlus } from "@mdi/js"; import "@polymer/paper-item/paper-icon-item"; import "@polymer/paper-item/paper-item-body"; import "@polymer/paper-listbox/paper-listbox"; @@ -35,6 +35,7 @@ import { updateZone, Zone, ZoneMutableParams, + HomeZoneMutableParams, } from "../../../data/zone"; import { showAlertDialog, @@ -47,6 +48,7 @@ import type { HomeAssistant, Route } from "../../../types"; import "../ha-config-section"; import { configSections } from "../ha-panel-config"; import { showZoneDetailDialog } from "./show-dialog-zone-detail"; +import { showHomeZoneDetailDialog } from "./show-dialog-home-zone-detail"; @customElement("ha-config-zone") export class HaConfigZone extends SubscribeMixin(LitElement) { @@ -193,12 +195,12 @@ export class HaConfigZone extends SubscribeMixin(LitElement) { !this._canEditCore} .path=${stateObject.entity_id === "zone.home" && this._canEditCore - ? mdiCog + ? mdiPencil : mdiPencilOff} .label=${stateObject.entity_id === "zone.home" ? hass.localize("ui.panel.config.zone.edit_home") : hass.localize("ui.panel.config.zone.edit_zone")} - @click=${this._openCoreConfig} + @click=${this._editHomeZone} > ${stateObject.entity_id !== "zone.home" ? html` @@ -400,7 +402,7 @@ export class HaConfigZone extends SubscribeMixin(LitElement) { this._openDialog(entry); } - private async _openCoreConfig(ev) { + private async _editHomeZone(ev) { if (ev.currentTarget.noEdit) { showAlertDialog(this, { title: this.hass.localize("ui.panel.config.zone.can_not_edit"), @@ -409,7 +411,9 @@ export class HaConfigZone extends SubscribeMixin(LitElement) { }); return; } - navigate("/config/general"); + showHomeZoneDetailDialog(this, { + updateEntry: (values) => this._updateHomeZoneEntry(values), + }); } private async _createEntry(values: ZoneMutableParams) { @@ -427,6 +431,14 @@ export class HaConfigZone extends SubscribeMixin(LitElement) { this._map?.fitMarker(created.id); } + private async _updateHomeZoneEntry(values: HomeZoneMutableParams) { + await saveCoreConfig(this.hass, { + latitude: values.latitude, + longitude: values.longitude, + }); + this._zoomZone("zone.home"); + } + private async _updateEntry( entry: Zone, values: Partial, diff --git a/src/panels/config/zone/show-dialog-home-zone-detail.ts b/src/panels/config/zone/show-dialog-home-zone-detail.ts new file mode 100644 index 000000000000..a4dc6c12c647 --- /dev/null +++ b/src/panels/config/zone/show-dialog-home-zone-detail.ts @@ -0,0 +1,20 @@ +import { fireEvent } from "../../../common/dom/fire_event"; +import { HomeZoneMutableParams } from "../../../data/zone"; + +export interface HomeZoneDetailDialogParams { + updateEntry?: (updates: HomeZoneMutableParams) => Promise; +} + +export const loadHomeZoneDetailDialog = () => + import("./dialog-home-zone-detail"); + +export const showHomeZoneDetailDialog = ( + element: HTMLElement, + params: HomeZoneDetailDialogParams +): void => { + fireEvent(element, "show-dialog", { + dialogTag: "dialog-home-zone-detail", + dialogImport: loadHomeZoneDetailDialog, + dialogParams: params, + }); +}; diff --git a/src/translations/en.json b/src/translations/en.json index 3e5142ff565b..6478c5a339f7 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -377,6 +377,11 @@ "upload_failed": "Upload failed", "unknown_file": "Unknown file" }, + "location": { + "latitude": "[%key:ui::panel::config::zone::detail::latitude%]", + "longitude": "[%key:ui::panel::config::zone::detail::longitude%]", + "radius": "[%key:ui::panel::config::zone::detail::radius%]" + }, "selector": { "options": "Selector Options", "types": { @@ -4122,10 +4127,6 @@ "confirm_delete": "Are you sure you want to delete this zone?", "can_not_edit": "Unable to edit zone", "configured_in_yaml": "Zones configured via configuration.yaml cannot be edited via the UI.", - "edit_home_zone": "The radius of the Home zone can't be edited from the frontend yet. Drag the marker on the map to move the home zone.", - "edit_home_zone_narrow": "The radius of the Home zone can't be edited from the frontend yet. The location can be changed from the general configuration.", - "go_to_core_config": "Go to general configuration?", - "home_zone_core_config": "The location of your home zone is editable from the general configuration page. The radius of the Home zone can't be edited from the frontend yet. Do you want to go to the general configuration?", "detail": { "new_zone": "New zone", "name": "Name", @@ -4140,7 +4141,8 @@ "required_error_msg": "This field is required", "delete": "Delete", "create": "Add", - "update": "Update" + "update": "Update", + "no_edit_home_zone_radius": "The radius of the home zone is not editable in the UI." }, "core_location_dialog": "Home Assistant location" },