use std::path::Path;
use anyhow::Result;
use tokio::task::JoinHandle;
use crate::config::TaskIntakeConfig;
use crate::operations::{TaskAttachmentAddInput, TaskDraft, TaskOutcome};
use crate::tui::authoring::PendingTaskAttachment;
use crate::undo::UndoCommand;
use super::TuiStore;
struct CreatedTaskMessage {
message: String,
selected: Option<usize>,
}
#[derive(Debug)]
pub(crate) struct TaskCreationCommittedError {
source: anyhow::Error,
}
impl std::fmt::Display for TaskCreationCommittedError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
formatter,
"task committed but TUI finalization failed: {}",
self.source
)
}
}
impl std::error::Error for TaskCreationCommittedError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
Some(self.source.as_ref())
}
}
pub(crate) fn task_creation_committed(error: &anyhow::Error) -> bool {
error.downcast_ref::<TaskCreationCommittedError>().is_some()
}
fn committed_error(source: anyhow::Error) -> anyhow::Error {
TaskCreationCommittedError { source }.into()
}
impl TuiStore {
pub(crate) fn spawn_task_intake(
&self,
config: TaskIntakeConfig,
input: String,
project: Option<String>,
) -> JoinHandle<Result<TaskDraft>> {
let database = self.database.clone();
let workspace = self.active_workspace.clone();
tokio::spawn(async move {
let context = crate::task_intake::TaskIntakeContext::load_with_database(
&database,
&workspace,
project.as_deref(),
)
.await?;
let output =
crate::task_intake::run_task_intake_command(&config, &context, &input).await?;
crate::task_intake::parsed_output_to_draft_with_database(&database, &context, &output)
.await
})
}
pub(crate) async fn create_task(
&mut self,
mut draft: TaskDraft,
current_selected_index: Option<usize>,
) -> Result<(String, Option<usize>)> {
if draft.project.is_none() {
draft.project = self.inferred_add_project().await?;
}
let outcome = self
.database
.create_task(&self.active_workspace, draft)
.await?;
let created = self
.finish_task_creation(outcome, Vec::new(), current_selected_index)
.await
.map_err(committed_error)?;
Ok((created.message, created.selected))
}
pub(crate) async fn create_task_with_attachments(
&mut self,
mut draft: TaskDraft,
current_selected_index: Option<usize>,
blob_dir: &Path,
lifecycle_policy: crate::attachments::lifecycle::LifecyclePolicy,
attachments: Vec<PendingTaskAttachment>,
) -> Result<(String, Option<usize>)> {
if draft.project.is_none() {
draft.project = self.inferred_add_project().await?;
}
let attachment_ids = attachments
.iter()
.map(|attachment| attachment.attachment_id.clone())
.collect();
let inputs = attachments
.into_iter()
.map(|attachment| TaskAttachmentAddInput {
attachment_id: attachment.attachment_id,
input: attachment.input,
})
.collect();
let outcome = self
.database
.create_task_with_attachments(
&self.active_workspace,
blob_dir,
lifecycle_policy,
draft,
inputs,
)
.await?;
let created = self
.finish_task_creation(outcome, attachment_ids, current_selected_index)
.await
.map_err(committed_error)?;
Ok((created.message, created.selected))
}
async fn finish_task_creation(
&mut self,
outcome: TaskOutcome,
attachment_ids: Vec<String>,
current_selected_index: Option<usize>,
) -> Result<CreatedTaskMessage> {
let previous_id = self
.selected_task(current_selected_index)
.map(|item| item.task.id.clone());
let task_id = outcome.task.id.clone();
let display_refs = self
.database
.display_ref_context(&self.active_workspace.id)
.await?;
let message_ref = display_refs.display_ref(&outcome.task);
let workspace_id = self.active_workspace.id.clone();
let snapshot = self
.database
.task_undo_snapshot(&workspace_id, &task_id)
.await?;
self.record_undo_commands(
&format!("task {task_id}"),
vec![UndoCommand::DeleteCreatedTask {
task_id: task_id.clone(),
create_change_id: outcome.create_change_id,
attachment_ids,
attachment_change_ids: outcome.attachment_change_ids,
expected: snapshot,
}],
)
.await?;
self.refresh(None).await?;
let created_index = self.tasks.iter().position(|item| item.task.id == task_id);
if created_index.is_some() {
return Ok(CreatedTaskMessage {
message: format!("created task {message_ref}"),
selected: created_index,
});
}
let restored = self.restored_task_selection(previous_id.as_ref());
Ok(CreatedTaskMessage {
message: format!("created task {message_ref} hidden by current filters"),
selected: restored,
})
}
pub(crate) async fn add_note_to_task(
&mut self,
task_id: &crate::ids::TaskId,
body: String,
) -> Result<String> {
let outcome = self
.database
.add_note(&self.active_workspace, task_id, body)
.await?;
let note_change_id = outcome.change_id.clone();
self.record_undo_commands(
&format!("note {}", outcome.note_id),
vec![UndoCommand::DeleteCreatedNote {
task_id: task_id.clone(),
note_id: outcome.note_id.clone(),
note_add_change_id: note_change_id,
}],
)
.await?;
Ok(outcome.note_id)
}
}