🐸 Frogue(Fed)

~ololduck/Frogue

Raw

use actix_web::HttpResponse;
use maud::{Markup, Render, html};

use super::Page;
use super::components::{
    Complaint, DeleteForm, RepositoryChrome, RepositoryTab, TextArea, TextField,
};
use super::form::{MergePolicySelect, VisibilityRadios};
use crate::entities::ticket_labels::Model as Label;

/// A repository as its own page shows it.
pub struct RepositoryDetail {
    pub slug: String,
    pub description: Option<String>,
    pub visibility: &'static str,
    /// The branch the repository opens on, which the browse tab addresses.
    pub default_branch: String,
    /// The URL a git client clones, fetches from, or pushes to.
    pub clone_url: String,
    /// The readme at the tip of the default branch, if there is one.
    pub readme: Option<ReadmeView>,
    /// The most recently committed branches, newest first.
    pub branches: Vec<RefSummary>,
    /// The most recently created tags, newest first.
    pub tags: Vec<RefSummary>,
}

/// A readme as the repository page shows it.
pub struct ReadmeView {
    /// The path within the tree, so the page says which readme this is.
    pub path: String,
    /// The rendered, sanitized HTML.
    pub html: String,
}

/// A branch or tag, and the commit it reaches, as a listing shows it.
pub struct RefSummary {
    pub name: String,
    /// The short hash, which is what a listing shows.
    pub hash: String,
    /// The full hash, which is what the hash's link is built from: an
    /// abbreviation is only as unambiguous as git found needed, which is
    /// not enough to address anything with.
    pub full_hash: String,
    pub author: String,
    pub subject: String,
}

/// A commit, as the history viewer shows it.
pub struct CommitSummary {
    pub hash: String,
    /// The full hash, which is what the hash's link is built from: an
    /// abbreviation is only as unambiguous as git found needed.
    pub full_hash: String,
    pub author: String,
    pub date: String,
    pub subject: String,
    /// Number of files changed in this commit.
    pub files_changed: u64,
    /// Number of lines added in this commit.
    pub insertions: u64,
    /// Number of lines deleted in this commit.
    pub deletions: u64,
}

/// What the new repository form shows when it is (re)rendered.
pub struct RepositoryForm {
    pub slug: Option<String>,
    pub description: Option<String>,
    /// The radio that was selected, `public` or `private`.
    pub visibility: &'static str,
    pub error: Option<String>,
}

impl Default for RepositoryForm {
    fn default() -> Self {
        RepositoryForm {
            slug: None,
            description: None,
            visibility: "public",
            error: None,
        }
    }
}

pub(super) struct NewRepositoryBody<'a> {
    owner: &'a str,
    form: &'a RepositoryForm,
}

impl Render for NewRepositoryBody<'_> {
    fn render(&self) -> Markup {
        html! {
            h2 { "New repository" }
            (Complaint(self.form.error.as_deref()))
            form method="post" action=(format!("/~{}/repositories", self.owner)) {
                (TextField {
                    label: Some("Name"),
                    name: "slug",
                    input_type: "text",
                    value: self.form.slug.as_deref(),
                    required: true,
                    maxlength: u32::MAX,
                })
                (TextArea {
                    label: Some("Description"),
                    name: "description",
                    rows: 4,
                    value: self.form.description.as_deref(),
                    required: false,
                })
                fieldset {
                    legend { "Visibility" }
                    (VisibilityRadios {
                        current: self.form.visibility,
                    })
                }
                button type="submit" { "Create" }
            }
        }
    }
}

pub(super) struct RepositoryBody<'a> {
    owner: &'a str,
    repository: &'a RepositoryDetail,
    viewer: Option<&'a str>,
}

impl Render for RepositoryBody<'_> {
    fn render(&self) -> Markup {
        html! {
            (RepositoryChrome {
                owner: self.owner,
                slug: &self.repository.slug,
                visibility: self.repository.visibility,
                default_branch: &self.repository.default_branch,
                current_tab: RepositoryTab::Summary,
                is_owner: self.viewer == Some(self.owner),
            })
            @if let Some(description) = &self.repository.description {
                p { (description) }
            }

            p id="clone_url" { "Clone URL: " code { (self.repository.clone_url) }}

            @if !self.repository.branches.is_empty() {
                h3 { "Branches" }
                (super::refs::RefList {
                    owner: self.owner,
                    slug: &self.repository.slug,
                    refs: &self.repository.branches,
                })
            }
            @if !self.repository.tags.is_empty() {
                h3 { "Tags" }
                (super::refs::RefList {
                    owner: self.owner,
                    slug: &self.repository.slug,
                    refs: &self.repository.tags,
                })
            }

            @if let Some(readme) = &self.repository.readme {
                section class="readme" {
                    header { p class="readme-path" { (readme.path)  }}
                    // Rendered and sanitized by `crate::markdown`; this is the
                    // one place a page embeds markup it did not write.
                    main{(maud::PreEscaped(&readme.html))}
                }
            }

            // The lists above are a preview of the first few; this is where
            // all of them live.
            p class="more-refs" {
                a href=(format!("/~{}/{}/refs", self.owner, self.repository.slug)) {
                    "View all branches and tags"
                }
            }
        }
    }
}

pub(super) struct RepositorySettingsBody<'a> {
    owner: &'a str,
    slug: &'a str,
    default_branch: &'a str,
    branches: &'a [String],
    default_merge_policy: &'a str,
    visibility: &'a str,
    labels: &'a [Label],
    error: Option<&'a str>,
}

impl Render for RepositorySettingsBody<'_> {
    fn render(&self) -> Markup {
        html! {
            (RepositoryChrome {
                owner: self.owner,
                slug: self.slug,
                visibility: self.visibility,
                default_branch: self.default_branch,
                current_tab: RepositoryTab::Settings,
                is_owner: true,
            })

            (Complaint(self.error))

            form method="post" action=(format!("/~{}/{}/settings", self.owner, self.slug)) {
                fieldset {
                    legend { "Visibility" }
                    (VisibilityRadios {
                        current: self.visibility,
                    })
                    button type="submit" { "Save" }
                }
            }

            form method="post" action=(format!("/~{}/{}/settings", self.owner, self.slug)) {
                fieldset {
                    legend { "Default branch" }
                    label {
                        "Branch"
                        select name="default_branch" required {
                            @if self.branches.is_empty() {
                                option value=(self.default_branch) selected { (self.default_branch) }
                            } @else {
                                @for branch in self.branches {
                                    option value=(branch) selected[branch == self.default_branch] {
                                        (branch)
                                    }
                                }
                            }
                        }
                    }
                    button type="submit" { "Save" }
                }
            }

            form method="post" action=(format!("/~{}/{}/settings", self.owner, self.slug)) {
                fieldset {
                    legend { "Default merge policy" }
                    (MergePolicySelect {
                        name: "default_merge_policy",
                        current: self.default_merge_policy,
                    })
                    button type="submit" { "Save" }
                }
            }

            h3 { "Labels" }

            @if self.labels.is_empty() {
                p class="empty" { "No labels yet." }
            } @else {
                ul class="label-list" {
                    @for label in self.labels {
                        li class="label-item" {
                            span class="label-name" { (label.name) }
                            (DeleteForm {
                                action: format!("/~{}/{}/settings/labels/{}/delete", self.owner, self.slug, label.id),
                                form_class: Some("inline-form"),
                                button_class: Some("danger"),
                                confirm: None,
                            })
                        }
                    }
                }
            }

            h3 { "Create label" }
            form method="post" action=(format!("/~{}/{}/settings/labels", self.owner, self.slug)) {
                (TextField {
                    label: Some("Name"),
                    name: "name",
                    input_type: "text",
                    value: None,
                    required: true,
                    maxlength: 64,
                })
                button type="submit" { "Create" }
            }
        }
    }
}

/// The form for creating a repository owned by `owner`.
pub fn new_repository(viewer: Option<&str>, owner: &str, form: &RepositoryForm) -> HttpResponse {
    Page {
        title: format!("~{owner}: New repository - Frogue"),
        viewer: viewer.map(str::to_string),
        extra_head: None,
        body: NewRepositoryBody { owner, form },
    }
    .into_response()
}

/// The page for a repository, showing how to reach it.
pub fn repository(
    viewer: Option<&str>,
    owner: &str,
    repository: &RepositoryDetail,
) -> HttpResponse {
    Page {
        title: format!("~{owner}/{} - Frogue", repository.slug),
        viewer: viewer.map(str::to_string),
        extra_head: None,
        body: RepositoryBody {
            owner,
            repository,
            viewer,
        },
    }
    .into_response()
}

/// Parameters for the repository settings page
pub struct RepositorySettingsParams<'a> {
    pub viewer: Option<&'a str>,
    pub owner: &'a str,
    pub slug: &'a str,
    pub default_branch: &'a str,
    pub branches: &'a [String],
    pub default_merge_policy: &'a str,
    pub visibility: &'a str,
    pub labels: &'a [Label],
    pub error: Option<&'a str>,
}

/// The settings page for a repository.
pub fn repository_settings(params: RepositorySettingsParams<'_>) -> HttpResponse {
    Page {
        title: format!(
            "~{}/{slug}: Settings - Frogue",
            params.owner,
            slug = params.slug
        ),
        viewer: params.viewer.map(str::to_string),
        extra_head: None,
        body: RepositorySettingsBody {
            owner: params.owner,
            slug: params.slug,
            default_branch: params.default_branch,
            branches: params.branches,
            default_merge_policy: params.default_merge_policy,
            visibility: params.visibility,
            labels: params.labels,
            error: params.error,
        },
    }
    .into_response()
}