mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-24 01:36:49 +00:00
Add support for todo component (#18289)
This commit is contained in:
parent
53b8d1bb0a
commit
2b9540fe03
@ -3,6 +3,15 @@ import { DemoConfig } from "../types";
|
|||||||
|
|
||||||
export const demoEntitiesArsaboo: DemoConfig["entities"] = (localize) =>
|
export const demoEntitiesArsaboo: DemoConfig["entities"] = (localize) =>
|
||||||
convertEntities({
|
convertEntities({
|
||||||
|
"todo.shopping_list": {
|
||||||
|
entity_id: "todo.shopping_list",
|
||||||
|
state: "2",
|
||||||
|
attributes: {
|
||||||
|
supported_features: 15,
|
||||||
|
friendly_name: "Shopping List",
|
||||||
|
icon: "mdi:cart",
|
||||||
|
},
|
||||||
|
},
|
||||||
"zone.home": {
|
"zone.home": {
|
||||||
entity_id: "zone.home",
|
entity_id: "zone.home",
|
||||||
state: "zoning",
|
state: "zoning",
|
||||||
|
@ -3,6 +3,15 @@ import { DemoConfig } from "../types";
|
|||||||
|
|
||||||
export const demoEntitiesJimpower: DemoConfig["entities"] = () =>
|
export const demoEntitiesJimpower: DemoConfig["entities"] = () =>
|
||||||
convertEntities({
|
convertEntities({
|
||||||
|
"todo.shopping_list": {
|
||||||
|
entity_id: "todo.shopping_list",
|
||||||
|
state: "2",
|
||||||
|
attributes: {
|
||||||
|
supported_features: 15,
|
||||||
|
friendly_name: "Shopping List",
|
||||||
|
icon: "mdi:cart",
|
||||||
|
},
|
||||||
|
},
|
||||||
"zone.powertec": {
|
"zone.powertec": {
|
||||||
entity_id: "zone.powertec",
|
entity_id: "zone.powertec",
|
||||||
state: "zoning",
|
state: "zoning",
|
||||||
|
@ -3,6 +3,15 @@ import { DemoConfig } from "../types";
|
|||||||
|
|
||||||
export const demoEntitiesKernehed: DemoConfig["entities"] = () =>
|
export const demoEntitiesKernehed: DemoConfig["entities"] = () =>
|
||||||
convertEntities({
|
convertEntities({
|
||||||
|
"todo.shopping_list": {
|
||||||
|
entity_id: "todo.shopping_list",
|
||||||
|
state: "2",
|
||||||
|
attributes: {
|
||||||
|
supported_features: 15,
|
||||||
|
friendly_name: "Shopping List",
|
||||||
|
icon: "mdi:cart",
|
||||||
|
},
|
||||||
|
},
|
||||||
"zone.anna": {
|
"zone.anna": {
|
||||||
entity_id: "zone.anna",
|
entity_id: "zone.anna",
|
||||||
state: "zoning",
|
state: "zoning",
|
||||||
|
@ -3,6 +3,15 @@ import { DemoConfig } from "../types";
|
|||||||
|
|
||||||
export const demoEntitiesTeachingbirds: DemoConfig["entities"] = () =>
|
export const demoEntitiesTeachingbirds: DemoConfig["entities"] = () =>
|
||||||
convertEntities({
|
convertEntities({
|
||||||
|
"todo.shopping_list": {
|
||||||
|
entity_id: "todo.shopping_list",
|
||||||
|
state: "2",
|
||||||
|
attributes: {
|
||||||
|
supported_features: 15,
|
||||||
|
friendly_name: "Shopping List",
|
||||||
|
icon: "mdi:cart",
|
||||||
|
},
|
||||||
|
},
|
||||||
"sensor.pollen_grabo": {
|
"sensor.pollen_grabo": {
|
||||||
entity_id: "sensor.pollen_grabo",
|
entity_id: "sensor.pollen_grabo",
|
||||||
state: "",
|
state: "",
|
||||||
|
@ -22,7 +22,7 @@ import { mockLovelace } from "./stubs/lovelace";
|
|||||||
import { mockMediaPlayer } from "./stubs/media_player";
|
import { mockMediaPlayer } from "./stubs/media_player";
|
||||||
import { mockPersistentNotification } from "./stubs/persistent_notification";
|
import { mockPersistentNotification } from "./stubs/persistent_notification";
|
||||||
import { mockRecorder } from "./stubs/recorder";
|
import { mockRecorder } from "./stubs/recorder";
|
||||||
import { mockShoppingList } from "./stubs/shopping_list";
|
import { mockTodo } from "./stubs/todo";
|
||||||
import { mockSystemLog } from "./stubs/system_log";
|
import { mockSystemLog } from "./stubs/system_log";
|
||||||
import { mockTemplate } from "./stubs/template";
|
import { mockTemplate } from "./stubs/template";
|
||||||
import { mockTranslations } from "./stubs/translations";
|
import { mockTranslations } from "./stubs/translations";
|
||||||
@ -49,7 +49,7 @@ export class HaDemo extends HomeAssistantAppEl {
|
|||||||
mockTranslations(hass);
|
mockTranslations(hass);
|
||||||
mockHistory(hass);
|
mockHistory(hass);
|
||||||
mockRecorder(hass);
|
mockRecorder(hass);
|
||||||
mockShoppingList(hass);
|
mockTodo(hass);
|
||||||
mockSystemLog(hass);
|
mockSystemLog(hass);
|
||||||
mockTemplate(hass);
|
mockTemplate(hass);
|
||||||
mockEvents(hass);
|
mockEvents(hass);
|
||||||
|
@ -1,44 +0,0 @@
|
|||||||
import { ShoppingListItem } from "../../../src/data/shopping-list";
|
|
||||||
import { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
|
||||||
|
|
||||||
let items: ShoppingListItem[] = [
|
|
||||||
{
|
|
||||||
id: 12,
|
|
||||||
name: "Milk",
|
|
||||||
complete: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 13,
|
|
||||||
name: "Eggs",
|
|
||||||
complete: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 14,
|
|
||||||
name: "Oranges",
|
|
||||||
complete: true,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export const mockShoppingList = (hass: MockHomeAssistant) => {
|
|
||||||
hass.mockWS("shopping_list/items", () => items);
|
|
||||||
hass.mockWS("shopping_list/items/add", (msg) => {
|
|
||||||
const item: ShoppingListItem = {
|
|
||||||
id: new Date().getTime(),
|
|
||||||
complete: false,
|
|
||||||
name: msg.name,
|
|
||||||
};
|
|
||||||
items.push(item);
|
|
||||||
hass.mockEvent("shopping_list_updated");
|
|
||||||
return item;
|
|
||||||
});
|
|
||||||
hass.mockWS("shopping_list/items/update", ({ type, item_id, ...updates }) => {
|
|
||||||
items = items.map((item) =>
|
|
||||||
item.id === item_id ? { ...item, ...updates } : item
|
|
||||||
);
|
|
||||||
hass.mockEvent("shopping_list_updated");
|
|
||||||
});
|
|
||||||
hass.mockWS("shopping_list/items/clear", () => {
|
|
||||||
items = items.filter((item) => !item.complete);
|
|
||||||
hass.mockEvent("shopping_list_updated");
|
|
||||||
});
|
|
||||||
};
|
|
24
demo/src/stubs/todo.ts
Normal file
24
demo/src/stubs/todo.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { TodoItem, TodoItemStatus } from "../../../src/data/todo";
|
||||||
|
import { 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[],
|
||||||
|
}));
|
||||||
|
};
|
@ -2,12 +2,25 @@ import { html, LitElement, PropertyValues, TemplateResult } from "lit";
|
|||||||
import { customElement, query } from "lit/decorators";
|
import { customElement, query } from "lit/decorators";
|
||||||
import { provideHass } from "../../../../src/fake_data/provide_hass";
|
import { provideHass } from "../../../../src/fake_data/provide_hass";
|
||||||
import "../../components/demo-cards";
|
import "../../components/demo-cards";
|
||||||
|
import { getEntity } from "../../../../src/fake_data/entity";
|
||||||
|
import { mockTodo } from "../../../../demo/src/stubs/todo";
|
||||||
|
|
||||||
|
const ENTITIES = [
|
||||||
|
getEntity("todo", "shopping_list", "2", {
|
||||||
|
friendly_name: "Shopping List",
|
||||||
|
supported_features: 15,
|
||||||
|
}),
|
||||||
|
getEntity("todo", "read_only", "2", {
|
||||||
|
friendly_name: "Read only",
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
const CONFIGS = [
|
const CONFIGS = [
|
||||||
{
|
{
|
||||||
heading: "List example",
|
heading: "List example",
|
||||||
config: `
|
config: `
|
||||||
- type: shopping-list
|
- type: shopping-list
|
||||||
|
entity: todo.shopping_list
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -15,6 +28,7 @@ const CONFIGS = [
|
|||||||
config: `
|
config: `
|
||||||
- type: shopping-list
|
- type: shopping-list
|
||||||
title: Shopping List
|
title: Shopping List
|
||||||
|
entity: todo.read_only
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@ -32,13 +46,9 @@ class DemoShoppingListEntity extends LitElement {
|
|||||||
const hass = provideHass(this._demoRoot);
|
const hass = provideHass(this._demoRoot);
|
||||||
hass.updateTranslations(null, "en");
|
hass.updateTranslations(null, "en");
|
||||||
hass.updateTranslations("lovelace", "en");
|
hass.updateTranslations("lovelace", "en");
|
||||||
|
hass.addEntities(ENTITIES);
|
||||||
|
|
||||||
hass.mockAPI("shopping_list", () => [
|
mockTodo(hass);
|
||||||
{ name: "list", id: 1, complete: false },
|
|
||||||
{ name: "all", id: 2, complete: false },
|
|
||||||
{ name: "the", id: 3, complete: false },
|
|
||||||
{ name: "things", id: 4, complete: true },
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -16,6 +16,7 @@ import {
|
|||||||
mdiCarCoolantLevel,
|
mdiCarCoolantLevel,
|
||||||
mdiCash,
|
mdiCash,
|
||||||
mdiChatSleep,
|
mdiChatSleep,
|
||||||
|
mdiClipboardCheck,
|
||||||
mdiClock,
|
mdiClock,
|
||||||
mdiCloudUpload,
|
mdiCloudUpload,
|
||||||
mdiCog,
|
mdiCog,
|
||||||
@ -120,6 +121,7 @@ export const FIXED_DOMAIN_ICONS = {
|
|||||||
siren: mdiBullhorn,
|
siren: mdiBullhorn,
|
||||||
stt: mdiMicrophoneMessage,
|
stt: mdiMicrophoneMessage,
|
||||||
text: mdiFormTextbox,
|
text: mdiFormTextbox,
|
||||||
|
todo: mdiClipboardCheck,
|
||||||
time: mdiClock,
|
time: mdiClock,
|
||||||
timer: mdiTimerOutline,
|
timer: mdiTimerOutline,
|
||||||
tts: mdiSpeakerMessage,
|
tts: mdiSpeakerMessage,
|
||||||
|
@ -2,9 +2,9 @@ import "@material/mwc-button/mwc-button";
|
|||||||
import {
|
import {
|
||||||
mdiBell,
|
mdiBell,
|
||||||
mdiCalendar,
|
mdiCalendar,
|
||||||
mdiCart,
|
|
||||||
mdiCellphoneCog,
|
mdiCellphoneCog,
|
||||||
mdiChartBox,
|
mdiChartBox,
|
||||||
|
mdiClipboardList,
|
||||||
mdiClose,
|
mdiClose,
|
||||||
mdiCog,
|
mdiCog,
|
||||||
mdiFormatListBulletedType,
|
mdiFormatListBulletedType,
|
||||||
@ -81,7 +81,7 @@ const PANEL_ICONS = {
|
|||||||
lovelace: mdiViewDashboard,
|
lovelace: mdiViewDashboard,
|
||||||
map: mdiTooltipAccount,
|
map: mdiTooltipAccount,
|
||||||
"media-browser": mdiPlayBoxMultiple,
|
"media-browser": mdiPlayBoxMultiple,
|
||||||
"shopping-list": mdiCart,
|
todo: mdiClipboardList,
|
||||||
};
|
};
|
||||||
|
|
||||||
const panelSorter = (
|
const panelSorter = (
|
||||||
|
@ -1,58 +0,0 @@
|
|||||||
import { HomeAssistant } from "../types";
|
|
||||||
|
|
||||||
export interface ShoppingListItem {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
complete: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const fetchItems = (hass: HomeAssistant): Promise<ShoppingListItem[]> =>
|
|
||||||
hass.callWS({
|
|
||||||
type: "shopping_list/items",
|
|
||||||
});
|
|
||||||
|
|
||||||
export const updateItem = (
|
|
||||||
hass: HomeAssistant,
|
|
||||||
itemId: number,
|
|
||||||
item: {
|
|
||||||
name?: string;
|
|
||||||
complete?: boolean;
|
|
||||||
}
|
|
||||||
): Promise<ShoppingListItem> =>
|
|
||||||
hass.callWS({
|
|
||||||
type: "shopping_list/items/update",
|
|
||||||
item_id: itemId,
|
|
||||||
...item,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const clearItems = (hass: HomeAssistant): Promise<void> =>
|
|
||||||
hass.callWS({
|
|
||||||
type: "shopping_list/items/clear",
|
|
||||||
});
|
|
||||||
|
|
||||||
export const addItem = (
|
|
||||||
hass: HomeAssistant,
|
|
||||||
name: string
|
|
||||||
): Promise<ShoppingListItem> =>
|
|
||||||
hass.callWS({
|
|
||||||
type: "shopping_list/items/add",
|
|
||||||
name,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const removeItem = (
|
|
||||||
hass: HomeAssistant,
|
|
||||||
item_id: string
|
|
||||||
): Promise<ShoppingListItem> =>
|
|
||||||
hass.callWS({
|
|
||||||
type: "shopping_list/items/remove",
|
|
||||||
item_id,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const reorderItems = (
|
|
||||||
hass: HomeAssistant,
|
|
||||||
itemIds: string[]
|
|
||||||
): Promise<ShoppingListItem> =>
|
|
||||||
hass.callWS({
|
|
||||||
type: "shopping_list/items/reorder",
|
|
||||||
item_ids: itemIds,
|
|
||||||
});
|
|
104
src/data/todo.ts
Normal file
104
src/data/todo.ts
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
import { HomeAssistant, ServiceCallResponse } from "../types";
|
||||||
|
import { computeDomain } from "../common/entity/compute_domain";
|
||||||
|
import { computeStateName } from "../common/entity/compute_state_name";
|
||||||
|
import { isUnavailableState } from "./entity";
|
||||||
|
|
||||||
|
export interface TodoList {
|
||||||
|
entity_id: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const enum TodoItemStatus {
|
||||||
|
NeedsAction = "needs-action",
|
||||||
|
Completed = "completed",
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TodoItem {
|
||||||
|
uid?: string;
|
||||||
|
summary: string;
|
||||||
|
status: TodoItemStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const enum TodoListEntityFeature {
|
||||||
|
CREATE_TODO_ITEM = 1,
|
||||||
|
DELETE_TODO_ITEM = 2,
|
||||||
|
UPDATE_TODO_ITEM = 4,
|
||||||
|
MOVE_TODO_ITEM = 8,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getTodoLists = (hass: HomeAssistant): TodoList[] =>
|
||||||
|
Object.keys(hass.states)
|
||||||
|
.filter(
|
||||||
|
(entityId) =>
|
||||||
|
computeDomain(entityId) === "todo" &&
|
||||||
|
!isUnavailableState(hass.states[entityId].state)
|
||||||
|
)
|
||||||
|
.sort()
|
||||||
|
.map((entityId) => ({
|
||||||
|
...hass.states[entityId],
|
||||||
|
entity_id: entityId,
|
||||||
|
name: computeStateName(hass.states[entityId]),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export interface TodoItems {
|
||||||
|
items: TodoItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const fetchItems = async (
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entityId: string
|
||||||
|
): Promise<TodoItem[]> => {
|
||||||
|
const result = await hass.callWS<TodoItems>({
|
||||||
|
type: "todo/item/list",
|
||||||
|
entity_id: entityId,
|
||||||
|
});
|
||||||
|
return result.items;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateItem = (
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entity_id: string,
|
||||||
|
item: TodoItem
|
||||||
|
): Promise<ServiceCallResponse> =>
|
||||||
|
hass.callService("todo", "update_item", item, { entity_id });
|
||||||
|
|
||||||
|
export const createItem = (
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entity_id: string,
|
||||||
|
summary: string
|
||||||
|
): Promise<ServiceCallResponse> =>
|
||||||
|
hass.callService(
|
||||||
|
"todo",
|
||||||
|
"create_item",
|
||||||
|
{
|
||||||
|
summary,
|
||||||
|
},
|
||||||
|
{ entity_id }
|
||||||
|
);
|
||||||
|
|
||||||
|
export const deleteItem = (
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entity_id: string,
|
||||||
|
uid: string
|
||||||
|
): Promise<ServiceCallResponse> =>
|
||||||
|
hass.callService(
|
||||||
|
"todo",
|
||||||
|
"delete_item",
|
||||||
|
{
|
||||||
|
uid,
|
||||||
|
},
|
||||||
|
{ entity_id }
|
||||||
|
);
|
||||||
|
|
||||||
|
export const moveItem = (
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entity_id: string,
|
||||||
|
uid: string,
|
||||||
|
pos: number
|
||||||
|
): Promise<void> =>
|
||||||
|
hass.callWS({
|
||||||
|
type: "todo/item/move",
|
||||||
|
entity_id,
|
||||||
|
uid,
|
||||||
|
pos,
|
||||||
|
});
|
@ -14,13 +14,7 @@ export const demoConfig: HassConfig = {
|
|||||||
wind_speed: "m/s",
|
wind_speed: "m/s",
|
||||||
accumulated_precipitation: "mm",
|
accumulated_precipitation: "mm",
|
||||||
},
|
},
|
||||||
components: [
|
components: ["notify.html5", "history", "todo", "forecast_solar", "energy"],
|
||||||
"notify.html5",
|
|
||||||
"history",
|
|
||||||
"shopping_list",
|
|
||||||
"forecast_solar",
|
|
||||||
"energy",
|
|
||||||
],
|
|
||||||
time_zone: "America/Los_Angeles",
|
time_zone: "America/Los_Angeles",
|
||||||
config_dir: "/config",
|
config_dir: "/config",
|
||||||
version: "DEMO",
|
version: "DEMO",
|
||||||
|
@ -34,8 +34,7 @@ const COMPONENTS = {
|
|||||||
map: () => import("../panels/map/ha-panel-map"),
|
map: () => import("../panels/map/ha-panel-map"),
|
||||||
my: () => import("../panels/my/ha-panel-my"),
|
my: () => import("../panels/my/ha-panel-my"),
|
||||||
profile: () => import("../panels/profile/ha-panel-profile"),
|
profile: () => import("../panels/profile/ha-panel-profile"),
|
||||||
"shopping-list": () =>
|
todo: () => import("../panels/todo/ha-panel-todo"),
|
||||||
import("../panels/shopping-list/ha-panel-shopping-list"),
|
|
||||||
"media-browser": () =>
|
"media-browser": () =>
|
||||||
import("../panels/media-browser/ha-panel-media-browser"),
|
import("../panels/media-browser/ha-panel-media-browser"),
|
||||||
};
|
};
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
import { mdiDrag, mdiNotificationClearAll, mdiPlus, mdiSort } from "@mdi/js";
|
import { mdiDrag, mdiNotificationClearAll, mdiPlus, mdiSort } from "@mdi/js";
|
||||||
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||||
import {
|
import {
|
||||||
css,
|
|
||||||
CSSResultGroup,
|
CSSResultGroup,
|
||||||
html,
|
|
||||||
LitElement,
|
LitElement,
|
||||||
|
PropertyValueMap,
|
||||||
PropertyValues,
|
PropertyValues,
|
||||||
|
css,
|
||||||
|
html,
|
||||||
nothing,
|
nothing,
|
||||||
} from "lit";
|
} from "lit";
|
||||||
import { customElement, property, query, state } from "lit/decorators";
|
import { customElement, property, query, state } from "lit/decorators";
|
||||||
@ -13,25 +14,31 @@ import { classMap } from "lit/directives/class-map";
|
|||||||
import { guard } from "lit/directives/guard";
|
import { guard } from "lit/directives/guard";
|
||||||
import { repeat } from "lit/directives/repeat";
|
import { repeat } from "lit/directives/repeat";
|
||||||
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 "../../../components/ha-card";
|
import "../../../components/ha-card";
|
||||||
import "../../../components/ha-checkbox";
|
import "../../../components/ha-checkbox";
|
||||||
|
import "../../../components/ha-list-item";
|
||||||
|
import "../../../components/ha-select";
|
||||||
import "../../../components/ha-svg-icon";
|
import "../../../components/ha-svg-icon";
|
||||||
import "../../../components/ha-textfield";
|
import "../../../components/ha-textfield";
|
||||||
import type { HaTextField } from "../../../components/ha-textfield";
|
import type { HaTextField } from "../../../components/ha-textfield";
|
||||||
import {
|
import {
|
||||||
addItem,
|
TodoItem,
|
||||||
clearItems,
|
TodoItemStatus,
|
||||||
|
TodoListEntityFeature,
|
||||||
|
createItem,
|
||||||
|
deleteItem,
|
||||||
fetchItems,
|
fetchItems,
|
||||||
removeItem,
|
getTodoLists,
|
||||||
reorderItems,
|
moveItem,
|
||||||
ShoppingListItem,
|
|
||||||
updateItem,
|
updateItem,
|
||||||
} from "../../../data/shopping-list";
|
} from "../../../data/todo";
|
||||||
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
|
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
|
||||||
import type { SortableInstance } from "../../../resources/sortable";
|
import type { SortableInstance } from "../../../resources/sortable";
|
||||||
import { HomeAssistant } from "../../../types";
|
import { HomeAssistant } from "../../../types";
|
||||||
|
import { findEntities } from "../common/find-entities";
|
||||||
import { LovelaceCard, LovelaceCardEditor } from "../types";
|
import { LovelaceCard, LovelaceCardEditor } from "../types";
|
||||||
import { SensorCardConfig, ShoppingListCardConfig } from "./types";
|
import { ShoppingListCardConfig } from "./types";
|
||||||
|
|
||||||
@customElement("hui-shopping-list-card")
|
@customElement("hui-shopping-list-card")
|
||||||
class HuiShoppingListCard
|
class HuiShoppingListCard
|
||||||
@ -43,17 +50,35 @@ class HuiShoppingListCard
|
|||||||
return document.createElement("hui-shopping-list-card-editor");
|
return document.createElement("hui-shopping-list-card-editor");
|
||||||
}
|
}
|
||||||
|
|
||||||
public static getStubConfig(): ShoppingListCardConfig {
|
public static getStubConfig(
|
||||||
return { type: "shopping-list" };
|
hass: HomeAssistant,
|
||||||
|
entities: string[],
|
||||||
|
entitiesFallback: string[]
|
||||||
|
): ShoppingListCardConfig {
|
||||||
|
const includeDomains = ["todo"];
|
||||||
|
const maxEntities = 1;
|
||||||
|
const foundEntities = findEntities(
|
||||||
|
hass,
|
||||||
|
maxEntities,
|
||||||
|
entities,
|
||||||
|
entitiesFallback,
|
||||||
|
includeDomains
|
||||||
|
);
|
||||||
|
|
||||||
|
return { type: "shopping-list", entity: foundEntities[0] || "" };
|
||||||
}
|
}
|
||||||
|
|
||||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||||
|
|
||||||
@state() private _config?: ShoppingListCardConfig;
|
@state() private _config?: ShoppingListCardConfig;
|
||||||
|
|
||||||
@state() private _uncheckedItems?: ShoppingListItem[];
|
@state() private _entityId?: string;
|
||||||
|
|
||||||
@state() private _checkedItems?: ShoppingListItem[];
|
@state() private _items: Record<string, TodoItem> = {};
|
||||||
|
|
||||||
|
@state() private _uncheckedItems?: TodoItem[];
|
||||||
|
|
||||||
|
@state() private _checkedItems?: TodoItem[];
|
||||||
|
|
||||||
@state() private _reordering = false;
|
@state() private _reordering = false;
|
||||||
|
|
||||||
@ -71,10 +96,39 @@ class HuiShoppingListCard
|
|||||||
this._config = config;
|
this._config = config;
|
||||||
this._uncheckedItems = [];
|
this._uncheckedItems = [];
|
||||||
this._checkedItems = [];
|
this._checkedItems = [];
|
||||||
|
|
||||||
|
if (this._config!.entity) {
|
||||||
|
this._entityId = this._config!.entity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public willUpdate(
|
||||||
|
_changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>
|
||||||
|
): void {
|
||||||
|
if (!this.hasUpdated) {
|
||||||
|
if (!this._entityId) {
|
||||||
|
const todoLists = getTodoLists(this.hass!);
|
||||||
|
if (todoLists.length) {
|
||||||
|
if (todoLists.length > 1) {
|
||||||
|
// find first entity provided by "shopping_list"
|
||||||
|
for (const list of todoLists) {
|
||||||
|
const entityReg = this.hass?.entities[list.entity_id];
|
||||||
|
if (entityReg?.platform === "shopping_list") {
|
||||||
|
this._entityId = list.entity_id;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!this._entityId) {
|
||||||
|
this._entityId = todoLists[0].entity_id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this._fetchData();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public hassSubscribe(): Promise<UnsubscribeFunc>[] {
|
public hassSubscribe(): Promise<UnsubscribeFunc>[] {
|
||||||
this._fetchData();
|
|
||||||
return [
|
return [
|
||||||
this.hass!.connection.subscribeEvents(
|
this.hass!.connection.subscribeEvents(
|
||||||
() => this._fetchData(),
|
() => this._fetchData(),
|
||||||
@ -91,7 +145,7 @@ class HuiShoppingListCard
|
|||||||
|
|
||||||
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
|
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
|
||||||
const oldConfig = changedProps.get("_config") as
|
const oldConfig = changedProps.get("_config") as
|
||||||
| SensorCardConfig
|
| ShoppingListCardConfig
|
||||||
| undefined;
|
| undefined;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@ -103,7 +157,7 @@ class HuiShoppingListCard
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
if (!this._config || !this.hass) {
|
if (!this._config || !this.hass || !this._entityId) {
|
||||||
return nothing;
|
return nothing;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -115,6 +169,8 @@ class HuiShoppingListCard
|
|||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<div class="addRow">
|
<div class="addRow">
|
||||||
|
${this.todoListSupportsFeature(TodoListEntityFeature.CREATE_TODO_ITEM)
|
||||||
|
? html`
|
||||||
<ha-svg-icon
|
<ha-svg-icon
|
||||||
class="addButton"
|
class="addButton"
|
||||||
.path=${mdiPlus}
|
.path=${mdiPlus}
|
||||||
@ -131,6 +187,10 @@ class HuiShoppingListCard
|
|||||||
)}
|
)}
|
||||||
@keydown=${this._addKeyPress}
|
@keydown=${this._addKeyPress}
|
||||||
></ha-textfield>
|
></ha-textfield>
|
||||||
|
`
|
||||||
|
: nothing}
|
||||||
|
${this.todoListSupportsFeature(TodoListEntityFeature.MOVE_TODO_ITEM)
|
||||||
|
? html`
|
||||||
<ha-svg-icon
|
<ha-svg-icon
|
||||||
class="reorderButton"
|
class="reorderButton"
|
||||||
.path=${mdiSort}
|
.path=${mdiSort}
|
||||||
@ -140,6 +200,8 @@ class HuiShoppingListCard
|
|||||||
@click=${this._toggleReorder}
|
@click=${this._toggleReorder}
|
||||||
>
|
>
|
||||||
</ha-svg-icon>
|
</ha-svg-icon>
|
||||||
|
`
|
||||||
|
: nothing}
|
||||||
</div>
|
</div>
|
||||||
${this._reordering
|
${this._reordering
|
||||||
? html`
|
? html`
|
||||||
@ -163,32 +225,43 @@ class HuiShoppingListCard
|
|||||||
"ui.panel.lovelace.cards.shopping-list.checked_items"
|
"ui.panel.lovelace.cards.shopping-list.checked_items"
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
<ha-svg-icon
|
${this.todoListSupportsFeature(
|
||||||
|
TodoListEntityFeature.DELETE_TODO_ITEM
|
||||||
|
)
|
||||||
|
? html` <ha-svg-icon
|
||||||
class="clearall"
|
class="clearall"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
.path=${mdiNotificationClearAll}
|
.path=${mdiNotificationClearAll}
|
||||||
.title=${this.hass!.localize(
|
.title=${this.hass!.localize(
|
||||||
"ui.panel.lovelace.cards.shopping-list.clear_items"
|
"ui.panel.lovelace.cards.shopping-list.clear_items"
|
||||||
)}
|
)}
|
||||||
@click=${this._clearItems}
|
@click=${this._clearCompletedItems}
|
||||||
>
|
>
|
||||||
</ha-svg-icon>
|
</ha-svg-icon>`
|
||||||
|
: nothing}
|
||||||
</div>
|
</div>
|
||||||
${repeat(
|
${repeat(
|
||||||
this._checkedItems!,
|
this._checkedItems!,
|
||||||
(item) => item.id,
|
(item) => item.uid,
|
||||||
(item) => html`
|
(item) => html`
|
||||||
<div class="editRow">
|
<div class="editRow">
|
||||||
<ha-checkbox
|
${this.todoListSupportsFeature(
|
||||||
|
TodoListEntityFeature.UPDATE_TODO_ITEM
|
||||||
|
)
|
||||||
|
? html` <ha-checkbox
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
.checked=${item.complete}
|
.checked=${item.status === TodoItemStatus.Completed}
|
||||||
.itemId=${item.id}
|
.itemId=${item.uid}
|
||||||
@change=${this._completeItem}
|
@change=${this._completeItem}
|
||||||
></ha-checkbox>
|
></ha-checkbox>`
|
||||||
|
: nothing}
|
||||||
<ha-textfield
|
<ha-textfield
|
||||||
class="item"
|
class="item"
|
||||||
.value=${item.name}
|
.disabled=${!this.todoListSupportsFeature(
|
||||||
.itemId=${item.id}
|
TodoListEntityFeature.UPDATE_TODO_ITEM
|
||||||
|
)}
|
||||||
|
.value=${item.summary}
|
||||||
|
.itemId=${item.uid}
|
||||||
@change=${this._saveEdit}
|
@change=${this._saveEdit}
|
||||||
></ha-textfield>
|
></ha-textfield>
|
||||||
</div>
|
</div>
|
||||||
@ -200,23 +273,30 @@ class HuiShoppingListCard
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _renderItems(items: ShoppingListItem[]) {
|
private _renderItems(items: TodoItem[]) {
|
||||||
return html`
|
return html`
|
||||||
${repeat(
|
${repeat(
|
||||||
items,
|
items,
|
||||||
(item) => item.id,
|
(item) => item.uid,
|
||||||
(item) => html`
|
(item) => html`
|
||||||
<div class="editRow" item-id=${item.id}>
|
<div class="editRow" item-id=${item.uid}>
|
||||||
<ha-checkbox
|
${this.todoListSupportsFeature(
|
||||||
|
TodoListEntityFeature.UPDATE_TODO_ITEM
|
||||||
|
)
|
||||||
|
? html` <ha-checkbox
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
.checked=${item.complete}
|
.checked=${item.status === TodoItemStatus.Completed}
|
||||||
.itemId=${item.id}
|
.itemId=${item.uid}
|
||||||
@change=${this._completeItem}
|
@change=${this._completeItem}
|
||||||
></ha-checkbox>
|
></ha-checkbox>`
|
||||||
|
: nothing}
|
||||||
<ha-textfield
|
<ha-textfield
|
||||||
class="item"
|
class="item"
|
||||||
.value=${item.name}
|
.disabled=${!this.todoListSupportsFeature(
|
||||||
.itemId=${item.id}
|
TodoListEntityFeature.UPDATE_TODO_ITEM
|
||||||
|
)}
|
||||||
|
.value=${item.summary}
|
||||||
|
.itemId=${item.uid}
|
||||||
@change=${this._saveEdit}
|
@change=${this._saveEdit}
|
||||||
></ha-textfield>
|
></ha-textfield>
|
||||||
${this._reordering
|
${this._reordering
|
||||||
@ -237,47 +317,70 @@ class HuiShoppingListCard
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private todoListSupportsFeature(feature: number): boolean {
|
||||||
|
const entityStateObj = this.hass!.states[this._entityId!];
|
||||||
|
return entityStateObj && supportsFeature(entityStateObj, feature);
|
||||||
|
}
|
||||||
|
|
||||||
private async _fetchData(): Promise<void> {
|
private async _fetchData(): Promise<void> {
|
||||||
if (!this.hass) {
|
if (!this.hass || !this._entityId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const checkedItems: ShoppingListItem[] = [];
|
const checkedItems: TodoItem[] = [];
|
||||||
const uncheckedItems: ShoppingListItem[] = [];
|
const uncheckedItems: TodoItem[] = [];
|
||||||
const items = await fetchItems(this.hass);
|
const items = await fetchItems(this.hass!, this._entityId!);
|
||||||
for (const key in items) {
|
const records: Record<string, TodoItem> = {};
|
||||||
if (items[key].complete) {
|
items.forEach((item) => {
|
||||||
checkedItems.push(items[key]);
|
records[item.uid!] = item;
|
||||||
|
if (item.status === TodoItemStatus.Completed) {
|
||||||
|
checkedItems.push(item);
|
||||||
} else {
|
} else {
|
||||||
uncheckedItems.push(items[key]);
|
uncheckedItems.push(item);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
this._items = records;
|
||||||
this._checkedItems = checkedItems;
|
this._checkedItems = checkedItems;
|
||||||
this._uncheckedItems = uncheckedItems;
|
this._uncheckedItems = uncheckedItems;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _completeItem(ev): void {
|
private _completeItem(ev): void {
|
||||||
updateItem(this.hass!, ev.target.itemId, {
|
const item = this._items[ev.target.itemId];
|
||||||
complete: ev.target.checked,
|
updateItem(this.hass!, this._entityId!, {
|
||||||
}).catch(() => this._fetchData());
|
...item,
|
||||||
|
status: ev.target.checked
|
||||||
|
? TodoItemStatus.Completed
|
||||||
|
: TodoItemStatus.NeedsAction,
|
||||||
|
}).finally(() => this._fetchData());
|
||||||
}
|
}
|
||||||
|
|
||||||
private _saveEdit(ev): void {
|
private _saveEdit(ev): void {
|
||||||
// If name is not empty, update the item otherwise remove it
|
// If name is not empty, update the item otherwise remove it
|
||||||
if (ev.target.value) {
|
if (ev.target.value) {
|
||||||
updateItem(this.hass!, ev.target.itemId, {
|
const item = this._items[ev.target.itemId];
|
||||||
name: ev.target.value,
|
updateItem(this.hass!, this._entityId!, {
|
||||||
}).catch(() => this._fetchData());
|
...item,
|
||||||
} else {
|
summary: ev.target.value,
|
||||||
removeItem(this.hass!, ev.target.itemId).catch(() => this._fetchData());
|
}).finally(() => this._fetchData());
|
||||||
|
} else if (
|
||||||
|
this.todoListSupportsFeature(TodoListEntityFeature.DELETE_TODO_ITEM)
|
||||||
|
) {
|
||||||
|
deleteItem(this.hass!, this._entityId!, ev.target.itemId).finally(() =>
|
||||||
|
this._fetchData()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
ev.target.blur();
|
ev.target.blur();
|
||||||
}
|
}
|
||||||
|
|
||||||
private _clearItems(): void {
|
private async _clearCompletedItems(): Promise<void> {
|
||||||
if (this.hass) {
|
if (!this.hass) {
|
||||||
clearItems(this.hass).catch(() => this._fetchData());
|
return;
|
||||||
}
|
}
|
||||||
|
const deleteActions: Array<Promise<any>> = [];
|
||||||
|
this._checkedItems!.forEach((item: TodoItem) => {
|
||||||
|
deleteActions.push(deleteItem(this.hass!, this._entityId!, item.uid!));
|
||||||
|
});
|
||||||
|
await Promise.all(deleteActions).finally(() => this._fetchData());
|
||||||
}
|
}
|
||||||
|
|
||||||
private get _newItem(): HaTextField {
|
private get _newItem(): HaTextField {
|
||||||
@ -286,9 +389,10 @@ class HuiShoppingListCard
|
|||||||
|
|
||||||
private _addItem(ev): void {
|
private _addItem(ev): void {
|
||||||
const newItem = this._newItem;
|
const newItem = this._newItem;
|
||||||
|
|
||||||
if (newItem.value!.length > 0) {
|
if (newItem.value!.length > 0) {
|
||||||
addItem(this.hass!, newItem.value!).catch(() => this._fetchData());
|
createItem(this.hass!, this._entityId!, newItem.value!).finally(() =>
|
||||||
|
this._fetchData()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
newItem.value = "";
|
newItem.value = "";
|
||||||
@ -329,9 +433,13 @@ class HuiShoppingListCard
|
|||||||
// Since this is `onEnd` event, it's possible that
|
// Since this is `onEnd` event, it's possible that
|
||||||
// an item wa dragged away and was put back to its original position.
|
// an item wa dragged away and was put back to its original position.
|
||||||
if (evt.oldIndex !== evt.newIndex) {
|
if (evt.oldIndex !== evt.newIndex) {
|
||||||
reorderItems(this.hass!, this._sortable!.toArray()).catch(() =>
|
const item = this._uncheckedItems![evt.oldIndex];
|
||||||
this._fetchData()
|
moveItem(
|
||||||
);
|
this.hass!,
|
||||||
|
this._entityId!,
|
||||||
|
item.uid!,
|
||||||
|
evt.newIndex
|
||||||
|
).finally(() => this._fetchData());
|
||||||
// Move the shopping list item in memory.
|
// Move the shopping list item in memory.
|
||||||
this._uncheckedItems!.splice(
|
this._uncheckedItems!.splice(
|
||||||
evt.newIndex,
|
evt.newIndex,
|
||||||
@ -416,6 +524,11 @@ class HuiShoppingListCard
|
|||||||
.clearall {
|
.clearall {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.todoList {
|
||||||
|
display: block;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -430,6 +430,7 @@ export interface SensorCardConfig extends LovelaceCardConfig {
|
|||||||
export interface ShoppingListCardConfig extends LovelaceCardConfig {
|
export interface ShoppingListCardConfig extends LovelaceCardConfig {
|
||||||
title?: string;
|
title?: string;
|
||||||
theme?: string;
|
theme?: string;
|
||||||
|
entity?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface StackCardConfig extends LovelaceCardConfig {
|
export interface StackCardConfig extends LovelaceCardConfig {
|
||||||
|
@ -3,6 +3,7 @@ import { customElement, property, state } from "lit/decorators";
|
|||||||
import { assert, assign, object, optional, string } from "superstruct";
|
import { assert, assign, 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/entity/ha-entity-picker";
|
||||||
import "../../../../components/ha-textfield";
|
import "../../../../components/ha-textfield";
|
||||||
import "../../../../components/ha-theme-picker";
|
import "../../../../components/ha-theme-picker";
|
||||||
import { HomeAssistant } from "../../../../types";
|
import { HomeAssistant } from "../../../../types";
|
||||||
@ -16,6 +17,7 @@ const cardConfigStruct = assign(
|
|||||||
object({
|
object({
|
||||||
title: optional(string()),
|
title: optional(string()),
|
||||||
theme: optional(string()),
|
theme: optional(string()),
|
||||||
|
entity: optional(string()),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -48,7 +50,7 @@ export class HuiShoppingListEditor
|
|||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div class="card-config">
|
<div class="card-config">
|
||||||
${!isComponentLoaded(this.hass, "shopping_list")
|
${!isComponentLoaded(this.hass, "todo")
|
||||||
? html`
|
? html`
|
||||||
<div class="error">
|
<div class="error">
|
||||||
${this.hass.localize(
|
${this.hass.localize(
|
||||||
@ -67,6 +69,14 @@ export class HuiShoppingListEditor
|
|||||||
.configValue=${"title"}
|
.configValue=${"title"}
|
||||||
@input=${this._valueChanged}
|
@input=${this._valueChanged}
|
||||||
></ha-textfield>
|
></ha-textfield>
|
||||||
|
<ha-entity-picker
|
||||||
|
.hass=${this.hass!}
|
||||||
|
.configValue=${"entity"}
|
||||||
|
.value=${this._config.entity}
|
||||||
|
.includeDomains=${["todo"]}
|
||||||
|
@value-changed=${this._valueChanged}
|
||||||
|
>
|
||||||
|
</ha-entity-picker>
|
||||||
<ha-theme-picker
|
<ha-theme-picker
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.value=${this._theme}
|
.value=${this._theme}
|
||||||
|
@ -1,106 +0,0 @@
|
|||||||
import { mdiMicrophone } from "@mdi/js";
|
|
||||||
import {
|
|
||||||
css,
|
|
||||||
CSSResultGroup,
|
|
||||||
html,
|
|
||||||
LitElement,
|
|
||||||
PropertyValues,
|
|
||||||
TemplateResult,
|
|
||||||
} from "lit";
|
|
||||||
import { customElement, property, state } from "lit/decorators";
|
|
||||||
import memoizeOne from "memoize-one";
|
|
||||||
import { isComponentLoaded } from "../../common/config/is_component_loaded";
|
|
||||||
import "../../components/ha-icon-button";
|
|
||||||
import "../../components/ha-menu-button";
|
|
||||||
import "../../components/ha-top-app-bar-fixed";
|
|
||||||
import { showVoiceCommandDialog } from "../../dialogs/voice-command-dialog/show-ha-voice-command-dialog";
|
|
||||||
import { haStyle } from "../../resources/styles";
|
|
||||||
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";
|
|
||||||
|
|
||||||
@customElement("ha-panel-shopping-list")
|
|
||||||
class PanelShoppingList extends LitElement {
|
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
|
||||||
|
|
||||||
@property({ type: Boolean, reflect: true }) public narrow!: boolean;
|
|
||||||
|
|
||||||
@state() private _card!: LovelaceCard | HuiErrorCard;
|
|
||||||
|
|
||||||
private _conversation = memoizeOne((_components) =>
|
|
||||||
isComponentLoaded(this.hass, "conversation")
|
|
||||||
);
|
|
||||||
|
|
||||||
protected firstUpdated(changedProperties: PropertyValues): void {
|
|
||||||
super.firstUpdated(changedProperties);
|
|
||||||
|
|
||||||
this._card = createCardElement({ type: "shopping-list" }) as LovelaceCard;
|
|
||||||
this._card.hass = this.hass;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected updated(changedProperties: PropertyValues): void {
|
|
||||||
super.updated(changedProperties);
|
|
||||||
|
|
||||||
if (changedProperties.has("hass")) {
|
|
||||||
this._card.hass = this.hass;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
|
||||||
return html`
|
|
||||||
<ha-top-app-bar-fixed>
|
|
||||||
<ha-menu-button
|
|
||||||
slot="navigationIcon"
|
|
||||||
.hass=${this.hass}
|
|
||||||
.narrow=${this.narrow}
|
|
||||||
></ha-menu-button>
|
|
||||||
<div slot="title">${this.hass.localize("panel.shopping_list")}</div>
|
|
||||||
${this._conversation(this.hass.config.components)
|
|
||||||
? html`
|
|
||||||
<ha-icon-button
|
|
||||||
slot="actionItems"
|
|
||||||
.label=${this.hass!.localize(
|
|
||||||
"ui.panel.shopping_list.start_conversation"
|
|
||||||
)}
|
|
||||||
.path=${mdiMicrophone}
|
|
||||||
@click=${this._showVoiceCommandDialog}
|
|
||||||
></ha-icon-button>
|
|
||||||
`
|
|
||||||
: ""}
|
|
||||||
<div id="columns">
|
|
||||||
<div class="column">${this._card}</div>
|
|
||||||
</div>
|
|
||||||
</ha-top-app-bar-fixed>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _showVoiceCommandDialog(): void {
|
|
||||||
showVoiceCommandDialog(this, this.hass, { pipeline_id: "last_used" });
|
|
||||||
}
|
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
|
||||||
return [
|
|
||||||
haStyle,
|
|
||||||
css`
|
|
||||||
#columns {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: center;
|
|
||||||
margin: 8px;
|
|
||||||
}
|
|
||||||
.column {
|
|
||||||
flex: 1 0 0;
|
|
||||||
max-width: 500px;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface HTMLElementTagNameMap {
|
|
||||||
"ha-panel-shopping-list": PanelShoppingList;
|
|
||||||
}
|
|
||||||
}
|
|
257
src/panels/todo/ha-panel-todo.ts
Normal file
257
src/panels/todo/ha-panel-todo.ts
Normal file
@ -0,0 +1,257 @@
|
|||||||
|
import { ResizeController } from "@lit-labs/observers/resize-controller";
|
||||||
|
import "@material/mwc-list";
|
||||||
|
import { mdiChevronDown, mdiDotsVertical, mdiMicrophone } from "@mdi/js";
|
||||||
|
import {
|
||||||
|
CSSResultGroup,
|
||||||
|
LitElement,
|
||||||
|
PropertyValues,
|
||||||
|
TemplateResult,
|
||||||
|
css,
|
||||||
|
html,
|
||||||
|
nothing,
|
||||||
|
} from "lit";
|
||||||
|
import { customElement, property, state } from "lit/decorators";
|
||||||
|
import memoizeOne from "memoize-one";
|
||||||
|
import { isComponentLoaded } from "../../common/config/is_component_loaded";
|
||||||
|
import { storage } from "../../common/decorators/storage";
|
||||||
|
import { computeDomain } from "../../common/entity/compute_domain";
|
||||||
|
import { computeStateName } from "../../common/entity/compute_state_name";
|
||||||
|
import "../../components/ha-button";
|
||||||
|
import "../../components/ha-icon-button";
|
||||||
|
import "../../components/ha-list-item";
|
||||||
|
import "../../components/ha-menu-button";
|
||||||
|
import "../../components/ha-state-icon";
|
||||||
|
import "../../components/ha-svg-icon";
|
||||||
|
import "../../components/ha-two-pane-top-app-bar-fixed";
|
||||||
|
import { getTodoLists } from "../../data/todo";
|
||||||
|
import { showVoiceCommandDialog } from "../../dialogs/voice-command-dialog/show-ha-voice-command-dialog";
|
||||||
|
import { haStyle } from "../../resources/styles";
|
||||||
|
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";
|
||||||
|
|
||||||
|
@customElement("ha-panel-todo")
|
||||||
|
class PanelTodo extends LitElement {
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property({ type: Boolean, reflect: true }) public narrow!: boolean;
|
||||||
|
|
||||||
|
@property({ type: Boolean, reflect: true }) public mobile = false;
|
||||||
|
|
||||||
|
@state() private _card?: LovelaceCard | HuiErrorCard;
|
||||||
|
|
||||||
|
@storage({
|
||||||
|
key: "selectedTodoEntity",
|
||||||
|
state: true,
|
||||||
|
})
|
||||||
|
private _entityId?: string;
|
||||||
|
|
||||||
|
private _headerHeight = 56;
|
||||||
|
|
||||||
|
private _showPaneController = new ResizeController(this, {
|
||||||
|
callback: (entries: ResizeObserverEntry[]) =>
|
||||||
|
entries[0]?.contentRect.width > 750,
|
||||||
|
});
|
||||||
|
|
||||||
|
private _mql?: MediaQueryList;
|
||||||
|
|
||||||
|
private _conversation = memoizeOne((_components) =>
|
||||||
|
isComponentLoaded(this.hass, "conversation")
|
||||||
|
);
|
||||||
|
|
||||||
|
public connectedCallback() {
|
||||||
|
super.connectedCallback();
|
||||||
|
this._mql = window.matchMedia(
|
||||||
|
"(max-width: 450px), all and (max-height: 500px)"
|
||||||
|
);
|
||||||
|
this._mql.addListener(this._setIsMobile);
|
||||||
|
this.mobile = this._mql.matches;
|
||||||
|
const computedStyles = getComputedStyle(this);
|
||||||
|
this._headerHeight = Number(
|
||||||
|
computedStyles.getPropertyValue("--header-height").replace("px", "")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public disconnectedCallback() {
|
||||||
|
super.disconnectedCallback();
|
||||||
|
this._mql?.removeListener(this._setIsMobile!);
|
||||||
|
this._mql = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _setIsMobile = (ev: MediaQueryListEvent) => {
|
||||||
|
this.mobile = ev.matches;
|
||||||
|
};
|
||||||
|
|
||||||
|
protected willUpdate(changedProperties: PropertyValues): void {
|
||||||
|
super.willUpdate(changedProperties);
|
||||||
|
|
||||||
|
if (!this.hasUpdated && !this._entityId) {
|
||||||
|
this._entityId = Object.keys(this.hass.states).find(
|
||||||
|
(entityId) => computeDomain(entityId) === "todo"
|
||||||
|
);
|
||||||
|
} else if (!this.hasUpdated) {
|
||||||
|
this._createCard();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected updated(changedProperties: PropertyValues): void {
|
||||||
|
super.updated(changedProperties);
|
||||||
|
|
||||||
|
if (changedProperties.has("_entityId")) {
|
||||||
|
this._createCard();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changedProperties.has("hass") && this._card) {
|
||||||
|
this._card.hass = this.hass;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _createCard(): void {
|
||||||
|
if (!this._entityId) {
|
||||||
|
this._card = undefined;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._card = createCardElement({
|
||||||
|
type: "shopping-list",
|
||||||
|
entity: this._entityId,
|
||||||
|
}) as LovelaceCard;
|
||||||
|
this._card.hass = this.hass;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render(): TemplateResult {
|
||||||
|
const showPane = this._showPaneController.value ?? !this.narrow;
|
||||||
|
const listItems = getTodoLists(this.hass).map(
|
||||||
|
(list) =>
|
||||||
|
html`<ha-list-item
|
||||||
|
graphic="icon"
|
||||||
|
@click=${this._handleEntityPicked}
|
||||||
|
.entityId=${list.entity_id}
|
||||||
|
.activated=${list.entity_id === this._entityId}
|
||||||
|
>
|
||||||
|
<ha-state-icon .state=${list} slot="graphic"></ha-state-icon
|
||||||
|
>${list.name}
|
||||||
|
</ha-list-item> `
|
||||||
|
);
|
||||||
|
return html`
|
||||||
|
<ha-two-pane-top-app-bar-fixed .pane=${showPane}>
|
||||||
|
<ha-menu-button
|
||||||
|
slot="navigationIcon"
|
||||||
|
.hass=${this.hass}
|
||||||
|
.narrow=${this.narrow}
|
||||||
|
></ha-menu-button>
|
||||||
|
<div slot="title">
|
||||||
|
${!showPane
|
||||||
|
? html`<ha-button-menu
|
||||||
|
class="lists"
|
||||||
|
activatable
|
||||||
|
fixed
|
||||||
|
.noAnchor=${this.mobile}
|
||||||
|
.y=${this.mobile
|
||||||
|
? this._headerHeight / 2
|
||||||
|
: this._headerHeight / 4}
|
||||||
|
.x=${this.mobile ? 0 : undefined}
|
||||||
|
>
|
||||||
|
<ha-button slot="trigger">
|
||||||
|
${this._entityId
|
||||||
|
? computeStateName(this.hass.states[this._entityId])
|
||||||
|
: ""}
|
||||||
|
<ha-svg-icon
|
||||||
|
slot="trailingIcon"
|
||||||
|
.path=${mdiChevronDown}
|
||||||
|
></ha-svg-icon>
|
||||||
|
</ha-button>
|
||||||
|
${listItems}
|
||||||
|
<li divider role="separator"></li>
|
||||||
|
</ha-button-menu>`
|
||||||
|
: "Lists"}
|
||||||
|
</div>
|
||||||
|
<mwc-list slot="pane" activatable>${listItems}</mwc-list>
|
||||||
|
<ha-button-menu slot="actionItems">
|
||||||
|
<ha-icon-button
|
||||||
|
slot="trigger"
|
||||||
|
.label=${""}
|
||||||
|
.path=${mdiDotsVertical}
|
||||||
|
></ha-icon-button>
|
||||||
|
${this._conversation(this.hass.config.components)
|
||||||
|
? html`<ha-list-item
|
||||||
|
graphic="icon"
|
||||||
|
@click=${this._showVoiceCommandDialog}
|
||||||
|
>
|
||||||
|
<ha-svg-icon .path=${mdiMicrophone} slot="graphic">
|
||||||
|
</ha-svg-icon>
|
||||||
|
${this.hass.localize("ui.panel.todo.start_conversation")}
|
||||||
|
</ha-list-item>`
|
||||||
|
: nothing}
|
||||||
|
</ha-button-menu>
|
||||||
|
<div id="columns">
|
||||||
|
<div class="column">${this._card}</div>
|
||||||
|
</div>
|
||||||
|
</ha-two-pane-top-app-bar-fixed>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _handleEntityPicked(ev) {
|
||||||
|
this._entityId = ev.currentTarget.entityId;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _showVoiceCommandDialog(): void {
|
||||||
|
showVoiceCommandDialog(this, this.hass, { pipeline_id: "last_used" });
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResultGroup {
|
||||||
|
return [
|
||||||
|
haStyle,
|
||||||
|
css`
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
#columns {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
margin: 8px;
|
||||||
|
}
|
||||||
|
.column {
|
||||||
|
flex: 1 0 0;
|
||||||
|
max-width: 500px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
:host([mobile]) .lists {
|
||||||
|
--mdc-menu-min-width: 100vw;
|
||||||
|
}
|
||||||
|
:host([mobile]) ha-button-menu {
|
||||||
|
--mdc-shape-medium: 0 0 var(--mdc-shape-medium)
|
||||||
|
var(--mdc-shape-medium);
|
||||||
|
}
|
||||||
|
ha-button-menu ha-button {
|
||||||
|
--mdc-theme-primary: currentColor;
|
||||||
|
--mdc-typography-button-text-transform: none;
|
||||||
|
--mdc-typography-button-font-size: var(
|
||||||
|
--mdc-typography-headline6-font-size,
|
||||||
|
1.25rem
|
||||||
|
);
|
||||||
|
--mdc-typography-button-font-weight: var(
|
||||||
|
--mdc-typography-headline6-font-weight,
|
||||||
|
500
|
||||||
|
);
|
||||||
|
--mdc-typography-button-letter-spacing: var(
|
||||||
|
--mdc-typography-headline6-letter-spacing,
|
||||||
|
0.0125em
|
||||||
|
);
|
||||||
|
--mdc-typography-button-line-height: var(
|
||||||
|
--mdc-typography-headline6-line-height,
|
||||||
|
2rem
|
||||||
|
);
|
||||||
|
--button-height: 40px;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-panel-todo": PanelTodo;
|
||||||
|
}
|
||||||
|
}
|
@ -8,7 +8,7 @@
|
|||||||
"logbook": "Logbook",
|
"logbook": "Logbook",
|
||||||
"history": "History",
|
"history": "History",
|
||||||
"mailbox": "Mailbox",
|
"mailbox": "Mailbox",
|
||||||
"shopping_list": "Shopping list",
|
"todo": "To-do Lists",
|
||||||
"developer_tools": "Developer tools",
|
"developer_tools": "Developer tools",
|
||||||
"media_browser": "Media",
|
"media_browser": "Media",
|
||||||
"profile": "Profile"
|
"profile": "Profile"
|
||||||
@ -4471,6 +4471,7 @@
|
|||||||
"never_triggered": "Never triggered"
|
"never_triggered": "Never triggered"
|
||||||
},
|
},
|
||||||
"shopping-list": {
|
"shopping-list": {
|
||||||
|
"lists": "To-do Lists",
|
||||||
"checked_items": "Checked items",
|
"checked_items": "Checked items",
|
||||||
"clear_items": "Clear checked items",
|
"clear_items": "Clear checked items",
|
||||||
"add_item": "Add item",
|
"add_item": "Add item",
|
||||||
@ -5046,7 +5047,7 @@
|
|||||||
"shopping-list": {
|
"shopping-list": {
|
||||||
"name": "Shopping list",
|
"name": "Shopping list",
|
||||||
"description": "The Shopping list card allows you to add, edit, check-off, and clear items from your shopping list.",
|
"description": "The Shopping list card allows you to add, edit, check-off, and clear items from your shopping list.",
|
||||||
"integration_not_loaded": "This card requires the `shopping_list` integration to be set up."
|
"integration_not_loaded": "This card requires the `todo` integration to be set up."
|
||||||
},
|
},
|
||||||
"thermostat": {
|
"thermostat": {
|
||||||
"name": "Thermostat",
|
"name": "Thermostat",
|
||||||
@ -5458,7 +5459,7 @@
|
|||||||
"qr_code_image": "QR code for token {name}"
|
"qr_code_image": "QR code for token {name}"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"shopping_list": {
|
"todo": {
|
||||||
"start_conversation": "Start conversation"
|
"start_conversation": "Start conversation"
|
||||||
},
|
},
|
||||||
"page-authorize": {
|
"page-authorize": {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user