mirror of
https://github.com/home-assistant/frontend.git
synced 2026-04-02 00:27:49 +01:00
Add a period option to todo-list-card (#30151)
* Added a todo-list card option "days_to_show" to filter tasks far in the future (#24020) * Adjusted min and 0 values as suggested in PR * Adjusted as suggested in review * Switched from days_to_show to period (calendar only for now) as suggested * removed days_to_show from editor UI * fixed lint error * Fixed code style with prettier * fix filtering * Update filtering period options * Update src/panels/lovelace/editor/config-elements/hui-todo-list-editor.ts Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com> * Apply suggestion from @MindFreeze Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com> * prettier/lint * fix memoization, items without status * no rolling window * refresh on date change * Show dialog on create when using due_date filter --------- Co-authored-by: cpetry <petry2christian@gmail.com> Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
This commit is contained in:
@@ -8,7 +8,17 @@ import {
|
||||
mdiPlus,
|
||||
mdiSort,
|
||||
} from "@mdi/js";
|
||||
import { endOfDay, isSameDay } from "date-fns";
|
||||
import {
|
||||
addDays,
|
||||
addMonths,
|
||||
addWeeks,
|
||||
addYears,
|
||||
endOfDay,
|
||||
endOfMonth,
|
||||
endOfWeek,
|
||||
endOfYear,
|
||||
isSameDay,
|
||||
} from "date-fns";
|
||||
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import type { PropertyValueMap, PropertyValues } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
@@ -19,6 +29,8 @@ import memoizeOne from "memoize-one";
|
||||
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
|
||||
import { supportsFeature } from "../../../common/entity/supports-feature";
|
||||
import { caseInsensitiveStringCompare } from "../../../common/string/compare";
|
||||
import { calcDate } from "../../../common/datetime/calc_date";
|
||||
import { firstWeekdayIndex } from "../../../common/datetime/first_weekday";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-check-list-item";
|
||||
import "../../../components/ha-checkbox";
|
||||
@@ -57,6 +69,13 @@ import type { TodoListCardConfig } from "./types";
|
||||
export const ITEM_TAP_ACTION_EDIT = "edit";
|
||||
export const ITEM_TAP_ACTION_TOGGLE = "toggle";
|
||||
|
||||
interface TodoDueDatePeriod {
|
||||
calendar?: {
|
||||
period: string;
|
||||
offset?: number;
|
||||
};
|
||||
}
|
||||
|
||||
@customElement("hui-todo-list-card")
|
||||
export class HuiTodoListCard extends LitElement implements LovelaceCard {
|
||||
public static async getConfigElement(): Promise<LovelaceCardEditor> {
|
||||
@@ -96,17 +115,21 @@ export class HuiTodoListCard extends LitElement implements LovelaceCard {
|
||||
|
||||
private _unsubItems?: Promise<UnsubscribeFunc>;
|
||||
|
||||
private _refreshTimer?: number;
|
||||
|
||||
connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
if (this.hasUpdated) {
|
||||
this._subscribeItems();
|
||||
}
|
||||
this._setRefreshTimer();
|
||||
}
|
||||
|
||||
disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
this._unsubItems?.then((unsub) => unsub());
|
||||
this._unsubItems = undefined;
|
||||
this._clearRefreshTimer();
|
||||
}
|
||||
|
||||
public getCardSize(): number {
|
||||
@@ -162,12 +185,18 @@ export class HuiTodoListCard extends LitElement implements LovelaceCard {
|
||||
}
|
||||
|
||||
private _getUncheckedAndItemsWithoutStatus = memoizeOne(
|
||||
(items?: TodoItem[], sort?: string | undefined): TodoItem[] =>
|
||||
(
|
||||
items?: TodoItem[],
|
||||
sort?: string | undefined,
|
||||
due_date_period?: TodoDueDatePeriod,
|
||||
_memoTime?: number
|
||||
): TodoItem[] =>
|
||||
items
|
||||
? this._sortItems(
|
||||
items.filter(
|
||||
(item) =>
|
||||
item.status === TodoItemStatus.NeedsAction || !item.status
|
||||
this._filterItems(
|
||||
items,
|
||||
[null, TodoItemStatus.NeedsAction],
|
||||
due_date_period
|
||||
),
|
||||
sort
|
||||
)
|
||||
@@ -175,35 +204,134 @@ export class HuiTodoListCard extends LitElement implements LovelaceCard {
|
||||
);
|
||||
|
||||
private _getCheckedItems = memoizeOne(
|
||||
(items?: TodoItem[], sort?: string | undefined): TodoItem[] =>
|
||||
(
|
||||
items?: TodoItem[],
|
||||
sort?: string | undefined,
|
||||
due_date_period?: TodoDueDatePeriod,
|
||||
_memoTime?: number
|
||||
): TodoItem[] =>
|
||||
items
|
||||
? this._sortItems(
|
||||
items.filter((item) => item.status === TodoItemStatus.Completed),
|
||||
this._filterItems(
|
||||
items,
|
||||
[TodoItemStatus.Completed],
|
||||
due_date_period
|
||||
),
|
||||
sort
|
||||
)
|
||||
: []
|
||||
);
|
||||
|
||||
private _getUncheckedItems = memoizeOne(
|
||||
(items?: TodoItem[], sort?: string | undefined): TodoItem[] =>
|
||||
(
|
||||
items?: TodoItem[],
|
||||
sort?: string | undefined,
|
||||
due_date_period?: TodoDueDatePeriod,
|
||||
_memoTime?: number
|
||||
): TodoItem[] =>
|
||||
items
|
||||
? this._sortItems(
|
||||
items.filter((item) => item.status === TodoItemStatus.NeedsAction),
|
||||
this._filterItems(
|
||||
items,
|
||||
[TodoItemStatus.NeedsAction],
|
||||
due_date_period
|
||||
),
|
||||
sort
|
||||
)
|
||||
: []
|
||||
);
|
||||
|
||||
private _getItemsWithoutStatus = memoizeOne(
|
||||
(items?: TodoItem[], sort?: string | undefined): TodoItem[] =>
|
||||
(
|
||||
items?: TodoItem[],
|
||||
sort?: string | undefined,
|
||||
due_date_period?: TodoDueDatePeriod,
|
||||
_memoTime?: number
|
||||
): TodoItem[] =>
|
||||
items
|
||||
? this._sortItems(
|
||||
items.filter((item) => !item.status),
|
||||
this._filterItems(items, [null], due_date_period),
|
||||
sort
|
||||
)
|
||||
: []
|
||||
);
|
||||
|
||||
private _filterItems(
|
||||
items: TodoItem[],
|
||||
status: (TodoItemStatus | null)[],
|
||||
period?: TodoDueDatePeriod
|
||||
): TodoItem[] {
|
||||
const endDate =
|
||||
period && period.calendar && period.calendar.period
|
||||
? this._addPeriod(new Date(), period.calendar)
|
||||
: undefined;
|
||||
|
||||
return items.filter((item) => {
|
||||
if (!status.includes(item.status || null)) {
|
||||
return false;
|
||||
}
|
||||
if (!endDate) {
|
||||
return true;
|
||||
}
|
||||
const dueDate = this._getDueDate(item);
|
||||
return dueDate && dueDate <= endDate;
|
||||
});
|
||||
}
|
||||
|
||||
private _addPeriod(
|
||||
date: Date,
|
||||
calendar: { period: string; offset?: number }
|
||||
): Date | undefined {
|
||||
const locale = this.hass!.locale;
|
||||
const config = this.hass!.config;
|
||||
const offset = calendar.offset || 0;
|
||||
switch (calendar.period) {
|
||||
case "day":
|
||||
return addDays(calcDate(date, endOfDay, locale, config), offset);
|
||||
case "week": {
|
||||
const weekStartsOn = firstWeekdayIndex(locale);
|
||||
return addWeeks(
|
||||
calcDate(date, endOfWeek, locale, config, {
|
||||
weekStartsOn,
|
||||
}),
|
||||
offset
|
||||
);
|
||||
}
|
||||
case "month":
|
||||
return addMonths(calcDate(date, endOfMonth, locale, config), offset);
|
||||
case "year":
|
||||
return addYears(calcDate(date, endOfYear, locale, config), offset);
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private _setRefreshTimer() {
|
||||
this._clearRefreshTimer();
|
||||
if (!this.hass || !this._config?.due_date_period) {
|
||||
return;
|
||||
}
|
||||
const nowDate = new Date();
|
||||
const timeout = calcDate(
|
||||
nowDate,
|
||||
endOfDay,
|
||||
this.hass.locale,
|
||||
this.hass.config
|
||||
);
|
||||
this._refreshTimer = window.setTimeout(() => {
|
||||
this._refreshTimer = undefined;
|
||||
this.requestUpdate();
|
||||
}, timeout.getTime() - nowDate.getTime());
|
||||
}
|
||||
|
||||
private _clearRefreshTimer() {
|
||||
if (this._refreshTimer === undefined) {
|
||||
return;
|
||||
}
|
||||
window.clearTimeout(this._refreshTimer);
|
||||
this._refreshTimer = undefined;
|
||||
}
|
||||
|
||||
public willUpdate(
|
||||
changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>
|
||||
): void {
|
||||
@@ -216,6 +344,10 @@ export class HuiTodoListCard extends LitElement implements LovelaceCard {
|
||||
this._items = undefined;
|
||||
this._subscribeItems();
|
||||
}
|
||||
|
||||
if (!this._refreshTimer) {
|
||||
this._setRefreshTimer();
|
||||
}
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues): void {
|
||||
@@ -254,24 +386,42 @@ export class HuiTodoListCard extends LitElement implements LovelaceCard {
|
||||
|
||||
const unavailable = isUnavailableState(stateObj.state);
|
||||
|
||||
// Discard memoization when we rollover to a new day, so filters can be recalculated
|
||||
const memoTime = this._config.due_date_period
|
||||
? calcDate(
|
||||
new Date(),
|
||||
endOfDay,
|
||||
this.hass.locale,
|
||||
this.hass.config
|
||||
).getTime()
|
||||
: 0;
|
||||
|
||||
const checkedItems = this._getCheckedItems(
|
||||
this._items,
|
||||
this._config.display_order
|
||||
this._config.display_order,
|
||||
this._config.due_date_period,
|
||||
memoTime
|
||||
);
|
||||
const uncheckedItems = this._getUncheckedItems(
|
||||
this._items,
|
||||
this._config.display_order
|
||||
this._config.display_order,
|
||||
this._config.due_date_period,
|
||||
memoTime
|
||||
);
|
||||
|
||||
const itemsWithoutStatus = this._getItemsWithoutStatus(
|
||||
this._items,
|
||||
this._config.display_order
|
||||
this._config.display_order,
|
||||
this._config.due_date_period,
|
||||
memoTime
|
||||
);
|
||||
|
||||
const reorderableItems = this._reordering
|
||||
? this._getUncheckedAndItemsWithoutStatus(
|
||||
this._items,
|
||||
this._config.display_order
|
||||
this._config.display_order,
|
||||
this._config.due_date_period,
|
||||
memoTime
|
||||
)
|
||||
: undefined;
|
||||
|
||||
@@ -671,12 +821,23 @@ export class HuiTodoListCard extends LitElement implements LovelaceCard {
|
||||
return this._input;
|
||||
}
|
||||
|
||||
private _addItem(ev): void {
|
||||
private async _addItem(ev): Promise<void> {
|
||||
const newItem = this._newItem;
|
||||
if (newItem.value!.length > 0) {
|
||||
createItem(this.hass!, this._entityId!, {
|
||||
summary: newItem.value!,
|
||||
});
|
||||
if (this._config?.due_date_period) {
|
||||
const item = {
|
||||
summary: newItem.value!,
|
||||
status: TodoItemStatus.NeedsAction,
|
||||
};
|
||||
await showTodoItemEditDialog(this, {
|
||||
entity: this._entityId!,
|
||||
item,
|
||||
});
|
||||
} else {
|
||||
createItem(this.hass!, this._entityId!, {
|
||||
summary: newItem.value!,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
newItem.value = "";
|
||||
|
||||
@@ -598,6 +598,9 @@ export interface TodoListCardConfig extends LovelaceCardConfig {
|
||||
hide_create?: boolean;
|
||||
hide_section_headers?: boolean;
|
||||
sort?: string;
|
||||
due_date_period?: {
|
||||
calendar?: { period: string; offset?: number };
|
||||
};
|
||||
}
|
||||
|
||||
export interface StackCardConfig extends LovelaceCardConfig {
|
||||
|
||||
@@ -2,7 +2,15 @@ import type { CSSResultGroup } from "lit";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { assert, assign, boolean, object, optional, string } from "superstruct";
|
||||
import {
|
||||
assert,
|
||||
assign,
|
||||
boolean,
|
||||
number,
|
||||
object,
|
||||
optional,
|
||||
string,
|
||||
} from "superstruct";
|
||||
import { mdiGestureTap } from "@mdi/js";
|
||||
import {
|
||||
ITEM_TAP_ACTION_EDIT,
|
||||
@@ -35,6 +43,13 @@ const cardConfigStruct = assign(
|
||||
hide_section_headers: optional(boolean()),
|
||||
display_order: optional(string()),
|
||||
item_tap_action: optional(string()),
|
||||
due_date_period: optional(
|
||||
object({
|
||||
calendar: optional(
|
||||
object({ period: string(), offset: optional(number()) })
|
||||
),
|
||||
})
|
||||
),
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ import "../../components/ha-textarea";
|
||||
import "../../components/ha-textfield";
|
||||
import "../../components/ha-time-input";
|
||||
import "../../components/ha-dialog";
|
||||
import type { TodoItem } from "../../data/todo";
|
||||
import {
|
||||
TodoItemStatus,
|
||||
TodoListEntityFeature,
|
||||
@@ -109,7 +110,8 @@ class DialogTodoItemEditor extends LitElement {
|
||||
if (!this._params) {
|
||||
return nothing;
|
||||
}
|
||||
const isCreate = this._params.item === undefined;
|
||||
const isCreate =
|
||||
this._params.item === undefined || !("uid" in this._params.item);
|
||||
const listName =
|
||||
this._params.entity in this.hass.states
|
||||
? computeStateName(this.hass.states[this._params.entity])
|
||||
@@ -364,7 +366,7 @@ class DialogTodoItemEditor extends LitElement {
|
||||
|
||||
try {
|
||||
await updateItem(this.hass!, this._params!.entity, {
|
||||
...entry,
|
||||
...(entry as TodoItem),
|
||||
summary: this._summary,
|
||||
description:
|
||||
this._description ||
|
||||
@@ -416,7 +418,9 @@ class DialogTodoItemEditor extends LitElement {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await deleteItems(this.hass!, this._params!.entity, [entry.uid]);
|
||||
await deleteItems(this.hass!, this._params!.entity, [
|
||||
(entry as TodoItem).uid,
|
||||
]);
|
||||
} catch (err: any) {
|
||||
this._error = err ? err.message : "Unknown error";
|
||||
return;
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { TodoItem } from "../../data/todo";
|
||||
|
||||
export interface TodoItemEditDialogParams {
|
||||
entity: string;
|
||||
item?: TodoItem;
|
||||
item?: TodoItem | Omit<TodoItem, "uid">;
|
||||
}
|
||||
|
||||
export const loadTodoItemEditDialog = () => import("./dialog-todo-item-editor");
|
||||
|
||||
Reference in New Issue
Block a user