Skip to content
28 changes: 19 additions & 9 deletions src/components/user/ha-user-picker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,14 @@ class HaUserPicker extends LitElement {

@property() public label?: string;

@property() public value?: string;
@property() public noUserLabel?: string;

@property() public value = "";

@property() public users?: User[];

@property({ type: Boolean }) public disabled = false;

private _sortedUsers = memoizeOne((users?: User[]) => {
if (!users) {
return [];
Expand All @@ -40,15 +44,19 @@ class HaUserPicker extends LitElement {

protected render(): TemplateResult {
return html`
<paper-dropdown-menu-light .label=${this.label}>
<paper-dropdown-menu-light
.label=${this.label}
.disabled=${this.disabled}
>
<paper-listbox
slot="dropdown-content"
.selected=${this._value}
.selected=${this.value}
attr-for-selected="data-user-id"
@iron-select=${this._userChanged}
>
<paper-icon-item data-user-id="">
No user
${this.noUserLabel ||
this.hass?.localize("ui.components.user-picker.no_user")}
</paper-icon-item>
${this._sortedUsers(this.users).map(
(user) => html`
Expand All @@ -67,10 +75,6 @@ class HaUserPicker extends LitElement {
`;
}

private get _value() {
return this.value || "";
}

protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
if (this.users === undefined) {
Expand All @@ -83,7 +87,7 @@ class HaUserPicker extends LitElement {
private _userChanged(ev) {
const newValue = ev.detail.item.dataset.userId;

if (newValue !== this._value) {
if (newValue !== this.value) {
this.value = ev.detail.value;
setTimeout(() => {
fireEvent(this, "value-changed", { value: newValue });
Expand Down Expand Up @@ -111,3 +115,9 @@ class HaUserPicker extends LitElement {
}

customElements.define("ha-user-picker", HaUserPicker);

declare global {
interface HTMLElementTagNameMap {
"ha-user-picker": HaUserPicker;
}
}
169 changes: 169 additions & 0 deletions src/components/user/ha-users-picker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
TemplateResult,
} from "lit-element";
import { fireEvent } from "../../common/dom/fire_event";
import type { PolymerChangedEvent } from "../../polymer-types";
import type { HomeAssistant } from "../../types";
import { fetchUsers, User } from "../../data/user";
import "./ha-user-picker";
import { mdiClose } from "@mdi/js";
import memoizeOne from "memoize-one";
import { guard } from "lit-html/directives/guard";

@customElement("ha-users-picker")
class HaUsersPickerLight extends LitElement {
@property({ attribute: false }) public hass?: HomeAssistant;

@property() public value?: string[];

@property({ attribute: "picked-user-label" })
public pickedUserLabel?: string;

@property({ attribute: "pick-user-label" })
public pickUserLabel?: string;

@property({ attribute: false })
public users?: User[];

protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
if (this.users === undefined) {
fetchUsers(this.hass!).then((users) => {
this.users = users;
});
}
}

protected render(): TemplateResult {
if (!this.hass || !this.users) {
return html``;
}

const notSelectedUsers = this._notSelectedUsers(this.users, this.value);
return html`
${guard([notSelectedUsers], () =>
this.value?.map(
(user_id, idx) => html`
<div>
<ha-user-picker
.label=${this.pickedUserLabel}
.noUserLabel=${this.hass?.localize(
"ui.components.user-picker.remove_user"
)}
.index=${idx}
.hass=${this.hass}
.value=${user_id}
.users=${this._notSelectedUsersAndSelected(
user_id,
this.users,
notSelectedUsers
)}
@value-changed=${this._userChanged}
></ha-user-picker>
<mwc-icon-button .userId=${user_id} @click=${this._removeUser}>
<ha-svg-icon .path=${mdiClose}></ha-svg-icon>
</mwc-icon-button>
</div>
`
)
)}
<ha-user-picker
.noUserLabel=${this.pickUserLabel ||
this.hass?.localize("ui.components.user-picker.add_user")}
.hass=${this.hass}
.users=${notSelectedUsers}
.disabled=${!notSelectedUsers?.length}
@value-changed=${this._addUser}
></ha-user-picker>
`;
}

private _notSelectedUsers = memoizeOne(
(users?: User[], currentUsers?: string[]) =>
currentUsers
? users?.filter(
(user) => !user.system_generated && !currentUsers.includes(user.id)
)
: users?.filter((user) => !user.system_generated)
);

private _notSelectedUsersAndSelected = (
userId: string,
users?: User[],
notSelected?: User[]
) => {
const selectedUser = users?.find((user) => user.id === userId);
if (selectedUser) {
return notSelected ? [...notSelected, selectedUser] : [selectedUser];
}
return notSelected;
};

private get _currentUsers() {
return this.value || [];
}

private async _updateUsers(users) {
this.value = users;
fireEvent(this, "value-changed", {
value: users,
});
}

private _userChanged(event: PolymerChangedEvent<string>) {
event.stopPropagation();
const index = (event.currentTarget as any).index;
const newValue = event.detail.value;
const newUsers = [...this._currentUsers];
if (newValue === "") {
newUsers.splice(index, 1);
} else {
newUsers.splice(index, 1, newValue);
}
this._updateUsers(newUsers);
}

private async _addUser(event: PolymerChangedEvent<string>) {
event.stopPropagation();
const toAdd = event.detail.value;
(event.currentTarget as any).value = "";
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bramkragten this doesn't work, because ha-user-picker is doing this:

const newValue = ev.detail.item.dataset.userId;

if (newValue !== this._value) {

And changing the value doesn't change the selected item. What's the best way to resolve this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's actually weird, because it's comparing to ev.detail.item.dataset.userId, yet setting to ev.detail.value:

private _userChanged(ev) {
    const newValue = ev.detail.item.dataset.userId;

    if (newValue !== this._value) {
      this.value = ev.detail.value;
      setTimeout(() => {
        fireEvent(this, "value-changed", { value: newValue });
        fireEvent(this, "change");
      }, 0);
    }
  }

Copy link
Member

@bramkragten bramkragten Oct 16, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that doesn't seem correct. I think newValue should be ev.detail.value?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is really strange. So ev.detail.value doesn't even exist (which means that this.value = ev.detail.value; is probably wrong), and ev.newValue is always undefined.
It seems like ev.detail.item.dataset.userId is indeed the right way to grab the currently selected user, but it's unclear how to programmatically select a user...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a red herring - this method on ha-user-picker isn't even called by setting value to "", and it's unclear that value even has any meaning (I thought it was called, because it was called twice when picking a user, so I assumed one of those was a result of setting value to "").
I'm completely stumped 🤷‍♂️

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll put this PR on my todo list :-)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any chance we can get it in time for the beta? The core feature is already merged

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll try 🙈

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be fixed now

if (!toAdd) {
return;
}
const currentUsers = this._currentUsers;
if (currentUsers.includes(toAdd)) {
return;
}

this._updateUsers([...currentUsers, toAdd]);
}

private _removeUser(event) {
const userId = (event.currentTarget as any).userId;
this._updateUsers(this._currentUsers.filter((user) => user !== userId));
}

static get styles(): CSSResult {
return css`
:host {
display: block;
}
div {
display: flex;
align-items: center;
}
`;
}
}

declare global {
interface HTMLElementTagNameMap {
"ha-users-picker": HaUsersPickerLight;
}
}
9 changes: 8 additions & 1 deletion src/data/automation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,10 +109,17 @@ export interface TemplateTrigger {
value_template: string;
}

export interface ContextConstraint {
context_id?: string;
parent_id?: string;
user_id?: string | string[];
}

export interface EventTrigger {
platform: "event";
event_type: string;
event_data: any;
event_data?: any;
context?: ContextConstraint;
}

export type Trigger =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ import "@polymer/paper-input/paper-input";
import { customElement, LitElement, property } from "lit-element";
import { html } from "lit-html";
import "../../../../../components/ha-yaml-editor";
import { fireEvent } from "../../../../../common/dom/fire_event";
import { EventTrigger } from "../../../../../data/automation";
import { HomeAssistant } from "../../../../../types";
import {
handleChangeEvent,
TriggerElement,
} from "../ha-automation-trigger-row";
import "../../../../../components/user/ha-users-picker";

@customElement("ha-automation-trigger-event")
export class HaEventTrigger extends LitElement implements TriggerElement {
Expand All @@ -16,11 +18,11 @@ export class HaEventTrigger extends LitElement implements TriggerElement {
@property() public trigger!: EventTrigger;

public static get defaultConfig() {
return { event_type: "", event_data: {} };
return { event_type: "" };
}

protected render() {
const { event_type, event_data } = this.trigger;
const { event_type, event_data, context } = this.trigger;
return html`
<paper-input
.label=${this.hass.localize(
Expand All @@ -38,9 +40,34 @@ export class HaEventTrigger extends LitElement implements TriggerElement {
.defaultValue=${event_data}
@value-changed=${this._dataChanged}
></ha-yaml-editor>
<br />
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.type.event.context_users"
)}
<ha-users-picker
.pickedUserLabel=${this.hass.localize(
"ui.panel.config.automation.editor.triggers.type.event.context_user_picked"
)}
.pickUserLabel=${this.hass.localize(
"ui.panel.config.automation.editor.triggers.type.event.context_user_pick"
)}
.hass=${this.hass}
.value=${this._wrapUsersInArray(context?.user_id)}
@value-changed=${this._usersChanged}
></ha-users-picker>
`;
}

private _wrapUsersInArray(user_id: string | string[] | undefined): string[] {
if (!user_id) {
return [];
}
if (typeof user_id === "string") {
return [user_id];
}
return user_id;
}

private _valueChanged(ev: CustomEvent): void {
ev.stopPropagation();
handleChangeEvent(this, ev);
Expand All @@ -53,6 +80,22 @@ export class HaEventTrigger extends LitElement implements TriggerElement {
}
handleChangeEvent(this, ev);
}

private _usersChanged(ev) {
ev.stopPropagation();
const value = { ...this.trigger };
if (!ev.detail.value.length && value.context) {
delete value.context.user_id;
} else {
if (!value.context) {
value.context = {};
}
value.context.user_id = ev.detail.value;
}
fireEvent(this, "value-changed", {
value,
});
}
}

declare global {
Expand Down
4 changes: 0 additions & 4 deletions src/panels/config/person/dialog-person-detail.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import "../../../components/entity/ha-entities-picker";
import { createCloseHeading } from "../../../components/ha-dialog";
import "../../../components/ha-picture-upload";
import type { HaPictureUpload } from "../../../components/ha-picture-upload";
import "../../../components/user/ha-user-picker";
import { PersonMutableParams } from "../../../data/person";
import { CropOptions } from "../../../dialogs/image-cropper-dialog/show-image-cropper-dialog";
import { PolymerChangedEvent } from "../../../polymer-types";
Expand Down Expand Up @@ -440,9 +439,6 @@ class DialogPersonDetail extends LitElement {
display: block;
padding: 16px 0;
}
ha-user-picker {
margin-top: 16px;
}
a {
color: var(--primary-color);
}
Expand Down
3 changes: 0 additions & 3 deletions src/panels/config/zone/dialog-zone-detail.ts
Original file line number Diff line number Diff line change
Expand Up @@ -303,9 +303,6 @@ class DialogZoneDetail extends LitElement {
ha-location-editor {
margin-top: 16px;
}
ha-user-picker {
margin-top: 16px;
}
a {
color: var(--primary-color);
}
Expand Down
10 changes: 9 additions & 1 deletion src/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,11 @@
"show_attributes": "Show attributes"
}
},
"user-picker": {
"no_user": "No user",
"add_user": "Add user",
"remove_user": "Remove user"
},
"device-picker": {
"clear": "Clear",
"toggle": "Toggle",
Expand Down Expand Up @@ -1089,7 +1094,10 @@
"event": {
"label": "Event",
"event_type": "Event type",
"event_data": "Event data"
"event_data": "Event data",
"context_users": "Limit to events triggered by",
"context_user_picked": "User firing event",
"context_user_pick": "Add user"
},
"geo_location": {
"label": "Geolocation",
Expand Down