use std::collections::{BTreeMap, BTreeSet};
use todoapp_core::{
Attachment, AttachmentKind, Command, ComponentStore, Date, Due, Duration, Id, IssueRef, Link,
LinkKind, Position, Recurrence, Status, Tags, TaskEntityStore, Title,
};
use crate::service::{Error, Services, TaskSnapshot};
pub enum Anchor {
Before(Id),
After(Id),
}
impl<'a, St: ComponentStore + TaskEntityStore> Services<'a, St> {
pub async fn create(
&self,
title: impl Into<String>,
parent: Option<&Id>,
status: Status,
tags: impl IntoIterator<Item = String>,
) -> Result<TaskSnapshot, Error> {
let id = self.ids.next_id();
let now = self.clock.now();
self.store.create(&id, now, now).await;
self.store.set(&id, Title(title.into())).await;
self.store.set(&id, status).await;
let tags: BTreeSet<String> = tags.into_iter().collect();
if !tags.is_empty() {
self.store.set(&id, Tags(tags)).await;
}
let root = Id::root();
self.attach(&id, parent.unwrap_or(&root), None).await?;
self.snapshot(&id).await
}
pub async fn batch_create(&self, text: &str) -> Result<Vec<TaskSnapshot>, Error> {
let mut created = Vec::new();
let mut stack: Vec<Id> = Vec::new();
for raw in text.lines() {
let title = raw.trim();
if title.is_empty() {
continue;
}
let depth = indent_depth(raw);
stack.truncate(depth);
let parent = stack.last().cloned();
let task = self
.create(title, parent.as_ref(), Status::Draft, [])
.await?;
stack.push(task.id.clone());
created.push(task);
}
Ok(created)
}
pub async fn set_title(
&self,
id: &Id,
title: impl Into<String>,
) -> Result<TaskSnapshot, Error> {
self.run(id, Command::SetTitle(title.into())).await
}
pub async fn set_notes(&self, id: &Id, notes: Option<String>) -> Result<TaskSnapshot, Error> {
self.run(id, Command::SetNotes(notes)).await
}
pub async fn set_status(&self, id: &Id, status: Status) -> Result<TaskSnapshot, Error> {
self.run(id, Command::SetStatus(status)).await
}
pub async fn set_due(&self, id: &Id, due: Option<Due>) -> Result<TaskSnapshot, Error> {
self.run(id, Command::SetSchedule(due)).await
}
pub async fn set_estimate(
&self,
id: &Id,
estimate: Option<Duration>,
) -> Result<TaskSnapshot, Error> {
self.run(id, Command::SetEstimate(estimate)).await
}
pub async fn add_time_spent(&self, id: &Id, spent: Duration) -> Result<TaskSnapshot, Error> {
self.run(id, Command::AddTimeSpent(spent)).await
}
pub async fn add_tag(&self, id: &Id, tag: impl Into<String>) -> Result<TaskSnapshot, Error> {
self.run(id, Command::AddTag(tag.into())).await
}
pub async fn remove_tag(&self, id: &Id, tag: impl Into<String>) -> Result<TaskSnapshot, Error> {
self.run(id, Command::RemoveTag(tag.into())).await
}
pub async fn assign(&self, id: &Id, actor: Id) -> Result<TaskSnapshot, Error> {
self.run(id, Command::Assign(actor)).await
}
pub async fn unassign(&self, id: &Id, actor: Id) -> Result<TaskSnapshot, Error> {
self.run(id, Command::Unassign(actor)).await
}
pub async fn claim(&self, id: &Id, actor: Id) -> Result<TaskSnapshot, Error> {
self.run(id, Command::Claim(actor)).await
}
pub async fn set_recurrence(
&self,
id: &Id,
recurrence: Option<Recurrence>,
) -> Result<TaskSnapshot, Error> {
self.run(id, Command::SetRecurrence(recurrence)).await
}
pub async fn set_issue_ref(
&self,
id: &Id,
issue_ref: Option<IssueRef>,
) -> Result<TaskSnapshot, Error> {
self.run(id, Command::SetIssueRef(issue_ref)).await
}
pub async fn set_time_log(
&self,
id: &Id,
time_log: BTreeMap<Date, Duration>,
) -> Result<TaskSnapshot, Error> {
self.run(id, Command::SetTimeLog(time_log)).await
}
pub async fn set_archived(&self, id: &Id, archived: bool) -> Result<TaskSnapshot, Error> {
self.run(id, Command::SetArchived(archived)).await
}
pub async fn add_attachment_from_bytes(
&self,
id: &Id,
title: impl Into<String>,
bytes: Vec<u8>,
mime: Option<String>,
) -> Result<TaskSnapshot, Error> {
let blob = self.blobs.put(bytes).await;
let att = Attachment {
id: self.ids.next_id(),
kind: AttachmentKind::File,
title: title.into(),
url: None,
blob: Some(blob),
mime,
};
self.run(id, Command::AddAttachment(att)).await
}
pub async fn add_attachment_link(
&self,
id: &Id,
title: impl Into<String>,
url: impl Into<String>,
) -> Result<TaskSnapshot, Error> {
let att = Attachment {
id: self.ids.next_id(),
kind: AttachmentKind::Link,
title: title.into(),
url: Some(url.into()),
blob: None,
mime: None,
};
self.run(id, Command::AddAttachment(att)).await
}
pub async fn remove_attachment(
&self,
id: &Id,
attachment_id: Id,
) -> Result<TaskSnapshot, Error> {
self.run(id, Command::RemoveAttachment(attachment_id)).await
}
pub async fn move_task(
&self,
id: &Id,
parent: &Id,
anchor: Option<Anchor>,
) -> Result<(), Error> {
if parent == id || self.descendants(id).await.contains(parent) {
return Err(Error::Cycle(format!("{id} cannot be moved under {parent}")));
}
if let Some(old) = self.raw_parent_of(id).await {
self.links.remove(&old, id, LinkKind::Child).await;
}
self.attach(id, parent, anchor).await
}
pub async fn reorder(&self, id: &Id, anchor: Anchor) -> Result<(), Error> {
let parent = self
.raw_parent_of(id)
.await
.ok_or_else(|| Error::Cycle(format!("{id} has no parent to reorder within")))?;
self.attach(id, &parent, Some(anchor)).await
}
pub async fn block(&self, blocker: &Id, blocked: &Id) -> Result<(), Error> {
if blocker == blocked || self.blocks_reaches(blocked, blocker).await {
return Err(Error::Cycle(format!("{blocker} blocks {blocked}")));
}
let last = self
.links
.outgoing(blocker, LinkKind::Blocks)
.await
.last()
.map(|l| l.position.0);
self.links
.put(Link {
from: blocker.clone(),
to: blocked.clone(),
kind: LinkKind::Blocks,
position: Position(Position::between(last, None)),
})
.await;
Ok(())
}
pub(crate) async fn attach(
&self,
id: &Id,
parent: &Id,
anchor: Option<Anchor>,
) -> Result<(), Error> {
let sibs: Vec<_> = self
.children_of(parent)
.await
.into_iter()
.filter(|l| &l.to != id)
.collect();
let pos = match anchor {
None => Position::between(sibs.last().map(|l| l.position.0), None),
Some(Anchor::Before(x)) => {
let i = sibs.iter().position(|l| l.to == x);
match i {
Some(i) => {
let before = if i == 0 {
None
} else {
Some(sibs[i - 1].position.0)
};
Position::between(before, Some(sibs[i].position.0))
}
None => Position::between(sibs.last().map(|l| l.position.0), None),
}
}
Some(Anchor::After(x)) => {
let i = sibs.iter().position(|l| l.to == x);
match i {
Some(i) => {
let after = sibs.get(i + 1).map(|l| l.position.0);
Position::between(Some(sibs[i].position.0), after)
}
None => Position::between(sibs.last().map(|l| l.position.0), None),
}
}
};
self.links
.put(Link {
from: parent.clone(),
to: id.clone(),
kind: LinkKind::Child,
position: Position(pos),
})
.await;
Ok(())
}
async fn blocks_reaches(&self, start: &Id, target: &Id) -> bool {
let mut stack = vec![start.clone()];
let mut seen = std::collections::HashSet::new();
while let Some(cur) = stack.pop() {
if &cur == target {
return true;
}
if seen.insert(cur.clone()) {
stack.extend(
self.links
.outgoing(&cur, LinkKind::Blocks)
.await
.into_iter()
.map(|l| l.to),
);
}
}
false
}
}
fn indent_depth(line: &str) -> usize {
let mut spaces = 0;
for c in line.chars() {
match c {
'\t' => spaces += 2,
' ' => spaces += 1,
_ => break,
}
}
spaces / 2
}