mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-28 19:56:42 +00:00
Add description and due support to todo lists (#19107)
This commit is contained in:
parent
8f07e6f141
commit
09dcc29175
@ -3,9 +3,15 @@ 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 { fireEvent } from "../common/dom/fire_event";
|
||||
|
||||
@customElement("ha-check-list-item")
|
||||
export class HaCheckListItem extends CheckListItemBase {
|
||||
async onChange(event) {
|
||||
super.onChange(event);
|
||||
fireEvent(this, event.type);
|
||||
}
|
||||
|
||||
static override styles = [
|
||||
styles,
|
||||
controlStyles,
|
||||
@ -22,6 +28,12 @@ export class HaCheckListItem extends CheckListItemBase {
|
||||
margin-inline-start: 0px;
|
||||
direction: var(--direction);
|
||||
}
|
||||
.mdc-deprecated-list-item__meta {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.mdc-deprecated-list-item__graphic {
|
||||
margin-top: var(--check-list-item-graphic-margin-top);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@ -18,6 +18,8 @@ export interface TodoItem {
|
||||
uid: string;
|
||||
summary: string;
|
||||
status: TodoItemStatus;
|
||||
description?: string;
|
||||
due?: string;
|
||||
}
|
||||
|
||||
export const enum TodoListEntityFeature {
|
||||
@ -25,6 +27,9 @@ export const enum TodoListEntityFeature {
|
||||
DELETE_TODO_ITEM = 2,
|
||||
UPDATE_TODO_ITEM = 4,
|
||||
MOVE_TODO_ITEM = 8,
|
||||
SET_DUE_DATE_ON_ITEM = 16,
|
||||
SET_DUE_DATETIME_ON_ITEM = 32,
|
||||
SET_DESCRIPTION_ON_ITEM = 64,
|
||||
}
|
||||
|
||||
export const getTodoLists = (hass: HomeAssistant): TodoList[] =>
|
||||
@ -74,20 +79,30 @@ export const updateItem = (
|
||||
hass.callService(
|
||||
"todo",
|
||||
"update_item",
|
||||
{ item: item.uid, rename: item.summary, status: item.status },
|
||||
{
|
||||
item: item.uid,
|
||||
rename: item.summary,
|
||||
status: item.status,
|
||||
description: item.description || undefined,
|
||||
due_datetime: item.due?.includes("T") ? item.due : undefined,
|
||||
due_date: item.due?.includes("T") ? undefined : item.due || undefined,
|
||||
},
|
||||
{ entity_id }
|
||||
);
|
||||
|
||||
export const createItem = (
|
||||
hass: HomeAssistant,
|
||||
entity_id: string,
|
||||
summary: string
|
||||
item: Omit<TodoItem, "uid" | "status">
|
||||
): Promise<ServiceCallResponse> =>
|
||||
hass.callService(
|
||||
"todo",
|
||||
"add_item",
|
||||
{
|
||||
item: summary,
|
||||
item: item.summary,
|
||||
description: item.description || undefined,
|
||||
due_datetime: item.due?.includes("T") ? item.due : undefined,
|
||||
due_date: item.due?.includes("T") ? undefined : item.due,
|
||||
},
|
||||
{ entity_id }
|
||||
);
|
||||
|
@ -1,10 +1,14 @@
|
||||
import "@material/mwc-list/mwc-list";
|
||||
import {
|
||||
mdiClock,
|
||||
mdiDelete,
|
||||
mdiDeleteSweep,
|
||||
mdiDotsVertical,
|
||||
mdiDrag,
|
||||
mdiNotificationClearAll,
|
||||
mdiPlus,
|
||||
mdiSort,
|
||||
} from "@mdi/js";
|
||||
import { endOfDay, isSameDay } from "date-fns";
|
||||
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import {
|
||||
CSSResultGroup,
|
||||
@ -22,11 +26,13 @@ import memoizeOne from "memoize-one";
|
||||
import type { SortableEvent } from "sortablejs";
|
||||
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
|
||||
import { supportsFeature } from "../../../common/entity/supports-feature";
|
||||
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-check-list-item";
|
||||
import "../../../components/ha-checkbox";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-list-item";
|
||||
import "../../../components/ha-markdown-element";
|
||||
import "../../../components/ha-relative-time";
|
||||
import "../../../components/ha-select";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import "../../../components/ha-textfield";
|
||||
@ -42,8 +48,10 @@ import {
|
||||
subscribeItems,
|
||||
updateItem,
|
||||
} from "../../../data/todo";
|
||||
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||
import type { SortableInstance } from "../../../resources/sortable";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { showTodoItemEditDialog } from "../../todo/show-dialog-todo-item-editor";
|
||||
import { findEntities } from "../common/find-entities";
|
||||
import { createEntityNotFoundWarning } from "../components/hui-warning";
|
||||
import { LovelaceCard, LovelaceCardEditor } from "../types";
|
||||
@ -199,6 +207,14 @@ export class HuiTodoListCard extends LitElement implements LovelaceCard {
|
||||
<div class="addRow">
|
||||
${this.todoListSupportsFeature(TodoListEntityFeature.CREATE_TODO_ITEM)
|
||||
? html`
|
||||
<ha-textfield
|
||||
class="addBox"
|
||||
.placeholder=${this.hass!.localize(
|
||||
"ui.panel.lovelace.cards.todo-list.add_item"
|
||||
)}
|
||||
@keydown=${this._addKeyPress}
|
||||
.disabled=${unavailable}
|
||||
></ha-textfield>
|
||||
<ha-icon-button
|
||||
class="addButton"
|
||||
.path=${mdiPlus}
|
||||
@ -209,38 +225,53 @@ export class HuiTodoListCard extends LitElement implements LovelaceCard {
|
||||
@click=${this._addItem}
|
||||
>
|
||||
</ha-icon-button>
|
||||
<ha-textfield
|
||||
class="addBox"
|
||||
.placeholder=${this.hass!.localize(
|
||||
"ui.panel.lovelace.cards.todo-list.add_item"
|
||||
)}
|
||||
@keydown=${this._addKeyPress}
|
||||
.disabled=${unavailable}
|
||||
></ha-textfield>
|
||||
`
|
||||
: nothing}
|
||||
${this.todoListSupportsFeature(TodoListEntityFeature.MOVE_TODO_ITEM)
|
||||
? html`
|
||||
</div>
|
||||
${uncheckedItems.length
|
||||
? html` <div class="header">
|
||||
<span>
|
||||
${this.hass!.localize(
|
||||
"ui.panel.lovelace.cards.todo-list.unchecked_items"
|
||||
)}
|
||||
</span>
|
||||
${this.todoListSupportsFeature(
|
||||
TodoListEntityFeature.MOVE_TODO_ITEM
|
||||
)
|
||||
? html`<ha-button-menu>
|
||||
<ha-icon-button
|
||||
class="reorderButton"
|
||||
.path=${mdiSort}
|
||||
.title=${this.hass!.localize(
|
||||
slot="trigger"
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
<ha-list-item
|
||||
@click=${this._toggleReorder}
|
||||
graphic="icon"
|
||||
>
|
||||
${this.hass!.localize(
|
||||
"ui.panel.lovelace.cards.todo-list.reorder_items"
|
||||
)}
|
||||
@click=${this._toggleReorder}
|
||||
<ha-svg-icon
|
||||
slot="graphic"
|
||||
.path=${mdiSort}
|
||||
.disabled=${unavailable}
|
||||
>
|
||||
</ha-icon-button>
|
||||
`
|
||||
</ha-svg-icon>
|
||||
</ha-list-item>
|
||||
</ha-button-menu>`
|
||||
: nothing}
|
||||
</div>
|
||||
<div id="unchecked">
|
||||
<mwc-list id="unchecked">
|
||||
${this._renderItems(uncheckedItems, unavailable)}
|
||||
</div>
|
||||
</mwc-list>`
|
||||
: html`<p class="empty">
|
||||
${this.hass.localize(
|
||||
"ui.panel.lovelace.cards.todo-list.no_unchecked_items"
|
||||
)}
|
||||
</p>`}
|
||||
${checkedItems.length
|
||||
? html`
|
||||
<div class="divider"></div>
|
||||
<div class="checked">
|
||||
<div class="header">
|
||||
<span>
|
||||
${this.hass!.localize(
|
||||
"ui.panel.lovelace.cards.todo-list.checked_items"
|
||||
@ -249,65 +280,33 @@ export class HuiTodoListCard extends LitElement implements LovelaceCard {
|
||||
${this.todoListSupportsFeature(
|
||||
TodoListEntityFeature.DELETE_TODO_ITEM
|
||||
)
|
||||
? html`<ha-svg-icon
|
||||
class="clearall"
|
||||
tabindex="0"
|
||||
.path=${mdiNotificationClearAll}
|
||||
.title=${this.hass!.localize(
|
||||
? html`<ha-button-menu>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
<ha-list-item
|
||||
@click=${this._clearCompletedItems}
|
||||
graphic="icon"
|
||||
class="warning"
|
||||
>
|
||||
${this.hass!.localize(
|
||||
"ui.panel.lovelace.cards.todo-list.clear_items"
|
||||
)}
|
||||
@click=${this._clearCompletedItems}
|
||||
<ha-svg-icon
|
||||
class="warning"
|
||||
slot="graphic"
|
||||
.path=${mdiDeleteSweep}
|
||||
.disabled=${unavailable}
|
||||
>
|
||||
</ha-svg-icon>`
|
||||
</ha-svg-icon>
|
||||
</ha-list-item>
|
||||
</ha-button-menu>`
|
||||
: nothing}
|
||||
</div>
|
||||
${repeat(
|
||||
checkedItems,
|
||||
(item) => item.uid,
|
||||
(item) => html`
|
||||
<div class="editRow">
|
||||
${this.todoListSupportsFeature(
|
||||
TodoListEntityFeature.UPDATE_TODO_ITEM
|
||||
)
|
||||
? html`<ha-checkbox
|
||||
tabindex="0"
|
||||
.checked=${item.status === TodoItemStatus.Completed}
|
||||
.itemId=${item.uid}
|
||||
@change=${this._completeItem}
|
||||
.disabled=${unavailable}
|
||||
></ha-checkbox>`
|
||||
: nothing}
|
||||
<ha-textfield
|
||||
class="item"
|
||||
.disabled=${unavailable ||
|
||||
!this.todoListSupportsFeature(
|
||||
TodoListEntityFeature.UPDATE_TODO_ITEM
|
||||
)}
|
||||
.value=${item.summary}
|
||||
.itemId=${item.uid}
|
||||
@change=${this._saveEdit}
|
||||
></ha-textfield>
|
||||
${this.todoListSupportsFeature(
|
||||
TodoListEntityFeature.DELETE_TODO_ITEM
|
||||
) &&
|
||||
!this.todoListSupportsFeature(
|
||||
TodoListEntityFeature.UPDATE_TODO_ITEM
|
||||
)
|
||||
? html`<ha-icon-button
|
||||
.title=${this.hass!.localize(
|
||||
"ui.panel.lovelace.cards.todo-list.delete_item"
|
||||
)}
|
||||
class="deleteItemButton"
|
||||
.path=${mdiDelete}
|
||||
.itemId=${item.uid}
|
||||
@click=${this._deleteItem}
|
||||
>
|
||||
</ha-icon-button>`
|
||||
: nothing}
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
<mwc-list multi id="checked">
|
||||
${this._renderItems(checkedItems, unavailable)}
|
||||
</mwc-list>
|
||||
`
|
||||
: ""}
|
||||
</ha-card>
|
||||
@ -319,30 +318,66 @@ export class HuiTodoListCard extends LitElement implements LovelaceCard {
|
||||
${repeat(
|
||||
items,
|
||||
(item) => item.uid,
|
||||
(item) => html`
|
||||
<div class="editRow" item-id=${item.uid}>
|
||||
${this.todoListSupportsFeature(
|
||||
(item) => {
|
||||
const showDelete =
|
||||
this.todoListSupportsFeature(
|
||||
TodoListEntityFeature.DELETE_TODO_ITEM
|
||||
) &&
|
||||
!this.todoListSupportsFeature(
|
||||
TodoListEntityFeature.UPDATE_TODO_ITEM
|
||||
)
|
||||
? html`<ha-checkbox
|
||||
tabindex="0"
|
||||
.checked=${item.status === TodoItemStatus.Completed}
|
||||
.itemId=${item.uid}
|
||||
.disabled=${unavailable}
|
||||
@change=${this._completeItem}
|
||||
></ha-checkbox>`
|
||||
: nothing}
|
||||
<ha-textfield
|
||||
class="item"
|
||||
);
|
||||
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))
|
||||
: undefined;
|
||||
const today =
|
||||
due && !item.due!.includes("T") && isSameDay(new Date(), due);
|
||||
return html`
|
||||
<ha-check-list-item
|
||||
left
|
||||
.hasMeta=${showReorder || showDelete}
|
||||
class="editRow ${classMap({
|
||||
completed: item.status === TodoItemStatus.Completed,
|
||||
multiline: Boolean(item.description || item.due),
|
||||
})}"
|
||||
.selected=${item.status === TodoItemStatus.Completed}
|
||||
.disabled=${unavailable ||
|
||||
!this.todoListSupportsFeature(
|
||||
TodoListEntityFeature.UPDATE_TODO_ITEM
|
||||
)}
|
||||
.value=${item.summary}
|
||||
item-id=${item.uid}
|
||||
.itemId=${item.uid}
|
||||
@change=${this._saveEdit}
|
||||
></ha-textfield>
|
||||
${this._reordering
|
||||
@change=${this._completeItem}
|
||||
@click=${this._openItem}
|
||||
@request-selected=${this._requestSelected}
|
||||
@keydown=${this._handleKeydown}
|
||||
>
|
||||
<div class="column">
|
||||
<span class="summary">${item.summary}</span>
|
||||
${item.description
|
||||
? html`<ha-markdown-element
|
||||
class="description"
|
||||
.content=${item.description}
|
||||
></ha-markdown-element>`
|
||||
: nothing}
|
||||
${due
|
||||
? html`<div class="due ${due < new Date() ? "overdue" : ""}">
|
||||
<ha-svg-icon .path=${mdiClock}></ha-svg-icon>${today
|
||||
? this.hass!.localize(
|
||||
"ui.panel.lovelace.cards.todo-list.today"
|
||||
)
|
||||
: html`<ha-relative-time
|
||||
capitalize
|
||||
.hass=${this.hass}
|
||||
.datetime=${due}
|
||||
></ha-relative-time>`}
|
||||
</div>`
|
||||
: nothing}
|
||||
</div>
|
||||
${showReorder
|
||||
? html`
|
||||
<ha-svg-icon
|
||||
.title=${this.hass!.localize(
|
||||
@ -350,15 +385,11 @@ export class HuiTodoListCard extends LitElement implements LovelaceCard {
|
||||
)}
|
||||
class="reorderButton handle"
|
||||
.path=${mdiDrag}
|
||||
slot="meta"
|
||||
>
|
||||
</ha-svg-icon>
|
||||
`
|
||||
: this.todoListSupportsFeature(
|
||||
TodoListEntityFeature.DELETE_TODO_ITEM
|
||||
) &&
|
||||
!this.todoListSupportsFeature(
|
||||
TodoListEntityFeature.UPDATE_TODO_ITEM
|
||||
)
|
||||
: showDelete
|
||||
? html`<ha-icon-button
|
||||
.title=${this.hass!.localize(
|
||||
"ui.panel.lovelace.cards.todo-list.delete_item"
|
||||
@ -366,12 +397,14 @@ export class HuiTodoListCard extends LitElement implements LovelaceCard {
|
||||
class="deleteItemButton"
|
||||
.path=${mdiDelete}
|
||||
.itemId=${item.uid}
|
||||
slot="meta"
|
||||
@click=${this._deleteItem}
|
||||
>
|
||||
</ha-icon-button>`
|
||||
: nothing}
|
||||
</div>
|
||||
`
|
||||
</ha-check-list-item>
|
||||
`;
|
||||
}
|
||||
)}
|
||||
`;
|
||||
}
|
||||
@ -401,39 +434,52 @@ export class HuiTodoListCard extends LitElement implements LovelaceCard {
|
||||
return this._items?.find((item) => item.uid === itemId);
|
||||
}
|
||||
|
||||
private _requestSelected(ev: Event): void {
|
||||
ev.stopPropagation();
|
||||
}
|
||||
|
||||
private _handleKeydown(ev) {
|
||||
if (ev.key === " ") {
|
||||
this._completeItem(ev);
|
||||
return;
|
||||
}
|
||||
if (ev.key === "Enter") {
|
||||
this._openItem(ev);
|
||||
}
|
||||
}
|
||||
|
||||
private _openItem(ev): void {
|
||||
ev.stopPropagation();
|
||||
|
||||
if (
|
||||
ev
|
||||
.composedPath()
|
||||
.find((el) => ["input", "a", "button"].includes(el.localName))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const item = this._getItem(ev.currentTarget.itemId);
|
||||
showTodoItemEditDialog(this, {
|
||||
entity: this._config!.entity!,
|
||||
item,
|
||||
});
|
||||
}
|
||||
|
||||
private _completeItem(ev): void {
|
||||
const item = this._getItem(ev.target.itemId);
|
||||
const item = this._getItem(ev.currentTarget.itemId);
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
updateItem(this.hass!, this._entityId!, {
|
||||
...item,
|
||||
status: ev.target.checked
|
||||
status:
|
||||
item.status === TodoItemStatus.NeedsAction
|
||||
? TodoItemStatus.Completed
|
||||
: TodoItemStatus.NeedsAction,
|
||||
});
|
||||
}
|
||||
|
||||
private _saveEdit(ev): void {
|
||||
// If name is not empty, update the item otherwise remove it
|
||||
if (ev.target.value) {
|
||||
const item = this._getItem(ev.target.itemId);
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
updateItem(this.hass!, this._entityId!, {
|
||||
...item,
|
||||
summary: ev.target.value,
|
||||
});
|
||||
} else if (
|
||||
this.todoListSupportsFeature(TodoListEntityFeature.DELETE_TODO_ITEM)
|
||||
) {
|
||||
deleteItems(this.hass!, this._entityId!, [ev.target.itemId]);
|
||||
}
|
||||
|
||||
ev.target.blur();
|
||||
}
|
||||
|
||||
private async _clearCompletedItems(): Promise<void> {
|
||||
if (!this.hass) {
|
||||
return;
|
||||
@ -464,7 +510,9 @@ export class HuiTodoListCard extends LitElement implements LovelaceCard {
|
||||
private _addItem(ev): void {
|
||||
const newItem = this._newItem;
|
||||
if (newItem.value!.length > 0) {
|
||||
createItem(this.hass!, this._entityId!, newItem.value!);
|
||||
createItem(this.hass!, this._entityId!, {
|
||||
summary: newItem.value!,
|
||||
});
|
||||
}
|
||||
|
||||
newItem.value = "";
|
||||
@ -559,7 +607,6 @@ export class HuiTodoListCard extends LitElement implements LovelaceCard {
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
ha-card {
|
||||
padding: 16px;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
@ -568,60 +615,127 @@ export class HuiTodoListCard extends LitElement implements LovelaceCard {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.editRow,
|
||||
.addRow {
|
||||
padding: 16px;
|
||||
padding-bottom: 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.addRow ha-icon-button {
|
||||
position: absolute;
|
||||
right: 16px;
|
||||
}
|
||||
|
||||
.addRow,
|
||||
.checked {
|
||||
.header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.header {
|
||||
padding-left: 30px;
|
||||
padding-right: 16px;
|
||||
margin-top: 8px;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.header span {
|
||||
color: var(--primary-text-color);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.empty {
|
||||
padding: 16px 32px;
|
||||
}
|
||||
|
||||
.item {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.addButton {
|
||||
margin-left: -12px;
|
||||
margin-inline-start: -12px;
|
||||
direction: var(--direction);
|
||||
ha-check-list-item {
|
||||
--mdc-list-item-meta-size: 56px;
|
||||
min-height: 56px;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.deleteItemButton {
|
||||
margin-right: -12px;
|
||||
margin-inline-end: -12px;
|
||||
direction: var(--direction);
|
||||
ha-check-list-item.multiline {
|
||||
align-items: flex-start;
|
||||
--check-list-item-graphic-margin-top: 8px;
|
||||
}
|
||||
|
||||
.reorderButton {
|
||||
margin-right: -12px;
|
||||
margin-inline-end: -12px;
|
||||
direction: var(--direction);
|
||||
.row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.multiline .column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-top: 18px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.completed .summary {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.description,
|
||||
.due {
|
||||
font-size: 12px;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
.description {
|
||||
white-space: initial;
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.description p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.description a {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.due {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.due ha-svg-icon {
|
||||
margin-right: 4px;
|
||||
--mdc-icon-size: 14px;
|
||||
}
|
||||
|
||||
.due.overdue {
|
||||
color: var(--warning-color);
|
||||
}
|
||||
|
||||
.completed .due.overdue {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
.handle {
|
||||
cursor: move;
|
||||
height: 24px;
|
||||
padding: 16px 4px;
|
||||
}
|
||||
|
||||
ha-checkbox {
|
||||
margin-left: -12px;
|
||||
margin-inline-start: -12px;
|
||||
direction: var(--direction);
|
||||
.deleteItemButton {
|
||||
position: relative;
|
||||
left: 8px;
|
||||
}
|
||||
|
||||
ha-textfield {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.checked {
|
||||
margin: 12px 0;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.checked span {
|
||||
color: var(--primary-text-color);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.divider {
|
||||
height: 1px;
|
||||
background-color: var(--divider-color);
|
||||
@ -636,6 +750,10 @@ export class HuiTodoListCard extends LitElement implements LovelaceCard {
|
||||
display: block;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.warning {
|
||||
color: var(--error-color);
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
442
src/panels/todo/dialog-todo-item-editor.ts
Normal file
442
src/panels/todo/dialog-todo-item-editor.ts
Normal file
@ -0,0 +1,442 @@
|
||||
import "@material/mwc-button";
|
||||
import { mdiClose } from "@mdi/js";
|
||||
import { formatInTimeZone, toDate } from "date-fns-tz";
|
||||
import { CSSResultGroup, LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import "../../components/ha-date-input";
|
||||
import "../../components/ha-textarea";
|
||||
import "../../components/ha-time-input";
|
||||
import {
|
||||
TodoItemStatus,
|
||||
TodoListEntityFeature,
|
||||
createItem,
|
||||
deleteItems,
|
||||
updateItem,
|
||||
} from "../../data/todo";
|
||||
import { TimeZone } from "../../data/translation";
|
||||
import { showConfirmationDialog } from "../../dialogs/generic/show-dialog-box";
|
||||
import { haStyleDialog } from "../../resources/styles";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import { TodoItemEditDialogParams } from "./show-dialog-todo-item-editor";
|
||||
import { supportsFeature } from "../../common/entity/supports-feature";
|
||||
|
||||
@customElement("dialog-todo-item-editor")
|
||||
class DialogTodoItemEditor extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
@state() private _params?: TodoItemEditDialogParams;
|
||||
|
||||
@state() private _summary = "";
|
||||
|
||||
@state() private _description? = "";
|
||||
|
||||
@state() private _due?: Date;
|
||||
|
||||
@state() private _checked = false;
|
||||
|
||||
@state() private _hasTime = false;
|
||||
|
||||
@state() private _submitting = false;
|
||||
|
||||
// Dates are manipulated and displayed in the browser timezone
|
||||
// which may be different from the Home Assistant timezone. When
|
||||
// events are persisted, they are relative to the Home Assistant
|
||||
// timezone, but floating without a timezone.
|
||||
private _timeZone?: string;
|
||||
|
||||
public showDialog(params: TodoItemEditDialogParams): void {
|
||||
this._error = undefined;
|
||||
this._params = params;
|
||||
this._timeZone =
|
||||
this.hass.locale.time_zone === TimeZone.local
|
||||
? Intl.DateTimeFormat().resolvedOptions().timeZone
|
||||
: this.hass.config.time_zone;
|
||||
if (params.item) {
|
||||
const entry = params.item;
|
||||
this._checked = entry.status === TodoItemStatus.Completed;
|
||||
this._summary = entry.summary;
|
||||
this._description = entry.description || "";
|
||||
this._due = entry.due ? new Date(entry.due) : undefined;
|
||||
this._hasTime = entry.due?.includes("T") || false;
|
||||
} else {
|
||||
this._hasTime = false;
|
||||
this._checked = false;
|
||||
this._due = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
public closeDialog(): void {
|
||||
if (!this._params) {
|
||||
return;
|
||||
}
|
||||
this._error = undefined;
|
||||
this._params = undefined;
|
||||
this._due = undefined;
|
||||
this._summary = "";
|
||||
this._description = "";
|
||||
this._hasTime = false;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._params) {
|
||||
return nothing;
|
||||
}
|
||||
const isCreate = this._params.item === undefined;
|
||||
|
||||
const { dueDate, dueTime } = this._getLocaleStrings(this._due);
|
||||
|
||||
const canUpdate = this._todoListSupportsFeature(
|
||||
TodoListEntityFeature.UPDATE_TODO_ITEM
|
||||
);
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
@closed=${this.closeDialog}
|
||||
scrimClickAction
|
||||
escapeKeyAction
|
||||
.heading=${html`
|
||||
<div class="header_title">
|
||||
${isCreate
|
||||
? this.hass.localize("ui.components.todo.item.add")
|
||||
: this._summary}
|
||||
</div>
|
||||
<ha-icon-button
|
||||
.label=${this.hass.localize("ui.dialogs.generic.close")}
|
||||
.path=${mdiClose}
|
||||
dialogAction="close"
|
||||
class="header_button"
|
||||
></ha-icon-button>
|
||||
`}
|
||||
>
|
||||
<div class="content">
|
||||
${this._error
|
||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||
: ""}
|
||||
|
||||
<div class="flex">
|
||||
<ha-checkbox
|
||||
.checked=${this._checked}
|
||||
@change=${this._checkedCanged}
|
||||
.disabled=${isCreate || !canUpdate}
|
||||
></ha-checkbox>
|
||||
<ha-textfield
|
||||
class="summary"
|
||||
name="summary"
|
||||
.label=${this.hass.localize("ui.components.todo.item.summary")}
|
||||
.value=${this._summary}
|
||||
required
|
||||
@input=${this._handleSummaryChanged}
|
||||
.validationMessage=${this.hass.localize(
|
||||
"ui.common.error_required"
|
||||
)}
|
||||
dialogInitialFocus
|
||||
.disabled=${!canUpdate}
|
||||
></ha-textfield>
|
||||
</div>
|
||||
${this._todoListSupportsFeature(
|
||||
TodoListEntityFeature.SET_DESCRIPTION_ON_ITEM
|
||||
)
|
||||
? html`<ha-textarea
|
||||
class="description"
|
||||
name="description"
|
||||
.label=${this.hass.localize(
|
||||
"ui.components.todo.item.description"
|
||||
)}
|
||||
.value=${this._description}
|
||||
@input=${this._handleDescriptionChanged}
|
||||
autogrow
|
||||
.disabled=${!canUpdate}
|
||||
></ha-textarea>`
|
||||
: nothing}
|
||||
${this._todoListSupportsFeature(
|
||||
TodoListEntityFeature.SET_DUE_DATE_ON_ITEM
|
||||
) ||
|
||||
this._todoListSupportsFeature(
|
||||
TodoListEntityFeature.SET_DUE_DATETIME_ON_ITEM
|
||||
)
|
||||
? html`<div>
|
||||
<span class="label"
|
||||
>${this.hass.localize("ui.components.todo.item.due")}:</span
|
||||
>
|
||||
<div class="flex">
|
||||
<ha-date-input
|
||||
.value=${dueDate}
|
||||
.locale=${this.hass.locale}
|
||||
.disabled=${!canUpdate}
|
||||
@value-changed=${this._endDateChanged}
|
||||
></ha-date-input>
|
||||
${this._todoListSupportsFeature(
|
||||
TodoListEntityFeature.SET_DUE_DATETIME_ON_ITEM
|
||||
)
|
||||
? html`<ha-time-input
|
||||
.value=${dueTime}
|
||||
.locale=${this.hass.locale}
|
||||
.disabled=${!canUpdate}
|
||||
@value-changed=${this._endTimeChanged}
|
||||
></ha-time-input>`
|
||||
: nothing}
|
||||
</div>
|
||||
</div>`
|
||||
: nothing}
|
||||
</div>
|
||||
${isCreate
|
||||
? html`
|
||||
<mwc-button
|
||||
slot="primaryAction"
|
||||
@click=${this._createItem}
|
||||
.disabled=${this._submitting}
|
||||
>
|
||||
${this.hass.localize("ui.components.todo.item.add")}
|
||||
</mwc-button>
|
||||
`
|
||||
: html`
|
||||
<mwc-button
|
||||
slot="primaryAction"
|
||||
@click=${this._saveItem}
|
||||
.disabled=${!canUpdate || this._submitting}
|
||||
>
|
||||
${this.hass.localize("ui.components.todo.item.save")}
|
||||
</mwc-button>
|
||||
${this._todoListSupportsFeature(
|
||||
TodoListEntityFeature.DELETE_TODO_ITEM
|
||||
)
|
||||
? html`
|
||||
<mwc-button
|
||||
slot="secondaryAction"
|
||||
class="warning"
|
||||
@click=${this._deleteItem}
|
||||
.disabled=${this._submitting}
|
||||
>
|
||||
${this.hass.localize("ui.components.todo.item.delete")}
|
||||
</mwc-button>
|
||||
`
|
||||
: ""}
|
||||
`}
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private _todoListSupportsFeature(feature: number): boolean {
|
||||
if (!this._params?.entity) {
|
||||
return false;
|
||||
}
|
||||
const entityStateObj = this.hass!.states[this._params?.entity];
|
||||
return entityStateObj && supportsFeature(entityStateObj, feature);
|
||||
}
|
||||
|
||||
private _getLocaleStrings = memoizeOne((due?: Date) => ({
|
||||
dueDate: due ? this._formatDate(due) : undefined,
|
||||
dueTime: due ? this._formatTime(due) : undefined,
|
||||
}));
|
||||
|
||||
// Formats a date in specified timezone, or defaulting to browser display timezone
|
||||
private _formatDate(date: Date, timeZone: string = this._timeZone!): string {
|
||||
return formatInTimeZone(date, timeZone, "yyyy-MM-dd");
|
||||
}
|
||||
|
||||
// Formats a time in specified timezone, or defaulting to browser display timezone
|
||||
private _formatTime(
|
||||
date: Date,
|
||||
timeZone: string = this._timeZone!
|
||||
): string | undefined {
|
||||
return this._hasTime
|
||||
? formatInTimeZone(date, timeZone, "HH:mm:ss")
|
||||
: undefined; // 24 hr
|
||||
}
|
||||
|
||||
// Parse a date in the browser timezone
|
||||
private _parseDate(dateStr: string): Date {
|
||||
return toDate(dateStr, { timeZone: this._timeZone! });
|
||||
}
|
||||
|
||||
private _checkedCanged(ev) {
|
||||
this._checked = ev.target.checked;
|
||||
}
|
||||
|
||||
private _handleSummaryChanged(ev) {
|
||||
this._summary = ev.target.value;
|
||||
}
|
||||
|
||||
private _handleDescriptionChanged(ev) {
|
||||
this._description = ev.target.value;
|
||||
}
|
||||
|
||||
private _endDateChanged(ev: CustomEvent) {
|
||||
const time = this._due ? this._formatTime(this._due) : undefined;
|
||||
this._due = this._parseDate(`${ev.detail.value}${time ? `T${time}` : ""}`);
|
||||
}
|
||||
|
||||
private _endTimeChanged(ev: CustomEvent) {
|
||||
this._hasTime = true;
|
||||
this._due = this._parseDate(
|
||||
`${this._formatDate(this._due || new Date())}T${ev.detail.value}`
|
||||
);
|
||||
}
|
||||
|
||||
private async _createItem() {
|
||||
if (!this._summary) {
|
||||
this._error = this.hass.localize(
|
||||
"ui.components.todo.item.not_all_required_fields"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
this._submitting = true;
|
||||
try {
|
||||
await createItem(this.hass!, this._params!.entity, {
|
||||
summary: this._summary,
|
||||
description: this._description,
|
||||
due: this._due
|
||||
? this._hasTime
|
||||
? this._due.toISOString()
|
||||
: this._formatDate(this._due)
|
||||
: undefined,
|
||||
});
|
||||
} catch (err: any) {
|
||||
this._error = err ? err.message : "Unknown error";
|
||||
return;
|
||||
} finally {
|
||||
this._submitting = false;
|
||||
}
|
||||
this.closeDialog();
|
||||
}
|
||||
|
||||
private async _saveItem() {
|
||||
if (!this._summary) {
|
||||
this._error = this.hass.localize(
|
||||
"ui.components.todo.item.not_all_required_fields"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
this._submitting = true;
|
||||
const entry = this._params!.item!;
|
||||
|
||||
try {
|
||||
await updateItem(this.hass!, this._params!.entity, {
|
||||
...entry,
|
||||
summary: this._summary,
|
||||
description:
|
||||
this._description ||
|
||||
(this._todoListSupportsFeature(
|
||||
TodoListEntityFeature.SET_DESCRIPTION_ON_ITEM
|
||||
)
|
||||
? // backend should accept null to clear the field, but it doesn't now
|
||||
" "
|
||||
: undefined),
|
||||
due: this._due
|
||||
? this._hasTime
|
||||
? this._due.toISOString()
|
||||
: this._formatDate(this._due)
|
||||
: undefined,
|
||||
status: this._checked
|
||||
? TodoItemStatus.Completed
|
||||
: TodoItemStatus.NeedsAction,
|
||||
});
|
||||
} catch (err: any) {
|
||||
this._error = err ? err.message : "Unknown error";
|
||||
return;
|
||||
} finally {
|
||||
this._submitting = false;
|
||||
}
|
||||
this.closeDialog();
|
||||
}
|
||||
|
||||
private async _deleteItem() {
|
||||
this._submitting = true;
|
||||
const entry = this._params!.item!;
|
||||
const confirm = await showConfirmationDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.components.todo.item.confirm_delete.delete"
|
||||
),
|
||||
text: this.hass.localize("ui.components.todo.item.confirm_delete.prompt"),
|
||||
});
|
||||
if (!confirm) {
|
||||
// Cancel
|
||||
this._submitting = false;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await deleteItems(this.hass!, this._params!.entity, [entry.uid]);
|
||||
} catch (err: any) {
|
||||
this._error = err ? err.message : "Unknown error";
|
||||
return;
|
||||
} finally {
|
||||
this._submitting = false;
|
||||
}
|
||||
this.closeDialog();
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyleDialog,
|
||||
css`
|
||||
ha-dialog {
|
||||
--mdc-dialog-min-width: min(600px, 95vw);
|
||||
--mdc-dialog-max-width: min(600px, 95vw);
|
||||
}
|
||||
ha-alert {
|
||||
display: block;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
ha-textfield,
|
||||
ha-textarea {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
ha-checkbox {
|
||||
margin-top: 4px;
|
||||
}
|
||||
ha-textarea {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
ha-date-input {
|
||||
flex-grow: 1;
|
||||
}
|
||||
ha-time-input {
|
||||
margin-left: 16px;
|
||||
}
|
||||
.flex {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.label {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--input-label-ink-color);
|
||||
}
|
||||
.date-range-details-content {
|
||||
display: inline-block;
|
||||
}
|
||||
ha-svg-icon {
|
||||
width: 40px;
|
||||
margin-right: 8px;
|
||||
margin-inline-end: 16px;
|
||||
margin-inline-start: initial;
|
||||
direction: var(--direction);
|
||||
vertical-align: top;
|
||||
}
|
||||
.key {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
}
|
||||
.value {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"dialog-todo-item-editor": DialogTodoItemEditor;
|
||||
}
|
||||
}
|
@ -23,7 +23,14 @@ import { isComponentLoaded } from "../../common/config/is_component_loaded";
|
||||
import { storage } from "../../common/decorators/storage";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { computeStateName } from "../../common/entity/compute_state_name";
|
||||
import { navigate } from "../../common/navigate";
|
||||
import { constructUrlCurrentPath } from "../../common/url/construct-url";
|
||||
import {
|
||||
createSearchParam,
|
||||
extractSearchParam,
|
||||
} from "../../common/url/search-params";
|
||||
import "../../components/ha-button";
|
||||
import "../../components/ha-fab";
|
||||
import "../../components/ha-icon-button";
|
||||
import "../../components/ha-list-item";
|
||||
import "../../components/ha-menu-button";
|
||||
@ -33,7 +40,7 @@ import "../../components/ha-two-pane-top-app-bar-fixed";
|
||||
import { deleteConfigEntry } from "../../data/config_entries";
|
||||
import { getExtendedEntityRegistryEntry } from "../../data/entity_registry";
|
||||
import { fetchIntegrationManifest } from "../../data/integration";
|
||||
import { getTodoLists } from "../../data/todo";
|
||||
import { TodoListEntityFeature, getTodoLists } from "../../data/todo";
|
||||
import { showConfigFlowDialog } from "../../dialogs/config-flow/show-dialog-config-flow";
|
||||
import {
|
||||
showAlertDialog,
|
||||
@ -45,12 +52,8 @@ import { HomeAssistant } from "../../types";
|
||||
import { HuiErrorCard } from "../lovelace/cards/hui-error-card";
|
||||
import { createCardElement } from "../lovelace/create-element/create-card-element";
|
||||
import { LovelaceCard } from "../lovelace/types";
|
||||
import { navigate } from "../../common/navigate";
|
||||
import {
|
||||
createSearchParam,
|
||||
extractSearchParam,
|
||||
} from "../../common/url/search-params";
|
||||
import { constructUrlCurrentPath } from "../../common/url/construct-url";
|
||||
import { showTodoItemEditDialog } from "./show-dialog-todo-item-editor";
|
||||
import { supportsFeature } from "../../common/entity/supports-feature";
|
||||
|
||||
@customElement("ha-panel-todo")
|
||||
class PanelTodo extends LitElement {
|
||||
@ -152,6 +155,9 @@ class PanelTodo extends LitElement {
|
||||
const entityRegistryEntry = this._entityId
|
||||
? this.hass.entities[this._entityId]
|
||||
: undefined;
|
||||
const entityState = this._entityId
|
||||
? this.hass.states[this._entityId]
|
||||
: undefined;
|
||||
const showPane = this._showPaneController.value ?? !this.narrow;
|
||||
const listItems = getTodoLists(this.hass).map(
|
||||
(list) =>
|
||||
@ -187,8 +193,8 @@ class PanelTodo extends LitElement {
|
||||
<ha-button slot="trigger">
|
||||
<div>
|
||||
${this._entityId
|
||||
? this._entityId in this.hass.states
|
||||
? computeStateName(this.hass.states[this._entityId])
|
||||
? entityState
|
||||
? computeStateName(entityState)
|
||||
: this._entityId
|
||||
: ""}
|
||||
</div>
|
||||
@ -255,6 +261,16 @@ class PanelTodo extends LitElement {
|
||||
<div id="columns">
|
||||
<div class="column">${this._card}</div>
|
||||
</div>
|
||||
${entityState &&
|
||||
supportsFeature(entityState, TodoListEntityFeature.CREATE_TODO_ITEM)
|
||||
? html`<ha-fab
|
||||
.label=${this.hass.localize("ui.panel.todo.add_item")}
|
||||
extended
|
||||
@click=${this._addItem}
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
|
||||
</ha-fab>`
|
||||
: nothing}
|
||||
</ha-two-pane-top-app-bar-fixed>
|
||||
`;
|
||||
}
|
||||
@ -329,6 +345,10 @@ class PanelTodo extends LitElement {
|
||||
showVoiceCommandDialog(this, this.hass, { pipeline_id: "last_used" });
|
||||
}
|
||||
|
||||
private _addItem() {
|
||||
showTodoItemEditDialog(this, { entity: this._entityId! });
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
@ -390,6 +410,11 @@ class PanelTodo extends LitElement {
|
||||
white-space: nowrap;
|
||||
display: block;
|
||||
}
|
||||
ha-fab {
|
||||
position: absolute;
|
||||
right: 16px;
|
||||
bottom: 16px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
20
src/panels/todo/show-dialog-todo-item-editor.ts
Normal file
20
src/panels/todo/show-dialog-todo-item-editor.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { TodoItem } from "../../data/todo";
|
||||
|
||||
export interface TodoItemEditDialogParams {
|
||||
entity: string;
|
||||
item?: TodoItem;
|
||||
}
|
||||
|
||||
export const loadTodoItemEditDialog = () => import("./dialog-todo-item-editor");
|
||||
|
||||
export const showTodoItemEditDialog = (
|
||||
element: HTMLElement,
|
||||
detailParams: TodoItemEditDialogParams
|
||||
): void => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "dialog-todo-item-editor",
|
||||
dialogImport: loadTodoItemEditDialog,
|
||||
dialogParams: detailParams,
|
||||
});
|
||||
};
|
@ -756,6 +756,22 @@
|
||||
"grid": "Grid",
|
||||
"list": "List"
|
||||
},
|
||||
"todo": {
|
||||
"item": {
|
||||
"summary": "Task name",
|
||||
"description": "Description",
|
||||
"add": "Add item",
|
||||
"delete": "Delete item",
|
||||
"edit": "Edit item",
|
||||
"save": "Save item",
|
||||
"due": "Due date",
|
||||
"not_all_required_fields": "Not all required fields are filled in",
|
||||
"confirm_delete": {
|
||||
"delete": "Delete item",
|
||||
"prompt": "Do you want to delete this item?"
|
||||
}
|
||||
}
|
||||
},
|
||||
"calendar": {
|
||||
"label": "Calendar",
|
||||
"my_calendars": "My calendars",
|
||||
@ -4690,14 +4706,17 @@
|
||||
"never_triggered": "Never triggered"
|
||||
},
|
||||
"todo-list": {
|
||||
"checked_items": "Checked items",
|
||||
"clear_items": "Clear checked items",
|
||||
"unchecked_items": "Active",
|
||||
"no_unchecked_items": "You have no to-do items!",
|
||||
"checked_items": "Completed",
|
||||
"clear_items": "Remove completed items",
|
||||
"add_item": "Add item",
|
||||
"today": "Today",
|
||||
"reorder_items": "Reorder items",
|
||||
"drag_and_drop": "Drag and drop",
|
||||
"delete_item": "Delete item",
|
||||
"delete_confirm_title": "Clear checked items?",
|
||||
"delete_confirm_text": "{number} {number, plural,\n one {item}\n other {items}\n} will be permanently deleted from the to-do list."
|
||||
"delete_confirm_title": "Remove completed items?",
|
||||
"delete_confirm_text": "{number} {number, plural,\n one {item}\n other {items}\n} will be permanently removed from the to-do list."
|
||||
},
|
||||
"picture-elements": {
|
||||
"hold": "Hold:",
|
||||
@ -5758,6 +5777,7 @@
|
||||
"assist": "[%key:ui::panel::lovelace::menu::assist%]",
|
||||
"create_list": "Create list",
|
||||
"delete_list": "Delete list",
|
||||
"add_item": "Add item",
|
||||
"information": "Information",
|
||||
"delete_confirm_title": "Remove {name}?",
|
||||
"delete_confirm_text": "Are you sure you want to remove this list and all of its items?",
|
||||
|
Loading…
x
Reference in New Issue
Block a user