mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-23 09:16:38 +00:00
Add MVP person editor (#2703)
* Add MVP person editor * Better highlight the config.yaml people * Add note
This commit is contained in:
parent
8938ad8f8d
commit
46e1139946
46
src/data/person.ts
Normal file
46
src/data/person.ts
Normal file
@ -0,0 +1,46 @@
|
||||
import { HomeAssistant } from "../types";
|
||||
|
||||
export interface Person {
|
||||
id: string;
|
||||
name: string;
|
||||
user_id?: string;
|
||||
device_trackers?: string[];
|
||||
}
|
||||
|
||||
export interface PersonMutableParams {
|
||||
name: string;
|
||||
user_id: string | null;
|
||||
device_trackers: string[];
|
||||
}
|
||||
|
||||
export const fetchPersons = (hass: HomeAssistant) =>
|
||||
hass.callWS<{
|
||||
storage: Person[];
|
||||
config: Person[];
|
||||
}>({ type: "person/list" });
|
||||
|
||||
export const createPerson = (
|
||||
hass: HomeAssistant,
|
||||
values: PersonMutableParams
|
||||
) =>
|
||||
hass.callWS<Person>({
|
||||
type: "person/create",
|
||||
...values,
|
||||
});
|
||||
|
||||
export const updatePerson = (
|
||||
hass: HomeAssistant,
|
||||
personId: string,
|
||||
updates: Partial<PersonMutableParams>
|
||||
) =>
|
||||
hass.callWS<Person>({
|
||||
type: "person/update",
|
||||
person_id: personId,
|
||||
...updates,
|
||||
});
|
||||
|
||||
export const deletePerson = (hass: HomeAssistant, personId: string) =>
|
||||
hass.callWS({
|
||||
type: "person/delete",
|
||||
person_id: personId,
|
||||
});
|
@ -14,7 +14,7 @@ export interface AreaRegistryDetailDialogParams {
|
||||
}
|
||||
|
||||
export const loadAreaRegistryDetailDialog = () =>
|
||||
import(/* webpackChunkName: "entity-registry-detail-dialog" */ "./dialog-area-registry-detail");
|
||||
import(/* webpackChunkName: "area-registry-detail-dialog" */ "./dialog-area-registry-detail");
|
||||
|
||||
export const showAreaRegistryDetailDialog = (
|
||||
element: HTMLElement,
|
||||
|
@ -52,13 +52,14 @@ class HaConfigNavigation extends LocalizeMixin(NavigateMixin(PolymerElement)) {
|
||||
type: Array,
|
||||
value: [
|
||||
"core",
|
||||
"customize",
|
||||
"person",
|
||||
"entity_registry",
|
||||
"area_registry",
|
||||
"automation",
|
||||
"script",
|
||||
"zha",
|
||||
"zwave",
|
||||
"customize",
|
||||
],
|
||||
},
|
||||
};
|
||||
|
@ -9,19 +9,6 @@ import isComponentLoaded from "../../common/config/is_component_loaded";
|
||||
import EventsMixin from "../../mixins/events-mixin";
|
||||
import NavigateMixin from "../../mixins/navigate-mixin";
|
||||
|
||||
import(/* webpackChunkName: "panel-config-area-registry" */ "./area_registry/ha-config-area-registry");
|
||||
import(/* webpackChunkName: "panel-config-automation" */ "./automation/ha-config-automation");
|
||||
import(/* webpackChunkName: "panel-config-cloud" */ "./cloud/ha-config-cloud");
|
||||
import(/* webpackChunkName: "panel-config-config" */ "./config-entries/ha-config-entries");
|
||||
import(/* webpackChunkName: "panel-config-core" */ "./core/ha-config-core");
|
||||
import(/* webpackChunkName: "panel-config-customize" */ "./customize/ha-config-customize");
|
||||
import(/* webpackChunkName: "panel-config-dashboard" */ "./dashboard/ha-config-dashboard");
|
||||
import(/* webpackChunkName: "panel-config-script" */ "./script/ha-config-script");
|
||||
import(/* webpackChunkName: "panel-config-entity-registry" */ "./entity_registry/ha-config-entity-registry");
|
||||
import(/* webpackChunkName: "panel-config-users" */ "./users/ha-config-users");
|
||||
import(/* webpackChunkName: "panel-config-zha" */ "./zha/ha-config-zha");
|
||||
import(/* webpackChunkName: "panel-config-zwave" */ "./zwave/ha-config-zwave");
|
||||
|
||||
/*
|
||||
* @appliesMixin EventsMixin
|
||||
* @appliesMixin NavigateMixin
|
||||
@ -136,6 +123,15 @@ class HaPanelConfig extends EventsMixin(NavigateMixin(PolymerElement)) {
|
||||
></ha-config-zwave>
|
||||
</template>
|
||||
|
||||
<template is="dom-if" if='[[_equals(_routeData.page, "person")]]' restamp>
|
||||
<ha-config-person
|
||||
page-name="person"
|
||||
route="[[route]]"
|
||||
hass="[[hass]]"
|
||||
is-wide="[[isWide]]"
|
||||
></ha-config-person>
|
||||
</template>
|
||||
|
||||
<template
|
||||
is="dom-if"
|
||||
if='[[_equals(_routeData.page, "customize")]]'
|
||||
@ -207,6 +203,19 @@ class HaPanelConfig extends EventsMixin(NavigateMixin(PolymerElement)) {
|
||||
this.addEventListener("ha-refresh-cloud-status", () =>
|
||||
this._updateCloudStatus()
|
||||
);
|
||||
import(/* webpackChunkName: "panel-config-area-registry" */ "./area_registry/ha-config-area-registry");
|
||||
import(/* webpackChunkName: "panel-config-automation" */ "./automation/ha-config-automation");
|
||||
import(/* webpackChunkName: "panel-config-cloud" */ "./cloud/ha-config-cloud");
|
||||
import(/* webpackChunkName: "panel-config-config" */ "./config-entries/ha-config-entries");
|
||||
import(/* webpackChunkName: "panel-config-core" */ "./core/ha-config-core");
|
||||
import(/* webpackChunkName: "panel-config-customize" */ "./customize/ha-config-customize");
|
||||
import(/* webpackChunkName: "panel-config-dashboard" */ "./dashboard/ha-config-dashboard");
|
||||
import(/* webpackChunkName: "panel-config-script" */ "./script/ha-config-script");
|
||||
import(/* webpackChunkName: "panel-config-entity-registry" */ "./entity_registry/ha-config-entity-registry");
|
||||
import(/* webpackChunkName: "panel-config-users" */ "./users/ha-config-users");
|
||||
import(/* webpackChunkName: "panel-config-zha" */ "./zha/ha-config-zha");
|
||||
import(/* webpackChunkName: "panel-config-zwave" */ "./zwave/ha-config-zwave");
|
||||
import(/* webpackChunkName: "panel-config-person" */ "./person/ha-config-person");
|
||||
}
|
||||
|
||||
async _updateCloudStatus() {
|
||||
|
169
src/panels/config/person/dialog-person-detail.ts
Normal file
169
src/panels/config/person/dialog-person-detail.ts
Normal file
@ -0,0 +1,169 @@
|
||||
import {
|
||||
LitElement,
|
||||
html,
|
||||
css,
|
||||
PropertyDeclarations,
|
||||
CSSResult,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import "@polymer/paper-dialog/paper-dialog";
|
||||
import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable";
|
||||
import "@polymer/paper-input/paper-input";
|
||||
|
||||
import { PersonDetailDialogParams } from "./show-dialog-person-detail";
|
||||
import { PolymerChangedEvent } from "../../../polymer-types";
|
||||
import { haStyleDialog } from "../../../resources/ha-style";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { PersonMutableParams } from "../../../data/person";
|
||||
|
||||
class DialogPersonDetail extends LitElement {
|
||||
public hass!: HomeAssistant;
|
||||
private _name!: string;
|
||||
private _error?: string;
|
||||
private _params?: PersonDetailDialogParams;
|
||||
private _submitting?: boolean;
|
||||
|
||||
static get properties(): PropertyDeclarations {
|
||||
return {
|
||||
_error: {},
|
||||
_name: {},
|
||||
_params: {},
|
||||
};
|
||||
}
|
||||
|
||||
public async showDialog(params: PersonDetailDialogParams): Promise<void> {
|
||||
this._params = params;
|
||||
this._error = undefined;
|
||||
this._name = this._params.entry ? this._params.entry.name : "";
|
||||
await this.updateComplete;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult | void {
|
||||
if (!this._params) {
|
||||
return html``;
|
||||
}
|
||||
const nameInvalid = this._name.trim() === "";
|
||||
return html`
|
||||
<paper-dialog
|
||||
with-backdrop
|
||||
opened
|
||||
@opened-changed="${this._openedChanged}"
|
||||
>
|
||||
<h2>${this._params.entry ? this._params.entry.name : "New Person"}</h2>
|
||||
<paper-dialog-scrollable>
|
||||
${this._error
|
||||
? html`
|
||||
<div class="error">${this._error}</div>
|
||||
`
|
||||
: ""}
|
||||
<div class="form">
|
||||
<paper-input
|
||||
.value=${this._name}
|
||||
@value-changed=${this._nameChanged}
|
||||
label="Name"
|
||||
error-message="Name is required"
|
||||
.invalid=${nameInvalid}
|
||||
></paper-input>
|
||||
</div>
|
||||
</paper-dialog-scrollable>
|
||||
<div class="paper-dialog-buttons">
|
||||
${this._params.entry
|
||||
? html`
|
||||
<paper-button
|
||||
class="danger"
|
||||
@click="${this._deleteEntry}"
|
||||
.disabled=${this._submitting}
|
||||
>
|
||||
DELETE
|
||||
</paper-button>
|
||||
`
|
||||
: html``}
|
||||
<paper-button
|
||||
@click="${this._updateEntry}"
|
||||
.disabled=${nameInvalid || this._submitting}
|
||||
>
|
||||
${this._params.entry ? "UPDATE" : "CREATE"}
|
||||
</paper-button>
|
||||
</div>
|
||||
</paper-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private _nameChanged(ev: PolymerChangedEvent<string>) {
|
||||
this._error = undefined;
|
||||
this._name = ev.detail.value;
|
||||
}
|
||||
|
||||
private async _updateEntry() {
|
||||
this._submitting = true;
|
||||
try {
|
||||
const values: PersonMutableParams = {
|
||||
name: this._name.trim(),
|
||||
// Temp, we will add this in a future PR.
|
||||
user_id: null,
|
||||
device_trackers: [],
|
||||
};
|
||||
if (this._params!.entry) {
|
||||
await this._params!.updateEntry(values);
|
||||
} else {
|
||||
await this._params!.createEntry(values);
|
||||
}
|
||||
this._params = undefined;
|
||||
} catch (err) {
|
||||
this._error = err;
|
||||
} finally {
|
||||
this._submitting = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async _deleteEntry() {
|
||||
this._submitting = true;
|
||||
try {
|
||||
if (await this._params!.removeEntry()) {
|
||||
this._params = undefined;
|
||||
}
|
||||
} finally {
|
||||
this._submitting = false;
|
||||
}
|
||||
}
|
||||
|
||||
private _openedChanged(ev: PolymerChangedEvent<boolean>): void {
|
||||
if (!(ev.detail as any).value) {
|
||||
this._params = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
haStyleDialog,
|
||||
css`
|
||||
paper-dialog {
|
||||
min-width: 400px;
|
||||
}
|
||||
.form {
|
||||
padding-bottom: 24px;
|
||||
}
|
||||
paper-button {
|
||||
font-weight: 500;
|
||||
}
|
||||
paper-button.danger {
|
||||
font-weight: 500;
|
||||
color: var(--google-red-500);
|
||||
margin-left: -12px;
|
||||
margin-right: auto;
|
||||
}
|
||||
.error {
|
||||
color: var(--google-red-500);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"dialog-person-detail": DialogPersonDetail;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("dialog-person-detail", DialogPersonDetail);
|
217
src/panels/config/person/ha-config-person.ts
Normal file
217
src/panels/config/person/ha-config-person.ts
Normal file
@ -0,0 +1,217 @@
|
||||
import {
|
||||
LitElement,
|
||||
TemplateResult,
|
||||
html,
|
||||
css,
|
||||
CSSResult,
|
||||
PropertyDeclarations,
|
||||
} from "lit-element";
|
||||
import "@polymer/paper-item/paper-item";
|
||||
import "@polymer/paper-item/paper-item-body";
|
||||
import "@polymer/paper-card/paper-card";
|
||||
import "@polymer/paper-fab/paper-fab";
|
||||
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import {
|
||||
Person,
|
||||
fetchPersons,
|
||||
updatePerson,
|
||||
deletePerson,
|
||||
createPerson,
|
||||
} from "../../../data/person";
|
||||
import "../../../layouts/hass-subpage";
|
||||
import "../../../layouts/hass-loading-screen";
|
||||
import compare from "../../../common/string/compare";
|
||||
import "../ha-config-section";
|
||||
import {
|
||||
showPersonDetailDialog,
|
||||
loadPersonDetailDialog,
|
||||
} from "./show-dialog-person-detail";
|
||||
|
||||
class HaConfigPerson extends LitElement {
|
||||
public hass?: HomeAssistant;
|
||||
public isWide?: boolean;
|
||||
private _storageItems?: Person[];
|
||||
private _configItems?: Person[];
|
||||
|
||||
static get properties(): PropertyDeclarations {
|
||||
return {
|
||||
hass: {},
|
||||
isWide: {},
|
||||
_storageItems: {},
|
||||
_configItems: {},
|
||||
};
|
||||
}
|
||||
|
||||
protected render(): TemplateResult | void {
|
||||
if (
|
||||
!this.hass ||
|
||||
this._storageItems === undefined ||
|
||||
this._configItems === undefined
|
||||
) {
|
||||
return html`
|
||||
<hass-loading-screen></hass-loading-screen>
|
||||
`;
|
||||
}
|
||||
return html`
|
||||
<hass-subpage header="Persons">
|
||||
<ha-config-section .isWide=${this.isWide}>
|
||||
<span slot="header">Persons</span>
|
||||
<span slot="introduction">
|
||||
Here you can define each person of interest in Home Assistant.
|
||||
${this._configItems.length > 0
|
||||
? html`
|
||||
<p>
|
||||
Note: people configured via configuration.yaml cannot be
|
||||
edited via the UI.
|
||||
</p>
|
||||
`
|
||||
: ""}
|
||||
</span>
|
||||
<paper-card class="storage">
|
||||
${this._storageItems.map((entry) => {
|
||||
return html`
|
||||
<paper-item @click=${this._openEditEntry} .entry=${entry}>
|
||||
<paper-item-body>
|
||||
${entry.name}
|
||||
</paper-item-body>
|
||||
</paper-item>
|
||||
`;
|
||||
})}
|
||||
${this._storageItems.length === 0
|
||||
? html`
|
||||
<div class="empty">
|
||||
Looks like you have no people yet!
|
||||
<paper-button @click=${this._createPerson}>
|
||||
CREATE PERSON</paper-button
|
||||
>
|
||||
</div>
|
||||
`
|
||||
: html``}
|
||||
</paper-card>
|
||||
${this._configItems.length > 0
|
||||
? html`
|
||||
<paper-card heading="Configuration.yaml people">
|
||||
${this._configItems.map((entry) => {
|
||||
return html`
|
||||
<paper-item>
|
||||
<paper-item-body>
|
||||
${entry.name}
|
||||
</paper-item-body>
|
||||
</paper-item>
|
||||
`;
|
||||
})}
|
||||
</paper-card>
|
||||
`
|
||||
: ""}
|
||||
</ha-config-section>
|
||||
</hass-subpage>
|
||||
|
||||
<paper-fab
|
||||
?is-wide=${this.isWide}
|
||||
icon="hass:plus"
|
||||
title="Create Area"
|
||||
@click=${this._createPerson}
|
||||
></paper-fab>
|
||||
`;
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProps) {
|
||||
super.firstUpdated(changedProps);
|
||||
this._fetchData();
|
||||
loadPersonDetailDialog();
|
||||
}
|
||||
|
||||
private async _fetchData() {
|
||||
const personData = await fetchPersons(this.hass!);
|
||||
|
||||
this._storageItems = personData.storage.sort((ent1, ent2) =>
|
||||
compare(ent1.name, ent2.name)
|
||||
);
|
||||
this._configItems = personData.config.sort((ent1, ent2) =>
|
||||
compare(ent1.name, ent2.name)
|
||||
);
|
||||
}
|
||||
|
||||
private _createPerson() {
|
||||
this._openDialog();
|
||||
}
|
||||
|
||||
private _openEditEntry(ev: MouseEvent) {
|
||||
const entry: Person = (ev.currentTarget! as any).entry;
|
||||
this._openDialog(entry);
|
||||
}
|
||||
|
||||
private _openDialog(entry?: Person) {
|
||||
showPersonDetailDialog(this, {
|
||||
entry,
|
||||
createEntry: async (values) => {
|
||||
const created = await createPerson(this.hass!, values);
|
||||
this._storageItems = this._storageItems!.concat(created).sort(
|
||||
(ent1, ent2) => compare(ent1.name, ent2.name)
|
||||
);
|
||||
},
|
||||
updateEntry: async (values) => {
|
||||
const updated = await updatePerson(this.hass!, entry!.id, values);
|
||||
this._storageItems = this._storageItems!.map((ent) =>
|
||||
ent === entry ? updated : ent
|
||||
);
|
||||
},
|
||||
removeEntry: async () => {
|
||||
if (
|
||||
!confirm(`Are you sure you want to delete this area?
|
||||
|
||||
All devices in this area will become unassigned.`)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
await deletePerson(this.hass!, entry!.id);
|
||||
this._storageItems = this._storageItems!.filter(
|
||||
(ent) => ent !== entry
|
||||
);
|
||||
return true;
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
a {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
paper-card {
|
||||
display: block;
|
||||
max-width: 600px;
|
||||
margin: 16px auto;
|
||||
}
|
||||
.empty {
|
||||
text-align: center;
|
||||
}
|
||||
paper-item {
|
||||
padding-top: 4px;
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
paper-card.storage paper-item {
|
||||
cursor: pointer;
|
||||
}
|
||||
paper-fab {
|
||||
position: fixed;
|
||||
bottom: 16px;
|
||||
right: 16px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
paper-fab[is-wide] {
|
||||
bottom: 24px;
|
||||
right: 24px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("ha-config-person", HaConfigPerson);
|
23
src/panels/config/person/show-dialog-person-detail.ts
Normal file
23
src/panels/config/person/show-dialog-person-detail.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { Person, PersonMutableParams } from "../../../data/person";
|
||||
|
||||
export interface PersonDetailDialogParams {
|
||||
entry?: Person;
|
||||
createEntry: (values: PersonMutableParams) => Promise<unknown>;
|
||||
updateEntry: (updates: Partial<PersonMutableParams>) => Promise<unknown>;
|
||||
removeEntry: () => Promise<boolean>;
|
||||
}
|
||||
|
||||
export const loadPersonDetailDialog = () =>
|
||||
import(/* webpackChunkName: "person-detail-dialog" */ "./dialog-person-detail");
|
||||
|
||||
export const showPersonDetailDialog = (
|
||||
element: HTMLElement,
|
||||
systemLogDetailParams: PersonDetailDialogParams
|
||||
): void => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "dialog-person-detail",
|
||||
dialogImport: loadPersonDetailDialog,
|
||||
dialogParams: systemLogDetailParams,
|
||||
});
|
||||
};
|
@ -757,6 +757,10 @@
|
||||
"caption": "Entity Registry",
|
||||
"description": "Overview of all known entities."
|
||||
},
|
||||
"person": {
|
||||
"caption": "People",
|
||||
"description": "Manage the people in Home Assistant."
|
||||
},
|
||||
"integrations": {
|
||||
"caption": "Integrations",
|
||||
"description": "Manage connected devices and services",
|
||||
|
Loading…
x
Reference in New Issue
Block a user