diff --git a/src/data/todo.ts b/src/data/todo.ts index fc4d9974ea..733e354e92 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 76cc977129..f2d38bc64f 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 e100f6606b..8dbdd77e8c 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 f889df4884..c99285a8c0 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",