mirror of
https://github.com/home-assistant/frontend.git
synced 2026-10-21 05:03:28 +00:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
624865b2a9 | ||
|
|
a321b07bba | ||
|
|
cfd5b4fd8d |
No files matched your search
@@ -153,7 +153,9 @@ gulp.task("rspack-watch-app", () => {
|
||||
: createAppConfig({ isProdBuild: false, latestBuild: true })
|
||||
).watch({ poll: isWsl }, doneHandler());
|
||||
gulp.watch(
|
||||
path.join(paths.translations_src, "en.json"),
|
||||
// Watch the folder via a glob; git replaces en.json on checkout/stash,
|
||||
// which can permanently drop a watch on the file itself
|
||||
path.join(paths.translations_src, "*.json"),
|
||||
gulp.series("build-translations", "copy-translations-app")
|
||||
);
|
||||
});
|
||||
|
||||
@@ -38,6 +38,10 @@ export class HaBaseTimeInput extends LitElement {
|
||||
*/
|
||||
@property() label?: string;
|
||||
|
||||
/** Accessible name for the input when it has no visible label. */
|
||||
@property({ attribute: "accessible-label" })
|
||||
public accessibleLabel?: string;
|
||||
|
||||
/**
|
||||
* Helper for the input
|
||||
*/
|
||||
@@ -178,6 +182,7 @@ export class HaBaseTimeInput extends LitElement {
|
||||
class="time-input-wrap"
|
||||
role="group"
|
||||
aria-labelledby=${ifDefined(this.label ? "label" : undefined)}
|
||||
aria-label=${ifDefined(this.label ? undefined : this.accessibleLabel)}
|
||||
>
|
||||
${
|
||||
this.enableSign
|
||||
|
||||
@@ -50,6 +50,10 @@ export class HaDateInput extends LitElement {
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
/** Accessible name for the input when it has no visible label. */
|
||||
@property({ attribute: "accessible-label" })
|
||||
public accessibleLabel?: string;
|
||||
|
||||
@property() public helper?: string;
|
||||
|
||||
@property({ attribute: "can-clear", type: Boolean }) public canClear = false;
|
||||
@@ -65,6 +69,7 @@ export class HaDateInput extends LitElement {
|
||||
.label=${this.label ?? ""}
|
||||
.hint=${this.helper ?? ""}
|
||||
.disabled=${this.disabled}
|
||||
.accessibleLabel=${this.accessibleLabel}
|
||||
readonly
|
||||
@click=${this._openDialog}
|
||||
@keydown=${this._keyDown}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type {
|
||||
HaFormBaseSchema,
|
||||
HaFormCondition,
|
||||
HaFormDataContainer,
|
||||
HaFormFieldCondition,
|
||||
HaFormSchema,
|
||||
HaFormVisible,
|
||||
} from "./types";
|
||||
|
||||
const isEmpty = (value: unknown): boolean =>
|
||||
@@ -57,10 +57,11 @@ export const evaluateCondition = (
|
||||
return matchFieldCondition(condition, data);
|
||||
};
|
||||
|
||||
export const isVisible = (
|
||||
visible: HaFormVisible | undefined,
|
||||
export const isFieldVisible = (
|
||||
schema: HaFormSchema,
|
||||
data: HaFormDataContainer | undefined
|
||||
): boolean => {
|
||||
const { visible } = schema as HaFormBaseSchema;
|
||||
if (visible === undefined || visible === true) {
|
||||
return true;
|
||||
}
|
||||
@@ -71,26 +72,10 @@ export const isVisible = (
|
||||
return conditions.every((condition) => evaluateCondition(condition, data));
|
||||
};
|
||||
|
||||
export const combineVisible = (
|
||||
...options: (HaFormVisible | undefined)[]
|
||||
): HaFormVisible => {
|
||||
const conditions: HaFormCondition[] = [];
|
||||
for (const visible of options) {
|
||||
if (visible === undefined || visible === true) {
|
||||
continue;
|
||||
}
|
||||
if (visible === false) {
|
||||
return false;
|
||||
}
|
||||
conditions.push(...(Array.isArray(visible) ? visible : [visible]));
|
||||
}
|
||||
return conditions;
|
||||
};
|
||||
|
||||
// Hiding a field drops its value, which can flip another field's condition, so
|
||||
// resolve the set to a fixpoint. Fields are given in order as name and visible.
|
||||
// resolve the set to a fixpoint.
|
||||
export const getHiddenFields = (
|
||||
fields: readonly (readonly [string, HaFormVisible | undefined])[],
|
||||
schema: readonly HaFormSchema[],
|
||||
data: HaFormDataContainer | undefined
|
||||
): Set<string> => {
|
||||
const hidden = new Set<string>();
|
||||
@@ -98,23 +83,14 @@ export const getHiddenFields = (
|
||||
let changed = true;
|
||||
while (changed) {
|
||||
changed = false;
|
||||
for (const [name, visible] of fields) {
|
||||
if (hidden.has(name) || isVisible(visible, evalData)) {
|
||||
for (const field of schema) {
|
||||
if (hidden.has(field.name) || isFieldVisible(field, evalData)) {
|
||||
continue;
|
||||
}
|
||||
hidden.add(name);
|
||||
delete evalData[name];
|
||||
hidden.add(field.name);
|
||||
delete evalData[field.name];
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
return hidden;
|
||||
};
|
||||
|
||||
export const getHiddenSchemaFields = (
|
||||
schema: readonly HaFormSchema[],
|
||||
data: HaFormDataContainer | undefined
|
||||
): Set<string> =>
|
||||
getHiddenFields(
|
||||
schema.map((field) => [field.name, field.visible] as const),
|
||||
data
|
||||
);
|
||||
@@ -2,7 +2,7 @@ import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, queryAll } from "lit/decorators";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { getHiddenSchemaFields } from "./conditions";
|
||||
import { getHiddenFields } from "./conditions";
|
||||
import "./ha-form";
|
||||
import type { HaForm } from "./ha-form";
|
||||
import type {
|
||||
@@ -68,7 +68,7 @@ export class HaFormGrid extends LitElement implements HaFormElement {
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const hiddenFields = getHiddenSchemaFields(this.schema.schema, this.data);
|
||||
const hiddenFields = getHiddenFields(this.schema.schema, this.data);
|
||||
|
||||
return html`
|
||||
${this.schema.schema
|
||||
|
||||
@@ -7,7 +7,7 @@ import { resolveSelectorContext } from "../../data/selector";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../../types";
|
||||
import "../ha-alert";
|
||||
import "../ha-selector/ha-selector";
|
||||
import { getHiddenSchemaFields } from "./conditions";
|
||||
import { getHiddenFields } from "./conditions";
|
||||
import type {
|
||||
HaFormData,
|
||||
HaFormDataContainer,
|
||||
@@ -136,7 +136,7 @@ export class HaForm extends LitElement implements HaFormElement {
|
||||
let isValid = true;
|
||||
let firstInvalidElement: HTMLElement | undefined;
|
||||
|
||||
const hiddenFields = getHiddenSchemaFields(this.schema, this.data);
|
||||
const hiddenFields = getHiddenFields(this.schema, this.data);
|
||||
const visibleSchema = this.schema.filter(
|
||||
(item) => !hiddenFields.has(item.name)
|
||||
);
|
||||
@@ -195,7 +195,7 @@ export class HaForm extends LitElement implements HaFormElement {
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const renderHiddenFields = getHiddenSchemaFields(this.schema, this.data);
|
||||
const renderHiddenFields = getHiddenFields(this.schema, this.data);
|
||||
|
||||
return html`
|
||||
<div class="root" part="root">
|
||||
|
||||
@@ -25,7 +25,7 @@ export interface HaFormBaseSchema {
|
||||
disabled?: boolean;
|
||||
// Field is visible while the condition holds (visible by default).
|
||||
// Serializable so it can be shared with the backend and other renderers.
|
||||
visible?: HaFormVisible;
|
||||
visible?: boolean | HaFormCondition | HaFormCondition[];
|
||||
description?: {
|
||||
suffix?: string;
|
||||
// This value will be set initially when form is loaded
|
||||
@@ -34,8 +34,6 @@ export interface HaFormBaseSchema {
|
||||
context?: Record<string, string>;
|
||||
}
|
||||
|
||||
export type HaFormVisible = boolean | HaFormCondition | HaFormCondition[];
|
||||
|
||||
export type HaFormConditionOperator =
|
||||
"eq" | "not_eq" | "in" | "not_in" | "exists" | "not_exists";
|
||||
|
||||
|
||||
@@ -15,6 +15,10 @@ export class HaTimeInput extends LitElement {
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
/** Accessible name for the input when it has no visible label. */
|
||||
@property({ attribute: "accessible-label" })
|
||||
public accessibleLabel?: string;
|
||||
|
||||
@property() public helper?: string;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
@@ -61,6 +65,7 @@ export class HaTimeInput extends LitElement {
|
||||
return html`
|
||||
<ha-base-time-input
|
||||
.label=${this.label}
|
||||
.accessibleLabel=${this.accessibleLabel}
|
||||
.hours=${hours}
|
||||
.minutes=${minutes}
|
||||
.seconds=${seconds}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { HaFormVisible } from "../components/ha-form/types";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type { ManualAutomationConfig } from "./automation";
|
||||
import type { ManualScriptConfig } from "./script";
|
||||
@@ -28,7 +27,6 @@ export interface BlueprintInput {
|
||||
description?: string;
|
||||
selector?: Selector;
|
||||
default?: any;
|
||||
visible?: HaFormVisible;
|
||||
}
|
||||
|
||||
export interface BlueprintInputSection {
|
||||
@@ -36,7 +34,6 @@ export interface BlueprintInputSection {
|
||||
icon?: string;
|
||||
description?: string;
|
||||
collapsed?: boolean;
|
||||
visible?: HaFormVisible;
|
||||
input: Record<string, BlueprintInput | null>;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import { fireEvent, type HASSDomEvent } from "../../common/dom/fire_event";
|
||||
import { isNavigationClick } from "../../common/dom/is-navigation-click";
|
||||
import "../../components/ha-alert";
|
||||
import { computeInitialHaFormData } from "../../components/ha-form/compute-initial-ha-form-data";
|
||||
import { getHiddenSchemaFields } from "../../components/ha-form/conditions";
|
||||
import { getHiddenFields } from "../../components/ha-form/conditions";
|
||||
import "../../components/ha-form/ha-form";
|
||||
import type {
|
||||
HaFormSchema,
|
||||
@@ -228,7 +228,7 @@ class StepFlowForm extends LitElement {
|
||||
schema: readonly HaFormSchema[],
|
||||
data: Record<string, any>
|
||||
) => {
|
||||
const hidden = getHiddenSchemaFields(schema, data);
|
||||
const hidden = getHiddenFields(schema, data);
|
||||
return schema.every(
|
||||
(field) =>
|
||||
hidden.has(field.name) ||
|
||||
@@ -259,7 +259,7 @@ class StepFlowForm extends LitElement {
|
||||
|
||||
const flowId = this.step.flow_id;
|
||||
|
||||
const hiddenFields = getHiddenSchemaFields(this.step.data_schema, stepData);
|
||||
const hiddenFields = getHiddenFields(this.step.data_schema, stepData);
|
||||
|
||||
const toSendData: Record<string, unknown> = {};
|
||||
Object.keys(stepData).forEach((key) => {
|
||||
|
||||
@@ -5,14 +5,6 @@ import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import "../../../components/ha-blueprint-picker";
|
||||
import "../../../components/ha-card";
|
||||
import {
|
||||
combineVisible,
|
||||
getHiddenFields,
|
||||
} from "../../../components/ha-form/conditions";
|
||||
import type {
|
||||
HaFormDataContainer,
|
||||
HaFormVisible,
|
||||
} from "../../../components/ha-form/types";
|
||||
import "../../../components/ha-spinner";
|
||||
import "../../../components/ha-markdown";
|
||||
import "../../../components/ha-selector/ha-selector";
|
||||
@@ -21,7 +13,6 @@ import type { BlueprintAutomationConfig } from "../../../data/automation";
|
||||
import type {
|
||||
BlueprintInput,
|
||||
BlueprintInputSection,
|
||||
BlueprintMetaData,
|
||||
BlueprintOrError,
|
||||
Blueprints,
|
||||
} from "../../../data/blueprint";
|
||||
@@ -55,17 +46,19 @@ export abstract class HaBlueprintGenericEditor extends LitElement {
|
||||
}
|
||||
|
||||
// Values of all inputs, including those inside sections, falling back to
|
||||
// their defaults, so selector contexts and visible conditions can name any
|
||||
// other input
|
||||
// their defaults, so a selector context can name any other input
|
||||
private _inputValues = memoizeOne(
|
||||
(
|
||||
input: HaFormDataContainer | undefined,
|
||||
metadataInput: BlueprintMetaData["input"]
|
||||
): HaFormDataContainer => {
|
||||
const values: HaFormDataContainer = {};
|
||||
input: Record<string, unknown> | undefined,
|
||||
metadataInput:
|
||||
| Record<string, BlueprintInput | BlueprintInputSection | null>
|
||||
| undefined
|
||||
): Record<string, unknown> => {
|
||||
const values: Record<string, unknown> = {};
|
||||
const addInputs = (inputs: Record<string, BlueprintInput | null>) => {
|
||||
for (const [key, value] of Object.entries(inputs)) {
|
||||
values[key] = input && key in input ? input[key] : value?.default;
|
||||
values[key] =
|
||||
input && key in input ? input[key] : (value?.default ?? undefined);
|
||||
}
|
||||
};
|
||||
for (const [key, value] of Object.entries(metadataInput ?? {})) {
|
||||
@@ -79,46 +72,6 @@ export abstract class HaBlueprintGenericEditor extends LitElement {
|
||||
}
|
||||
);
|
||||
|
||||
private _hiddenInputs = memoizeOne(
|
||||
(
|
||||
values: HaFormDataContainer,
|
||||
metadataInput: BlueprintMetaData["input"]
|
||||
): Set<string> => {
|
||||
const fields: [string, HaFormVisible | undefined][] = [];
|
||||
for (const [key, value] of Object.entries(metadataInput ?? {})) {
|
||||
if (value && "input" in value) {
|
||||
for (const [inputKey, input] of Object.entries(value.input ?? {})) {
|
||||
fields.push([
|
||||
inputKey,
|
||||
combineVisible(value.visible, input?.visible),
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
fields.push([key, value?.visible]);
|
||||
}
|
||||
}
|
||||
return getHiddenFields(fields, values);
|
||||
}
|
||||
);
|
||||
|
||||
private get _metadataInput() {
|
||||
return this._blueprint && "metadata" in this._blueprint
|
||||
? this._blueprint.metadata.input
|
||||
: undefined;
|
||||
}
|
||||
|
||||
private get _values() {
|
||||
return this._inputValues(
|
||||
// The input holds the values of the inputs by key
|
||||
this._config.use_blueprint.input as HaFormDataContainer | undefined,
|
||||
this._metadataInput
|
||||
);
|
||||
}
|
||||
|
||||
private get _hidden() {
|
||||
return this._hiddenInputs(this._values, this._metadataInput);
|
||||
}
|
||||
|
||||
protected abstract get _config():
|
||||
BlueprintAutomationConfig | BlueprintScriptConfig;
|
||||
|
||||
@@ -177,25 +130,10 @@ export abstract class HaBlueprintGenericEditor extends LitElement {
|
||||
? Object.entries(blueprint.metadata.input).map(
|
||||
([key, value]) => {
|
||||
if (value && "input" in value) {
|
||||
const inputs = Object.entries(
|
||||
value.input ?? {}
|
||||
).filter(
|
||||
([inputKey]) => !this._hidden.has(inputKey)
|
||||
);
|
||||
if (!inputs.length) {
|
||||
return nothing;
|
||||
}
|
||||
const section = this._renderSection(
|
||||
key,
|
||||
value,
|
||||
inputs
|
||||
);
|
||||
const section = this._renderSection(key, value);
|
||||
border = false;
|
||||
return section;
|
||||
}
|
||||
if (this._hidden.has(key)) {
|
||||
return nothing;
|
||||
}
|
||||
const row = this._renderSettingRow(
|
||||
key,
|
||||
value,
|
||||
@@ -217,15 +155,13 @@ export abstract class HaBlueprintGenericEditor extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderSection(
|
||||
sectionKey: string,
|
||||
section: BlueprintInputSection,
|
||||
inputs: [string, BlueprintInput | null][]
|
||||
) {
|
||||
private _renderSection(sectionKey: string, section: BlueprintInputSection) {
|
||||
const title = section?.name || sectionKey;
|
||||
const anyRequired = inputs.some(
|
||||
([, item]) => item === null || item.default === undefined
|
||||
);
|
||||
const anyRequired =
|
||||
section.input &&
|
||||
Object.values(section.input).some(
|
||||
(item) => item === null || item.default === undefined
|
||||
);
|
||||
const expanded = !section.collapsed || anyRequired;
|
||||
|
||||
return html`<ha-expansion-panel
|
||||
@@ -253,7 +189,13 @@ export abstract class HaBlueprintGenericEditor extends LitElement {
|
||||
? html`<ha-markdown .content=${section.description}></ha-markdown>`
|
||||
: nothing
|
||||
}
|
||||
${inputs.map(([key, value]) => this._renderSettingRow(key, value, true))}
|
||||
${
|
||||
section.input
|
||||
? Object.entries(section.input).map(([key, value]) =>
|
||||
this._renderSettingRow(key, value, true)
|
||||
)
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
</ha-expansion-panel>`;
|
||||
}
|
||||
@@ -281,7 +223,17 @@ export abstract class HaBlueprintGenericEditor extends LitElement {
|
||||
.selector=${selector}
|
||||
.context=${
|
||||
value?.selector
|
||||
? resolveSelectorContext(value.selector, this._values)
|
||||
? resolveSelectorContext(
|
||||
value.selector,
|
||||
this._inputValues(
|
||||
// The input holds the values of the inputs by key
|
||||
this._config.use_blueprint.input as
|
||||
Record<string, unknown> | undefined,
|
||||
this._blueprint && "metadata" in this._blueprint
|
||||
? this._blueprint.metadata.input
|
||||
: undefined
|
||||
)
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
.key=${key}
|
||||
|
||||
@@ -44,11 +44,14 @@ class HuiDateEntityRow extends LitElement implements LovelaceRow {
|
||||
|
||||
const unavailable = stateObj.state === UNAVAILABLE;
|
||||
|
||||
const name = this.hass.formatEntityName(stateObj, this._config.name);
|
||||
|
||||
return html`
|
||||
<hui-generic-entity-row .hass=${this.hass} .config=${this._config}>
|
||||
<ha-date-input
|
||||
.locale=${this.hass.locale}
|
||||
.disabled=${unavailable}
|
||||
.accessibleLabel=${name}
|
||||
.value=${
|
||||
stateObj.state === UNAVAILABLE || stateObj.state === UNKNOWN
|
||||
? undefined
|
||||
|
||||
@@ -72,6 +72,7 @@ class HuiInputDatetimeEntityRow extends LitElement implements LovelaceRow {
|
||||
</ha-date-input>
|
||||
<ha-time-input
|
||||
.value=${time}
|
||||
.accessibleLabel=${name}
|
||||
.disabled=${unavailable}
|
||||
.locale=${this.hass.locale}
|
||||
@value-changed=${this._timeChanged}
|
||||
|
||||
@@ -68,6 +68,7 @@ class HuiInputDatetimeEntityRow extends LitElement implements LovelaceRow {
|
||||
? html`
|
||||
<ha-date-input
|
||||
.label=${stateObj.attributes.has_time ? name : undefined}
|
||||
.accessibleLabel=${stateObj.attributes.has_time ? undefined : name}
|
||||
.locale=${this.hass.locale}
|
||||
.disabled=${stateObj.state === UNAVAILABLE}
|
||||
.value=${stateToIsoDateString(stateObj)}
|
||||
@@ -88,6 +89,7 @@ class HuiInputDatetimeEntityRow extends LitElement implements LovelaceRow {
|
||||
? stateObj.state.split(" ")[1]
|
||||
: stateObj.state
|
||||
}
|
||||
.accessibleLabel=${name}
|
||||
.locale=${this.hass.locale}
|
||||
.disabled=${stateObj.state === UNAVAILABLE}
|
||||
@value-changed=${this._timeChanged}
|
||||
|
||||
@@ -44,6 +44,8 @@ class HuiTimeEntityRow extends LitElement implements LovelaceRow {
|
||||
|
||||
const unavailable = stateObj.state === UNAVAILABLE;
|
||||
|
||||
const name = this.hass.formatEntityName(stateObj, this._config.name);
|
||||
|
||||
return html`
|
||||
<hui-generic-entity-row .hass=${this.hass} .config=${this._config}>
|
||||
<ha-time-input
|
||||
@@ -52,6 +54,7 @@ class HuiTimeEntityRow extends LitElement implements LovelaceRow {
|
||||
? undefined
|
||||
: stateObj.state
|
||||
}
|
||||
.accessibleLabel=${name}
|
||||
.locale=${this.hass.locale}
|
||||
.disabled=${unavailable}
|
||||
@value-changed=${this._timeChanged}
|
||||
|
||||
@@ -29,6 +29,7 @@ const marketplaceTableParams = {
|
||||
source: "string?",
|
||||
sort: "string?",
|
||||
direction: "string?",
|
||||
group: "string?",
|
||||
} satisfies Redirect["params"];
|
||||
|
||||
export const getMyRedirects = (): Redirects => ({
|
||||
|
||||
@@ -2,111 +2,106 @@ import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
getHiddenFields,
|
||||
getHiddenSchemaFields,
|
||||
isVisible,
|
||||
isFieldVisible,
|
||||
} from "../../../src/components/ha-form/conditions";
|
||||
import type {
|
||||
HaFormSchema,
|
||||
HaFormVisible,
|
||||
} from "../../../src/components/ha-form/types";
|
||||
import type { HaFormSchema } from "../../../src/components/ha-form/types";
|
||||
|
||||
describe("isVisible", () => {
|
||||
const field = (visible: HaFormSchema["visible"]): HaFormSchema =>
|
||||
({ name: "field", selector: { text: {} }, visible }) as HaFormSchema;
|
||||
|
||||
describe("isFieldVisible", () => {
|
||||
it("shows a field without a visible condition", () => {
|
||||
expect(isVisible(undefined, { a: 1 })).toBe(true);
|
||||
expect(isFieldVisible(field(undefined), { a: 1 })).toBe(true);
|
||||
});
|
||||
|
||||
it("honors a boolean visible", () => {
|
||||
expect(isVisible(true, {})).toBe(true);
|
||||
expect(isVisible(false, {})).toBe(false);
|
||||
expect(isFieldVisible(field(true), {})).toBe(true);
|
||||
expect(isFieldVisible(field(false), {})).toBe(false);
|
||||
});
|
||||
|
||||
describe("operators", () => {
|
||||
it("eq (default) matches equal values", () => {
|
||||
expect(isVisible({ field: "a", value: 1 }, { a: 1 })).toBe(true);
|
||||
expect(isVisible({ field: "a", value: 1 }, { a: 2 })).toBe(false);
|
||||
expect(isFieldVisible(field({ field: "a", value: 1 }), { a: 1 })).toBe(
|
||||
true
|
||||
);
|
||||
expect(isFieldVisible(field({ field: "a", value: 1 }), { a: 2 })).toBe(
|
||||
false
|
||||
);
|
||||
});
|
||||
|
||||
it("not_eq matches different values", () => {
|
||||
const visible: HaFormVisible = {
|
||||
field: "a",
|
||||
operator: "not_eq",
|
||||
value: 1,
|
||||
};
|
||||
expect(isVisible(visible, { a: 2 })).toBe(true);
|
||||
expect(isVisible(visible, { a: 1 })).toBe(false);
|
||||
const schema = field({ field: "a", operator: "not_eq", value: 1 });
|
||||
expect(isFieldVisible(schema, { a: 2 })).toBe(true);
|
||||
expect(isFieldVisible(schema, { a: 1 })).toBe(false);
|
||||
});
|
||||
|
||||
it("in matches membership", () => {
|
||||
const visible: HaFormVisible = {
|
||||
field: "a",
|
||||
operator: "in",
|
||||
value: ["x", "y"],
|
||||
};
|
||||
expect(isVisible(visible, { a: "y" })).toBe(true);
|
||||
expect(isVisible(visible, { a: "z" })).toBe(false);
|
||||
const schema = field({ field: "a", operator: "in", value: ["x", "y"] });
|
||||
expect(isFieldVisible(schema, { a: "y" })).toBe(true);
|
||||
expect(isFieldVisible(schema, { a: "z" })).toBe(false);
|
||||
});
|
||||
|
||||
it("not_in matches non-membership", () => {
|
||||
const visible: HaFormVisible = {
|
||||
const schema = field({
|
||||
field: "a",
|
||||
operator: "not_in",
|
||||
value: ["x", "y"],
|
||||
};
|
||||
expect(isVisible(visible, { a: "z" })).toBe(true);
|
||||
expect(isVisible(visible, { a: "x" })).toBe(false);
|
||||
});
|
||||
expect(isFieldVisible(schema, { a: "z" })).toBe(true);
|
||||
expect(isFieldVisible(schema, { a: "x" })).toBe(false);
|
||||
});
|
||||
|
||||
it("exists matches a defined non-empty value", () => {
|
||||
const visible: HaFormVisible = { field: "a", operator: "exists" };
|
||||
expect(isVisible(visible, { a: "x" })).toBe(true);
|
||||
expect(isVisible(visible, { a: "" })).toBe(false);
|
||||
expect(isVisible(visible, {})).toBe(false);
|
||||
const schema = field({ field: "a", operator: "exists" });
|
||||
expect(isFieldVisible(schema, { a: "x" })).toBe(true);
|
||||
expect(isFieldVisible(schema, { a: "" })).toBe(false);
|
||||
expect(isFieldVisible(schema, {})).toBe(false);
|
||||
});
|
||||
|
||||
it("not_exists matches a missing or empty value", () => {
|
||||
const visible: HaFormVisible = { field: "a", operator: "not_exists" };
|
||||
expect(isVisible(visible, {})).toBe(true);
|
||||
expect(isVisible(visible, { a: null } as any)).toBe(true);
|
||||
expect(isVisible(visible, { a: "x" })).toBe(false);
|
||||
const schema = field({ field: "a", operator: "not_exists" });
|
||||
expect(isFieldVisible(schema, {})).toBe(true);
|
||||
expect(isFieldVisible(schema, { a: null } as any)).toBe(true);
|
||||
expect(isFieldVisible(schema, { a: "x" })).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("combinators", () => {
|
||||
it("and requires every condition", () => {
|
||||
const visible: HaFormVisible = {
|
||||
const schema = field({
|
||||
condition: "and",
|
||||
conditions: [
|
||||
{ field: "a", value: 1 },
|
||||
{ field: "b", value: 2 },
|
||||
],
|
||||
};
|
||||
expect(isVisible(visible, { a: 1, b: 2 })).toBe(true);
|
||||
expect(isVisible(visible, { a: 1, b: 9 })).toBe(false);
|
||||
});
|
||||
expect(isFieldVisible(schema, { a: 1, b: 2 })).toBe(true);
|
||||
expect(isFieldVisible(schema, { a: 1, b: 9 })).toBe(false);
|
||||
});
|
||||
|
||||
it("or requires any condition", () => {
|
||||
const visible: HaFormVisible = {
|
||||
const schema = field({
|
||||
condition: "or",
|
||||
conditions: [
|
||||
{ field: "a", value: 1 },
|
||||
{ field: "b", value: 2 },
|
||||
],
|
||||
};
|
||||
expect(isVisible(visible, { a: 9, b: 2 })).toBe(true);
|
||||
expect(isVisible(visible, { a: 9, b: 9 })).toBe(false);
|
||||
});
|
||||
expect(isFieldVisible(schema, { a: 9, b: 2 })).toBe(true);
|
||||
expect(isFieldVisible(schema, { a: 9, b: 9 })).toBe(false);
|
||||
});
|
||||
|
||||
it("not negates its conditions", () => {
|
||||
const visible: HaFormVisible = {
|
||||
const schema = field({
|
||||
condition: "not",
|
||||
conditions: [{ field: "a", value: 1 }],
|
||||
};
|
||||
expect(isVisible(visible, { a: 2 })).toBe(true);
|
||||
expect(isVisible(visible, { a: 1 })).toBe(false);
|
||||
});
|
||||
expect(isFieldVisible(schema, { a: 2 })).toBe(true);
|
||||
expect(isFieldVisible(schema, { a: 1 })).toBe(false);
|
||||
});
|
||||
|
||||
it("nests combinators", () => {
|
||||
const visible: HaFormVisible = {
|
||||
const schema = field({
|
||||
condition: "and",
|
||||
conditions: [
|
||||
{ field: "a", value: 1 },
|
||||
@@ -118,65 +113,57 @@ describe("isVisible", () => {
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
expect(isVisible(visible, { a: 1, b: 9, c: 3 })).toBe(true);
|
||||
expect(isVisible(visible, { a: 1, b: 9, c: 9 })).toBe(false);
|
||||
expect(isVisible(visible, { a: 9, b: 2, c: 3 })).toBe(false);
|
||||
});
|
||||
expect(isFieldVisible(schema, { a: 1, b: 9, c: 3 })).toBe(true);
|
||||
expect(isFieldVisible(schema, { a: 1, b: 9, c: 9 })).toBe(false);
|
||||
expect(isFieldVisible(schema, { a: 9, b: 2, c: 3 })).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it("treats an array of conditions as AND", () => {
|
||||
const visible: HaFormVisible = [
|
||||
const schema = field([
|
||||
{ field: "a", value: 1 },
|
||||
{ field: "b", value: 2 },
|
||||
];
|
||||
expect(isVisible(visible, { a: 1, b: 2 })).toBe(true);
|
||||
expect(isVisible(visible, { a: 1, b: 9 })).toBe(false);
|
||||
]);
|
||||
expect(isFieldVisible(schema, { a: 1, b: 2 })).toBe(true);
|
||||
expect(isFieldVisible(schema, { a: 1, b: 9 })).toBe(false);
|
||||
});
|
||||
|
||||
it("handles missing data", () => {
|
||||
expect(isVisible({ field: "a", value: 1 }, undefined)).toBe(false);
|
||||
expect(isFieldVisible(field({ field: "a", value: 1 }), undefined)).toBe(
|
||||
false
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getHiddenFields", () => {
|
||||
const named = (
|
||||
name: string,
|
||||
visible?: HaFormSchema["visible"]
|
||||
): HaFormSchema =>
|
||||
({ name, selector: { text: {} }, visible }) as HaFormSchema;
|
||||
|
||||
it("collects the fields that are not visible", () => {
|
||||
const fields = [
|
||||
["mode", undefined],
|
||||
["token", { field: "mode", value: "advanced" }],
|
||||
] as const;
|
||||
expect([...getHiddenFields(fields, { mode: "simple" })]).toEqual(["token"]);
|
||||
expect([...getHiddenFields(fields, { mode: "advanced" })]).toEqual([]);
|
||||
const schema = [
|
||||
named("mode"),
|
||||
named("token", { field: "mode", value: "advanced" }),
|
||||
];
|
||||
expect([...getHiddenFields(schema, { mode: "simple" })]).toEqual(["token"]);
|
||||
expect([...getHiddenFields(schema, { mode: "advanced" })]).toEqual([]);
|
||||
});
|
||||
|
||||
it("treats a hidden field as absent to the other conditions", () => {
|
||||
// "token" is hidden, so its value must not satisfy the condition on "extra"
|
||||
const fields = [
|
||||
["mode", undefined],
|
||||
["token", { field: "mode", value: "advanced" }],
|
||||
["extra", { field: "token", operator: "exists" }],
|
||||
] as const;
|
||||
const schema = [
|
||||
named("mode"),
|
||||
named("token", { field: "mode", value: "advanced" }),
|
||||
named("extra", { field: "token", operator: "exists" }),
|
||||
];
|
||||
expect([
|
||||
...getHiddenFields(fields, { mode: "simple", token: "stale" }),
|
||||
...getHiddenFields(schema, { mode: "simple", token: "stale" }),
|
||||
]).toEqual(["token", "extra"]);
|
||||
expect([
|
||||
...getHiddenFields(fields, { mode: "advanced", token: "kept" }),
|
||||
...getHiddenFields(schema, { mode: "advanced", token: "kept" }),
|
||||
]).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getHiddenSchemaFields", () => {
|
||||
it("reads the visible condition of each schema field", () => {
|
||||
const schema = [
|
||||
{ name: "mode", selector: { text: {} } },
|
||||
{
|
||||
name: "token",
|
||||
selector: { text: {} },
|
||||
visible: { field: "mode", value: "advanced" },
|
||||
},
|
||||
] as HaFormSchema[];
|
||||
expect([...getHiddenSchemaFields(schema, { mode: "simple" })]).toEqual([
|
||||
"token",
|
||||
]);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user