From 2c2186075b69577090ecf34fe2e82f97bbb53a4e Mon Sep 17 00:00:00 2001 From: Amolith Date: Tue, 17 Mar 2026 08:35:40 -0600 Subject: [PATCH] Add tri-state status controls to web UI task pages --- src/cmd/webui.rs | 67 ++++++++++++++++++++++++++++++------------ templates/macros.html | 30 +++++++++++++++++-- templates/project.html | 32 ++++++++++++++++++++ templates/task.html | 61 +++++++++++++++++++++++++++++--------- 4 files changed, 154 insertions(+), 36 deletions(-) diff --git a/src/cmd/webui.rs b/src/cmd/webui.rs index ec7550bd5a52aeaeb42fa740c1b8ba399b0efe5d..91d26849840d58835ad6bb0c9334b68d396d894c 100644 --- a/src/cmd/webui.rs +++ b/src/cmd/webui.rs @@ -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 = 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 = 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, #[serde(default)] effort: Option, + #[serde(default)] + redirect: Option, } #[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; diff --git a/templates/macros.html b/templates/macros.html index 2a923abf73216232d11db0f90e5d5cbafad024e3..32380c59fde7a5b323c27f6399f18ceecf0984cc 100644 --- a/templates/macros.html +++ b/templates/macros.html @@ -40,7 +40,7 @@ Effort{{ sort_ctx.arrow("effort") }} Title{{ sort_ctx.arrow("title") }} Created{{ sort_ctx.arrow("created") }} - Actions + Change status @@ -53,9 +53,33 @@ {{ t.title }} + + + + + + + + + {% if t.status != "open" %} + + {% endif %} + {% if t.status != "in_progress" %} + + {% endif %} {% if t.status != "closed" %} -
- + + +
{% endif %} diff --git a/templates/project.html b/templates/project.html index 8b884fd8928e6397c6d24dea5f786939db36b63d..3a0d3b399646feafe6d1842861cfaec7d83223e5 100644 --- a/templates/project.html +++ b/templates/project.html @@ -41,6 +41,7 @@ ID Score Title + Change status @@ -50,6 +51,37 @@ {{ s.short_id }} {{ s.score }} {{ s.title }} + + + + + + + + + + {% if s.status != "open" %} + + {% endif %} + {% if s.status != "in_progress" %} + + {% endif %} + {% if s.status != "closed" %} + + {% endif %} + {% endfor %} diff --git a/templates/task.html b/templates/task.html index f0e1f17a3a949f2f5c7f41ec0fa1b6bccc3e83e3..f18962c4522707a19ac8dadec6465a939f37a3dc 100644 --- a/templates/task.html +++ b/templates/task.html @@ -33,18 +33,47 @@
- {% if task.status != "closed" %} -
- + + + + + + + + + {% if task.status != "open" %} + +
- {% else %} -
- + {% endif %} + {% if task.status != "in_progress" %} + +
{% endif %} -
- + {% if task.status != "closed" %} + +
+ {% endif %} + + +
+
+

Are you sure?

+

This will delete {{ task.short_id }}.

+
+
+
+ + +
+
+
+
{% if !task.labels.is_empty() %} @@ -63,10 +92,12 @@ {% endif %}
-
+ - - +
+ + +
@@ -109,10 +140,12 @@ {% endfor %} {% endif %} -
+ - - +
+ + +