Add support for menu step (#68203)
This commit is contained in:
parent
aa57907c18
commit
f6af93ae35
4 changed files with 81 additions and 4 deletions
|
@ -21,6 +21,7 @@ RESULT_TYPE_EXTERNAL_STEP = "external"
|
|||
RESULT_TYPE_EXTERNAL_STEP_DONE = "external_done"
|
||||
RESULT_TYPE_SHOW_PROGRESS = "progress"
|
||||
RESULT_TYPE_SHOW_PROGRESS_DONE = "progress_done"
|
||||
RESULT_TYPE_MENU = "menu"
|
||||
|
||||
# Event that is fired when a flow is progressed via external or progress source.
|
||||
EVENT_DATA_ENTRY_FLOW_PROGRESSED = "data_entry_flow_progressed"
|
||||
|
@ -82,6 +83,7 @@ class FlowResult(TypedDict, total=False):
|
|||
result: Any
|
||||
last_step: bool | None
|
||||
options: Mapping[str, Any]
|
||||
menu_options: list[str] | Mapping[str, Any]
|
||||
|
||||
|
||||
@callback
|
||||
|
@ -249,7 +251,15 @@ class FlowManager(abc.ABC):
|
|||
if cur_step.get("data_schema") is not None and user_input is not None:
|
||||
user_input = cur_step["data_schema"](user_input)
|
||||
|
||||
result = await self._async_handle_step(flow, cur_step["step_id"], user_input)
|
||||
# Handle a menu navigation choice
|
||||
if cur_step["type"] == RESULT_TYPE_MENU and user_input:
|
||||
result = await self._async_handle_step(
|
||||
flow, user_input["next_step_id"], None
|
||||
)
|
||||
else:
|
||||
result = await self._async_handle_step(
|
||||
flow, cur_step["step_id"], user_input
|
||||
)
|
||||
|
||||
if cur_step["type"] in (RESULT_TYPE_EXTERNAL_STEP, RESULT_TYPE_SHOW_PROGRESS):
|
||||
if cur_step["type"] == RESULT_TYPE_EXTERNAL_STEP and result["type"] not in (
|
||||
|
@ -343,6 +353,7 @@ class FlowManager(abc.ABC):
|
|||
RESULT_TYPE_EXTERNAL_STEP_DONE,
|
||||
RESULT_TYPE_SHOW_PROGRESS,
|
||||
RESULT_TYPE_SHOW_PROGRESS_DONE,
|
||||
RESULT_TYPE_MENU,
|
||||
):
|
||||
raise ValueError(f"Handler returned incorrect type: {result['type']}")
|
||||
|
||||
|
@ -352,6 +363,7 @@ class FlowManager(abc.ABC):
|
|||
RESULT_TYPE_EXTERNAL_STEP_DONE,
|
||||
RESULT_TYPE_SHOW_PROGRESS,
|
||||
RESULT_TYPE_SHOW_PROGRESS_DONE,
|
||||
RESULT_TYPE_MENU,
|
||||
):
|
||||
flow.cur_step = result
|
||||
return result
|
||||
|
@ -507,6 +519,28 @@ class FlowHandler:
|
|||
"step_id": next_step_id,
|
||||
}
|
||||
|
||||
@callback
|
||||
def async_show_menu(
|
||||
self,
|
||||
*,
|
||||
step_id: str,
|
||||
menu_options: list[str] | dict[str, str],
|
||||
description_placeholders: dict | None = None,
|
||||
) -> FlowResult:
|
||||
"""Show a navigation menu to the user.
|
||||
|
||||
Options dict maps step_id => i18n label
|
||||
"""
|
||||
return {
|
||||
"type": RESULT_TYPE_MENU,
|
||||
"flow_id": self.flow_id,
|
||||
"handler": self.handler,
|
||||
"step_id": step_id,
|
||||
"data_schema": vol.Schema({"next_step_id": vol.In(menu_options)}),
|
||||
"menu_options": menu_options,
|
||||
"description_placeholders": description_placeholders,
|
||||
}
|
||||
|
||||
|
||||
@callback
|
||||
def _create_abort_data(
|
||||
|
|
|
@ -6,6 +6,7 @@ from typing import Any
|
|||
|
||||
from aiohttp import web
|
||||
import voluptuous as vol
|
||||
import voluptuous_serialize
|
||||
|
||||
from homeassistant import config_entries, data_entry_flow
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
|
@ -32,11 +33,9 @@ class _BaseFlowManagerView(HomeAssistantView):
|
|||
data.pop("data")
|
||||
return data
|
||||
|
||||
if result["type"] != data_entry_flow.RESULT_TYPE_FORM:
|
||||
if "data_schema" not in result:
|
||||
return result
|
||||
|
||||
import voluptuous_serialize # pylint: disable=import-outside-toplevel
|
||||
|
||||
data = result.copy()
|
||||
|
||||
if (schema := data["data_schema"]) is None:
|
||||
|
|
|
@ -110,6 +110,7 @@ def gen_data_entry_schema(
|
|||
step_title_class("title"): cv.string_with_no_html,
|
||||
vol.Optional("description"): cv.string_with_no_html,
|
||||
vol.Optional("data"): {str: cv.string_with_no_html},
|
||||
vol.Optional("menu_options"): {str: cv.string_with_no_html},
|
||||
}
|
||||
},
|
||||
vol.Optional("error"): {str: cv.string_with_no_html},
|
||||
|
|
|
@ -487,3 +487,46 @@ async def test_abort_raises_unknown_flow_if_not_in_progress(manager):
|
|||
"""Test abort raises UnknownFlow if the flow is not in progress."""
|
||||
with pytest.raises(data_entry_flow.UnknownFlow):
|
||||
await manager.async_abort("wrong_flow_id")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"menu_options",
|
||||
(["target1", "target2"], {"target1": "Target 1", "target2": "Target 2"}),
|
||||
)
|
||||
async def test_show_menu(hass, manager, menu_options):
|
||||
"""Test show menu."""
|
||||
manager.hass = hass
|
||||
|
||||
@manager.mock_reg_handler("test")
|
||||
class TestFlow(data_entry_flow.FlowHandler):
|
||||
VERSION = 5
|
||||
data = None
|
||||
task_one_done = False
|
||||
|
||||
async def async_step_init(self, user_input=None):
|
||||
return self.async_show_menu(
|
||||
step_id="init",
|
||||
menu_options=menu_options,
|
||||
description_placeholders={"name": "Paulus"},
|
||||
)
|
||||
|
||||
async def async_step_target1(self, user_input=None):
|
||||
return self.async_show_form(step_id="target1")
|
||||
|
||||
async def async_step_target2(self, user_input=None):
|
||||
return self.async_show_form(step_id="target2")
|
||||
|
||||
result = await manager.async_init("test")
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_MENU
|
||||
assert result["menu_options"] == menu_options
|
||||
assert result["description_placeholders"] == {"name": "Paulus"}
|
||||
assert len(manager.async_progress()) == 1
|
||||
assert len(manager.async_progress_by_handler("test")) == 1
|
||||
assert manager.async_get(result["flow_id"])["handler"] == "test"
|
||||
|
||||
# Mimic picking a step
|
||||
result = await manager.async_configure(
|
||||
result["flow_id"], {"next_step_id": "target1"}
|
||||
)
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["step_id"] == "target1"
|
||||
|
|
Loading…
Add table
Reference in a new issue