mod.rs

  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}