1use std::collections::{BTreeMap, BTreeSet};
4
5use todoapp_core::{
6 Attachment, AttachmentKind, Command, ComponentStore, Date, Due, Duration, Id, IssueRef, Link,
7 LinkKind, Position, Recurrence, Status, Tags, TaskEntityStore, Title,
8};
9
10use crate::service::{Error, Services, TaskSnapshot};
11
12pub enum Anchor {
14 Before(Id),
15 After(Id),
16}
17
18impl<'a, St: ComponentStore + TaskEntityStore> Services<'a, St> {
19 pub async fn create(
21 &self,
22 title: impl Into<String>,
23 parent: Option<&Id>,
24 status: Status,
25 tags: impl IntoIterator<Item = String>,
26 ) -> Result<TaskSnapshot, Error> {
27 let id = self.ids.next_id();
28 let now = self.clock.now();
29 self.store.create(&id, now, now).await;
30 self.store.set(&id, Title(title.into())).await;
31 self.store.set(&id, status).await;
32 let tags: BTreeSet<String> = tags.into_iter().collect();
33 if !tags.is_empty() {
34 self.store.set(&id, Tags(tags)).await;
35 }
36 let root = Id::root();
39 self.attach(&id, parent.unwrap_or(&root), None).await?;
40 self.snapshot(&id).await
41 }
42
43 pub async fn batch_create(&self, text: &str) -> Result<Vec<TaskSnapshot>, Error> {
46 let mut created = Vec::new();
47 let mut stack: Vec<Id> = Vec::new();
49 for raw in text.lines() {
50 let title = raw.trim();
51 if title.is_empty() {
52 continue;
53 }
54 let depth = indent_depth(raw);
55 stack.truncate(depth);
56 let parent = stack.last().cloned();
57 let task = self
58 .create(title, parent.as_ref(), Status::Draft, [])
59 .await?;
60 stack.push(task.id.clone());
61 created.push(task);
62 }
63 Ok(created)
64 }
65
66 pub async fn set_title(
69 &self,
70 id: &Id,
71 title: impl Into<String>,
72 ) -> Result<TaskSnapshot, Error> {
73 self.run(id, Command::SetTitle(title.into())).await
74 }
75 pub async fn set_notes(&self, id: &Id, notes: Option<String>) -> Result<TaskSnapshot, Error> {
76 self.run(id, Command::SetNotes(notes)).await
77 }
78 pub async fn set_status(&self, id: &Id, status: Status) -> Result<TaskSnapshot, Error> {
79 self.run(id, Command::SetStatus(status)).await
80 }
81 pub async fn set_due(&self, id: &Id, due: Option<Due>) -> Result<TaskSnapshot, Error> {
82 self.run(id, Command::SetSchedule(due)).await
83 }
84 pub async fn set_estimate(
85 &self,
86 id: &Id,
87 estimate: Option<Duration>,
88 ) -> Result<TaskSnapshot, Error> {
89 self.run(id, Command::SetEstimate(estimate)).await
90 }
91 pub async fn add_time_spent(&self, id: &Id, spent: Duration) -> Result<TaskSnapshot, Error> {
92 self.run(id, Command::AddTimeSpent(spent)).await
93 }
94 pub async fn add_tag(&self, id: &Id, tag: impl Into<String>) -> Result<TaskSnapshot, Error> {
95 self.run(id, Command::AddTag(tag.into())).await
96 }
97 pub async fn remove_tag(&self, id: &Id, tag: impl Into<String>) -> Result<TaskSnapshot, Error> {
98 self.run(id, Command::RemoveTag(tag.into())).await
99 }
100 pub async fn assign(&self, id: &Id, actor: Id) -> Result<TaskSnapshot, Error> {
101 self.run(id, Command::Assign(actor)).await
102 }
103 pub async fn unassign(&self, id: &Id, actor: Id) -> Result<TaskSnapshot, Error> {
104 self.run(id, Command::Unassign(actor)).await
105 }
106 pub async fn claim(&self, id: &Id, actor: Id) -> Result<TaskSnapshot, Error> {
108 self.run(id, Command::Claim(actor)).await
109 }
110 pub async fn set_recurrence(
113 &self,
114 id: &Id,
115 recurrence: Option<Recurrence>,
116 ) -> Result<TaskSnapshot, Error> {
117 self.run(id, Command::SetRecurrence(recurrence)).await
118 }
119 pub async fn set_issue_ref(
120 &self,
121 id: &Id,
122 issue_ref: Option<IssueRef>,
123 ) -> Result<TaskSnapshot, Error> {
124 self.run(id, Command::SetIssueRef(issue_ref)).await
125 }
126 pub async fn set_time_log(
127 &self,
128 id: &Id,
129 time_log: BTreeMap<Date, Duration>,
130 ) -> Result<TaskSnapshot, Error> {
131 self.run(id, Command::SetTimeLog(time_log)).await
132 }
133 pub async fn set_archived(&self, id: &Id, archived: bool) -> Result<TaskSnapshot, Error> {
134 self.run(id, Command::SetArchived(archived)).await
135 }
136 pub async fn add_attachment_from_bytes(
138 &self,
139 id: &Id,
140 title: impl Into<String>,
141 bytes: Vec<u8>,
142 mime: Option<String>,
143 ) -> Result<TaskSnapshot, Error> {
144 let blob = self.blobs.put(bytes).await;
145 let att = Attachment {
146 id: self.ids.next_id(),
147 kind: AttachmentKind::File,
148 title: title.into(),
149 url: None,
150 blob: Some(blob),
151 mime,
152 };
153 self.run(id, Command::AddAttachment(att)).await
154 }
155 pub async fn add_attachment_link(
157 &self,
158 id: &Id,
159 title: impl Into<String>,
160 url: impl Into<String>,
161 ) -> Result<TaskSnapshot, Error> {
162 let att = Attachment {
163 id: self.ids.next_id(),
164 kind: AttachmentKind::Link,
165 title: title.into(),
166 url: Some(url.into()),
167 blob: None,
168 mime: None,
169 };
170 self.run(id, Command::AddAttachment(att)).await
171 }
172 pub async fn remove_attachment(
173 &self,
174 id: &Id,
175 attachment_id: Id,
176 ) -> Result<TaskSnapshot, Error> {
177 self.run(id, Command::RemoveAttachment(attachment_id)).await
178 }
179
180 pub async fn move_task(
185 &self,
186 id: &Id,
187 parent: &Id,
188 anchor: Option<Anchor>,
189 ) -> Result<(), Error> {
190 if parent == id || self.descendants(id).await.contains(parent) {
191 return Err(Error::Cycle(format!("{id} cannot be moved under {parent}")));
192 }
193 if let Some(old) = self.raw_parent_of(id).await {
194 self.links.remove(&old, id, LinkKind::Child).await;
195 }
196 self.attach(id, parent, anchor).await
197 }
198
199 pub async fn reorder(&self, id: &Id, anchor: Anchor) -> Result<(), Error> {
201 let parent = self
203 .raw_parent_of(id)
204 .await
205 .ok_or_else(|| Error::Cycle(format!("{id} has no parent to reorder within")))?;
206 self.attach(id, &parent, Some(anchor)).await
207 }
208
209 pub async fn block(&self, blocker: &Id, blocked: &Id) -> Result<(), Error> {
211 if blocker == blocked || self.blocks_reaches(blocked, blocker).await {
212 return Err(Error::Cycle(format!("{blocker} blocks {blocked}")));
213 }
214 let last = self
215 .links
216 .outgoing(blocker, LinkKind::Blocks)
217 .await
218 .last()
219 .map(|l| l.position.0);
220 self.links
221 .put(Link {
222 from: blocker.clone(),
223 to: blocked.clone(),
224 kind: LinkKind::Blocks,
225 position: Position(Position::between(last, None)),
226 })
227 .await;
228 Ok(())
229 }
230
231 pub(crate) async fn attach(
233 &self,
234 id: &Id,
235 parent: &Id,
236 anchor: Option<Anchor>,
237 ) -> Result<(), Error> {
238 let sibs: Vec<_> = self
240 .children_of(parent)
241 .await
242 .into_iter()
243 .filter(|l| &l.to != id)
244 .collect();
245 let pos = match anchor {
246 None => Position::between(sibs.last().map(|l| l.position.0), None),
247 Some(Anchor::Before(x)) => {
248 let i = sibs.iter().position(|l| l.to == x);
249 match i {
250 Some(i) => {
251 let before = if i == 0 {
252 None
253 } else {
254 Some(sibs[i - 1].position.0)
255 };
256 Position::between(before, Some(sibs[i].position.0))
257 }
258 None => Position::between(sibs.last().map(|l| l.position.0), None),
259 }
260 }
261 Some(Anchor::After(x)) => {
262 let i = sibs.iter().position(|l| l.to == x);
263 match i {
264 Some(i) => {
265 let after = sibs.get(i + 1).map(|l| l.position.0);
266 Position::between(Some(sibs[i].position.0), after)
267 }
268 None => Position::between(sibs.last().map(|l| l.position.0), None),
269 }
270 }
271 };
272 self.links
273 .put(Link {
274 from: parent.clone(),
275 to: id.clone(),
276 kind: LinkKind::Child,
277 position: Position(pos),
278 })
279 .await;
280 Ok(())
281 }
282
283 async fn blocks_reaches(&self, start: &Id, target: &Id) -> bool {
285 let mut stack = vec![start.clone()];
286 let mut seen = std::collections::HashSet::new();
287 while let Some(cur) = stack.pop() {
288 if &cur == target {
289 return true;
290 }
291 if seen.insert(cur.clone()) {
292 stack.extend(
293 self.links
294 .outgoing(&cur, LinkKind::Blocks)
295 .await
296 .into_iter()
297 .map(|l| l.to),
298 );
299 }
300 }
301 false
302 }
303}
304
305fn indent_depth(line: &str) -> usize {
306 let mut spaces = 0;
307 for c in line.chars() {
308 match c {
309 '\t' => spaces += 2,
310 ' ' => spaces += 1,
311 _ => break,
312 }
313 }
314 spaces / 2
315}