Add description and due support to todo lists (#19107)

This commit is contained in:
Bram Kragten 2023-12-21 21:30:24 +01:00 committed by GitHub
parent 8f07e6f141
commit 09dcc29175
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 854 additions and 202 deletions

View File

@ -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);
}
`,
];
}

View File

@ -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<TodoItem, "uid" | "status">
): Promise<ServiceCallResponse> =>
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 }
);

View File

@ -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 {
<div class="addRow">
${this.todoListSupportsFeature(TodoListEntityFeature.CREATE_TODO_ITEM)
? html`
<ha-textfield
class="addBox"
.placeholder=${this.hass!.localize(
"ui.panel.lovelace.cards.todo-list.add_item"
)}
@keydown=${this._addKeyPress}
.disabled=${unavailable}
></ha-textfield>
<ha-icon-button
class="addButton"
.path=${mdiPlus}
@ -209,38 +225,53 @@ export class HuiTodoListCard extends LitElement implements LovelaceCard {
@click=${this._addItem}
>
</ha-icon-button>
<ha-textfield
class="addBox"
.placeholder=${this.hass!.localize(
"ui.panel.lovelace.cards.todo-list.add_item"
)}
@keydown=${this._addKeyPress}
.disabled=${unavailable}
></ha-textfield>
`
: nothing}
${this.todoListSupportsFeature(TodoListEntityFeature.MOVE_TODO_ITEM)
? html`
</div>
${uncheckedItems.length
? html` <div class="header">
<span>
${this.hass!.localize(
"ui.panel.lovelace.cards.todo-list.unchecked_items"
)}
</span>
${this.todoListSupportsFeature(
TodoListEntityFeature.MOVE_TODO_ITEM
)
? html`<ha-button-menu>
<ha-icon-button
class="reorderButton"
.path=${mdiSort}
.title=${this.hass!.localize(
slot="trigger"
.path=${mdiDotsVertical}
></ha-icon-button>
<ha-list-item
@click=${this._toggleReorder}
graphic="icon"
>
${this.hass!.localize(
"ui.panel.lovelace.cards.todo-list.reorder_items"
)}
@click=${this._toggleReorder}
<ha-svg-icon
slot="graphic"
.path=${mdiSort}
.disabled=${unavailable}
>
</ha-icon-button>
`
</ha-svg-icon>
</ha-list-item>
</ha-button-menu>`
: nothing}
</div>
<div id="unchecked">
<mwc-list id="unchecked">
${this._renderItems(uncheckedItems, unavailable)}
</div>
</mwc-list>`
: html`<p class="empty">
${this.hass.localize(
"ui.panel.lovelace.cards.todo-list.no_unchecked_items"
)}
</p>`}
${checkedItems.length
? html`
<div class="divider"></div>
<div class="checked">
<div class="header">
<span>
${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`<ha-svg-icon
class="clearall"
tabindex="0"
.path=${mdiNotificationClearAll}
.title=${this.hass!.localize(
? html`<ha-button-menu>
<ha-icon-button
slot="trigger"
.path=${mdiDotsVertical}
></ha-icon-button>
<ha-list-item
@click=${this._clearCompletedItems}
graphic="icon"
class="warning"
>
${this.hass!.localize(
"ui.panel.lovelace.cards.todo-list.clear_items"
)}
@click=${this._clearCompletedItems}
<ha-svg-icon
class="warning"
slot="graphic"
.path=${mdiDeleteSweep}
.disabled=${unavailable}
>
</ha-svg-icon>`
</ha-svg-icon>
</ha-list-item>
</ha-button-menu>`
: nothing}
</div>
${repeat(
checkedItems,
(item) => item.uid,
(item) => html`
<div class="editRow">
${this.todoListSupportsFeature(
TodoListEntityFeature.UPDATE_TODO_ITEM
)
? html`<ha-checkbox
tabindex="0"
.checked=${item.status === TodoItemStatus.Completed}
.itemId=${item.uid}
@change=${this._completeItem}
.disabled=${unavailable}
></ha-checkbox>`
: nothing}
<ha-textfield
class="item"
.disabled=${unavailable ||
!this.todoListSupportsFeature(
TodoListEntityFeature.UPDATE_TODO_ITEM
)}
.value=${item.summary}
.itemId=${item.uid}
@change=${this._saveEdit}
></ha-textfield>
${this.todoListSupportsFeature(
TodoListEntityFeature.DELETE_TODO_ITEM
) &&
!this.todoListSupportsFeature(
TodoListEntityFeature.UPDATE_TODO_ITEM
)
? html`<ha-icon-button
.title=${this.hass!.localize(
"ui.panel.lovelace.cards.todo-list.delete_item"
)}
class="deleteItemButton"
.path=${mdiDelete}
.itemId=${item.uid}
@click=${this._deleteItem}
>
</ha-icon-button>`
: nothing}
</div>
`
)}
<mwc-list multi id="checked">
${this._renderItems(checkedItems, unavailable)}
</mwc-list>
`
: ""}
</ha-card>
@ -319,30 +318,66 @@ export class HuiTodoListCard extends LitElement implements LovelaceCard {
${repeat(
items,
(item) => item.uid,
(item) => html`
<div class="editRow" item-id=${item.uid}>
${this.todoListSupportsFeature(
(item) => {
const showDelete =
this.todoListSupportsFeature(
TodoListEntityFeature.DELETE_TODO_ITEM
) &&
!this.todoListSupportsFeature(
TodoListEntityFeature.UPDATE_TODO_ITEM
)
? html`<ha-checkbox
tabindex="0"
.checked=${item.status === TodoItemStatus.Completed}
.itemId=${item.uid}
.disabled=${unavailable}
@change=${this._completeItem}
></ha-checkbox>`
: nothing}
<ha-textfield
class="item"
);
const showReorder =
item.status !== TodoItemStatus.Completed && this._reordering;
const due = item.due
? item.due.includes("T")
? new Date(item.due)
: endOfDay(new Date(item.due))
: undefined;
const today =
due && !item.due!.includes("T") && isSameDay(new Date(), due);
return html`
<ha-check-list-item
left
.hasMeta=${showReorder || showDelete}
class="editRow ${classMap({
completed: item.status === TodoItemStatus.Completed,
multiline: Boolean(item.description || item.due),
})}"
.selected=${item.status === TodoItemStatus.Completed}
.disabled=${unavailable ||
!this.todoListSupportsFeature(
TodoListEntityFeature.UPDATE_TODO_ITEM
)}
.value=${item.summary}
item-id=${item.uid}
.itemId=${item.uid}
@change=${this._saveEdit}
></ha-textfield>
${this._reordering
@change=${this._completeItem}
@click=${this._openItem}
@request-selected=${this._requestSelected}
@keydown=${this._handleKeydown}
>
<div class="column">
<span class="summary">${item.summary}</span>
${item.description
? html`<ha-markdown-element
class="description"
.content=${item.description}
></ha-markdown-element>`
: nothing}
${due
? html`<div class="due ${due < new Date() ? "overdue" : ""}">
<ha-svg-icon .path=${mdiClock}></ha-svg-icon>${today
? this.hass!.localize(
"ui.panel.lovelace.cards.todo-list.today"
)
: html`<ha-relative-time
capitalize
.hass=${this.hass}
.datetime=${due}
></ha-relative-time>`}
</div>`
: nothing}
</div>
${showReorder
? html`
<ha-svg-icon
.title=${this.hass!.localize(
@ -350,15 +385,11 @@ export class HuiTodoListCard extends LitElement implements LovelaceCard {
)}
class="reorderButton handle"
.path=${mdiDrag}
slot="meta"
>
</ha-svg-icon>
`
: this.todoListSupportsFeature(
TodoListEntityFeature.DELETE_TODO_ITEM
) &&
!this.todoListSupportsFeature(
TodoListEntityFeature.UPDATE_TODO_ITEM
)
: showDelete
? html`<ha-icon-button
.title=${this.hass!.localize(
"ui.panel.lovelace.cards.todo-list.delete_item"
@ -366,12 +397,14 @@ export class HuiTodoListCard extends LitElement implements LovelaceCard {
class="deleteItemButton"
.path=${mdiDelete}
.itemId=${item.uid}
slot="meta"
@click=${this._deleteItem}
>
</ha-icon-button>`
: nothing}
</div>
`
</ha-check-list-item>
`;
}
)}
`;
}
@ -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
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<void> {
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);
}
`;
}
}

View File

@ -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`
<ha-dialog
open
@closed=${this.closeDialog}
scrimClickAction
escapeKeyAction
.heading=${html`
<div class="header_title">
${isCreate
? this.hass.localize("ui.components.todo.item.add")
: this._summary}
</div>
<ha-icon-button
.label=${this.hass.localize("ui.dialogs.generic.close")}
.path=${mdiClose}
dialogAction="close"
class="header_button"
></ha-icon-button>
`}
>
<div class="content">
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: ""}
<div class="flex">
<ha-checkbox
.checked=${this._checked}
@change=${this._checkedCanged}
.disabled=${isCreate || !canUpdate}
></ha-checkbox>
<ha-textfield
class="summary"
name="summary"
.label=${this.hass.localize("ui.components.todo.item.summary")}
.value=${this._summary}
required
@input=${this._handleSummaryChanged}
.validationMessage=${this.hass.localize(
"ui.common.error_required"
)}
dialogInitialFocus
.disabled=${!canUpdate}
></ha-textfield>
</div>
${this._todoListSupportsFeature(
TodoListEntityFeature.SET_DESCRIPTION_ON_ITEM
)
? html`<ha-textarea
class="description"
name="description"
.label=${this.hass.localize(
"ui.components.todo.item.description"
)}
.value=${this._description}
@input=${this._handleDescriptionChanged}
autogrow
.disabled=${!canUpdate}
></ha-textarea>`
: nothing}
${this._todoListSupportsFeature(
TodoListEntityFeature.SET_DUE_DATE_ON_ITEM
) ||
this._todoListSupportsFeature(
TodoListEntityFeature.SET_DUE_DATETIME_ON_ITEM
)
? html`<div>
<span class="label"
>${this.hass.localize("ui.components.todo.item.due")}:</span
>
<div class="flex">
<ha-date-input
.value=${dueDate}
.locale=${this.hass.locale}
.disabled=${!canUpdate}
@value-changed=${this._endDateChanged}
></ha-date-input>
${this._todoListSupportsFeature(
TodoListEntityFeature.SET_DUE_DATETIME_ON_ITEM
)
? html`<ha-time-input
.value=${dueTime}
.locale=${this.hass.locale}
.disabled=${!canUpdate}
@value-changed=${this._endTimeChanged}
></ha-time-input>`
: nothing}
</div>
</div>`
: nothing}
</div>
${isCreate
? html`
<mwc-button
slot="primaryAction"
@click=${this._createItem}
.disabled=${this._submitting}
>
${this.hass.localize("ui.components.todo.item.add")}
</mwc-button>
`
: html`
<mwc-button
slot="primaryAction"
@click=${this._saveItem}
.disabled=${!canUpdate || this._submitting}
>
${this.hass.localize("ui.components.todo.item.save")}
</mwc-button>
${this._todoListSupportsFeature(
TodoListEntityFeature.DELETE_TODO_ITEM
)
? html`
<mwc-button
slot="secondaryAction"
class="warning"
@click=${this._deleteItem}
.disabled=${this._submitting}
>
${this.hass.localize("ui.components.todo.item.delete")}
</mwc-button>
`
: ""}
`}
</ha-dialog>
`;
}
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;
}
}

View File

@ -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 {
<ha-button slot="trigger">
<div>
${this._entityId
? this._entityId in this.hass.states
? computeStateName(this.hass.states[this._entityId])
? entityState
? computeStateName(entityState)
: this._entityId
: ""}
</div>
@ -255,6 +261,16 @@ class PanelTodo extends LitElement {
<div id="columns">
<div class="column">${this._card}</div>
</div>
${entityState &&
supportsFeature(entityState, TodoListEntityFeature.CREATE_TODO_ITEM)
? html`<ha-fab
.label=${this.hass.localize("ui.panel.todo.add_item")}
extended
@click=${this._addItem}
>
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
</ha-fab>`
: nothing}
</ha-two-pane-top-app-bar-fixed>
`;
}
@ -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;
}
`,
];
}

View File

@ -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,
});
};

View File

@ -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?",