mirror of
https://github.com/home-assistant/frontend.git
synced 2026-04-28 13:33:11 +00:00
Compare commits
6 Commits
automation
...
selector-t
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5f09129533 | ||
|
|
8215a124b9 | ||
|
|
94187923c7 | ||
|
|
6a8a3f3628 | ||
|
|
4a9e20c731 | ||
|
|
595a45202e |
@@ -50,6 +50,10 @@ class DeveloperToolsRouter extends HassRouterPage {
|
||||
tag: "developer-tools-assist",
|
||||
load: () => import("./assist/developer-tools-assist"),
|
||||
},
|
||||
selectors: {
|
||||
tag: "developer-tools-selectors",
|
||||
load: () => import("./selectors/developer-tools-selectors"),
|
||||
},
|
||||
debug: {
|
||||
tag: "developer-tools-debug",
|
||||
load: () => import("./debug/developer-tools-debug"),
|
||||
|
||||
@@ -55,6 +55,11 @@ class PanelDeveloperTools extends LitElement {
|
||||
"ui.panel.config.developer-tools.tabs.debug.title"
|
||||
)}
|
||||
</ha-dropdown-item>
|
||||
<ha-dropdown-item value="selectors">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.developer-tools.tabs.selectors.title"
|
||||
)}
|
||||
</ha-dropdown-item>
|
||||
</ha-dropdown>
|
||||
</div>
|
||||
<ha-tab-group @wa-tab-show=${this._handlePageSelected}>
|
||||
@@ -138,8 +143,8 @@ class PanelDeveloperTools extends LitElement {
|
||||
|
||||
private async _handleMenuAction(ev: HaDropdownSelectEvent) {
|
||||
const action = ev.detail.item.value;
|
||||
if (action === "debug") {
|
||||
navigate(`/config/developer-tools/debug`);
|
||||
if (action === "debug" || action === "selectors") {
|
||||
navigate(`/config/developer-tools/${action}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,596 @@
|
||||
import { mdiContentCopy } from "@mdi/js";
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { dump } from "js-yaml";
|
||||
import { copyToClipboard } from "../../../../common/util/copy-clipboard";
|
||||
import "../../../../components/ha-alert";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-card";
|
||||
import "../../../../components/ha-code-editor";
|
||||
import "../../../../components/ha-generic-picker";
|
||||
import "../../../../components/ha-icon-button";
|
||||
import "../../../../components/ha-md-list";
|
||||
import "../../../../components/ha-md-list-item";
|
||||
import "../../../../components/ha-switch";
|
||||
import "../../../../components/ha-yaml-editor";
|
||||
import "../../../../components/ha-selector/ha-selector";
|
||||
import type { PickerComboBoxItem } from "../../../../components/ha-picker-combo-box";
|
||||
import type { PickerValueRenderer } from "../../../../components/ha-picker-field";
|
||||
import type { Selector } from "../../../../data/selector";
|
||||
import { haStyle } from "../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import { showToast } from "../../../../util/toast";
|
||||
import type { SelectorKey } from "./presets";
|
||||
import {
|
||||
SELECTOR_OPTIONS,
|
||||
SELECTOR_PRESETS,
|
||||
formatSelectorName,
|
||||
getInitialConfig,
|
||||
getVariantGroups,
|
||||
getVariants,
|
||||
} from "./presets";
|
||||
|
||||
const formatYaml = (value: unknown): string => {
|
||||
if (value === undefined) {
|
||||
return "";
|
||||
}
|
||||
try {
|
||||
return dump(value, { quotingType: '"', noRefs: true, lineWidth: 100 });
|
||||
} catch (_err) {
|
||||
return String(value);
|
||||
}
|
||||
};
|
||||
|
||||
@customElement("developer-tools-selectors")
|
||||
class DeveloperToolsSelectors extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@state() private _type: SelectorKey = "select";
|
||||
|
||||
@state() private _variantId: string =
|
||||
getVariants("select")[0]?.id ?? "default";
|
||||
|
||||
@state() private _config: Record<string, unknown> =
|
||||
getInitialConfig("select");
|
||||
|
||||
@state() private _configValid = true;
|
||||
|
||||
@state() private _value: unknown;
|
||||
|
||||
@state() private _label = true;
|
||||
|
||||
@state() private _helper = false;
|
||||
|
||||
@state() private _required = false;
|
||||
|
||||
@state() private _disabled = false;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const selector = { [this._type]: this._config } as unknown as Selector;
|
||||
const configYaml = formatYaml(this._buildFullConfig(selector)).trimEnd();
|
||||
const variants = getVariants(this._type);
|
||||
const groups = getVariantGroups(this._type);
|
||||
const showVariants = variants.length > 1;
|
||||
const variantSections = groups.map((g) => ({
|
||||
id: g.id,
|
||||
label: g.label,
|
||||
}));
|
||||
return html`
|
||||
<div class="content">
|
||||
<ha-card header=${this._localizeTitle()}>
|
||||
<div class="card-content">
|
||||
<p class="description">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.developer-tools.tabs.selectors.description"
|
||||
)}
|
||||
</p>
|
||||
|
||||
<div class="grid">
|
||||
<div class="config-pane">
|
||||
<ha-generic-picker
|
||||
.hass=${this.hass}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.developer-tools.tabs.selectors.selector_type"
|
||||
)}
|
||||
.value=${this._type}
|
||||
.getItems=${this._getTypeItems}
|
||||
.valueRenderer=${this._typeValueRenderer}
|
||||
@value-changed=${this._typePicked}
|
||||
></ha-generic-picker>
|
||||
${showVariants
|
||||
? html`
|
||||
<ha-generic-picker
|
||||
.hass=${this.hass}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.developer-tools.tabs.selectors.preset"
|
||||
)}
|
||||
.value=${this._variantId}
|
||||
.getItems=${this._getVariantItems}
|
||||
.valueRenderer=${this._variantValueRenderer}
|
||||
.sections=${variantSections.length
|
||||
? variantSections
|
||||
: undefined}
|
||||
@value-changed=${this._variantPicked}
|
||||
></ha-generic-picker>
|
||||
`
|
||||
: nothing}
|
||||
|
||||
<ha-md-list class="toggles">
|
||||
<ha-md-list-item>
|
||||
<span slot="headline">Label</span>
|
||||
<span slot="supporting-text"
|
||||
>Pass the selector type as the field label.</span
|
||||
>
|
||||
<ha-switch
|
||||
slot="end"
|
||||
.name=${"label"}
|
||||
.checked=${this._label}
|
||||
@change=${this._toggleChanged}
|
||||
></ha-switch>
|
||||
</ha-md-list-item>
|
||||
<ha-md-list-item>
|
||||
<span slot="headline">Helper</span>
|
||||
<span slot="supporting-text"
|
||||
>Show a helper text below the field.</span
|
||||
>
|
||||
<ha-switch
|
||||
slot="end"
|
||||
.name=${"helper"}
|
||||
.checked=${this._helper}
|
||||
@change=${this._toggleChanged}
|
||||
></ha-switch>
|
||||
</ha-md-list-item>
|
||||
<ha-md-list-item>
|
||||
<span slot="headline">Required</span>
|
||||
<span slot="supporting-text"
|
||||
>Mark the selector as required.</span
|
||||
>
|
||||
<ha-switch
|
||||
slot="end"
|
||||
.name=${"required"}
|
||||
.checked=${this._required}
|
||||
@change=${this._toggleChanged}
|
||||
></ha-switch>
|
||||
</ha-md-list-item>
|
||||
<ha-md-list-item>
|
||||
<span slot="headline">Disabled</span>
|
||||
<span slot="supporting-text"
|
||||
>Render the selector in its disabled state.</span
|
||||
>
|
||||
<ha-switch
|
||||
slot="end"
|
||||
.name=${"disabled"}
|
||||
.checked=${this._disabled}
|
||||
@change=${this._toggleChanged}
|
||||
></ha-switch>
|
||||
</ha-md-list-item>
|
||||
</ha-md-list>
|
||||
|
||||
<div class="field">
|
||||
<div class="field-label">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.developer-tools.tabs.selectors.configuration"
|
||||
)}
|
||||
</div>
|
||||
<ha-yaml-editor
|
||||
.hass=${this.hass}
|
||||
.value=${this._config}
|
||||
@value-changed=${this._configChanged}
|
||||
></ha-yaml-editor>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<ha-button @click=${this._resetConfig} appearance="plain">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.developer-tools.tabs.selectors.reset_config"
|
||||
)}
|
||||
</ha-button>
|
||||
<ha-button @click=${this._resetValue} appearance="plain">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.developer-tools.tabs.selectors.reset_value"
|
||||
)}
|
||||
</ha-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="preview-pane">
|
||||
<div class="field">
|
||||
<div class="field-label">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.developer-tools.tabs.selectors.preview"
|
||||
)}
|
||||
</div>
|
||||
<div class="preview">
|
||||
${this._configValid
|
||||
? html`
|
||||
<ha-selector
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.selector=${selector}
|
||||
.value=${this._value}
|
||||
.label=${this._label
|
||||
? formatSelectorName(this._type)
|
||||
: undefined}
|
||||
.helper=${this._helper
|
||||
? "Example helper text"
|
||||
: undefined}
|
||||
.required=${this._required}
|
||||
.disabled=${this._disabled}
|
||||
.localizeValue=${this._localizeValue}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-selector>
|
||||
`
|
||||
: html`
|
||||
<ha-alert alert-type="warning">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.developer-tools.tabs.selectors.invalid_config"
|
||||
)}
|
||||
</ha-alert>
|
||||
`}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<div class="field-header">
|
||||
<div class="field-label">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.developer-tools.tabs.selectors.configuration_used"
|
||||
)}
|
||||
</div>
|
||||
<ha-icon-button
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.developer-tools.tabs.selectors.copy_configuration"
|
||||
)}
|
||||
.path=${mdiContentCopy}
|
||||
.disabled=${!this._configValid}
|
||||
@click=${this._copyConfig}
|
||||
></ha-icon-button>
|
||||
</div>
|
||||
<ha-code-editor
|
||||
class="readonly-editor"
|
||||
mode="yaml"
|
||||
.hass=${this.hass}
|
||||
.value=${configYaml}
|
||||
read-only
|
||||
dir="ltr"
|
||||
></ha-code-editor>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<div class="field-label">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.developer-tools.tabs.selectors.value"
|
||||
)}
|
||||
</div>
|
||||
${this._value === undefined || this._value === null
|
||||
? html`<div class="empty-value">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.developer-tools.tabs.selectors.no_value"
|
||||
)}
|
||||
</div>`
|
||||
: html`<ha-code-editor
|
||||
class="readonly-editor"
|
||||
mode="yaml"
|
||||
.hass=${this.hass}
|
||||
.value=${formatYaml(this._value).trimEnd()}
|
||||
read-only
|
||||
dir="ltr"
|
||||
></ha-code-editor>`}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ha-card>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _copyConfig() {
|
||||
const selector = { [this._type]: this._config } as unknown as Selector;
|
||||
const yaml = formatYaml(this._buildFullConfig(selector)).trimEnd();
|
||||
if (!yaml) {
|
||||
return;
|
||||
}
|
||||
await copyToClipboard(yaml);
|
||||
showToast(this, {
|
||||
message: this.hass.localize("ui.common.copied_clipboard"),
|
||||
});
|
||||
}
|
||||
|
||||
// Mirrors the props flipped by the switches so the YAML output matches what
|
||||
// the preview is rendering. Ordered so the selector body stays at the bottom.
|
||||
private _buildFullConfig(selector: Selector): Record<string, unknown> {
|
||||
const config: Record<string, unknown> = {};
|
||||
if (this._label) {
|
||||
config.label = formatSelectorName(this._type);
|
||||
}
|
||||
if (this._helper) {
|
||||
config.helper = "Example helper text";
|
||||
}
|
||||
if (this._required) {
|
||||
config.required = true;
|
||||
}
|
||||
if (this._disabled) {
|
||||
config.disabled = true;
|
||||
}
|
||||
config.selector = selector;
|
||||
return config;
|
||||
}
|
||||
|
||||
private _localizeTitle() {
|
||||
return this.hass.localize(
|
||||
"ui.panel.config.developer-tools.tabs.selectors.title"
|
||||
);
|
||||
}
|
||||
|
||||
private _typePicked(ev: CustomEvent<{ value: string | undefined }>) {
|
||||
ev.stopPropagation();
|
||||
const newType = ev.detail?.value as SelectorKey | undefined;
|
||||
if (!newType || newType === this._type) {
|
||||
return;
|
||||
}
|
||||
this._type = newType;
|
||||
const variants = getVariants(newType);
|
||||
this._variantId = variants[0]?.id ?? "default";
|
||||
this._config = { ...(variants[0]?.config ?? {}) };
|
||||
this._configValid = true;
|
||||
this._value = undefined;
|
||||
}
|
||||
|
||||
private _getTypeItems = (searchString?: string): PickerComboBoxItem[] => {
|
||||
const lowerSearch = searchString?.toLowerCase();
|
||||
const items = lowerSearch
|
||||
? SELECTOR_OPTIONS.filter(
|
||||
(o) =>
|
||||
o.value.toLowerCase().includes(lowerSearch) ||
|
||||
o.label.toLowerCase().includes(lowerSearch)
|
||||
)
|
||||
: SELECTOR_OPTIONS;
|
||||
return items.map((o) => ({
|
||||
id: o.value,
|
||||
primary: o.label,
|
||||
secondary: o.value,
|
||||
sorting_label: o.label,
|
||||
}));
|
||||
};
|
||||
|
||||
private _typeValueRenderer: PickerValueRenderer = (value) => {
|
||||
const option = SELECTOR_OPTIONS.find((o) => o.value === value);
|
||||
return html`<span slot="headline">${option?.label ?? value}</span>`;
|
||||
};
|
||||
|
||||
private _variantPicked(ev: CustomEvent<{ value: string | undefined }>) {
|
||||
ev.stopPropagation();
|
||||
const newVariantId = ev.detail?.value;
|
||||
if (!newVariantId || newVariantId === this._variantId) {
|
||||
return;
|
||||
}
|
||||
const variant = getVariants(this._type).find((v) => v.id === newVariantId);
|
||||
if (!variant) {
|
||||
return;
|
||||
}
|
||||
this._variantId = newVariantId;
|
||||
this._config = { ...variant.config };
|
||||
this._configValid = true;
|
||||
this._value = undefined;
|
||||
}
|
||||
|
||||
// Feeds items (with string section headers) into ha-generic-picker's list.
|
||||
// When `section` is set the user has clicked a section filter button and we
|
||||
// emit only that group's variants with no header.
|
||||
private _getVariantItems = (
|
||||
searchString?: string,
|
||||
section?: string
|
||||
): (PickerComboBoxItem | string)[] => {
|
||||
const groups = getVariantGroups(this._type);
|
||||
const lowerSearch = searchString?.toLowerCase();
|
||||
const items: (PickerComboBoxItem | string)[] = [];
|
||||
|
||||
if (!groups.length) {
|
||||
return getVariants(this._type).map((v) => ({
|
||||
id: v.id,
|
||||
primary: v.name,
|
||||
sorting_label: v.name,
|
||||
}));
|
||||
}
|
||||
|
||||
for (const group of groups) {
|
||||
if (section && section !== group.id) {
|
||||
continue;
|
||||
}
|
||||
const matching = lowerSearch
|
||||
? group.variants.filter((v) =>
|
||||
v.name.toLowerCase().includes(lowerSearch)
|
||||
)
|
||||
: group.variants;
|
||||
if (!matching.length) {
|
||||
continue;
|
||||
}
|
||||
if (!section) {
|
||||
items.push(group.label);
|
||||
}
|
||||
for (const v of matching) {
|
||||
items.push({
|
||||
id: v.id,
|
||||
primary: v.name,
|
||||
sorting_label: v.name,
|
||||
});
|
||||
}
|
||||
}
|
||||
return items;
|
||||
};
|
||||
|
||||
private _variantValueRenderer: PickerValueRenderer = (value) => {
|
||||
const variant = getVariants(this._type).find((v) => v.id === value);
|
||||
return html`<span slot="headline">${variant?.name ?? value}</span>`;
|
||||
};
|
||||
|
||||
private _configChanged(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
const { value, isValid } = ev.detail as {
|
||||
value: Record<string, unknown>;
|
||||
isValid: boolean;
|
||||
};
|
||||
this._configValid = isValid;
|
||||
if (!isValid) {
|
||||
return;
|
||||
}
|
||||
this._config = value ?? {};
|
||||
}
|
||||
|
||||
private _valueChanged(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
this._value = ev.detail?.value;
|
||||
}
|
||||
|
||||
// Fake localizer so presets that set `translation_key` show a visible effect
|
||||
// in the preview. `ha-selector-select` calls this with
|
||||
// `<translation_key>.options.<value>` and swaps the option label with the
|
||||
// returned string when truthy.
|
||||
private _localizeValue = (key: string): string => {
|
||||
const match = key.match(/^demo_select\.options\.(.+)$/);
|
||||
if (!match) {
|
||||
return "";
|
||||
}
|
||||
const value = match[1];
|
||||
const labels: Record<string, string> = {
|
||||
option_1: "Living room",
|
||||
option_2: "Kitchen",
|
||||
option_3: "Bedroom",
|
||||
option_4: "Office",
|
||||
};
|
||||
return labels[value] ?? `Localized: ${value}`;
|
||||
};
|
||||
|
||||
private _toggleChanged(ev: Event) {
|
||||
const target = ev.target as HTMLInputElement & { name: string };
|
||||
const key = `_${target.name}` as
|
||||
| "_label"
|
||||
| "_helper"
|
||||
| "_required"
|
||||
| "_disabled";
|
||||
this[key] = target.checked;
|
||||
}
|
||||
|
||||
private _resetConfig() {
|
||||
const variant = getVariants(this._type).find(
|
||||
(v) => v.id === this._variantId
|
||||
);
|
||||
this._config = {
|
||||
...(variant?.config ?? SELECTOR_PRESETS[this._type] ?? {}),
|
||||
};
|
||||
this._configValid = true;
|
||||
}
|
||||
|
||||
private _resetValue() {
|
||||
this._value = undefined;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
.content {
|
||||
padding: var(--ha-space-4);
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.description {
|
||||
margin-top: 0;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: var(--ha-space-5);
|
||||
}
|
||||
@media (max-width: 870px) {
|
||||
.grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
.config-pane,
|
||||
.preview-pane {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--ha-space-4);
|
||||
min-width: 0;
|
||||
}
|
||||
ha-generic-picker {
|
||||
display: block;
|
||||
}
|
||||
.toggles {
|
||||
padding: 0;
|
||||
background: none;
|
||||
border: 1px solid var(--divider-color);
|
||||
border-radius: var(--ha-border-radius-md, 8px);
|
||||
overflow: hidden;
|
||||
}
|
||||
.field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--ha-space-2);
|
||||
min-width: 0;
|
||||
}
|
||||
.field-label {
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
color: var(--secondary-text-color);
|
||||
font-size: var(--ha-font-size-s);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
.field-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--ha-space-2);
|
||||
}
|
||||
.field-header .field-label {
|
||||
flex: 1;
|
||||
}
|
||||
.field-header ha-icon-button {
|
||||
--mdc-icon-button-size: 32px;
|
||||
--mdc-icon-size: 18px;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
.preview {
|
||||
padding: var(--ha-space-3);
|
||||
border: 1px solid var(--divider-color);
|
||||
border-radius: var(--ha-border-radius-md, 8px);
|
||||
background: var(--card-background-color);
|
||||
min-height: 56px;
|
||||
}
|
||||
.readonly-editor {
|
||||
display: block;
|
||||
border: 1px solid var(--divider-color);
|
||||
border-radius: var(--ha-border-radius-md, 8px);
|
||||
overflow: hidden;
|
||||
}
|
||||
.empty-value {
|
||||
padding: var(--ha-space-3);
|
||||
border: 1px solid var(--divider-color);
|
||||
border-radius: var(--ha-border-radius-md, 8px);
|
||||
color: var(--secondary-text-color);
|
||||
font-style: italic;
|
||||
}
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: var(--ha-space-2);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"developer-tools-selectors": DeveloperToolsSelectors;
|
||||
}
|
||||
}
|
||||
200
src/panels/config/developer-tools/selectors/presets/index.ts
Normal file
200
src/panels/config/developer-tools/selectors/presets/index.ts
Normal file
@@ -0,0 +1,200 @@
|
||||
// Shared preset data for the developer tools Selectors test page.
|
||||
//
|
||||
// Each selector type has a default config in SELECTOR_PRESETS. Types that have
|
||||
// multiple meaningful configurations worth side-by-side testing expose named
|
||||
// variants via SELECTOR_VARIANT_GROUPS; groups become section filter buttons
|
||||
// and section headers in the `ha-generic-picker` preset picker. Types without
|
||||
// an entry there fall back to a single "Default" variant built from
|
||||
// SELECTOR_PRESETS.
|
||||
//
|
||||
// Per-type variant groups live in sibling files (e.g. ./select.ts) and are
|
||||
// aggregated into SELECTOR_VARIANT_GROUPS below.
|
||||
|
||||
import { SELECT_DEFAULT_CONFIG, SELECT_VARIANT_GROUPS } from "./select";
|
||||
|
||||
export type SelectorKey =
|
||||
| "action"
|
||||
| "addon"
|
||||
| "app"
|
||||
| "area"
|
||||
| "areas_display"
|
||||
| "assist_pipeline"
|
||||
| "attribute"
|
||||
| "backup_location"
|
||||
| "boolean"
|
||||
| "button_toggle"
|
||||
| "choose"
|
||||
| "color_rgb"
|
||||
| "color_temp"
|
||||
| "condition"
|
||||
| "config_entry"
|
||||
| "constant"
|
||||
| "conversation_agent"
|
||||
| "country"
|
||||
| "date"
|
||||
| "datetime"
|
||||
| "device"
|
||||
| "duration"
|
||||
| "entity"
|
||||
| "entity_name"
|
||||
| "file"
|
||||
| "floor"
|
||||
| "icon"
|
||||
| "label"
|
||||
| "language"
|
||||
| "location"
|
||||
| "media"
|
||||
| "navigation"
|
||||
| "number"
|
||||
| "numeric_threshold"
|
||||
| "object"
|
||||
| "period"
|
||||
| "qr_code"
|
||||
| "select"
|
||||
| "selector"
|
||||
| "serial_port"
|
||||
| "state"
|
||||
| "statistic"
|
||||
| "stt"
|
||||
| "target"
|
||||
| "template"
|
||||
| "text"
|
||||
| "theme"
|
||||
| "time"
|
||||
| "timezone"
|
||||
| "trigger"
|
||||
| "tts"
|
||||
| "tts_voice"
|
||||
| "ui_action"
|
||||
| "ui_color"
|
||||
| "ui_state_content";
|
||||
|
||||
export interface SelectorVariant {
|
||||
id: string;
|
||||
name: string;
|
||||
config: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface SelectorVariantGroup {
|
||||
id: string;
|
||||
label: string;
|
||||
variants: SelectorVariant[];
|
||||
}
|
||||
|
||||
export const SELECTOR_PRESETS: Record<SelectorKey, Record<string, unknown>> = {
|
||||
action: {},
|
||||
addon: {},
|
||||
app: {},
|
||||
area: {},
|
||||
areas_display: {},
|
||||
assist_pipeline: {},
|
||||
attribute: { entity_id: "" },
|
||||
backup_location: {},
|
||||
boolean: {},
|
||||
button_toggle: {
|
||||
options: [
|
||||
{ value: "on", label: "On" },
|
||||
{ value: "off", label: "Off" },
|
||||
],
|
||||
},
|
||||
choose: {
|
||||
choices: {
|
||||
number: { selector: { number: { min: 0, max: 100 } } },
|
||||
text: { selector: { text: {} } },
|
||||
},
|
||||
},
|
||||
color_rgb: {},
|
||||
color_temp: {},
|
||||
condition: {},
|
||||
config_entry: {},
|
||||
constant: { value: true, label: "Enabled" },
|
||||
conversation_agent: {},
|
||||
country: {},
|
||||
date: {},
|
||||
datetime: {},
|
||||
device: {},
|
||||
duration: {},
|
||||
entity: {},
|
||||
entity_name: {},
|
||||
file: { accept: "*" },
|
||||
floor: {},
|
||||
icon: {},
|
||||
label: {},
|
||||
language: {},
|
||||
location: {},
|
||||
media: {},
|
||||
navigation: {},
|
||||
number: { min: 0, max: 100, mode: "slider", step: 1 },
|
||||
numeric_threshold: {},
|
||||
object: {},
|
||||
period: {},
|
||||
qr_code: { data: "https://www.home-assistant.io" },
|
||||
select: SELECT_DEFAULT_CONFIG,
|
||||
selector: {},
|
||||
serial_port: {},
|
||||
state: { entity_id: "" },
|
||||
statistic: {},
|
||||
stt: {},
|
||||
target: {},
|
||||
template: {},
|
||||
text: {},
|
||||
theme: {},
|
||||
time: {},
|
||||
timezone: {},
|
||||
trigger: {},
|
||||
tts: {},
|
||||
tts_voice: {},
|
||||
ui_action: {},
|
||||
ui_color: {},
|
||||
ui_state_content: {},
|
||||
};
|
||||
|
||||
export const SELECTOR_TYPES = (
|
||||
Object.keys(SELECTOR_PRESETS) as SelectorKey[]
|
||||
).sort();
|
||||
|
||||
export const SELECTOR_VARIANT_GROUPS: Partial<
|
||||
Record<SelectorKey, SelectorVariantGroup[]>
|
||||
> = {
|
||||
select: SELECT_VARIANT_GROUPS,
|
||||
};
|
||||
|
||||
export const getVariantGroups = (type: SelectorKey): SelectorVariantGroup[] =>
|
||||
SELECTOR_VARIANT_GROUPS[type] ?? [];
|
||||
|
||||
export const getVariants = (type: SelectorKey): SelectorVariant[] => {
|
||||
const groups = getVariantGroups(type);
|
||||
if (groups.length) {
|
||||
return groups.flatMap((g) => g.variants);
|
||||
}
|
||||
return [
|
||||
{
|
||||
id: "default",
|
||||
name: "Default",
|
||||
config: SELECTOR_PRESETS[type] ?? {},
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
export const getInitialConfig = (
|
||||
type: SelectorKey
|
||||
): Record<string, unknown> => ({
|
||||
...(getVariants(type)[0]?.config ?? {}),
|
||||
});
|
||||
|
||||
const ACRONYMS = new Set(["qr", "rgb", "stt", "tts", "ui"]);
|
||||
|
||||
export const formatSelectorName = (type: string): string =>
|
||||
type
|
||||
.split("_")
|
||||
.map((word) =>
|
||||
ACRONYMS.has(word)
|
||||
? word.toUpperCase()
|
||||
: word.charAt(0).toUpperCase() + word.slice(1)
|
||||
)
|
||||
.join(" ");
|
||||
|
||||
export const SELECTOR_OPTIONS = SELECTOR_TYPES.map((type) => ({
|
||||
value: type,
|
||||
label: formatSelectorName(type),
|
||||
}));
|
||||
360
src/panels/config/developer-tools/selectors/presets/select.ts
Normal file
360
src/panels/config/developer-tools/selectors/presets/select.ts
Normal file
@@ -0,0 +1,360 @@
|
||||
// Preset variants for the `select` selector, grouped by the UI branch of
|
||||
// `ha-selector-select` that they exercise:
|
||||
//
|
||||
// - Dropdown : <ha-select> (single, mode=dropdown or auto >= 6)
|
||||
// - List : <ha-radio> / <ha-checkbox>
|
||||
// (mode=list, no reorder/custom_value)
|
||||
// - Box : <ha-select-box> (single, mode=box)
|
||||
// - Chips : <ha-chip-set> + <ha-generic-picker>
|
||||
// (multiple)
|
||||
// - Picker : <ha-generic-picker> (single + custom_value)
|
||||
//
|
||||
// Configs mirror real usage from the codebase where applicable, e.g.:
|
||||
// - Box multi-column: frontend/src/panels/lovelace/editor/view-header/
|
||||
// hui-view-header-settings-editor.ts
|
||||
// - Box with images/descriptions: frontend/src/panels/config/integrations/
|
||||
// integration-panels/zwave_js/add-node/zwave-js-add-node-configure-device.ts
|
||||
// - Single-column box with descriptions: .../zwave-js-add-node-select-
|
||||
// security-strategy.ts
|
||||
// - Chips with reorder + custom value: frontend/src/panels/lovelace/editor/
|
||||
// config-elements/hui-area-card-editor.ts
|
||||
|
||||
import type { SelectorVariantGroup } from ".";
|
||||
|
||||
const SELECT_OPTIONS_SIMPLE = [
|
||||
"Option 1",
|
||||
"Option 2",
|
||||
"Option 3",
|
||||
"Option 4",
|
||||
"Option 5",
|
||||
"Option 6",
|
||||
];
|
||||
|
||||
const SELECT_OPTIONS_SHORT = SELECT_OPTIONS_SIMPLE.slice(0, 4);
|
||||
|
||||
const DISABLED_OPTIONS = [
|
||||
{ label: "Option 1", value: "option_1" },
|
||||
{ label: "Option 2", value: "option_2" },
|
||||
{ label: "Option 3 (disabled)", value: "option_3", disabled: true },
|
||||
{ label: "Option 4", value: "option_4" },
|
||||
];
|
||||
|
||||
// Options whose values line up with the `demo_select` localizer in
|
||||
// developer-tools-selectors.ts so the `translation_key` preset shows visible
|
||||
// relabeling in the preview.
|
||||
const TRANSLATION_KEY_OPTIONS = [
|
||||
{ value: "option_1", label: "Option 1 (raw)" },
|
||||
{ value: "option_2", label: "Option 2 (raw)" },
|
||||
{ value: "option_3", label: "Option 3 (raw)" },
|
||||
{ value: "option_4", label: "Option 4 (raw)" },
|
||||
];
|
||||
|
||||
export const SELECT_DEFAULT_CONFIG: Record<string, unknown> = {
|
||||
options: SELECT_OPTIONS_SIMPLE,
|
||||
};
|
||||
|
||||
export const SELECT_VARIANT_GROUPS: SelectorVariantGroup[] = [
|
||||
{
|
||||
id: "dropdown",
|
||||
label: "Dropdown",
|
||||
variants: [
|
||||
{
|
||||
id: "dropdown",
|
||||
name: "Basic",
|
||||
config: { mode: "dropdown", options: SELECT_OPTIONS_SIMPLE },
|
||||
},
|
||||
{
|
||||
id: "dropdown_sorted",
|
||||
name: "Sorted",
|
||||
config: {
|
||||
mode: "dropdown",
|
||||
sort: true,
|
||||
options: ["Charlie", "Alpha", "Echo", "Bravo", "Delta"],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "dropdown_disabled_options",
|
||||
name: "With disabled option",
|
||||
config: { mode: "dropdown", options: DISABLED_OPTIONS },
|
||||
},
|
||||
{
|
||||
id: "dropdown_auto_from_count",
|
||||
name: "Auto-selected for 6+ options",
|
||||
config: { options: SELECT_OPTIONS_SIMPLE },
|
||||
},
|
||||
{
|
||||
id: "dropdown_translation_key",
|
||||
name: "translation_key (relabels via localizeValue)",
|
||||
config: {
|
||||
mode: "dropdown",
|
||||
translation_key: "demo_select",
|
||||
options: TRANSLATION_KEY_OPTIONS,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "list",
|
||||
label: "List",
|
||||
variants: [
|
||||
{
|
||||
id: "list_radios",
|
||||
name: "Radios",
|
||||
config: { mode: "list", options: SELECT_OPTIONS_SHORT },
|
||||
},
|
||||
{
|
||||
id: "list_radios_disabled",
|
||||
name: "Radios with disabled option",
|
||||
config: { mode: "list", options: DISABLED_OPTIONS },
|
||||
},
|
||||
{
|
||||
id: "list_checkboxes",
|
||||
name: "Checkboxes (multiple)",
|
||||
config: {
|
||||
mode: "list",
|
||||
multiple: true,
|
||||
options: SELECT_OPTIONS_SHORT,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "list_auto_from_count",
|
||||
name: "Auto-selected for <6 options",
|
||||
config: { options: SELECT_OPTIONS_SHORT },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "box",
|
||||
label: "Box",
|
||||
variants: [
|
||||
{
|
||||
id: "box_basic",
|
||||
name: "Basic",
|
||||
config: { mode: "box", options: SELECT_OPTIONS_SHORT },
|
||||
},
|
||||
{
|
||||
id: "box_multi_column",
|
||||
name: "Multi-column",
|
||||
config: {
|
||||
mode: "box",
|
||||
box_max_columns: 3,
|
||||
options: [
|
||||
{
|
||||
value: "responsive",
|
||||
label: "Responsive",
|
||||
description: "Fill the available width responsively.",
|
||||
},
|
||||
{
|
||||
value: "start",
|
||||
label: "Start",
|
||||
description: "Align content to the start of the container.",
|
||||
},
|
||||
{
|
||||
value: "center",
|
||||
label: "Center",
|
||||
description: "Center content in the container.",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "box_single_column_descriptions",
|
||||
name: "Single column with descriptions",
|
||||
config: {
|
||||
mode: "box",
|
||||
box_max_columns: 1,
|
||||
options: [
|
||||
{
|
||||
value: "default",
|
||||
label: "Default",
|
||||
description:
|
||||
"Use the highest security protocol advertised by the device.",
|
||||
},
|
||||
{
|
||||
value: "s2",
|
||||
label: "Security S2",
|
||||
description:
|
||||
"Use Security S2 for this inclusion (requires compatible hardware).",
|
||||
},
|
||||
{
|
||||
value: "s0",
|
||||
label: "Security S0",
|
||||
description: "Use Security S0 for this inclusion (legacy).",
|
||||
},
|
||||
{
|
||||
value: "insecure",
|
||||
label: "Insecure",
|
||||
description: "Skip encryption entirely. Not recommended.",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "box_with_images",
|
||||
name: "With images (object form)",
|
||||
config: {
|
||||
mode: "box",
|
||||
box_max_columns: 1,
|
||||
options: [
|
||||
{
|
||||
value: "long_range",
|
||||
label: "Long Range",
|
||||
description:
|
||||
"High-bandwidth, star-topology network with per-device ranges of several kilometers.",
|
||||
image: {
|
||||
src: "/static/images/z-wave-add-node/long-range.svg",
|
||||
src_dark: "/static/images/z-wave-add-node/long-range_dark.svg",
|
||||
flip_rtl: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
value: "mesh",
|
||||
label: "Mesh",
|
||||
description:
|
||||
"Traditional mesh network where devices route traffic for each other.",
|
||||
image: {
|
||||
src: "/static/images/z-wave-add-node/mesh.svg",
|
||||
src_dark: "/static/images/z-wave-add-node/mesh_dark.svg",
|
||||
flip_rtl: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "box_image_string",
|
||||
name: "With image (string form)",
|
||||
config: {
|
||||
mode: "box",
|
||||
box_max_columns: 1,
|
||||
options: [
|
||||
{
|
||||
value: "ohf",
|
||||
label: "Open Home Foundation",
|
||||
description: "image is a plain URL string, not an object.",
|
||||
image: "/static/images/ohf-badge.svg",
|
||||
},
|
||||
{
|
||||
value: "none",
|
||||
label: "None",
|
||||
description: "An option without an image for contrast.",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "box_two_columns",
|
||||
name: "Two columns",
|
||||
config: {
|
||||
mode: "box",
|
||||
box_max_columns: 2,
|
||||
options: SELECT_OPTIONS_SHORT,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "box_disabled_option",
|
||||
name: "With disabled option",
|
||||
config: {
|
||||
mode: "box",
|
||||
box_max_columns: 2,
|
||||
options: DISABLED_OPTIONS,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "chips",
|
||||
label: "Chips",
|
||||
variants: [
|
||||
{
|
||||
id: "chips_basic",
|
||||
name: "Basic (multiple)",
|
||||
config: { multiple: true, options: SELECT_OPTIONS_SIMPLE },
|
||||
},
|
||||
{
|
||||
id: "chips_reorder",
|
||||
name: "Reorder",
|
||||
config: {
|
||||
multiple: true,
|
||||
reorder: true,
|
||||
options: SELECT_OPTIONS_SIMPLE,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "chips_custom_value",
|
||||
name: "Custom value",
|
||||
config: {
|
||||
multiple: true,
|
||||
custom_value: true,
|
||||
options: SELECT_OPTIONS_SIMPLE,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "chips_reorder_custom_value",
|
||||
name: "Reorder + custom value",
|
||||
config: {
|
||||
multiple: true,
|
||||
reorder: true,
|
||||
custom_value: true,
|
||||
options: SELECT_OPTIONS_SIMPLE,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "chips_sorted",
|
||||
name: "Sorted",
|
||||
config: {
|
||||
multiple: true,
|
||||
sort: true,
|
||||
options: ["Charlie", "Alpha", "Echo", "Bravo", "Delta"],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "chips_disabled_option",
|
||||
name: "With disabled option",
|
||||
config: {
|
||||
multiple: true,
|
||||
options: DISABLED_OPTIONS,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "chips_list_mode_reorder",
|
||||
name: "mode:list + reorder (falls through to chips)",
|
||||
config: {
|
||||
mode: "list",
|
||||
multiple: true,
|
||||
reorder: true,
|
||||
options: SELECT_OPTIONS_SIMPLE,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "chips_box_mode",
|
||||
name: "mode:box + multiple (falls through to chips)",
|
||||
config: {
|
||||
mode: "box",
|
||||
multiple: true,
|
||||
options: SELECT_OPTIONS_SIMPLE,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "picker",
|
||||
label: "Picker",
|
||||
variants: [
|
||||
{
|
||||
id: "picker_custom_value",
|
||||
name: "Custom value (searchable)",
|
||||
config: { custom_value: true, options: SELECT_OPTIONS_SIMPLE },
|
||||
},
|
||||
{
|
||||
id: "picker_list_mode",
|
||||
name: "mode:list + custom_value (falls through to picker)",
|
||||
config: {
|
||||
mode: "list",
|
||||
custom_value: true,
|
||||
options: SELECT_OPTIONS_SIMPLE,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -3719,6 +3719,21 @@
|
||||
"no_match": "No intent matched",
|
||||
"language": "[%key:ui::components::language-picker::language%]"
|
||||
},
|
||||
"selectors": {
|
||||
"title": "Selectors",
|
||||
"description": "Preview any selector with a custom configuration. Changes to the configuration update the preview live.",
|
||||
"selector_type": "Selector type",
|
||||
"preset": "Preset",
|
||||
"configuration": "Configuration",
|
||||
"configuration_used": "Configuration used",
|
||||
"copy_configuration": "Copy configuration",
|
||||
"preview": "Preview",
|
||||
"value": "Value",
|
||||
"no_value": "No value set",
|
||||
"invalid_config": "Configuration is not valid YAML.",
|
||||
"reset_config": "Reset configuration",
|
||||
"reset_value": "Reset value"
|
||||
},
|
||||
"debug": {
|
||||
"title": "Debug tools",
|
||||
"debug_connection": {
|
||||
|
||||
Reference in New Issue
Block a user