mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-12 20:06:33 +00:00
Render todo items with no state, change look of read only items (#24529)
* Render todo items with no state, change look of read only items Co-authored-by: Erik Montnemery <erik@montnemery.com> Co-authored-by: Wendelin <12148533+wendevlin@users.noreply.github.com> Co-authored-by: Wendelin <w@pe8.at>
This commit is contained in:
parent
7c46d2d2f4
commit
66dbafb5f5
@ -2,26 +2,36 @@ import type { TodoItem } from "../../../src/data/todo";
|
||||
import { TodoItemStatus } from "../../../src/data/todo";
|
||||
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
||||
|
||||
export const mockTodo = (hass: MockHomeAssistant) => {
|
||||
hass.mockWS("todo/item/list", () => ({
|
||||
items: [
|
||||
{
|
||||
uid: "12",
|
||||
summary: "Milk",
|
||||
status: TodoItemStatus.NeedsAction,
|
||||
},
|
||||
{
|
||||
uid: "13",
|
||||
summary: "Eggs",
|
||||
status: TodoItemStatus.NeedsAction,
|
||||
},
|
||||
{
|
||||
uid: "14",
|
||||
summary: "Oranges",
|
||||
status: TodoItemStatus.Completed,
|
||||
},
|
||||
] as TodoItem[],
|
||||
}));
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
hass.mockWS("todo/item/subscribe", (_msg, _hass) => () => {});
|
||||
const items = {
|
||||
items: [
|
||||
{
|
||||
uid: "12",
|
||||
summary: "Milk",
|
||||
status: TodoItemStatus.NeedsAction,
|
||||
},
|
||||
{
|
||||
uid: "13",
|
||||
summary: "Eggs",
|
||||
status: TodoItemStatus.NeedsAction,
|
||||
},
|
||||
{
|
||||
uid: "14",
|
||||
summary: "Oranges",
|
||||
status: TodoItemStatus.Completed,
|
||||
},
|
||||
{
|
||||
uid: "15",
|
||||
summary: "Beer",
|
||||
},
|
||||
] as TodoItem[],
|
||||
};
|
||||
|
||||
export const mockTodo = (hass: MockHomeAssistant) => {
|
||||
hass.mockWS("todo/item/list", () => items);
|
||||
hass.mockWS("todo/item/move", () => undefined);
|
||||
hass.mockWS("todo/item/subscribe", (_msg, _hass, onChange) => {
|
||||
onChange!(items);
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
return () => {};
|
||||
});
|
||||
};
|
||||
|
@ -1,11 +1,11 @@
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, query } from "lit/decorators";
|
||||
import { mockIcons } from "../../../../demo/src/stubs/icons";
|
||||
import { mockTodo } from "../../../../demo/src/stubs/todo";
|
||||
import { getEntity } from "../../../../src/fake_data/entity";
|
||||
import { provideHass } from "../../../../src/fake_data/provide_hass";
|
||||
import "../../components/demo-cards";
|
||||
import { getEntity } from "../../../../src/fake_data/entity";
|
||||
import { mockTodo } from "../../../../demo/src/stubs/todo";
|
||||
import { mockIcons } from "../../../../demo/src/stubs/icons";
|
||||
|
||||
const ENTITIES = [
|
||||
getEntity("todo", "shopping_list", "2", {
|
||||
|
@ -1,17 +1,54 @@
|
||||
import { css } from "lit";
|
||||
import { CheckListItemBase } from "@material/mwc-list/mwc-check-list-item-base";
|
||||
import { styles as controlStyles } from "@material/mwc-list/mwc-control-list-item.css";
|
||||
import { styles } from "@material/mwc-list/mwc-list-item.css";
|
||||
import { customElement } from "lit/decorators";
|
||||
import { css, html, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import "./ha-checkbox";
|
||||
|
||||
@customElement("ha-check-list-item")
|
||||
export class HaCheckListItem extends CheckListItemBase {
|
||||
@property({ type: Boolean, attribute: "checkbox-disabled" })
|
||||
checkboxDisabled = false;
|
||||
|
||||
@property({ type: Boolean })
|
||||
indeterminate = false;
|
||||
|
||||
async onChange(event) {
|
||||
super.onChange(event);
|
||||
fireEvent(this, event.type);
|
||||
}
|
||||
|
||||
override render() {
|
||||
const checkboxClasses = {
|
||||
"mdc-deprecated-list-item__graphic": this.left,
|
||||
"mdc-deprecated-list-item__meta": !this.left,
|
||||
};
|
||||
|
||||
const text = this.renderText();
|
||||
const graphic =
|
||||
this.graphic && this.graphic !== "control" && !this.left
|
||||
? this.renderGraphic()
|
||||
: nothing;
|
||||
const meta = this.hasMeta && this.left ? this.renderMeta() : nothing;
|
||||
const ripple = this.renderRipple();
|
||||
|
||||
return html` ${ripple} ${graphic} ${this.left ? "" : text}
|
||||
<span class=${classMap(checkboxClasses)}>
|
||||
<ha-checkbox
|
||||
reducedTouchTarget
|
||||
tabindex=${this.tabindex}
|
||||
.checked=${this.selected}
|
||||
.indeterminate=${this.indeterminate}
|
||||
?disabled=${this.disabled || this.checkboxDisabled}
|
||||
@change=${this.onChange}
|
||||
>
|
||||
</ha-checkbox>
|
||||
</span>
|
||||
${this.left ? text : ""} ${meta}`;
|
||||
}
|
||||
|
||||
static override styles = [
|
||||
styles,
|
||||
controlStyles,
|
||||
|
@ -25,7 +25,7 @@ export enum TodoSortMode {
|
||||
export interface TodoItem {
|
||||
uid: string;
|
||||
summary: string;
|
||||
status: TodoItemStatus;
|
||||
status: TodoItemStatus | null;
|
||||
description?: string | null;
|
||||
due?: string | null;
|
||||
}
|
||||
@ -72,7 +72,7 @@ export const fetchItems = async (
|
||||
export const subscribeItems = (
|
||||
hass: HomeAssistant,
|
||||
entity_id: string,
|
||||
callback: (item) => void
|
||||
callback: (update: TodoItems) => void
|
||||
) =>
|
||||
hass.connection.subscribeMessage<any>(callback, {
|
||||
type: "todo/item/subscribe",
|
||||
|
@ -156,6 +156,19 @@ export class HuiTodoListCard extends LitElement implements LovelaceCard {
|
||||
return items;
|
||||
}
|
||||
|
||||
private _getUncheckedAndItemsWithoutStatus = memoizeOne(
|
||||
(items?: TodoItem[], sort?: string | undefined): TodoItem[] =>
|
||||
items
|
||||
? this._sortItems(
|
||||
items.filter(
|
||||
(item) =>
|
||||
item.status === TodoItemStatus.NeedsAction || !item.status
|
||||
),
|
||||
sort
|
||||
)
|
||||
: []
|
||||
);
|
||||
|
||||
private _getCheckedItems = memoizeOne(
|
||||
(items?: TodoItem[], sort?: string | undefined): TodoItem[] =>
|
||||
items
|
||||
@ -176,6 +189,16 @@ export class HuiTodoListCard extends LitElement implements LovelaceCard {
|
||||
: []
|
||||
);
|
||||
|
||||
private _getItemsWithoutStatus = memoizeOne(
|
||||
(items?: TodoItem[], sort?: string | undefined): TodoItem[] =>
|
||||
items
|
||||
? this._sortItems(
|
||||
items.filter((item) => !item.status),
|
||||
sort
|
||||
)
|
||||
: []
|
||||
);
|
||||
|
||||
public willUpdate(
|
||||
changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>
|
||||
): void {
|
||||
@ -235,6 +258,18 @@ export class HuiTodoListCard extends LitElement implements LovelaceCard {
|
||||
this._config.display_order
|
||||
);
|
||||
|
||||
const itemsWithoutStatus = this._getItemsWithoutStatus(
|
||||
this._items,
|
||||
this._config.display_order
|
||||
);
|
||||
|
||||
const reorderableItems = this._reordering
|
||||
? this._getUncheckedAndItemsWithoutStatus(
|
||||
this._items,
|
||||
this._config.display_order
|
||||
)
|
||||
: undefined;
|
||||
|
||||
return html`
|
||||
<ha-card
|
||||
.header=${this._config.title}
|
||||
@ -274,7 +309,24 @@ export class HuiTodoListCard extends LitElement implements LovelaceCard {
|
||||
@item-moved=${this._itemMoved}
|
||||
>
|
||||
<ha-list wrapFocus multi>
|
||||
${uncheckedItems.length
|
||||
${!uncheckedItems.length && !itemsWithoutStatus.length
|
||||
? html`<p class="empty">
|
||||
${this.hass.localize(
|
||||
"ui.panel.lovelace.cards.todo-list.no_unchecked_items"
|
||||
)}
|
||||
</p>`
|
||||
: this._reordering
|
||||
? html`<div class="header" role="seperator">
|
||||
<h2>
|
||||
${this.hass!.localize(
|
||||
"ui.panel.lovelace.cards.todo-list.reorder_items"
|
||||
)}
|
||||
</h2>
|
||||
${this._renderMenu(this._config, unavailable)}
|
||||
</div>
|
||||
${this._renderItems(reorderableItems ?? [], unavailable)}`
|
||||
: nothing}
|
||||
${!this._reordering && uncheckedItems.length
|
||||
? html`
|
||||
<div class="header" role="seperator">
|
||||
<h2>
|
||||
@ -282,47 +334,35 @@ export class HuiTodoListCard extends LitElement implements LovelaceCard {
|
||||
"ui.panel.lovelace.cards.todo-list.unchecked_items"
|
||||
)}
|
||||
</h2>
|
||||
${(!this._config.display_order ||
|
||||
this._config.display_order === TodoSortMode.NONE) &&
|
||||
this._todoListSupportsFeature(
|
||||
TodoListEntityFeature.MOVE_TODO_ITEM
|
||||
)
|
||||
? html`<ha-button-menu
|
||||
@closed=${stopPropagation}
|
||||
fixed
|
||||
@action=${this._handlePrimaryMenuAction}
|
||||
>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
<ha-list-item graphic="icon">
|
||||
${this.hass!.localize(
|
||||
this._reordering
|
||||
? "ui.panel.lovelace.cards.todo-list.exit_reorder_items"
|
||||
: "ui.panel.lovelace.cards.todo-list.reorder_items"
|
||||
)}
|
||||
<ha-svg-icon
|
||||
slot="graphic"
|
||||
.path=${mdiSort}
|
||||
.disabled=${unavailable}
|
||||
>
|
||||
</ha-svg-icon>
|
||||
</ha-list-item>
|
||||
</ha-button-menu>`
|
||||
: nothing}
|
||||
${this._renderMenu(this._config, unavailable)}
|
||||
</div>
|
||||
${this._renderItems(uncheckedItems, unavailable)}
|
||||
`
|
||||
: html`<p class="empty">
|
||||
${this.hass.localize(
|
||||
"ui.panel.lovelace.cards.todo-list.no_unchecked_items"
|
||||
)}
|
||||
</p>`}
|
||||
: nothing}
|
||||
${!this._reordering && itemsWithoutStatus.length
|
||||
? html`
|
||||
<div>
|
||||
${uncheckedItems.length
|
||||
? html`<div class="divider" role="seperator"></div>`
|
||||
: nothing}
|
||||
<div class="header" role="seperator">
|
||||
<h2>
|
||||
${this.hass!.localize(
|
||||
"ui.panel.lovelace.cards.todo-list.no_status_items"
|
||||
)}
|
||||
</h2>
|
||||
${!uncheckedItems.length
|
||||
? this._renderMenu(this._config, unavailable)
|
||||
: nothing}
|
||||
</div>
|
||||
</div>
|
||||
${this._renderItems(itemsWithoutStatus, unavailable)}
|
||||
`
|
||||
: nothing}
|
||||
${!this._config.hide_completed && checkedItems.length
|
||||
? html`
|
||||
<div role="separator">
|
||||
<div class="divider"></div>
|
||||
<div>
|
||||
<div class="divider" role="separator"></div>
|
||||
<div class="header">
|
||||
<h2>
|
||||
${this.hass!.localize(
|
||||
@ -359,13 +399,43 @@ export class HuiTodoListCard extends LitElement implements LovelaceCard {
|
||||
</div>
|
||||
${this._renderItems(checkedItems, unavailable)}
|
||||
`
|
||||
: ""}
|
||||
: nothing}
|
||||
</ha-list>
|
||||
</ha-sortable>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderMenu(config: TodoListCardConfig, unavailable: boolean) {
|
||||
return (!config.display_order ||
|
||||
config.display_order === TodoSortMode.NONE) &&
|
||||
this._todoListSupportsFeature(TodoListEntityFeature.MOVE_TODO_ITEM)
|
||||
? html`<ha-button-menu
|
||||
@closed=${stopPropagation}
|
||||
fixed
|
||||
@action=${this._handlePrimaryMenuAction}
|
||||
>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
<ha-list-item graphic="icon">
|
||||
${this.hass!.localize(
|
||||
this._reordering
|
||||
? "ui.panel.lovelace.cards.todo-list.exit_reorder_items"
|
||||
: "ui.panel.lovelace.cards.todo-list.reorder_items"
|
||||
)}
|
||||
<ha-svg-icon
|
||||
slot="graphic"
|
||||
.path=${mdiSort}
|
||||
.disabled=${unavailable}
|
||||
>
|
||||
</ha-svg-icon>
|
||||
</ha-list-item>
|
||||
</ha-button-menu>`
|
||||
: nothing;
|
||||
}
|
||||
|
||||
private _getDueDate(item: TodoItem): Date | undefined {
|
||||
return item.due
|
||||
? item.due.includes("T")
|
||||
@ -397,16 +467,19 @@ export class HuiTodoListCard extends LitElement implements LovelaceCard {
|
||||
left
|
||||
.hasMeta=${showReorder || showDelete}
|
||||
class="editRow ${classMap({
|
||||
draggable: item.status === TodoItemStatus.NeedsAction,
|
||||
draggable: item.status !== TodoItemStatus.Completed,
|
||||
completed: item.status === TodoItemStatus.Completed,
|
||||
multiline: Boolean(item.description || item.due),
|
||||
})}"
|
||||
.selected=${item.status === TodoItemStatus.Completed}
|
||||
.disabled=${unavailable ||
|
||||
!this._todoListSupportsFeature(
|
||||
.disabled=${unavailable}
|
||||
.checkboxDisabled=${!this._todoListSupportsFeature(
|
||||
TodoListEntityFeature.UPDATE_TODO_ITEM
|
||||
)}
|
||||
.indeterminate=${!item.status}
|
||||
.noninteractive=${!this._todoListSupportsFeature(
|
||||
TodoListEntityFeature.UPDATE_TODO_ITEM
|
||||
)}
|
||||
item-id=${item.uid}
|
||||
.itemId=${item.uid}
|
||||
@change=${this._completeItem}
|
||||
@click=${this._openItem}
|
||||
@ -631,35 +704,53 @@ export class HuiTodoListCard extends LitElement implements LovelaceCard {
|
||||
this._moveItem(oldIndex, newIndex);
|
||||
}
|
||||
|
||||
private async _moveItem(oldIndex: number, newIndex: number) {
|
||||
// correct index for header
|
||||
oldIndex -= 1;
|
||||
newIndex -= 1;
|
||||
const uncheckedItems = this._getUncheckedItems(this._items);
|
||||
const item = uncheckedItems[oldIndex];
|
||||
let prevItem: TodoItem | undefined;
|
||||
if (newIndex > 0) {
|
||||
if (newIndex < oldIndex) {
|
||||
prevItem = uncheckedItems[newIndex - 1];
|
||||
} else {
|
||||
prevItem = uncheckedItems[newIndex];
|
||||
private _findFirstItem(
|
||||
items: HTMLCollection,
|
||||
start: number,
|
||||
direction: "up" | "down"
|
||||
) {
|
||||
let item: Element | undefined;
|
||||
let index = direction === "up" ? start - 1 : start;
|
||||
while (item?.localName !== "ha-check-list-item") {
|
||||
item = items[index];
|
||||
index = direction === "up" ? index - 1 : index + 1;
|
||||
if (!item) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return item;
|
||||
}
|
||||
|
||||
private async _moveItem(oldIndex: number, newIndex: number) {
|
||||
await this.updateComplete;
|
||||
|
||||
const list = this.renderRoot.querySelector("ha-list")!;
|
||||
|
||||
const items = list.children;
|
||||
|
||||
const itemId = (items[oldIndex] as any).itemId as string;
|
||||
|
||||
const prevItemId = (
|
||||
this._findFirstItem(
|
||||
items,
|
||||
newIndex,
|
||||
newIndex < oldIndex ? "up" : "down"
|
||||
) as any
|
||||
)?.itemId;
|
||||
|
||||
// Optimistic change
|
||||
const itemIndex = this._items!.findIndex((itm) => itm.uid === item.uid);
|
||||
this._items!.splice(itemIndex, 1);
|
||||
if (newIndex === 0) {
|
||||
const itemIndex = this._items!.findIndex((itm) => itm.uid === itemId);
|
||||
const item = this._items!.splice(itemIndex, 1)[0];
|
||||
|
||||
if (!prevItemId) {
|
||||
this._items!.unshift(item);
|
||||
} else {
|
||||
const prevIndex = this._items!.findIndex(
|
||||
(itm) => itm.uid === prevItem!.uid
|
||||
);
|
||||
const prevIndex = this._items!.findIndex((itm) => itm.uid === prevItemId);
|
||||
this._items!.splice(prevIndex + 1, 0, item);
|
||||
}
|
||||
this._items = [...this._items!];
|
||||
|
||||
await moveItem(this.hass!, this._entityId!, item.uid, prevItem?.uid);
|
||||
await moveItem(this.hass!, this._entityId!, itemId, prevItemId);
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
|
@ -6529,6 +6529,7 @@
|
||||
"unchecked_items": "Active",
|
||||
"no_unchecked_items": "You have no to-do items!",
|
||||
"checked_items": "Completed",
|
||||
"no_status_items": "No status",
|
||||
"clear_items": "Remove completed items",
|
||||
"add_item": "Add item",
|
||||
"today": "Today",
|
||||
|
Loading…
x
Reference in New Issue
Block a user