Option to sort todo lists (#23579)

Co-authored-by: Wendelin <12148533+wendevlin@users.noreply.github.com>
This commit is contained in:
karwosts 2025-01-16 00:40:00 -08:00 committed by GitHub
parent 173d60b913
commit 57edb86c5c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 139 additions and 33 deletions

View File

@ -14,6 +14,14 @@ export const enum TodoItemStatus {
Completed = "completed", 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 { export interface TodoItem {
uid: string; uid: string;
summary: string; summary: string;

View File

@ -20,6 +20,7 @@ import memoizeOne from "memoize-one";
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element"; import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
import { supportsFeature } from "../../../common/entity/supports-feature"; import { supportsFeature } from "../../../common/entity/supports-feature";
import { stopPropagation } from "../../../common/dom/stop_propagation"; import { stopPropagation } from "../../../common/dom/stop_propagation";
import { caseInsensitiveStringCompare } from "../../../common/string/compare";
import "../../../components/ha-card"; import "../../../components/ha-card";
import "../../../components/ha-check-list-item"; import "../../../components/ha-check-list-item";
import "../../../components/ha-checkbox"; import "../../../components/ha-checkbox";
@ -42,6 +43,7 @@ import {
moveItem, moveItem,
subscribeItems, subscribeItems,
updateItem, updateItem,
TodoSortMode,
} from "../../../data/todo"; } from "../../../data/todo";
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box"; import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
import type { HomeAssistant } from "../../../types"; import type { HomeAssistant } from "../../../types";
@ -123,15 +125,53 @@ export class HuiTodoListCard extends LitElement implements LovelaceCard {
return undefined; return undefined;
} }
private _getCheckedItems = memoizeOne((items?: TodoItem[]): TodoItem[] => 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 items
? items.filter((item) => item.status === TodoItemStatus.Completed) ? this._sortItems(
items.filter((item) => item.status === TodoItemStatus.Completed),
sort
)
: [] : []
); );
private _getUncheckedItems = memoizeOne((items?: TodoItem[]): TodoItem[] => private _getUncheckedItems = memoizeOne(
(items?: TodoItem[], sort?: string | undefined): TodoItem[] =>
items items
? items.filter((item) => item.status === TodoItemStatus.NeedsAction) ? this._sortItems(
items.filter((item) => item.status === TodoItemStatus.NeedsAction),
sort
)
: [] : []
); );
@ -185,8 +225,14 @@ export class HuiTodoListCard extends LitElement implements LovelaceCard {
const unavailable = isUnavailableState(stateObj.state); const unavailable = isUnavailableState(stateObj.state);
const checkedItems = this._getCheckedItems(this._items); const checkedItems = this._getCheckedItems(
const uncheckedItems = this._getUncheckedItems(this._items); this._items,
this._config.display_order
);
const uncheckedItems = this._getUncheckedItems(
this._items,
this._config.display_order
);
return html` return html`
<ha-card <ha-card
@ -235,7 +281,9 @@ export class HuiTodoListCard extends LitElement implements LovelaceCard {
"ui.panel.lovelace.cards.todo-list.unchecked_items" "ui.panel.lovelace.cards.todo-list.unchecked_items"
)} )}
</h2> </h2>
${this._todoListSupportsFeature( ${(!this._config.display_order ||
this._config.display_order === TodoSortMode.NONE) &&
this._todoListSupportsFeature(
TodoListEntityFeature.MOVE_TODO_ITEM TodoListEntityFeature.MOVE_TODO_ITEM
) )
? html`<ha-button-menu @closed=${stopPropagation}> ? html`<ha-button-menu @closed=${stopPropagation}>
@ -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) { private _renderItems(items: TodoItem[], unavailable = false) {
return html` return html`
${repeat( ${repeat(
@ -331,11 +387,7 @@ export class HuiTodoListCard extends LitElement implements LovelaceCard {
); );
const showReorder = const showReorder =
item.status !== TodoItemStatus.Completed && this._reordering; item.status !== TodoItemStatus.Completed && this._reordering;
const due = item.due const due = this._getDueDate(item);
? item.due.includes("T")
? new Date(item.due)
: endOfDay(new Date(`${item.due}T00:00:00`))
: undefined;
const today = const today =
due && !item.due!.includes("T") && isSameDay(new Date(), due); due && !item.due!.includes("T") && isSameDay(new Date(), due);
return html` return html`

View File

@ -480,6 +480,7 @@ export interface TodoListCardConfig extends LovelaceCardConfig {
entity?: string; entity?: string;
hide_completed?: boolean; hide_completed?: boolean;
hide_create?: boolean; hide_create?: boolean;
sort?: string;
} }
export interface StackCardConfig extends LovelaceCardConfig { export interface StackCardConfig extends LovelaceCardConfig {

View File

@ -1,17 +1,21 @@
import type { CSSResultGroup } from "lit"; import type { CSSResultGroup } from "lit";
import { html, LitElement, nothing } from "lit"; import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators"; 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, object, optional, string } from "superstruct";
import { isComponentLoaded } from "../../../../common/config/is_component_loaded"; import { isComponentLoaded } from "../../../../common/config/is_component_loaded";
import { fireEvent } from "../../../../common/dom/fire_event"; import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-alert"; import "../../../../components/ha-alert";
import "../../../../components/ha-form/ha-form"; import "../../../../components/ha-form/ha-form";
import type { HomeAssistant } from "../../../../types"; import type { HomeAssistant } from "../../../../types";
import type { LocalizeFunc } from "../../../../common/translations/localize";
import type { TodoListCardConfig } from "../../cards/types"; import type { TodoListCardConfig } from "../../cards/types";
import type { LovelaceCardEditor } from "../../types"; import type { LovelaceCardEditor } from "../../types";
import { baseLovelaceCardConfig } from "../structs/base-card-struct"; import { baseLovelaceCardConfig } from "../structs/base-card-struct";
import type { SchemaUnion } from "../../../../components/ha-form/types"; import type { SchemaUnion } from "../../../../components/ha-form/types";
import { configElementStyle } from "./config-elements-style"; import { configElementStyle } from "./config-elements-style";
import { TodoListEntityFeature, TodoSortMode } from "../../../../data/todo";
import { supportsFeature } from "../../../../common/entity/supports-feature";
const cardConfigStruct = assign( const cardConfigStruct = assign(
baseLovelaceCardConfig, baseLovelaceCardConfig,
@ -21,21 +25,10 @@ const cardConfigStruct = assign(
entity: optional(string()), entity: optional(string()),
hide_completed: optional(boolean()), hide_completed: optional(boolean()),
hide_create: 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") @customElement("hui-todo-list-card-editor")
export class HuiTodoListEditor export class HuiTodoListEditor
extends LitElement extends LitElement
@ -45,6 +38,39 @@ export class HuiTodoListEditor
@state() private _config?: TodoListCardConfig; @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 { public setConfig(config: TodoListCardConfig): void {
assert(config, cardConfigStruct); assert(config, cardConfigStruct);
this._config = config; this._config = config;
@ -69,8 +95,8 @@ export class HuiTodoListEditor
} }
<ha-form <ha-form
.hass=${this.hass} .hass=${this.hass}
.data=${this._config} .data=${this._data(this._config)}
.schema=${SCHEMA} .schema=${this._schema(this.hass.localize, this._todoListSupportsFeature(TodoListEntityFeature.MOVE_TODO_ITEM))}
.computeLabel=${this._computeLabelCallback} .computeLabel=${this._computeLabelCallback}
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
></ha-form> ></ha-form>
@ -83,7 +109,16 @@ export class HuiTodoListEditor
fireEvent(this, "config-changed", { config }); fireEvent(this, "config-changed", { config });
} }
private _computeLabelCallback = (schema: SchemaUnion<typeof SCHEMA>) => { 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<ReturnType<typeof this._schema>>
) => {
switch (schema.name) { switch (schema.name) {
case "theme": case "theme":
return `${this.hass!.localize( return `${this.hass!.localize(
@ -92,8 +127,9 @@ export class HuiTodoListEditor
"ui.panel.lovelace.editor.card.config.optional" "ui.panel.lovelace.editor.card.config.optional"
)})`; )})`;
case "hide_completed": case "hide_completed":
case "display_order":
return this.hass!.localize( return this.hass!.localize(
"ui.panel.lovelace.editor.card.todo-list.hide_completed" `ui.panel.lovelace.editor.card.todo-list.${schema.name}`
); );
default: default:
return this.hass!.localize( return this.hass!.localize(

View File

@ -6961,7 +6961,16 @@
"name": "To-do list", "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.", "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.", "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": { "thermostat": {
"name": "Thermostat", "name": "Thermostat",