mirror of
https://github.com/home-assistant/core.git
synced 2025-04-23 16:57:53 +00:00
Section support for data entry flows (#118369)
* Add expandable support for data entry form flows * Update config_validation.py * optional options * Adjust * Correct translations of data within sections * Update homeassistant/components/kitchen_sink/config_flow.py Co-authored-by: Robert Resch <robert@resch.dev> * Add missing import * Update tests/components/kitchen_sink/test_config_flow.py Co-authored-by: Robert Resch <robert@resch.dev> * Format code * Match frontend when serializing * Move section class to data_entry_flow * Correct serializing * Fix import in kitchen_sink * Move and update test --------- Co-authored-by: Bram Kragten <mail@bramkragten.nl> Co-authored-by: Robert Resch <robert@resch.dev>
This commit is contained in:
parent
3d1ff72a88
commit
0545ed8082
@ -4,16 +4,36 @@ from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import data_entry_flow
|
||||
from homeassistant.config_entries import (
|
||||
ConfigEntry,
|
||||
ConfigFlow,
|
||||
ConfigFlowResult,
|
||||
OptionsFlowWithConfigEntry,
|
||||
)
|
||||
from homeassistant.core import callback
|
||||
|
||||
from . import DOMAIN
|
||||
|
||||
CONF_BOOLEAN = "bool"
|
||||
CONF_INT = "int"
|
||||
|
||||
|
||||
class KitchenSinkConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Kitchen Sink configuration flow."""
|
||||
|
||||
VERSION = 1
|
||||
|
||||
@staticmethod
|
||||
@callback
|
||||
def async_get_options_flow(
|
||||
config_entry: ConfigEntry,
|
||||
) -> OptionsFlowHandler:
|
||||
"""Get the options flow for this handler."""
|
||||
return OptionsFlowHandler(config_entry)
|
||||
|
||||
async def async_step_import(self, import_info: dict[str, Any]) -> ConfigFlowResult:
|
||||
"""Set the config entry up from yaml."""
|
||||
if self._async_current_entries():
|
||||
@ -30,3 +50,50 @@ class KitchenSinkConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
if user_input is None:
|
||||
return self.async_show_form(step_id="reauth_confirm")
|
||||
return self.async_abort(reason="reauth_successful")
|
||||
|
||||
|
||||
class OptionsFlowHandler(OptionsFlowWithConfigEntry):
|
||||
"""Handle options."""
|
||||
|
||||
async def async_step_init(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Manage the options."""
|
||||
return await self.async_step_options_1()
|
||||
|
||||
async def async_step_options_1(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Manage the options."""
|
||||
if user_input is not None:
|
||||
self.options.update(user_input)
|
||||
return await self._update_options()
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="options_1",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required("section_1"): data_entry_flow.section(
|
||||
vol.Schema(
|
||||
{
|
||||
vol.Optional(
|
||||
CONF_BOOLEAN,
|
||||
default=self.config_entry.options.get(
|
||||
CONF_BOOLEAN, False
|
||||
),
|
||||
): bool,
|
||||
vol.Optional(
|
||||
CONF_INT,
|
||||
default=self.config_entry.options.get(CONF_INT, 10),
|
||||
): int,
|
||||
}
|
||||
),
|
||||
{"collapsed": False},
|
||||
),
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
async def _update_options(self) -> ConfigFlowResult:
|
||||
"""Update config entry options."""
|
||||
return self.async_create_entry(title="", data=self.options)
|
||||
|
11
homeassistant/components/kitchen_sink/icons.json
Normal file
11
homeassistant/components/kitchen_sink/icons.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"options": {
|
||||
"step": {
|
||||
"options_1": {
|
||||
"section": {
|
||||
"section_1": "mdi:robot"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -6,6 +6,26 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {}
|
||||
},
|
||||
"options_1": {
|
||||
"section": {
|
||||
"section_1": {
|
||||
"data": {
|
||||
"bool": "Optional boolean",
|
||||
"int": "Numeric input"
|
||||
},
|
||||
"description": "This section allows input of some extra data",
|
||||
"name": "Collapsible section"
|
||||
}
|
||||
},
|
||||
"submit": "Save!"
|
||||
}
|
||||
}
|
||||
},
|
||||
"device": {
|
||||
"n_ch_power_strip": {
|
||||
"name": "Power strip with {number_of_sockets} sockets"
|
||||
|
@ -906,6 +906,33 @@ class FlowHandler(Generic[_FlowResultT, _HandlerT]):
|
||||
self.__progress_task = progress_task
|
||||
|
||||
|
||||
class SectionConfig(TypedDict, total=False):
|
||||
"""Class to represent a section config."""
|
||||
|
||||
collapsed: bool
|
||||
|
||||
|
||||
class section:
|
||||
"""Data entry flow section."""
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional("collapsed", default=False): bool,
|
||||
},
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self, schema: vol.Schema, options: SectionConfig | None = None
|
||||
) -> None:
|
||||
"""Initialize."""
|
||||
self.schema = schema
|
||||
self.options: SectionConfig = self.CONFIG_SCHEMA(options or {})
|
||||
|
||||
def __call__(self, value: Any) -> Any:
|
||||
"""Validate input."""
|
||||
return self.schema(value)
|
||||
|
||||
|
||||
# These can be removed if no deprecated constant are in this module anymore
|
||||
__getattr__ = partial(check_if_deprecated_constant, module_globals=globals())
|
||||
__dir__ = partial(
|
||||
|
@ -1037,6 +1037,7 @@ def key_dependency(
|
||||
|
||||
def custom_serializer(schema: Any) -> Any:
|
||||
"""Serialize additional types for voluptuous_serialize."""
|
||||
from .. import data_entry_flow # pylint: disable=import-outside-toplevel
|
||||
from . import selector # pylint: disable=import-outside-toplevel
|
||||
|
||||
if schema is positive_time_period_dict:
|
||||
@ -1048,6 +1049,15 @@ def custom_serializer(schema: Any) -> Any:
|
||||
if schema is boolean:
|
||||
return {"type": "boolean"}
|
||||
|
||||
if isinstance(schema, data_entry_flow.section):
|
||||
return {
|
||||
"type": "expandable",
|
||||
"schema": voluptuous_serialize.convert(
|
||||
schema.schema, custom_serializer=custom_serializer
|
||||
),
|
||||
"expanded": not schema.options["collapsed"],
|
||||
}
|
||||
|
||||
if isinstance(schema, multi_select):
|
||||
return {"type": "multi_select", "options": schema.options}
|
||||
|
||||
|
@ -47,6 +47,19 @@ def ensure_not_same_as_default(value: dict) -> dict:
|
||||
return value
|
||||
|
||||
|
||||
DATA_ENTRY_ICONS_SCHEMA = vol.Schema(
|
||||
{
|
||||
"step": {
|
||||
str: {
|
||||
"section": {
|
||||
str: icon_value_validator,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def icon_schema(integration_type: str, no_entity_platform: bool) -> vol.Schema:
|
||||
"""Create an icon schema."""
|
||||
|
||||
@ -73,6 +86,11 @@ def icon_schema(integration_type: str, no_entity_platform: bool) -> vol.Schema:
|
||||
|
||||
schema = vol.Schema(
|
||||
{
|
||||
vol.Optional("config"): DATA_ENTRY_ICONS_SCHEMA,
|
||||
vol.Optional("issues"): vol.Schema(
|
||||
{str: {"fix_flow": DATA_ENTRY_ICONS_SCHEMA}}
|
||||
),
|
||||
vol.Optional("options"): DATA_ENTRY_ICONS_SCHEMA,
|
||||
vol.Optional("services"): state_validator,
|
||||
}
|
||||
)
|
||||
|
@ -166,6 +166,13 @@ def gen_data_entry_schema(
|
||||
vol.Optional("data_description"): {str: translation_value_validator},
|
||||
vol.Optional("menu_options"): {str: translation_value_validator},
|
||||
vol.Optional("submit"): translation_value_validator,
|
||||
vol.Optional("section"): {
|
||||
str: {
|
||||
vol.Optional("data"): {str: translation_value_validator},
|
||||
vol.Optional("description"): translation_value_validator,
|
||||
vol.Optional("name"): translation_value_validator,
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
vol.Optional("error"): {str: translation_value_validator},
|
||||
|
@ -1,13 +1,28 @@
|
||||
"""Test the Everything but the Kitchen Sink config flow."""
|
||||
|
||||
from collections.abc import AsyncGenerator
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant import config_entries, setup
|
||||
from homeassistant.components.kitchen_sink import DOMAIN
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def no_platforms() -> AsyncGenerator[None, None]:
|
||||
"""Don't enable any platforms."""
|
||||
with patch(
|
||||
"homeassistant.components.kitchen_sink.COMPONENTS_WITH_DEMO_PLATFORM",
|
||||
[],
|
||||
):
|
||||
yield
|
||||
|
||||
|
||||
async def test_import(hass: HomeAssistant) -> None:
|
||||
"""Test that we can import a config entry."""
|
||||
@ -66,3 +81,26 @@ async def test_reauth(hass: HomeAssistant) -> None:
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "reauth_successful"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("no_platforms")
|
||||
async def test_options_flow(hass: HomeAssistant) -> None:
|
||||
"""Test config flow options."""
|
||||
config_entry = MockConfigEntry(domain=DOMAIN)
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
result = await hass.config_entries.options.async_init(config_entry.entry_id)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "options_1"
|
||||
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={"section_1": {"bool": True, "int": 15}},
|
||||
)
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert config_entry.options == {"section_1": {"bool": True, "int": 15}}
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
@ -10,6 +10,7 @@ import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries, data_entry_flow
|
||||
from homeassistant.core import Event, HomeAssistant, callback
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.util.decorator import Registry
|
||||
|
||||
from .common import (
|
||||
@ -1075,3 +1076,25 @@ def test_deprecated_constants(
|
||||
import_and_test_deprecated_constant_enum(
|
||||
caplog, data_entry_flow, enum, "RESULT_TYPE_", "2025.1"
|
||||
)
|
||||
|
||||
|
||||
def test_section_in_serializer() -> None:
|
||||
"""Test section with custom_serializer."""
|
||||
assert cv.custom_serializer(
|
||||
data_entry_flow.section(
|
||||
vol.Schema(
|
||||
{
|
||||
vol.Optional("option_1", default=False): bool,
|
||||
vol.Required("option_2"): int,
|
||||
}
|
||||
),
|
||||
{"collapsed": False},
|
||||
)
|
||||
) == {
|
||||
"expanded": True,
|
||||
"schema": [
|
||||
{"default": False, "name": "option_1", "optional": True, "type": "boolean"},
|
||||
{"name": "option_2", "required": True, "type": "integer"},
|
||||
],
|
||||
"type": "expandable",
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user