diff options
Diffstat (limited to 'pkgs/by-name/ba/back/src/web')
-rw-r--r-- | pkgs/by-name/ba/back/src/web/issue_html.rs | 166 | ||||
-rw-r--r-- | pkgs/by-name/ba/back/src/web/mod.rs | 134 | ||||
-rw-r--r-- | pkgs/by-name/ba/back/src/web/prefix.rs | 35 |
3 files changed, 335 insertions, 0 deletions
diff --git a/pkgs/by-name/ba/back/src/web/issue_html.rs b/pkgs/by-name/ba/back/src/web/issue_html.rs new file mode 100644 index 0000000..45c0281 --- /dev/null +++ b/pkgs/by-name/ba/back/src/web/issue_html.rs @@ -0,0 +1,166 @@ +// Back - An extremely simple git issue tracking system. Inspired by tvix's +// panettone +// +// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de> +// SPDX-License-Identifier: AGPL-3.0-or-later +// +// This file is part of Back. +// +// You should have received a copy of the License along with this program. +// If not, see <https://www.gnu.org/licenses/agpl.txt>. + +use rocket::response::content::RawHtml; + +use crate::{ + config::BackConfig, + git_bug::{ + format::HtmlString, + issue::{identity::Author, CollapsedIssue, Comment}, + }, +}; + +impl CollapsedIssue { + pub fn to_list_entry(&self) -> RawHtml<String> { + let comment_list = if self.comments.is_empty() { + String::new() + } else { + let comments_string = if self.comments.len() > 1 { + "comments" + } else { + "comment" + }; + + format!( + r#" + <span class="comment-count"> - {} {}</span> + "#, + self.comments.len(), + comments_string + ) + }; + + let CollapsedIssue { + id, + title, + message: _, + author, + timestamp, + comments: _, + status: _, + last_status_change: _, + labels: _, + } = self; + + let Author { name, email, id: _ } = author; + + RawHtml(format!( + r#" + <li> + <a href="/issue/{id}"> + <p> + <span class="issue-subject">{title}</span> + </p> + <span class="issue-number">{id}</span> - <span class="created-by-at">Opened by <span class="user-name">{name}</span> <span class="user-email"><{email}></span> at <span class="timestamp">{timestamp}</span></span>{comment_list} </a> + </li> +"#, + )) + } + + pub fn to_html(&self, config: &BackConfig) -> RawHtml<String> { + let comments = if self.comments.is_empty() { + String::new() + } else { + let fmt_comments: String = self + .comments + .iter() + .map(|val| { + let Comment { + id, + author, + message, + timestamp, + } = val; + let Author { + name, + email: _, + id: _, + } = author; + + format!( + r#" + <li class="comment" id="{id}"> + {message} + <p class="comment-info"><span class="user-name">{name} at {timestamp}</span></p> + </li> + "#, + ) + }) + .collect::<Vec<String>>() + .join("\n"); + + format!( + r#" + <ol class="issue-history"> + {fmt_comments} + </ol> + "# + ) + }; + + { + let CollapsedIssue { + id, + title, + message, + author, + timestamp, + comments: _, + status: _, + last_status_change: _, + labels: _, + } = self; + let Author { name, email, id: _ } = author; + let html_title = HtmlString::from(title.clone()); + + RawHtml(format!( + r#" +<!DOCTYPE html> +<html lang="en"> + <head> + <title>{html_title} | Back</title> + <link href="/style.css" rel="stylesheet" type="text/css"> + <meta content="width=device-width,initial-scale=1" name="viewport"> + </head> + <body> + <div class="content"> + <nav> + <a href="/issues/open">Open Issues</a> + <a href="/issues/closed">Closed Issues</a> + </nav> + <header> + <h1>{title}</h1> + <div class="issue-number">{id}</div> + </header> + <main> + <div class="issue-info"> + <span class="created-by-at">Opened by <span class="user-name">{name}</span> <span class="user-email"><{email}></span> at <span class="timestamp">{timestamp}</span></span> + </div> + {message} + {comments} + </main> + <footer> + <nav> + <a href="/issues/open">Open Issues</a> + <a href="{}">Source code</a> + <a href="/issues/closed">Closed Issues</a> + </nav> + </footer> + </div> + </body> +</html> +"#, + config.source_code_repository_url + )) + } + } +} diff --git a/pkgs/by-name/ba/back/src/web/mod.rs b/pkgs/by-name/ba/back/src/web/mod.rs new file mode 100644 index 0000000..35dc59f --- /dev/null +++ b/pkgs/by-name/ba/back/src/web/mod.rs @@ -0,0 +1,134 @@ +// Back - An extremely simple git issue tracking system. Inspired by tvix's +// panettone +// +// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de> +// SPDX-License-Identifier: AGPL-3.0-or-later +// +// This file is part of Back. +// +// You should have received a copy of the License along with this program. +// If not, see <https://www.gnu.org/licenses/agpl.txt>. + +use crate::{ + config::BackConfig, + error::{self, Error}, + git_bug::{ + dag::issues_from_repository, + issue::{CollapsedIssue, Status}, + }, +}; +use prefix::BackPrefix; +use rocket::{ + get, + response::content::{RawCss, RawHtml}, + State, +}; + +mod issue_html; +pub mod prefix; + +#[get("/style.css")] +pub fn styles() -> RawCss<String> { + RawCss(include_str!("../../assets/style.css").to_owned()) +} + +pub fn issue_list_boilerplate( + config: &State<BackConfig>, + wanted_status: Status, + counter_status: Status, +) -> error::Result<RawHtml<String>> { + let repository = &config.repository; + + let mut issue_list = issues_from_repository(&repository.to_thread_local())? + .into_iter() + .map(|issue| issue.collapse()) + .collect::<Vec<CollapsedIssue>>(); + + // Sort by date descending. + issue_list.sort_by_key(|issue| unsafe { issue.timestamp.to_unsafe() }); + issue_list.reverse(); + + let issue_list_str = issue_list.into_iter().fold(String::new(), |acc, issue| { + format!("{}{}", acc, { + if issue.status == wanted_status { + let issue_entry = issue.to_list_entry(); + issue_entry.0 + } else { + String::new() + } + }) + }); + + let counter_status_lower = counter_status.to_string().to_lowercase(); + Ok(RawHtml(format!( + r#" + <!DOCTYPE html> + <html lang="en"> + <head> + <title>Back</title> + <link href="/style.css" rel="stylesheet" type="text/css"> + <meta content="width=device-width,initial-scale=1" name="viewport"> + </head> + <body> + <div class="content"> + <header> + <h1>{wanted_status} Issues</h1> + </header> + <main> + <div class="issue-links"> + <a href="/issues/{counter_status_lower}/">View {counter_status} issues</a> + <a href="{}">Source code</a> + <!-- + <form class="issue-search" method="get"> + <input name="search" title="Issue search query" type="search"> + <input class="sr-only" type="submit" value="Search Issues"> + </form> + --> + </div> + <ol class="issue-list"> + {issue_list_str} + </ol> + </main> + </div> + </body> + </html> + "#, + config.source_code_repository_url + ))) +} + +#[get("/issues/open")] +pub fn open(config: &State<BackConfig>) -> error::Result<RawHtml<String>> { + issue_list_boilerplate(config, Status::Open, Status::Closed) +} +#[get("/issues/closed")] +pub fn closed(config: &State<BackConfig>) -> error::Result<RawHtml<String>> { + issue_list_boilerplate(config, Status::Closed, Status::Open) +} + +#[get("/issue/<prefix>")] +pub fn show_issue( + config: &State<BackConfig>, + prefix: Result<BackPrefix, gix::hash::prefix::from_hex::Error>, +) -> error::Result<RawHtml<String>> { + // NOTE(@bpeetz): Explicitly unwrap the `prefix` here (instead of taking the unwrapped value as + // argument), to avoid triggering rockets "errors forward to the next route" feature. + // This ensures, that our error message actually reaches the user. <2024-12-26> + let prefix = prefix?; + + let repository = config.repository.to_thread_local(); + + let all_issues: Vec<CollapsedIssue> = issues_from_repository(&repository)? + .into_iter() + .map(|val| val.collapse()) + .collect(); + + let maybe_issue = all_issues + .iter() + .find(|issue| issue.id.to_string().starts_with(&prefix.to_string())); + + match maybe_issue { + Some(issue) => Ok(issue.to_html(config)), + None => Err(Error::IssuesPrefixMissing { prefix }), + } +} diff --git a/pkgs/by-name/ba/back/src/web/prefix.rs b/pkgs/by-name/ba/back/src/web/prefix.rs new file mode 100644 index 0000000..5143799 --- /dev/null +++ b/pkgs/by-name/ba/back/src/web/prefix.rs @@ -0,0 +1,35 @@ +// Back - An extremely simple git issue tracking system. Inspired by tvix's +// panettone +// +// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de> +// SPDX-License-Identifier: AGPL-3.0-or-later +// +// This file is part of Back. +// +// You should have received a copy of the License along with this program. +// If not, see <https://www.gnu.org/licenses/agpl.txt>. + +use std::fmt::Display; + +use gix::hash::Prefix; +use rocket::request::FromParam; + +#[derive(Debug)] +pub struct BackPrefix { + prefix: Prefix, +} +impl Display for BackPrefix { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.prefix.fmt(f) + } +} + +impl<'a> FromParam<'a> for BackPrefix { + type Error = gix::hash::prefix::from_hex::Error; + + fn from_param(param: &'a str) -> Result<Self, Self::Error> { + let prefix = Prefix::from_hex(param)?; + + Ok(Self { prefix }) + } +} |