aven-core 0.1.35

Core library for the Aven local-first task manager
Documentation
use super::*;

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EpicProgress {
    pub total: u32,
    pub open: u32,
    pub done: u32,
    pub canceled: u32,
}

impl From<crate::query::EpicRollup> for EpicProgress {
    fn from(value: crate::query::EpicRollup) -> Self {
        Self {
            total: value.total.min(u32::MAX as usize) as u32,
            open: value.open.min(u32::MAX as usize) as u32,
            done: value.done.min(u32::MAX as usize) as u32,
            canceled: value.canceled.min(u32::MAX as usize) as u32,
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EpicSummary {
    pub task: TaskListRow,
    pub progress: EpicProgress,
}

impl Store {
    pub async fn epics(&self, workspace_id: &WorkspaceId) -> Result<Vec<EpicSummary>, Error> {
        self.workspace(workspace_id).await?;
        let items = self
            .database
            .list_task_summary_items(
                workspace_id,
                TaskFilters {
                    epics_only: true,
                    ..TaskFilters::default()
                },
                TaskQueryMode::Flat,
                TaskSort::Updated,
                SortDirection::Desc,
                None,
            )
            .await
            .map_err(Error::from_internal)?;
        Ok(items
            .into_iter()
            .map(|mut item| EpicSummary {
                progress: item.epic_rollup.take().unwrap_or_default().into(),
                task: item.into(),
            })
            .collect())
    }

    pub async fn epic_candidates(
        &self,
        workspace_id: &WorkspaceId,
        epic_id: &TaskId,
    ) -> Result<Vec<TaskListRow>, Error> {
        let workspace = self.workspace(workspace_id).await?;
        let epic = self
            .database
            .resolve_task_ref(&workspace, epic_id.as_str())
            .await
            .map_err(Error::from_internal)?;
        if epic.deleted || !epic.is_epic {
            return Err(Error::new(
                ErrorCode::Validation,
                "epic is unavailable".into(),
            ));
        }
        self.database
            .list_task_summary_items(
                workspace_id,
                TaskFilters {
                    project: Some(epic.project_key),
                    exclude_epics: true,
                    without_epic_link: true,
                    ..TaskFilters::default()
                },
                TaskQueryMode::Flat,
                TaskSort::Updated,
                SortDirection::Desc,
                None,
            )
            .await
            .map(|items| items.into_iter().map(Into::into).collect())
            .map_err(Error::from_internal)
    }

    pub async fn create_epic(
        &self,
        workspace_id: &WorkspaceId,
        input: TaskCapture,
    ) -> Result<TaskId, Error> {
        self.create_consumer_epic_task(workspace_id, None, input)
            .await
    }

    pub async fn create_epic_child(
        &self,
        workspace_id: &WorkspaceId,
        epic_id: &TaskId,
        input: TaskCapture,
    ) -> Result<TaskId, Error> {
        self.create_consumer_epic_task(workspace_id, Some(epic_id.clone()), input)
            .await
    }

    async fn create_consumer_epic_task(
        &self,
        workspace_id: &WorkspaceId,
        epic_id: Option<TaskId>,
        input: TaskCapture,
    ) -> Result<TaskId, Error> {
        validate_optional_date("due_on", input.due_on.as_deref())?;
        let workspace = self.workspace(workspace_id).await?;
        let project = input
            .project
            .ok_or_else(|| Error::new(ErrorCode::Validation, "project is required".into()))?;
        let outcome = self
            .database
            .create_task_with_options(
                &workspace,
                TaskDraft {
                    title: input.title.trim().to_string(),
                    description: input.description,
                    project: Some(project),
                    status: input.status.as_str().into(),
                    priority: input.priority.as_str().into(),
                    source: input.source,
                    labels: input.labels,
                    metadata: Vec::new(),
                    available_at: None,
                    due_on: input.due_on,
                    is_epic: epic_id.is_none(),
                },
                TaskCreationOptions::for_consumer_epic(epic_id),
            )
            .await
            .map_err(Error::from_internal)?;
        Ok(outcome.task.id)
    }

    pub async fn set_epic_child(
        &self,
        workspace_id: &WorkspaceId,
        epic_id: &TaskId,
        child_id: &TaskId,
        linked: bool,
    ) -> Result<bool, Error> {
        let workspace = self.workspace(workspace_id).await?;
        self.database
            .set_epic_child(&workspace, epic_id, child_id, linked)
            .await
            .map_err(Error::from_internal)
    }
}