Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Option to sort todo lists #23579

Open
wants to merge 6 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/data/todo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
84 changes: 68 additions & 16 deletions src/panels/lovelace/cards/hui-todo-list-card.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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";
Expand Down Expand Up @@ -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 | undefined) {
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(
Expand Down Expand Up @@ -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`
<ha-card
Expand Down Expand Up @@ -235,7 +281,9 @@ export class HuiTodoListCard extends LitElement implements LovelaceCard {
"ui.panel.lovelace.cards.todo-list.unchecked_items"
)}
</h2>
${this._todoListSupportsFeature(
${(!this._config.display_order ||
this._config.display_order === TodoSortMode.NONE) &&
this._todoListSupportsFeature(
TodoListEntityFeature.MOVE_TODO_ITEM
)
? html`<ha-button-menu @closed=${stopPropagation}>
Expand Down Expand Up @@ -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(
Expand All @@ -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`
Expand Down
1 change: 1 addition & 0 deletions src/panels/lovelace/cards/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -479,6 +479,7 @@ export interface TodoListCardConfig extends LovelaceCardConfig {
entity?: string;
hide_completed?: boolean;
hide_create?: boolean;
sort?: string;
}

export interface StackCardConfig extends LovelaceCardConfig {
Expand Down
68 changes: 52 additions & 16 deletions src/panels/lovelace/editor/config-elements/hui-todo-list-editor.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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
Expand All @@ -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;
Expand All @@ -69,8 +95,8 @@ export class HuiTodoListEditor
}
<ha-form
.hass=${this.hass}
.data=${this._config}
.schema=${SCHEMA}
.data=${this._data(this._config)}
.schema=${this._schema(this.hass.localize, this._todoListSupportsFeature(TodoListEntityFeature.MOVE_TODO_ITEM))}
.computeLabel=${this._computeLabelCallback}
@value-changed=${this._valueChanged}
></ha-form>
Expand All @@ -83,7 +109,16 @@ export class HuiTodoListEditor
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) {
case "theme":
return `${this.hass!.localize(
Expand All @@ -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(
Expand Down
11 changes: 10 additions & 1 deletion src/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading