Apply the filter modal's search on enter, not as you type

Typing narrowed the mounted checkboxes without touching the search the listing
was built from, so a term matching more than one page hid rows from whichever
page the user happened to be on, left the rest of the matches a page away, and
paged on the previous search while the term still sat in the box. The term now
applies on enter, and the footer says so until it does.
This commit is contained in:
Yiorgis Gozadinos 2026-08-28 09:59:33 +03:00
parent bee67e736a
commit 5e81054a1e
No known key found for this signature in database
2 changed files with 49 additions and 30 deletions

View file

@ -18,8 +18,6 @@ class DocumentCheckbox(Checkbox):
def __init__(self, label: str, doc_id: str, *, value: bool) -> None:
super().__init__(label, value=value, classes="doc-checkbox")
self.doc_id = doc_id
# `label` is a reactive Text; narrowing the page wants the plain string.
self.search_text = label
def _labelled(docs) -> list[tuple[str, str]]:
@ -147,7 +145,10 @@ class DocumentFilterModal(ModalScreen):
def compose(self) -> ComposeResult:
with Vertical(id="filter-container"):
yield Static("[bold]Filter Documents[/bold]", id="filter-header")
yield Input(placeholder="Search documents...", id="filter-search")
yield Input(
placeholder="Search documents; press Enter to search...",
id="filter-search",
)
with VerticalScroll(id="filter-list"):
yield Static("Loading...", id="loading-indicator")
yield Static("", id="filter-footer")
@ -170,7 +171,7 @@ class DocumentFilterModal(ModalScreen):
is showing and every selection stays reachable.
"""
if search is not None:
self._search = search
self._search = search.strip()
self._page = 0
self._listing_selected = False
@ -223,11 +224,8 @@ class DocumentFilterModal(ModalScreen):
return max(1, -(-self._matching // DOCUMENT_PAGE))
def _update_footer(self) -> None:
"""Report the selection, and where in the listing this page sits.
Counted from the checkboxes on screen, so narrowing the page as the user
types reports what they can actually see.
"""
"""Report the selection, where in the listing this page sits, and
whether the search box holds a term the listing was not built from."""
footer = self.query_one("#filter-footer", Static)
count = len(self._selected)
if count == 0:
@ -236,17 +234,13 @@ class DocumentFilterModal(ModalScreen):
state = f"[bold]{count}[/bold] document(s) selected"
if self._listing_selected:
state += " [dim]— listing the selected[/dim]"
visible = sum(1 for box in self.query(DocumentCheckbox) if box.display)
if self._pages > 1:
state += (
f" [dim]— page {self._page + 1} of {self._pages}"
f" ({self._matching} total)[/dim]"
)
elif self._matching > visible:
state += (
f" [dim]— showing {visible} of {self._matching};"
" type and press enter to search[/dim]"
)
if self.query_one("#filter-search", Input).value.strip() != self._search:
state += " [dim]— press enter to search[/dim]"
footer.update(state)
async def on_checkbox_changed(self, event: Checkbox.Changed) -> None:
@ -275,14 +269,7 @@ class DocumentFilterModal(ModalScreen):
await self._load_documents(event.value)
def on_input_changed(self, event: Input.Changed) -> None:
"""Narrow the page already shown, for feedback while typing."""
search_term = event.value.lower().strip()
filter_list = self.query_one("#filter-list", VerticalScroll)
for checkbox in filter_list.query(DocumentCheckbox):
checkbox.display = (
search_term == "" or search_term in checkbox.search_text.lower()
)
"""Prompt when the search term has not been submitted."""
self._update_footer()
async def _turn_to(self, page: int) -> None:

View file

@ -945,10 +945,22 @@ class TestKeepingSelectionsReachable:
assert "No documents match" in str(empty.content)
@pytest.mark.asyncio
async def test_narrowing_as_you_type_updates_the_count(self, temp_db_path: Path):
from textual.widgets import Input, Static
async def test_typing_without_submitting_leaves_the_listing_alone(
self, temp_db_path: Path
):
"""The listing is what the last submitted search asked for, and it pages.
from haiku.rag.chat.widgets.document_filter_modal import DocumentFilterModal
Narrowing it as the user types would hide rows from the page the search
landed on while the rest of the matches stayed a page away, so the term
applies on enter and says so until then.
"""
from textual.widgets import Button, Input, Static
from haiku.rag.chat.widgets.document_filter_modal import (
DOCUMENT_PAGE,
DocumentCheckbox,
DocumentFilterModal,
)
from haiku.rag.store.models.document import Document
client = AsyncMock()
@ -958,7 +970,7 @@ class TestKeepingSelectionsReachable:
Document(id="id-one", content="", title="Capital region"),
Document(id="id-two", content="", title="Nobel laureates"),
]
client.count_documents.return_value = 9
client.count_documents.return_value = DOCUMENT_PAGE * 2
modal = DocumentFilterModal(client=client)
app, _ = _make_app(temp_db_path, client)
@ -970,12 +982,32 @@ class TestKeepingSelectionsReachable:
await app.push_screen(modal)
await pilot.pause()
footer = modal.query_one("#filter-footer", Static)
assert "showing 2 of 9" in str(footer.content)
assert "press enter to search" not in str(footer.content)
modal.on_input_changed(Input.Changed(Input(), "nobel"))
search = modal.query_one("#filter-search", Input)
search.value = "nobel"
modal.on_input_changed(Input.Changed(search, "nobel"))
assert "press enter to search" in str(footer.content)
assert all(box.display for box in modal.query(DocumentCheckbox))
await modal.on_button_pressed(
Button.Pressed(modal.query_one("#next-btn", Button))
)
await pilot.pause()
assert "showing 1 of 9" in str(footer.content)
paged = client.list_documents.await_args.kwargs
assert paged["offset"] == DOCUMENT_PAGE
assert paged["filter"] is None
assert "press enter to search" in str(footer.content)
await modal.on_input_submitted(Input.Submitted(search, search.value))
await pilot.pause()
submitted = client.list_documents.await_args.kwargs
assert submitted["offset"] == 0
assert "nobel" in submitted["filter"]
assert "press enter to search" not in str(footer.content)
class TestDocumentSearchFilter: