Proposal page: the issue page's sidebar 8e96499f2705
Sections with headings; Reviewers and Labels as gear menus, filterable, applied at once (the reviewers form takes action=set with the whole list; the people offered are those who can be assigned, less the author); the gear icon shared by both pages. Test. closes #84
- Author
- Andy McCall @andymccall <[email protected]>
- When
- · 2026-10-02 18:19 UTC
- Signature
- ✔ signed by Andy McCall
- Hash
8e96499f270589313852b3f68508c023ece9f0f4593ff17d570be42232d612e0- Depends on
- 145306044896 18e381ea88c2 38833ed78707 4da8b828a822 749d9a5fcd9f a1b9869565aa b5668108487f b64e2537d702 cb71e18eb6f9 cc1cd197df1d d23464c0ac60 de505df5cf8d e1ec9fbf538e
- Depended on by
- a2ef53c5db26 b5a0a69b3622 ffb67a81188c 3ea0b8af2d46 206c1fc1b06a 7d2eb590051f 991b55638aaf 9e50caedf4db 4f37afcc3583 ea85398a51bc
- In bookmarks
- api-unknown-fields cancel-landed-runs clear-waiting close-proposal-140 diffusion-pin-bookmark draft-ready image-check job-waits main oh-comment-body-file proposal-list-fast run-one-job wiki-says-so
- In tags
- v0.3.1 v0.4.0 v0.4.1 v0.4.2 v0.5.0 v0.5.1 v0.5.2 v0.6.0 v0.6.1 v0.7.0 v0.8.0 v0.9.0 v0.9.1 v0.9.11 v0.9.15 v0.9.16 v0.9.19 v0.9.2 v0.9.20 v0.9.21 v0.9.22 v0.9.23 v0.9.24 v0.9.3 v0.9.4 v0.9.5 v0.9.6 v0.9.7 v0.9.8 v0.9.9
- Edits
- 24 atoms, 1 tree operation, 7 files
Browse the tree at this change
added templates/gear.html +1 −0
| 1 | + | <svg viewBox="0 0 16 16" width="16" height="16" aria-hidden="true"><path fill="currentColor" d="M8 0a8.2 8.2 0 0 1 .7.03c.4.04.7.34.76.73l.2 1.37c.33.11.65.24.96.4l1.12-.83a.85.85 0 0 1 1.07.06c.34.3.65.63.94.97.25.3.27.73.06 1.06l-.83 1.12c.16.3.29.63.4.96l1.37.2c.39.06.69.37.73.76a8 8 0 0 1 0 1.4.85.85 0 0 1-.73.76l-1.37.2c-.11.33-.24.65-.4.96l.83 1.12c.21.33.19.76-.06 1.06-.29.34-.6.67-.94.97a.85.85 0 0 1-1.07.06l-1.12-.83c-.3.16-.63.29-.96.4l-.2 1.37a.85.85 0 0 1-.76.73 8 8 0 0 1-1.4 0 .85.85 0 0 1-.76-.73l-.2-1.37a5.6 5.6 0 0 1-.96-.4l-1.12.83a.85.85 0 0 1-1.07-.06 8 8 0 0 1-.94-.97.85.85 0 0 1-.06-1.06l.83-1.12a5.6 5.6 0 0 1-.4-.96l-1.37-.2A.85.85 0 0 1 0 8.7a8 8 0 0 1 0-1.4c.04-.39.34-.7.73-.76l1.37-.2c.11-.33.24-.65.4-.96l-.83-1.12a.85.85 0 0 1 .06-1.06c.29-.34.6-.67.94-.97a.85.85 0 0 1 1.07-.06l1.12.83c.3-.16.63-.29.96-.4l.2-1.37A.85.85 0 0 1 7.3.03 8 8 0 0 1 8 0Zm0 5a3 3 0 1 0 0 6 3 3 0 0 0 0-6Z"/></svg> |
modified CHANGELOG.md +2 −0
| … | 3 unchanged lines |
||
| 4 | 4 | ||
| 5 | 5 | ## Unreleased |
|
| 6 | 6 | ||
| 7 | + | - **The proposal page's sidebar matches the issue page's (#84).** Sections with headings; Reviewers and Labels open filterable menus from a gear (people with access to the repository, with avatars; labels with colour and description) that apply in one go; requested reviewers show with avatars; Landing, Checks, Approvals, Projects, Code owners and Depends on as sections. The reviewers form takes the whole list (`action=set`). |
|
| 8 | + | ||
| 7 | 9 | - **Device sign-in says it is done (#83).** After Approve, `/device` shows "Signed in: oxy on saturn is now signed in as you; return to it, there is nothing to copy or enter" with a link to its token, instead of the code form again, which read as if the code were needed a second time. Denying shows a matching "Denied" card. |
|
| 8 | 10 | ||
| 9 | 11 | - **The issue page's sidebar, laid out as GitHub's (#82).** Assignees, Labels and Milestone each show what is set under their heading (avatars, coloured label pills) with a gear opening a menu: a filterable checklist (people who can be assigned, with avatars; labels with their colour and description; a milestone) that applies in one go. "Assign yourself" when nobody is assigned. Close, Not planned, Lock and Pin are quiet rows with icons at the foot, evenly spaced, and the comment box is as wide as the conversation. |
|
| … | 39 unchanged lines |
modified crates/hub/src/web/proposals.rs +37 −8
| … | 634 unchanged lines |
||
| 635 | 635 | projects: Vec<(db::projects::Project, String)>, |
|
| 636 | 636 | /// Asked to review and not yet reviewing. |
|
| 637 | 637 | requested: Vec<String>, |
|
| 638 | + | /// Who can be asked to review, for the sidebar's menu (oxygenhub#84). |
|
| 639 | + | reviewable: Vec<String>, |
|
| 638 | 640 | /// The viewer may ask for reviews (the author, or a writer). |
|
| 639 | 641 | can_request: bool, |
|
| 640 | 642 | } |
|
| … | 67 unchanged lines |
||
| 708 | 710 | let can_edit = me.is_some_and(|a| Some(a.id) == p.author_id || role >= Role::Maintain); |
|
| 709 | 711 | let can_land = role >= Role::Write && p.is_open() && v.blockers.is_empty(); |
|
| 710 | 712 | let can_request = p.is_open() && me.is_some_and(|a| Some(a.id) == p.author_id || role >= Role::Write); |
|
| 713 | + | let requested = pdb::review_requests(&app.db, p.id).await.unwrap_or_default(); |
|
| 714 | + | let mut reviewable: Vec<String> = if can_request { |
|
| 715 | + | accounts::assignable(&app.db, row.id, &row.owner).await.unwrap_or_default().into_iter().filter(|u| *u != p.author).collect() |
|
| 716 | + | } else { |
|
| 717 | + | Vec::new() |
|
| 718 | + | }; |
|
| 719 | + | for r in &requested { |
|
| 720 | + | if !reviewable.iter().any(|x| x.eq_ignore_ascii_case(r)) { |
|
| 721 | + | reviewable.push(r.clone()); |
|
| 722 | + | } |
|
| 723 | + | } |
|
| 711 | 724 | let delegated = match viewer.account() { |
|
| 712 | 725 | Some(a) => super::edit::delegated_identity(app, a.id).await.is_some(), |
|
| 713 | 726 | None => false, |
|
| … | 72 unchanged lines |
||
| 786 | 799 | all_labels: db::issues::labels(&app.db, row.id).await.unwrap_or_default(), |
|
| 787 | 800 | checks: db::hooks::checks_for_state(&app.db, row.id, &loaded.state).await.unwrap_or_default(), |
|
| 788 | 801 | required_checks: db::hooks::required_checks(&app.db, row.id).await.unwrap_or_default(), |
|
| 789 | - | requested: pdb::review_requests(&app.db, loaded.row.id).await.unwrap_or_default(), |
|
| 802 | + | requested, |
|
| 803 | + | reviewable, |
|
| 790 | 804 | can_request, |
|
| 791 | 805 | })) |
|
| 792 | 806 | } |
|
| … | 561 unchanged lines |
||
| 1354 | 1368 | pub struct ReviewersForm { |
|
| 1355 | 1369 | #[serde(rename = "_csrf")] |
|
| 1356 | 1370 | csrf_token: String, |
|
| 1357 | - | /// `add` or `remove`. |
|
| 1371 | + | /// `add`, `remove`, or `set` (the menu: exactly these are asked). |
|
| 1358 | 1372 | action: String, |
|
| 1359 | - | username: String, |
|
| 1373 | + | #[serde(default)] |
|
| 1374 | + | username: Vec<String>, |
|
| 1360 | 1375 | } |
|
| 1361 | 1376 | ||
| 1362 | 1377 | /// `POST /{owner}/{name}/proposal/{number}/reviewers`: the author or a |
|
| … | 2 unchanged lines |
||
| 1365 | 1380 | State(app): State<Shared>, |
|
| 1366 | 1381 | viewer: Viewer, |
|
| 1367 | 1382 | Path((owner, name, number)): Path<(String, String, i64)>, |
|
| 1368 | - | Form(f): Form<ReviewersForm>, |
|
| 1383 | + | axum_extra::extract::Form(f): axum_extra::extract::Form<ReviewersForm>, |
|
| 1369 | 1384 | ) -> Result<Response, Error> { |
|
| 1370 | 1385 | let shell = Shell::new(&app, &format!("/{owner}/{name}/proposal/{number}"), &viewer); |
|
| 1371 | 1386 | let me = require(&viewer, &shell)?; |
|
| … | 6 unchanged lines |
||
| 1378 | 1393 | if Some(me.account.id) != p.author_id && role < Role::Write { |
|
| 1379 | 1394 | return Err(Error::not_found(shell)); |
|
| 1380 | 1395 | } |
|
| 1381 | - | let who = f.username.trim().trim_start_matches('@').to_string(); |
|
| 1382 | - | if f.action == "remove" { |
|
| 1383 | - | if let Some(a) = accounts::account_by_name(&app.db, &who).await.ok().flatten() { |
|
| 1396 | + | let names: Vec<String> = f.username.iter().map(|n| n.trim().trim_start_matches('@').to_string()).filter(|n| !n.is_empty()).collect(); |
|
| 1397 | + | let current = pdb::review_requests(&app.db, p.id).await.map_err(|e| Error::from_db(shell.clone(), e))?; |
|
| 1398 | + | // Withdrawn: those named (remove), or those the menu left out (set). |
|
| 1399 | + | let withdraw: Vec<String> = match f.action.as_str() { |
|
| 1400 | + | "remove" => names.clone(), |
|
| 1401 | + | "set" => current.iter().filter(|c| !names.iter().any(|n| n.eq_ignore_ascii_case(c))).cloned().collect(), |
|
| 1402 | + | _ => Vec::new(), |
|
| 1403 | + | }; |
|
| 1404 | + | for who in &withdraw { |
|
| 1405 | + | if let Some(a) = accounts::account_by_name(&app.db, who).await.ok().flatten() { |
|
| 1384 | 1406 | if pdb::unrequest_review(&app.db, p.id, a.id).await.map_err(|e| Error::from_db(shell.clone(), e))? { |
|
| 1385 | 1407 | pdb::add_event(&app.db, p.id, &me.account.username, "review request withdrawn", &a.username).await.ok(); |
|
| 1386 | 1408 | } |
|
| 1387 | 1409 | } |
|
| 1410 | + | } |
|
| 1411 | + | let ask: Vec<String> = match f.action.as_str() { |
|
| 1412 | + | "add" => names, |
|
| 1413 | + | "set" => names.into_iter().filter(|n| !current.iter().any(|c| c.eq_ignore_ascii_case(n))).collect(), |
|
| 1414 | + | _ => return Ok(back(&owner, &name, number, "conversation", None, None)), |
|
| 1415 | + | }; |
|
| 1416 | + | if ask.is_empty() { |
|
| 1388 | 1417 | return Ok(back(&owner, &name, number, "conversation", None, None)); |
|
| 1389 | 1418 | } |
|
| 1390 | - | match request_reviewers(&app, &row, &p, &me.account, &[who]).await { |
|
| 1419 | + | match request_reviewers(&app, &row, &p, &me.account, &ask).await { |
|
| 1391 | 1420 | Ok(asked) if asked.is_empty() => Ok(back(&owner, &name, number, "conversation", Some("Already asked.".into()), None)), |
|
| 1392 | 1421 | Ok(asked) => Ok(back(&owner, &name, number, "conversation", Some(format!("Asked {} to review.", asked.join(", "))), None)), |
|
| 1393 | 1422 | Err(e) => Ok(back(&owner, &name, number, "conversation", None, Some(e))), |
|
| … | 2 unchanged lines |
modified crates/hub/tests/api.rs +14 −0
| … | 408 unchanged lines |
||
| 409 | 409 | assert_eq!(st, StatusCode::NO_CONTENT); |
|
| 410 | 410 | let (_, v) = api(app, "GET", "/api/v1/repos/andy/demo/proposals/2", None, None).await; |
|
| 411 | 411 | assert_eq!(v["requested_reviewers"], serde_json::json!([])); |
|
| 412 | + | // oxygenhub#84: the page's menu sets who is asked, all at once. |
|
| 413 | + | let page = call(app, "GET", "/andy/demo/proposal/2", None, Some(&bob), None, None).await; |
|
| 414 | + | assert!( |
|
| 415 | + | page.body.contains("Request reviews") && page.body.contains("name=\"username\" value=\"andy\""), |
|
| 416 | + | "the reviewers menu lists andy" |
|
| 417 | + | ); |
|
| 418 | + | let csrf = csrf_of(&page.body); |
|
| 419 | + | let form = |list: &str| format!("_csrf={csrf}&action=set{list}"); |
|
| 420 | + | call(app, "POST", "/andy/demo/proposal/2/reviewers", None, Some(&bob), None, Some(&form("&username=andy"))).await; |
|
| 421 | + | let (_, v) = api(app, "GET", "/api/v1/repos/andy/demo/proposals/2", None, None).await; |
|
| 422 | + | assert_eq!(v["requested_reviewers"], serde_json::json!(["andy"])); |
|
| 423 | + | call(app, "POST", "/andy/demo/proposal/2/reviewers", None, Some(&bob), None, Some(&form(""))).await; |
|
| 424 | + | let (_, v) = api(app, "GET", "/api/v1/repos/andy/demo/proposals/2", None, None).await; |
|
| 425 | + | assert_eq!(v["requested_reviewers"], serde_json::json!([]), "an empty menu withdraws them"); |
|
| 412 | 426 | ||
| 413 | 427 | // A source bookmark with `/` in its name, as a feature/N-name workflow |
|
| 414 | 428 | // makes them: by name, and as this repository's owner/name:bookmark. |
|
| … | 916 unchanged lines |
modified templates/issue.html +3 −3
| … | 46 unchanged lines |
||
| 47 | 47 | {% endif %} |
|
| 48 | 48 | <section class="side-sec"> |
|
| 49 | 49 | <div class="side-head"><h3>Assignees</h3> |
|
| 50 | - | {% if can_triage %}<details class="menu"><summary class="gear" title="Edit assignees" aria-label="Edit assignees"><svg viewBox="0 0 16 16" width="16" height="16" aria-hidden="true"><path fill="currentColor" d="M8 0a8.2 8.2 0 0 1 .7.03c.4.04.7.34.76.73l.2 1.37c.33.11.65.24.96.4l1.12-.83a.85.85 0 0 1 1.07.06c.34.3.65.63.94.97.25.3.27.73.06 1.06l-.83 1.12c.16.3.29.63.4.96l1.37.2c.39.06.69.37.73.76a8 8 0 0 1 0 1.4.85.85 0 0 1-.73.76l-1.37.2c-.11.33-.24.65-.4.96l.83 1.12c.21.33.19.76-.06 1.06-.29.34-.6.67-.94.97a.85.85 0 0 1-1.07.06l-1.12-.83c-.3.16-.63.29-.96.4l-.2 1.37a.85.85 0 0 1-.76.73 8 8 0 0 1-1.4 0 .85.85 0 0 1-.76-.73l-.2-1.37a5.6 5.6 0 0 1-.96-.4l-1.12.83a.85.85 0 0 1-1.07-.06 8 8 0 0 1-.94-.97.85.85 0 0 1-.06-1.06l.83-1.12a5.6 5.6 0 0 1-.4-.96l-1.37-.2A.85.85 0 0 1 0 8.7a8 8 0 0 1 0-1.4c.04-.39.34-.7.73-.76l1.37-.2c.11-.33.24-.65.4-.96l-.83-1.12a.85.85 0 0 1 .06-1.06c.29-.34.6-.67.94-.97a.85.85 0 0 1 1.07-.06l1.12.83c.3-.16.63-.29.96-.4l.2-1.37A.85.85 0 0 1 7.3.03 8 8 0 0 1 8 0Zm0 5a3 3 0 1 0 0 6 3 3 0 0 0 0-6Z"/></svg></summary> |
|
| 50 | + | {% if can_triage %}<details class="menu"><summary class="gear" title="Edit assignees" aria-label="Edit assignees">{% include "gear.html" %}</summary> |
|
| 51 | 51 | <form class="menu-pop" method="post" action="{{ repo.url }}/issue/{{ i.number }}/assign"><input type="hidden" name="_csrf" value="{{ shell.csrf }}"><input type="hidden" name="action" value="set"> |
|
| 52 | 52 | <p class="menu-title">Assign people to this issue</p> |
|
| 53 | 53 | <input class="menu-filter" type="search" placeholder="Filter people" aria-label="Filter people" data-menu-filter> |
|
| … | 6 unchanged lines |
||
| 60 | 60 | </section> |
|
| 61 | 61 | <section class="side-sec"> |
|
| 62 | 62 | <div class="side-head"><h3>Labels</h3> |
|
| 63 | - | {% if can_triage %}<details class="menu"><summary class="gear" title="Edit labels" aria-label="Edit labels"><svg viewBox="0 0 16 16" width="16" height="16" aria-hidden="true"><path fill="currentColor" d="M8 0a8.2 8.2 0 0 1 .7.03c.4.04.7.34.76.73l.2 1.37c.33.11.65.24.96.4l1.12-.83a.85.85 0 0 1 1.07.06c.34.3.65.63.94.97.25.3.27.73.06 1.06l-.83 1.12c.16.3.29.63.4.96l1.37.2c.39.06.69.37.73.76a8 8 0 0 1 0 1.4.85.85 0 0 1-.73.76l-1.37.2c-.11.33-.24.65-.4.96l.83 1.12c.21.33.19.76-.06 1.06-.29.34-.6.67-.94.97a.85.85 0 0 1-1.07.06l-1.12-.83c-.3.16-.63.29-.96.4l-.2 1.37a.85.85 0 0 1-.76.73 8 8 0 0 1-1.4 0 .85.85 0 0 1-.76-.73l-.2-1.37a5.6 5.6 0 0 1-.96-.4l-1.12.83a.85.85 0 0 1-1.07-.06 8 8 0 0 1-.94-.97.85.85 0 0 1-.06-1.06l.83-1.12a5.6 5.6 0 0 1-.4-.96l-1.37-.2A.85.85 0 0 1 0 8.7a8 8 0 0 1 0-1.4c.04-.39.34-.7.73-.76l1.37-.2c.11-.33.24-.65.4-.96l-.83-1.12a.85.85 0 0 1 .06-1.06c.29-.34.6-.67.94-.97a.85.85 0 0 1 1.07-.06l1.12.83c.3-.16.63-.29.96-.4l.2-1.37A.85.85 0 0 1 7.3.03 8 8 0 0 1 8 0Zm0 5a3 3 0 1 0 0 6 3 3 0 0 0 0-6Z"/></svg></summary> |
|
| 63 | + | {% if can_triage %}<details class="menu"><summary class="gear" title="Edit labels" aria-label="Edit labels">{% include "gear.html" %}</summary> |
|
| 64 | 64 | <form class="menu-pop" method="post" action="{{ repo.url }}/issue/{{ i.number }}/labels"><input type="hidden" name="_csrf" value="{{ shell.csrf }}"> |
|
| 65 | 65 | <p class="menu-title">Apply labels to this issue</p> |
|
| 66 | 66 | {% if !all_labels.is_empty() %}<input class="menu-filter" type="search" placeholder="Filter labels" aria-label="Filter labels" data-menu-filter>{% endif %} |
|
| … | 5 unchanged lines |
||
| 72 | 72 | </section> |
|
| 73 | 73 | <section class="side-sec"> |
|
| 74 | 74 | <div class="side-head"><h3>Milestone</h3> |
|
| 75 | - | {% if can_triage && !milestones.is_empty() %}<details class="menu"><summary class="gear" title="Set milestone" aria-label="Set milestone"><svg viewBox="0 0 16 16" width="16" height="16" aria-hidden="true"><path fill="currentColor" d="M8 0a8.2 8.2 0 0 1 .7.03c.4.04.7.34.76.73l.2 1.37c.33.11.65.24.96.4l1.12-.83a.85.85 0 0 1 1.07.06c.34.3.65.63.94.97.25.3.27.73.06 1.06l-.83 1.12c.16.3.29.63.4.96l1.37.2c.39.06.69.37.73.76a8 8 0 0 1 0 1.4.85.85 0 0 1-.73.76l-1.37.2c-.11.33-.24.65-.4.96l.83 1.12c.21.33.19.76-.06 1.06-.29.34-.6.67-.94.97a.85.85 0 0 1-1.07.06l-1.12-.83c-.3.16-.63.29-.96.4l-.2 1.37a.85.85 0 0 1-.76.73 8 8 0 0 1-1.4 0 .85.85 0 0 1-.76-.73l-.2-1.37a5.6 5.6 0 0 1-.96-.4l-1.12.83a.85.85 0 0 1-1.07-.06 8 8 0 0 1-.94-.97.85.85 0 0 1-.06-1.06l.83-1.12a5.6 5.6 0 0 1-.4-.96l-1.37-.2A.85.85 0 0 1 0 8.7a8 8 0 0 1 0-1.4c.04-.39.34-.7.73-.76l1.37-.2c.11-.33.24-.65.4-.96l-.83-1.12a.85.85 0 0 1 .06-1.06c.29-.34.6-.67.94-.97a.85.85 0 0 1 1.07-.06l1.12.83c.3-.16.63-.29.96-.4l.2-1.37A.85.85 0 0 1 7.3.03 8 8 0 0 1 8 0Zm0 5a3 3 0 1 0 0 6 3 3 0 0 0 0-6Z"/></svg></summary> |
|
| 75 | + | {% if can_triage && !milestones.is_empty() %}<details class="menu"><summary class="gear" title="Set milestone" aria-label="Set milestone">{% include "gear.html" %}</summary> |
|
| 76 | 76 | <form class="menu-pop" method="post" action="{{ repo.url }}/issue/{{ i.number }}/milestone"><input type="hidden" name="_csrf" value="{{ shell.csrf }}"> |
|
| 77 | 77 | <p class="menu-title">Set milestone</p> |
|
| 78 | 78 | <div class="menu-list"><label class="menu-item"><input type="radio" name="milestone" value=""{% if milestone.is_none() %} checked{% endif %}> <span class="menu-name">No milestone</span></label> |
|
| … | 23 unchanged lines |
modified templates/proposal.html +44 −25
| … | 150 unchanged lines |
||
| 151 | 151 | </details> |
|
| 152 | 152 | {% endif %} |
|
| 153 | 153 | </div> |
|
| 154 | - | <aside class="side"> |
|
| 155 | - | <h3>Landing</h3> |
|
| 154 | + | <aside class="side side-sections"> |
|
| 155 | + | <section class="side-sec"> |
|
| 156 | + | <div class="side-head"><h3>Landing</h3></div> |
|
| 156 | 157 | {% if p.state == "landed" %}<p><span class="sig sig-valid">✔</span> landed{% if !p.landed_state.is_empty() %} at <code>{{ p.landed_state|short }}</code>{% endif %}</p> |
|
| 157 | 158 | {% if !p.landed_by.is_empty() %}<p class="small mute">by <a href="/{{ p.landed_by }}">{{ p.landed_by }}</a>, {{ p.landed_via }}, {{ p.landed_ms|ago }}</p>{% endif %} |
|
| 158 | 159 | {% else if p.state == "closed" %}<p class="mute">closed</p> |
|
| … | 6 unchanged lines |
||
| 165 | 166 | {% else %}<p class="small">This repository has a trust policy, so land from a client with your key:</p><p class="secret">oxy proposal land {{ shell.base_url }}{{ repo.url }}/proposal/{{ p.number }}</p>{% endif %} |
|
| 166 | 167 | {% endif %} |
|
| 167 | 168 | {% endif %} |
|
| 168 | - | {% if !closes.is_empty() %}<h3>Closes</h3><ul class="plain">{% for c in closes %}<li><a href="{{ c.url }}" class="kind-issue">issue #{{ c.number }}</a> {{ c.title }} <span class="small mute">{{ c.state }}</span></li>{% endfor %}</ul>{% endif %} |
|
| 169 | - | <h3>Labels{% if can_triage %} <a class="side-edit" href="{{ repo.url }}/labels" title="Manage this repository's labels">manage</a>{% endif %}</h3> |
|
| 170 | - | {% if labels.is_empty() %}<p class="mute small">None.</p>{% else %}<p>{% for l in labels %}<span class="label" style="--c: {{ l.colour }}" title="{{ l.description }}">{{ l.name }}</span> {% endfor %}</p>{% endif %} |
|
| 171 | - | {% if can_triage %} |
|
| 172 | - | {% if all_labels.is_empty() %}<p class="small mute">This repository has no labels yet: <a href="{{ repo.url }}/labels">create some</a>.</p> |
|
| 173 | - | {% else %}<details class="side-picker"><summary>Edit labels</summary> |
|
| 174 | - | <form method="post" action="{{ repo.url }}/proposal/{{ p.number }}/labels"><input type="hidden" name="_csrf" value="{{ shell.csrf }}"> |
|
| 175 | - | {% for l in all_labels %}<label class="side-check"><input type="checkbox" name="label" value="{{ l.id }}"{% if labels.contains(l) %} checked{% endif %}> <span class="label" style="--c: {{ l.colour }}">{{ l.name }}</span></label>{% endfor %} |
|
| 176 | - | <button type="submit" class="small">Apply</button> |
|
| 177 | - | </form></details>{% endif %} |
|
| 178 | - | {% endif %} |
|
| 179 | - | <h3>Projects</h3> |
|
| 180 | - | {% if projects.is_empty() %}<p class="mute small">None.</p>{% else %}<ul class="plain small">{% for (pr, col) in projects %}<li><a href="/projects/{{ pr.owner }}/{{ pr.number }}">{{ pr.title }}</a> <span class="mute">· {{ col }}</span>{% if project_choices.is_some() %}<form class="tiny" method="post" action="{{ repo.url }}/proposal/{{ p.number }}/project"><input type="hidden" name="_csrf" value="{{ shell.csrf }}"><input type="hidden" name="project" value="{{ pr.number }}"><button name="action" value="remove" class="linkish" title="Take it off this project">remove</button></form>{% endif %}</li>{% endfor %}</ul>{% endif %} |
|
| 181 | - | {% if let Some(choices) = project_choices %}{% if choices.is_empty() %}{% if projects.is_empty() %}<p class="small mute"><a href="/projects/{{ repo.owner }}">{{ repo.owner }}'s projects</a></p>{% endif %}{% else %}<form class="side-picker-inline" method="post" action="{{ repo.url }}/proposal/{{ p.number }}/project"><input type="hidden" name="_csrf" value="{{ shell.csrf }}"><select name="project" aria-label="Project">{% for pr in choices %}<option value="{{ pr.number }}">{{ pr.title }}</option>{% endfor %}</select> <button name="action" value="add" class="small">Add</button></form>{% endif %}{% endif %} |
|
| 182 | - | <h3>Checks</h3> |
|
| 169 | + | </section> |
|
| 170 | + | {% if !closes.is_empty() %}<section class="side-sec"><div class="side-head"><h3>Closes</h3></div><ul class="plain small side-list">{% for c in closes %}<li><a href="{{ c.url }}" class="kind-issue">issue #{{ c.number }}</a> {{ c.title }} <span class="mute">{{ c.state }}</span></li>{% endfor %}</ul></section>{% endif %} |
|
| 171 | + | <section class="side-sec"> |
|
| 172 | + | <div class="side-head"><h3>Reviewers</h3> |
|
| 173 | + | {% if can_request %}<details class="menu"><summary class="gear" title="Request reviews" aria-label="Request reviews">{% include "gear.html" %}</summary> |
|
| 174 | + | <form class="menu-pop" method="post" action="{{ repo.url }}/proposal/{{ p.number }}/reviewers"><input type="hidden" name="_csrf" value="{{ shell.csrf }}"><input type="hidden" name="action" value="set"> |
|
| 175 | + | <p class="menu-title">Request reviews</p> |
|
| 176 | + | <input class="menu-filter" type="search" placeholder="Filter people" aria-label="Filter people" data-menu-filter> |
|
| 177 | + | <div class="menu-list">{% for u in reviewable %}<label class="menu-item"><input type="checkbox" name="username" value="{{ u }}"{% if requested.contains(u) %} checked{% endif %}> <img class="avatar xs" src="/avatar/{{ u }}" alt="" loading="lazy"> <span class="menu-name">{{ u }}</span></label>{% endfor %} |
|
| 178 | + | {% if reviewable.is_empty() %}<p class="menu-empty">Nobody else with access to this repository yet.</p>{% endif %}</div> |
|
| 179 | + | <div class="menu-foot"><button type="submit" class="small primary-btn">Apply</button></div> |
|
| 180 | + | </form></details>{% endif %}</div> |
|
| 181 | + | {% if requested.is_empty() %}<p class="side-empty">No reviews requested</p>{% else %}<ul class="side-people">{% for r in requested %}<li><a href="/{{ r }}"><img class="avatar xs" src="/avatar/{{ r }}" alt="" loading="lazy"> {{ r }}</a> <span class="side-note-inline">awaiting review</span></li>{% endfor %}</ul>{% endif %} |
|
| 182 | + | </section> |
|
| 183 | + | <section class="side-sec"> |
|
| 184 | + | <div class="side-head"><h3>Labels</h3> |
|
| 185 | + | {% if can_triage %}<details class="menu"><summary class="gear" title="Edit labels" aria-label="Edit labels">{% include "gear.html" %}</summary> |
|
| 186 | + | <form class="menu-pop" method="post" action="{{ repo.url }}/proposal/{{ p.number }}/labels"><input type="hidden" name="_csrf" value="{{ shell.csrf }}"> |
|
| 187 | + | <p class="menu-title">Apply labels to this proposal</p> |
|
| 188 | + | {% if !all_labels.is_empty() %}<input class="menu-filter" type="search" placeholder="Filter labels" aria-label="Filter labels" data-menu-filter>{% endif %} |
|
| 189 | + | <div class="menu-list">{% for l in all_labels %}<label class="menu-item"><input type="checkbox" name="label" value="{{ l.id }}"{% if labels.contains(l) %} checked{% endif %}> <span class="swatch" style="--c: {{ l.colour }}"></span> <span class="menu-name">{{ l.name }}{% if !l.description.is_empty() %}<small>{{ l.description }}</small>{% endif %}</span></label>{% endfor %} |
|
| 190 | + | {% if all_labels.is_empty() %}<p class="menu-empty">This repository has no labels yet.</p>{% endif %}</div> |
|
| 191 | + | <div class="menu-foot"><a class="small" href="{{ repo.url }}/labels">Edit labels</a> <button type="submit" class="small primary-btn">Apply</button></div> |
|
| 192 | + | </form></details>{% endif %}</div> |
|
| 193 | + | {% if labels.is_empty() %}<p class="side-empty">None yet</p>{% else %}<p class="side-labels">{% for l in labels %}<span class="label" style="--c: {{ l.colour }}" title="{{ l.description }}">{{ l.name }}</span> {% endfor %}</p>{% endif %} |
|
| 194 | + | </section> |
|
| 195 | + | <section class="side-sec"> |
|
| 196 | + | <div class="side-head"><h3>Projects</h3></div> |
|
| 197 | + | {% if projects.is_empty() %}<p class="side-empty">None yet</p>{% else %}<ul class="plain small">{% for (pr, col) in projects %}<li><a href="/projects/{{ pr.owner }}/{{ pr.number }}">{{ pr.title }}</a> <span class="mute">· {{ col }}</span>{% if project_choices.is_some() %}<form class="inline" method="post" action="{{ repo.url }}/proposal/{{ p.number }}/project"><input type="hidden" name="_csrf" value="{{ shell.csrf }}"><input type="hidden" name="project" value="{{ pr.number }}"><button name="action" value="remove" class="linkish" title="Take it off this project">remove</button></form>{% endif %}</li>{% endfor %}</ul>{% endif %} |
|
| 198 | + | {% if let Some(choices) = project_choices %}{% if choices.is_empty() %}{% if projects.is_empty() %}<p class="side-note"><a href="/projects/{{ repo.owner }}">{{ repo.owner }}'s projects</a></p>{% endif %}{% else %}<form class="side-picker-inline" method="post" action="{{ repo.url }}/proposal/{{ p.number }}/project"><input type="hidden" name="_csrf" value="{{ shell.csrf }}"><select name="project" aria-label="Project">{% for pr in choices %}<option value="{{ pr.number }}">{{ pr.title }}</option>{% endfor %}</select> <button name="action" value="add" class="small">Add</button></form>{% endif %}{% endif %} |
|
| 199 | + | </section> |
|
| 200 | + | <section class="side-sec"> |
|
| 201 | + | <div class="side-head"><h3>Checks</h3></div> |
|
| 183 | 202 | <div id="checks"> |
|
| 184 | - | {% if checks.is_empty() %}<p class="mute small">None reported on the projected state{% if !required_checks.is_empty() %}; required: {{ required_checks.join(", ") }}{% endif %}.</p>{% else %} |
|
| 203 | + | {% if checks.is_empty() %}<p class="side-empty">None reported on the projected state{% if !required_checks.is_empty() %}; required: {{ required_checks.join(", ") }}{% endif %}.</p>{% else %} |
|
| 185 | 204 | <ul class="plain">{% for c in checks %}<li><span class="sig{% if c.status == "passed" %} sig-valid{% else if c.status == "failed" %} sig-invalid{% else %} sig-unsigned{% endif %}">{% if c.status == "passed" %}✔{% else if c.status == "failed" %}✘{% else if c.status == "running" %}…{% else %}○{% endif %}</span> {% if c.url.is_empty() %}{{ c.name }}{% else %}<a href="{{ c.url }}">{{ c.name }}</a>{% endif %} <span class="mute small">{{ c.status }}{% if !c.summary.is_empty() %} · {{ c.summary }}{% endif %}{% if !c.seal.is_empty() %} · sealed{% endif %}</span></li>{% endfor %}</ul>{% endif %} |
|
| 186 | 205 | </div> |
|
| 187 | 206 | <script>(function(){if(!window.EventSource)return;var es=new EventSource("{{ repo.url }}/proposal/{{ p.number }}/events");es.addEventListener("check",function(){es.close();location.reload();});})();</script> |
|
| 188 | - | <h3>Reviewers</h3> |
|
| 189 | - | {% if requested.is_empty() %}<p class="small mute">No review requested.</p>{% else %}<ul class="plain">{% for r in requested %}<li><img class="tl-mini" src="/avatar/{{ r }}" alt="" width="18" height="18"> <a href="/{{ r }}">{{ r }}</a> <span class="small mute">asked</span>{% if can_request %} <form class="tiny inline" method="post" action="{{ repo.url }}/proposal/{{ p.number }}/reviewers"><input type="hidden" name="_csrf" value="{{ shell.csrf }}"><input type="hidden" name="action" value="remove"><input type="hidden" name="username" value="{{ r }}"><button title="Withdraw the request">×</button></form>{% endif %}</li>{% endfor %}</ul>{% endif %} |
|
| 190 | - | {% if can_request %}<form class="side-picker-inline" method="post" action="{{ repo.url }}/proposal/{{ p.number }}/reviewers"><input type="hidden" name="_csrf" value="{{ shell.csrf }}"><input type="hidden" name="action" value="add"><input type="text" name="username" placeholder="username" aria-label="Ask someone to review" size="12" required> <button class="small">Ask</button></form>{% endif %} |
|
| 191 | - | <h3>Approvals</h3> |
|
| 192 | - | {% if approvals.is_empty() %}<p class="mute small">None yet{% if requirements.required_approvals > 0 %}; {{ requirements.required_approvals }} sealed required{% endif %}.</p>{% else %} |
|
| 207 | + | </section> |
|
| 208 | + | <section class="side-sec"> |
|
| 209 | + | <div class="side-head"><h3>Approvals</h3></div> |
|
| 210 | + | {% if approvals.is_empty() %}<p class="side-empty">None yet{% if requirements.required_approvals > 0 %}; {{ requirements.required_approvals }} sealed required{% endif %}.</p>{% else %} |
|
| 193 | 211 | <ul class="plain">{% for a in approvals %}<li>{% if a.sealed %}<span class="sig sig-valid">◈</span>{% else %}<span class="sig sig-unsigned">○</span>{% endif %} <a href="/{{ a.account }}">{{ a.account }}</a>{% if !a.sealed %} <span class="mute small">seal pending</span>{% endif %}</li>{% endfor %}</ul>{% endif %} |
|
| 194 | - | {% if !owners.is_empty() %}<h3>Code owners</h3><ul class="plain">{% for o in owners %}<li>{{ o }}</li>{% endfor %}</ul>{% endif %} |
|
| 195 | - | {% if !depends_on.is_empty() %}<h3>Depends on</h3><ul class="plain">{% for n in depends_on %}<li><a href="{{ repo.url }}/proposal/{{ n }}">#{{ n }}</a></li>{% endfor %}</ul>{% endif %} |
|
| 212 | + | </section> |
|
| 213 | + | {% if !owners.is_empty() %}<section class="side-sec"><div class="side-head"><h3>Code owners</h3></div><ul class="plain small">{% for o in owners %}<li>{{ o }}</li>{% endfor %}</ul></section>{% endif %} |
|
| 214 | + | {% if !depends_on.is_empty() %}<section class="side-sec"><div class="side-head"><h3>Depends on</h3></div><ul class="plain small">{% for n in depends_on %}<li><a href="{{ repo.url }}/proposal/{{ n }}">#{{ n }}</a></li>{% endfor %}</ul></section>{% endif %} |
|
| 196 | 215 | </aside> |
|
| 197 | 216 | </div> |
|
| 198 | 217 | {% endif %} |
|
| … | 1 unchanged lines |
modified templates/style.css +2 −0
| … | 710 unchanged lines |
||
| 711 | 711 | .device-mark { display: inline-flex; align-items: center; justify-content: center; width: 28px; height: 28px; border-radius: 50%; font-size: 16px; color: #fff; } |
|
| 712 | 712 | .device-done.ok .device-mark { background: var(--ok); } |
|
| 713 | 713 | .device-done.no .device-mark { background: var(--bad); } |
|
| 714 | + | .side-note-inline { font-size: 12px; color: var(--mute); font-weight: 400; } |
|
| 715 | + | .side-list li { margin: 0 0 4px; } |