Skip to content

Commit 93f0aa5

Browse files
committed
feat(tasks): implement bulk task actions (complete/delete)
- Added checkboxes to task rows and "Select All" in header - Implemented floating action panel for selected tasks - Added backend endpoints for bulk complete and delete - Updated frontend state to track selected UUIDs - Added confirmation dialogs and loading states - Added comprehensive tests for bulk selection and actions - Fixes: #178
1 parent 543a4a8 commit 93f0aa5

File tree

8 files changed

+1063
-220
lines changed

8 files changed

+1063
-220
lines changed
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package controllers
2+
3+
import (
4+
"ccsync_backend/models"
5+
"ccsync_backend/utils/tw"
6+
"encoding/json"
7+
"fmt"
8+
"io"
9+
"net/http"
10+
)
11+
12+
// BulkCompleteTaskHandler godoc
13+
// @Summary Bulk complete tasks
14+
// @Description Mark multiple tasks as completed in Taskwarrior
15+
// @Tags Tasks
16+
// @Accept json
17+
// @Produce json
18+
// @Param task body models.BulkCompleteTaskRequestBody true "Bulk task completion details"
19+
// @Success 202 {string} string "Bulk task completion accepted for processing"
20+
// @Failure 400 {string} string "Invalid request - missing or empty taskuuids"
21+
// @Failure 405 {string} string "Method not allowed"
22+
// @Router /complete-tasks [post]
23+
func BulkCompleteTaskHandler(w http.ResponseWriter, r *http.Request) {
24+
if r.Method != http.MethodPost {
25+
http.Error(w, "Invalid request method", http.StatusMethodNotAllowed)
26+
return
27+
}
28+
29+
body, err := io.ReadAll(r.Body)
30+
if err != nil {
31+
http.Error(w, fmt.Sprintf("error reading request body: %v", err), http.StatusBadRequest)
32+
return
33+
}
34+
defer r.Body.Close()
35+
36+
var requestBody models.BulkCompleteTaskRequestBody
37+
38+
if err := json.Unmarshal(body, &requestBody); err != nil {
39+
http.Error(w, fmt.Sprintf("error decoding request body: %v", err), http.StatusBadRequest)
40+
return
41+
}
42+
43+
email := requestBody.Email
44+
encryptionSecret := requestBody.EncryptionSecret
45+
uuid := requestBody.UUID
46+
taskUUIDs := requestBody.TaskUUIDs
47+
48+
if len(taskUUIDs) == 0 {
49+
http.Error(w, "taskuuids is required and cannot be empty", http.StatusBadRequest)
50+
return
51+
}
52+
53+
logStore := models.GetLogStore()
54+
55+
// Create a *single* job for all UUIDs
56+
job := Job{
57+
Name: "Bulk Complete Tasks",
58+
Execute: func() error {
59+
for _, tu := range taskUUIDs {
60+
logStore.AddLog("INFO", fmt.Sprintf("[Bulk Complete] Starting: %s", tu), uuid, "Bulk Complete Task")
61+
62+
err := tw.CompleteTaskInTaskwarrior(email, encryptionSecret, uuid, tu)
63+
if err != nil {
64+
logStore.AddLog("ERROR", fmt.Sprintf("[Bulk Complete] Failed: %s (%v)", tu, err), uuid, "Bulk Complete Task")
65+
continue
66+
}
67+
68+
logStore.AddLog("INFO", fmt.Sprintf("[Bulk Complete] Completed: %s", tu), uuid, "Bulk Complete Task")
69+
}
70+
return nil
71+
},
72+
}
73+
74+
GlobalJobQueue.AddJob(job)
75+
w.WriteHeader(http.StatusAccepted)
76+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package controllers
2+
3+
import (
4+
"ccsync_backend/models"
5+
"ccsync_backend/utils/tw"
6+
"encoding/json"
7+
"fmt"
8+
"io"
9+
"net/http"
10+
)
11+
12+
// BulkDeleteTaskHandler godoc
13+
// @Summary Bulk delete tasks
14+
// @Description Delete multiple tasks in Taskwarrior
15+
// @Tags Tasks
16+
// @Accept json
17+
// @Produce json
18+
// @Param task body models.BulkDeleteTaskRequestBody true "Bulk task deletion details"
19+
// @Success 202 {string} string "Bulk task deletion accepted for processing"
20+
// @Failure 400 {string} string "Invalid request - missing or empty taskuuids"
21+
// @Failure 405 {string} string "Method not allowed"
22+
// @Router /delete-tasks [post]
23+
func BulkDeleteTaskHandler(w http.ResponseWriter, r *http.Request) {
24+
if r.Method != http.MethodPost {
25+
http.Error(w, "Invalid request method", http.StatusMethodNotAllowed)
26+
return
27+
}
28+
29+
body, err := io.ReadAll(r.Body)
30+
if err != nil {
31+
http.Error(w, fmt.Sprintf("error reading request body: %v", err), http.StatusBadRequest)
32+
return
33+
}
34+
defer r.Body.Close()
35+
36+
var requestBody models.BulkDeleteTaskRequestBody
37+
38+
if err := json.Unmarshal(body, &requestBody); err != nil {
39+
http.Error(w, fmt.Sprintf("error decoding request body: %v", err), http.StatusBadRequest)
40+
return
41+
}
42+
43+
email := requestBody.Email
44+
encryptionSecret := requestBody.EncryptionSecret
45+
uuid := requestBody.UUID
46+
taskUUIDs := requestBody.TaskUUIDs
47+
48+
if len(taskUUIDs) == 0 {
49+
http.Error(w, "taskuuids is required and cannot be empty", http.StatusBadRequest)
50+
return
51+
}
52+
53+
logStore := models.GetLogStore()
54+
55+
job := Job{
56+
Name: "Bulk Delete Tasks",
57+
Execute: func() error {
58+
for _, tu := range taskUUIDs {
59+
logStore.AddLog("INFO", fmt.Sprintf("[Bulk Delete] Starting: %s", tu), uuid, "Bulk Delete Task")
60+
61+
err := tw.DeleteTaskInTaskwarrior(email, encryptionSecret, uuid, tu)
62+
if err != nil {
63+
logStore.AddLog("ERROR", fmt.Sprintf("[Bulk Delete] Failed: %s (%v)", tu, err), uuid, "Bulk Delete Task")
64+
continue
65+
}
66+
67+
logStore.AddLog("INFO", fmt.Sprintf("[Bulk Delete] Deleted: %s", tu), uuid, "Bulk Delete Task")
68+
}
69+
return nil
70+
},
71+
}
72+
73+
GlobalJobQueue.AddJob(job)
74+
w.WriteHeader(http.StatusAccepted)
75+
}

backend/main.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,8 @@ func main() {
9292
mux.Handle("/complete-task", rateLimitedHandler(http.HandlerFunc(controllers.CompleteTaskHandler)))
9393
mux.Handle("/delete-task", rateLimitedHandler(http.HandlerFunc(controllers.DeleteTaskHandler)))
9494
mux.Handle("/sync/logs", rateLimitedHandler(http.HandlerFunc(controllers.SyncLogsHandler)))
95+
mux.Handle("/complete-tasks", rateLimitedHandler(http.HandlerFunc(controllers.BulkCompleteTaskHandler)))
96+
mux.Handle("/delete-tasks", rateLimitedHandler(http.HandlerFunc(controllers.BulkDeleteTaskHandler)))
9597

9698
mux.HandleFunc("/ws", controllers.WebSocketHandler)
9799

backend/models/request_body.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,15 @@ type DeleteTaskRequestBody struct {
4949
UUID string `json:"UUID"`
5050
TaskUUID string `json:"taskuuid"`
5151
}
52+
type BulkCompleteTaskRequestBody struct {
53+
Email string `json:"email"`
54+
EncryptionSecret string `json:"encryptionSecret"`
55+
UUID string `json:"UUID"`
56+
TaskUUIDs []string `json:"taskuuids"`
57+
}
58+
type BulkDeleteTaskRequestBody struct {
59+
Email string `json:"email"`
60+
EncryptionSecret string `json:"encryptionSecret"`
61+
UUID string `json:"UUID"`
62+
TaskUUIDs []string `json:"taskuuids"`
63+
}

0 commit comments

Comments
 (0)