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:
parent
bee67e736a
commit
5e81054a1e
2 changed files with 49 additions and 30 deletions
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
Loading…
Reference in a new issue