Fix document-filter searches after the initial load
Every load looked up the loading indicator, which only the first page has, so pressing enter in the search box raised `NoMatches`. The indicator is a child of the list `remove_children()` already clears. A document's id and search text travel on a `DocumentCheckbox` instead of being assigned onto a `Checkbox` behind type suppressions. The positional widget ids are gone; nothing queried them.
This commit is contained in:
parent
ea6b864f6e
commit
0e0ce8d3f0
2 changed files with 68 additions and 36 deletions
|
|
@ -13,6 +13,14 @@ from haiku.rag.utils import escape_sql_string
|
||||||
DOCUMENT_PAGE = 200
|
DOCUMENT_PAGE = 200
|
||||||
|
|
||||||
|
|
||||||
|
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 search_filter(term: str) -> str | None:
|
def search_filter(term: str) -> str | None:
|
||||||
"""A document filter matching `term` in a title or URI, or None for no term.
|
"""A document filter matching `term` in a title or URI, or None for no term.
|
||||||
|
|
||||||
|
|
@ -139,10 +147,6 @@ class DocumentFilterModal(ModalScreen):
|
||||||
)
|
)
|
||||||
self._matching = await self.client.count_documents(filter=document_filter)
|
self._matching = await self.client.count_documents(filter=document_filter)
|
||||||
|
|
||||||
loading = self.query_one("#loading-indicator", Static)
|
|
||||||
if loading.parent is not None:
|
|
||||||
loading.remove()
|
|
||||||
|
|
||||||
filter_list = self.query_one("#filter-list", VerticalScroll)
|
filter_list = self.query_one("#filter-list", VerticalScroll)
|
||||||
await filter_list.remove_children()
|
await filter_list.remove_children()
|
||||||
|
|
||||||
|
|
@ -161,23 +165,10 @@ class DocumentFilterModal(ModalScreen):
|
||||||
key=lambda pair: pair[0],
|
key=lambda pair: pair[0],
|
||||||
)
|
)
|
||||||
|
|
||||||
boxes = []
|
boxes = [
|
||||||
for position, (label, doc_id) in enumerate(labelled):
|
DocumentCheckbox(label, doc_id, value=doc_id in self._selected)
|
||||||
checkbox = Checkbox(
|
for label, doc_id in labelled
|
||||||
label,
|
]
|
||||||
value=doc_id in self._selected,
|
|
||||||
# Positional, because a label is not unique and a repeated
|
|
||||||
# widget id is an error.
|
|
||||||
id=f"doc-{position}",
|
|
||||||
classes="doc-checkbox",
|
|
||||||
)
|
|
||||||
# The selection is the document id: a title repeats within a corpus
|
|
||||||
# and across databases, so selecting by name widens to documents the
|
|
||||||
# user did not pick.
|
|
||||||
checkbox._doc_id = doc_id # type: ignore[attr-defined] # ty: ignore[unresolved-attribute]
|
|
||||||
# Not `_label`: Textual's ToggleButton owns that name.
|
|
||||||
checkbox._search_text = label # type: ignore[attr-defined] # ty: ignore[unresolved-attribute]
|
|
||||||
boxes.append(checkbox)
|
|
||||||
if boxes:
|
if boxes:
|
||||||
await filter_list.mount_all(boxes)
|
await filter_list.mount_all(boxes)
|
||||||
|
|
||||||
|
|
@ -202,14 +193,13 @@ class DocumentFilterModal(ModalScreen):
|
||||||
def on_checkbox_changed(self, event: Checkbox.Changed) -> None:
|
def on_checkbox_changed(self, event: Checkbox.Changed) -> None:
|
||||||
"""Handle checkbox state changes."""
|
"""Handle checkbox state changes."""
|
||||||
checkbox = event.checkbox
|
checkbox = event.checkbox
|
||||||
doc_id = getattr(checkbox, "_doc_id", None)
|
if not isinstance(checkbox, DocumentCheckbox):
|
||||||
if doc_id is None:
|
|
||||||
return
|
return
|
||||||
|
|
||||||
if event.value:
|
if event.value:
|
||||||
self._selected.add(doc_id)
|
self._selected.add(checkbox.doc_id)
|
||||||
else:
|
else:
|
||||||
self._selected.discard(doc_id)
|
self._selected.discard(checkbox.doc_id)
|
||||||
|
|
||||||
self._update_footer()
|
self._update_footer()
|
||||||
|
|
||||||
|
|
@ -222,12 +212,10 @@ class DocumentFilterModal(ModalScreen):
|
||||||
search_term = event.value.lower().strip()
|
search_term = event.value.lower().strip()
|
||||||
filter_list = self.query_one("#filter-list", VerticalScroll)
|
filter_list = self.query_one("#filter-list", VerticalScroll)
|
||||||
|
|
||||||
for checkbox in filter_list.query(Checkbox):
|
for checkbox in filter_list.query(DocumentCheckbox):
|
||||||
label = getattr(checkbox, "_search_text", "")
|
checkbox.display = (
|
||||||
if search_term == "" or search_term in label.lower():
|
search_term == "" or search_term in checkbox.search_text.lower()
|
||||||
checkbox.display = True
|
)
|
||||||
else:
|
|
||||||
checkbox.display = False
|
|
||||||
|
|
||||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||||
"""Handle button presses."""
|
"""Handle button presses."""
|
||||||
|
|
|
||||||
|
|
@ -631,9 +631,10 @@ class TestDocumentSelectionIdentity:
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_a_repeated_title_selects_one_document(self, temp_db_path: Path):
|
async def test_a_repeated_title_selects_one_document(self, temp_db_path: Path):
|
||||||
from textual.widgets import Checkbox
|
from haiku.rag.chat.widgets.document_filter_modal import (
|
||||||
|
DocumentCheckbox,
|
||||||
from haiku.rag.chat.widgets.document_filter_modal import DocumentFilterModal
|
DocumentFilterModal,
|
||||||
|
)
|
||||||
from haiku.rag.store.models.document import Document
|
from haiku.rag.store.models.document import Document
|
||||||
|
|
||||||
client = AsyncMock()
|
client = AsyncMock()
|
||||||
|
|
@ -655,8 +656,8 @@ class TestDocumentSelectionIdentity:
|
||||||
await app.push_screen(modal)
|
await app.push_screen(modal)
|
||||||
await pilot.pause()
|
await pilot.pause()
|
||||||
|
|
||||||
boxes = list(modal.query(Checkbox))
|
boxes = list(modal.query(DocumentCheckbox))
|
||||||
selected_ids = [getattr(box, "_doc_id", None) for box in boxes]
|
selected_ids = [box.doc_id for box in boxes]
|
||||||
assert selected_ids == ["id-one", "id-two"]
|
assert selected_ids == ["id-one", "id-two"]
|
||||||
labels = [str(b.label) for b in boxes]
|
labels = [str(b.label) for b in boxes]
|
||||||
assert "Capital region (arxiv)" in labels
|
assert "Capital region (arxiv)" in labels
|
||||||
|
|
@ -714,3 +715,46 @@ class TestDocumentSearchFilter:
|
||||||
|
|
||||||
assert built is not None
|
assert built is not None
|
||||||
assert "o''brien" in built
|
assert "o''brien" in built
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_submitting_a_search_reloads_the_page(self, temp_db_path: Path):
|
||||||
|
"""The typed term reaches the database and replaces what is shown."""
|
||||||
|
from textual.widgets import Input
|
||||||
|
|
||||||
|
from haiku.rag.chat.widgets.document_filter_modal import (
|
||||||
|
DocumentCheckbox,
|
||||||
|
DocumentFilterModal,
|
||||||
|
)
|
||||||
|
from haiku.rag.store.models.document import Document
|
||||||
|
|
||||||
|
client = AsyncMock()
|
||||||
|
client.covers_multiple = False
|
||||||
|
client.source_names = ()
|
||||||
|
client.list_documents.return_value = [
|
||||||
|
Document(id="id-one", content="", title="Capital region"),
|
||||||
|
Document(id="id-two", content="", title="Nobel laureates"),
|
||||||
|
]
|
||||||
|
client.count_documents.return_value = 2
|
||||||
|
|
||||||
|
modal = DocumentFilterModal(client=client)
|
||||||
|
app, _ = _make_app(temp_db_path, client)
|
||||||
|
with (
|
||||||
|
patch("haiku.rag.chat.app.HaikuRAG") as _stub_rag,
|
||||||
|
_covering_returns(_stub_rag, client),
|
||||||
|
):
|
||||||
|
async with app.run_test() as pilot:
|
||||||
|
await app.push_screen(modal)
|
||||||
|
await pilot.pause()
|
||||||
|
assert len(list(modal.query(DocumentCheckbox))) == 2
|
||||||
|
|
||||||
|
client.list_documents.return_value = [
|
||||||
|
Document(id="id-two", content="", title="Nobel laureates"),
|
||||||
|
]
|
||||||
|
client.count_documents.return_value = 1
|
||||||
|
await modal.on_input_submitted(Input.Submitted(Input(), "Nobel"))
|
||||||
|
await pilot.pause()
|
||||||
|
|
||||||
|
assert client.list_documents.await_args is not None
|
||||||
|
assert "nobel" in client.list_documents.await_args.kwargs["filter"]
|
||||||
|
labels = [str(b.label) for b in modal.query(DocumentCheckbox)]
|
||||||
|
assert labels == ["Nobel laureates"]
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue