diff --git a/src/components/ha-check-list-item.ts b/src/components/ha-check-list-item.ts index 96e62a475e..c5946798ad 100644 --- a/src/components/ha-check-list-item.ts +++ b/src/components/ha-check-list-item.ts @@ -3,9 +3,15 @@ import { CheckListItemBase } from "@material/mwc-list/mwc-check-list-item-base"; import { styles as controlStyles } from "@material/mwc-list/mwc-control-list-item.css"; import { styles } from "@material/mwc-list/mwc-list-item.css"; import { customElement } from "lit/decorators"; +import { fireEvent } from "../common/dom/fire_event"; @customElement("ha-check-list-item") export class HaCheckListItem extends CheckListItemBase { + async onChange(event) { + super.onChange(event); + fireEvent(this, event.type); + } + static override styles = [ styles, controlStyles, @@ -22,6 +28,12 @@ export class HaCheckListItem extends CheckListItemBase { margin-inline-start: 0px; direction: var(--direction); } + .mdc-deprecated-list-item__meta { + flex-shrink: 0; + } + .mdc-deprecated-list-item__graphic { + margin-top: var(--check-list-item-graphic-margin-top); + } `, ]; } diff --git a/src/data/todo.ts b/src/data/todo.ts index 15a1397066..e4f9909077 100644 --- a/src/data/todo.ts +++ b/src/data/todo.ts @@ -18,6 +18,8 @@ export interface TodoItem { uid: string; summary: string; status: TodoItemStatus; + description?: string; + due?: string; } export const enum TodoListEntityFeature { @@ -25,6 +27,9 @@ export const enum TodoListEntityFeature { DELETE_TODO_ITEM = 2, UPDATE_TODO_ITEM = 4, MOVE_TODO_ITEM = 8, + SET_DUE_DATE_ON_ITEM = 16, + SET_DUE_DATETIME_ON_ITEM = 32, + SET_DESCRIPTION_ON_ITEM = 64, } export const getTodoLists = (hass: HomeAssistant): TodoList[] => @@ -74,20 +79,30 @@ export const updateItem = ( hass.callService( "todo", "update_item", - { item: item.uid, rename: item.summary, status: item.status }, + { + item: item.uid, + rename: item.summary, + status: item.status, + description: item.description || undefined, + due_datetime: item.due?.includes("T") ? item.due : undefined, + due_date: item.due?.includes("T") ? undefined : item.due || undefined, + }, { entity_id } ); export const createItem = ( hass: HomeAssistant, entity_id: string, - summary: string + item: Omit ): Promise => hass.callService( "todo", "add_item", { - item: summary, + item: item.summary, + description: item.description || undefined, + due_datetime: item.due?.includes("T") ? item.due : undefined, + due_date: item.due?.includes("T") ? undefined : item.due, }, { entity_id } ); diff --git a/src/panels/lovelace/cards/hui-todo-list-card.ts b/src/panels/lovelace/cards/hui-todo-list-card.ts index c8ded0bcb0..3d3119f8f2 100644 --- a/src/panels/lovelace/cards/hui-todo-list-card.ts +++ b/src/panels/lovelace/cards/hui-todo-list-card.ts @@ -1,10 +1,14 @@ +import "@material/mwc-list/mwc-list"; import { + mdiClock, mdiDelete, + mdiDeleteSweep, + mdiDotsVertical, mdiDrag, - mdiNotificationClearAll, mdiPlus, mdiSort, } from "@mdi/js"; +import { endOfDay, isSameDay } from "date-fns"; import { UnsubscribeFunc } from "home-assistant-js-websocket"; import { CSSResultGroup, @@ -22,11 +26,13 @@ import memoizeOne from "memoize-one"; import type { SortableEvent } from "sortablejs"; import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element"; import { supportsFeature } from "../../../common/entity/supports-feature"; -import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box"; import "../../../components/ha-card"; +import "../../../components/ha-check-list-item"; import "../../../components/ha-checkbox"; import "../../../components/ha-icon-button"; import "../../../components/ha-list-item"; +import "../../../components/ha-markdown-element"; +import "../../../components/ha-relative-time"; import "../../../components/ha-select"; import "../../../components/ha-svg-icon"; import "../../../components/ha-textfield"; @@ -42,8 +48,10 @@ import { subscribeItems, updateItem, } from "../../../data/todo"; +import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box"; import type { SortableInstance } from "../../../resources/sortable"; import { HomeAssistant } from "../../../types"; +import { showTodoItemEditDialog } from "../../todo/show-dialog-todo-item-editor"; import { findEntities } from "../common/find-entities"; import { createEntityNotFoundWarning } from "../components/hui-warning"; import { LovelaceCard, LovelaceCardEditor } from "../types"; @@ -199,6 +207,14 @@ export class HuiTodoListCard extends LitElement implements LovelaceCard {
${this.todoListSupportsFeature(TodoListEntityFeature.CREATE_TODO_ITEM) ? html` + - - ` - : nothing} - ${this.todoListSupportsFeature(TodoListEntityFeature.MOVE_TODO_ITEM) - ? html` - - ` : nothing}
-
- ${this._renderItems(uncheckedItems, unavailable)} -
+ ${uncheckedItems.length + ? html`
+ + ${this.hass!.localize( + "ui.panel.lovelace.cards.todo-list.unchecked_items" + )} + + ${this.todoListSupportsFeature( + TodoListEntityFeature.MOVE_TODO_ITEM + ) + ? html` + + + ${this.hass!.localize( + "ui.panel.lovelace.cards.todo-list.reorder_items" + )} + + + + ` + : nothing} +
+ + ${this._renderItems(uncheckedItems, unavailable)} + ` + : html`

+ ${this.hass.localize( + "ui.panel.lovelace.cards.todo-list.no_unchecked_items" + )} +

`} ${checkedItems.length ? html`
-
+
${this.hass!.localize( "ui.panel.lovelace.cards.todo-list.checked_items" @@ -249,65 +280,33 @@ export class HuiTodoListCard extends LitElement implements LovelaceCard { ${this.todoListSupportsFeature( TodoListEntityFeature.DELETE_TODO_ITEM ) - ? html` - ` + ? html` + + + ${this.hass!.localize( + "ui.panel.lovelace.cards.todo-list.clear_items" + )} + + + + ` : nothing}
- ${repeat( - checkedItems, - (item) => item.uid, - (item) => html` -
- ${this.todoListSupportsFeature( - TodoListEntityFeature.UPDATE_TODO_ITEM - ) - ? html`` - : nothing} - - ${this.todoListSupportsFeature( - TodoListEntityFeature.DELETE_TODO_ITEM - ) && - !this.todoListSupportsFeature( - TodoListEntityFeature.UPDATE_TODO_ITEM - ) - ? html` - ` - : nothing} -
- ` - )} + + ${this._renderItems(checkedItems, unavailable)} + ` : ""} @@ -319,59 +318,93 @@ export class HuiTodoListCard extends LitElement implements LovelaceCard { ${repeat( items, (item) => item.uid, - (item) => html` -
- ${this.todoListSupportsFeature( + (item) => { + const showDelete = + this.todoListSupportsFeature( + TodoListEntityFeature.DELETE_TODO_ITEM + ) && + !this.todoListSupportsFeature( TodoListEntityFeature.UPDATE_TODO_ITEM - ) - ? html`` - : nothing} - - ${this._reordering - ? html` - - - ` - : this.todoListSupportsFeature( - TodoListEntityFeature.DELETE_TODO_ITEM - ) && - !this.todoListSupportsFeature( - TodoListEntityFeature.UPDATE_TODO_ITEM - ) - ? html` - ` - : nothing} -
- ` + @change=${this._completeItem} + @click=${this._openItem} + @request-selected=${this._requestSelected} + @keydown=${this._handleKeydown} + > +
+ ${item.summary} + ${item.description + ? html`` + : nothing} + ${due + ? html`
+ ${today + ? this.hass!.localize( + "ui.panel.lovelace.cards.todo-list.today" + ) + : html``} +
` + : nothing} +
+ ${showReorder + ? html` + + + ` + : showDelete + ? html` + ` + : nothing} + + `; + } )} `; } @@ -401,39 +434,52 @@ export class HuiTodoListCard extends LitElement implements LovelaceCard { return this._items?.find((item) => item.uid === itemId); } + private _requestSelected(ev: Event): void { + ev.stopPropagation(); + } + + private _handleKeydown(ev) { + if (ev.key === " ") { + this._completeItem(ev); + return; + } + if (ev.key === "Enter") { + this._openItem(ev); + } + } + + private _openItem(ev): void { + ev.stopPropagation(); + + if ( + ev + .composedPath() + .find((el) => ["input", "a", "button"].includes(el.localName)) + ) { + return; + } + + const item = this._getItem(ev.currentTarget.itemId); + showTodoItemEditDialog(this, { + entity: this._config!.entity!, + item, + }); + } + private _completeItem(ev): void { - const item = this._getItem(ev.target.itemId); + const item = this._getItem(ev.currentTarget.itemId); if (!item) { return; } updateItem(this.hass!, this._entityId!, { ...item, - status: ev.target.checked - ? TodoItemStatus.Completed - : TodoItemStatus.NeedsAction, + status: + item.status === TodoItemStatus.NeedsAction + ? TodoItemStatus.Completed + : TodoItemStatus.NeedsAction, }); } - private _saveEdit(ev): void { - // If name is not empty, update the item otherwise remove it - if (ev.target.value) { - const item = this._getItem(ev.target.itemId); - if (!item) { - return; - } - updateItem(this.hass!, this._entityId!, { - ...item, - summary: ev.target.value, - }); - } else if ( - this.todoListSupportsFeature(TodoListEntityFeature.DELETE_TODO_ITEM) - ) { - deleteItems(this.hass!, this._entityId!, [ev.target.itemId]); - } - - ev.target.blur(); - } - private async _clearCompletedItems(): Promise { if (!this.hass) { return; @@ -464,7 +510,9 @@ export class HuiTodoListCard extends LitElement implements LovelaceCard { private _addItem(ev): void { const newItem = this._newItem; if (newItem.value!.length > 0) { - createItem(this.hass!, this._entityId!, newItem.value!); + createItem(this.hass!, this._entityId!, { + summary: newItem.value!, + }); } newItem.value = ""; @@ -559,7 +607,6 @@ export class HuiTodoListCard extends LitElement implements LovelaceCard { static get styles(): CSSResultGroup { return css` ha-card { - padding: 16px; height: 100%; box-sizing: border-box; } @@ -568,60 +615,127 @@ export class HuiTodoListCard extends LitElement implements LovelaceCard { padding-top: 0; } - .editRow, + .addRow { + padding: 16px; + padding-bottom: 0; + position: relative; + } + + .addRow ha-icon-button { + position: absolute; + right: 16px; + } + .addRow, - .checked { + .header { display: flex; flex-direction: row; align-items: center; } + .header { + padding-left: 30px; + padding-right: 16px; + margin-top: 8px; + justify-content: space-between; + } + + .header span { + color: var(--primary-text-color); + font-weight: 500; + } + + .empty { + padding: 16px 32px; + } + .item { margin-top: 8px; } - .addButton { - margin-left: -12px; - margin-inline-start: -12px; - direction: var(--direction); + ha-check-list-item { + --mdc-list-item-meta-size: 56px; + min-height: 56px; + height: auto; } - .deleteItemButton { - margin-right: -12px; - margin-inline-end: -12px; - direction: var(--direction); + ha-check-list-item.multiline { + align-items: flex-start; + --check-list-item-graphic-margin-top: 8px; } - .reorderButton { - margin-right: -12px; - margin-inline-end: -12px; - direction: var(--direction); + .row { + display: flex; + justify-content: space-between; + } + + .multiline .column { + display: flex; + flex-direction: column; + margin-top: 18px; + margin-bottom: 12px; + } + + .completed .summary { + text-decoration: line-through; + } + + .description, + .due { + font-size: 12px; + color: var(--secondary-text-color); + } + + .description { + white-space: initial; + overflow: hidden; + display: -webkit-box; + -webkit-line-clamp: 3; + line-clamp: 3; + -webkit-box-orient: vertical; + } + + .description p { + margin: 0; + } + + .description a { + color: var(--primary-color); + } + + .due { + display: flex; + align-items: center; + } + + .due ha-svg-icon { + margin-right: 4px; + --mdc-icon-size: 14px; + } + + .due.overdue { + color: var(--warning-color); + } + + .completed .due.overdue { + color: var(--secondary-text-color); } .handle { cursor: move; + height: 24px; + padding: 16px 4px; } - ha-checkbox { - margin-left: -12px; - margin-inline-start: -12px; - direction: var(--direction); + .deleteItemButton { + position: relative; + left: 8px; } ha-textfield { flex-grow: 1; } - .checked { - margin: 12px 0; - justify-content: space-between; - } - - .checked span { - color: var(--primary-text-color); - font-weight: 500; - } - .divider { height: 1px; background-color: var(--divider-color); @@ -636,6 +750,10 @@ export class HuiTodoListCard extends LitElement implements LovelaceCard { display: block; padding: 8px; } + + .warning { + color: var(--error-color); + } `; } } diff --git a/src/panels/todo/dialog-todo-item-editor.ts b/src/panels/todo/dialog-todo-item-editor.ts new file mode 100644 index 0000000000..3dd4782df6 --- /dev/null +++ b/src/panels/todo/dialog-todo-item-editor.ts @@ -0,0 +1,442 @@ +import "@material/mwc-button"; +import { mdiClose } from "@mdi/js"; +import { formatInTimeZone, toDate } from "date-fns-tz"; +import { CSSResultGroup, LitElement, css, html, nothing } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import memoizeOne from "memoize-one"; +import { fireEvent } from "../../common/dom/fire_event"; +import "../../components/ha-date-input"; +import "../../components/ha-textarea"; +import "../../components/ha-time-input"; +import { + TodoItemStatus, + TodoListEntityFeature, + createItem, + deleteItems, + updateItem, +} from "../../data/todo"; +import { TimeZone } from "../../data/translation"; +import { showConfirmationDialog } from "../../dialogs/generic/show-dialog-box"; +import { haStyleDialog } from "../../resources/styles"; +import { HomeAssistant } from "../../types"; +import { TodoItemEditDialogParams } from "./show-dialog-todo-item-editor"; +import { supportsFeature } from "../../common/entity/supports-feature"; + +@customElement("dialog-todo-item-editor") +class DialogTodoItemEditor extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @state() private _error?: string; + + @state() private _params?: TodoItemEditDialogParams; + + @state() private _summary = ""; + + @state() private _description? = ""; + + @state() private _due?: Date; + + @state() private _checked = false; + + @state() private _hasTime = false; + + @state() private _submitting = false; + + // Dates are manipulated and displayed in the browser timezone + // which may be different from the Home Assistant timezone. When + // events are persisted, they are relative to the Home Assistant + // timezone, but floating without a timezone. + private _timeZone?: string; + + public showDialog(params: TodoItemEditDialogParams): void { + this._error = undefined; + this._params = params; + this._timeZone = + this.hass.locale.time_zone === TimeZone.local + ? Intl.DateTimeFormat().resolvedOptions().timeZone + : this.hass.config.time_zone; + if (params.item) { + const entry = params.item; + this._checked = entry.status === TodoItemStatus.Completed; + this._summary = entry.summary; + this._description = entry.description || ""; + this._due = entry.due ? new Date(entry.due) : undefined; + this._hasTime = entry.due?.includes("T") || false; + } else { + this._hasTime = false; + this._checked = false; + this._due = undefined; + } + } + + public closeDialog(): void { + if (!this._params) { + return; + } + this._error = undefined; + this._params = undefined; + this._due = undefined; + this._summary = ""; + this._description = ""; + this._hasTime = false; + fireEvent(this, "dialog-closed", { dialog: this.localName }); + } + + protected render() { + if (!this._params) { + return nothing; + } + const isCreate = this._params.item === undefined; + + const { dueDate, dueTime } = this._getLocaleStrings(this._due); + + const canUpdate = this._todoListSupportsFeature( + TodoListEntityFeature.UPDATE_TODO_ITEM + ); + + return html` + + ${isCreate + ? this.hass.localize("ui.components.todo.item.add") + : this._summary} +
+ + `} + > +
+ ${this._error + ? html`${this._error}` + : ""} + +
+ + +
+ ${this._todoListSupportsFeature( + TodoListEntityFeature.SET_DESCRIPTION_ON_ITEM + ) + ? html`` + : nothing} + ${this._todoListSupportsFeature( + TodoListEntityFeature.SET_DUE_DATE_ON_ITEM + ) || + this._todoListSupportsFeature( + TodoListEntityFeature.SET_DUE_DATETIME_ON_ITEM + ) + ? html`
+ ${this.hass.localize("ui.components.todo.item.due")}: +
+ + ${this._todoListSupportsFeature( + TodoListEntityFeature.SET_DUE_DATETIME_ON_ITEM + ) + ? html`` + : nothing} +
+
` + : nothing} +
+ ${isCreate + ? html` + + ${this.hass.localize("ui.components.todo.item.add")} + + ` + : html` + + ${this.hass.localize("ui.components.todo.item.save")} + + ${this._todoListSupportsFeature( + TodoListEntityFeature.DELETE_TODO_ITEM + ) + ? html` + + ${this.hass.localize("ui.components.todo.item.delete")} + + ` + : ""} + `} + + `; + } + + private _todoListSupportsFeature(feature: number): boolean { + if (!this._params?.entity) { + return false; + } + const entityStateObj = this.hass!.states[this._params?.entity]; + return entityStateObj && supportsFeature(entityStateObj, feature); + } + + private _getLocaleStrings = memoizeOne((due?: Date) => ({ + dueDate: due ? this._formatDate(due) : undefined, + dueTime: due ? this._formatTime(due) : undefined, + })); + + // Formats a date in specified timezone, or defaulting to browser display timezone + private _formatDate(date: Date, timeZone: string = this._timeZone!): string { + return formatInTimeZone(date, timeZone, "yyyy-MM-dd"); + } + + // Formats a time in specified timezone, or defaulting to browser display timezone + private _formatTime( + date: Date, + timeZone: string = this._timeZone! + ): string | undefined { + return this._hasTime + ? formatInTimeZone(date, timeZone, "HH:mm:ss") + : undefined; // 24 hr + } + + // Parse a date in the browser timezone + private _parseDate(dateStr: string): Date { + return toDate(dateStr, { timeZone: this._timeZone! }); + } + + private _checkedCanged(ev) { + this._checked = ev.target.checked; + } + + private _handleSummaryChanged(ev) { + this._summary = ev.target.value; + } + + private _handleDescriptionChanged(ev) { + this._description = ev.target.value; + } + + private _endDateChanged(ev: CustomEvent) { + const time = this._due ? this._formatTime(this._due) : undefined; + this._due = this._parseDate(`${ev.detail.value}${time ? `T${time}` : ""}`); + } + + private _endTimeChanged(ev: CustomEvent) { + this._hasTime = true; + this._due = this._parseDate( + `${this._formatDate(this._due || new Date())}T${ev.detail.value}` + ); + } + + private async _createItem() { + if (!this._summary) { + this._error = this.hass.localize( + "ui.components.todo.item.not_all_required_fields" + ); + return; + } + + this._submitting = true; + try { + await createItem(this.hass!, this._params!.entity, { + summary: this._summary, + description: this._description, + due: this._due + ? this._hasTime + ? this._due.toISOString() + : this._formatDate(this._due) + : undefined, + }); + } catch (err: any) { + this._error = err ? err.message : "Unknown error"; + return; + } finally { + this._submitting = false; + } + this.closeDialog(); + } + + private async _saveItem() { + if (!this._summary) { + this._error = this.hass.localize( + "ui.components.todo.item.not_all_required_fields" + ); + return; + } + + this._submitting = true; + const entry = this._params!.item!; + + try { + await updateItem(this.hass!, this._params!.entity, { + ...entry, + summary: this._summary, + description: + this._description || + (this._todoListSupportsFeature( + TodoListEntityFeature.SET_DESCRIPTION_ON_ITEM + ) + ? // backend should accept null to clear the field, but it doesn't now + " " + : undefined), + due: this._due + ? this._hasTime + ? this._due.toISOString() + : this._formatDate(this._due) + : undefined, + status: this._checked + ? TodoItemStatus.Completed + : TodoItemStatus.NeedsAction, + }); + } catch (err: any) { + this._error = err ? err.message : "Unknown error"; + return; + } finally { + this._submitting = false; + } + this.closeDialog(); + } + + private async _deleteItem() { + this._submitting = true; + const entry = this._params!.item!; + const confirm = await showConfirmationDialog(this, { + title: this.hass.localize( + "ui.components.todo.item.confirm_delete.delete" + ), + text: this.hass.localize("ui.components.todo.item.confirm_delete.prompt"), + }); + if (!confirm) { + // Cancel + this._submitting = false; + return; + } + try { + await deleteItems(this.hass!, this._params!.entity, [entry.uid]); + } catch (err: any) { + this._error = err ? err.message : "Unknown error"; + return; + } finally { + this._submitting = false; + } + this.closeDialog(); + } + + static get styles(): CSSResultGroup { + return [ + haStyleDialog, + css` + ha-dialog { + --mdc-dialog-min-width: min(600px, 95vw); + --mdc-dialog-max-width: min(600px, 95vw); + } + ha-alert { + display: block; + margin-bottom: 16px; + } + ha-textfield, + ha-textarea { + display: block; + width: 100%; + } + ha-checkbox { + margin-top: 4px; + } + ha-textarea { + margin-bottom: 16px; + } + ha-date-input { + flex-grow: 1; + } + ha-time-input { + margin-left: 16px; + } + .flex { + display: flex; + justify-content: space-between; + } + .label { + font-size: 12px; + font-weight: 500; + color: var(--input-label-ink-color); + } + .date-range-details-content { + display: inline-block; + } + ha-svg-icon { + width: 40px; + margin-right: 8px; + margin-inline-end: 16px; + margin-inline-start: initial; + direction: var(--direction); + vertical-align: top; + } + .key { + display: inline-block; + vertical-align: top; + } + .value { + display: inline-block; + vertical-align: top; + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "dialog-todo-item-editor": DialogTodoItemEditor; + } +} diff --git a/src/panels/todo/ha-panel-todo.ts b/src/panels/todo/ha-panel-todo.ts index ba03971c12..d781fb38f0 100644 --- a/src/panels/todo/ha-panel-todo.ts +++ b/src/panels/todo/ha-panel-todo.ts @@ -23,7 +23,14 @@ 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 { navigate } from "../../common/navigate"; +import { constructUrlCurrentPath } from "../../common/url/construct-url"; +import { + createSearchParam, + extractSearchParam, +} from "../../common/url/search-params"; import "../../components/ha-button"; +import "../../components/ha-fab"; import "../../components/ha-icon-button"; import "../../components/ha-list-item"; import "../../components/ha-menu-button"; @@ -33,7 +40,7 @@ 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 { TodoListEntityFeature, getTodoLists } from "../../data/todo"; import { showConfigFlowDialog } from "../../dialogs/config-flow/show-dialog-config-flow"; import { showAlertDialog, @@ -45,12 +52,8 @@ 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 { navigate } from "../../common/navigate"; -import { - createSearchParam, - extractSearchParam, -} from "../../common/url/search-params"; -import { constructUrlCurrentPath } from "../../common/url/construct-url"; +import { showTodoItemEditDialog } from "./show-dialog-todo-item-editor"; +import { supportsFeature } from "../../common/entity/supports-feature"; @customElement("ha-panel-todo") class PanelTodo extends LitElement { @@ -152,6 +155,9 @@ class PanelTodo extends LitElement { const entityRegistryEntry = this._entityId ? this.hass.entities[this._entityId] : undefined; + const entityState = this._entityId + ? this.hass.states[this._entityId] + : undefined; const showPane = this._showPaneController.value ?? !this.narrow; const listItems = getTodoLists(this.hass).map( (list) => @@ -187,8 +193,8 @@ class PanelTodo extends LitElement {
${this._entityId - ? this._entityId in this.hass.states - ? computeStateName(this.hass.states[this._entityId]) + ? entityState + ? computeStateName(entityState) : this._entityId : ""}
@@ -255,6 +261,16 @@ class PanelTodo extends LitElement {
${this._card}
+ ${entityState && + supportsFeature(entityState, TodoListEntityFeature.CREATE_TODO_ITEM) + ? html` + + ` + : nothing} `; } @@ -329,6 +345,10 @@ class PanelTodo extends LitElement { showVoiceCommandDialog(this, this.hass, { pipeline_id: "last_used" }); } + private _addItem() { + showTodoItemEditDialog(this, { entity: this._entityId! }); + } + static get styles(): CSSResultGroup { return [ haStyle, @@ -390,6 +410,11 @@ class PanelTodo extends LitElement { white-space: nowrap; display: block; } + ha-fab { + position: absolute; + right: 16px; + bottom: 16px; + } `, ]; } diff --git a/src/panels/todo/show-dialog-todo-item-editor.ts b/src/panels/todo/show-dialog-todo-item-editor.ts new file mode 100644 index 0000000000..4560df8f6e --- /dev/null +++ b/src/panels/todo/show-dialog-todo-item-editor.ts @@ -0,0 +1,20 @@ +import { fireEvent } from "../../common/dom/fire_event"; +import { TodoItem } from "../../data/todo"; + +export interface TodoItemEditDialogParams { + entity: string; + item?: TodoItem; +} + +export const loadTodoItemEditDialog = () => import("./dialog-todo-item-editor"); + +export const showTodoItemEditDialog = ( + element: HTMLElement, + detailParams: TodoItemEditDialogParams +): void => { + fireEvent(element, "show-dialog", { + dialogTag: "dialog-todo-item-editor", + dialogImport: loadTodoItemEditDialog, + dialogParams: detailParams, + }); +}; diff --git a/src/translations/en.json b/src/translations/en.json index 3bbffcec76..1917d1e8f6 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -756,6 +756,22 @@ "grid": "Grid", "list": "List" }, + "todo": { + "item": { + "summary": "Task name", + "description": "Description", + "add": "Add item", + "delete": "Delete item", + "edit": "Edit item", + "save": "Save item", + "due": "Due date", + "not_all_required_fields": "Not all required fields are filled in", + "confirm_delete": { + "delete": "Delete item", + "prompt": "Do you want to delete this item?" + } + } + }, "calendar": { "label": "Calendar", "my_calendars": "My calendars", @@ -4690,14 +4706,17 @@ "never_triggered": "Never triggered" }, "todo-list": { - "checked_items": "Checked items", - "clear_items": "Clear checked items", + "unchecked_items": "Active", + "no_unchecked_items": "You have no to-do items!", + "checked_items": "Completed", + "clear_items": "Remove completed items", "add_item": "Add item", + "today": "Today", "reorder_items": "Reorder items", "drag_and_drop": "Drag and drop", "delete_item": "Delete item", - "delete_confirm_title": "Clear checked items?", - "delete_confirm_text": "{number} {number, plural,\n one {item}\n other {items}\n} will be permanently deleted from the to-do list." + "delete_confirm_title": "Remove completed items?", + "delete_confirm_text": "{number} {number, plural,\n one {item}\n other {items}\n} will be permanently removed from the to-do list." }, "picture-elements": { "hold": "Hold:", @@ -5758,6 +5777,7 @@ "assist": "[%key:ui::panel::lovelace::menu::assist%]", "create_list": "Create list", "delete_list": "Delete list", + "add_item": "Add item", "information": "Information", "delete_confirm_title": "Remove {name}?", "delete_confirm_text": "Are you sure you want to remove this list and all of its items?",