From 96af7b2dc0ad04ec16b151846a9bf97f1865d187 Mon Sep 17 00:00:00 2001 From: Jim Lancaster <40281233+zoltan57@users.noreply.github.com> Date: Mon, 24 Aug 2026 12:58:34 -0500 Subject: [PATCH] Minor UI refinement --- docs/ui/pages/documents.md | 2 +- docs/ui/pages/sources.md | 2 +- .../ui/components/table/sources.py | 12 +++--- src/transcription/ui/pages/documents_page.py | 11 +++++- src/transcription/ui/pages/people_page.py | 23 +++++++---- tests/ui/test_documents_page.py | 2 + tests/ui/test_people_page.py | 39 +++++++++++++++++++ tests/ui/test_sources_page.py | 10 +++++ 8 files changed, 85 insertions(+), 16 deletions(-) diff --git a/docs/ui/pages/documents.md b/docs/ui/pages/documents.md index 2a4361b..3f2eb6c 100644 --- a/docs/ui/pages/documents.md +++ b/docs/ui/pages/documents.md @@ -71,7 +71,7 @@ Rules: - The heading shows name, type, and internal ID. - The header includes a contextual back action: **Back to Documents** by default, **Back to Person** when opened from Person Detail, and **Back to Job** when opened from Job Detail. - The first Source, when present, appears in the dark-room viewer. -- Archival Metadata shows authors, Document Type, tags, Document date (`MM-DD-YYYY` for exact dates), location, and archive identifier. Notes appear in a separate archival-notes block within the same card. +- Archival Metadata shows authors, Document Type, tags, Document date (`MM-DD-YYYY` for exact dates), location (linked to Google Maps when present), and archive identifier. Notes appear in a separate archival-notes block within the same card. - System Logistics shows created and updated timestamps. - Related People are grouped by role and link to Person Detail. - **Sources & Pipeline Jobs** shows counts and actions for filtered Sources, Document Jobs, and adding a Job. diff --git a/docs/ui/pages/sources.md b/docs/ui/pages/sources.md index 7f6d399..564a0bc 100644 --- a/docs/ui/pages/sources.md +++ b/docs/ui/pages/sources.md @@ -20,7 +20,7 @@ The list accepts optional `document_id` and `job_id` query parameters. Document - Global context provides **Create Job**. - Filtered context provides **Back to Document** or **Back to Job**. - Rows are ordered by page number and then upload name. -- Columns are Document Name, Page Number, Upload Title, Status, and Error Detail. +- Columns are Upload Title, Page Number, Document Name, Status, and Error Detail. - Document Name, Upload Title, and Error Detail are left-aligned; Status is centered. - Status labels are presented in uppercase for consistency with Jobs. - Stored Filename is intentionally absent from the list. diff --git a/src/transcription/ui/components/table/sources.py b/src/transcription/ui/components/table/sources.py index 2fb1004..98f6805 100644 --- a/src/transcription/ui/components/table/sources.py +++ b/src/transcription/ui/components/table/sources.py @@ -53,9 +53,9 @@ def render_sources_table(rows: Sequence[SourceTableRow]) -> None: rows=_serialize_rows(rows), columns=[ { - "name": "document_name", - "label": "Document Name", - "field": "document_name", + "name": "upload_name", + "label": "Upload Title", + "field": "upload_name", "sortable": True, "classes": "font-serif text-left ui-table-cell-wrap", "align": "left", @@ -69,9 +69,9 @@ def render_sources_table(rows: Sequence[SourceTableRow]) -> None: "style": "width: 10%;", }, { - "name": "upload_name", - "label": "Upload Title", - "field": "upload_name", + "name": "document_name", + "label": "Document Name", + "field": "document_name", "sortable": True, "classes": "font-serif text-left ui-table-cell-wrap", "align": "left", diff --git a/src/transcription/ui/pages/documents_page.py b/src/transcription/ui/pages/documents_page.py index 82fda73..edc6d52 100644 --- a/src/transcription/ui/pages/documents_page.py +++ b/src/transcription/ui/pages/documents_page.py @@ -27,10 +27,12 @@ from transcription.ui.components.confirm_delete import dependency_summary from transcription.ui.components.confirm_delete import render_delete_actions from transcription.ui.components.confirm_delete import render_delete_blocked_notice from transcription.ui.components.data_display import archival_badge +from transcription.ui.components.data_display import metadata_link_row from transcription.ui.components.data_display import metadata_row from transcription.ui.components.error_presenter import run_ui_action from transcription.ui.components.error_presenter import show_error from transcription.ui.components.formatters import compact_date +from transcription.ui.components.formatters import google_maps_search_url from transcription.ui.components.formatters import parse_iso_date from transcription.ui.components.formatters import parse_uuid from transcription.ui.components.guards import parsed_record_id @@ -560,7 +562,14 @@ def _render_bento_metadata_zone(document: Document) -> None: ) metadata_row("Tags:", ", ".join(tags) if tags else "Not set") metadata_row("Document Date:", _detail_document_date(document.document_date, document.document_date_raw)) - metadata_row("Location Created:", document.location_created or "Not set") + if document.location_created: + metadata_link_row( + "Location Created:", + document.location_created, + google_maps_search_url(document.location_created), + ) + else: + metadata_row("Location Created:", "Not set") metadata_row("Archive Identifier:", document.archive_identifier or "Not set") with ui.column().classes("w-full mt-2"): diff --git a/src/transcription/ui/pages/people_page.py b/src/transcription/ui/pages/people_page.py index 0fc69f8..8b59281 100644 --- a/src/transcription/ui/pages/people_page.py +++ b/src/transcription/ui/pages/people_page.py @@ -655,13 +655,22 @@ async def _render_person_photo_zone( active_index = [0] with ui.column().classes("col-span-12 lg:col-span-4"), archival_card(title="Photos", extra_classes="gap-3"): - _render_photo_viewer_with_navigation( - photos=photos, - active_index=active_index, - settings=settings, - request=request, - empty_message="No portrait photo uploaded yet.", - ) + + @ui.refreshable + def render_photo_viewer() -> None: + def refresh_photo_viewer() -> None: + render_photo_viewer.refresh() + + _render_photo_viewer_with_navigation( + photos=photos, + active_index=active_index, + settings=settings, + request=request, + empty_message="No portrait photo uploaded yet.", + on_change=refresh_photo_viewer, + ) + + render_photo_viewer() def _shift_gallery_index(*, photos: list, active_index: list[int], step: int) -> None: diff --git a/tests/ui/test_documents_page.py b/tests/ui/test_documents_page.py index d4a2820..0486a50 100644 --- a/tests/ui/test_documents_page.py +++ b/tests/ui/test_documents_page.py @@ -37,6 +37,7 @@ async def seed_person_and_document(): document_type_id=letter_type.id, archive_identifier="ZC-1924-001", document_date=date(1924, 7, 4), + location_created="Salt Lake City, Utah", ) session.add(doc) await session.flush() @@ -151,6 +152,7 @@ class TestDocumentsPageRendering: assert "Letter" in response.text assert "07-04-1924" in response.text assert "1924-07-04" not in response.text + assert "google.com/maps/search/?api=1&query=Salt+Lake+City%2C+Utah" in response.text assert "PIPELINE JOBS" in response.text.upper() assert "Edit Document" in response.text assert "Back to Documents" in response.text diff --git a/tests/ui/test_people_page.py b/tests/ui/test_people_page.py index ee24aaf..1e95b79 100644 --- a/tests/ui/test_people_page.py +++ b/tests/ui/test_people_page.py @@ -186,6 +186,45 @@ class TestPeoplePageRendering: assert response.text.count("Edit Photo(s)") == 1 assert "Upload photo(s)" not in response.text + @pytest.mark.asyncio + async def test_person_detail_page_shows_gallery_navigation_for_multiple_photos(self, app_client): + app, client = app_client + upload_dirs = {app.state.settings.upload_dir, get_settings().upload_dir} + for upload_dir in upload_dirs: + primary_file = upload_dir / "photos" / "portrait-primary.png" + secondary_file = upload_dir / "photos" / "portrait-secondary.png" + primary_file.parent.mkdir(parents=True, exist_ok=True) + primary_file.write_bytes(b"portrait-primary") + secondary_file.write_bytes(b"portrait-secondary") + + async with session_scope() as session: + person = Person(given_names="Gallery", last_name="Navigation") + session.add(person) + await session.flush() + session.add_all( + [ + Photo( + person_id=person.id, + path="photos/portrait-primary.png", + is_primary=True, + ), + Photo( + person_id=person.id, + path="photos/portrait-secondary.png", + is_primary=False, + ), + ] + ) + await session.commit() + person_id = str(person.id) + + response = client.get(f"/ui/people/{person_id}") + + assert response.status_code == 200 + assert "Previous" in response.text + assert "Next" in response.text + assert "1 of 2" in response.text + @pytest.mark.asyncio async def test_person_photos_page_renders_photo_management_controls(self, app_client): app, client = app_client diff --git a/tests/ui/test_sources_page.py b/tests/ui/test_sources_page.py index d4ac1ee..6677a4b 100644 --- a/tests/ui/test_sources_page.py +++ b/tests/ui/test_sources_page.py @@ -1,5 +1,6 @@ """Tests for the sources page routes and Source model properties.""" +import re from datetime import UTC from datetime import datetime from pathlib import Path @@ -198,6 +199,15 @@ class TestSourcesPageRendering: assert "page_one.png" in response.text assert "Source Document" in response.text assert "Stored Filename" not in response.text + assert re.search( + r'"name":"upload_name","label":"Upload Title".*' + r'"name":"page_number","label":"Page Number".*' + r'"name":"document_name","label":"Document Name".*' + r'"name":"job_source_status","label":"Status".*' + r'"name":"job_source_error_detail","label":"Error Detail"', + response.text, + re.DOTALL, + ) @pytest.mark.asyncio async def test_sources_page_filters_to_document_context(self, app_client):