d4c836aef91b0171a994afd57ae487d5ea91d4db / frogue / src / views / repositories.rs
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()
}