1use anyhow::Result;
2use axum::extract::{Path as AxumPath, State};
3use axum::response::Response;
4
5use crate::db::{self, Store};
6
7use super::helpers::{
8 error_response, friendly_date, friendly_status, list_projects_safe, render, render_markdown,
9};
10use super::project::views::TaskRow;
11use super::AppState;
12
13mod views;
14use views::{BlockerRef, LogView, TaskTemplate, TaskView};
15
16pub(in crate::cmd::webui) async fn task_handler(
17 State(state): State<AppState>,
18 AxumPath((name, id)): AxumPath<(String, String)>,
19) -> Response {
20 let root = state.data_root.clone();
21 let result = tokio::task::spawn_blocking(move || -> Result<TaskTemplate> {
22 let all_projects = list_projects_safe(&root);
23 let store = Store::open(&root, &name)?;
24
25 let task_id = db::resolve_task_id(&store, &id, false)?;
26 let task = store
27 .get_task(&task_id, false)?
28 .ok_or_else(|| anyhow::anyhow!("task '{id}' not found"))?;
29
30 // Partition blockers.
31 let partition = db::partition_blockers(&store, &task.blockers)?;
32 let blockers_open: Vec<BlockerRef> = partition
33 .open
34 .iter()
35 .map(|b| BlockerRef {
36 full_id: b.as_str().to_string(),
37 short_id: b.short(),
38 })
39 .collect();
40 let blockers_resolved: Vec<BlockerRef> = partition
41 .resolved
42 .iter()
43 .map(|b| BlockerRef {
44 full_id: b.as_str().to_string(),
45 short_id: b.short(),
46 })
47 .collect();
48
49 // Find subtasks.
50 let all_tasks = store.list_tasks()?;
51 let subtasks: Vec<TaskRow> = all_tasks
52 .iter()
53 .filter(|t| t.parent.as_ref() == Some(&task_id))
54 .map(|t| {
55 let status = t.status.as_str().to_string();
56 TaskRow {
57 full_id: t.id.as_str().to_string(),
58 short_id: t.id.short(),
59 status_display: friendly_status(&status),
60 status,
61 task_type: t.task_type.clone(),
62 priority: t.priority.as_str().to_string(),
63 effort: t.effort.as_str().to_string(),
64 title: t.title.clone(),
65 labels: t.labels.clone(),
66 created_at_display: friendly_date(&t.created_at),
67 created_at: t.created_at.clone(),
68 }
69 })
70 .collect();
71
72 let task_view = TaskView {
73 full_id: task.id.as_str().to_string(),
74 short_id: task.id.short(),
75 title: task.title.clone(),
76 description: render_markdown(&task.description),
77 description_raw: task.description.clone(),
78 task_type: task.task_type.clone(),
79 status: task.status.as_str().to_string(),
80 priority: task.priority.as_str().to_string(),
81 effort: task.effort.as_str().to_string(),
82 created_at_display: friendly_date(&task.created_at),
83 created_at: task.created_at.clone(),
84 updated_at_display: friendly_date(&task.updated_at),
85 updated_at: task.updated_at.clone(),
86 parent_id: task.parent.as_ref().map(|p| p.short()).unwrap_or_default(),
87 labels: task.labels.clone(),
88 logs: task
89 .logs
90 .iter()
91 .map(|l| LogView {
92 timestamp_display: friendly_date(&l.timestamp),
93 timestamp: l.timestamp.clone(),
94 message: render_markdown(&l.message),
95 })
96 .collect(),
97 };
98
99 let edit_heading = format!("Edit {}", task_view.short_id);
100 let edit_form_action = format!(
101 "/projects/{}/tasks/{}",
102 store.project_name(),
103 task_view.full_id
104 );
105
106 Ok(TaskTemplate {
107 all_projects,
108 active_project: Some(name),
109 project_name: store.project_name().to_string(),
110 task: task_view,
111 blockers_open,
112 blockers_resolved,
113 subtasks,
114 edit_heading,
115 edit_form_action,
116 })
117 })
118 .await;
119
120 match result {
121 Ok(Ok(tmpl)) => render(tmpl),
122 Ok(Err(e)) => error_response(500, &format!("{e}"), &[]),
123 Err(e) => error_response(500, &format!("join error: {e}"), &[]),
124 }
125}