Skip to content

Commit 9611ecb

Browse files
briemlaclaude
andcommitted
Edit flow properties on double click in WebUI
Double-clicking a flow in the flow table selects it and opens the properties dialog, as in the customtkinter GUI. Double clicks are ignored while the form is disabled during a running action. The selection handler is annotated with the list of rows it actually receives. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 5aa1604 commit 9611ecb

2 files changed

Lines changed: 118 additions & 2 deletions

File tree

  • OTAnalytics/plugin_ui/nicegui_gui/pages/sections_and_flow_form
  • tests/unit/OTAnalytics/plugin_ui/nicegui_gui/pages/sections_and_flow_form

‎OTAnalytics/plugin_ui/nicegui_gui/pages/sections_and_flow_form/flow_form.py‎

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Iterable, Self
1+
from typing import Any, Iterable, Self
22

33
from nicegui import ui
44
from nicegui.elements.button import Button
@@ -62,7 +62,9 @@ def __init__(
6262
marker=MARKER_FLOW_TABLE,
6363
# Let the table auto-select the row on click and forward selection
6464
auto_select_on_row_click=True,
65+
on_row_double_click_method=self._edit_flow,
6566
)
67+
self._disabled = False
6668
self._button_remove: ui.button | None = None
6769
self._button_add: ui.button | None = None
6870
self._button_generate: ui.button | None = None
@@ -73,11 +75,17 @@ def _introduce_to_viewmodel(self) -> None:
7375
self._viewmodel.set_flows_frame(self)
7476
self._viewmodel.set_treeview_flows(self)
7577

76-
def _select_flow(self, e: dict) -> None:
78+
def _select_flow(self, e: list[dict[str, Any]]) -> None:
7779
flow_ids = [event[COLUMN_ID] for event in e]
7880
self._viewmodel.set_selected_flow_ids(flow_ids)
7981
self._viewmodel.refresh_items_on_canvas()
8082

83+
async def _edit_flow(self, row: dict[str, Any]) -> None:
84+
if self._disabled:
85+
return
86+
self._select_flow([row])
87+
await self._viewmodel.edit_selected_flow()
88+
8189
def build(self) -> Self:
8290
self._flow_table.build()
8391
with ui.row().style(BASIC_WIDTH):
@@ -133,6 +141,7 @@ def update_selected_items(self, item_ids: list[str]) -> None:
133141
self._flow_table.select(item_ids)
134142

135143
def enable(self) -> None:
144+
self._disabled = False
136145
if (
137146
self._button_remove
138147
and self._button_properties
@@ -145,6 +154,7 @@ def enable(self) -> None:
145154
self._button_properties.enable()
146155

147156
def disable(self) -> None:
157+
self._disabled = True
148158
if (
149159
self._button_remove
150160
and self._button_properties
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
"""Tests editing a flow by double-clicking it in the WebUI flow table."""
2+
3+
from typing import Any
4+
from unittest.mock import MagicMock
5+
6+
import pytest
7+
from nicegui import ui
8+
from nicegui.testing import User
9+
10+
from OTAnalytics.adapter_ui.view_model import ViewModel
11+
from OTAnalytics.application.resources.resource_manager import ResourceManager
12+
from OTAnalytics.application.state import FlowState
13+
from OTAnalytics.plugin_ui.nicegui_gui.pages.sections_and_flow_form.flow_form import (
14+
MARKER_FLOW_TABLE,
15+
FlowForm,
16+
)
17+
from tests.utils.nicegui_table import double_click_row, wait_until_awaited
18+
19+
ENDPOINT_NAME = "/test-flow-form-double-click"
20+
FLOW_ID = "flow-1"
21+
FLOW_ROW = {"id": FLOW_ID, "name": "Flow 1"}
22+
23+
24+
class FakeFlow:
25+
def __init__(self, flow_id: str, name: str) -> None:
26+
self.id = flow_id
27+
self.name = name
28+
29+
def to_dict(self) -> dict[str, Any]:
30+
return {"id": self.id, "name": self.name}
31+
32+
33+
def create_viewmodel() -> MagicMock:
34+
viewmodel = MagicMock(spec=ViewModel)
35+
viewmodel.get_all_flows.return_value = [FakeFlow(FLOW_ID, "Flow 1")]
36+
return viewmodel
37+
38+
39+
def create_flow_state() -> MagicMock:
40+
flow_state = MagicMock(spec=FlowState)
41+
flow_state.selected_flows = MagicMock()
42+
flow_state.selected_flows.get.return_value = []
43+
return flow_state
44+
45+
46+
class TestFlowFormDoubleClick:
47+
48+
@pytest.mark.asyncio
49+
async def test_double_click_selects_flow_and_opens_properties(
50+
self, user: User, resource_manager: ResourceManager
51+
) -> None:
52+
viewmodel = create_viewmodel()
53+
target = FlowForm(viewmodel, create_flow_state(), resource_manager)
54+
55+
@ui.page(ENDPOINT_NAME)
56+
def page() -> None:
57+
target.build()
58+
59+
await user.open(ENDPOINT_NAME)
60+
61+
double_click_row(user, MARKER_FLOW_TABLE, FLOW_ROW)
62+
await wait_until_awaited(viewmodel.edit_selected_flow)
63+
64+
viewmodel.set_selected_flow_ids.assert_called_with([FLOW_ID])
65+
viewmodel.edit_selected_flow.assert_awaited_once()
66+
67+
@pytest.mark.asyncio
68+
async def test_double_click_does_nothing_while_disabled(
69+
self, user: User, resource_manager: ResourceManager
70+
) -> None:
71+
viewmodel = create_viewmodel()
72+
target = FlowForm(viewmodel, create_flow_state(), resource_manager)
73+
74+
@ui.page(ENDPOINT_NAME)
75+
def page() -> None:
76+
target.build()
77+
78+
await user.open(ENDPOINT_NAME)
79+
target.disable()
80+
viewmodel.set_selected_flow_ids.reset_mock()
81+
82+
double_click_row(user, MARKER_FLOW_TABLE, FLOW_ROW)
83+
await wait_until_awaited(viewmodel.edit_selected_flow)
84+
85+
viewmodel.set_selected_flow_ids.assert_not_called()
86+
viewmodel.edit_selected_flow.assert_not_awaited()
87+
88+
@pytest.mark.asyncio
89+
async def test_double_click_works_again_after_enable(
90+
self, user: User, resource_manager: ResourceManager
91+
) -> None:
92+
viewmodel = create_viewmodel()
93+
target = FlowForm(viewmodel, create_flow_state(), resource_manager)
94+
95+
@ui.page(ENDPOINT_NAME)
96+
def page() -> None:
97+
target.build()
98+
99+
await user.open(ENDPOINT_NAME)
100+
target.disable()
101+
target.enable()
102+
103+
double_click_row(user, MARKER_FLOW_TABLE, FLOW_ROW)
104+
await wait_until_awaited(viewmodel.edit_selected_flow)
105+
106+
viewmodel.edit_selected_flow.assert_awaited_once()

0 commit comments

Comments
 (0)