diff --git a/src/components/entity/ha-entity-picker.ts b/src/components/entity/ha-entity-picker.ts index 3cafa0a88b..94ddcf4517 100644 --- a/src/components/entity/ha-entity-picker.ts +++ b/src/components/entity/ha-entity-picker.ts @@ -93,6 +93,8 @@ export class HaEntityPicker extends LitElement { @property() public entityFilter?: HaEntityPickerEntityFilterFunc; + @property({ type: Boolean }) public hideClearIcon = false; + @property({ type: Boolean }) private _opened = false; @query("vaadin-combo-box-light") private _comboBox!: HTMLElement; @@ -204,7 +206,7 @@ export class HaEntityPicker extends LitElement { autocorrect="off" spellcheck="false" > - ${this.value + ${this.value && !this.hideClearIcon ? html` { - return false; + if (!this.hass!.auth.external || !this.allowExoPlayer) { + return false; + } + const externalConfig = await getExternalConfig(this.hass!.auth.external); + return externalConfig && externalConfig.hasExoPlayer; } private async _startHls(): Promise { - let hls: any; const videoEl = this._videoEl; - this._useExoPlayer = await this._getUseExoPlayer(); - if (!this._useExoPlayer) { - hls = ((await import(/* webpackChunkName: "hls.js" */ "hls.js")) as any) - .default as HLSModule; - let hlsSupported = hls.isSupported(); + const playlist_url = this.url.replace("master_playlist", "playlist"); + const useExoPlayerPromise = this._getUseExoPlayer(); + const masterPlaylistPromise = fetch(this.url); - if (!hlsSupported) { - hlsSupported = - videoEl.canPlayType("application/vnd.apple.mpegurl") !== ""; - } + const hls = ((await import( + /* webpackChunkName: "hls.js" */ "hls.js" + )) as any).default as HLSModule; + let hlsSupported = hls.isSupported(); - if (!hlsSupported) { - this._videoEl.innerHTML = this.hass.localize( - "ui.components.media-browser.video_not_supported" - ); - return; - } + if (!hlsSupported) { + hlsSupported = + videoEl.canPlayType("application/vnd.apple.mpegurl") !== ""; } - const url = this.url; + if (!hlsSupported) { + this._videoEl.innerHTML = this.hass.localize( + "ui.components.media-browser.video_not_supported" + ); + return; + } + this._useExoPlayer = await useExoPlayerPromise; + let hevcRegexp: RegExp; + let masterPlaylist: string; if (this._useExoPlayer) { - this._renderHLSExoPlayer(url); + hevcRegexp = /CODECS=".*?((hev1)|(hvc1))\..*?"/; + masterPlaylist = await (await masterPlaylistPromise).text(); + } + if (this._useExoPlayer && hevcRegexp!.test(masterPlaylist!)) { + this._renderHLSExoPlayer(playlist_url); } else if (hls.isSupported()) { - this._renderHLSPolyfill(videoEl, hls, url); + this._renderHLSPolyfill(videoEl, hls, playlist_url); } else { - this._renderHLSNative(videoEl, url); + this._renderHLSNative(videoEl, playlist_url); } } diff --git a/src/components/ha-icon.ts b/src/components/ha-icon.ts index 4dc27b2205..b76adac8b4 100644 --- a/src/components/ha-icon.ts +++ b/src/components/ha-icon.ts @@ -31,9 +31,7 @@ interface DeprecatedIcon { }; } -const mdiDeprecatedIcons: DeprecatedIcon = { - scooter: { removeIn: "117", newName: "human-scooter" }, -}; +const mdiDeprecatedIcons: DeprecatedIcon = {}; const chunks: Chunks = {}; diff --git a/src/data/lovelace.ts b/src/data/lovelace.ts index bf8f4c726f..d0a19bf20c 100644 --- a/src/data/lovelace.ts +++ b/src/data/lovelace.ts @@ -92,6 +92,7 @@ export interface LovelaceViewElement extends HTMLElement { index?: number; cards?: Array; badges?: LovelaceBadge[]; + setConfig(config: LovelaceViewConfig): void; } export interface ShowViewConfig { diff --git a/src/panels/config/ha-panel-config.ts b/src/panels/config/ha-panel-config.ts index 1857338ac7..457f9bf1c8 100644 --- a/src/panels/config/ha-panel-config.ts +++ b/src/panels/config/ha-panel-config.ts @@ -136,6 +136,7 @@ export const configSections: { [name: string]: PageNavigation[] } = { translationKey: "ui.panel.config.users.caption", iconPath: mdiBadgeAccountHorizontal, core: true, + advancedOnly: true, }, ], general: [ diff --git a/src/panels/config/person/dialog-person-detail.ts b/src/panels/config/person/dialog-person-detail.ts index 043d407607..5844a0374f 100644 --- a/src/panels/config/person/dialog-person-detail.ts +++ b/src/panels/config/person/dialog-person-detail.ts @@ -22,6 +22,22 @@ import { haStyleDialog } from "../../../resources/styles"; import { HomeAssistant } from "../../../types"; import { documentationUrl } from "../../../util/documentation-url"; import { PersonDetailDialogParams } from "./show-dialog-person-detail"; +import "../../../components/ha-formfield"; +import { computeRTLDirection } from "../../../common/util/compute_rtl"; +import { + User, + SYSTEM_GROUP_ID_ADMIN, + deleteUser, + SYSTEM_GROUP_ID_USER, + updateUser, +} from "../../../data/user"; +import { + showAlertDialog, + showPromptDialog, + showConfirmationDialog, +} from "../../../dialogs/generic/show-dialog-box"; +import { adminChangePassword } from "../../../data/auth"; +import { showAddUserDialog } from "../users/show-dialog-add-user"; const includeDomains = ["device_tracker"]; @@ -39,6 +55,10 @@ class DialogPersonDetail extends LitElement { @internalProperty() private _userId?: string; + @internalProperty() private _user?: User; + + @internalProperty() private _isAdmin?: boolean; + @internalProperty() private _deviceTrackers!: string[]; @internalProperty() private _picture!: string | null; @@ -64,9 +84,15 @@ class DialogPersonDetail extends LitElement { this._userId = this._params.entry.user_id || undefined; this._deviceTrackers = this._params.entry.device_trackers || []; this._picture = this._params.entry.picture || null; + this._user = this._userId + ? this._params.users.find((user) => user.id === this._userId) + : undefined; + this._isAdmin = this._user?.group_ids.includes(SYSTEM_GROUP_ID_ADMIN); } else { this._name = ""; this._userId = undefined; + this._user = undefined; + this._isAdmin = undefined; this._deviceTrackers = []; this._picture = null; } @@ -115,15 +141,37 @@ class DialogPersonDetail extends LitElement { @change=${this._pictureChanged} > - + + + + + ${this._user + ? html` + + + ` + : ""} ${this._deviceTrackersAvailable(this.hass) ? html`

@@ -185,10 +233,21 @@ class DialogPersonDetail extends LitElement { slot="secondaryAction" class="warning" @click="${this._deleteEntry}" - .disabled=${this._submitting} + .disabled=${(this._user && this._user.is_owner) || + this._submitting} > ${this.hass!.localize("ui.panel.config.person.detail.delete")} + ${this._user && this.hass.user?.is_owner + ? html` + ${this.hass.localize( + "ui.panel.config.users.editor.change_password" + )} + ` + : ""} ` : html``} ) { - this._error = undefined; - this._userId = ev.detail.value; + private async _adminChanged(ev): Promise { + this._isAdmin = ev.target.checked; + } + + private async _allowLoginChanged(ev): Promise { + const target = ev.target; + if (target.checked) { + target.checked = false; + showAddUserDialog(this, { + userAddedCallback: async (user?: User) => { + if (user) { + target.checked = true; + this._user = user; + this._userId = user.id; + this._isAdmin = user.group_ids.includes(SYSTEM_GROUP_ID_ADMIN); + this._params?.refreshUsers(); + } + }, + name: this._name, + }); + } else if (this._userId) { + if ( + !(await showConfirmationDialog(this, { + text: this.hass!.localize( + "ui.panel.config.person.detail.confirm_delete_user", + "name", + this._name + ), + confirmText: this.hass!.localize( + "ui.panel.config.person.detail.delete" + ), + dismissText: this.hass!.localize("ui.common.cancel"), + })) + ) { + target.checked = true; + return; + } + await deleteUser(this.hass, this._userId); + this._params?.refreshUsers(); + this._userId = undefined; + } } private _deviceTrackersChanged(ev: PolymerChangedEvent) { @@ -228,9 +325,70 @@ class DialogPersonDetail extends LitElement { this._picture = (ev.target as HaPictureUpload).value; } + private async _changePassword() { + if (!this._user) { + return; + } + const credential = this._user.credentials.find( + (cred) => cred.type === "homeassistant" + ); + if (!credential) { + showAlertDialog(this, { + title: "No Home Assistant credentials found.", + }); + return; + } + const newPassword = await showPromptDialog(this, { + title: this.hass.localize("ui.panel.config.users.editor.change_password"), + inputType: "password", + inputLabel: this.hass.localize( + "ui.panel.config.users.editor.new_password" + ), + }); + if (!newPassword) { + return; + } + const confirmPassword = await showPromptDialog(this, { + title: this.hass.localize("ui.panel.config.users.editor.change_password"), + inputType: "password", + inputLabel: this.hass.localize( + "ui.panel.config.users.add_user.password_confirm" + ), + }); + if (!confirmPassword) { + return; + } + if (newPassword !== confirmPassword) { + showAlertDialog(this, { + title: this.hass.localize( + "ui.panel.config.users.add_user.password_not_match" + ), + }); + return; + } + await adminChangePassword(this.hass, this._user.id, newPassword); + showAlertDialog(this, { + title: this.hass.localize( + "ui.panel.config.users.editor.password_changed" + ), + }); + } + private async _updateEntry() { this._submitting = true; try { + if ( + (this._userId && this._name !== this._params!.entry?.name) || + this._isAdmin !== this._user?.group_ids.includes(SYSTEM_GROUP_ID_ADMIN) + ) { + await updateUser(this.hass!, this._userId!, { + name: this._name.trim(), + group_ids: [ + this._isAdmin ? SYSTEM_GROUP_ID_ADMIN : SYSTEM_GROUP_ID_USER, + ], + }); + this._params?.refreshUsers(); + } const values: PersonMutableParams = { name: this._name.trim(), device_trackers: this._deviceTrackers, @@ -254,6 +412,9 @@ class DialogPersonDetail extends LitElement { this._submitting = true; try { if (await this._params!.removeEntry()) { + if (this._params!.entry!.user_id) { + deleteUser(this.hass, this._params!.entry!.user_id); + } this._params = undefined; } } finally { @@ -275,6 +436,10 @@ class DialogPersonDetail extends LitElement { ha-picture-upload { display: block; } + ha-formfield { + display: block; + padding: 16px 0; + } ha-user-picker { margin-top: 16px; } diff --git a/src/panels/config/person/ha-config-person.ts b/src/panels/config/person/ha-config-person.ts index 3d1de87bd0..829b12278a 100644 --- a/src/panels/config/person/ha-config-person.ts +++ b/src/panels/config/person/ha-config-person.ts @@ -1,6 +1,7 @@ import { mdiPlus } from "@mdi/js"; import "@polymer/paper-item/paper-icon-item"; import "@polymer/paper-item/paper-item-body"; +import "@material/mwc-fab"; import { css, CSSResult, @@ -223,6 +224,9 @@ class HaConfigPerson extends LitElement { return false; } }, + refreshUsers: () => { + this._usersLoad = fetchUsers(this.hass!); + }, }); } diff --git a/src/panels/config/person/show-dialog-person-detail.ts b/src/panels/config/person/show-dialog-person-detail.ts index 4f2f3f0e1c..3ffd16577f 100644 --- a/src/panels/config/person/show-dialog-person-detail.ts +++ b/src/panels/config/person/show-dialog-person-detail.ts @@ -5,6 +5,7 @@ import { User } from "../../../data/user"; export interface PersonDetailDialogParams { entry?: Person; users: User[]; + refreshUsers: () => void; createEntry: (values: PersonMutableParams) => Promise; updateEntry: (updates: Partial) => Promise; removeEntry: () => Promise; diff --git a/src/panels/config/users/dialog-add-user.ts b/src/panels/config/users/dialog-add-user.ts index 6d6fd0ce76..7d291e9307 100644 --- a/src/panels/config/users/dialog-add-user.ts +++ b/src/panels/config/users/dialog-add-user.ts @@ -50,15 +50,24 @@ export class DialogAddUser extends LitElement { @internalProperty() private _isAdmin?: boolean; + @internalProperty() private _allowChangeName = true; + public showDialog(params: AddUserDialogParams) { this._params = params; - this._name = ""; + this._name = this._params.name || ""; this._username = ""; this._password = ""; this._passwordConfirm = ""; this._isAdmin = false; this._error = undefined; this._loading = false; + + if (this._params.name) { + this._allowChangeName = false; + this._maybePopulateUsername(); + } else { + this._allowChangeName = true; + } } protected firstUpdated(changedProperties: PropertyValues) { @@ -84,19 +93,22 @@ export class DialogAddUser extends LitElement { >

${this._error ? html`
${this._error}
` : ""} - - + ${this._allowChangeName + ? html` ` + : ""} diff --git a/src/panels/config/users/show-dialog-add-user.ts b/src/panels/config/users/show-dialog-add-user.ts index 5ff0e375e9..4ed4b18f9e 100644 --- a/src/panels/config/users/show-dialog-add-user.ts +++ b/src/panels/config/users/show-dialog-add-user.ts @@ -3,6 +3,7 @@ import { User } from "../../../data/user"; export interface AddUserDialogParams { userAddedCallback: (user: User) => void; + name?: string; } export const loadAddUserDialog = () => diff --git a/src/panels/logbook/ha-logbook.ts b/src/panels/logbook/ha-logbook.ts index 3d86c88b3d..3c451a0d8d 100644 --- a/src/panels/logbook/ha-logbook.ts +++ b/src/panels/logbook/ha-logbook.ts @@ -244,8 +244,7 @@ class HaLogbook extends LitElement { line-height: 2em; padding: 8px 16px; box-sizing: border-box; - border-top: 1px solid - var(--mdc-dialog-scroll-divider-color, rgba(0, 0, 0, 0.12)); + border-top: 1px solid var(--divider-color); } .entry.no-entity, diff --git a/src/panels/lovelace/cards/hui-entities-card.ts b/src/panels/lovelace/cards/hui-entities-card.ts index 90383f3bdb..2ef80eaa7f 100644 --- a/src/panels/lovelace/cards/hui-entities-card.ts +++ b/src/panels/lovelace/cards/hui-entities-card.ts @@ -3,8 +3,8 @@ import { CSSResult, customElement, html, - LitElement, internalProperty, + LitElement, PropertyValues, TemplateResult, } from "lit-element"; @@ -13,19 +13,23 @@ import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_elemen import { computeDomain } from "../../../common/entity/compute_domain"; import "../../../components/ha-card"; import { HomeAssistant } from "../../../types"; +import { computeCardSize } from "../common/compute-card-size"; import { findEntities } from "../common/find-entites"; import { processConfigEntities } from "../common/process-config-entities"; import "../components/hui-entities-toggle"; import { createHeaderFooterElement } from "../create-element/create-header-footer-element"; import { createRowElement } from "../create-element/create-row-element"; -import { LovelaceRow } from "../entity-rows/types"; +import { + EntityConfig, + LovelaceRow, + LovelaceRowConfig, +} from "../entity-rows/types"; import { LovelaceCard, LovelaceCardEditor, LovelaceHeaderFooter, } from "../types"; -import { EntitiesCardConfig, EntitiesCardEntityConfig } from "./types"; -import { computeCardSize } from "../common/compute-card-size"; +import { EntitiesCardConfig } from "./types"; @customElement("hui-entities-card") class HuiEntitiesCard extends LitElement implements LovelaceCard { @@ -57,7 +61,7 @@ class HuiEntitiesCard extends LitElement implements LovelaceCard { private _hass?: HomeAssistant; - private _configEntities?: EntitiesCardEntityConfig[]; + private _configEntities?: LovelaceRowConfig[]; private _showHeaderToggle?: boolean; @@ -115,7 +119,7 @@ class HuiEntitiesCard extends LitElement implements LovelaceCard { // Default value is show toggle if we can at least toggle 2 entities. let toggleable = 0; for (const rowConf of entities) { - if (!rowConf.entity) { + if (!("entity" in rowConf)) { continue; } toggleable += Number(DOMAINS_TOGGLE.has(computeDomain(rowConf.entity))); @@ -188,7 +192,7 @@ class HuiEntitiesCard extends LitElement implements LovelaceCard { ? html` ` : ""} @@ -198,10 +202,10 @@ class HuiEntitiesCard extends LitElement implements LovelaceCard { ? html`` : html` "type" in conf + ) as EntityConfig[]).map((conf) => conf.entity)} > `}
@@ -285,20 +289,20 @@ class HuiEntitiesCard extends LitElement implements LovelaceCard { `; } - private renderEntity(entityConf: EntitiesCardEntityConfig): TemplateResult { + private renderEntity(entityConf: LovelaceRowConfig): TemplateResult { const element = createRowElement( - this._config!.state_color - ? { + !("type" in entityConf) && this._config!.state_color + ? ({ state_color: true, - ...entityConf, - } + ...(entityConf as EntityConfig), + } as EntityConfig) : entityConf ); if (this._hass) { element.hass = this._hass; } - return html`
${element}
`; + return html`
${element}
`; } } diff --git a/src/panels/lovelace/cards/types.ts b/src/panels/lovelace/cards/types.ts index 8c2923967d..78997f8b31 100644 --- a/src/panels/lovelace/cards/types.ts +++ b/src/panels/lovelace/cards/types.ts @@ -3,7 +3,11 @@ import { FullCalendarView } from "../../../types"; import { Condition } from "../common/validate-condition"; import { HuiImage } from "../components/hui-image"; import { LovelaceElementConfig } from "../elements/types"; -import { EntityConfig, EntityFilterEntityConfig } from "../entity-rows/types"; +import { + EntityConfig, + EntityFilterEntityConfig, + LovelaceRowConfig, +} from "../entity-rows/types"; import { LovelaceHeaderFooterConfig } from "../header-footer/types"; export interface AlarmPanelCardConfig extends LovelaceCardConfig { @@ -60,7 +64,7 @@ export interface EntitiesCardConfig extends LovelaceCardConfig { type: "entities"; show_header_toggle?: boolean; title?: string; - entities: Array; + entities: Array; theme?: string; icon?: string; header?: LovelaceHeaderFooterConfig; diff --git a/src/panels/lovelace/common/has-changed.ts b/src/panels/lovelace/common/has-changed.ts index d4b3320410..db4b9de518 100644 --- a/src/panels/lovelace/common/has-changed.ts +++ b/src/panels/lovelace/common/has-changed.ts @@ -56,6 +56,7 @@ export function hasConfigOrEntitiesChanged( return entities.some( (entity) => + "entity" in entity && oldHass.states[entity.entity] !== element.hass!.states[entity.entity] ); } diff --git a/src/panels/lovelace/common/process-config-entities.ts b/src/panels/lovelace/common/process-config-entities.ts index e0b9045432..7d9a858227 100644 --- a/src/panels/lovelace/common/process-config-entities.ts +++ b/src/panels/lovelace/common/process-config-entities.ts @@ -1,8 +1,10 @@ // Parse array of entity objects from config import { isValidEntityId } from "../../../common/entity/valid_entity_id"; -import { EntityConfig } from "../entity-rows/types"; +import { EntityConfig, LovelaceRowConfig } from "../entity-rows/types"; -export const processConfigEntities = ( +export const processConfigEntities = < + T extends EntityConfig | LovelaceRowConfig +>( entities: Array ): T[] => { if (!entities || !Array.isArray(entities)) { @@ -24,7 +26,7 @@ export const processConfigEntities = ( if (typeof entityConf === "string") { config = { entity: entityConf } as T; } else if (typeof entityConf === "object" && !Array.isArray(entityConf)) { - if (!entityConf.entity) { + if (!("entity" in entityConf)) { throw new Error( `Entity object at position ${index} is missing entity field.` ); @@ -34,9 +36,11 @@ export const processConfigEntities = ( throw new Error(`Invalid entity specified at position ${index}.`); } - if (!isValidEntityId(config.entity)) { + if (!isValidEntityId((config as EntityConfig).entity!)) { throw new Error( - `Invalid entity ID at position ${index}: ${config.entity}` + `Invalid entity ID at position ${index}: ${ + (config as EntityConfig).entity + }` ); } diff --git a/src/panels/lovelace/create-element/create-element-base.ts b/src/panels/lovelace/create-element/create-element-base.ts index 23b2083e1e..a857921f8e 100644 --- a/src/panels/lovelace/create-element/create-element-base.ts +++ b/src/panels/lovelace/create-element/create-element-base.ts @@ -16,6 +16,7 @@ import { LovelaceCard, LovelaceCardConstructor, LovelaceHeaderFooter, + LovelaceRowConstructor, } from "../types"; const TIMEOUT = 2000; @@ -39,7 +40,7 @@ interface CreateElementConfigTypes { row: { config: LovelaceRowConfig; element: LovelaceRow; - constructor: unknown; + constructor: LovelaceRowConstructor; }; "header-footer": { config: LovelaceHeaderFooterConfig; diff --git a/src/panels/lovelace/create-element/create-row-element.ts b/src/panels/lovelace/create-element/create-row-element.ts index 433b50e0a5..f697b89adf 100644 --- a/src/panels/lovelace/create-element/create-row-element.ts +++ b/src/panels/lovelace/create-element/create-row-element.ts @@ -4,11 +4,14 @@ import "../entity-rows/hui-script-entity-row"; import "../entity-rows/hui-sensor-entity-row"; import "../entity-rows/hui-text-entity-row"; import "../entity-rows/hui-toggle-entity-row"; -import { EntityConfig } from "../entity-rows/types"; +import { LovelaceRowConfig } from "../entity-rows/types"; import "../special-rows/hui-attribute-row"; import "../special-rows/hui-button-row"; import "../special-rows/hui-call-service-row"; -import { createLovelaceElement } from "./create-element-base"; +import { + createLovelaceElement, + getLovelaceElementClass, +} from "./create-element-base"; const ALWAYS_LOADED_TYPES = new Set([ "media-player-entity", @@ -74,7 +77,7 @@ const DOMAIN_TO_ELEMENT_TYPE = { weather: "weather", }; -export const createRowElement = (config: EntityConfig) => +export const createRowElement = (config: LovelaceRowConfig) => createLovelaceElement( "row", config, @@ -83,3 +86,12 @@ export const createRowElement = (config: EntityConfig) => DOMAIN_TO_ELEMENT_TYPE, undefined ); + +export const getRowElementClass = (type: string) => { + return getLovelaceElementClass( + type, + "row", + ALWAYS_LOADED_TYPES, + LAZY_LOAD_TYPES + ); +}; diff --git a/src/panels/lovelace/editor/card-editor/hui-dialog-edit-card.ts b/src/panels/lovelace/editor/card-editor/hui-dialog-edit-card.ts index e700cd097b..aa45460df6 100755 --- a/src/panels/lovelace/editor/card-editor/hui-dialog-edit-card.ts +++ b/src/panels/lovelace/editor/card-editor/hui-dialog-edit-card.ts @@ -1,42 +1,42 @@ +import { mdiHelpCircle } from "@mdi/js"; import deepFreeze from "deep-freeze"; import { css, CSSResultArray, customElement, html, + internalProperty, LitElement, property, - internalProperty, + PropertyValues, query, TemplateResult, - PropertyValues, } from "lit-element"; -import { mdiHelpCircle } from "@mdi/js"; - -import { fireEvent } from "../../../../common/dom/fire_event"; -import { haStyleDialog } from "../../../../resources/styles"; -import { showSaveSuccessToast } from "../../../../util/toast-saved-success"; -import { addCard, replaceCard } from "../config-util"; -import { getCardDocumentationURL } from "../get-card-documentation-url"; -import { computeRTLDirection } from "../../../../common/util/compute_rtl"; -import { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog-box"; - -import type { HomeAssistant } from "../../../../types"; -import type { GUIModeChangedEvent } from "../types"; -import type { ConfigChangedEvent, HuiCardEditor } from "./hui-card-editor"; -import type { EditCardDialogParams } from "./show-edit-card-dialog"; -import type { HassDialog } from "../../../../dialogs/make-dialog-manager"; import type { HASSDomEvent } from "../../../../common/dom/fire_event"; +import { fireEvent } from "../../../../common/dom/fire_event"; +import { computeRTLDirection } from "../../../../common/util/compute_rtl"; +import "../../../../components/ha-circular-progress"; +import "../../../../components/ha-dialog"; +import "../../../../components/ha-header-bar"; import type { LovelaceCardConfig, LovelaceViewConfig, } from "../../../../data/lovelace"; - -import "./hui-card-editor"; +import { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog-box"; +import type { HassDialog } from "../../../../dialogs/make-dialog-manager"; +import { haStyleDialog } from "../../../../resources/styles"; +import type { HomeAssistant } from "../../../../types"; +import { showSaveSuccessToast } from "../../../../util/toast-saved-success"; +import { addCard, replaceCard } from "../config-util"; +import { getCardDocumentationURL } from "../get-card-documentation-url"; +import "../hui-element-editor"; +import type { + ConfigChangedEvent, + HuiElementEditor, +} from "../hui-element-editor"; +import type { GUIModeChangedEvent } from "../types"; import "./hui-card-preview"; -import "../../../../components/ha-dialog"; -import "../../../../components/ha-header-bar"; -import "../../../../components/ha-circular-progress"; +import type { EditCardDialogParams } from "./show-edit-card-dialog"; declare global { // for fire event @@ -65,7 +65,7 @@ export class HuiDialogEditCard extends LitElement implements HassDialog { @internalProperty() private _guiModeAvailable? = true; - @query("hui-card-editor") private _cardEditorEl?: HuiCardEditor; + @query("hui-element-editor") private _cardEditorEl?: HuiElementEditor; @internalProperty() private _GUImode = true; @@ -183,14 +183,14 @@ export class HuiDialogEditCard extends LitElement implements HassDialog {
- + >
- ha-switch { - padding: 16px 0; - } - .side-by-side { - display: flex; - } - .side-by-side > * { - flex: 1; - padding-right: 4px; - } - .suffix { - margin: 0 8px; - } - +export const configElementStyle = css` + ha-switch { + padding: 16px 0; + } + .side-by-side { + display: flex; + } + .side-by-side > * { + flex: 1; + padding-right: 4px; + } + .suffix { + margin: 0 8px; + } `; diff --git a/src/panels/lovelace/editor/config-elements/hui-alarm-panel-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-alarm-panel-card-editor.ts index bf9c963199..f21af05f04 100644 --- a/src/panels/lovelace/editor/config-elements/hui-alarm-panel-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-alarm-panel-card-editor.ts @@ -3,14 +3,15 @@ import "@polymer/paper-item/paper-item"; import "@polymer/paper-listbox/paper-listbox"; import { css, - CSSResult, + CSSResultArray, customElement, html, + internalProperty, LitElement, property, - internalProperty, TemplateResult, } from "lit-element"; +import { array, assert, object, optional, string } from "superstruct"; import { fireEvent } from "../../../../common/dom/fire_event"; import "../../../../components/entity/ha-entity-picker"; import "../../../../components/ha-icon"; @@ -20,7 +21,6 @@ import "../../components/hui-theme-select-editor"; import { LovelaceCardEditor } from "../../types"; import { EditorTarget, EntitiesEditorEvent } from "../types"; import { configElementStyle } from "./config-elements-style"; -import { assert, object, string, optional, array } from "superstruct"; const cardConfigStruct = object({ type: string(), @@ -68,7 +68,6 @@ export class HuiAlarmPanelCardEditor extends LitElement const states = ["arm_home", "arm_away", "arm_night", "arm_custom_bypass"]; return html` - ${configElementStyle}
- + > ` : html` + ${this.hass.localize( + "ui.panel.lovelace.editor.card.entities.entity_row_editor" + )} + + + `; + } + return html` - ${configElementStyle}
): void { + this._editRowIndex = ev.detail.index; + this._editRowConfig = this._configEntities![this._editRowIndex]; + } + + private _goBack(): void { + this._editRowIndex = undefined; + this._editRowConfig = undefined; + this._editRowGuiModeAvailable = true; + this._editRowGuiMode = true; + } + + private _toggleMode(): void { + this._cardEditorEl?.toggleMode(); + } + + private _handleEntityRowConfigChanged(ev: CustomEvent): void { + ev.stopPropagation(); + const value = ev.detail.config as LovelaceRowConfig; + this._editRowGuiModeAvailable = ev.detail.guiModeAvailable; + + const newConfigEntities = this._configEntities!.concat(); + + if (!value) { + newConfigEntities.splice(this._editRowIndex!, 1); + this._goBack(); + } else { + newConfigEntities[this._editRowIndex!] = value; + } + + this._editRowConfig = value; + + this._config = { ...this._config!, entities: newConfigEntities }; + + this._configEntities = processEditorEntities(this._config!.entities); + + fireEvent(this, "config-changed", { config: this._config! }); + } + + private _handleGUIModeChanged(ev: HASSDomEvent): void { + ev.stopPropagation(); + this._editRowGuiMode = ev.detail.guiMode; + this._editRowGuiModeAvailable = ev.detail.guiModeAvailable; + } + + static get styles(): CSSResultArray { + return [ + configElementStyle, + css` + .edit-entity-row-header { + display: flex; + justify-content: space-between; + align-items: center; + font-size: 18px; + } + `, + ]; + } } declare global { diff --git a/src/panels/lovelace/editor/config-elements/hui-entity-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-entity-card-editor.ts index 49a9db601c..f4d8f54921 100644 --- a/src/panels/lovelace/editor/config-elements/hui-entity-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-entity-card-editor.ts @@ -1,5 +1,6 @@ import "@polymer/paper-input/paper-input"; import { + CSSResult, customElement, html, internalProperty, @@ -75,7 +76,6 @@ export class HuiEntityCardEditor extends LitElement } return html` - ${configElementStyle}
+ +
+ + +
+ + + ${this.hass!.localize( + "ui.panel.lovelace.editor.card.entities.secondary_info_values.none" + )} + ${Object.keys(SecondaryInfoValues).map((info) => { + if ( + !("domains" in SecondaryInfoValues[info]) || + ("domains" in SecondaryInfoValues[info] && + SecondaryInfoValues[info].domains!.includes(domain)) + ) { + return html` + ${this.hass!.localize( + `ui.panel.lovelace.editor.card.entities.secondary_info_values.${info}` + )} + `; + } + return ""; + })} + + +
+ `; + } + + private _valueChanged(ev: EntitiesEditorEvent): void { + if (!this._config || !this.hass) { + return; + } + const target = ev.target! as EditorTarget; + const value = target.value || ev.detail?.item?.value; + + if (this[`_${target.configValue}`] === value) { + return; + } + + if (target.configValue) { + if (value === "" || !value) { + this._config = { ...this._config }; + delete this._config[target.configValue!]; + } else { + this._config = { + ...this._config, + [target.configValue!]: value, + }; + } + } + + fireEvent(this, "config-changed", { config: this._config }); + } + + static get styles(): CSSResult { + return configElementStyle; + } +} + +declare global { + interface HTMLElementTagNameMap { + "hui-generic-entity-row-editor": HuiGenericEntityRowEditor; + } +} diff --git a/src/panels/lovelace/editor/config-elements/hui-glance-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-glance-card-editor.ts index 7d3de02655..9372cba30b 100644 --- a/src/panels/lovelace/editor/config-elements/hui-glance-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-glance-card-editor.ts @@ -2,19 +2,31 @@ import "@polymer/paper-dropdown-menu/paper-dropdown-menu"; import "@polymer/paper-item/paper-item"; import "@polymer/paper-listbox/paper-listbox"; import { + CSSResult, customElement, html, + internalProperty, LitElement, property, - internalProperty, TemplateResult, } from "lit-element"; +import { + array, + assert, + boolean, + number, + object, + optional, + string, + union, +} from "superstruct"; import { fireEvent } from "../../../../common/dom/fire_event"; +import { computeRTLDirection } from "../../../../common/util/compute_rtl"; import "../../../../components/entity/state-badge"; import "../../../../components/ha-card"; +import "../../../../components/ha-formfield"; import "../../../../components/ha-icon"; import "../../../../components/ha-switch"; -import "../../../../components/ha-formfield"; import { HomeAssistant } from "../../../../types"; import { ConfigEntity, GlanceCardConfig } from "../../cards/types"; import "../../components/hui-entity-editor"; @@ -27,17 +39,6 @@ import { EntitiesEditorEvent, } from "../types"; import { configElementStyle } from "./config-elements-style"; -import { computeRTLDirection } from "../../../../common/util/compute_rtl"; -import { - string, - union, - object, - optional, - number, - boolean, - assert, - array, -} from "superstruct"; const cardConfigStruct = object({ type: string(), @@ -102,7 +103,6 @@ export class HuiGlanceCardEditor extends LitElement const dir = computeRTLDirection(this.hass!); return html` - ${configElementStyle}
): void { assert(config, cardConfigStruct); @@ -128,13 +129,13 @@ export class HuiStackCardEditor extends LitElement >
- + > ` : html` +
+ + + + +
+ + ${this.hass.localize( + this.guiMode + ? "ui.panel.lovelace.editor.edit_card.show_code_editor" + : "ui.panel.lovelace.editor.edit_card.show_visual_editor" + )} + +
+ + `; + } + + private _goBack(): void { + fireEvent(this, "go-back"); + } + + private _toggleMode(): void { + fireEvent(this, "toggle-gui-mode"); + } + + static get styles(): CSSResult { + return css` + .header { + display: flex; + justify-content: space-between; + align-items: center; + } + .back-title { + display: flex; + align-items: center; + font-size: 18px; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "hui-detail-editor-base": HuiDetailEditorBase; + } +} diff --git a/src/panels/lovelace/editor/card-editor/hui-card-editor.ts b/src/panels/lovelace/editor/hui-element-editor.ts similarity index 70% rename from src/panels/lovelace/editor/card-editor/hui-card-editor.ts rename to src/panels/lovelace/editor/hui-element-editor.ts index ff4613ed0e..c8f6078096 100644 --- a/src/panels/lovelace/editor/card-editor/hui-card-editor.ts +++ b/src/panels/lovelace/editor/hui-element-editor.ts @@ -11,26 +11,33 @@ import { query, TemplateResult, } from "lit-element"; -import { fireEvent } from "../../../../common/dom/fire_event"; -import { computeRTL } from "../../../../common/util/compute_rtl"; -import { deepEqual } from "../../../../common/util/deep-equal"; -import "../../../../components/ha-circular-progress"; -import "../../../../components/ha-code-editor"; -import type { HaCodeEditor } from "../../../../components/ha-code-editor"; +import { fireEvent } from "../../../common/dom/fire_event"; +import { computeRTL } from "../../../common/util/compute_rtl"; +import { deepEqual } from "../../../common/util/deep-equal"; +import "../../../components/ha-circular-progress"; +import "../../../components/ha-code-editor"; +import type { HaCodeEditor } from "../../../components/ha-code-editor"; import type { LovelaceCardConfig, LovelaceConfig, -} from "../../../../data/lovelace"; -import type { HomeAssistant } from "../../../../types"; -import { handleStructError } from "../../common/structs/handle-errors"; -import { getCardElementClass } from "../../create-element/create-card-element"; -import type { LovelaceRowConfig } from "../../entity-rows/types"; -import type { LovelaceCardEditor } from "../../types"; -import { GUISupportError } from "../gui-support-error"; -import type { GUIModeChangedEvent } from "../types"; +} from "../../../data/lovelace"; +import type { HomeAssistant } from "../../../types"; +import { handleStructError } from "../common/structs/handle-errors"; +import { getCardElementClass } from "../create-element/create-card-element"; +import { getRowElementClass } from "../create-element/create-row-element"; +import type { LovelaceRowConfig } from "../entity-rows/types"; +import type { + LovelaceCardConstructor, + LovelaceCardEditor, + LovelaceRowConstructor, + LovelaceRowEditor, +} from "../types"; +import "./config-elements/hui-generic-entity-row-editor"; +import { GUISupportError } from "./gui-support-error"; +import { GUIModeChangedEvent } from "./types"; export interface ConfigChangedEvent { - config: LovelaceCardConfig; + config: LovelaceCardConfig | LovelaceRowConfig; error?: string; guiModeAvailable?: boolean; } @@ -47,21 +54,27 @@ declare global { export interface UIConfigChangedEvent extends Event { detail: { - config: LovelaceCardConfig; + config: LovelaceCardConfig | LovelaceRowConfig; }; } -@customElement("hui-card-editor") -export class HuiCardEditor extends LitElement { +const GENERIC_ROW_TYPE = "generic-row"; + +@customElement("hui-element-editor") +export class HuiElementEditor extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public lovelace?: LovelaceConfig; + @property() public elementType: "row" | "card" = "card"; + @internalProperty() private _yaml?: string; - @internalProperty() private _config?: LovelaceCardConfig; + @internalProperty() private _config?: LovelaceCardConfig | LovelaceRowConfig; - @internalProperty() private _configElement?: LovelaceCardEditor; + @internalProperty() private _configElement?: + | LovelaceCardEditor + | LovelaceRowEditor; @internalProperty() private _configElType?: string; @@ -95,11 +108,11 @@ export class HuiCardEditor extends LitElement { this._setConfig(); } - public get value(): LovelaceCardConfig | undefined { + public get value(): LovelaceCardConfig | LovelaceRowConfig | undefined { return this._config; } - public set value(config: LovelaceCardConfig | undefined) { + public set value(config: LovelaceCardConfig | LovelaceRowConfig | undefined) { if (this._config && deepEqual(config, this._config)) { return; } @@ -220,7 +233,11 @@ export class HuiCardEditor extends LitElement { if (this._configElement && changedProperties.has("hass")) { this._configElement.hass = this.hass; } - if (this._configElement && changedProperties.has("lovelace")) { + if ( + this._configElement && + "lovelace" in this._configElement && + changedProperties.has("lovelace") + ) { this._configElement.lovelace = this.lovelace; } } @@ -244,37 +261,61 @@ export class HuiCardEditor extends LitElement { return; } - const cardType = this.value.type; + let type: string; + + if ( + this.elementType === "row" && + !this.value.type && + "entity" in this.value + ) { + type = GENERIC_ROW_TYPE; + } else { + type = this.value.type!; + } + let configElement = this._configElement; try { this._error = undefined; this._warnings = undefined; - if (this._configElType !== cardType) { - // If the card type has changed, we need to load a new GUI editor - if (!this.value.type) { - throw new Error("No card type defined"); + if (this._configElType !== type) { + // If the type has changed, we need to load a new GUI editor + if (!type) { + throw new Error(`No ${this.elementType} type defined`); } - const elClass = await getCardElementClass(cardType); + let elClass: + | LovelaceCardConstructor + | LovelaceRowConstructor + | undefined; + + if (this.elementType === "card") { + elClass = await getCardElementClass(type); + } else if (this.elementType === "row" && type !== GENERIC_ROW_TYPE) { + elClass = await getRowElementClass(type); + } this._loading = true; // Check if a GUI editor exists if (elClass && elClass.getConfigElement) { configElement = await elClass.getConfigElement(); + } else if (this.elementType === "row" && type === GENERIC_ROW_TYPE) { + configElement = document.createElement( + "hui-generic-entity-row-editor" + ); } else { configElement = undefined; - throw new GUISupportError( - `No visual editor available for: ${cardType}` - ); + throw new GUISupportError(`No visual editor available for: ${type}`); } this._configElement = configElement; - this._configElType = cardType; + this._configElType = type; // Perform final setup this._configElement.hass = this.hass; - this._configElement.lovelace = this.lovelace; + if ("lovelace" in this._configElement) { + this._configElement.lovelace = this.lovelace; + } this._configElement.addEventListener("config-changed", (ev) => this._handleUIConfigChanged(ev as UIConfigChangedEvent) ); @@ -282,6 +323,7 @@ export class HuiCardEditor extends LitElement { // Setup GUI editor and check that it can handle the current config try { + // @ts-ignore this._configElement!.setConfig(this.value); } catch (err) { throw new GUISupportError( @@ -340,6 +382,6 @@ export class HuiCardEditor extends LitElement { declare global { interface HTMLElementTagNameMap { - "hui-card-editor": HuiCardEditor; + "hui-element-editor": HuiElementEditor; } } diff --git a/src/panels/lovelace/editor/hui-entities-card-row-editor.ts b/src/panels/lovelace/editor/hui-entities-card-row-editor.ts index 760d285f9a..0e6a42bdb8 100644 --- a/src/panels/lovelace/editor/hui-entities-card-row-editor.ts +++ b/src/panels/lovelace/editor/hui-entities-card-row-editor.ts @@ -1,4 +1,5 @@ -import { mdiClose, mdiDrag } from "@mdi/js"; +import "@material/mwc-icon-button"; +import { mdiClose, mdiDrag, mdiPencil } from "@mdi/js"; import { css, CSSResult, @@ -19,7 +20,7 @@ import Sortable, { import { fireEvent } from "../../../common/dom/fire_event"; import "../../../components/entity/ha-entity-picker"; import type { HaEntityPicker } from "../../../components/entity/ha-entity-picker"; -import "../../../components/ha-icon-button"; +import "../../../components/ha-svg-icon"; import { sortableStyles } from "../../../resources/ha-sortable-style"; import { HomeAssistant } from "../../../types"; import { EntityConfig, LovelaceRowConfig } from "../entity-rows/types"; @@ -85,26 +86,38 @@ export class HuiEntitiesCardRowEditor extends LitElement { )}
- - -
` : html` `} + + + + + +
`; }) @@ -164,7 +177,7 @@ export class HuiEntitiesCardRowEditor extends LitElement { animation: 150, fallbackClass: "sortable-fallback", handle: ".handle", - onEnd: async (evt: SortableEvent) => this._entityMoved(evt), + onEnd: async (evt: SortableEvent) => this._rowMoved(evt), }); } @@ -180,7 +193,7 @@ export class HuiEntitiesCardRowEditor extends LitElement { fireEvent(this, "entities-changed", { entities: newConfigEntities }); } - private _entityMoved(ev: SortableEvent): void { + private _rowMoved(ev: SortableEvent): void { if (ev.oldIndex === ev.newIndex) { return; } @@ -192,7 +205,7 @@ export class HuiEntitiesCardRowEditor extends LitElement { fireEvent(this, "entities-changed", { entities: newEntities }); } - private _removeSpecialRow(ev: CustomEvent): void { + private _removeRow(ev: CustomEvent): void { const index = (ev.currentTarget as any).index; const newConfigEntities = this.entities!.concat(); @@ -218,6 +231,12 @@ export class HuiEntitiesCardRowEditor extends LitElement { fireEvent(this, "entities-changed", { entities: newConfigEntities }); } + private _editRow(ev: CustomEvent): void { + fireEvent(this, "edit-row", { + index: (ev.currentTarget as any).index, + }); + } + static get styles(): CSSResult[] { return [ sortableStyles, @@ -226,13 +245,16 @@ export class HuiEntitiesCardRowEditor extends LitElement { display: flex; align-items: center; } + .entity .handle { padding-right: 8px; cursor: move; } + .entity ha-entity-picker { flex-grow: 1; } + .special-row { height: 60px; font-size: 16px; @@ -247,7 +269,8 @@ export class HuiEntitiesCardRowEditor extends LitElement { flex-direction: column; } - .special-row mwc-icon-button { + .remove-icon, + .edit-icon { --mdc-icon-button-size: 36px; color: var(--secondary-text-color); } diff --git a/src/panels/lovelace/editor/lovelace-editor/hui-lovelace-editor.ts b/src/panels/lovelace/editor/lovelace-editor/hui-lovelace-editor.ts index 0d806692a3..dff6c618a4 100644 --- a/src/panels/lovelace/editor/lovelace-editor/hui-lovelace-editor.ts +++ b/src/panels/lovelace/editor/lovelace-editor/hui-lovelace-editor.ts @@ -1,5 +1,6 @@ import "@polymer/paper-input/paper-input"; import { + CSSResult, customElement, html, LitElement, @@ -35,7 +36,6 @@ export class HuiLovelaceEditor extends LitElement { protected render(): TemplateResult { return html` - ${configElementStyle}
@@ -144,25 +138,6 @@ class LovelacePanel extends LitElement { `; } - protected updated(changedProps: PropertyValues): void { - super.updated(changedProps); - - if (changedProps.has("narrow")) { - this._updateColumns(); - return; - } - - if (!changedProps.has("hass")) { - return; - } - - const oldHass = changedProps.get("hass") as this["hass"]; - - if (oldHass && this.hass!.dockedSidebar !== oldHass.dockedSidebar) { - this._updateColumns(); - } - } - protected firstUpdated() { this._fetchConfig(false); if (!this._unsubUpdates) { @@ -174,13 +149,6 @@ class LovelacePanel extends LitElement { this._fetchConfig(false); } }); - this._updateColumns = this._updateColumns.bind(this); - this.mqls = [300, 600, 900, 1200].map((width) => { - const mql = matchMedia(`(min-width: ${width}px)`); - mql.addListener(this._updateColumns); - return mql; - }); - this._updateColumns(); } private async _regenerateConfig() { @@ -201,19 +169,6 @@ class LovelacePanel extends LitElement { this._state = "loaded"; } - private _updateColumns() { - const matchColumns = this.mqls!.reduce( - (cols, mql) => cols + Number(mql.matches), - 0 - ); - // Do -1 column if the menu is docked and open - this._columns = Math.max( - 1, - matchColumns - - Number(!this.narrow && this.hass!.dockedSidebar === "docked") - ); - } - private _lovelaceChanged() { if (this._ignoreNextUpdateEvent) { this._ignoreNextUpdateEvent = false; diff --git a/src/panels/lovelace/types.ts b/src/panels/lovelace/types.ts index 4bd956ee1d..8609f47b78 100644 --- a/src/panels/lovelace/types.ts +++ b/src/panels/lovelace/types.ts @@ -4,6 +4,7 @@ import { LovelaceConfig, } from "../../data/lovelace"; import { Constructor, HomeAssistant } from "../../types"; +import { LovelaceRow, LovelaceRowConfig } from "./entity-rows/types"; import { LovelaceHeaderFooterConfig } from "./header-footer/types"; declare global { @@ -48,6 +49,10 @@ export interface LovelaceCardConstructor extends Constructor { getConfigElement?: () => LovelaceCardEditor; } +export interface LovelaceRowConstructor extends Constructor { + getConfigElement?: () => LovelaceRowEditor; +} + export interface LovelaceHeaderFooter extends HTMLElement { hass?: HomeAssistant; getCardSize(): number | Promise; @@ -60,3 +65,9 @@ export interface LovelaceCardEditor extends HTMLElement { setConfig(config: LovelaceCardConfig): void; refreshYamlEditor?: (focus: boolean) => void; } + +export interface LovelaceRowEditor extends HTMLElement { + hass?: HomeAssistant; + setConfig(config: LovelaceRowConfig): void; + refreshYamlEditor?: (focus: boolean) => void; +} diff --git a/src/panels/lovelace/views/hui-masonry-view.ts b/src/panels/lovelace/views/hui-masonry-view.ts index 30b9f61395..7f264f53ea 100644 --- a/src/panels/lovelace/views/hui-masonry-view.ts +++ b/src/panels/lovelace/views/hui-masonry-view.ts @@ -98,11 +98,13 @@ export class MasonryView extends LitElement implements LovelaceViewElement { } protected firstUpdated(): void { + this._updateColumns = this._updateColumns.bind(this); this._mqls = [300, 600, 900, 1200].map((width) => { const mql = matchMedia(`(min-width: ${width}px)`); mql.addEventListener("change", this._updateColumns); return mql; }); + this._updateColumns(); } protected updated(changedProperties: PropertyValues): void { @@ -130,11 +132,14 @@ export class MasonryView extends LitElement implements LovelaceViewElement { } } - const oldLovelace = changedProperties.get("lovelace") as Lovelace; + const oldLovelace = changedProperties.get("lovelace") as + | Lovelace + | undefined; if ( - oldLovelace?.config !== this.lovelace?.config || - oldLovelace?.editMode !== this.lovelace?.editMode || + (changedProperties.has("lovelace") && ( + oldLovelace?.config !== this.lovelace?.config || + oldLovelace?.editMode !== this.lovelace?.editMode)) || changedProperties.has("_columns") ) { this._createColumns(); @@ -237,6 +242,9 @@ export class MasonryView extends LitElement implements LovelaceViewElement { } private _updateColumns() { + if (!this._mqls) { + return; + } const matchColumns = this._mqls!.reduce( (cols, mql) => cols + Number(mql.matches), 0 @@ -260,6 +268,8 @@ export class MasonryView extends LitElement implements LovelaceViewElement { display: flex; flex-direction: row; justify-content: center; + margin-left: 4px; + margin-right: 4px; } .column { @@ -288,11 +298,6 @@ export class MasonryView extends LitElement implements LovelaceViewElement { } @media (max-width: 500px) { - :host { - padding-left: 0; - padding-right: 0; - } - .column > * { margin-left: 0; margin-right: 0; diff --git a/src/panels/lovelace/views/hui-panel-view.ts b/src/panels/lovelace/views/hui-panel-view.ts index 44f9d27fe3..91c0582f9f 100644 --- a/src/panels/lovelace/views/hui-panel-view.ts +++ b/src/panels/lovelace/views/hui-panel-view.ts @@ -57,7 +57,13 @@ export class PanelView extends LitElement implements LovelaceViewElement { ); } - const oldLovelace = changedProperties.get("lovelace") as Lovelace; + if (!changedProperties.has("lovelace")) { + return; + } + + const oldLovelace = changedProperties.get("lovelace") as + | Lovelace + | undefined; if ( oldLovelace?.config !== this.lovelace?.config || diff --git a/src/resources/styles.ts b/src/resources/styles.ts index ef04c15548..eedbcd8d5b 100644 --- a/src/resources/styles.ts +++ b/src/resources/styles.ts @@ -86,6 +86,7 @@ export const derivedStyles = { "mdc-radio-disabled-color": "var(--disabled-text-color)", "mdc-tab-text-label-color-default": "var(--primary-text-color)", "mdc-button-disabled-ink-color": "var(--disabled-text-color)", + "mdc-dialog-scroll-divider-color": "var(--divider-color)", }; export const haStyle = css` diff --git a/src/translations/en.json b/src/translations/en.json index d23e6b68d3..11b2e3fb1c 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -1656,7 +1656,10 @@ "device_tracker_pick": "Pick device to track", "delete": "Delete", "create": "Create", - "update": "Update" + "update": "Update", + "confirm_delete_user": "Are you sure you want to delete the user account for {name}? You can still track the user, but the person will no longer be able to login.", + "admin": "[%key:ui::panel::config::users::editor::admin%]", + "allow_login": "Allow person to login" } }, "zone": { @@ -1676,7 +1679,7 @@ "new_zone": "New Zone", "name": "Name", "icon": "Icon", - "icon_error_msg": "Icon should be in the format prefix:iconname, for example: mdi:home", + "icon_error_msg": "Icon should be in the format ''prefix:iconname'', for example: ''mdi:home''", "radius": "Radius", "latitude": "Latitude", "longitude": "Longitude", @@ -2328,6 +2331,16 @@ "description": "The Entities card is the most common type of card. It groups items together into lists.", "special_row": "special row", "edit_special_row": "Edit row using the code editor", + "entity_row_editor": "Entity Row Editor", + "secondary_info_values": { + "none": "No Secondary Info", + "entity-id": "Entity ID", + "last-changed": "Last Changed", + "last-triggered": "Last Triggered", + "position": "Position", + "tilt-position": "Tilt Position", + "brightness": "Brightness" + }, "entity_row": { "divider": "Divider", "call-service": "Call Service",