Skip to main content

aven_core/api/
epics.rs

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