From ebf659b26c7a7a5030f129c502ceba112e3ff172 Mon Sep 17 00:00:00 2001 From: Jim Lancaster <40281233+zoltan57@users.noreply.github.com> Date: Sun, 23 Aug 2026 15:28:41 -0500 Subject: [PATCH] V5.1 UI refinements --- docs/ui/pages/documents.md | 3 +- docs/ui/pages/people.md | 4 +-- docs/ui/pages/sources.md | 3 +- .../ui/components/table/people.py | 28 +++++++++---------- .../ui/components/table/sources.py | 4 +-- src/transcription/ui/pages/documents_page.py | 11 ++++++-- src/transcription/ui/pages/jobs_page.py | 4 +-- src/transcription/ui/pages/people_page.py | 9 ++++-- src/transcription/ui/pages/sources_page.py | 2 +- tests/ui/test_documents_page.py | 5 ++++ tests/ui/test_people_page.py | 16 +++++++---- tests/ui/test_sources_page.py | 2 +- 12 files changed, 58 insertions(+), 33 deletions(-) diff --git a/docs/ui/pages/documents.md b/docs/ui/pages/documents.md index 5fb4918..92cc92c 100644 --- a/docs/ui/pages/documents.md +++ b/docs/ui/pages/documents.md @@ -50,6 +50,7 @@ Optional: Rules: - Exact date must parse as `YYYY-MM-DD`; browser presentation may follow locale. +- The exact-date input is labeled **Document date**. - Existing people appear with disambiguating labels. - Tag assignment supports selecting existing tags and adding new labels inline. - **Create new person** opens Person creation. @@ -68,7 +69,7 @@ Rules: - The heading shows name, type, and internal ID. - The first Source, when present, appears in the dark-room viewer. -- Archival Metadata shows authors, Document Type, tags, compact Document date, 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, 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/people.md b/docs/ui/pages/people.md index 4d5ad6d..75431c9 100644 --- a/docs/ui/pages/people.md +++ b/docs/ui/pages/people.md @@ -20,8 +20,8 @@ People manages reusable historical-person records. A Person may appear in many D - The title is **Archival Entities: People**. - **Create new person** opens the create route. - The table defaults to Last Name order and supports search and column sorting. -- Columns are Last Name, First & Middle, FamilySearch ID, Birth Date, Death Date, and # Documents. -- Last Name and First & Middle are left-aligned; FamilySearch ID, date columns, and # Documents are centered. +- Columns are Last Name, First & Middle, Tags, FamilySearch ID, Birth Date, Death Date, and # Documents. +- Name and Tags are left-aligned; FamilySearch ID, date columns, and # Documents are centered. - # Documents reflects how many linked Documents each Person is connected to. - Birth and death values independently prefer exact date, then approximate date, then `Unknown`. - Selecting a row opens Person Detail. diff --git a/docs/ui/pages/sources.md b/docs/ui/pages/sources.md index f0ba3f1..7f6d399 100644 --- a/docs/ui/pages/sources.md +++ b/docs/ui/pages/sources.md @@ -22,6 +22,7 @@ The list accepts optional `document_id` and `job_id` query parameters. Document - Rows are ordered by page number and then upload name. - Columns are Document Name, Page Number, Upload Title, 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. - Selecting a row opens Source Detail. - No records displays `No source asset records found in repository.` @@ -39,7 +40,7 @@ The list accepts optional `document_id` and `job_id` query parameters. Document - Wide+narrow landscape images switch to a stacked left layout (image above Editable Revision) with metadata on the right. - Editable Revision is seeded from an existing revision or the preferred machine transcription. - Source Metadata shows upload name, stored filename, page number, Document Name, Document ID, and stored path. Source ID appears in the page-header subtitle. -- SourceJob Metadata shows latest status, Job ID, execution time, provider, model, prompt, and failure detail. +- SourceJob Metadata shows latest status (uppercase display), Job ID, execution time, provider, model, prompt, and failure detail. - Revision Logistics shows revised state, last-revised time, and upload time. - Candidate Machine Transcriptions appears below the image/revision area, remains compact until expanded, then compares it with the preferred machine result and requires confirmation before **Use this transcription**. diff --git a/src/transcription/ui/components/table/people.py b/src/transcription/ui/components/table/people.py index f2fc34d..49c58b4 100644 --- a/src/transcription/ui/components/table/people.py +++ b/src/transcription/ui/components/table/people.py @@ -19,8 +19,8 @@ class PersonTableRow: """Read model consumed by the people table component.""" id: UUID - last_name: str - given_names: str + name: str + tags: str family_search_id: str birth_date: str death_date: str @@ -31,8 +31,8 @@ def _serialize_rows(rows: Sequence[PersonTableRow]) -> list[dict[str, Any]]: return [ { "id": str(row.id), - "last_name": row.last_name, - "given_names": row.given_names, + "name": row.name, + "tags": row.tags or "Not set", "family_search_id": row.family_search_id or "Not set", "birth_date": row.birth_date or "Unknown", "death_date": row.death_date or "Unknown", @@ -53,18 +53,18 @@ def render_people_table(rows: Sequence[PersonTableRow]) -> None: rows=_serialize_rows(rows), columns=[ { - "name": "last_name", - "label": "Last Name", - "field": "last_name", + "name": "name", + "label": "Last Name, First & Middle", + "field": "name", "sortable": True, "classes": "font-serif font-semibold text-left ui-table-cell-wrap", }, { - "name": "given_names", - "label": "First & Middle", - "field": "given_names", + "name": "tags", + "label": "Tags", + "field": "tags", "sortable": True, - "classes": "font-serif font-semibold text-left ui-table-cell-wrap", + "classes": "text-left ui-table-cell-wrap", }, {"name": "family_search_id", "label": "FamilySearch ID", "field": "family_search_id", "sortable": True}, { @@ -92,14 +92,14 @@ def render_people_table(rows: Sequence[PersonTableRow]) -> None: "classes": "font-mono", }, ], - default_sort_by="last_name", - search_placeholder="Search people by last name, given names, FamilySearch ID, or dates...", + default_sort_by="name", + search_placeholder="Search people by name, tags, FamilySearch ID, or dates...", on_row_click_id=lambda person_id: ui.navigate.to(f"/people/{person_id}"), ) # Custom column template adding an archival entity icon next to person's name table.add_slot( - "body-cell-last_name", + "body-cell-name", r"""
diff --git a/src/transcription/ui/components/table/sources.py b/src/transcription/ui/components/table/sources.py index 5baa396..2fb1004 100644 --- a/src/transcription/ui/components/table/sources.py +++ b/src/transcription/ui/components/table/sources.py @@ -35,7 +35,7 @@ def _serialize_rows(rows: Sequence[SourceTableRow]) -> list[dict[str, Any]]: "upload_name": row.upload_name, "document_id": str(row.document_id), "document_name": row.document_name or "-", - "job_source_status": row.job_source_status or "-", + "job_source_status": (row.job_source_status or "-").upper(), "job_source_error_detail": row.job_source_error_detail or "-", } for row in rows @@ -109,7 +109,7 @@ def render_sources_table(rows: Sequence[SourceTableRow]) -> None: dense square size="sm" - :class="`ui-status ui-status--${props.value}`" + :class="`ui-status ui-status--${props.value.toLowerCase()}`" > {{ props.value }} diff --git a/src/transcription/ui/pages/documents_page.py b/src/transcription/ui/pages/documents_page.py index a912474..6d211c8 100644 --- a/src/transcription/ui/pages/documents_page.py +++ b/src/transcription/ui/pages/documents_page.py @@ -3,6 +3,7 @@ from __future__ import annotations from dataclasses import dataclass +from datetime import date from typing import Any from uuid import UUID @@ -448,7 +449,7 @@ def _render_document_form_fields( with ui.row().classes("w-full gap-3 grid grid-cols-1 md:grid-cols-2"): date_input = ( ui.input( - label="Exact date (YYYY-MM-DD)", + label="Document date", value=document.document_date.isoformat() if document and document.document_date else "", ) .props('outlined type="date"') @@ -542,7 +543,7 @@ def _render_bento_metadata_zone(document: Document) -> None: key=str.casefold, ) metadata_row("Tags:", ", ".join(tags) if tags else "Not set") - metadata_row("Document Date:", compact_date(document.document_date, document.document_date_raw)) + metadata_row("Document Date:", _detail_document_date(document.document_date, document.document_date_raw)) metadata_row("Location Created:", document.location_created or "Not set") metadata_row("Archive Identifier:", document.archive_identifier or "Not set") @@ -555,6 +556,12 @@ def _render_bento_metadata_zone(document: Document) -> None: ui.label(f"Updated: {document.updated_at.isoformat()}").classes("text-[11px] ui-text-muted") +def _detail_document_date(exact: date | None, approximate: str | None) -> str: + if exact is not None: + return exact.strftime("%m-%d-%Y") + return (approximate or "").strip() or "Unknown" + + def _render_bento_relations_zone(document: Document) -> None: with ui.column().classes("col-span-12 lg:col-span-4 gap-4"): _render_related_people_card(document) diff --git a/src/transcription/ui/pages/jobs_page.py b/src/transcription/ui/pages/jobs_page.py index 7863c7b..7441588 100644 --- a/src/transcription/ui/pages/jobs_page.py +++ b/src/transcription/ui/pages/jobs_page.py @@ -313,7 +313,7 @@ def register_page() -> None: # noqa: PLR0915 with archival_card(extra_classes="gap-2"): ui.label(f"Job ID: {job.id}").classes("text-sm font-semibold font-mono ui-text-primary") - metadata_row("Current Status:", job.status.value) + metadata_row("Current Status:", job.status.value.upper()) ui.label( "Cancel stops processing and marks remaining non-transcribed sources as cancelled. " "Cancelled sources can be resubmitted." @@ -369,7 +369,7 @@ def register_page() -> None: # noqa: PLR0915 with archival_card(extra_classes="gap-2"): ui.label(f"Job ID: {job.id}").classes("text-sm font-semibold font-mono ui-text-primary") - metadata_row("Current Status:", job.status.value) + metadata_row("Current Status:", job.status.value.upper()) metadata_row("Resubmittable Sources:", str(resubmittable_count)) ui.label( "Resubmit queues failed and cancelled linked sources. " diff --git a/src/transcription/ui/pages/people_page.py b/src/transcription/ui/pages/people_page.py index 1099d69..f9b47ec 100644 --- a/src/transcription/ui/pages/people_page.py +++ b/src/transcription/ui/pages/people_page.py @@ -98,8 +98,13 @@ def register_page() -> None: # noqa: PLR0915 rows = [ PersonTableRow( id=person.id, - last_name=person.last_name, - given_names=person.given_names, + name=f"{person.last_name}, {person.given_names}", + tags=", ".join( + sorted( + [link.tag_ref.label for link in person.person_tags if link.tag_ref is not None], + key=str.casefold, + ) + ), family_search_id=person.family_search_id or "", birth_date=compact_date(person.birth_date, person.birth_date_raw), death_date=compact_date(person.death_date, person.death_date_raw), diff --git a/src/transcription/ui/pages/sources_page.py b/src/transcription/ui/pages/sources_page.py index 01d59c0..6b1619f 100644 --- a/src/transcription/ui/pages/sources_page.py +++ b/src/transcription/ui/pages/sources_page.py @@ -456,7 +456,7 @@ def _render_source_job_metadata_zone( status = latest_job_source.status.value with ui.row().classes("w-full justify-between items-center mb-2"): ui.label("Latest Status").classes("text-xs ui-text-muted") - archival_badge(status) + archival_badge(status.upper()) metadata_row("Job ID:", str(latest_job_source.job_id)) metadata_row( diff --git a/tests/ui/test_documents_page.py b/tests/ui/test_documents_page.py index bea00ce..4ae2906 100644 --- a/tests/ui/test_documents_page.py +++ b/tests/ui/test_documents_page.py @@ -36,6 +36,7 @@ async def seed_person_and_document(): name="Letter from Hig", document_type_id=letter_type.id, archive_identifier="ZC-1924-001", + document_date=date(1924, 7, 4), ) session.add(doc) await session.flush() @@ -148,6 +149,8 @@ class TestDocumentsPageRendering: assert "Zenna Cochran" in response.text assert "Document Type:" in response.text assert "Letter" in response.text + assert "07-04-1924" in response.text + assert "1924-07-04" not in response.text assert "PIPELINE JOBS" in response.text.upper() assert "Edit Document" in response.text @@ -183,6 +186,8 @@ class TestDocumentsPageRendering: assert response.status_code == 200 assert "Edit Document Record" in response.text assert "Letter from Hig" in response.text + assert "Document date" in response.text + assert "Exact date (YYYY-MM-DD)" not in response.text assert "ZC-1924-001" in response.text @pytest.mark.asyncio diff --git a/tests/ui/test_people_page.py b/tests/ui/test_people_page.py index 97e6e8f..bb05add 100644 --- a/tests/ui/test_people_page.py +++ b/tests/ui/test_people_page.py @@ -11,9 +11,11 @@ from transcription.db import session_scope from transcription.db.models import Document from transcription.db.models import DocumentPerson from transcription.db.models import Person +from transcription.db.models import PersonTag from transcription.db.models import PersonRole from transcription.db.models import Photo from transcription.db.models import Source +from transcription.db.models import Tag @pytest.mark.integration @@ -35,16 +37,20 @@ class TestPeoplePageRendering: _, client = app_client async with session_scope() as session: - session.add(Person(given_names="Ada", last_name="Lovelace")) + person = Person(given_names="Ada", last_name="Lovelace") + tag = Tag(label="Pioneer", normalized_label="pioneer") + session.add_all([person, tag]) + await session.flush() + session.add(PersonTag(person_id=person.id, tag_id=tag.id)) await session.commit() response = client.get("/ui/people") assert response.status_code == 200 - assert "Lovelace" in response.text - assert "Ada" in response.text - assert "Last Name" in response.text - assert "First & Middle" in response.text or "First & Middle" in response.text + assert "Lovelace, Ada" in response.text + assert "Last Name, First & Middle" in response.text or "Last Name, First & Middle" in response.text + assert "Tags" in response.text + assert "Pioneer" in response.text assert "FamilySearch ID" in response.text assert "# Documents" in response.text diff --git a/tests/ui/test_sources_page.py b/tests/ui/test_sources_page.py index 266bed7..d4ac1ee 100644 --- a/tests/ui/test_sources_page.py +++ b/tests/ui/test_sources_page.py @@ -266,7 +266,7 @@ class TestSourcesPageRendering: assert response.status_code == 200 assert "job-failed-page.png" in response.text - assert "failed" in response.text.lower() + assert "FAILED" in response.text assert "Provider timed out" in response.text @pytest.mark.asyncio