Skip to main content

aven_core/api/
epics.rs

1use super::*;
2
3#[derive(Debug, Clone, PartialEq, Eq)]
4pub struct EpicProgress {
5    pub total: u32,
6    pub open: u32,
7    pub done: u32,
8    pub canceled: u32,
9}
10
11impl From<crate::query::EpicRollup> for EpicProgress {
12    fn from(value: crate::query::EpicRollup) -> Self {
13        Self {
14            total: value.total.min(u32::MAX as usize) as u32,
15            open: value.open.min(u32::MAX as usize) as u32,
16            done: value.done.min(u32::MAX as usize) as u32,
17            canceled: value.canceled.min(u32::MAX as usize) as u32,
18        }
19    }
20}
21
22#[derive(Debug, Clone, PartialEq, Eq)]
23pub struct EpicSummary {
24    pub task: TaskListRow,
25    pub progress: EpicProgress,
26}
27
28impl Store {
29    pub async fn epics(&self, workspace_id: &WorkspaceId) -> Result<Vec<EpicSummary>, Error> {
30        self.workspace(workspace_id).await?;
31        let items = self
32            .database
33            .list_task_summary_items(
34                workspace_id,
35                TaskFilters {
36                    epics_only: true,
37                    ..TaskFilters::default()
38                },
39                TaskQueryMode::Flat,
40                TaskSort::Updated,
41                SortDirection::Desc,
42                None,
43            )
44            .await
45            .map_err(Error::from_internal)?;
46        Ok(items
47            .into_iter()
48            .map(|mut item| EpicSummary {
49                progress: item.epic_rollup.take().unwrap_or_default().into(),
50                task: item.into(),
51            })
52            .collect())
53    }
54
55    pub async fn epic_candidates(
56        &self,
57        workspace_id: &WorkspaceId,
58        epic_id: &TaskId,
59    ) -> Result<Vec<TaskListRow>, Error> {
60        let workspace = self.workspace(workspace_id).await?;
61        let epic = self
62            .database
63            .resolve_task_ref(&workspace, epic_id.as_str())
64            .await
65            .map_err(Error::from_internal)?;
66        if epic.deleted || !epic.is_epic {
67            return Err(Error::new(
68                ErrorCode::Validation,
69                "epic is unavailable".into(),
70            ));
71        }
72        self.database
73            .list_task_summary_items(
74                workspace_id,
75                TaskFilters {
76                    project: Some(epic.project_key),
77                    exclude_epics: true,
78                    without_epic_link: true,
79                    ..TaskFilters::default()
80                },
81                TaskQueryMode::Flat,
82                TaskSort::Updated,
83                SortDirection::Desc,
84                None,
85            )
86            .await
87            .map(|items| items.into_iter().map(Into::into).collect())
88            .map_err(Error::from_internal)
89    }
90
91    pub async fn create_epic(
92        &self,
93        workspace_id: &WorkspaceId,
94        input: TaskCapture,
95    ) -> Result<TaskId, Error> {
96        self.create_consumer_epic_task(workspace_id, None, input)
97            .await
98    }
99
100    pub async fn create_epic_child(
101        &self,
102        workspace_id: &WorkspaceId,
103        epic_id: &TaskId,
104        input: TaskCapture,
105    ) -> Result<TaskId, Error> {
106        self.create_consumer_epic_task(workspace_id, Some(epic_id.clone()), input)
107            .await
108    }
109
110    async fn create_consumer_epic_task(
111        &self,
112        workspace_id: &WorkspaceId,
113        epic_id: Option<TaskId>,
114        input: TaskCapture,
115    ) -> Result<TaskId, Error> {
116        validate_optional_date("due_on", input.due_on.as_deref())?;
117        let workspace = self.workspace(workspace_id).await?;
118        let project = input
119            .project
120            .ok_or_else(|| Error::new(ErrorCode::Validation, "project is required".into()))?;
121        let outcome = self
122            .database
123            .create_task_with_options(
124                &workspace,
125                TaskDraft {
126                    title: input.title.trim().to_string(),
127                    description: input.description,
128                    project: Some(project),
129                    status: input.status.as_str().into(),
130                    priority: input.priority.as_str().into(),
131                    source: input.source,
132                    labels: input.labels,
133                    metadata: Vec::new(),
134                    available_at: None,
135                    due_on: input.due_on,
136                    is_epic: epic_id.is_none(),
137                },
138                TaskCreationOptions::for_consumer_epic(epic_id),
139            )
140            .await
141            .map_err(Error::from_internal)?;
142        Ok(outcome.task.id)
143    }
144
145    pub async fn set_epic_child(
146        &self,
147        workspace_id: &WorkspaceId,
148        epic_id: &TaskId,
149        child_id: &TaskId,
150        linked: bool,
151    ) -> Result<bool, Error> {
152        let workspace = self.workspace(workspace_id).await?;
153        self.database
154            .set_epic_child(&workspace, epic_id, child_id, linked)
155            .await
156            .map_err(Error::from_internal)
157    }
158}