diff --git a/src/components/user/ha-user-picker.ts b/src/components/user/ha-user-picker.ts index ecd99f188f..6b3e865c05 100644 --- a/src/components/user/ha-user-picker.ts +++ b/src/components/user/ha-user-picker.ts @@ -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 []; @@ -40,15 +44,19 @@ class HaUserPicker extends LitElement { protected render(): TemplateResult { return html` - + - No user + ${this.noUserLabel || + this.hass?.localize("ui.components.user-picker.no_user")} ${this._sortedUsers(this.users).map( (user) => html` @@ -67,10 +75,6 @@ class HaUserPicker extends LitElement { `; } - private get _value() { - return this.value || ""; - } - protected firstUpdated(changedProps) { super.firstUpdated(changedProps); if (this.users === undefined) { @@ -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 }); @@ -111,3 +115,9 @@ class HaUserPicker extends LitElement { } customElements.define("ha-user-picker", HaUserPicker); + +declare global { + interface HTMLElementTagNameMap { + "ha-user-picker": HaUserPicker; + } +} diff --git a/src/components/user/ha-users-picker.ts b/src/components/user/ha-users-picker.ts new file mode 100644 index 0000000000..d4fac81d48 --- /dev/null +++ b/src/components/user/ha-users-picker.ts @@ -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` +
+ + + + +
+ ` + ) + )} + + `; + } + + 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) { + 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) { + event.stopPropagation(); + const toAdd = event.detail.value; + (event.currentTarget as any).value = ""; + 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; + } +} diff --git a/src/data/automation.ts b/src/data/automation.ts index acbd13df47..bb35448575 100644 --- a/src/data/automation.ts +++ b/src/data/automation.ts @@ -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 = diff --git a/src/panels/config/automation/trigger/types/ha-automation-trigger-event.ts b/src/panels/config/automation/trigger/types/ha-automation-trigger-event.ts index cb7bd866ca..eed3d63e0f 100644 --- a/src/panels/config/automation/trigger/types/ha-automation-trigger-event.ts +++ b/src/panels/config/automation/trigger/types/ha-automation-trigger-event.ts @@ -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 { @@ -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` +
+ ${this.hass.localize( + "ui.panel.config.automation.editor.triggers.type.event.context_users" + )} + `; } + 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); @@ -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 { diff --git a/src/panels/config/person/dialog-person-detail.ts b/src/panels/config/person/dialog-person-detail.ts index 5844a0374f..67802ea928 100644 --- a/src/panels/config/person/dialog-person-detail.ts +++ b/src/panels/config/person/dialog-person-detail.ts @@ -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"; @@ -440,9 +439,6 @@ class DialogPersonDetail extends LitElement { display: block; padding: 16px 0; } - ha-user-picker { - margin-top: 16px; - } a { color: var(--primary-color); } diff --git a/src/panels/config/zone/dialog-zone-detail.ts b/src/panels/config/zone/dialog-zone-detail.ts index 227c1b0ab8..f31f5e0565 100644 --- a/src/panels/config/zone/dialog-zone-detail.ts +++ b/src/panels/config/zone/dialog-zone-detail.ts @@ -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); } diff --git a/src/translations/en.json b/src/translations/en.json index a060c08866..284f574513 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -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", @@ -1105,7 +1110,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",