From 6fc30c339205833b7467087c437ea718a7094914 Mon Sep 17 00:00:00 2001 From: Ocean Dekeyser Date: Thu, 22 Oct 2026 09:14:22 +0200 Subject: [PATCH] Tidy the buttons on the Projects screen and a Project's page (#81) Projects screen: only "New Project" next to the heading; the GitHub and Trello actions for all Projects are a "GitHub" and a "Trello" menu next to the Active/Archived filter. A Project's page: status badge, Open for choice, Archive/Restore and Delete grouped in one Status card; "Add Member" sits next to the Student select. Co-Authored-By: Claude Opus 5.5 --- frontend/src/pmp_frontend/projects_page.py | 76 +++++++++++++--------- frontend/src/pmp_frontend/repos.py | 17 ++--- frontend/src/pmp_frontend/trello.py | 21 +++--- frontend/tests/test_archive.py | 10 ++- 4 files changed, 77 insertions(+), 47 deletions(-) diff --git a/frontend/src/pmp_frontend/projects_page.py b/frontend/src/pmp_frontend/projects_page.py index 90ac378..b8e6980 100644 --- a/frontend/src/pmp_frontend/projects_page.py +++ b/frontend/src/pmp_frontend/projects_page.py @@ -133,18 +133,24 @@ async def create() -> None: ui.button("Create", on_click=create).mark("create-project") ui.button("Cancel", on_click=dialog.close).props("flat color=dark") + # The one everyday action next to the heading; the GitHub and Trello + # actions for all Projects in a menu each, by the filter (#81). with ui.row().classes("w-full items-center"): ui.label("Projects").classes("text-h4") ui.space() + ui.button("New Project", icon="add", on_click=dialog.open).mark( + "new-project" + ) + with ui.row().classes("w-full items-center gap-2"): + # Active Projects are the current work; Archived ones the history. + status_filter = ui.toggle( + {"active": "Active", "archived": "Archived"}, + value="active", + on_change=refresh, + ).mark("status-filter") + ui.space() await github_buttons(refresh) await trello_buttons(refresh) - ui.button("New Project", on_click=dialog.open).mark("new-project") - # Active Projects are the current work; Archived ones the history overview. - status_filter = ui.toggle( - {"active": "Active", "archived": "Archived"}, - value="active", - on_change=refresh, - ).mark("status-filter") table = ui.table(columns=COLUMNS, rows=[], row_key="id").classes("w-full") table.mark("projects") table.add_slot("body-cell-photo", THUMBNAIL_CELL) @@ -236,25 +242,36 @@ async def toggle_choice(event) -> None: archived = project["status"] == "archived" ui.link("← Projects", "/projects").classes("text-dark") heading = ui.label(project["title"]).classes("text-h4") - with ui.row().classes("items-center gap-4"): - ui.label(f"Status: {project['status'].capitalize()}") - ui.button( - "Restore" if archived else "Archive", on_click=change_status - ).props("outline color=dark").mark("change-status") - # Only Active Projects without Makers can be deleted (the backend - # also refuses one with Members, with a message). - if not archived and not project["makers"]: - ui.button("Delete", on_click=delete).props( - "outline color=negative" - ).mark("delete-project") - if not archived: - # Students can pick it in their Top 3 (spec #37). - ui.switch( - "Open for choice", - value=project["open_for_choice"], - on_change=toggle_choice, - ).mark("open-for-choice") - status_error = ErrorMessage() + # Status, Open for choice, Archive/Restore and Delete together in one + # card instead of loose under the title (#81). + with ui.card().classes("w-full max-w-lg").mark("status-card"): + with ui.row().classes("w-full items-center"): + ui.label("Status").classes("text-h6") + ui.badge( + project["status"].capitalize(), + color="grey-7" if archived else "positive", + ).mark("status-badge") + if not archived: + # Students can pick it in their Top 3 (spec #37). + ui.switch( + "Open for choice", + value=project["open_for_choice"], + on_change=toggle_choice, + ).mark("open-for-choice") + status_error = ErrorMessage() + with ui.row().classes("w-full items-center gap-2"): + ui.button( + "Restore" if archived else "Archive", + icon="unarchive" if archived else "archive", + on_click=change_status, + ).props("outline color=dark").mark("change-status") + ui.space() + # Only Active Projects without Makers can be deleted (the + # backend also refuses one with Members, with a message). + if not archived and not project["makers"]: + ui.button("Delete", icon="delete", on_click=delete).props( + "flat color=negative" + ).mark("delete-project") photo_card(project, editable=not archived) await repository_card(project, editable=not archived) await trello_card(project, editable=not archived) @@ -369,11 +386,12 @@ def members() -> None: ).mark("remove-member") members() - choice = ui.select({}, label="Add a Student", with_input=True).classes("w-full") - choice.mark("student-choice") + with ui.row().classes("w-full items-center no-wrap"): + choice = ui.select({}, label="Add a Student", with_input=True).classes("grow") + choice.mark("student-choice") + ui.button("Add Member", icon="person_add", on_click=add).mark("add-member") update_choices() add_error = ErrorMessage() - ui.button("Add Member", on_click=add).mark("add-member") def _read_only_details(project: dict[str, Any]) -> None: diff --git a/frontend/src/pmp_frontend/repos.py b/frontend/src/pmp_frontend/repos.py index 82700e3..d3ad5de 100644 --- a/frontend/src/pmp_frontend/repos.py +++ b/frontend/src/pmp_frontend/repos.py @@ -102,8 +102,8 @@ def show_results(title: str, lines: list[tuple[str, bool]]) -> None: async def github_buttons(after) -> None: - """ "Create repos for all Projects" and "Check all members", or why they are - missing; `after` reloads the Projects table.""" + """A "GitHub" menu with "Create repos for all Projects" and "Check all + members", or why it is missing; `after` reloads the Projects table.""" status = await api.github_status(token()) if not status["connected"]: ui.label("GitHub isn't connected.").classes("text-caption").mark( @@ -164,12 +164,13 @@ async def check_all() -> None: ) await after() - ui.button("Create repos for all Projects", on_click=create).props( - "outline color=dark" - ).mark("create-repos") - ui.button("Check all members", on_click=check_all).props("outline color=dark").mark( - "check-all-members" - ) + with ( + ui.dropdown_button("GitHub", icon="code") + .props("outline color=dark no-caps") + .mark("github-menu") + ): + ui.item("Create repos for all Projects", on_click=create).mark("create-repos") + ui.item("Check all members", on_click=check_all).mark("check-all-members") NEXT_STEP = { diff --git a/frontend/src/pmp_frontend/trello.py b/frontend/src/pmp_frontend/trello.py index 10faa32..163d7a4 100644 --- a/frontend/src/pmp_frontend/trello.py +++ b/frontend/src/pmp_frontend/trello.py @@ -76,8 +76,8 @@ async def access() -> None: async def trello_buttons(after) -> None: - """ "Create Trello boards for all Projects" and "Check all Trello members", or - why they are missing; `after` reloads the Projects table.""" + """A "Trello" menu with "Create Trello boards for all Projects" and "Check all + Trello members", or why it is missing; `after` reloads the Projects table.""" status = await api.trello_status(token()) if not status["connected"]: ui.label("Trello isn't connected.").classes("text-caption").mark( @@ -137,12 +137,17 @@ async def check_all() -> None: ) await after() - ui.button("Create Trello boards for all Projects", on_click=create).props( - "outline color=dark" - ).mark("create-trello-boards") - ui.button("Check all Trello members", on_click=check_all).props( - "outline color=dark" - ).mark("check-all-trello-members") + with ( + ui.dropdown_button("Trello", icon="view_kanban") + .props("outline color=dark no-caps") + .mark("trello-menu") + ): + ui.item("Create Trello boards for all Projects", on_click=create).mark( + "create-trello-boards" + ) + ui.item("Check all Trello members", on_click=check_all).mark( + "check-all-trello-members" + ) async def my_trello_card() -> None: diff --git a/frontend/tests/test_archive.py b/frontend/tests/test_archive.py index 2ea8f0e..a2712dd 100644 --- a/frontend/tests/test_archive.py +++ b/frontend/tests/test_archive.py @@ -10,6 +10,12 @@ ) +def status_badge(user: User) -> str | None: + """The text of the Status card's badge, once the page shows it.""" + found = user.find(marker="status-badge").elements + return next((badge.text for badge in found if isinstance(badge, ui.badge)), None) + + async def test_teacher_archives_and_restores_a_project(user: User): await ready_to_work(user, "teacher") await create_project(user, "Drone", 1, 3) @@ -24,7 +30,7 @@ async def test_teacher_archives_and_restores_a_project(user: User): user.find(marker="confirm").click() # Read-only, with the former Member listed as a Maker. - await user.should_see("Status: Archived", retries=30) + await eventually(lambda: status_badge(user) == "Archived") await user.should_see("Made by") await user.should_see("Student Account — Electronics-ICT, year 1") await user.should_not_see(marker="save-project") @@ -43,6 +49,6 @@ async def test_teacher_archives_and_restores_a_project(user: User): await user.should_see("Restore Drone?", retries=10) user.find(marker="confirm").click() - await user.should_see("Status: Active", retries=30) + await eventually(lambda: status_badge(user) == "Active") await user.should_see("Members (0 / 1–3)") await user.should_see("Made by")