mirror of
https://github.com/home-assistant/frontend.git
synced 2026-06-09 18:03:03 +00:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0f81311c76 | |||
| 8a85d1cf31 | |||
| 9ba34bdf9a | |||
| f0f28789de |
@@ -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);
|
||||
@@ -1477,6 +1477,11 @@ export class HaDataTable extends LitElement {
|
||||
.mdc-data-table__table.auto-height .scroller {
|
||||
overflow-y: hidden !important;
|
||||
}
|
||||
|
||||
.mdc-data-table__table.auto-height lit-virtualizer {
|
||||
overscroll-behavior-y: auto;
|
||||
}
|
||||
|
||||
.grows {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
|
||||
@@ -112,12 +112,16 @@ export class HaCameraStream extends LitElement {
|
||||
return nothing;
|
||||
}
|
||||
if (stream.type === MJPEG_STREAM) {
|
||||
const streamUrl = __DEMO__
|
||||
? this.stateObj.attributes.entity_picture
|
||||
: this._connected
|
||||
? computeMJPEGStreamUrl(this.stateObj)
|
||||
: this._posterUrl;
|
||||
if (!streamUrl) {
|
||||
return nothing;
|
||||
}
|
||||
return html`<img
|
||||
.src=${__DEMO__
|
||||
? this.stateObj.attributes.entity_picture!
|
||||
: this._connected
|
||||
? computeMJPEGStreamUrl(this.stateObj)
|
||||
: this._posterUrl || ""}
|
||||
.src=${streamUrl}
|
||||
style=${styleMap({
|
||||
aspectRatio: this.aspectRatio,
|
||||
objectFit: this.fitMode,
|
||||
|
||||
+7
-3
@@ -17,7 +17,7 @@ export type StreamType = typeof STREAM_TYPE_HLS | typeof STREAM_TYPE_WEB_RTC;
|
||||
|
||||
interface CameraEntityAttributes extends HassEntityAttributeBase {
|
||||
model_name: string;
|
||||
access_token: string;
|
||||
access_token?: string;
|
||||
brand: string;
|
||||
motion_detection: boolean;
|
||||
frontend_stream_type: string;
|
||||
@@ -78,8 +78,12 @@ export const cameraUrlWithWidthHeight = (
|
||||
height: number
|
||||
) => `${base_url}&width=${width}&height=${height}`;
|
||||
|
||||
export const computeMJPEGStreamUrl = (entity: CameraEntity) =>
|
||||
`/api/camera_proxy_stream/${entity.entity_id}?token=${entity.attributes.access_token}`;
|
||||
export const computeMJPEGStreamUrl = (
|
||||
entity: CameraEntity
|
||||
): string | undefined =>
|
||||
entity.attributes.access_token
|
||||
? `/api/camera_proxy_stream/${entity.entity_id}?token=${entity.attributes.access_token}`
|
||||
: undefined;
|
||||
|
||||
export const fetchThumbnailUrlWithCache = async (
|
||||
hass: HomeAssistant,
|
||||
|
||||
+5
-3
@@ -4,12 +4,14 @@ import type {
|
||||
} from "home-assistant-js-websocket";
|
||||
|
||||
interface ImageEntityAttributes extends HassEntityAttributeBase {
|
||||
access_token: string;
|
||||
access_token?: string;
|
||||
}
|
||||
|
||||
export interface ImageEntity extends HassEntityBase {
|
||||
attributes: ImageEntityAttributes;
|
||||
}
|
||||
|
||||
export const computeImageUrl = (entity: ImageEntity): string =>
|
||||
`/api/image_proxy/${entity.entity_id}?token=${entity.attributes.access_token}&state=${entity.state}`;
|
||||
export const computeImageUrl = (entity: ImageEntity): string | undefined =>
|
||||
entity.attributes.access_token
|
||||
? `/api/image_proxy/${entity.entity_id}?token=${entity.attributes.access_token}&state=${entity.state}`
|
||||
: undefined;
|
||||
|
||||
@@ -106,7 +106,9 @@ class EntityPreviewRow extends LitElement {
|
||||
}
|
||||
`;
|
||||
|
||||
private _renderEntityState(stateObj: HassEntity): TemplateResult | string {
|
||||
private _renderEntityState(
|
||||
stateObj: HassEntity
|
||||
): TemplateResult | string | typeof nothing {
|
||||
const domain = stateObj.entity_id.split(".", 1)[0];
|
||||
const disabled = stateObj.state === UNAVAILABLE;
|
||||
const noValue =
|
||||
@@ -216,7 +218,10 @@ class EntityPreviewRow extends LitElement {
|
||||
}
|
||||
|
||||
if (domain === "image") {
|
||||
const image: string = computeImageUrl(stateObj as ImageEntity);
|
||||
const image = computeImageUrl(stateObj as ImageEntity);
|
||||
if (!image) {
|
||||
return nothing;
|
||||
}
|
||||
return html`
|
||||
<img
|
||||
alt=${ifDefined(stateObj?.attributes.friendly_name)}
|
||||
|
||||
@@ -15,9 +15,13 @@ class MoreInfoImage extends LitElement {
|
||||
if (!this.hass || !this.stateObj) {
|
||||
return nothing;
|
||||
}
|
||||
const imageUrl = computeImageUrl(this.stateObj);
|
||||
if (!imageUrl) {
|
||||
return nothing;
|
||||
}
|
||||
return html`<img
|
||||
alt=${this.stateObj.attributes.friendly_name || this.stateObj.entity_id}
|
||||
src=${this.hass.hassUrl(computeImageUrl(this.stateObj))}
|
||||
src=${this.hass.hassUrl(imageUrl)}
|
||||
/> `;
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ import { transform } from "../../../common/decorators/transform";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { computeDeviceNameDisplay } from "../../../common/entity/compute_device_name";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
import { computeEntityPickerDisplay } from "../../../common/entity/compute_entity_name_display";
|
||||
import { computeStateName } from "../../../common/entity/compute_state_name";
|
||||
import { goBack, navigate } from "../../../common/navigate";
|
||||
import { computeRTL } from "../../../common/util/compute_rtl";
|
||||
@@ -47,7 +48,11 @@ import {
|
||||
} from "../../../data/context";
|
||||
import type { DeviceRegistryEntry } from "../../../data/device/device_registry";
|
||||
import type { EntityRegistryEntry } from "../../../data/entity/entity_registry";
|
||||
import { updateEntityRegistryEntry } from "../../../data/entity/entity_registry";
|
||||
import {
|
||||
entityRegistryByEntityId,
|
||||
updateEntityRegistryEntry,
|
||||
} from "../../../data/entity/entity_registry";
|
||||
import { domainToName } from "../../../data/integration";
|
||||
import type {
|
||||
SceneConfig,
|
||||
SceneEntities,
|
||||
@@ -344,6 +349,9 @@ export class HaSceneEditor extends PreventUnsavedMixin(
|
||||
this._deviceEntityLookup,
|
||||
Object.values(this.hass.devices)
|
||||
);
|
||||
const entityRegistryLookup = entityRegistryByEntityId(
|
||||
this._entityRegistryEntries
|
||||
);
|
||||
return html` <div
|
||||
id="root"
|
||||
class=${classMap({
|
||||
@@ -423,9 +431,19 @@ export class HaSceneEditor extends PreventUnsavedMixin(
|
||||
if (!entityStateObj) {
|
||||
return nothing;
|
||||
}
|
||||
const { secondary } = computeEntityPickerDisplay(
|
||||
this.hass,
|
||||
entityStateObj
|
||||
);
|
||||
const platform =
|
||||
entityRegistryLookup[entityId]?.platform;
|
||||
const integrationName = platform
|
||||
? domainToName(this.hass.localize, platform)
|
||||
: undefined;
|
||||
return html`
|
||||
<ha-list-item
|
||||
hasMeta
|
||||
?twoline=${!!secondary}
|
||||
.graphic=${this._mode === "live"
|
||||
? "icon"
|
||||
: undefined}
|
||||
@@ -445,6 +463,14 @@ export class HaSceneEditor extends PreventUnsavedMixin(
|
||||
`
|
||||
: nothing}
|
||||
${computeStateName(entityStateObj)}
|
||||
${secondary
|
||||
? html`<span slot="secondary">${secondary}</span>`
|
||||
: nothing}
|
||||
${integrationName
|
||||
? html`<span slot="meta" class="domain"
|
||||
>${integrationName}</span
|
||||
>`
|
||||
: nothing}
|
||||
</ha-list-item>
|
||||
`;
|
||||
})}
|
||||
@@ -496,10 +522,19 @@ export class HaSceneEditor extends PreventUnsavedMixin(
|
||||
if (!entityStateObj) {
|
||||
return nothing;
|
||||
}
|
||||
const { secondary } = computeEntityPickerDisplay(
|
||||
this.hass,
|
||||
entityStateObj
|
||||
);
|
||||
const domainName = domainToName(
|
||||
this.hass.localize,
|
||||
computeDomain(entityId)
|
||||
);
|
||||
return html`
|
||||
<ha-list-item
|
||||
class="entity"
|
||||
hasMeta
|
||||
?twoline=${!!secondary}
|
||||
.graphic=${this._mode === "live"
|
||||
? "icon"
|
||||
: undefined}
|
||||
@@ -517,7 +552,13 @@ export class HaSceneEditor extends PreventUnsavedMixin(
|
||||
></state-badge>`
|
||||
: nothing}
|
||||
${computeStateName(entityStateObj)}
|
||||
${secondary
|
||||
? html`<span slot="secondary"
|
||||
>${secondary}</span
|
||||
>`
|
||||
: nothing}
|
||||
<div slot="meta">
|
||||
<span class="domain">${domainName}</span>
|
||||
<ha-icon-button
|
||||
.path=${mdiDelete}
|
||||
.entityId=${entityId}
|
||||
@@ -1355,10 +1396,21 @@ export class HaSceneEditor extends PreventUnsavedMixin(
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
ha-list-item {
|
||||
/* let the trailing label size to its content instead of the default
|
||||
fixed meta width, which would clip it */
|
||||
--mdc-list-item-meta-size: auto;
|
||||
}
|
||||
ha-list-item.entity {
|
||||
padding-right: 28px;
|
||||
}
|
||||
.domain {
|
||||
font-size: var(--ha-font-size-s);
|
||||
color: var(--secondary-text-color);
|
||||
white-space: nowrap;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -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