diff --git a/src/components/ha-selector/ha-selector-period.ts b/src/components/ha-selector/ha-selector-period.ts new file mode 100644 index 0000000000..937716ddd1 --- /dev/null +++ b/src/components/ha-selector/ha-selector-period.ts @@ -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` + + `; + } + + 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; + } +} diff --git a/src/components/ha-selector/ha-selector.ts b/src/components/ha-selector/ha-selector.ts index c4e43c5251..9aea57f614 100644 --- a/src/components/ha-selector/ha-selector.ts +++ b/src/components/ha-selector/ha-selector.ts @@ -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"), diff --git a/src/data/selector.ts b/src/data/selector.ts index a26ccb1633..642fda3b43 100644 --- a/src/data/selector.ts +++ b/src/data/selector.ts @@ -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; diff --git a/src/panels/lovelace/editor/config-elements/hui-statistic-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-statistic-card-editor.ts index 035c15aa96..5d99c6d02b 100644 --- a/src/panels/lovelace/editor/config-elements/hui-statistic-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-statistic-card-editor.ts @@ -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, - })), - }, - } - : { object: {} }, + selector: { + period: { + options: [ + "today", + "yesterday", + "this_week", + "last_week", + "this_month", + "last_month", + "this_year", + "last_year", + ], + }, + }, }, ] : []), @@ -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 && diff --git a/src/panels/lovelace/editor/config-elements/hui-todo-list-editor.ts b/src/panels/lovelace/editor/config-elements/hui-todo-list-editor.ts index 1953e60c00..8fe7323e46 100644 --- a/src/panels/lovelace/editor/config-elements/hui-todo-list-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-todo-list-editor.ts @@ -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` ); diff --git a/src/translations/en.json b/src/translations/en.json index 435d9c4862..12f6582d6e 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -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)",