mirror of
https://github.com/home-assistant/frontend.git
synced 2026-07-25 15:34:17 +01:00
Use typed query param handling in todo and refactor handler typing (#52505)
* Use typed query param handlers for todo * Refactor to query param config obj * Remove type casts * Use main window Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com> * Fix import --------- Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
This commit is contained in:
@@ -16,32 +16,17 @@ export type HistoryLogbookTargetParamKey =
|
||||
| "area_id"
|
||||
| "device_id";
|
||||
|
||||
export type HistoryLogbookDateParamKey = "start_date" | "end_date";
|
||||
|
||||
export type HistoryLogbookBooleanParamKey = "back";
|
||||
|
||||
export type HistoryLogbookQueryParams = QueryParamValues<
|
||||
HistoryLogbookTargetParamKey,
|
||||
HistoryLogbookDateParamKey,
|
||||
HistoryLogbookBooleanParamKey
|
||||
>;
|
||||
|
||||
export const historyLogbookTargetParamKeys: HistoryLogbookTargetParamKey[] = [
|
||||
"entity_id",
|
||||
"label_id",
|
||||
"floor_id",
|
||||
"area_id",
|
||||
"device_id",
|
||||
];
|
||||
export const historyLogbookTargetParamKeys: readonly HistoryLogbookTargetParamKey[] =
|
||||
["entity_id", "label_id", "floor_id", "area_id", "device_id"];
|
||||
|
||||
export const historyLogbookQueryParamConfig = {
|
||||
list: historyLogbookTargetParamKeys,
|
||||
date: ["start_date", "end_date"],
|
||||
boolean: [{ key: "back", trueValue: "1" }],
|
||||
} satisfies QueryParamConfig<
|
||||
HistoryLogbookTargetParamKey,
|
||||
HistoryLogbookDateParamKey,
|
||||
HistoryLogbookBooleanParamKey
|
||||
} as const satisfies QueryParamConfig;
|
||||
|
||||
export type HistoryLogbookQueryParams = QueryParamValues<
|
||||
typeof historyLogbookQueryParamConfig
|
||||
>;
|
||||
|
||||
export const decodeHistoryLogbookQueryParams = (
|
||||
|
||||
@@ -6,29 +6,49 @@ export type SearchParamsSource =
|
||||
| Record<string, string>
|
||||
| string;
|
||||
|
||||
export interface QueryParamConfig<
|
||||
ListKey extends string,
|
||||
DateKey extends string,
|
||||
BooleanKey extends string,
|
||||
> {
|
||||
list?: readonly ListKey[];
|
||||
date?: readonly DateKey[];
|
||||
export interface QueryParamConfig {
|
||||
list?: readonly string[];
|
||||
date?: readonly string[];
|
||||
boolean?: readonly {
|
||||
key: BooleanKey;
|
||||
key: string;
|
||||
trueValue: string;
|
||||
}[];
|
||||
string?: readonly string[];
|
||||
}
|
||||
|
||||
export type QueryParamValues<
|
||||
ListKey extends string,
|
||||
DateKey extends string,
|
||||
BooleanKey extends string,
|
||||
> = Partial<
|
||||
Record<ListKey, string[]> &
|
||||
Record<DateKey, Date> &
|
||||
Record<BooleanKey, boolean>
|
||||
type ListKeyOf<C extends QueryParamConfig> = C extends {
|
||||
list: readonly (infer K extends string)[];
|
||||
}
|
||||
? K
|
||||
: never;
|
||||
|
||||
type DateKeyOf<C extends QueryParamConfig> = C extends {
|
||||
date: readonly (infer K extends string)[];
|
||||
}
|
||||
? K
|
||||
: never;
|
||||
|
||||
type BooleanKeyOf<C extends QueryParamConfig> = C extends {
|
||||
boolean: readonly { key: infer K extends string }[];
|
||||
}
|
||||
? K
|
||||
: never;
|
||||
|
||||
type StringKeyOf<C extends QueryParamConfig> = C extends {
|
||||
string: readonly (infer K extends string)[];
|
||||
}
|
||||
? K
|
||||
: never;
|
||||
|
||||
export type QueryParamValues<C extends QueryParamConfig> = Partial<
|
||||
Record<ListKeyOf<C>, string[]> &
|
||||
Record<DateKeyOf<C>, Date> &
|
||||
Record<BooleanKeyOf<C>, boolean> &
|
||||
Record<StringKeyOf<C>, string>
|
||||
>;
|
||||
|
||||
type QueryParamValue = string[] | Date | boolean | string;
|
||||
|
||||
export type ServiceTargetQueryParams<
|
||||
Key extends keyof HassServiceTarget & string,
|
||||
> = Partial<Record<Key, string[]>>;
|
||||
@@ -46,53 +66,59 @@ const getSearchParam = (
|
||||
return searchParams[key] ?? null;
|
||||
};
|
||||
|
||||
export const decodeQueryParams = <
|
||||
ListKey extends string,
|
||||
DateKey extends string,
|
||||
BooleanKey extends string,
|
||||
>(
|
||||
export function decodeQueryParams<C extends QueryParamConfig>(
|
||||
searchParams: SearchParamsSource,
|
||||
config: QueryParamConfig<ListKey, DateKey, BooleanKey>
|
||||
): QueryParamValues<ListKey, DateKey, BooleanKey> => {
|
||||
const params: QueryParamValues<ListKey, DateKey, BooleanKey> = {};
|
||||
config: C
|
||||
): QueryParamValues<C>;
|
||||
export function decodeQueryParams(
|
||||
searchParams: SearchParamsSource,
|
||||
config: QueryParamConfig
|
||||
): Record<string, QueryParamValue | undefined> {
|
||||
const params: Record<string, QueryParamValue> = {};
|
||||
for (const key of config.list ?? []) {
|
||||
const value = getSearchParam(searchParams, key);
|
||||
if (value) {
|
||||
params[key] = value.split(",") as (typeof params)[typeof key];
|
||||
params[key] = value.split(",");
|
||||
}
|
||||
}
|
||||
for (const key of config.date ?? []) {
|
||||
const value = getSearchParam(searchParams, key);
|
||||
if (value) {
|
||||
params[key] = new Date(value) as (typeof params)[typeof key];
|
||||
params[key] = new Date(value);
|
||||
}
|
||||
}
|
||||
for (const { key, trueValue } of config.boolean ?? []) {
|
||||
if (getSearchParam(searchParams, key) === trueValue) {
|
||||
params[key] = true as (typeof params)[typeof key];
|
||||
params[key] = true;
|
||||
}
|
||||
}
|
||||
for (const key of config.string ?? []) {
|
||||
const value = getSearchParam(searchParams, key);
|
||||
if (value) {
|
||||
params[key] = value;
|
||||
}
|
||||
}
|
||||
return params;
|
||||
};
|
||||
}
|
||||
|
||||
export const createQueryString = <
|
||||
ListKey extends string,
|
||||
DateKey extends string,
|
||||
BooleanKey extends string,
|
||||
>(
|
||||
values: QueryParamValues<ListKey, DateKey, BooleanKey>,
|
||||
config: QueryParamConfig<ListKey, DateKey, BooleanKey>
|
||||
): string => {
|
||||
export function createQueryString<C extends QueryParamConfig>(
|
||||
values: QueryParamValues<NoInfer<C>>,
|
||||
config: C
|
||||
): string;
|
||||
export function createQueryString(
|
||||
values: Record<string, QueryParamValue | undefined>,
|
||||
config: QueryParamConfig
|
||||
): string {
|
||||
const searchParams = new URLSearchParams();
|
||||
for (const key of config.list ?? []) {
|
||||
const value = values[key] as string[] | undefined;
|
||||
if (value?.length) {
|
||||
const value = values[key];
|
||||
if (Array.isArray(value) && value.length) {
|
||||
searchParams.append(key, value.join(","));
|
||||
}
|
||||
}
|
||||
for (const key of config.date ?? []) {
|
||||
const value = values[key] as Date | undefined;
|
||||
if (value) {
|
||||
const value = values[key];
|
||||
if (value instanceof Date) {
|
||||
searchParams.append(key, value.toISOString());
|
||||
}
|
||||
}
|
||||
@@ -101,8 +127,14 @@ export const createQueryString = <
|
||||
searchParams.append(key, trueValue);
|
||||
}
|
||||
}
|
||||
for (const key of config.string ?? []) {
|
||||
const value = values[key];
|
||||
if (typeof value === "string" && value) {
|
||||
searchParams.append(key, value);
|
||||
}
|
||||
}
|
||||
return searchParams.toString();
|
||||
};
|
||||
}
|
||||
|
||||
export const serviceTargetFromQueryParams = <
|
||||
Key extends keyof HassServiceTarget & string,
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import {
|
||||
createQueryString,
|
||||
decodeQueryParams,
|
||||
type QueryParamConfig,
|
||||
type QueryParamValues,
|
||||
type SearchParamsSource,
|
||||
} from "./query-params";
|
||||
|
||||
export const todoQueryParamConfig = {
|
||||
string: ["entity_id"],
|
||||
boolean: [{ key: "add_item", trueValue: "true" }],
|
||||
} as const satisfies QueryParamConfig;
|
||||
|
||||
export type TodoQueryParams = QueryParamValues<typeof todoQueryParamConfig>;
|
||||
|
||||
export const decodeTodoQueryParams = (
|
||||
searchParams: SearchParamsSource
|
||||
): TodoQueryParams => decodeQueryParams(searchParams, todoQueryParamConfig);
|
||||
|
||||
export const createTodoQueryString = (values: TodoQueryParams): string =>
|
||||
createQueryString(values, todoQueryParamConfig);
|
||||
@@ -19,11 +19,11 @@ import { computeStateName } from "../../common/entity/compute_state_name";
|
||||
import { supportsFeature } from "../../common/entity/supports-feature";
|
||||
import { navigate } from "../../common/navigate";
|
||||
import { constructUrlCurrentPath } from "../../common/url/construct-url";
|
||||
import { extractSearchParamsObject } from "../../common/url/search-params";
|
||||
import {
|
||||
createSearchParam,
|
||||
extractSearchParam,
|
||||
removeSearchParam,
|
||||
} from "../../common/url/search-params";
|
||||
createTodoQueryString,
|
||||
decodeTodoQueryParams,
|
||||
} from "../../common/url/todo-query-params";
|
||||
import "../../components/ha-button";
|
||||
import "../../components/ha-dropdown";
|
||||
import type { HaDropdownSelectEvent } from "../../components/ha-dropdown";
|
||||
@@ -104,10 +104,11 @@ class PanelTodo extends LitElement {
|
||||
if (!this.hasUpdated) {
|
||||
this.hass.loadFragmentTranslation("lovelace");
|
||||
|
||||
const urlEntityId = extractSearchParam("entity_id");
|
||||
this._openAddItemFromUrl = extractSearchParam("add_item") === "true";
|
||||
if (urlEntityId) {
|
||||
this._entityId = urlEntityId;
|
||||
const params = decodeTodoQueryParams(extractSearchParamsObject());
|
||||
this._openAddItemFromUrl = params.add_item ?? false;
|
||||
|
||||
if (params.entity_id) {
|
||||
this._entityId = params.entity_id;
|
||||
} else {
|
||||
if (this._entityId && !(this._entityId in this.hass.states)) {
|
||||
this._entityId = undefined;
|
||||
@@ -127,9 +128,12 @@ class PanelTodo extends LitElement {
|
||||
}
|
||||
|
||||
this._openAddItemFromUrl = false;
|
||||
navigate(constructUrlCurrentPath(removeSearchParam("add_item")), {
|
||||
replace: true,
|
||||
});
|
||||
navigate(
|
||||
constructUrlCurrentPath(
|
||||
createTodoQueryString({ entity_id: this._entityId })
|
||||
),
|
||||
{ replace: true }
|
||||
);
|
||||
if (
|
||||
supportsFeature(
|
||||
this.hass.states[this._entityId],
|
||||
@@ -146,7 +150,9 @@ class PanelTodo extends LitElement {
|
||||
return;
|
||||
}
|
||||
navigate(
|
||||
constructUrlCurrentPath(createSearchParam({ entity_id: this._entityId })),
|
||||
constructUrlCurrentPath(
|
||||
createTodoQueryString({ entity_id: this._entityId })
|
||||
),
|
||||
{ replace: true }
|
||||
);
|
||||
}
|
||||
|
||||
@@ -12,7 +12,12 @@ import {
|
||||
decodeQueryParams,
|
||||
queryParamsFromServiceTarget,
|
||||
serviceTargetFromQueryParams,
|
||||
type QueryParamConfig,
|
||||
} from "../../../src/common/url/query-params";
|
||||
import {
|
||||
createTodoQueryString,
|
||||
decodeTodoQueryParams,
|
||||
} from "../../../src/common/url/todo-query-params";
|
||||
|
||||
const panelQueryParams = [
|
||||
{
|
||||
@@ -144,3 +149,94 @@ describe("history logbook query params", () => {
|
||||
).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("string params", () => {
|
||||
const stringConfig = {
|
||||
string: ["name", "color"],
|
||||
} as const satisfies QueryParamConfig;
|
||||
|
||||
it("decodes scalar string params", () => {
|
||||
expect(decodeQueryParams("?name=hello&color=blue", stringConfig)).toEqual({
|
||||
name: "hello",
|
||||
color: "blue",
|
||||
});
|
||||
});
|
||||
|
||||
it("ignores empty string values", () => {
|
||||
expect(decodeQueryParams("?name=&color=blue", stringConfig)).toEqual({
|
||||
color: "blue",
|
||||
});
|
||||
});
|
||||
|
||||
it("ignores missing string params", () => {
|
||||
expect(decodeQueryParams("?color=green", stringConfig)).toEqual({
|
||||
color: "green",
|
||||
});
|
||||
});
|
||||
|
||||
it("encodes scalar string params", () => {
|
||||
expect(
|
||||
createQueryString({ name: "hello", color: "blue" }, stringConfig)
|
||||
).toBe("name=hello&color=blue");
|
||||
});
|
||||
|
||||
it("omits undefined string values from encoding", () => {
|
||||
expect(createQueryString({ name: "hello" }, stringConfig)).toBe(
|
||||
"name=hello"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("todo query params", () => {
|
||||
it("decodes entity_id", () => {
|
||||
expect(decodeTodoQueryParams("?entity_id=todo.shopping")).toEqual({
|
||||
entity_id: "todo.shopping",
|
||||
});
|
||||
});
|
||||
|
||||
it("decodes entity_id with add_item", () => {
|
||||
expect(
|
||||
decodeTodoQueryParams("?entity_id=todo.shopping&add_item=true")
|
||||
).toEqual({
|
||||
entity_id: "todo.shopping",
|
||||
add_item: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("ignores add_item with non-true value", () => {
|
||||
expect(
|
||||
decodeTodoQueryParams("?entity_id=todo.shopping&add_item=false")
|
||||
).toEqual({
|
||||
entity_id: "todo.shopping",
|
||||
});
|
||||
});
|
||||
|
||||
it("returns empty for no params", () => {
|
||||
expect(decodeTodoQueryParams("")).toEqual({});
|
||||
});
|
||||
|
||||
it("creates query string with entity_id only", () => {
|
||||
expect(createTodoQueryString({ entity_id: "todo.shopping" })).toBe(
|
||||
"entity_id=todo.shopping"
|
||||
);
|
||||
});
|
||||
|
||||
it("creates query string with entity_id and add_item", () => {
|
||||
expect(
|
||||
createTodoQueryString({ entity_id: "todo.shopping", add_item: true })
|
||||
).toBe("add_item=true&entity_id=todo.shopping");
|
||||
});
|
||||
|
||||
it("omits add_item when false or undefined", () => {
|
||||
expect(
|
||||
createTodoQueryString({ entity_id: "todo.shopping", add_item: false })
|
||||
).toBe("entity_id=todo.shopping");
|
||||
});
|
||||
|
||||
it("round-trips decode and encode", () => {
|
||||
const original = "?entity_id=todo.tasks&add_item=true";
|
||||
const decoded = decodeTodoQueryParams(original);
|
||||
const encoded = createTodoQueryString(decoded);
|
||||
expect(encoded).toBe("add_item=true&entity_id=todo.tasks");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user