@@ -121,6 +121,17 @@ fn sort_tasks(tasks: &mut [&db::Task], field: SortField, order: SortOrder) {
});
}
+/// Return a human-friendly status label (e.g. "In progress" instead of
+/// "in_progress").
+fn friendly_status(raw: &str) -> &'static str {
+ match raw {
+ "open" => "Open",
+ "in_progress" => "In progress",
+ "closed" => "Closed",
+ _ => "Open",
+ }
+}
+
/// Format an ISO 8601 timestamp into a human-friendly form (e.g. "15 Mar 2026")
/// for the noscript fallback. Returns the original string unchanged on parse
/// failure so the page still renders something sensible.
@@ -182,6 +193,8 @@ struct ScoredEntry {
short_id: String,
title: String,
score: String,
+ status: String,
+ status_display: &'static str,
}
/// Minimal view-model for a task row in the project task table.
@@ -189,6 +202,7 @@ struct TaskRow {
full_id: String,
short_id: String,
status: String,
+ status_display: &'static str,
priority: String,
effort: String,
title: String,
@@ -557,6 +571,8 @@ async fn project_handler(
id: s.id,
title: s.title,
score: format!("{:.2}", s.score),
+ status: "open".to_string(),
+ status_display: friendly_status("open"),
})
.collect();
@@ -621,15 +637,19 @@ async fn project_handler(
let page_tasks: Vec<TaskRow> = filtered[start..end]
.iter()
- .map(|t| TaskRow {
- full_id: t.id.as_str().to_string(),
- short_id: t.id.short(),
- status: db::status_label(t.status).to_string(),
- priority: db::priority_label(t.priority).to_string(),
- effort: db::effort_label(t.effort).to_string(),
- title: t.title.clone(),
- created_at_display: friendly_date(&t.created_at),
- created_at: t.created_at.clone(),
+ .map(|t| {
+ let status = db::status_label(t.status).to_string();
+ TaskRow {
+ full_id: t.id.as_str().to_string(),
+ short_id: t.id.short(),
+ status_display: friendly_status(&status),
+ status,
+ priority: db::priority_label(t.priority).to_string(),
+ effort: db::effort_label(t.effort).to_string(),
+ title: t.title.clone(),
+ created_at_display: friendly_date(&t.created_at),
+ created_at: t.created_at.clone(),
+ }
})
.collect();
@@ -730,15 +750,19 @@ async fn task_handler(
let subtasks: Vec<TaskRow> = all_tasks
.iter()
.filter(|t| t.parent.as_ref() == Some(&task_id))
- .map(|t| TaskRow {
- full_id: t.id.as_str().to_string(),
- short_id: t.id.short(),
- status: db::status_label(t.status).to_string(),
- priority: db::priority_label(t.priority).to_string(),
- effort: db::effort_label(t.effort).to_string(),
- title: t.title.clone(),
- created_at_display: friendly_date(&t.created_at),
- created_at: t.created_at.clone(),
+ .map(|t| {
+ let status = db::status_label(t.status).to_string();
+ TaskRow {
+ full_id: t.id.as_str().to_string(),
+ short_id: t.id.short(),
+ status_display: friendly_status(&status),
+ status,
+ priority: db::priority_label(t.priority).to_string(),
+ effort: db::effort_label(t.effort).to_string(),
+ title: t.title.clone(),
+ created_at_display: friendly_date(&t.created_at),
+ created_at: t.created_at.clone(),
+ }
})
.collect();
@@ -872,6 +896,8 @@ struct UpdateForm {
priority: Option<String>,
#[serde(default)]
effort: Option<String>,
+ #[serde(default)]
+ redirect: Option<String>,
}
#[derive(serde::Deserialize)]
@@ -1015,7 +1041,10 @@ async fn update_handler(
},
)?;
- let redirect = format!("/projects/{}/tasks/{}", name, task.id.as_str());
+ let redirect = form
+ .redirect
+ .filter(|r| r.starts_with('/'))
+ .unwrap_or_else(|| format!("/projects/{}/tasks/{}", name, task.id.as_str()));
Ok((task, redirect))
})
.await;