main / frogue / src / views / remote_forge.rs
Raw
//! Views for remote ticket and merge request submission.
//!
//! These views allow local users to open tickets or submit merge requests
//! on remote trackers via the ForgeFed protocol.
use actix_web::HttpResponse;
use maud::{Markup, Render, html};
use super::Page;
use super::components::Complaint;
/// List of remote tickets/MRs a user has opened
pub struct RemoteTicketsList {
pub username: String,
pub tickets: Vec<RemoteTicketRow>,
}
/// One remote ticket/MR as shown in the list
pub struct RemoteTicketRow {
pub id: i64,
pub kind: String,
pub status: String,
pub tracker_url: String,
pub summary: String,
pub assigned_url: Option<String>,
pub created_at: chrono::DateTime<chrono::FixedOffset>,
}
struct RemoteTicketsBody<'a> {
username: &'a str,
tickets: &'a [RemoteTicketRow],
}
impl Render for RemoteTicketsBody<'_> {
fn render(&self) -> Markup {
html! {
h1 { "Remote tickets for " span class="username" { "~" (self.username) } }
@if self.tickets.is_empty() {
p { "You haven't opened any remote tickets yet." }
a href=(format!("/~{}/remote-tickets/new", self.username)) {
"Open a ticket on a remote tracker"
}
} @else {
a href=(format!("/~{}/remote-tickets/new", self.username)) {
"Open a new ticket"
}
table {
thead {
tr {
th { "Kind" }
th { "Summary" }
th { "Status" }
th { "Tracker" }
th { "Assigned URL" }
th { "Created" }
}
}
tbody {
@for ticket in self.tickets {
tr {
td { (ticket.kind) }
td { (ticket.summary) }
td {
span class=(format!("status-{}", ticket.status)) {
(ticket.status)
}
}
td {
a href=(ticket.tracker_url) { (ticket.tracker_url) }
}
td {
@if let Some(ref url) = ticket.assigned_url {
a href=(url) { (url) }
} @else {
"—"
}
}
td { (ticket.created_at.format("%Y-%m-%d %H:%M")) }
}
}
}
}
}
}
}
}
/// Render the list of remote tickets
pub fn remote_tickets(viewer: Option<&str>, list: &RemoteTicketsList) -> HttpResponse {
Page {
title: format!("Remote tickets for ~{} - Frogue", list.username),
viewer: viewer.map(str::to_string),
extra_head: None,
body: RemoteTicketsBody {
username: &list.username,
tickets: &list.tickets,
},
}
.into_response()
}
/// Form for opening a new ticket/MR on a remote tracker
pub struct NewRemoteTicket {
pub username: String,
pub error: Option<String>,
}
struct NewRemoteTicketBody<'a> {
username: &'a str,
error: Option<&'a str>,
}
impl Render for NewRemoteTicketBody<'_> {
fn render(&self) -> Markup {
html! {
h1 { "Open a ticket on a remote tracker" }
(Complaint(self.error))
form method="post" action=(format!("/~{}/remote-tickets", self.username)) {
div class="field" {
label for="target" { "Target repository URL" }
input type="text" id="target" name="target" required;
small {
"The full URL of the remote repository, e.g. "
code { "https://example.com/~user/repo" }
}
}
div class="field" {
label for="kind" { "Kind" }
select id="kind" name="kind" required {
option value="ticket" { "Ticket" }
option value="mr" { "Patch" }
}
}
div class="field" {
label for="summary" { "Summary" }
input type="text" id="summary" name="summary" required
placeholder="Brief description of the issue or proposal";
}
div class="field" {
label for="description" { "Description" }
textarea id="description" name="description" rows="10" {
"Describe the issue or proposal in detail. Markdown is supported."
}
}
div class="field mr-fields" style="display: none;" {
label for="origin_repo" { "Origin repository URL (patch only)" }
input type="text" id="origin_repo" name="origin_repo"
placeholder="https://example.com/~user/your-repo";
small {
"URL of your local repository containing the changes"
}
}
div class="field mr-fields" style="display: none;" {
label for="origin_branch" { "Origin branch (patch only)" }
input type="text" id="origin_branch" name="origin_branch"
placeholder="feature-branch";
small {
"Branch containing your changes (patches will be generated from default branch to this branch)"
}
}
div class="field mr-fields" style="display: none;" {
label for="target_branch" { "Target branch (patch only)" }
input type="text" id="target_branch" name="target_branch"
placeholder="main";
small {
"Branch on the remote repository to merge into"
}
}
div class="actions" {
button type="submit" { "Submit" }
a href=(format!("/~{}/remote-tickets", self.username)) { "Cancel" }
}
}
script {
(r#"
document.addEventListener('DOMContentLoaded', function() {
var kindSelect = document.getElementById('kind');
var mrFields = document.querySelectorAll('.mr-fields');
function toggleMrFields() {
var isMr = kindSelect.value === 'mr';
mrFields.forEach(function(field) {
field.style.display = isMr ? 'block' : 'none';
var inputs = field.querySelectorAll('input');
inputs.forEach(function(input) {
if (isMr) {
input.setAttribute('required', 'required');
} else {
input.removeAttribute('required');
}
});
});
}
kindSelect.addEventListener('change', toggleMrFields);
toggleMrFields(); // Initialize on page load
});
"#)
}
}
}
}
/// Render the new remote ticket form
pub fn new_remote_ticket(viewer: Option<&str>, form: &NewRemoteTicket) -> HttpResponse {
Page {
title: format!("Open remote ticket for ~{} - Frogue", form.username),
viewer: viewer.map(str::to_string),
extra_head: None,
body: NewRemoteTicketBody {
username: &form.username,
error: form.error.as_deref(),
},
}
.into_response()
}