From b0b7e77e28bfd959b94c1dd2086b00803d7d7748 Mon Sep 17 00:00:00 2001 From: karwosts <32912880+karwosts@users.noreply.github.com> Date: Wed, 25 Oct 2023 06:21:05 -0700 Subject: [PATCH 01/12] Fix schedule form rendering after disconnect (#18401) --- .../config/helpers/forms/ha-schedule-form.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/panels/config/helpers/forms/ha-schedule-form.ts b/src/panels/config/helpers/forms/ha-schedule-form.ts index f38c36b3ba3f..a1c08c083e6b 100644 --- a/src/panels/config/helpers/forms/ha-schedule-form.ts +++ b/src/panels/config/helpers/forms/ha-schedule-form.ts @@ -93,6 +93,20 @@ class HaScheduleForm extends LitElement { } } + public disconnectedCallback(): void { + super.disconnectedCallback(); + this.calendar?.destroy(); + this.calendar = undefined; + this.renderRoot.querySelector("style[data-fullcalendar]")?.remove(); + } + + public connectedCallback(): void { + super.connectedCallback(); + if (this.hasUpdated && !this.calendar) { + this.setupCalendar(); + } + } + public focus() { this.updateComplete.then( () => @@ -165,6 +179,10 @@ class HaScheduleForm extends LitElement { } protected firstUpdated(): void { + this.setupCalendar(); + } + + private setupCalendar(): void { const config: CalendarOptions = { ...defaultFullCalendarConfig, locale: this.hass.language, From e8b4eeec6702bedc65d9fe094a71a842edbd1f4b Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Wed, 25 Oct 2023 15:38:39 +0200 Subject: [PATCH 02/12] Allow any number in above and below numeric condition (#18403) --- .../editor/conditions/types/ha-card-condition-numeric_state.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/panels/lovelace/editor/conditions/types/ha-card-condition-numeric_state.ts b/src/panels/lovelace/editor/conditions/types/ha-card-condition-numeric_state.ts index 7804195fcbd9..ff33d6352ce6 100644 --- a/src/panels/lovelace/editor/conditions/types/ha-card-condition-numeric_state.ts +++ b/src/panels/lovelace/editor/conditions/types/ha-card-condition-numeric_state.ts @@ -48,6 +48,7 @@ export class HaCardConditionNumericState extends LitElement { name: "above", selector: { number: { + step: "any", mode: "box", unit_of_measurement: stateObj?.attributes.unit_of_measurement, }, @@ -57,6 +58,7 @@ export class HaCardConditionNumericState extends LitElement { name: "below", selector: { number: { + step: "any", mode: "box", unit_of_measurement: stateObj?.attributes.unit_of_measurement, }, From 81053f2e078c7f1b04945bdf63c9cf1a806add62 Mon Sep 17 00:00:00 2001 From: karwosts <32912880+karwosts@users.noreply.github.com> Date: Wed, 25 Oct 2023 06:48:05 -0700 Subject: [PATCH 03/12] Fix an undefined exception in more-info popup for history graph (#18404) --- src/components/chart/state-history-charts.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/chart/state-history-charts.ts b/src/components/chart/state-history-charts.ts index 24854e8719d1..61d02699ea6f 100644 --- a/src/components/chart/state-history-charts.ts +++ b/src/components/chart/state-history-charts.ts @@ -223,7 +223,7 @@ export class StateHistoryCharts extends LitElement { ); } else { this._computedStartTime = new Date( - this.historyData.timeline.reduce( + (this.historyData?.timeline ?? []).reduce( (minTime, stateInfo) => Math.min( minTime, From 184ef7b7ff7e7bb72cdccc2f00ab194c874ce830 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 25 Oct 2023 16:04:05 +0200 Subject: [PATCH 04/12] Bar media player fixes (#18402) --- .../media-browser/ha-bar-media-player.ts | 42 ++++++++++--------- .../media-browser/ha-panel-media-browser.ts | 2 +- 2 files changed, 24 insertions(+), 20 deletions(-) diff --git a/src/panels/media-browser/ha-bar-media-player.ts b/src/panels/media-browser/ha-bar-media-player.ts index 3e0b312f50d3..a1e408ac0b28 100644 --- a/src/panels/media-browser/ha-bar-media-player.ts +++ b/src/panels/media-browser/ha-bar-media-player.ts @@ -56,6 +56,7 @@ import { BrowserMediaPlayer, ERR_UNSUPPORTED_MEDIA, } from "./browser-media-player"; +import { debounce } from "../../common/util/debounce"; declare global { interface HASSDomEvents { @@ -118,8 +119,14 @@ export class BarMediaPlayer extends SubscribeMixin(LitElement) { public showResolvingNewMediaPicked() { this._tearDownBrowserPlayer(); this._newMediaExpected = true; + // Sometimes the state does not update when playing media, like with TTS, so we wait max 2 secs and then stop waiting + this._debouncedResetMediaExpected(); } + private _debouncedResetMediaExpected = debounce(() => { + this._newMediaExpected = false; + }, 2000); + public hideResolvingNewMediaPicked() { this._newMediaExpected = false; } @@ -154,7 +161,7 @@ export class BarMediaPlayer extends SubscribeMixin(LitElement) { protected render() { if (this._newMediaExpected) { return html` -
+
${until( // Only show spinner after 500ms new Promise((resolve) => { @@ -240,9 +247,13 @@ export class BarMediaPlayer extends SubscribeMixin(LitElement) {
-
+
${stateObj.state === "buffering" - ? html` ` + ? html`` : html`
${controls === undefined @@ -541,7 +552,8 @@ export class BarMediaPlayer extends SubscribeMixin(LitElement) { return css` :host { display: flex; - min-height: 100px; + height: 100px; + box-sizing: border-box; background: var( --ha-card-background, var(--card-background-color, white) @@ -627,12 +639,11 @@ export class BarMediaPlayer extends SubscribeMixin(LitElement) { } img { - max-height: 100px; - max-width: 100px; + height: 100%; } .app img { - max-height: 68px; + height: 68px; margin: 16px 0 16px 16px; } @@ -641,8 +652,7 @@ export class BarMediaPlayer extends SubscribeMixin(LitElement) { } :host([narrow]) { - min-height: 56px; - max-height: 56px; + height: 57px; } :host([narrow]) .controls-progress { @@ -650,6 +660,10 @@ export class BarMediaPlayer extends SubscribeMixin(LitElement) { min-width: 48px; } + :host([narrow]) .controls-progress.buffering { + flex: 1; + } + :host([narrow]) .media-info { padding-left: 8px; } @@ -672,16 +686,6 @@ export class BarMediaPlayer extends SubscribeMixin(LitElement) { justify-content: flex-end; } - :host([narrow]) img { - max-height: 56px; - max-width: 56px; - } - - :host([narrow]) .blank-image { - height: 56px; - width: 56px; - } - :host([narrow]) mwc-linear-progress { padding: 0; position: absolute; diff --git a/src/panels/media-browser/ha-panel-media-browser.ts b/src/panels/media-browser/ha-panel-media-browser.ts index e71291bec8ea..986cb57c22a1 100644 --- a/src/panels/media-browser/ha-panel-media-browser.ts +++ b/src/panels/media-browser/ha-panel-media-browser.ts @@ -286,7 +286,7 @@ class PanelMediaBrowser extends LitElement { } :host([narrow]) ha-media-player-browse { - height: calc(100vh - (80px + var(--header-height))); + height: calc(100vh - (57px + var(--header-height))); } ha-bar-media-player { From 94ad47c60ef281af7bd1318f5292a85dd5bca05d Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Wed, 25 Oct 2023 17:15:43 +0200 Subject: [PATCH 05/12] Hide reveal icon on edge for text-field (#18408) --- src/components/ha-textfield.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/components/ha-textfield.ts b/src/components/ha-textfield.ts index 5053435283cf..c75fd226ebbc 100644 --- a/src/components/ha-textfield.ts +++ b/src/components/ha-textfield.ts @@ -136,6 +136,11 @@ export class HaTextField extends TextFieldBase { text-align: var(--text-field-text-align, start); } + /* Edge, hide reveal password icon */ + ::-ms-reveal { + display: none; + } + /* Chrome, Safari, Edge, Opera */ :host([no-spinner]) input::-webkit-outer-spin-button, :host([no-spinner]) input::-webkit-inner-spin-button { From e16a101de803b01d72777344aca56e8c6b8383e2 Mon Sep 17 00:00:00 2001 From: Steve Repsher Date: Wed, 25 Oct 2023 11:17:11 -0400 Subject: [PATCH 06/12] Compress service worker (#18407) --- build-scripts/gulp/app.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build-scripts/gulp/app.js b/build-scripts/gulp/app.js index 9733da11d6d9..e21802d8d5ac 100644 --- a/build-scripts/gulp/app.js +++ b/build-scripts/gulp/app.js @@ -45,8 +45,8 @@ gulp.task( gulp.parallel("gen-icons-json", "build-translations", "build-locale-data"), "copy-static-app", env.useRollup() ? "rollup-prod-app" : "webpack-prod-app", + gulp.parallel("gen-pages-app-prod", "gen-service-worker-app-prod"), // Don't compress running tests - ...(env.isTestBuild() ? [] : ["compress-app"]), - gulp.parallel("gen-pages-app-prod", "gen-service-worker-app-prod") + ...(env.isTestBuild() ? [] : ["compress-app"]) ) ); From c3743b57eabbbde3e7a9f73da48e86f6d99ea358 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 25 Oct 2023 14:29:44 -0500 Subject: [PATCH 07/12] Speed up first load by preloading recorder info (#18412) --- src/data/lovelace.ts | 5 ----- src/data/preloads.ts | 8 ++++++++ src/data/recorder.ts | 5 +++-- src/entrypoints/core.ts | 18 +++++++++--------- src/layouts/home-assistant.ts | 13 +++++++++++-- src/panels/lovelace/ha-panel-lovelace.ts | 12 ++++++------ 6 files changed, 37 insertions(+), 24 deletions(-) create mode 100644 src/data/preloads.ts diff --git a/src/data/lovelace.ts b/src/data/lovelace.ts index b8d13b2004dd..87279bc2b0ff 100644 --- a/src/data/lovelace.ts +++ b/src/data/lovelace.ts @@ -348,11 +348,6 @@ export const getLegacyLovelaceCollection = (conn: Connection) => ) ); -export interface WindowWithLovelaceProm extends Window { - llConfProm?: Promise; - llResProm?: Promise; -} - export interface ActionHandlerOptions { hasHold?: boolean; hasDoubleClick?: boolean; diff --git a/src/data/preloads.ts b/src/data/preloads.ts new file mode 100644 index 000000000000..ab2c47281330 --- /dev/null +++ b/src/data/preloads.ts @@ -0,0 +1,8 @@ +import { LovelaceConfig, LovelaceResource } from "./lovelace"; +import { RecorderInfo } from "./recorder"; + +export interface WindowWithPreloads extends Window { + llConfProm?: Promise; + llResProm?: Promise; + recorderInfoProm?: Promise; +} diff --git a/src/data/recorder.ts b/src/data/recorder.ts index 32b471f4a75d..23b63a6d1cb4 100644 --- a/src/data/recorder.ts +++ b/src/data/recorder.ts @@ -1,3 +1,4 @@ +import { Connection } from "home-assistant-js-websocket"; import { computeStateName } from "../common/entity/compute_state_name"; import { HaDurationData } from "../components/ha-duration-input"; import { HomeAssistant } from "../types"; @@ -115,8 +116,8 @@ export interface StatisticsValidationResults { [statisticId: string]: StatisticsValidationResult[]; } -export const getRecorderInfo = (hass: HomeAssistant) => - hass.callWS({ +export const getRecorderInfo = (conn: Connection) => + conn.sendMessagePromise({ type: "recorder/info", }); diff --git a/src/entrypoints/core.ts b/src/entrypoints/core.ts index 5353a142435c..a5083b2b2a31 100644 --- a/src/entrypoints/core.ts +++ b/src/entrypoints/core.ts @@ -13,12 +13,9 @@ import { import { loadTokens, saveTokens } from "../common/auth/token_storage"; import { hassUrl } from "../data/auth"; import { isExternal } from "../data/external"; +import { getRecorderInfo } from "../data/recorder"; import { subscribeFrontendUserData } from "../data/frontend"; -import { - fetchConfig, - fetchResources, - WindowWithLovelaceProm, -} from "../data/lovelace"; +import { fetchConfig, fetchResources } from "../data/lovelace"; import { subscribePanels } from "../data/ws-panels"; import { subscribeThemes } from "../data/ws-themes"; import { subscribeRepairsIssueRegistry } from "../data/repairs"; @@ -27,6 +24,7 @@ import type { ExternalAuth } from "../external_app/external_auth"; import "../resources/array.flat.polyfill"; import "../resources/safari-14-attachshadow-patch"; import { MAIN_WINDOW_NAME } from "../data/main_window"; +import { WindowWithPreloads } from "../data/preloads"; window.name = MAIN_WINDOW_NAME; (window as any).frontendVersion = __VERSION__; @@ -124,12 +122,14 @@ window.hassConnection.then(({ conn }) => { subscribeFrontendUserData(conn, "core", noop); subscribeRepairsIssueRegistry(conn, noop); + const preloadWindow = window as WindowWithPreloads; + preloadWindow.recorderInfoProm = getRecorderInfo(conn); + if (location.pathname === "/" || location.pathname.startsWith("/lovelace/")) { - const llWindow = window as WindowWithLovelaceProm; - llWindow.llConfProm = fetchConfig(conn, null, false); - llWindow.llConfProm.catch(() => { + preloadWindow.llConfProm = fetchConfig(conn, null, false); + preloadWindow.llConfProm.catch(() => { // Ignore it, it is handled by Lovelace panel. }); - llWindow.llResProm = fetchResources(conn); + preloadWindow.llResProm = fetchResources(conn); } }); diff --git a/src/layouts/home-assistant.ts b/src/layouts/home-assistant.ts index ba9f321c7976..f9781eef5db5 100644 --- a/src/layouts/home-assistant.ts +++ b/src/layouts/home-assistant.ts @@ -3,11 +3,12 @@ import { customElement, state } from "lit/decorators"; import { isNavigationClick } from "../common/dom/is-navigation-click"; import { navigate } from "../common/navigate"; import { getStorageDefaultPanelUrlPath } from "../data/panel"; -import { getRecorderInfo } from "../data/recorder"; +import { getRecorderInfo, RecorderInfo } from "../data/recorder"; import "../resources/custom-card-support"; import { HassElement } from "../state/hass-element"; import QuickBarMixin from "../state/quick-bar-mixin"; import { HomeAssistant, Route } from "../types"; +import { WindowWithPreloads } from "../data/preloads"; import { storeState } from "../util/ha-pref-storage"; import { renderLaunchScreenInfoBox, @@ -204,7 +205,15 @@ export class HomeAssistantAppEl extends QuickBarMixin(HassElement) { protected async checkDataBaseMigration() { if (this.hass?.config?.components.includes("recorder")) { - const info = await getRecorderInfo(this.hass); + let recorderInfoProm: Promise | undefined; + const preloadWindow = window as WindowWithPreloads; + // On first load, we speed up loading page by having recorderInfoProm ready + if (preloadWindow.recorderInfoProm) { + recorderInfoProm = preloadWindow.recorderInfoProm; + preloadWindow.recorderInfoProm = undefined; + } + const info = await (recorderInfoProm || + getRecorderInfo(this.hass.connection)); this._databaseMigration = info.migration_in_progress && !info.migration_is_live; if (this._databaseMigration) { diff --git a/src/panels/lovelace/ha-panel-lovelace.ts b/src/panels/lovelace/ha-panel-lovelace.ts index 9fd8da1d9db8..ae6bc209ed09 100644 --- a/src/panels/lovelace/ha-panel-lovelace.ts +++ b/src/panels/lovelace/ha-panel-lovelace.ts @@ -15,8 +15,8 @@ import { LovelaceConfig, saveConfig, subscribeLovelaceUpdates, - WindowWithLovelaceProm, } from "../../data/lovelace"; +import { WindowWithPreloads } from "../../data/preloads"; import "../../layouts/hass-error-screen"; import "../../layouts/hass-loading-screen"; import { HomeAssistant, PanelInfo, Route } from "../../types"; @@ -220,16 +220,16 @@ export class LovelacePanel extends LitElement { let rawConf: LovelaceConfig | undefined; let confMode: Lovelace["mode"] = this.panel!.config.mode; let confProm: Promise | undefined; - const llWindow = window as WindowWithLovelaceProm; + const preloadWindow = window as WindowWithPreloads; // On first load, we speed up loading page by having LL promise ready - if (llWindow.llConfProm) { - confProm = llWindow.llConfProm; - llWindow.llConfProm = undefined; + if (preloadWindow.llConfProm) { + confProm = preloadWindow.llConfProm; + preloadWindow.llConfProm = undefined; } if (!resourcesLoaded) { resourcesLoaded = true; - const resources = await (llWindow.llResProm || + const resources = await (preloadWindow.llResProm || fetchResources(this.hass!.connection)); loadLovelaceResources(resources, this.hass!); } From d8c7db6ebf3b83cd4aad3751c06aff9e7be0bf31 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Thu, 26 Oct 2023 12:32:09 +0200 Subject: [PATCH 08/12] Add translations to tile state content options (#18428) --- .../editor/config-elements/hui-tile-card-editor.ts | 8 ++++++-- src/translations/en.json | 4 ++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/panels/lovelace/editor/config-elements/hui-tile-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-tile-card-editor.ts index ebff8fb27df7..b4460d730827 100644 --- a/src/panels/lovelace/editor/config-elements/hui-tile-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-tile-card-editor.ts @@ -186,11 +186,15 @@ export class HuiTileCardEditor multiple: true, options: [ { - label: "State", + label: localize( + `ui.panel.lovelace.editor.card.tile.state_content_options.state` + ), value: "state", }, { - label: "Last changed", + label: localize( + `ui.panel.lovelace.editor.card.tile.state_content_options.last-changed` + ), value: "last-changed", }, ...Object.keys(stateObj?.attributes ?? {}) diff --git a/src/translations/en.json b/src/translations/en.json index 4b7444cb2de8..f889df488415 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -5114,6 +5114,10 @@ "vertical": "Vertical", "hide_state": "Hide state", "state_content": "State content", + "state_content_options": { + "state": "State", + "last-changed": "Last changed" + }, "features": { "name": "Features", "not_compatible": "Not compatible", From a7dc2cfaa6643eb4829cc3277e880203a9d94490 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Thu, 26 Oct 2023 15:25:47 +0200 Subject: [PATCH 09/12] Reduce slider handle size (#18427) --- src/components/ha-slider.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/ha-slider.ts b/src/components/ha-slider.ts index 5a902233e789..328ca788cc20 100644 --- a/src/components/ha-slider.ts +++ b/src/components/ha-slider.ts @@ -10,7 +10,8 @@ export class HaSlider extends MdSlider { :host { --md-sys-color-primary: var(--primary-color); --md-sys-color-outline: var(--outline-color); - + --md-slider-handle-width: 14px; + --md-slider-handle-height: 14px; min-width: 100px; min-inline-size: 100px; width: 200px; From cf0fde0f3cd85a1d92cd0bc317b01fb7282a37b1 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 26 Oct 2023 15:37:13 +0200 Subject: [PATCH 10/12] Change move item todo API (#18410) * Change move item todo API * Handle entity unavailable, add link to more info, allow to delete local todo --- src/data/todo.ts | 6 +- .../lovelace/cards/hui-todo-list-card.ts | 141 ++++++++++++------ src/panels/todo/ha-panel-todo.ts | 107 ++++++++++++- src/translations/en.json | 7 +- 4 files changed, 206 insertions(+), 55 deletions(-) diff --git a/src/data/todo.ts b/src/data/todo.ts index fc4d9974eaf3..733e354e926e 100644 --- a/src/data/todo.ts +++ b/src/data/todo.ts @@ -15,7 +15,7 @@ export const enum TodoItemStatus { } export interface TodoItem { - uid?: string; + uid: string; summary: string; status: TodoItemStatus; } @@ -95,11 +95,11 @@ export const moveItem = ( hass: HomeAssistant, entity_id: string, uid: string, - pos: number + previous_uid: string | undefined ): Promise => hass.callWS({ type: "todo/item/move", entity_id, uid, - pos, + previous_uid, }); diff --git a/src/panels/lovelace/cards/hui-todo-list-card.ts b/src/panels/lovelace/cards/hui-todo-list-card.ts index 76cc977129e2..f2d38bc64f83 100644 --- a/src/panels/lovelace/cards/hui-todo-list-card.ts +++ b/src/panels/lovelace/cards/hui-todo-list-card.ts @@ -21,6 +21,7 @@ import "../../../components/ha-checkbox"; import "../../../components/ha-list-item"; import "../../../components/ha-select"; import "../../../components/ha-svg-icon"; +import "../../../components/ha-icon-button"; import "../../../components/ha-textfield"; import type { HaTextField } from "../../../components/ha-textfield"; import { @@ -37,8 +38,10 @@ import { SubscribeMixin } from "../../../mixins/subscribe-mixin"; import type { SortableInstance } from "../../../resources/sortable"; import { HomeAssistant } from "../../../types"; import { findEntities } from "../common/find-entities"; +import { createEntityNotFoundWarning } from "../components/hui-warning"; import { LovelaceCard, LovelaceCardEditor } from "../types"; import { TodoListCardConfig } from "./types"; +import { isUnavailableState } from "../../../data/entity"; @customElement("hui-todo-list-card") export class HuiTodoListCard @@ -74,7 +77,7 @@ export class HuiTodoListCard @state() private _entityId?: string; - @state() private _items?: Record; + @state() private _items?: TodoItem[]; @state() private _reordering = false; @@ -104,22 +107,16 @@ export class HuiTodoListCard return undefined; } - private _getCheckedItems = memoizeOne( - (items?: Record): TodoItem[] => - items - ? Object.values(items).filter( - (item) => item.status === TodoItemStatus.Completed - ) - : [] + private _getCheckedItems = memoizeOne((items?: TodoItem[]): TodoItem[] => + items + ? items.filter((item) => item.status === TodoItemStatus.Completed) + : [] ); - private _getUncheckedItems = memoizeOne( - (items?: Record): TodoItem[] => - items - ? Object.values(items).filter( - (item) => item.status === TodoItemStatus.NeedsAction - ) - : [] + private _getUncheckedItems = memoizeOne((items?: TodoItem[]): TodoItem[] => + items + ? items.filter((item) => item.status === TodoItemStatus.NeedsAction) + : [] ); public willUpdate( @@ -169,6 +166,18 @@ export class HuiTodoListCard return nothing; } + const stateObj = this.hass.states[this._entityId]; + + if (!stateObj) { + return html` + + ${createEntityNotFoundWarning(this.hass, this._entityId)} + + `; + } + + const unavailable = isUnavailableState(stateObj.state); + const checkedItems = this._getCheckedItems(this._items); const uncheckedItems = this._getUncheckedItems(this._items); @@ -182,39 +191,44 @@ export class HuiTodoListCard
${this.todoListSupportsFeature(TodoListEntityFeature.CREATE_TODO_ITEM) ? html` - - + ` : nothing} ${this.todoListSupportsFeature(TodoListEntityFeature.MOVE_TODO_ITEM) ? html` - - + ` : nothing}
-
${this._renderItems(uncheckedItems)}
+
+ ${this._renderItems(uncheckedItems, unavailable)} +
${checkedItems.length ? html`
@@ -235,6 +249,7 @@ export class HuiTodoListCard "ui.panel.lovelace.cards.todo-list.clear_items" )} @click=${this._clearCompletedItems} + .disabled=${unavailable} > ` : nothing} @@ -247,16 +262,18 @@ export class HuiTodoListCard ${this.todoListSupportsFeature( TodoListEntityFeature.UPDATE_TODO_ITEM ) - ? html` ` : nothing} ` : nothing} = {}; - items.forEach((item) => { - records[item.uid!] = item; - }); - this._items = records; + if (!(this._entityId in this.hass.states)) { + return; + } + this._items = await fetchItems(this.hass!, this._entityId!); + } + + private _getItem(itemId: string) { + return this._items?.find((item) => item.uid === itemId); } private _completeItem(ev): void { - const item = this._items![ev.target.itemId]; + const item = this._getItem(ev.target.itemId); + if (!item) { + return; + } updateItem(this.hass!, this._entityId!, { ...item, status: ev.target.checked @@ -346,7 +370,10 @@ export class HuiTodoListCard private _saveEdit(ev): void { // If name is not empty, update the item otherwise remove it if (ev.target.value) { - const item = this._items![ev.target.itemId]; + const item = this._getItem(ev.target.itemId); + if (!item) { + return; + } updateItem(this.hass!, this._entityId!, { ...item, summary: ev.target.value, @@ -368,7 +395,7 @@ export class HuiTodoListCard } const deleteActions: Array> = []; this._getCheckedItems(this._items).forEach((item: TodoItem) => { - deleteActions.push(deleteItem(this.hass!, this._entityId!, item.uid!)); + deleteActions.push(deleteItem(this.hass!, this._entityId!, item.uid)); }); await Promise.all(deleteActions).finally(() => this._fetchData()); } @@ -438,11 +465,37 @@ export class HuiTodoListCard }); } - private async _moveItem(oldIndex, newIndex) { - const item = this._getUncheckedItems(this._items)[oldIndex]; - await moveItem(this.hass!, this._entityId!, item.uid!, newIndex).finally( - () => this._fetchData() - ); + private async _moveItem(oldIndex: number, newIndex: number) { + const uncheckedItems = this._getUncheckedItems(this._items); + const item = uncheckedItems[oldIndex]; + let prevItem: TodoItem | undefined; + if (newIndex > 0) { + if (newIndex < oldIndex) { + prevItem = uncheckedItems[newIndex - 1]; + } else { + prevItem = uncheckedItems[newIndex]; + } + } + + // Optimistic change + const itemIndex = this._items!.findIndex((itm) => itm.uid === item.uid); + this._items!.splice(itemIndex, 1); + if (newIndex === 0) { + this._items!.unshift(item); + } else { + const prevIndex = this._items!.findIndex( + (itm) => itm.uid === prevItem!.uid + ); + this._items!.splice(prevIndex + 1, 0, item); + } + this._items = [...this._items!]; + + await moveItem( + this.hass!, + this._entityId!, + item.uid, + prevItem?.uid + ).finally(() => this._fetchData()); } static get styles(): CSSResultGroup { @@ -470,16 +523,14 @@ export class HuiTodoListCard } .addButton { - padding-right: 16px; - padding-inline-end: 16px; - cursor: pointer; + margin-left: -12px; + margin-inline-start: -12px; direction: var(--direction); } .reorderButton { - padding-left: 16px; - padding-inline-start: 16px; - cursor: pointer; + margin-right: -12px; + margin-inline-end: -12px; direction: var(--direction); } diff --git a/src/panels/todo/ha-panel-todo.ts b/src/panels/todo/ha-panel-todo.ts index e100f6606b2f..8dbdd77e8c7c 100644 --- a/src/panels/todo/ha-panel-todo.ts +++ b/src/panels/todo/ha-panel-todo.ts @@ -2,7 +2,9 @@ import { ResizeController } from "@lit-labs/observers/resize-controller"; import "@material/mwc-list"; import { mdiChevronDown, + mdiDelete, mdiDotsVertical, + mdiInformationOutline, mdiMicrophone, mdiPlus, } from "@mdi/js"; @@ -19,6 +21,7 @@ import { customElement, property, state } from "lit/decorators"; import memoizeOne from "memoize-one"; import { isComponentLoaded } from "../../common/config/is_component_loaded"; import { storage } from "../../common/decorators/storage"; +import { fireEvent } from "../../common/dom/fire_event"; import { computeStateName } from "../../common/entity/compute_state_name"; import "../../components/ha-button"; import "../../components/ha-icon-button"; @@ -27,15 +30,21 @@ import "../../components/ha-menu-button"; import "../../components/ha-state-icon"; import "../../components/ha-svg-icon"; import "../../components/ha-two-pane-top-app-bar-fixed"; +import { deleteConfigEntry } from "../../data/config_entries"; +import { getExtendedEntityRegistryEntry } from "../../data/entity_registry"; +import { fetchIntegrationManifest } from "../../data/integration"; import { getTodoLists } from "../../data/todo"; +import { showConfigFlowDialog } from "../../dialogs/config-flow/show-dialog-config-flow"; +import { + showAlertDialog, + showConfirmationDialog, +} from "../../dialogs/generic/show-dialog-box"; import { showVoiceCommandDialog } from "../../dialogs/voice-command-dialog/show-ha-voice-command-dialog"; import { haStyle } from "../../resources/styles"; import { HomeAssistant } from "../../types"; import { HuiErrorCard } from "../lovelace/cards/hui-error-card"; import { createCardElement } from "../lovelace/create-element/create-card-element"; import { LovelaceCard } from "../lovelace/types"; -import { fetchIntegrationManifest } from "../../data/integration"; -import { showConfigFlowDialog } from "../../dialogs/config-flow/show-dialog-config-flow"; @customElement("ha-panel-todo") class PanelTodo extends LitElement { @@ -92,6 +101,10 @@ class PanelTodo extends LitElement { protected willUpdate(changedProperties: PropertyValues): void { super.willUpdate(changedProperties); + if (!this.hasUpdated) { + this.hass.loadFragmentTranslation("lovelace"); + } + if (!this.hasUpdated && !this._entityId) { this._entityId = getTodoLists(this.hass)[0]?.entity_id; } else if (!this.hasUpdated) { @@ -124,6 +137,9 @@ class PanelTodo extends LitElement { } protected render(): TemplateResult { + const entityRegistryEntry = this._entityId + ? this.hass.entities[this._entityId] + : undefined; const showPane = this._showPaneController.value ?? !this.narrow; const listItems = getTodoLists(this.hass).map( (list) => @@ -158,7 +174,9 @@ class PanelTodo extends LitElement { > ${this._entityId - ? computeStateName(this.hass.states[this._entityId]) + ? this._entityId in this.hass.states + ? computeStateName(this.hass.states[this._entityId]) + : this._entityId : ""} - + - ${this.hass.localize("ui.panel.todo.start_conversation")} + ${this.hass.localize("ui.panel.todo.information")} ` : nothing} +
  • + + + ${this.hass.localize("ui.panel.todo.start_conversation")} + + ${entityRegistryEntry?.platform === "local_todo" + ? html`
  • + + + + ${this.hass.localize("ui.panel.todo.delete_list")} + ` + : nothing}
    ${this._card}
    @@ -215,6 +256,60 @@ class PanelTodo extends LitElement { }); } + private _showMoreInfoDialog(): void { + if (!this._entityId) { + return; + } + fireEvent(this, "hass-more-info", { entityId: this._entityId }); + } + + private async _deleteList(): Promise { + if (!this._entityId) { + return; + } + + const entityRegistryEntry = await getExtendedEntityRegistryEntry( + this.hass, + this._entityId + ); + + if (entityRegistryEntry.platform !== "local_todo") { + return; + } + + const entryId = entityRegistryEntry.config_entry_id; + + if (!entryId) { + return; + } + + const confirmed = await showConfirmationDialog(this, { + title: this.hass.localize("ui.panel.todo.delete_confirm_title", { + name: + this._entityId in this.hass.states + ? computeStateName(this.hass.states[this._entityId]) + : this._entityId, + }), + text: this.hass.localize("ui.panel.todo.delete_confirm_text"), + confirmText: this.hass!.localize("ui.common.delete"), + dismissText: this.hass!.localize("ui.common.cancel"), + destructive: true, + }); + + if (!confirmed) { + return; + } + const result = await deleteConfigEntry(this.hass, entryId); + + this._entityId = getTodoLists(this.hass)[0]?.entity_id; + + if (result.require_restart) { + showAlertDialog(this, { + text: this.hass.localize("ui.panel.todo.restart_confirm"), + }); + } + } + private _showVoiceCommandDialog(): void { showVoiceCommandDialog(this, this.hass, { pipeline_id: "last_used" }); } diff --git a/src/translations/en.json b/src/translations/en.json index f889df488415..c99285a8c0b9 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -5516,7 +5516,12 @@ }, "todo": { "start_conversation": "Start conversation", - "create_list": "Create list" + "create_list": "Create list", + "delete_list": "Delete list", + "information": "Information", + "delete_confirm_title": "Remove {name}?", + "delete_confirm_text": "Are you sure you want to remove this list and all of its items?", + "restart_confirm": "Restart Home Assistant to finish removing this to-do list" }, "page-authorize": { "initializing": "Initializing", From d491d8f5ace2603d149f935d525c0432421c0ef5 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Thu, 26 Oct 2023 15:37:31 +0200 Subject: [PATCH 11/12] Quick fix for lovelace resources not loaded (#18430) --- src/panels/lovelace/common/load-resources.ts | 3 ++- src/panels/lovelace/ha-panel-lovelace.ts | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/panels/lovelace/common/load-resources.ts b/src/panels/lovelace/common/load-resources.ts index 27e1d8a416ea..d8d0f5a1b3a9 100644 --- a/src/panels/lovelace/common/load-resources.ts +++ b/src/panels/lovelace/common/load-resources.ts @@ -11,7 +11,8 @@ export const loadLovelaceResources = ( hass: HomeAssistant ) => { // Don't load ressources on safe mode - if (hass.config.safe_mode) { + // Sometimes, hass.config is null but it should not. + if (hass.config?.safe_mode) { return; } resources.forEach((resource) => { diff --git a/src/panels/lovelace/ha-panel-lovelace.ts b/src/panels/lovelace/ha-panel-lovelace.ts index ae6bc209ed09..61bd60ae62c5 100644 --- a/src/panels/lovelace/ha-panel-lovelace.ts +++ b/src/panels/lovelace/ha-panel-lovelace.ts @@ -229,9 +229,9 @@ export class LovelacePanel extends LitElement { } if (!resourcesLoaded) { resourcesLoaded = true; - const resources = await (preloadWindow.llResProm || - fetchResources(this.hass!.connection)); - loadLovelaceResources(resources, this.hass!); + (preloadWindow.llResProm || fetchResources(this.hass!.connection)).then( + (resources) => loadLovelaceResources(resources, this.hass!) + ); } if (this.urlPath !== null || !confProm) { From f1748e4dd5e6919f759a90441af91659bf147847 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Thu, 26 Oct 2023 15:39:36 +0200 Subject: [PATCH 12/12] Bumped version to 20231026.0 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 88e5452ec2e6..3c695ec8291b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "home-assistant-frontend" -version = "20231025.1" +version = "20231026.0" license = {text = "Apache-2.0"} description = "The Home Assistant frontend" readme = "README.md"