From c85a73eba225f115a00086cb00712c29414a9f31 Mon Sep 17 00:00:00 2001 From: karwosts Date: Sat, 4 Jan 2025 08:10:43 -0800 Subject: [PATCH 1/5] Option to sort todo lists --- .../lovelace/cards/hui-todo-list-card.ts | 76 +++++++++++++++---- src/panels/lovelace/cards/types.ts | 1 + .../config-elements/hui-todo-list-editor.ts | 61 +++++++++++---- src/translations/en.json | 8 +- 4 files changed, 113 insertions(+), 33 deletions(-) diff --git a/src/panels/lovelace/cards/hui-todo-list-card.ts b/src/panels/lovelace/cards/hui-todo-list-card.ts index 7182212aa38e..f10a1d36c99a 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"; @@ -51,6 +52,10 @@ import { createEntityNotFoundWarning } from "../components/hui-warning"; import type { LovelaceCard, LovelaceCardEditor } from "../types"; import type { TodoListCardConfig } from "./types"; +export const SORT_ALPHA = "alpha"; +export const SORT_DUEDATE = "duedate"; +export const SORT_NONE = "none"; + @customElement("hui-todo-list-card") export class HuiTodoListCard extends LitElement implements LovelaceCard { public static async getConfigElement(): Promise { @@ -123,16 +128,47 @@ 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 | undefined) { + if (sort === SORT_ALPHA) { + return items.sort((a, b) => + caseInsensitiveStringCompare( + a.summary, + b.summary, + this.hass?.locale.language + ) + ); + } + if (sort === SORT_DUEDATE) { + 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 ? -1 : 1; + }); + } + 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 +221,11 @@ 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.sort); + const uncheckedItems = this._getUncheckedItems( + this._items, + this._config.sort + ); return html` - ${this._todoListSupportsFeature( + ${(!this._config.sort || this._config.sort === SORT_NONE) && + this._todoListSupportsFeature( TodoListEntityFeature.MOVE_TODO_ITEM ) ? html` @@ -317,6 +357,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( @@ -332,11 +380,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 7045e90aa690..01bdb9f281ce 100644 --- a/src/panels/lovelace/cards/types.ts +++ b/src/panels/lovelace/cards/types.ts @@ -478,6 +478,7 @@ export interface TodoListCardConfig extends LovelaceCardConfig { theme?: string; entity?: string; hide_completed?: 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 6179225a2597..c215518a1556 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,24 @@ 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 { + SORT_ALPHA, + SORT_DUEDATE, + SORT_NONE, +} from "../../cards/hui-todo-list-card"; const cardConfigStruct = assign( baseLovelaceCardConfig, @@ -20,21 +27,10 @@ const cardConfigStruct = assign( theme: optional(string()), entity: optional(string()), hide_completed: optional(boolean()), + sort: 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 @@ -44,6 +40,36 @@ export class HuiTodoListEditor @state() private _config?: TodoListCardConfig; + private _schema = memoizeOne( + (localize: LocalizeFunc) => + [ + { name: "title", selector: { text: {} } }, + { + name: "entity", + selector: { + entity: { domain: "todo" }, + }, + }, + { name: "theme", selector: { theme: {} } }, + { name: "hide_completed", selector: { boolean: {} } }, + { + name: "sort", + selector: { + select: { + options: [SORT_NONE, SORT_ALPHA, SORT_DUEDATE].map((sort) => ({ + value: sort, + label: localize( + `ui.panel.lovelace.editor.card.todo-list.sort_modes.${sort}` + ), + })), + }, + }, + }, + ] as const + ); + + private _data = memoizeOne((config) => ({ sort: "none", ...config })); + public setConfig(config: TodoListCardConfig): void { assert(config, cardConfigStruct); this._config = config; @@ -68,8 +94,8 @@ export class HuiTodoListEditor } @@ -82,7 +108,9 @@ export class HuiTodoListEditor fireEvent(this, "config-changed", { config }); } - private _computeLabelCallback = (schema: SchemaUnion) => { + private _computeLabelCallback = ( + schema: SchemaUnion> + ) => { switch (schema.name) { case "theme": return `${this.hass!.localize( @@ -91,8 +119,9 @@ export class HuiTodoListEditor "ui.panel.lovelace.editor.card.config.optional" )})`; case "hide_completed": + case "sort": 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 ef2321facdae..3cab8f0d3f2a 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -6563,7 +6563,13 @@ "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", + "sort": "Sort Items", + "sort_modes": { + "none": "None", + "alpha": "Alphabetical", + "duedate": "Due Date" + } }, "thermostat": { "name": "Thermostat", From a585357ff068cf3364b3aae89b75e50d86fb5d65 Mon Sep 17 00:00:00 2001 From: karwosts <32912880+karwosts@users.noreply.github.com> Date: Mon, 6 Jan 2025 05:25:28 -0800 Subject: [PATCH 2/5] fix merge --- src/panels/lovelace/cards/types.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/panels/lovelace/cards/types.ts b/src/panels/lovelace/cards/types.ts index b6f78fd803c8..b6b82b90f108 100644 --- a/src/panels/lovelace/cards/types.ts +++ b/src/panels/lovelace/cards/types.ts @@ -480,6 +480,7 @@ export interface TodoListCardConfig extends LovelaceCardConfig { hide_completed?: boolean; hide_create?: boolean; sort?: string; +} export interface StackCardConfig extends LovelaceCardConfig { cards: LovelaceCardConfig[]; From 27e6feeb629ee156a3223eb72eae7fb3a1c97776 Mon Sep 17 00:00:00 2001 From: karwosts Date: Mon, 6 Jan 2025 13:43:14 +0000 Subject: [PATCH 3/5] prettier --- .../lovelace/editor/config-elements/hui-todo-list-editor.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 99d865cda530..d412a6308564 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 @@ -27,7 +27,7 @@ const cardConfigStruct = assign( theme: optional(string()), entity: optional(string()), hide_completed: optional(boolean()), - hide_create: optional(boolean()), + hide_create: optional(boolean()), sort: optional(string()), }) ); From 08b1ede9c0131038cc84e648dcd880ad280a9942 Mon Sep 17 00:00:00 2001 From: karwosts Date: Wed, 15 Jan 2025 15:48:05 +0000 Subject: [PATCH 4/5] update from feedback --- src/data/todo.ts | 8 ++++ .../lovelace/cards/hui-todo-list-card.ts | 40 +++++++++++-------- .../config-elements/hui-todo-list-editor.ts | 32 +++++++++------ src/translations/en.json | 11 +++-- 4 files changed, 58 insertions(+), 33 deletions(-) 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 a58a9c7c4d13..9bae9b59ff4c 100644 --- a/src/panels/lovelace/cards/hui-todo-list-card.ts +++ b/src/panels/lovelace/cards/hui-todo-list-card.ts @@ -43,6 +43,7 @@ import { moveItem, subscribeItems, updateItem, + TodoSortMode, } from "../../../data/todo"; import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box"; import type { HomeAssistant } from "../../../types"; @@ -52,10 +53,6 @@ import { createEntityNotFoundWarning } from "../components/hui-warning"; import type { LovelaceCard, LovelaceCardEditor } from "../types"; import type { TodoListCardConfig } from "./types"; -export const SORT_ALPHA = "alpha"; -export const SORT_DUEDATE = "duedate"; -export const SORT_NONE = "none"; - @customElement("hui-todo-list-card") export class HuiTodoListCard extends LitElement implements LovelaceCard { public static async getConfigElement(): Promise { @@ -129,23 +126,30 @@ export class HuiTodoListCard extends LitElement implements LovelaceCard { } private _sortItems(items: TodoItem[], sort?: string | undefined) { - if (sort === SORT_ALPHA) { - return items.sort((a, b) => - caseInsensitiveStringCompare( - a.summary, - b.summary, - this.hass?.locale.language - ) + 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 === SORT_DUEDATE) { + 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 ? -1 : 1; + return aDue < bDue ? -sortOrder : sortOrder; }); } return items; @@ -221,10 +225,13 @@ export class HuiTodoListCard extends LitElement implements LovelaceCard { const unavailable = isUnavailableState(stateObj.state); - const checkedItems = this._getCheckedItems(this._items, this._config.sort); + const checkedItems = this._getCheckedItems( + this._items, + this._config.display_order + ); const uncheckedItems = this._getUncheckedItems( this._items, - this._config.sort + this._config.display_order ); return html` @@ -274,7 +281,8 @@ export class HuiTodoListCard extends LitElement implements LovelaceCard { "ui.panel.lovelace.cards.todo-list.unchecked_items" )} - ${(!this._config.sort || this._config.sort === SORT_NONE) && + ${(!this._config.display_order || + this._config.display_order === TodoSortMode.NONE) && this._todoListSupportsFeature( TodoListEntityFeature.MOVE_TODO_ITEM ) 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 d412a6308564..807880c5a461 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 @@ -14,11 +14,8 @@ 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 { - SORT_ALPHA, - SORT_DUEDATE, - SORT_NONE, -} from "../../cards/hui-todo-list-card"; +import { TodoListEntityFeature, TodoSortMode } from "../../../../data/todo"; +import { supportsFeature } from "../../../../common/entity/supports-feature"; const cardConfigStruct = assign( baseLovelaceCardConfig, @@ -28,7 +25,7 @@ const cardConfigStruct = assign( entity: optional(string()), hide_completed: optional(boolean()), hide_create: optional(boolean()), - sort: optional(string()), + display_order: optional(string()), }) ); @@ -42,7 +39,7 @@ export class HuiTodoListEditor @state() private _config?: TodoListCardConfig; private _schema = memoizeOne( - (localize: LocalizeFunc) => + (localize: LocalizeFunc, supportsManualSort: boolean) => [ { name: "title", selector: { text: {} } }, { @@ -54,13 +51,13 @@ export class HuiTodoListEditor { name: "theme", selector: { theme: {} } }, { name: "hide_completed", selector: { boolean: {} } }, { - name: "sort", + name: "display_order", selector: { select: { - options: [SORT_NONE, SORT_ALPHA, SORT_DUEDATE].map((sort) => ({ + options: Object.values(TodoSortMode).map((sort) => ({ value: sort, label: localize( - `ui.panel.lovelace.editor.card.todo-list.sort_modes.${sort}` + `ui.panel.lovelace.editor.card.todo-list.sort_modes.${sort === TodoSortMode.NONE && supportsManualSort ? "manual" : sort}` ), })), }, @@ -69,7 +66,10 @@ export class HuiTodoListEditor ] as const ); - private _data = memoizeOne((config) => ({ sort: "none", ...config })); + private _data = memoizeOne((config) => ({ + display_order: "none", + ...config, + })); public setConfig(config: TodoListCardConfig): void { assert(config, cardConfigStruct); @@ -96,7 +96,7 @@ export class HuiTodoListEditor @@ -109,6 +109,12 @@ export class HuiTodoListEditor fireEvent(this, "config-changed", { config }); } + private _todoListSupportsFeature(feature: number): boolean { + const entityStateObj = + this._config?.entity && this.hass!.states[this._config.entity]; + return entityStateObj && supportsFeature(entityStateObj, feature); + } + private _computeLabelCallback = ( schema: SchemaUnion> ) => { @@ -120,7 +126,7 @@ export class HuiTodoListEditor "ui.panel.lovelace.editor.card.config.optional" )})`; case "hide_completed": - case "sort": + case "display_order": return this.hass!.localize( `ui.panel.lovelace.editor.card.todo-list.${schema.name}` ); diff --git a/src/translations/en.json b/src/translations/en.json index 0e04b77a0250..724a92af435c 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -6568,11 +6568,14 @@ "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", - "sort": "Sort Items", + "display_order": "Display Order", "sort_modes": { - "none": "None", - "alpha": "Alphabetical", - "duedate": "Due Date" + "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": { From fd3ec7170b405e946fe9a3d7cbb6033f571d9478 Mon Sep 17 00:00:00 2001 From: karwosts Date: Wed, 15 Jan 2025 15:57:12 +0000 Subject: [PATCH 5/5] lint --- .../editor/config-elements/hui-todo-list-editor.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) 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 807880c5a461..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 @@ -110,9 +110,10 @@ export class HuiTodoListEditor } private _todoListSupportsFeature(feature: number): boolean { - const entityStateObj = - this._config?.entity && this.hass!.states[this._config.entity]; - return entityStateObj && supportsFeature(entityStateObj, feature); + const entityStateObj = this._config?.entity + ? this.hass!.states[this._config?.entity] + : undefined; + return !!entityStateObj && supportsFeature(entityStateObj, feature); } private _computeLabelCallback = (