1use crate::{
4 api::{ApiClient, ApiError},
5 api::{SingleResponse, api_delete, api_post, api_put},
6 models::{
7 Task, TaskCreateRequest, TaskListParams, TaskReference, TaskSearchParams, TaskSort,
8 TaskUpdateRequest,
9 },
10};
11use futures_util::{StreamExt, pin_mut};
12use serde::{Deserialize, Serialize};
13use std::collections::BTreeSet;
14use tracing::debug;
15
16pub async fn list_tasks(
22 client: &ApiClient,
23 mut params: TaskListParams,
24) -> Result<Vec<Task>, ApiError> {
25 ensure_default_fields(&mut params);
26
27 let query = params.to_query();
28 let max_items = params.limit;
29 let stream = client.paginate_with_limit::<Task>("/tasks", query, max_items);
30 pin_mut!(stream);
31
32 let mut tasks = Vec::new();
33 while let Some(page) = stream.next().await {
34 let mut page = page?;
35 tasks.append(&mut page);
36 }
37
38 if params.include_subtasks {
43 debug!("Fetching subtasks for {} parent tasks", tasks.len());
44 let mut all_subtasks = Vec::new();
45 for task in &tasks {
46 match list_subtasks(client, &task.gid, params.fields.iter().cloned().collect()).await {
48 Ok(subtasks) => {
49 if !subtasks.is_empty() {
50 debug!("Found {} subtasks for task {}", subtasks.len(), task.gid);
51 }
52 all_subtasks.extend(subtasks);
53 }
54 Err(e) => {
55 debug!("Failed to fetch subtasks for task {}: {}", task.gid, e);
56 }
58 }
59 }
60 debug!("Total subtasks fetched: {}", all_subtasks.len());
61 tasks.extend(all_subtasks);
62 }
63
64 params.apply_post_filters(&mut tasks);
65
66 if let Some(sort) = params.sort {
67 sort_tasks(&mut tasks, sort);
68 }
69
70 Ok(tasks)
71}
72
73pub async fn search_tasks(
79 client: &ApiClient,
80 mut params: TaskSearchParams,
81) -> Result<Vec<Task>, ApiError> {
82 ensure_default_fields_for_search(&mut params);
83
84 let query = params.to_query();
85 let max_items = params.limit;
86 let endpoint = format!("/workspaces/{}/tasks/search", params.workspace);
87 let stream = client.paginate_with_limit::<Task>(&endpoint, query, max_items);
88 pin_mut!(stream);
89
90 let mut tasks = Vec::new();
91 while let Some(page) = stream.next().await {
92 let mut page = page?;
93 tasks.append(&mut page);
94 }
95
96 Ok(tasks)
97}
98
99fn ensure_default_fields_for_search(params: &mut TaskSearchParams) {
100 let defaults = [
102 "gid",
103 "name",
104 "completed",
105 "completed_at",
106 "due_on",
107 "due_at",
108 "start_on",
109 "start_at",
110 "assignee.name",
111 "assignee.gid",
112 "assignee.email",
113 "resource_type",
114 "resource_subtype",
115 "modified_at",
116 "workspace.name",
117 "workspace.gid",
118 "projects.name",
119 "projects.gid",
120 "tags.name",
121 "tags.gid",
122 "memberships.project.name",
123 "memberships.project.gid",
124 "memberships.section.name",
125 "memberships.section.gid",
126 "permalink_url",
127 "num_subtasks",
128 ];
129 for field in defaults {
130 params.fields.insert(field.to_string());
131 }
132}
133
134pub async fn get_task(
140 client: &ApiClient,
141 gid: &str,
142 fields: Vec<String>,
143) -> Result<Task, ApiError> {
144 let mut field_set: BTreeSet<String> = fields.into_iter().collect();
145 for field in detail_defaults() {
146 field_set.insert((*field).to_string());
147 }
148
149 let mut query = Vec::new();
150 if !field_set.is_empty() {
151 query.push((
152 "opt_fields".into(),
153 field_set.into_iter().collect::<Vec<_>>().join(","),
154 ));
155 }
156
157 let response: SingleResponse<Task> = client
158 .get_json_with_pairs(&format!("/tasks/{gid}"), query)
159 .await?;
160 Ok(response.data)
161}
162
163pub async fn create_task(client: &ApiClient, request: TaskCreateRequest) -> Result<Task, ApiError> {
169 api_post(client, "/tasks", &request).await
170}
171
172pub async fn update_task(
178 client: &ApiClient,
179 gid: &str,
180 request: TaskUpdateRequest,
181) -> Result<Task, ApiError> {
182 api_put(client, &format!("/tasks/{gid}"), &request).await
183}
184
185pub async fn delete_task(client: &ApiClient, gid: &str) -> Result<(), ApiError> {
191 api_delete(client, &format!("/tasks/{gid}")).await
192}
193
194pub async fn list_subtasks(
200 client: &ApiClient,
201 gid: &str,
202 fields: Vec<String>,
203) -> Result<Vec<Task>, ApiError> {
204 let mut field_set: BTreeSet<String> = fields.into_iter().collect();
205 ensure_subtask_fields(&mut field_set);
206
207 let mut query = Vec::new();
208 if !field_set.is_empty() {
209 let list = field_set.into_iter().collect::<Vec<_>>().join(",");
210 query.push(("opt_fields".into(), list));
211 }
212
213 let stream = client.paginate_with_limit::<Task>(&format!("/tasks/{gid}/subtasks"), query, None);
214 pin_mut!(stream);
215
216 let mut tasks = Vec::new();
217 while let Some(page) = stream.next().await {
218 let mut page = page?;
219 tasks.append(&mut page);
220 }
221 Ok(tasks)
222}
223
224pub async fn list_dependencies(
230 client: &ApiClient,
231 gid: &str,
232) -> Result<Vec<TaskReference>, ApiError> {
233 let response: TaskReferenceResponse = client
234 .get_json_with_pairs(&format!("/tasks/{gid}/dependencies"), vec![])
235 .await?;
236 Ok(response.data)
237}
238
239pub async fn list_dependents(
245 client: &ApiClient,
246 gid: &str,
247) -> Result<Vec<TaskReference>, ApiError> {
248 let response: TaskReferenceResponse = client
249 .get_json_with_pairs(&format!("/tasks/{gid}/dependents"), vec![])
250 .await?;
251 Ok(response.data)
252}
253
254pub async fn add_dependencies(
260 client: &ApiClient,
261 gid: &str,
262 dependencies: Vec<String>,
263) -> Result<(), ApiError> {
264 if dependencies.is_empty() {
265 return Ok(());
266 }
267
268 let payload = DependencyModifyRequest {
269 data: DependencyList { dependencies },
270 };
271 client
272 .post_void(&format!("/tasks/{gid}/addDependencies"), &payload)
273 .await
274}
275
276pub async fn remove_dependencies(
282 client: &ApiClient,
283 gid: &str,
284 dependencies: Vec<String>,
285) -> Result<(), ApiError> {
286 if dependencies.is_empty() {
287 return Ok(());
288 }
289
290 let payload = DependencyModifyRequest {
291 data: DependencyList { dependencies },
292 };
293 client
294 .post_void(&format!("/tasks/{gid}/removeDependencies"), &payload)
295 .await
296}
297
298pub async fn add_dependents(
304 client: &ApiClient,
305 gid: &str,
306 dependents: Vec<String>,
307) -> Result<(), ApiError> {
308 if dependents.is_empty() {
309 return Ok(());
310 }
311
312 let payload = DependentModifyRequest {
313 data: DependentList { dependents },
314 };
315 client
316 .post_void(&format!("/tasks/{gid}/addDependents"), &payload)
317 .await
318}
319
320pub async fn remove_dependents(
326 client: &ApiClient,
327 gid: &str,
328 dependents: Vec<String>,
329) -> Result<(), ApiError> {
330 if dependents.is_empty() {
331 return Ok(());
332 }
333
334 let payload = DependentModifyRequest {
335 data: DependentList { dependents },
336 };
337 client
338 .post_void(&format!("/tasks/{gid}/removeDependents"), &payload)
339 .await
340}
341
342pub async fn add_project(
348 client: &ApiClient,
349 gid: &str,
350 project: String,
351 section: Option<String>,
352) -> Result<(), ApiError> {
353 let payload = ProjectModifyRequest {
354 data: ProjectModifyData { project, section },
355 };
356 client
357 .post_void(&format!("/tasks/{gid}/addProject"), &payload)
358 .await
359}
360
361pub async fn remove_project(
367 client: &ApiClient,
368 gid: &str,
369 project: String,
370) -> Result<(), ApiError> {
371 let payload = ProjectModifyRequest {
372 data: ProjectModifyData {
373 project,
374 section: None,
375 },
376 };
377 client
378 .post_void(&format!("/tasks/{gid}/removeProject"), &payload)
379 .await
380}
381
382pub async fn add_followers(
388 client: &ApiClient,
389 gid: &str,
390 followers: Vec<String>,
391) -> Result<(), ApiError> {
392 if followers.is_empty() {
393 return Ok(());
394 }
395
396 let payload = FollowersModifyRequest {
397 data: FollowersList { followers },
398 };
399 client
400 .post_void(&format!("/tasks/{gid}/addFollowers"), &payload)
401 .await
402}
403
404pub async fn remove_followers(
410 client: &ApiClient,
411 gid: &str,
412 followers: Vec<String>,
413) -> Result<(), ApiError> {
414 if followers.is_empty() {
415 return Ok(());
416 }
417
418 let payload = FollowersModifyRequest {
419 data: FollowersList { followers },
420 };
421 client
422 .post_void(&format!("/tasks/{gid}/removeFollowers"), &payload)
423 .await
424}
425
426pub async fn add_tag(client: &ApiClient, gid: &str, tag: String) -> Result<(), ApiError> {
432 let payload = TagModifyRequest {
433 data: TagModifyData { tag },
434 };
435 client
436 .post_void(&format!("/tasks/{gid}/addTag"), &payload)
437 .await
438}
439
440pub async fn remove_tag(client: &ApiClient, gid: &str, tag: String) -> Result<(), ApiError> {
446 let payload = TagModifyRequest {
447 data: TagModifyData { tag },
448 };
449 client
450 .post_void(&format!("/tasks/{gid}/removeTag"), &payload)
451 .await
452}
453
454fn ensure_default_fields(params: &mut TaskListParams) {
455 let defaults = [
456 "gid",
457 "name",
458 "completed",
459 "completed_at",
460 "due_on",
461 "due_at",
462 "start_on",
463 "start_at",
464 "assignee.name",
465 "assignee.gid",
466 "assignee.email",
467 "resource_type",
468 "resource_subtype",
469 "modified_at",
470 "workspace.name",
471 "workspace.gid",
472 "projects.name",
473 "projects.gid",
474 "tags.name",
475 "tags.gid",
476 "memberships.project.name",
477 "memberships.project.gid",
478 "memberships.section.name",
479 "memberships.section.gid",
480 "permalink_url",
481 "num_subtasks",
482 ];
483 for field in defaults {
484 params.fields.insert(field.to_string());
485 }
486}
487
488const fn detail_defaults() -> &'static [&'static str] {
489 &[
490 "gid",
491 "name",
492 "completed",
493 "completed_at",
494 "due_on",
495 "due_at",
496 "start_on",
497 "start_at",
498 "notes",
499 "html_notes",
500 "assignee",
501 "assignee_status",
502 "assignee.name",
503 "assignee.email",
504 "completed_by",
505 "created_at",
506 "modified_at",
507 "workspace",
508 "workspace.name",
509 "workspace.gid",
510 "parent",
511 "projects",
512 "projects.name",
513 "projects.gid",
514 "memberships",
515 "tags",
516 "followers",
517 "followers.name",
518 "followers.email",
519 "dependencies",
520 "dependents",
521 "custom_fields",
522 "attachments",
523 "permalink_url",
524 "resource_subtype",
525 "num_subtasks",
526 ]
527}
528
529fn ensure_subtask_fields(fields: &mut BTreeSet<String>) {
530 fields.insert("parent".to_string());
532 fields.insert("parent.gid".to_string());
533 fields.insert("parent.name".to_string());
534
535 if fields.len() == 3 {
536 let defaults = ["gid", "name", "completed", "assignee.name"];
538 for field in defaults {
539 fields.insert(field.to_string());
540 }
541 }
542}
543
544fn sort_tasks(tasks: &mut [Task], sort: TaskSort) {
545 match sort {
546 TaskSort::Name => tasks.sort_by(|a, b| {
547 a.name
548 .to_ascii_lowercase()
549 .cmp(&b.name.to_ascii_lowercase())
550 }),
551 TaskSort::DueOn => tasks.sort_by(|a, b| {
552 a.due_on
553 .cmp(&b.due_on)
554 .then_with(|| a.due_at.cmp(&b.due_at))
555 }),
556 TaskSort::CreatedAt => tasks.sort_by(|a, b| a.created_at.cmp(&b.created_at)),
557 TaskSort::ModifiedAt => tasks.sort_by(|a, b| a.modified_at.cmp(&b.modified_at)),
558 TaskSort::Assignee => tasks.sort_by_key(assignee_label),
559 }
560}
561
562fn assignee_label(task: &Task) -> Option<String> {
563 task.assignee
564 .as_ref()
565 .map(crate::models::user::UserReference::label)
566}
567
568#[derive(Debug, Deserialize)]
569struct TaskReferenceResponse {
570 data: Vec<TaskReference>,
571}
572
573#[derive(Debug, Serialize)]
574struct DependencyModifyRequest {
575 data: DependencyList,
576}
577
578#[derive(Debug, Serialize)]
579struct DependencyList {
580 #[serde(skip_serializing_if = "Vec::is_empty")]
581 dependencies: Vec<String>,
582}
583
584#[derive(Debug, Serialize)]
585struct DependentModifyRequest {
586 data: DependentList,
587}
588
589#[derive(Debug, Serialize)]
590struct DependentList {
591 #[serde(skip_serializing_if = "Vec::is_empty")]
592 dependents: Vec<String>,
593}
594
595#[derive(Debug, Serialize)]
596struct ProjectModifyRequest {
597 data: ProjectModifyData,
598}
599
600#[derive(Debug, Serialize)]
601struct ProjectModifyData {
602 project: String,
603 #[serde(skip_serializing_if = "Option::is_none")]
604 section: Option<String>,
605}
606
607#[derive(Debug, Serialize)]
608struct FollowersModifyRequest {
609 data: FollowersList,
610}
611
612#[derive(Debug, Serialize)]
613struct FollowersList {
614 #[serde(skip_serializing_if = "Vec::is_empty")]
615 followers: Vec<String>,
616}
617
618#[derive(Debug, Serialize)]
619struct TagModifyRequest {
620 data: TagModifyData,
621}
622
623#[derive(Debug, Serialize)]
624struct TagModifyData {
625 tag: String,
626}