Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 35 additions & 1 deletion homeassistant/data_entry_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
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"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -249,7 +251,15 @@ async def async_configure(
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 (
Expand Down Expand Up @@ -343,6 +353,7 @@ async def _async_handle_step(
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']}")

Expand All @@ -352,6 +363,7 @@ async def _async_handle_step(
RESULT_TYPE_EXTERNAL_STEP_DONE,
RESULT_TYPE_SHOW_PROGRESS,
RESULT_TYPE_SHOW_PROGRESS_DONE,
RESULT_TYPE_MENU,
):
flow.cur_step = result
return result
Expand Down Expand Up @@ -507,6 +519,28 @@ def async_show_progress_done(self, *, next_step_id: str) -> FlowResult:
"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(
Expand Down
5 changes: 2 additions & 3 deletions homeassistant/helpers/data_entry_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

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
Expand All @@ -32,11 +33,9 @@ def _prepare_result_json(
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:
Expand Down
1 change: 1 addition & 0 deletions script/hassfest/translations.py
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down
43 changes: 43 additions & 0 deletions tests/test_data_entry_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"