| 736 | option value=(value.as_str()) selected[vis == value] { (label) } | 736 | option value=(value.as_str()) selected[vis == value] { (label) } |
| 737 | } | 737 | } |
| 738 | }; | 738 | }; |
| 739 | let perm_option = |value: &str, label: &str| { | | |
| 740 | html! { option value=(value) { (label) } } | | |
| 741 | }; | | |
| 742 | let body = html! { | 739 | let body = html! { |
| 743 | (repo_header(state, &ctx, Tab::Settings, &ctx.repo.default_branch, &branches)) | 740 | (repo_header(state, &ctx, Tab::Settings, &ctx.repo.default_branch, &branches)) |
| 744 | section class="listing" { | 741 | section class="listing" { |
| 758 | } | 755 | } |
| 759 | } | 756 | } |
| 760 | } | 757 | } |
| | 758 | (collaborators_section(id, &chrome.csrf, &collaborators)) |
| | 759 | section class="listing" { |
| | 760 | h2 { "Archive" } |
| | 761 | div class="card" { |
| | 762 | @if ctx.repo.archived_at.is_some() { |
| | 763 | p class="muted" { "This repository is archived (read-only). Unarchiving restores pushing." } |
| | 764 | form method="post" action=(format!("/repo-settings/{id}/archive")) { |
| | 765 | input type="hidden" name="_csrf" value=(chrome.csrf); |
| | 766 | input type="hidden" name="archived" value="false"; |
| | 767 | button class="btn btn-primary" type="submit" { "Unarchive repository" } |
| | 768 | } |
| | 769 | } @else { |
| | 770 | p class="muted" { "Archiving makes the repository read-only; pushes are refused until it is unarchived." } |
| | 771 | form method="post" action=(format!("/repo-settings/{id}/archive")) { |
| | 772 | input type="hidden" name="_csrf" value=(chrome.csrf); |
| | 773 | input type="hidden" name="archived" value="true"; |
| | 774 | button class="btn" type="submit" { "Archive repository" } |
| | 775 | } |
| | 776 | } |
| | 777 | } |
| | 778 | } |
| | 779 | section class="listing" { |
| | 780 | h2 { "Danger zone" } |
| | 781 | div class="card danger-zone" { |
| | 782 | p { strong { "Delete this repository." } " This permanently removes it from fabrica; the on-disk copy is moved to trash." } |
| | 783 | form method="post" action=(format!("/repo-settings/{id}/delete")) { |
| | 784 | input type="hidden" name="_csrf" value=(chrome.csrf); |
| | 785 | button class="btn btn-danger" type="submit" { "Delete repository" } |
| | 786 | } |
| | 787 | } |
| | 788 | } |
| | 789 | }; |
| | 790 | let title = format!("{}/{}: settings", ctx.owner.username, ctx.repo.path); |
| | 791 | Ok((jar, page(&chrome, &title, body)).into_response()) |
| | 792 | } |
| | 793 | |
| | 794 | |
| | 795 | |
| | 796 | fn collaborators_section(id: &str, csrf: &str, collaborators: &[store::Collaborator]) -> Markup { |
| | 797 | let perm_option = |value: &str, label: &str| html! { option value=(value) { (label) } }; |
| | 798 | html! { |
| 761 | section class="listing" { | 799 | section class="listing" { |
| 762 | h2 { "Collaborators" } | 800 | h2 { "Collaborators" } |
| 763 | div class="card" { | 801 | div class="card" { |
| 765 | p class="muted" { "No collaborators yet." } | 803 | p class="muted" { "No collaborators yet." } |
| 766 | } @else { | 804 | } @else { |
| 767 | ul class="entry-list collab-list" { | 805 | ul class="entry-list collab-list" { |
| 768 | @for c in &collaborators { | 806 | @for c in collaborators { |
| 769 | li class="entry-row" { | 807 | li class="entry-row" { |
| 770 | div class="entry-row-body" { | 808 | div class="entry-row-body" { |
| 771 | a class="entry-row-title" href={ "/" (c.username) } { (c.username) } | 809 | a class="entry-row-title" href={ "/" (c.username) } { (c.username) } |
| 774 | div class="entry-row-actions" { | 812 | div class="entry-row-actions" { |
| 775 | form method="post" | 813 | form method="post" |
| 776 | action=(format!("/repo-settings/{id}/collaborators/remove")) { | 814 | action=(format!("/repo-settings/{id}/collaborators/remove")) { |
| 777 | input type="hidden" name="_csrf" value=(chrome.csrf); | 815 | input type="hidden" name="_csrf" value=(csrf); |
| 778 | input type="hidden" name="user_id" value=(c.user_id); | 816 | input type="hidden" name="user_id" value=(c.user_id); |
| 779 | button class="btn btn-danger" type="submit" { "Remove" } | 817 | button class="btn btn-danger" type="submit" { "Remove" } |
| 780 | } | 818 | } |
| 785 | } | 823 | } |
| 786 | form class="collab-add" method="post" | 824 | form class="collab-add" method="post" |
| 787 | action=(format!("/repo-settings/{id}/collaborators")) { | 825 | action=(format!("/repo-settings/{id}/collaborators")) { |
| 788 | input type="hidden" name="_csrf" value=(chrome.csrf); | 826 | input type="hidden" name="_csrf" value=(csrf); |
| 789 | input type="text" name="username" placeholder="Username" aria-label="Username" required; | 827 | input type="text" name="username" placeholder="Username" aria-label="Username" required; |
| 790 | select name="permission" aria-label="Permission" { | 828 | select name="permission" aria-label="Permission" { |
| 791 | (perm_option("read", "Read")) | 829 | (perm_option("read", "Read")) |
| 796 | } | 834 | } |
| 797 | } | 835 | } |
| 798 | } | 836 | } |
| 799 | section class="listing" { | 837 | } |
| 800 | h2 { "Danger zone" } | | |
| 801 | div class="card danger-zone" { | | |
| 802 | p { strong { "Delete this repository." } " This permanently removes it from fabrica; the on-disk copy is moved to trash." } | | |
| 803 | form method="post" action=(format!("/repo-settings/{id}/delete")) { | | |
| 804 | input type="hidden" name="_csrf" value=(chrome.csrf); | | |
| 805 | button class="btn btn-danger" type="submit" { "Delete repository" } | | |
| 806 | } | | |
| 807 | } | | |
| 808 | } | | |
| 809 | }; | | |
| 810 | let title = format!("{}/{}: settings", ctx.owner.username, ctx.repo.path); | | |
| 811 | Ok((jar, page(&chrome, &title, body)).into_response()) | | |
| 812 | } | 838 | } |
| 813 | | 839 | |
| 814 | | 840 | |
| 958 | } | 984 | } |
| 959 | } | 985 | } |
| 960 | | 986 | |
| | 987 | |
| | 988 | #[derive(Debug, Deserialize)] |
| | 989 | pub struct RepoArchiveForm { |
| | 990 | |
| | 991 | #[serde(default)] |
| | 992 | archived: String, |
| | 993 | |
| | 994 | #[serde(rename = "_csrf")] |
| | 995 | csrf: String, |
| | 996 | } |
| | 997 | |
| | 998 | |
| | 999 | pub async fn settings_archive( |
| | 1000 | State(state): State<AppState>, |
| | 1001 | MaybeUser(viewer): MaybeUser, |
| | 1002 | jar: CookieJar, |
| | 1003 | headers: HeaderMap, |
| | 1004 | Path(id): Path<String>, |
| | 1005 | Form(form): Form<RepoArchiveForm>, |
| | 1006 | ) -> Response { |
| | 1007 | if verify_csrf(&jar, &headers, Some(&form.csrf)).is_err() { |
| | 1008 | return AppError::Forbidden.into_response(); |
| | 1009 | } |
| | 1010 | let result = async { |
| | 1011 | let repo = require_admin_repo(&state, viewer.as_ref(), &id).await?; |
| | 1012 | state |
| | 1013 | .store |
| | 1014 | .set_archived(&repo.id, form.archived == "true") |
| | 1015 | .await?; |
| | 1016 | Ok::<String, AppError>(repo_settings_url(&state, &repo).await) |
| | 1017 | } |
| | 1018 | .await; |
| | 1019 | match result { |
| | 1020 | Ok(dest) => Redirect::to(&dest).into_response(), |
| | 1021 | Err(err) => err.into_response(), |
| | 1022 | } |
| | 1023 | } |
| | 1024 | |
| 961 | | 1025 | |
| 962 | async fn repo_settings_url(state: &AppState, repo: &Repo) -> String { | 1026 | async fn repo_settings_url(state: &AppState, repo: &Repo) -> String { |
| 963 | let owner = state | 1027 | let owner = state |
| 1503 | | 1567 | |
| 1504 | | 1568 | |
| 1505 | pub(crate) updated: i64, | 1569 | pub(crate) updated: i64, |
| | 1570 | |
| | 1571 | pub(crate) archived: bool, |
| 1506 | | 1572 | |
| 1507 | pub(crate) description: Option<String>, | 1573 | pub(crate) description: Option<String>, |
| 1508 | } | 1574 | } |
| 1516 | visibility: repo.visibility, | 1582 | visibility: repo.visibility, |
| 1517 | default_branch: repo.default_branch.clone(), | 1583 | default_branch: repo.default_branch.clone(), |
| 1518 | updated: repo.pushed_at.unwrap_or(repo.updated_at), | 1584 | updated: repo.pushed_at.unwrap_or(repo.updated_at), |
| | 1585 | archived: repo.archived_at.is_some(), |
| 1519 | description: repo.description.clone(), | 1586 | description: repo.description.clone(), |
| 1520 | } | 1587 | } |
| 1521 | } | 1588 | } |
| 1529 | visibility: repo.visibility, | 1596 | visibility: repo.visibility, |
| 1530 | default_branch: repo.default_branch.clone(), | 1597 | default_branch: repo.default_branch.clone(), |
| 1531 | updated: repo.pushed_at.unwrap_or(repo.updated_at), | 1598 | updated: repo.pushed_at.unwrap_or(repo.updated_at), |
| | 1599 | archived: repo.archived_at.is_some(), |
| 1532 | description: repo.description.clone(), | 1600 | description: repo.description.clone(), |
| 1533 | } | 1601 | } |
| 1534 | } | 1602 | } |
| 1543 | } | 1611 | } |
| 1544 | } | 1612 | } |
| 1545 | | 1613 | |
| | 1614 | |
| | 1615 | fn archived_badge(archived: bool) -> Markup { |
| | 1616 | html! { |
| | 1617 | @if archived { |
| | 1618 | span class="badge badge-archived" { "archived" } |
| | 1619 | } |
| | 1620 | } |
| | 1621 | } |
| | 1622 | |
| 1546 | | 1623 | |
| 1547 | | 1624 | |
| 1548 | | 1625 | |
| 1561 | span class="repo-row-name" { | 1638 | span class="repo-row-name" { |
| 1562 | (icon(Icon::Box)) span { (e.label) } | 1639 | (icon(Icon::Box)) span { (e.label) } |
| 1563 | (visibility_badge(e.visibility)) | 1640 | (visibility_badge(e.visibility)) |
| | 1641 | (archived_badge(e.archived)) |
| 1564 | } | 1642 | } |
| 1565 | @if let Some(desc) = &e.description { | 1643 | @if let Some(desc) = &e.description { |
| 1566 | span class="repo-row-desc muted" { (desc) } | 1644 | span class="repo-row-desc muted" { (desc) } |
| 1698 | span class="slug-sep" { "/" } | 1776 | span class="slug-sep" { "/" } |
| 1699 | a href=(base) { (ctx.repo.name) } | 1777 | a href=(base) { (ctx.repo.name) } |
| 1700 | (visibility_badge(ctx.repo.visibility)) | 1778 | (visibility_badge(ctx.repo.visibility)) |
| | 1779 | (archived_badge(ctx.repo.archived_at.is_some())) |
| 1701 | } | 1780 | } |
| 1702 | @if let Some(desc) = &ctx.repo.description { p class="muted" { (desc) } } | 1781 | @if let Some(desc) = &ctx.repo.description { p class="muted" { (desc) } } |
| 1703 | div class="repo-nav" { | 1782 | div class="repo-nav" { |