diff --git a/src/data/todo.ts b/src/data/todo.ts index cb1a79447855..0c02d76c1aa5 100644 --- a/src/data/todo.ts +++ b/src/data/todo.ts @@ -14,6 +14,14 @@ export const enum TodoItemStatus { Completed = "completed", } +export enum TodoSortMode { + NONE = "none", + ALPHA_ASC = "alpha_asc", + ALPHA_DESC = "alpha_desc", + DUEDATE_ASC = "duedate_asc", + DUEDATE_DESC = "duedate_desc", +} + export interface TodoItem { uid: string; summary: string; diff --git a/src/panels/lovelace/cards/hui-todo-list-card.ts b/src/panels/lovelace/cards/hui-todo-list-card.ts index 7c41a16df5d0..494a1040e411 100644 --- a/src/panels/lovelace/cards/hui-todo-list-card.ts +++ b/src/panels/lovelace/cards/hui-todo-list-card.ts @@ -20,6 +20,7 @@ import memoizeOne from "memoize-one"; import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element"; import { supportsFeature } from "../../../common/entity/supports-feature"; import { stopPropagation } from "../../../common/dom/stop_propagation"; +import { caseInsensitiveStringCompare } from "../../../common/string/compare"; import "../../../components/ha-card"; import "../../../components/ha-check-list-item"; import "../../../components/ha-checkbox"; @@ -42,6 +43,7 @@ import { moveItem, subscribeItems, updateItem, + TodoSortMode, } from "../../../data/todo"; import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box"; import type { HomeAssistant } from "../../../types"; @@ -123,16 +125,54 @@ export class HuiTodoListCard extends LitElement implements LovelaceCard { return undefined; } - private _getCheckedItems = memoizeOne((items?: TodoItem[]): TodoItem[] => - items - ? items.filter((item) => item.status === TodoItemStatus.Completed) - : [] + private _sortItems(items: TodoItem[], sort?: string) { + if (sort === TodoSortMode.ALPHA_ASC || sort === TodoSortMode.ALPHA_DESC) { + const sortOrder = sort === TodoSortMode.ALPHA_ASC ? 1 : -1; + return items.sort( + (a, b) => + sortOrder * + caseInsensitiveStringCompare( + a.summary, + b.summary, + this.hass?.locale.language + ) + ); + } + if ( + sort === TodoSortMode.DUEDATE_ASC || + sort === TodoSortMode.DUEDATE_DESC + ) { + const sortOrder = sort === TodoSortMode.DUEDATE_ASC ? 1 : -1; + return items.sort((a, b) => { + const aDue = this._getDueDate(a) ?? Infinity; + const bDue = this._getDueDate(b) ?? Infinity; + if (aDue === bDue) { + return 0; + } + return aDue < bDue ? -sortOrder : sortOrder; + }); + } + return items; + } + + private _getCheckedItems = memoizeOne( + (items?: TodoItem[], sort?: string | undefined): TodoItem[] => + items + ? this._sortItems( + items.filter((item) => item.status === TodoItemStatus.Completed), + sort + ) + : [] ); - private _getUncheckedItems = memoizeOne((items?: TodoItem[]): TodoItem[] => - items - ? items.filter((item) => item.status === TodoItemStatus.NeedsAction) - : [] + private _getUncheckedItems = memoizeOne( + (items?: TodoItem[], sort?: string | undefined): TodoItem[] => + items + ? this._sortItems( + items.filter((item) => item.status === TodoItemStatus.NeedsAction), + sort + ) + : [] ); public willUpdate( @@ -185,8 +225,14 @@ export class HuiTodoListCard extends LitElement implements LovelaceCard { const unavailable = isUnavailableState(stateObj.state); - const checkedItems = this._getCheckedItems(this._items); - const uncheckedItems = this._getUncheckedItems(this._items); + const checkedItems = this._getCheckedItems( + this._items, + this._config.display_order + ); + const uncheckedItems = this._getUncheckedItems( + this._items, + this._config.display_order + ); return html` - ${this._todoListSupportsFeature( + ${(!this._config.display_order || + this._config.display_order === TodoSortMode.NONE) && + this._todoListSupportsFeature( TodoListEntityFeature.MOVE_TODO_ITEM ) ? html` @@ -316,6 +364,14 @@ export class HuiTodoListCard extends LitElement implements LovelaceCard { `; } + private _getDueDate(item: TodoItem): Date | undefined { + return item.due + ? item.due.includes("T") + ? new Date(item.due) + : endOfDay(new Date(`${item.due}T00:00:00`)) + : undefined; + } + private _renderItems(items: TodoItem[], unavailable = false) { return html` ${repeat( @@ -331,11 +387,7 @@ export class HuiTodoListCard extends LitElement implements LovelaceCard { ); 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}T00:00:00`)) - : undefined; + const due = this._getDueDate(item); const today = due && !item.due!.includes("T") && isSameDay(new Date(), due); return html` diff --git a/src/panels/lovelace/cards/types.ts b/src/panels/lovelace/cards/types.ts index 9a7d555b7fb2..b6b82b90f108 100644 --- a/src/panels/lovelace/cards/types.ts +++ b/src/panels/lovelace/cards/types.ts @@ -479,6 +479,7 @@ export interface TodoListCardConfig extends LovelaceCardConfig { entity?: string; hide_completed?: boolean; hide_create?: boolean; + sort?: string; } export interface StackCardConfig extends LovelaceCardConfig { diff --git a/src/panels/lovelace/editor/config-elements/hui-todo-list-editor.ts b/src/panels/lovelace/editor/config-elements/hui-todo-list-editor.ts index d64fbda8d494..47a7228efff0 100644 --- a/src/panels/lovelace/editor/config-elements/hui-todo-list-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-todo-list-editor.ts @@ -1,17 +1,21 @@ 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 { isComponentLoaded } from "../../../../common/config/is_component_loaded"; import { fireEvent } from "../../../../common/dom/fire_event"; import "../../../../components/ha-alert"; import "../../../../components/ha-form/ha-form"; import type { HomeAssistant } from "../../../../types"; +import type { LocalizeFunc } from "../../../../common/translations/localize"; import type { TodoListCardConfig } from "../../cards/types"; import type { LovelaceCardEditor } from "../../types"; import { baseLovelaceCardConfig } from "../structs/base-card-struct"; import type { SchemaUnion } from "../../../../components/ha-form/types"; import { configElementStyle } from "./config-elements-style"; +import { TodoListEntityFeature, TodoSortMode } from "../../../../data/todo"; +import { supportsFeature } from "../../../../common/entity/supports-feature"; const cardConfigStruct = assign( baseLovelaceCardConfig, @@ -21,21 +25,10 @@ const cardConfigStruct = assign( entity: optional(string()), hide_completed: optional(boolean()), hide_create: optional(boolean()), + display_order: optional(string()), }) ); -const SCHEMA = [ - { name: "title", selector: { text: {} } }, - { - name: "entity", - selector: { - entity: { domain: "todo" }, - }, - }, - { name: "theme", selector: { theme: {} } }, - { name: "hide_completed", selector: { boolean: {} } }, -] as const; - @customElement("hui-todo-list-card-editor") export class HuiTodoListEditor extends LitElement @@ -45,6 +38,39 @@ export class HuiTodoListEditor @state() private _config?: TodoListCardConfig; + private _schema = memoizeOne( + (localize: LocalizeFunc, supportsManualSort: boolean) => + [ + { name: "title", selector: { text: {} } }, + { + name: "entity", + selector: { + entity: { domain: "todo" }, + }, + }, + { name: "theme", selector: { theme: {} } }, + { name: "hide_completed", selector: { boolean: {} } }, + { + name: "display_order", + selector: { + select: { + options: Object.values(TodoSortMode).map((sort) => ({ + value: sort, + label: localize( + `ui.panel.lovelace.editor.card.todo-list.sort_modes.${sort === TodoSortMode.NONE && supportsManualSort ? "manual" : sort}` + ), + })), + }, + }, + }, + ] as const + ); + + private _data = memoizeOne((config) => ({ + display_order: "none", + ...config, + })); + public setConfig(config: TodoListCardConfig): void { assert(config, cardConfigStruct); this._config = config; @@ -69,8 +95,8 @@ export class HuiTodoListEditor } @@ -83,7 +109,16 @@ export class HuiTodoListEditor fireEvent(this, "config-changed", { config }); } - private _computeLabelCallback = (schema: SchemaUnion) => { + private _todoListSupportsFeature(feature: number): boolean { + const entityStateObj = this._config?.entity + ? this.hass!.states[this._config?.entity] + : undefined; + return !!entityStateObj && supportsFeature(entityStateObj, feature); + } + + private _computeLabelCallback = ( + schema: SchemaUnion> + ) => { switch (schema.name) { case "theme": return `${this.hass!.localize( @@ -92,8 +127,9 @@ export class HuiTodoListEditor "ui.panel.lovelace.editor.card.config.optional" )})`; case "hide_completed": + case "display_order": return this.hass!.localize( - "ui.panel.lovelace.editor.card.todo-list.hide_completed" + `ui.panel.lovelace.editor.card.todo-list.${schema.name}` ); default: return this.hass!.localize( diff --git a/src/translations/en.json b/src/translations/en.json index 89bc99e4e28a..724a92af435c 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -6567,7 +6567,16 @@ "name": "To-do list", "description": "The To-do list card allows you to add, edit, check-off, and remove items from your to-do list.", "integration_not_loaded": "This card requires the `todo` integration to be set up.", - "hide_completed": "Hide completed items" + "hide_completed": "Hide completed items", + "display_order": "Display Order", + "sort_modes": { + "none": "Default", + "manual": "Manual", + "alpha_asc": "Alphabetical (A-Z)", + "alpha_desc": "Alphabetical (Z-A)", + "duedate_asc": "Due Date (Soonest First)", + "duedate_desc": "Due Date (Latest First)" + } }, "thermostat": { "name": "Thermostat",