Signed by hannaSSH key fingerprint: SHA256:4g9cWhkAAw8gwqhJUcVbSnGEvdGwklW+1Aa/fMUu59k
assets/base.css +31 −1
| @@ -742,7 +742,8 @@ body.drawer-open .drawer-backdrop { | |||
| 742 | margin-bottom: 0; | 742 | margin-bottom: 0; |
| 743 | } | 743 | } |
| 744 | 744 | ||
| 745 | /* Collaborators: a bordered list inside the card, then an add row. */ | 745 | /* Collaborators: a bordered list inside the card, then an add row. Each row is |
| 746 | [avatar] [name] [role] on the left, remove button on the right. */ | ||
| 746 | .collab-list { | 747 | .collab-list { |
| 747 | margin-bottom: 1rem; | 748 | margin-bottom: 1rem; |
| 748 | } | 749 | } |
| @@ -750,6 +751,28 @@ body.drawer-open .drawer-backdrop { | |||
| 750 | padding-left: 0; | 751 | padding-left: 0; |
| 751 | padding-right: 0; | 752 | padding-right: 0; |
| 752 | } | 753 | } |
| 754 | /* The first row abuts the card padding; drop its own top padding so the list | ||
| 755 | doesn't sit with a doubled gap at the top of the card. */ | ||
| 756 | .collab-list .entry-row:first-child { | ||
| 757 | padding-top: 0; | ||
| 758 | } | ||
| 759 | .collab-ident { | ||
| 760 | display: flex; | ||
| 761 | align-items: center; | ||
| 762 | gap: 0.6rem; | ||
| 763 | min-width: 0; | ||
| 764 | } | ||
| 765 | .collab-ident .entry-row-title { | ||
| 766 | display: inline; | ||
| 767 | } | ||
| 768 | .collab-perm { | ||
| 769 | font-size: 0.8em; | ||
| 770 | color: var(--fb-fg-muted); | ||
| 771 | border: 1px solid var(--fb-border); | ||
| 772 | border-radius: 999px; | ||
| 773 | padding: 0.05rem 0.55rem; | ||
| 774 | text-transform: capitalize; | ||
| 775 | } | ||
| 753 | .collab-list .entry-row-actions .btn { | 776 | .collab-list .entry-row-actions .btn { |
| 754 | padding: 0.25rem 0.6rem; | 777 | padding: 0.25rem 0.6rem; |
| 755 | font-size: 0.85em; | 778 | font-size: 0.85em; |
| @@ -1192,6 +1215,13 @@ label { | |||
| 1192 | margin: 0.75rem 0 0.25rem; | 1215 | margin: 0.75rem 0 0.25rem; |
| 1193 | font-weight: 600; | 1216 | font-weight: 600; |
| 1194 | } | 1217 | } |
| 1218 | /* The first label in a card form abuts the card padding — drop its top margin so | ||
| 1219 | the form doesn't sit with a doubled gap at the top. The CSRF hidden input is | ||
| 1220 | usually the real first child, so also match the label right after it. */ | ||
| 1221 | .card form > label:first-child, | ||
| 1222 | .card form > input[type="hidden"]:first-child + label { | ||
| 1223 | margin-top: 0; | ||
| 1224 | } | ||
| 1195 | input[type="text"], | 1225 | input[type="text"], |
| 1196 | input[type="password"], | 1226 | input[type="password"], |
| 1197 | input[type="email"], | 1227 | input[type="email"], |
crates/web/src/groups.rs +5 −49
| @@ -287,7 +287,11 @@ pub async fn settings_view( | |||
| 287 | } | 287 | } |
| 288 | } | 288 | } |
| 289 | } | 289 | } |
| 290 | (group_collaborators_section(gid, &chrome.csrf, &collaborators)) | 290 | section class="listing" { |
| 291 | h2 { "Collaborators" } | ||
| 292 | p class="muted field-hint" { "Group collaborators gain the same access to every repository within this group." } | ||
| 293 | (crate::repo::collaborators_card(&format!("/group-settings/{gid}"), &chrome.csrf, &collaborators)) | ||
| 294 | } | ||
| 291 | section class="listing" { | 295 | section class="listing" { |
| 292 | h2 { "Danger zone" } | 296 | h2 { "Danger zone" } |
| 293 | div class="card danger-zone" { | 297 | div class="card danger-zone" { |
| @@ -312,54 +316,6 @@ pub async fn settings_view( | |||
| 312 | result.unwrap_or_else(IntoResponse::into_response) | 316 | result.unwrap_or_else(IntoResponse::into_response) |
| 313 | } | 317 | } |
| 314 | 318 | ||
| 315 | /// The group collaborators section: current members with remove buttons, add form. | ||
| 316 | fn group_collaborators_section( | ||
| 317 | id: &str, | ||
| 318 | csrf: &str, | ||
| 319 | collaborators: &[store::Collaborator], | ||
| 320 | ) -> Markup { | ||
| 321 | let perm_option = |value: &str, label: &str| html! { option value=(value) { (label) } }; | ||
| 322 | html! { | ||
| 323 | section class="listing" { | ||
| 324 | h2 { "Collaborators" } | ||
| 325 | p class="muted field-hint" { "Group collaborators gain the same access to every repository within this group." } | ||
| 326 | div class="card" { | ||
| 327 | @if collaborators.is_empty() { | ||
| 328 | p class="muted" { "No collaborators yet." } | ||
| 329 | } @else { | ||
| 330 | ul class="entry-list collab-list" { | ||
| 331 | @for c in collaborators { | ||
| 332 | li class="entry-row" { | ||
| 333 | div class="entry-row-body" { | ||
| 334 | a class="entry-row-title" href={ "/" (c.username) } { (c.username) } | ||
| 335 | div class="entry-row-meta muted" { (c.permission) } | ||
| 336 | } | ||
| 337 | div class="entry-row-actions" { | ||
| 338 | form method="post" action=(format!("/group-settings/{id}/collaborators/remove")) { | ||
| 339 | input type="hidden" name="_csrf" value=(csrf); | ||
| 340 | input type="hidden" name="user_id" value=(c.user_id); | ||
| 341 | button class="btn btn-danger" type="submit" { "Remove" } | ||
| 342 | } | ||
| 343 | } | ||
| 344 | } | ||
| 345 | } | ||
| 346 | } | ||
| 347 | } | ||
| 348 | form class="collab-add" method="post" action=(format!("/group-settings/{id}/collaborators")) { | ||
| 349 | input type="hidden" name="_csrf" value=(csrf); | ||
| 350 | input type="text" name="username" placeholder="Username" aria-label="Username" required; | ||
| 351 | select name="permission" aria-label="Permission" { | ||
| 352 | (perm_option("read", "Read")) | ||
| 353 | (perm_option("write", "Write")) | ||
| 354 | (perm_option("admin", "Admin")) | ||
| 355 | } | ||
| 356 | button class="btn btn-primary inline-btn" type="submit" { "Add" } | ||
| 357 | } | ||
| 358 | } | ||
| 359 | } | ||
| 360 | } | ||
| 361 | } | ||
| 362 | |||
| 363 | /// The group general-settings form (rename + visibility). | 319 | /// The group general-settings form (rename + visibility). |
| 364 | #[derive(Debug, Deserialize)] | 320 | #[derive(Debug, Deserialize)] |
| 365 | pub struct GroupGeneralForm { | 321 | pub struct GroupGeneralForm { |
crates/web/src/repo.rs +41 −29
| @@ -1504,44 +1504,56 @@ fn fmt_duration(secs: i64) -> String { | |||
| 1504 | /// The Collaborators section of the repo settings page: the current list with | 1504 | /// The Collaborators section of the repo settings page: the current list with |
| 1505 | /// remove buttons, and the add form. | 1505 | /// remove buttons, and the add form. |
| 1506 | fn collaborators_section(id: &str, csrf: &str, collaborators: &[store::Collaborator]) -> Markup { | 1506 | fn collaborators_section(id: &str, csrf: &str, collaborators: &[store::Collaborator]) -> Markup { |
| 1507 | let perm_option = |value: &str, label: &str| html! { option value=(value) { (label) } }; | ||
| 1508 | html! { | 1507 | html! { |
| 1509 | section class="listing" { | 1508 | section class="listing" { |
| 1510 | h2 { "Collaborators" } | 1509 | h2 { "Collaborators" } |
| 1511 | div class="card" { | 1510 | (collaborators_card(&format!("/repo-settings/{id}"), csrf, collaborators)) |
| 1512 | @if collaborators.is_empty() { | 1511 | } |
| 1513 | p class="muted" { "No collaborators yet." } | 1512 | } |
| 1514 | } @else { | 1513 | } |
| 1515 | ul class="entry-list collab-list" { | 1514 | |
| 1516 | @for c in collaborators { | 1515 | /// The collaborators card — an avatar/name/role list with remove buttons, then |
| 1517 | li class="entry-row" { | 1516 | /// an add row. Shared by the repo and group settings pages; `base` is the action |
| 1518 | div class="entry-row-body" { | 1517 | /// URL prefix (`/repo-settings/{id}` or `/group-settings/{id}`). |
| 1519 | a class="entry-row-title" href={ "/" (c.username) } { (c.username) } | 1518 | pub(crate) fn collaborators_card( |
| 1520 | div class="entry-row-meta muted" { (c.permission) } | 1519 | base: &str, |
| 1521 | } | 1520 | csrf: &str, |
| 1522 | div class="entry-row-actions" { | 1521 | collaborators: &[store::Collaborator], |
| 1523 | form method="post" | 1522 | ) -> Markup { |
| 1524 | action=(format!("/repo-settings/{id}/collaborators/remove")) { | 1523 | let perm_option = |value: &str, label: &str| html! { option value=(value) { (label) } }; |
| 1525 | input type="hidden" name="_csrf" value=(csrf); | 1524 | html! { |
| 1526 | input type="hidden" name="user_id" value=(c.user_id); | 1525 | div class="card" { |
| 1527 | button class="btn btn-danger" type="submit" { "Remove" } | 1526 | @if collaborators.is_empty() { |
| 1528 | } | 1527 | p class="muted" { "No collaborators yet." } |
| 1528 | } @else { | ||
| 1529 | ul class="entry-list collab-list" { | ||
| 1530 | @for c in collaborators { | ||
| 1531 | li class="entry-row" { | ||
| 1532 | div class="collab-ident" { | ||
| 1533 | img class="avatar avatar-sm" src=(format!("/avatar/{}", c.username)) alt=""; | ||
| 1534 | a class="entry-row-title" href={ "/" (c.username) } { (c.username) } | ||
| 1535 | span class="collab-perm" { (c.permission) } | ||
| 1536 | } | ||
| 1537 | div class="entry-row-actions" { | ||
| 1538 | form method="post" action=(format!("{base}/collaborators/remove")) { | ||
| 1539 | input type="hidden" name="_csrf" value=(csrf); | ||
| 1540 | input type="hidden" name="user_id" value=(c.user_id); | ||
| 1541 | button class="btn btn-danger" type="submit" { "Remove" } | ||
| 1529 | } | 1542 | } |
| 1530 | } | 1543 | } |
| 1531 | } | 1544 | } |
| 1532 | } | 1545 | } |
| 1533 | } | 1546 | } |
| 1534 | form class="collab-add" method="post" | 1547 | } |
| 1535 | action=(format!("/repo-settings/{id}/collaborators")) { | 1548 | form class="collab-add" method="post" action=(format!("{base}/collaborators")) { |
| 1536 | input type="hidden" name="_csrf" value=(csrf); | 1549 | input type="hidden" name="_csrf" value=(csrf); |
| 1537 | input type="text" name="username" placeholder="Username" aria-label="Username" required; | 1550 | input type="text" name="username" placeholder="Username" aria-label="Username" required; |
| 1538 | select name="permission" aria-label="Permission" { | 1551 | select name="permission" aria-label="Permission" { |
| 1539 | (perm_option("read", "Read")) | 1552 | (perm_option("read", "Read")) |
| 1540 | (perm_option("write", "Write")) | 1553 | (perm_option("write", "Write")) |
| 1541 | (perm_option("admin", "Admin")) | 1554 | (perm_option("admin", "Admin")) |
| 1542 | } | ||
| 1543 | button class="btn btn-primary inline-btn" type="submit" { "Add" } | ||
| 1544 | } | 1555 | } |
| 1556 | button class="btn btn-primary inline-btn" type="submit" { "Add" } | ||
| 1545 | } | 1557 | } |
| 1546 | } | 1558 | } |
| 1547 | } | 1559 | } |
data/fabrica/trash/01kydt075vpteqnjzyms8v3vtm-1785061358405.git/HEAD +0 −0
No textual changes.
data/fabrica/trash/01kydt075vpteqnjzyms8v3vtm-1785061358405.git/config +0 −0
No textual changes.
data/fabrica/trash/01kydt075vpteqnjzyms8v3vtm-1785061358405.git/description +0 −0
No textual changes.
data/fabrica/trash/01kydt075vpteqnjzyms8v3vtm-1785061358405.git/hooks/README.sample +0 −0
No textual changes.
data/fabrica/trash/01kydt075vpteqnjzyms8v3vtm-1785061358405.git/hooks/post-receive +0 −0
No textual changes.
data/fabrica/trash/01kydt075vpteqnjzyms8v3vtm-1785061358405.git/hooks/pre-receive +0 −0
No textual changes.
data/fabrica/trash/01kydt075vpteqnjzyms8v3vtm-1785061358405.git/info/exclude +0 −0
No textual changes.
data/fabrica/trash/01kydta3a2b6gmfh0c4x7tx4b0-1785061363714.git/HEAD +0 −0
No textual changes.
data/fabrica/trash/01kydta3a2b6gmfh0c4x7tx4b0-1785061363714.git/config +0 −0
No textual changes.
data/fabrica/trash/01kydta3a2b6gmfh0c4x7tx4b0-1785061363714.git/description +0 −0
No textual changes.
data/fabrica/trash/01kydta3a2b6gmfh0c4x7tx4b0-1785061363714.git/hooks/README.sample +0 −0
No textual changes.
data/fabrica/trash/01kydta3a2b6gmfh0c4x7tx4b0-1785061363714.git/hooks/post-receive +0 −0
No textual changes.
data/fabrica/trash/01kydta3a2b6gmfh0c4x7tx4b0-1785061363714.git/hooks/pre-receive +0 −0
No textual changes.
data/fabrica/trash/01kydta3a2b6gmfh0c4x7tx4b0-1785061363714.git/info/exclude +0 −0
No textual changes.
data/fabrica/trash/01kydtah34meqbxcfatt6m90p3-1785061376374.git/HEAD +0 −0
No textual changes.
data/fabrica/trash/01kydtah34meqbxcfatt6m90p3-1785061376374.git/config +0 −0
No textual changes.
data/fabrica/trash/01kydtah34meqbxcfatt6m90p3-1785061376374.git/description +0 −0
No textual changes.
data/fabrica/trash/01kydtah34meqbxcfatt6m90p3-1785061376374.git/hooks/README.sample +0 −0
No textual changes.
data/fabrica/trash/01kydtah34meqbxcfatt6m90p3-1785061376374.git/hooks/post-receive +0 −0
No textual changes.
data/fabrica/trash/01kydtah34meqbxcfatt6m90p3-1785061376374.git/hooks/pre-receive +0 −0
No textual changes.
data/fabrica/trash/01kydtah34meqbxcfatt6m90p3-1785061376374.git/info/exclude +0 −0
No textual changes.