1
0
mirror of https://github.com/home-assistant/frontend.git synced 2026-04-02 00:27:49 +01:00

Add due_date_period to todo UI, create period selector (#51263)

* Add due_date_period to todo UI, create period selector

* updates

* Apply suggestions from code review

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>

* ??

---------

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
This commit is contained in:
karwosts
2026-03-31 23:10:15 -07:00
committed by GitHub
parent 7c52ac8ca7
commit 514cb9da9d
6 changed files with 224 additions and 65 deletions

View File

@@ -0,0 +1,144 @@
import { LitElement, css, html } from "lit";
import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../common/dom/fire_event";
import type { PeriodKey, PeriodSelector } from "../../data/selector";
import type { HomeAssistant } from "../../types";
import { deepEqual } from "../../common/util/deep-equal";
import type { LocalizeFunc } from "../../common/translations/localize";
import "../ha-form/ha-form";
const PERIODS = {
none: undefined,
today: { calendar: { period: "day" } },
yesterday: { calendar: { period: "day", offset: -1 } },
tomorrow: { calendar: { period: "day", offset: 1 } },
this_week: { calendar: { period: "week" } },
last_week: { calendar: { period: "week", offset: -1 } },
next_week: { calendar: { period: "week", offset: 1 } },
this_month: { calendar: { period: "month" } },
last_month: { calendar: { period: "month", offset: -1 } },
next_month: { calendar: { period: "month", offset: 1 } },
this_year: { calendar: { period: "year" } },
last_year: { calendar: { period: "year", offset: -1 } },
next_7d: { calendar: { period: "day", offset: 7 } },
next_30d: { calendar: { period: "day", offset: 30 } },
} as const;
@customElement("ha-selector-period")
export class HaPeriodSelector extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public selector!: PeriodSelector;
@property({ attribute: false }) public value?: unknown;
@property() public label?: string;
@property() public helper?: string;
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public required = true;
private _schema = memoizeOne(
(
selectedPeriodKey: PeriodKey | undefined,
selector: PeriodSelector,
localize: LocalizeFunc
) =>
[
{
name: "period",
required: this.required,
selector:
selectedPeriodKey && selectedPeriodKey in this._periods(selector)
? {
select: {
multiple: false,
options: Object.keys(this._periods(selector)).map(
(periodKey) => ({
value: periodKey,
label:
localize(
`ui.components.selectors.period.periods.${periodKey as PeriodKey}`
) || periodKey,
})
),
},
}
: { object: {} },
},
] as const
);
protected render() {
const data = this._data(this.value, this.selector);
const schema = this._schema(
typeof data.period === "string" ? (data.period as PeriodKey) : undefined,
this.selector,
this.hass.localize
);
return html`
<ha-form
.hass=${this.hass}
.data=${data}
.schema=${schema}
.computeHelper=${this._computeHelperCallback}
.computeLabel=${this._computeLabelCallback}
.disabled=${this.disabled}
@value-changed=${this._valueChanged}
></ha-form>
`;
}
private _periods = memoizeOne((selector: PeriodSelector) =>
Object.fromEntries(
Object.entries(PERIODS).filter(([key]) =>
selector.period?.options?.includes(key as any)
)
)
);
private _data = memoizeOne((value: unknown, selector: PeriodSelector) => {
for (const [periodKey, period] of Object.entries(this._periods(selector))) {
if (deepEqual(period, value)) {
return { period: periodKey };
}
}
return { period: value };
});
private _valueChanged(ev: CustomEvent) {
ev.stopPropagation();
const newValue = ev.detail.value;
if (typeof newValue.period === "string") {
const periods = this._periods(this.selector);
if (newValue.period in periods) {
const period = this._periods(this.selector)[newValue.period];
fireEvent(this, "value-changed", { value: period });
}
} else {
fireEvent(this, "value-changed", { value: newValue.period });
}
}
private _computeHelperCallback = () => this.helper;
private _computeLabelCallback = () => this.label;
static styles = css`
:host {
position: relative;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-selector-period": HaPeriodSelector;
}
}

View File

@@ -41,6 +41,7 @@ const LOAD_ELEMENTS = {
number: () => import("./ha-selector-number"),
numeric_threshold: () => import("./ha-selector-numeric-threshold"),
object: () => import("./ha-selector-object"),
period: () => import("./ha-selector-period"),
qr_code: () => import("./ha-selector-qr-code"),
select: () => import("./ha-selector-select"),
selector: () => import("./ha-selector-selector"),

View File

@@ -60,6 +60,7 @@ export type Selector =
| NumberSelector
| NumericThresholdSelector
| ObjectSelector
| PeriodSelector
| AssistPipelineSelector
| QRCodeSelector
| SelectSelector
@@ -392,6 +393,27 @@ export interface ObjectSelector {
} | null;
}
export type PeriodKey =
| "today"
| "yesterday"
| "tomorrow"
| "this_week"
| "last_week"
| "next_week"
| "this_month"
| "last_month"
| "next_month"
| "this_year"
| "last_year"
| "next_7d"
| "next_30d"
| "none";
export interface PeriodSelector {
period: {
options: readonly PeriodKey[];
} | null;
}
export interface AssistPipelineSelector {
assist_pipeline: {
include_last_used?: boolean;

View File

@@ -12,7 +12,6 @@ import {
} from "superstruct";
import { fireEvent } from "../../../../common/dom/fire_event";
import type { LocalizeFunc } from "../../../../common/translations/localize";
import { deepEqual } from "../../../../common/util/deep-equal";
import "../../../../components/ha-form/ha-form";
import type { HaFormSchema } from "../../../../components/ha-form/types";
import type {
@@ -60,17 +59,6 @@ const statTypeMap: Record<(typeof stat_types)[number], StatisticType> = {
change: "sum",
};
const periods = {
today: { calendar: { period: "day" } },
yesterday: { calendar: { period: "day", offset: -1 } },
this_week: { calendar: { period: "week" } },
last_week: { calendar: { period: "week", offset: -1 } },
this_month: { calendar: { period: "month" } },
last_month: { calendar: { period: "month", offset: -1 } },
this_year: { calendar: { period: "year" } },
last_year: { calendar: { period: "year", offset: -1 } },
} as const;
@customElement("hui-statistic-card-editor")
export class HuiStatisticCardEditor
extends LitElement
@@ -109,21 +97,8 @@ export class HuiStatisticCardEditor
});
}
private _data = memoizeOne((config: StatisticCardConfig) => {
if (!config || !config.period) {
return config;
}
for (const [periodKey, period] of Object.entries(periods)) {
if (deepEqual(period, config.period)) {
return { ...config, period: periodKey };
}
}
return config;
});
private _schema = memoizeOne(
(
selectedPeriodKey: string | undefined,
localize: LocalizeFunc,
enableDateSelect: boolean,
metadata?: StatisticsMetaData
@@ -153,21 +128,20 @@ export class HuiStatisticCardEditor
{
name: "period",
required: true,
selector:
selectedPeriodKey && selectedPeriodKey in periods
? {
select: {
multiple: false,
options: Object.keys(periods).map((periodKey) => ({
value: periodKey,
label:
localize(
`ui.panel.lovelace.editor.card.statistic.periods.${periodKey}`
) || periodKey,
})),
selector: {
period: {
options: [
"today",
"yesterday",
"this_week",
"last_week",
"this_month",
"last_month",
"this_year",
"last_year",
],
},
},
}
: { object: {} },
},
]
: []),
@@ -221,10 +195,9 @@ export class HuiStatisticCardEditor
return nothing;
}
const data = this._data(this._config);
const data = this._config;
const schema = this._schema(
typeof data.period === "string" ? data.period : undefined,
this.hass.localize,
!!this._config!.energy_date_selection,
this._metadata
@@ -255,13 +228,6 @@ export class HuiStatisticCardEditor
const config = { ...ev.detail.value } as StatisticCardConfig;
Object.keys(config).forEach((k) => config[k] === "" && delete config[k]);
if (typeof config.period === "string") {
const period = periods[config.period];
if (period) {
config.period = period;
}
}
if (
config.stat_type &&
config.entity &&

View File

@@ -2,15 +2,7 @@ import type { CSSResultGroup } from "lit";
import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import {
assert,
assign,
boolean,
number,
object,
optional,
string,
} from "superstruct";
import { assert, assign, boolean, object, optional, string } from "superstruct";
import { mdiGestureTap } from "@mdi/js";
import {
ITEM_TAP_ACTION_EDIT,
@@ -43,13 +35,7 @@ const cardConfigStruct = assign(
hide_section_headers: optional(boolean()),
display_order: optional(string()),
item_tap_action: optional(string()),
due_date_period: optional(
object({
calendar: optional(
object({ period: string(), offset: optional(number()) })
),
})
),
due_date_period: optional(object()),
})
);
@@ -89,6 +75,24 @@ export class HuiTodoListEditor
},
},
},
{
name: "due_date_period",
selector: {
period: {
options: [
"today",
"tomorrow",
"this_week",
"next_week",
"this_month",
"next_month",
"next_7d",
"next_30d",
"none",
],
},
},
},
{
name: "interactions",
type: "expandable",
@@ -185,6 +189,7 @@ export class HuiTodoListEditor
case "hide_section_headers":
case "display_order":
case "item_tap_action":
case "due_date_period":
return this.hass!.localize(
`ui.panel.lovelace.editor.card.todo-list.${schema.name}`
);
@@ -200,6 +205,7 @@ export class HuiTodoListEditor
) => {
switch (schema.name) {
case "hide_section_headers":
case "due_date_period":
return this.hass!.localize(
`ui.panel.lovelace.editor.card.todo-list.${schema.name}_helper`
);

View File

@@ -518,6 +518,24 @@
"radius": "[%key:ui::panel::config::zone::detail::radius%]",
"radius_meters": "[%key:ui::panel::config::core::section::core::core_config::elevation_meters%]"
},
"period": {
"periods": {
"none": "None",
"today": "[%key:ui::panel::lovelace::editor::card::statistic::periods::today%]",
"tomorrow": "Tomorrow",
"yesterday": "[%key:ui::panel::lovelace::editor::card::statistic::periods::yesterday%]",
"this_week": "[%key:ui::panel::lovelace::editor::card::statistic::periods::this_week%]",
"last_week": "[%key:ui::panel::lovelace::editor::card::statistic::periods::last_week%]",
"next_week": "Next week",
"this_month": "[%key:ui::panel::lovelace::editor::card::statistic::periods::this_month%]",
"last_month": "[%key:ui::panel::lovelace::editor::card::statistic::periods::last_month%]",
"next_month": "Next month",
"this_year": "[%key:ui::panel::lovelace::editor::card::statistic::periods::this_year%]",
"last_year": "[%key:ui::panel::lovelace::editor::card::statistic::periods::last_year%]",
"next_7d": "Next 7 days",
"next_30d": "Next 30 days"
}
},
"selector": {
"options": "Selector options",
"type": "Type",
@@ -9529,6 +9547,8 @@
"hide_section_headers": "Hide section headers",
"hide_section_headers_helper": "Removes the 'Active' and 'Completed' section headers and their overflow menus.",
"display_order": "Display order",
"due_date_period": "Due date",
"due_date_period_helper": "Filter items to those which have a due date in the selected period.",
"item_tap_action": "Item tap behavior",
"actions": {
"edit": "Default (edit item)",