mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-23 09:16:38 +00:00
Add location selector, convert zone editor (#11902)
This commit is contained in:
parent
0dac10aa23
commit
4cdff3faea
@ -38,6 +38,7 @@ const SCHEMAS: {
|
||||
select: "Select",
|
||||
icon: "Icon",
|
||||
media: "Media",
|
||||
location: "Location",
|
||||
},
|
||||
schema: [
|
||||
{ name: "addon", selector: { addon: {} } },
|
||||
@ -75,6 +76,10 @@ const SCHEMAS: {
|
||||
media: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "location",
|
||||
selector: { location: { radius: true, icon: "mdi:home" } },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
@ -168,6 +168,11 @@ const SCHEMAS: {
|
||||
},
|
||||
icon: { name: "Icon", selector: { icon: {} } },
|
||||
media: { name: "Media", selector: { media: {} } },
|
||||
location: { name: "Location", selector: { location: {} } },
|
||||
location_radius: {
|
||||
name: "Location with radius",
|
||||
selector: { location: { radius: true, icon: "mdi:home" } },
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
@ -9,7 +9,9 @@ export class HaFormConstant extends LitElement implements HaFormElement {
|
||||
@property() public label!: string;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`<span class="label">${this.label}</span>: ${this.schema.value}`;
|
||||
return html`<span class="label">${this.label}</span>${this.schema.value
|
||||
? `: ${this.schema.value}`
|
||||
: ""}`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
|
@ -25,6 +25,8 @@ import { HomeAssistant } from "../../types";
|
||||
const getValue = (obj, item) =>
|
||||
obj ? (!item.name ? obj : obj[item.name]) : null;
|
||||
|
||||
const getError = (obj, item) => (obj && item.name ? obj[item.name] : null);
|
||||
|
||||
let selectorImported = false;
|
||||
|
||||
@customElement("ha-form")
|
||||
@ -84,7 +86,7 @@ export class HaForm extends LitElement implements HaFormElement {
|
||||
`
|
||||
: ""}
|
||||
${this.schema.map((item) => {
|
||||
const error = getValue(this.error, item);
|
||||
const error = getError(this.error, item);
|
||||
|
||||
return html`
|
||||
${error
|
||||
|
@ -40,7 +40,7 @@ export interface HaFormSelector extends HaFormBaseSchema {
|
||||
|
||||
export interface HaFormConstantSchema extends HaFormBaseSchema {
|
||||
type: "constant";
|
||||
value: string;
|
||||
value?: string;
|
||||
}
|
||||
|
||||
export interface HaFormIntegerSchema extends HaFormBaseSchema {
|
||||
|
80
src/components/ha-selector/ha-selector-location.ts
Normal file
80
src/components/ha-selector/ha-selector-location.ts
Normal file
@ -0,0 +1,80 @@
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type {
|
||||
LocationSelector,
|
||||
LocationSelectorValue,
|
||||
} from "../../data/selector";
|
||||
import "../../panels/lovelace/components/hui-theme-select-editor";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { MarkerLocation } from "../map/ha-locations-editor";
|
||||
import "../map/ha-locations-editor";
|
||||
|
||||
@customElement("ha-selector-location")
|
||||
export class HaLocationSelector extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public selector!: LocationSelector;
|
||||
|
||||
@property() public value?: LocationSelectorValue;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public disabled = false;
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-locations-editor
|
||||
class="flex"
|
||||
.hass=${this.hass}
|
||||
.locations=${this._location(this.selector, this.value)}
|
||||
@location-updated=${this._locationChanged}
|
||||
@radius-updated=${this._radiusChanged}
|
||||
></ha-locations-editor>
|
||||
`;
|
||||
}
|
||||
|
||||
private _location = memoizeOne(
|
||||
(
|
||||
selector: LocationSelector,
|
||||
value?: LocationSelectorValue
|
||||
): MarkerLocation[] => {
|
||||
const computedStyles = getComputedStyle(this);
|
||||
const zoneRadiusColor = selector.location.radius
|
||||
? computedStyles.getPropertyValue("--zone-radius-color") ||
|
||||
computedStyles.getPropertyValue("--accent-color")
|
||||
: undefined;
|
||||
return [
|
||||
{
|
||||
id: "location",
|
||||
latitude: value?.latitude || this.hass.config.latitude,
|
||||
longitude: value?.longitude || this.hass.config.longitude,
|
||||
radius: selector.location.radius ? value?.radius || 1000 : undefined,
|
||||
radius_color: zoneRadiusColor,
|
||||
icon: selector.location.icon,
|
||||
location_editable: true,
|
||||
radius_editable: true,
|
||||
},
|
||||
];
|
||||
}
|
||||
);
|
||||
|
||||
private _locationChanged(ev: CustomEvent) {
|
||||
const [latitude, longitude] = ev.detail.location;
|
||||
fireEvent(this, "value-changed", {
|
||||
value: { ...this.value, latitude, longitude },
|
||||
});
|
||||
}
|
||||
|
||||
private _radiusChanged(ev: CustomEvent) {
|
||||
const radius = ev.detail.radius;
|
||||
fireEvent(this, "value-changed", { value: { ...this.value, radius } });
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-selector-location": HaLocationSelector;
|
||||
}
|
||||
}
|
@ -20,6 +20,7 @@ import "./ha-selector-time";
|
||||
import "./ha-selector-icon";
|
||||
import "./ha-selector-media";
|
||||
import "./ha-selector-theme";
|
||||
import "./ha-selector-location";
|
||||
|
||||
@customElement("ha-selector")
|
||||
export class HaSelector extends LitElement {
|
||||
|
@ -15,7 +15,8 @@ export type Selector =
|
||||
| SelectSelector
|
||||
| IconSelector
|
||||
| MediaSelector
|
||||
| ThemeSelector;
|
||||
| ThemeSelector
|
||||
| LocationSelector;
|
||||
|
||||
export interface EntitySelector {
|
||||
entity: {
|
||||
@ -164,6 +165,16 @@ export interface MediaSelector {
|
||||
media: {};
|
||||
}
|
||||
|
||||
export interface LocationSelector {
|
||||
location: { radius?: boolean; icon?: string };
|
||||
}
|
||||
|
||||
export interface LocationSelectorValue {
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
radius?: number;
|
||||
}
|
||||
|
||||
export interface MediaSelectorValue {
|
||||
entity_id?: string;
|
||||
media_content_id?: string;
|
||||
|
@ -12,12 +12,12 @@ export interface Zone {
|
||||
}
|
||||
|
||||
export interface ZoneMutableParams {
|
||||
name: string;
|
||||
icon?: string;
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
name: string;
|
||||
passive: boolean;
|
||||
radius: number;
|
||||
passive?: boolean;
|
||||
radius?: number;
|
||||
}
|
||||
|
||||
export const fetchZones = (hass: HomeAssistant) =>
|
||||
|
@ -1,17 +1,12 @@
|
||||
import "@material/mwc-button";
|
||||
import "@polymer/paper-input/paper-input";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { addDistanceToCoord } from "../../../common/location/add_distance_to_coord";
|
||||
import { computeRTLDirection } from "../../../common/util/compute_rtl";
|
||||
import { createCloseHeading } from "../../../components/ha-dialog";
|
||||
import "../../../components/ha-formfield";
|
||||
import "../../../components/ha-icon-picker";
|
||||
import "../../../components/ha-switch";
|
||||
import "../../../components/map/ha-locations-editor";
|
||||
import type { MarkerLocation } from "../../../components/map/ha-locations-editor";
|
||||
import "../../../components/ha-form/ha-form";
|
||||
import { HaFormSchema } from "../../../components/ha-form/types";
|
||||
import { getZoneEditorInitData, ZoneMutableParams } from "../../../data/zone";
|
||||
import { haStyleDialog } from "../../../resources/styles";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
@ -20,19 +15,9 @@ import { ZoneDetailDialogParams } from "./show-dialog-zone-detail";
|
||||
class DialogZoneDetail extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _name!: string;
|
||||
@state() private _error?: Record<string, string>;
|
||||
|
||||
@state() private _icon!: string;
|
||||
|
||||
@state() private _latitude!: number;
|
||||
|
||||
@state() private _longitude!: number;
|
||||
|
||||
@state() private _passive!: boolean;
|
||||
|
||||
@state() private _radius!: number;
|
||||
|
||||
@state() private _error?: string;
|
||||
@state() private _data?: ZoneMutableParams;
|
||||
|
||||
@state() private _params?: ZoneDetailDialogParams;
|
||||
|
||||
@ -42,13 +27,7 @@ class DialogZoneDetail extends LitElement {
|
||||
this._params = params;
|
||||
this._error = undefined;
|
||||
if (this._params.entry) {
|
||||
this._name = this._params.entry.name || "";
|
||||
this._icon = this._params.entry.icon || "";
|
||||
this._latitude = this._params.entry.latitude || this.hass.config.latitude;
|
||||
this._longitude =
|
||||
this._params.entry.longitude || this.hass.config.longitude;
|
||||
this._passive = this._params.entry.passive || false;
|
||||
this._radius = this._params.entry.radius || 100;
|
||||
this._data = this._params.entry;
|
||||
} else {
|
||||
const initConfig = getZoneEditorInitData();
|
||||
let movedHomeLocation;
|
||||
@ -59,30 +38,34 @@ class DialogZoneDetail extends LitElement {
|
||||
Math.random() * 500 * (Math.random() < 0.5 ? -1 : 1)
|
||||
);
|
||||
}
|
||||
this._latitude = initConfig?.latitude || movedHomeLocation[0];
|
||||
this._longitude = initConfig?.longitude || movedHomeLocation[1];
|
||||
this._name = initConfig?.name || "";
|
||||
this._icon = initConfig?.icon || "mdi:map-marker";
|
||||
|
||||
this._passive = false;
|
||||
this._radius = 100;
|
||||
this._data = {
|
||||
latitude: initConfig?.latitude || movedHomeLocation[0],
|
||||
longitude: initConfig?.longitude || movedHomeLocation[1],
|
||||
name: initConfig?.name || "",
|
||||
icon: initConfig?.icon || "mdi:map-marker",
|
||||
passive: false,
|
||||
radius: 100,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public closeDialog(): void {
|
||||
this._params = undefined;
|
||||
this._data = undefined;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this._params) {
|
||||
if (!this._params || !this._data) {
|
||||
return html``;
|
||||
}
|
||||
const nameInvalid = this._name.trim() === "";
|
||||
const iconInvalid = Boolean(this._icon && !this._icon.trim().includes(":"));
|
||||
const latInvalid = String(this._latitude) === "";
|
||||
const lngInvalid = String(this._longitude) === "";
|
||||
const radiusInvalid = String(this._radius) === "";
|
||||
const nameInvalid = this._data.name.trim() === "";
|
||||
const iconInvalid = Boolean(
|
||||
this._data.icon && !this._data.icon.trim().includes(":")
|
||||
);
|
||||
const latInvalid = String(this._data.latitude) === "";
|
||||
const lngInvalid = String(this._data.longitude) === "";
|
||||
const radiusInvalid = String(this._data.radius) === "";
|
||||
|
||||
const valid =
|
||||
!nameInvalid &&
|
||||
@ -105,96 +88,15 @@ class DialogZoneDetail extends LitElement {
|
||||
)}
|
||||
>
|
||||
<div>
|
||||
${this._error ? html` <div class="error">${this._error}</div> ` : ""}
|
||||
<div class="form">
|
||||
<paper-input
|
||||
dialogInitialFocus
|
||||
.value=${this._name}
|
||||
.configValue=${"name"}
|
||||
@value-changed=${this._valueChanged}
|
||||
.label=${this.hass!.localize("ui.panel.config.zone.detail.name")}
|
||||
.errorMessage=${this.hass!.localize(
|
||||
"ui.panel.config.zone.detail.required_error_msg"
|
||||
)}
|
||||
required
|
||||
auto-validate
|
||||
></paper-input>
|
||||
<ha-icon-picker
|
||||
.value=${this._icon}
|
||||
.configValue=${"icon"}
|
||||
@value-changed=${this._valueChanged}
|
||||
.label=${this.hass!.localize("ui.panel.config.zone.detail.icon")}
|
||||
.errorMessage=${this.hass!.localize(
|
||||
"ui.panel.config.zone.detail.icon_error_msg"
|
||||
)}
|
||||
.invalid=${iconInvalid}
|
||||
></ha-icon-picker>
|
||||
<ha-locations-editor
|
||||
class="flex"
|
||||
<ha-form
|
||||
.hass=${this.hass}
|
||||
.locations=${this._location(
|
||||
this._latitude,
|
||||
this._longitude,
|
||||
this._radius,
|
||||
this._passive,
|
||||
this._icon
|
||||
)}
|
||||
@location-updated=${this._locationChanged}
|
||||
@radius-updated=${this._radiusChanged}
|
||||
></ha-locations-editor>
|
||||
<div class="location">
|
||||
<paper-input
|
||||
.value=${this._latitude}
|
||||
.configValue=${"latitude"}
|
||||
.schema=${this._schema(this._data.icon)}
|
||||
.data=${this._formData(this._data)}
|
||||
.error=${this._error}
|
||||
.computeLabel=${this._computeLabel}
|
||||
class=${this._data.passive ? "passive" : ""}
|
||||
@value-changed=${this._valueChanged}
|
||||
.label=${this.hass!.localize(
|
||||
"ui.panel.config.zone.detail.latitude"
|
||||
)}
|
||||
.errorMessage=${this.hass!.localize(
|
||||
"ui.panel.config.zone.detail.required_error_msg"
|
||||
)}
|
||||
.invalid=${latInvalid}
|
||||
></paper-input>
|
||||
<paper-input
|
||||
.value=${this._longitude}
|
||||
.configValue=${"longitude"}
|
||||
@value-changed=${this._valueChanged}
|
||||
.label=${this.hass!.localize(
|
||||
"ui.panel.config.zone.detail.longitude"
|
||||
)}
|
||||
.errorMessage=${this.hass!.localize(
|
||||
"ui.panel.config.zone.detail.required_error_msg"
|
||||
)}
|
||||
.invalid=${lngInvalid}
|
||||
></paper-input>
|
||||
</div>
|
||||
<paper-input
|
||||
.value=${this._radius}
|
||||
.configValue=${"radius"}
|
||||
@value-changed=${this._valueChanged}
|
||||
.label=${this.hass!.localize(
|
||||
"ui.panel.config.zone.detail.radius"
|
||||
)}
|
||||
.errorMessage=${this.hass!.localize(
|
||||
"ui.panel.config.zone.detail.required_error_msg"
|
||||
)}
|
||||
.invalid=${radiusInvalid}
|
||||
></paper-input>
|
||||
<p>
|
||||
${this.hass!.localize("ui.panel.config.zone.detail.passive_note")}
|
||||
</p>
|
||||
<ha-formfield
|
||||
.label=${this.hass!.localize(
|
||||
"ui.panel.config.zone.detail.passive"
|
||||
)}
|
||||
.dir=${computeRTLDirection(this.hass)}
|
||||
>
|
||||
<ha-switch
|
||||
.checked=${this._passive}
|
||||
@change=${this._passiveChanged}
|
||||
></ha-switch>
|
||||
</ha-formfield>
|
||||
</div>
|
||||
></ha-form>
|
||||
</div>
|
||||
${this._params.entry
|
||||
? html`
|
||||
@ -221,74 +123,94 @@ class DialogZoneDetail extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _location = memoizeOne(
|
||||
(
|
||||
lat: number,
|
||||
lng: number,
|
||||
radius: number,
|
||||
passive: boolean,
|
||||
icon: string
|
||||
): MarkerLocation[] => {
|
||||
const computedStyles = getComputedStyle(this);
|
||||
const zoneRadiusColor = computedStyles.getPropertyValue("--accent-color");
|
||||
const passiveRadiusColor = computedStyles.getPropertyValue(
|
||||
"--secondary-text-color"
|
||||
);
|
||||
return [
|
||||
private _schema = memoizeOne((icon?: string): HaFormSchema[] => [
|
||||
{
|
||||
id: "location",
|
||||
latitude: Number(lat),
|
||||
longitude: Number(lng),
|
||||
radius,
|
||||
radius_color: passive ? passiveRadiusColor : zoneRadiusColor,
|
||||
icon,
|
||||
location_editable: true,
|
||||
radius_editable: true,
|
||||
name: "name",
|
||||
required: true,
|
||||
selector: {
|
||||
text: {},
|
||||
},
|
||||
];
|
||||
}
|
||||
);
|
||||
},
|
||||
{
|
||||
name: "icon",
|
||||
required: false,
|
||||
selector: {
|
||||
icon: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "location",
|
||||
required: true,
|
||||
selector: { location: { radius: true, icon } },
|
||||
},
|
||||
{
|
||||
name: "",
|
||||
type: "grid",
|
||||
schema: [
|
||||
{
|
||||
name: "latitude",
|
||||
required: true,
|
||||
selector: { text: {} },
|
||||
},
|
||||
{
|
||||
name: "longitude",
|
||||
required: true,
|
||||
|
||||
private _locationChanged(ev: CustomEvent) {
|
||||
[this._latitude, this._longitude] = ev.detail.location;
|
||||
}
|
||||
selector: { text: {} },
|
||||
},
|
||||
],
|
||||
},
|
||||
{ name: "passive_note", type: "constant" },
|
||||
{ name: "passive", selector: { boolean: {} } },
|
||||
{
|
||||
name: "radius",
|
||||
required: false,
|
||||
selector: { number: { min: 0, max: 999999, mode: "box" } },
|
||||
},
|
||||
]);
|
||||
|
||||
private _radiusChanged(ev: CustomEvent) {
|
||||
this._radius = ev.detail.radius;
|
||||
}
|
||||
|
||||
private _passiveChanged(ev) {
|
||||
this._passive = ev.target.checked;
|
||||
}
|
||||
private _formData = memoizeOne((data: ZoneMutableParams) => ({
|
||||
...data,
|
||||
location: {
|
||||
latitude: data.latitude,
|
||||
longitude: data.longitude,
|
||||
radius: data.radius,
|
||||
},
|
||||
}));
|
||||
|
||||
private _valueChanged(ev: CustomEvent) {
|
||||
const configValue = (ev.target as any).configValue;
|
||||
|
||||
this._error = undefined;
|
||||
this[`_${configValue}`] = ev.detail.value;
|
||||
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);
|
||||
}
|
||||
delete value.location;
|
||||
if (!value.icon) {
|
||||
delete value.icon;
|
||||
}
|
||||
this._data = value;
|
||||
}
|
||||
|
||||
private _computeLabel = (entry: HaFormSchema): string =>
|
||||
this.hass.localize(`ui.panel.config.zone.detail.${entry.name}`);
|
||||
|
||||
private async _updateEntry() {
|
||||
this._submitting = true;
|
||||
try {
|
||||
const values: ZoneMutableParams = {
|
||||
name: this._name.trim(),
|
||||
latitude: this._latitude,
|
||||
longitude: this._longitude,
|
||||
passive: this._passive,
|
||||
radius: this._radius,
|
||||
};
|
||||
if (this._icon) {
|
||||
values.icon = this._icon.trim();
|
||||
}
|
||||
if (this._params!.entry) {
|
||||
await this._params!.updateEntry!(values);
|
||||
await this._params!.updateEntry!(this._data!);
|
||||
} else {
|
||||
await this._params!.createEntry(values);
|
||||
await this._params!.createEntry(this._data!);
|
||||
}
|
||||
this._params = undefined;
|
||||
this.closeDialog();
|
||||
} catch (err: any) {
|
||||
this._error = err ? err.message : "Unknown error";
|
||||
this._error = { base: err ? err.message : "Unknown error" };
|
||||
} finally {
|
||||
this._submitting = false;
|
||||
}
|
||||
@ -309,24 +231,18 @@ class DialogZoneDetail extends LitElement {
|
||||
return [
|
||||
haStyleDialog,
|
||||
css`
|
||||
.location {
|
||||
display: flex;
|
||||
ha-dialog {
|
||||
--mdc-dialog-min-width: 600px;
|
||||
}
|
||||
.location > * {
|
||||
flex-grow: 1;
|
||||
min-width: 0;
|
||||
@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)
|
||||
);
|
||||
}
|
||||
.location > *:first-child {
|
||||
margin-right: 4px;
|
||||
}
|
||||
.location > *:last-child {
|
||||
margin-left: 4px;
|
||||
}
|
||||
ha-locations-editor {
|
||||
margin-top: 16px;
|
||||
}
|
||||
a {
|
||||
color: var(--primary-color);
|
||||
ha-form.passive {
|
||||
--zone-radius-color: var(--secondary-text-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
Loading…
x
Reference in New Issue
Block a user