diff --git a/backend/cmd/server/main.go b/backend/cmd/server/main.go index 2298dea..e79a539 100644 --- a/backend/cmd/server/main.go +++ b/backend/cmd/server/main.go @@ -288,12 +288,12 @@ func main() { api.HandleFunc("/spaces/{spaceId}/tasks/{taskId}/notes/{noteId}", taskHandler.UnlinkTaskNote).Methods("DELETE") api.HandleFunc("/spaces/{spaceId}/notes/{noteId}/tasks", taskHandler.ListTasksByNote).Methods("GET") - // Task status endpoints - api.HandleFunc("/spaces/{spaceId}/task-statuses", taskHandler.ListStatuses).Methods("GET") - api.HandleFunc("/spaces/{spaceId}/task-statuses", taskHandler.CreateStatus).Methods("POST") - api.HandleFunc("/spaces/{spaceId}/task-statuses/reorder", taskHandler.ReorderStatuses).Methods("PUT") - api.HandleFunc("/spaces/{spaceId}/task-statuses/{statusId}", taskHandler.UpdateStatus).Methods("PUT") - api.HandleFunc("/spaces/{spaceId}/task-statuses/{statusId}", taskHandler.DeleteStatus).Methods("DELETE") + // Task status endpoints (scoped to task list) + api.HandleFunc("/spaces/{spaceId}/task-lists/{taskListId}/statuses", taskHandler.ListStatuses).Methods("GET") + api.HandleFunc("/spaces/{spaceId}/task-lists/{taskListId}/statuses", taskHandler.CreateStatus).Methods("POST") + api.HandleFunc("/spaces/{spaceId}/task-lists/{taskListId}/statuses/reorder", taskHandler.ReorderStatuses).Methods("PUT") + api.HandleFunc("/spaces/{spaceId}/task-lists/{taskListId}/statuses/{statusId}", taskHandler.UpdateStatus).Methods("PUT") + api.HandleFunc("/spaces/{spaceId}/task-lists/{taskListId}/statuses/{statusId}", taskHandler.DeleteStatus).Methods("DELETE") // File explorer endpoints (space-scoped) api.HandleFunc("/spaces/{spaceId}/files/list", fileHandler.ListFiles).Methods("GET") diff --git a/backend/internal/application/dto/dto.go b/backend/internal/application/dto/dto.go index 46afb45..ea30db8 100644 --- a/backend/internal/application/dto/dto.go +++ b/backend/internal/application/dto/dto.go @@ -530,13 +530,13 @@ type ReorderTaskStatusesRequest struct { // TaskStatusDTO represents a task status in API responses. type TaskStatusDTO struct { - ID string `json:"id"` - SpaceID string `json:"space_id"` - Name string `json:"name"` - Color string `json:"color,omitempty"` - Order int `json:"order"` - CreatedAt string `json:"created_at"` - UpdatedAt string `json:"updated_at"` + ID string `json:"id"` + TaskListID string `json:"task_list_id"` + Name string `json:"name"` + Color string `json:"color,omitempty"` + Order int `json:"order"` + CreatedAt string `json:"created_at"` + UpdatedAt string `json:"updated_at"` } // CreateTaskListRequest represents task list creation input. @@ -620,13 +620,13 @@ func NewTaskListDTO(taskList *entities.TaskList) *TaskListDTO { // NewTaskStatusDTO creates a DTO from a task status entity. func NewTaskStatusDTO(status *entities.TaskStatus) *TaskStatusDTO { return &TaskStatusDTO{ - ID: status.ID.Hex(), - SpaceID: status.SpaceID.Hex(), - Name: status.Name, - Color: status.Color, - Order: status.Order, - CreatedAt: status.CreatedAt.Format("2006-01-02T15:04:05Z"), - UpdatedAt: status.UpdatedAt.Format("2006-01-02T15:04:05Z"), + ID: status.ID.Hex(), + TaskListID: status.TaskListID.Hex(), + Name: status.Name, + Color: status.Color, + Order: status.Order, + CreatedAt: status.CreatedAt.Format("2006-01-02T15:04:05Z"), + UpdatedAt: status.UpdatedAt.Format("2006-01-02T15:04:05Z"), } } diff --git a/backend/internal/application/services/task_service.go b/backend/internal/application/services/task_service.go index 19b0f13..af7ee3b 100644 --- a/backend/internal/application/services/task_service.go +++ b/backend/internal/application/services/task_service.go @@ -46,8 +46,8 @@ func NewTaskService( } } -func (s *TaskService) ensureDefaultStatuses(ctx context.Context, spaceID bson.ObjectID) error { - statuses, err := s.taskStatusRepo.ListStatuses(ctx, spaceID) +func (s *TaskService) ensureDefaultStatuses(ctx context.Context, taskListID bson.ObjectID) error { + statuses, err := s.taskStatusRepo.ListStatuses(ctx, taskListID) if err != nil { return err } @@ -66,10 +66,10 @@ func (s *TaskService) ensureDefaultStatuses(ctx context.Context, spaceID bson.Ob for idx, status := range defaults { if err := s.taskStatusRepo.CreateStatus(ctx, &entities.TaskStatus{ - SpaceID: spaceID, - Name: status.name, - Color: status.color, - Order: idx, + TaskListID: taskListID, + Name: status.name, + Color: status.color, + Order: idx, }); err != nil { return err } @@ -142,9 +142,9 @@ func (s *TaskService) validateNoteLinks(ctx context.Context, spaceID bson.Object return nil } -func (s *TaskService) validateStatus(ctx context.Context, spaceID, statusID bson.ObjectID) (*entities.TaskStatus, error) { +func (s *TaskService) validateStatus(ctx context.Context, taskListID, statusID bson.ObjectID) (*entities.TaskStatus, error) { status, err := s.taskStatusRepo.GetStatusByID(ctx, statusID) - if err != nil || status.SpaceID != spaceID { + if err != nil || status.TaskListID != taskListID { return nil, errors.New("invalid task status") } return status, nil @@ -165,11 +165,11 @@ func (s *TaskService) resolveDepthAndParent(ctx context.Context, spaceID bson.Ob return depth, nil } -func (s *TaskService) isAdjacentStatusMove(ctx context.Context, spaceID, currentStatusID, requestedStatusID bson.ObjectID) (bool, error) { +func (s *TaskService) isAdjacentStatusMove(ctx context.Context, taskListID, currentStatusID, requestedStatusID bson.ObjectID) (bool, error) { if currentStatusID == requestedStatusID { return true, nil } - statuses, err := s.taskStatusRepo.ListStatuses(ctx, spaceID) + statuses, err := s.taskStatusRepo.ListStatuses(ctx, taskListID) if err != nil { return false, err } @@ -205,10 +205,6 @@ func (s *TaskService) CreateTask(ctx context.Context, spaceID, userID bson.Objec return nil, errors.New("insufficient permissions") } - if err := s.ensureDefaultStatuses(ctx, spaceID); err != nil { - return nil, err - } - parentTaskID, err := toObjectIDPtr(req.ParentTaskID) if err != nil { return nil, errors.New("invalid parent task") @@ -226,6 +222,10 @@ func (s *TaskService) CreateTask(ctx context.Context, spaceID, userID bson.Objec return nil, err } + if err := s.ensureDefaultStatuses(ctx, taskListID); err != nil { + return nil, err + } + noteLinks, err := toObjectIDs(req.NoteLinks) if err != nil { return nil, err @@ -234,7 +234,7 @@ func (s *TaskService) CreateTask(ctx context.Context, spaceID, userID bson.Objec return nil, err } - statuses, err := s.taskStatusRepo.ListStatuses(ctx, spaceID) + statuses, err := s.taskStatusRepo.ListStatuses(ctx, taskListID) if err != nil { return nil, err } @@ -252,7 +252,7 @@ func (s *TaskService) CreateTask(ctx context.Context, spaceID, userID bson.Objec if parseErr != nil { return nil, errors.New("invalid task status") } - if _, validateErr := s.validateStatus(ctx, spaceID, parsedStatusID); validateErr != nil { + if _, validateErr := s.validateStatus(ctx, taskListID, parsedStatusID); validateErr != nil { return nil, validateErr } statusID = parsedStatusID @@ -299,7 +299,7 @@ func (s *TaskService) GetTaskByID(ctx context.Context, spaceID, taskID, userID b return nil, errors.New("task not found") } - status, err := s.validateStatus(ctx, spaceID, task.StatusID) + status, err := s.validateStatus(ctx, task.TaskListID, task.StatusID) if err != nil { return nil, err } @@ -331,9 +331,6 @@ func (s *TaskService) ListTasks( if err := s.requireSpaceAccess(ctx, userID, spaceID); err != nil { return nil, err } - if err := s.ensureDefaultStatuses(ctx, spaceID); err != nil { - return nil, err - } filters := map[string]any{} if taskListID != nil && strings.TrimSpace(*taskListID) != "" { @@ -403,23 +400,33 @@ func (s *TaskService) ListTasksLinkedToNote(ctx context.Context, spaceID, noteID if _, err := s.noteRepo.GetNoteByID(ctx, noteID); err != nil { return nil, errors.New("note not found") } - statuses, err := s.taskStatusRepo.ListStatuses(ctx, spaceID) - if err != nil { - return nil, err - } - statusByID := map[bson.ObjectID]*entities.TaskStatus{} - for _, status := range statuses { - statusByID[status.ID] = status - } tasks, err := s.taskRepo.ListTasks(ctx, spaceID, map[string]any{"note_links": noteID}) if err != nil { return nil, err } + // Collect statuses per task list + statusCache := map[bson.ObjectID]map[bson.ObjectID]*entities.TaskStatus{} + getStatus := func(taskListID, statusID bson.ObjectID) *entities.TaskStatus { + byID, ok := statusCache[taskListID] + if !ok { + statuses, err := s.taskStatusRepo.ListStatuses(ctx, taskListID) + if err != nil { + return nil + } + byID = make(map[bson.ObjectID]*entities.TaskStatus, len(statuses)) + for _, st := range statuses { + byID[st.ID] = st + } + statusCache[taskListID] = byID + } + return byID[statusID] + } + result := make([]*dto.TaskWithStatusDTO, 0, len(tasks)) for _, task := range tasks { - status := statusByID[task.StatusID] + status := getStatus(task.TaskListID, task.StatusID) if status == nil { continue } @@ -509,10 +516,10 @@ func (s *TaskService) UpdateTask(ctx context.Context, spaceID, taskID, userID bs if parseErr != nil { return nil, errors.New("invalid status") } - if _, err := s.validateStatus(ctx, spaceID, statusID); err != nil { + if _, err := s.validateStatus(ctx, task.TaskListID, statusID); err != nil { return nil, err } - adjacent, err := s.isAdjacentStatusMove(ctx, spaceID, task.StatusID, statusID) + adjacent, err := s.isAdjacentStatusMove(ctx, task.TaskListID, task.StatusID, statusID) if err != nil { return nil, err } @@ -582,7 +589,7 @@ func (s *TaskService) TransitionTaskStatus(ctx context.Context, spaceID, taskID, return nil, errors.New("task not found") } - statuses, err := s.taskStatusRepo.ListStatuses(ctx, spaceID) + statuses, err := s.taskStatusRepo.ListStatuses(ctx, task.TaskListID) if err != nil { return nil, err } @@ -816,17 +823,24 @@ func (s *TaskService) DeleteTaskList(ctx context.Context, spaceID, taskListID, u return err } + if err := s.taskStatusRepo.DeleteStatusesByTaskListID(ctx, taskListID); err != nil { + return err + } + return s.taskListRepo.DeleteTaskList(ctx, taskListID) } -func (s *TaskService) ListStatuses(ctx context.Context, spaceID, userID bson.ObjectID) ([]*dto.TaskStatusDTO, error) { +func (s *TaskService) ListStatuses(ctx context.Context, spaceID, taskListID, userID bson.ObjectID) ([]*dto.TaskStatusDTO, error) { if err := s.requireSpaceAccess(ctx, userID, spaceID); err != nil { return nil, err } - if err := s.ensureDefaultStatuses(ctx, spaceID); err != nil { + if err := s.validateTaskList(ctx, spaceID, taskListID); err != nil { return nil, err } - statuses, err := s.taskStatusRepo.ListStatuses(ctx, spaceID) + if err := s.ensureDefaultStatuses(ctx, taskListID); err != nil { + return nil, err + } + statuses, err := s.taskStatusRepo.ListStatuses(ctx, taskListID) if err != nil { return nil, err } @@ -837,7 +851,7 @@ func (s *TaskService) ListStatuses(ctx context.Context, spaceID, userID bson.Obj return result, nil } -func (s *TaskService) CreateStatus(ctx context.Context, spaceID, userID bson.ObjectID, req *dto.CreateTaskStatusRequest) (*dto.TaskStatusDTO, error) { +func (s *TaskService) CreateStatus(ctx context.Context, spaceID, taskListID, userID bson.ObjectID, req *dto.CreateTaskStatusRequest) (*dto.TaskStatusDTO, error) { if err := s.requireSpaceAccess(ctx, userID, spaceID); err != nil { return nil, err } @@ -848,16 +862,19 @@ func (s *TaskService) CreateStatus(ctx context.Context, spaceID, userID bson.Obj if !hasPermission { return nil, errors.New("insufficient permissions") } + if err := s.validateTaskList(ctx, spaceID, taskListID); err != nil { + return nil, err + } - statuses, err := s.taskStatusRepo.ListStatuses(ctx, spaceID) + statuses, err := s.taskStatusRepo.ListStatuses(ctx, taskListID) if err != nil { return nil, err } status := &entities.TaskStatus{ - SpaceID: spaceID, - Name: strings.TrimSpace(req.Name), - Color: strings.TrimSpace(req.Color), - Order: len(statuses), + TaskListID: taskListID, + Name: strings.TrimSpace(req.Name), + Color: strings.TrimSpace(req.Color), + Order: len(statuses), } if status.Name == "" { return nil, errors.New("status name is required") @@ -868,7 +885,7 @@ func (s *TaskService) CreateStatus(ctx context.Context, spaceID, userID bson.Obj return dto.NewTaskStatusDTO(status), nil } -func (s *TaskService) UpdateStatus(ctx context.Context, spaceID, statusID, userID bson.ObjectID, req *dto.UpdateTaskStatusRequest) (*dto.TaskStatusDTO, error) { +func (s *TaskService) UpdateStatus(ctx context.Context, spaceID, taskListID, statusID, userID bson.ObjectID, req *dto.UpdateTaskStatusRequest) (*dto.TaskStatusDTO, error) { if err := s.requireSpaceAccess(ctx, userID, spaceID); err != nil { return nil, err } @@ -881,7 +898,7 @@ func (s *TaskService) UpdateStatus(ctx context.Context, spaceID, statusID, userI } status, err := s.taskStatusRepo.GetStatusByID(ctx, statusID) - if err != nil || status.SpaceID != spaceID { + if err != nil || status.TaskListID != taskListID { return nil, errors.New("task status not found") } @@ -896,7 +913,7 @@ func (s *TaskService) UpdateStatus(ctx context.Context, spaceID, statusID, userI return dto.NewTaskStatusDTO(status), nil } -func (s *TaskService) DeleteStatus(ctx context.Context, spaceID, statusID, userID bson.ObjectID) error { +func (s *TaskService) DeleteStatus(ctx context.Context, spaceID, taskListID, statusID, userID bson.ObjectID) error { if err := s.requireSpaceAccess(ctx, userID, spaceID); err != nil { return err } @@ -908,7 +925,7 @@ func (s *TaskService) DeleteStatus(ctx context.Context, spaceID, statusID, userI return errors.New("insufficient permissions") } - statuses, err := s.taskStatusRepo.ListStatuses(ctx, spaceID) + statuses, err := s.taskStatusRepo.ListStatuses(ctx, taskListID) if err != nil { return err } @@ -928,10 +945,10 @@ func (s *TaskService) DeleteStatus(ctx context.Context, spaceID, statusID, userI return err } - return s.normalizeStatusOrder(ctx, spaceID) + return s.normalizeStatusOrder(ctx, taskListID) } -func (s *TaskService) ReorderStatuses(ctx context.Context, spaceID, userID bson.ObjectID, orderedStatusIDs []string) ([]*dto.TaskStatusDTO, error) { +func (s *TaskService) ReorderStatuses(ctx context.Context, spaceID, taskListID, userID bson.ObjectID, orderedStatusIDs []string) ([]*dto.TaskStatusDTO, error) { if err := s.requireSpaceAccess(ctx, userID, spaceID); err != nil { return nil, err } @@ -942,8 +959,11 @@ func (s *TaskService) ReorderStatuses(ctx context.Context, spaceID, userID bson. if !hasPermission { return nil, errors.New("insufficient permissions") } + if err := s.validateTaskList(ctx, spaceID, taskListID); err != nil { + return nil, err + } - statuses, err := s.taskStatusRepo.ListStatuses(ctx, spaceID) + statuses, err := s.taskStatusRepo.ListStatuses(ctx, taskListID) if err != nil { return nil, err } @@ -965,7 +985,7 @@ func (s *TaskService) ReorderStatuses(ctx context.Context, spaceID, userID bson. } status := statusByID[statusID] if status == nil { - return nil, errors.New("status id does not belong to this space") + return nil, errors.New("status id does not belong to this task list") } if _, exists := seen[statusID]; exists { return nil, errors.New("duplicate status id in ordered_status_ids") @@ -996,7 +1016,7 @@ func (s *TaskService) ReorderStatuses(ctx context.Context, spaceID, userID bson. } } - updatedStatuses, err := s.taskStatusRepo.ListStatuses(ctx, spaceID) + updatedStatuses, err := s.taskStatusRepo.ListStatuses(ctx, taskListID) if err != nil { return nil, err } @@ -1008,8 +1028,8 @@ func (s *TaskService) ReorderStatuses(ctx context.Context, spaceID, userID bson. return result, nil } -func (s *TaskService) normalizeStatusOrder(ctx context.Context, spaceID bson.ObjectID) error { - statuses, err := s.taskStatusRepo.ListStatuses(ctx, spaceID) +func (s *TaskService) normalizeStatusOrder(ctx context.Context, taskListID bson.ObjectID) error { + statuses, err := s.taskStatusRepo.ListStatuses(ctx, taskListID) if err != nil { return err } diff --git a/backend/internal/domain/entities/task.go b/backend/internal/domain/entities/task.go index 12d68b3..5fcff0e 100644 --- a/backend/internal/domain/entities/task.go +++ b/backend/internal/domain/entities/task.go @@ -25,15 +25,15 @@ type Task struct { UpdatedAt time.Time `bson:"updated_at"` } -// TaskStatus defines the ordered linear status progression for a space. +// TaskStatus defines the ordered linear status progression for a task list. type TaskStatus struct { - ID bson.ObjectID `bson:"_id,omitempty"` - SpaceID bson.ObjectID `bson:"space_id"` - Name string `bson:"name"` - Color string `bson:"color,omitempty"` - Order int `bson:"order"` - CreatedAt time.Time `bson:"created_at"` - UpdatedAt time.Time `bson:"updated_at"` + ID bson.ObjectID `bson:"_id,omitempty"` + TaskListID bson.ObjectID `bson:"task_list_id"` + Name string `bson:"name"` + Color string `bson:"color,omitempty"` + Order int `bson:"order"` + CreatedAt time.Time `bson:"created_at"` + UpdatedAt time.Time `bson:"updated_at"` } // TaskList groups tasks under a named list that can be attached to a category. diff --git a/backend/internal/domain/repositories/interfaces.go b/backend/internal/domain/repositories/interfaces.go index dc2a467..001fb74 100644 --- a/backend/internal/domain/repositories/interfaces.go +++ b/backend/internal/domain/repositories/interfaces.go @@ -245,7 +245,8 @@ type TaskListRepository interface { type TaskStatusRepository interface { CreateStatus(ctx context.Context, status *entities.TaskStatus) error GetStatusByID(ctx context.Context, id bson.ObjectID) (*entities.TaskStatus, error) - ListStatuses(ctx context.Context, spaceID bson.ObjectID) ([]*entities.TaskStatus, error) + ListStatuses(ctx context.Context, taskListID bson.ObjectID) ([]*entities.TaskStatus, error) UpdateStatus(ctx context.Context, status *entities.TaskStatus) error DeleteStatus(ctx context.Context, id bson.ObjectID) error + DeleteStatusesByTaskListID(ctx context.Context, taskListID bson.ObjectID) error } diff --git a/backend/internal/infrastructure/database/task_repository.go b/backend/internal/infrastructure/database/task_repository.go index 199e092..34bd335 100644 --- a/backend/internal/infrastructure/database/task_repository.go +++ b/backend/internal/infrastructure/database/task_repository.go @@ -232,8 +232,8 @@ func (r *TaskStatusRepository) GetStatusByID(ctx context.Context, id bson.Object return &status, nil } -func (r *TaskStatusRepository) ListStatuses(ctx context.Context, spaceID bson.ObjectID) ([]*entities.TaskStatus, error) { - cursor, err := r.collection.Find(ctx, bson.M{"space_id": spaceID}, options.Find().SetSort(bson.D{{Key: "order", Value: 1}})) +func (r *TaskStatusRepository) ListStatuses(ctx context.Context, taskListID bson.ObjectID) ([]*entities.TaskStatus, error) { + cursor, err := r.collection.Find(ctx, bson.M{"task_list_id": taskListID}, options.Find().SetSort(bson.D{{Key: "order", Value: 1}})) if err != nil { return nil, err } @@ -257,14 +257,19 @@ func (r *TaskStatusRepository) DeleteStatus(ctx context.Context, id bson.ObjectI return err } +func (r *TaskStatusRepository) DeleteStatusesByTaskListID(ctx context.Context, taskListID bson.ObjectID) error { + _, err := r.collection.DeleteMany(ctx, bson.M{"task_list_id": taskListID}) + return err +} + func (r *TaskStatusRepository) EnsureIndexes(ctx context.Context) error { _, err := r.collection.Indexes().CreateMany(ctx, []mongo.IndexModel{ { - Keys: bson.D{{Key: "space_id", Value: 1}, {Key: "name", Value: 1}}, + Keys: bson.D{{Key: "task_list_id", Value: 1}, {Key: "name", Value: 1}}, Options: options.Index().SetUnique(true), }, { - Keys: bson.D{{Key: "space_id", Value: 1}, {Key: "order", Value: 1}}, + Keys: bson.D{{Key: "task_list_id", Value: 1}, {Key: "order", Value: 1}}, Options: options.Index().SetUnique(true), }, }) diff --git a/backend/internal/interfaces/handlers/task_handler.go b/backend/internal/interfaces/handlers/task_handler.go index a1d28ca..d83a690 100644 --- a/backend/internal/interfaces/handlers/task_handler.go +++ b/backend/internal/interfaces/handlers/task_handler.go @@ -380,8 +380,13 @@ func (h *TaskHandler) ListStatuses(w http.ResponseWriter, r *http.Request) { http.Error(w, "invalid request", http.StatusBadRequest) return } + taskListID, err := bson.ObjectIDFromHex(mux.Vars(r)["taskListId"]) + if err != nil { + http.Error(w, "invalid task list id", http.StatusBadRequest) + return + } - statuses, err := h.taskService.ListStatuses(r.Context(), spaceID, userID) + statuses, err := h.taskService.ListStatuses(r.Context(), spaceID, taskListID, userID) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return @@ -397,6 +402,11 @@ func (h *TaskHandler) CreateStatus(w http.ResponseWriter, r *http.Request) { http.Error(w, "invalid request", http.StatusBadRequest) return } + taskListID, err := bson.ObjectIDFromHex(mux.Vars(r)["taskListId"]) + if err != nil { + http.Error(w, "invalid task list id", http.StatusBadRequest) + return + } var req dto.CreateTaskStatusRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { @@ -404,7 +414,7 @@ func (h *TaskHandler) CreateStatus(w http.ResponseWriter, r *http.Request) { return } - status, err := h.taskService.CreateStatus(r.Context(), spaceID, userID, &req) + status, err := h.taskService.CreateStatus(r.Context(), spaceID, taskListID, userID, &req) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return @@ -421,6 +431,11 @@ func (h *TaskHandler) UpdateStatus(w http.ResponseWriter, r *http.Request) { http.Error(w, "invalid request", http.StatusBadRequest) return } + taskListID, err := bson.ObjectIDFromHex(mux.Vars(r)["taskListId"]) + if err != nil { + http.Error(w, "invalid task list id", http.StatusBadRequest) + return + } statusID, err := bson.ObjectIDFromHex(mux.Vars(r)["statusId"]) if err != nil { http.Error(w, "invalid status id", http.StatusBadRequest) @@ -433,7 +448,7 @@ func (h *TaskHandler) UpdateStatus(w http.ResponseWriter, r *http.Request) { return } - status, err := h.taskService.UpdateStatus(r.Context(), spaceID, statusID, userID, &req) + status, err := h.taskService.UpdateStatus(r.Context(), spaceID, taskListID, statusID, userID, &req) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return @@ -449,13 +464,18 @@ func (h *TaskHandler) DeleteStatus(w http.ResponseWriter, r *http.Request) { http.Error(w, "invalid request", http.StatusBadRequest) return } + taskListID, err := bson.ObjectIDFromHex(mux.Vars(r)["taskListId"]) + if err != nil { + http.Error(w, "invalid task list id", http.StatusBadRequest) + return + } statusID, err := bson.ObjectIDFromHex(mux.Vars(r)["statusId"]) if err != nil { http.Error(w, "invalid status id", http.StatusBadRequest) return } - if err := h.taskService.DeleteStatus(r.Context(), spaceID, statusID, userID); err != nil { + if err := h.taskService.DeleteStatus(r.Context(), spaceID, taskListID, statusID, userID); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } @@ -468,6 +488,11 @@ func (h *TaskHandler) ReorderStatuses(w http.ResponseWriter, r *http.Request) { http.Error(w, "invalid request", http.StatusBadRequest) return } + taskListID, err := bson.ObjectIDFromHex(mux.Vars(r)["taskListId"]) + if err != nil { + http.Error(w, "invalid task list id", http.StatusBadRequest) + return + } var req dto.ReorderTaskStatusesRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { @@ -475,7 +500,7 @@ func (h *TaskHandler) ReorderStatuses(w http.ResponseWriter, r *http.Request) { return } - statuses, err := h.taskService.ReorderStatuses(r.Context(), spaceID, userID, req.OrderedStatusIDs) + statuses, err := h.taskService.ReorderStatuses(r.Context(), spaceID, taskListID, userID, req.OrderedStatusIDs) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return diff --git a/frontend/src/components/EditTaskListModal.vue b/frontend/src/components/EditTaskListModal.vue new file mode 100644 index 0000000..1df543a --- /dev/null +++ b/frontend/src/components/EditTaskListModal.vue @@ -0,0 +1,317 @@ + + + + + diff --git a/frontend/src/components/TaskBoard.vue b/frontend/src/components/TaskBoard.vue index c7f84dc..9913702 100644 --- a/frontend/src/components/TaskBoard.vue +++ b/frontend/src/components/TaskBoard.vue @@ -5,6 +5,7 @@

Tasks

Track work with ordered statuses.

+
@@ -23,36 +24,6 @@
-
-
- Status Progression - -
-
-
- - - {{ status.name }} -
- -
-
-
-
-
No tasks matched these filters.
@@ -184,65 +155,11 @@
- - - - - - - - - diff --git a/frontend/src/components/app/AppWorkspaceContent.vue b/frontend/src/components/app/AppWorkspaceContent.vue index e565571..4dcb666 100644 --- a/frontend/src/components/app/AppWorkspaceContent.vue +++ b/frontend/src/components/app/AppWorkspaceContent.vue @@ -5,15 +5,10 @@ :tasks="tasks" :statuses="taskStatuses" :selected-task-list="selectedTaskList" - :can-delete-task-list="canDeleteTasks" @select-task="emit('select-task', $event)" @filter-change="emit('filter-change', $event)" - @reorder-status="emit('reorder-status', $event)" - @create-status="emit('create-status', $event)" - @rename-status="emit('rename-status', $event)" - @delete-status="emit('delete-status', $event)" @update-task-status="emit('update-task-status', $event)" - @delete-task-list="emit('delete-task-list', $event)" + @edit-task-list="emit('edit-task-list')" /> + + list.id === taskListId) || null; + await spaceStore.fetchTaskStatuses(currentSpace.value?.id, taskListId); await applyTaskFilters({ taskListId }); return; } @@ -1056,6 +1069,7 @@ const selectTaskList = async (taskList) => { showSidebar.value = false; if (currentSpace.value?.id && taskList?.id) { await router.push(dashboardTaskRoute(currentSpace.value.id, taskList.id)); + await spaceStore.fetchTaskStatuses(currentSpace.value.id, taskList.id); } await applyTaskFilters({ @@ -1253,22 +1267,22 @@ const createSubtask = (parentTask) => { }; const createTaskStatus = async (payload) => { - if (!currentSpace.value?.id) { + if (!currentSpace.value?.id || !selectedTaskList.value?.id) { return; } try { - await spaceStore.createTaskStatus(currentSpace.value.id, payload); + await spaceStore.createTaskStatus(currentSpace.value.id, selectedTaskList.value.id, payload); } catch (error) { alert(error?.response?.data || "Unable to create status."); } }; const renameTaskStatus = async (status) => { - if (!currentSpace.value?.id || !status?.id) { + if (!currentSpace.value?.id || !selectedTaskList.value?.id || !status?.id) { return; } try { - await spaceStore.updateTaskStatus(currentSpace.value.id, status.id, { + await spaceStore.updateTaskStatus(currentSpace.value.id, selectedTaskList.value.id, status.id, { name: status.name, color: status.color, }); @@ -1300,12 +1314,12 @@ const requestDeleteTaskStatus = (status) => { }; const deleteTaskStatus = async (status) => { - if (!currentSpace.value?.id || !status?.id) { + if (!currentSpace.value?.id || !selectedTaskList.value?.id || !status?.id) { return; } try { - await spaceStore.deleteTaskStatus(currentSpace.value.id, status.id); + await spaceStore.deleteTaskStatus(currentSpace.value.id, selectedTaskList.value.id, status.id); } catch (error) { alert(error?.response?.data || "Unable to delete status."); throw error; @@ -1313,11 +1327,11 @@ const deleteTaskStatus = async (status) => { }; const reorderTaskStatuses = async (orderedIds) => { - if (!currentSpace.value?.id) { + if (!currentSpace.value?.id || !selectedTaskList.value?.id) { return; } try { - await spaceStore.reorderTaskStatuses(currentSpace.value.id, orderedIds); + await spaceStore.reorderTaskStatuses(currentSpace.value.id, selectedTaskList.value.id, orderedIds); } catch (error) { alert(error?.response?.data || "Unable to reorder statuses."); } @@ -1390,10 +1404,25 @@ const createTaskList = async (taskListData) => { } }; +const updateTaskListFromModal = async (payload) => { + if (!currentSpace.value?.id || !selectedTaskList.value?.id) return; + try { + await spaceStore.updateTaskList(currentSpace.value.id, selectedTaskList.value.id, { + name: payload.name, + description: payload.description, + category_id: payload.category_id, + }); + selectedTaskList.value = { ...selectedTaskList.value, name: payload.name, category_id: payload.category_id }; + } catch (error) { + alert(error?.response?.data || "Unable to update task list."); + } +}; + const requestRemoveTaskList = (taskList) => { if (!currentSpace.value?.id || !taskList?.id || !canDeleteTasks.value) { return; } + showEditTaskListModal.value = false; taskDeleteIntent.value = { type: "task-list", payload: taskList, diff --git a/frontend/src/stores/spaceStore.js b/frontend/src/stores/spaceStore.js index 6d6b110..c05181d 100644 --- a/frontend/src/stores/spaceStore.js +++ b/frontend/src/stores/spaceStore.js @@ -19,7 +19,7 @@ export const useSpaceStore = defineStore("space", () => { const noteLinkedTasks = ref([]); const refreshSpaceData = async (spaceId) => { - await Promise.all([fetchCategories(spaceId), fetchNotes(spaceId), fetchTaskLists(spaceId), fetchTaskStatuses(spaceId), fetchTasks(spaceId)]); + await Promise.all([fetchCategories(spaceId), fetchNotes(spaceId), fetchTaskLists(spaceId), fetchTasks(spaceId)]); }; const fetchSpaces = async () => { @@ -212,13 +212,13 @@ export const useSpaceStore = defineStore("space", () => { searchResults.value = []; }; - const fetchTaskStatuses = async (spaceId) => { - if (!spaceId) { + const fetchTaskStatuses = async (spaceId, taskListId) => { + if (!spaceId || !taskListId) { taskStatuses.value = []; return []; } try { - const response = await apiClient.get(`/api/v1/spaces/${spaceId}/task-statuses`); + const response = await apiClient.get(`/api/v1/spaces/${spaceId}/task-lists/${taskListId}/statuses`); taskStatuses.value = response.data || []; return taskStatuses.value; } catch (error) { @@ -261,25 +261,25 @@ export const useSpaceStore = defineStore("space", () => { await fetchTaskLists(spaceId); }; - const createTaskStatus = async (spaceId, payload) => { - const response = await apiClient.post(`/api/v1/spaces/${spaceId}/task-statuses`, payload); - await fetchTaskStatuses(spaceId); + const createTaskStatus = async (spaceId, taskListId, payload) => { + const response = await apiClient.post(`/api/v1/spaces/${spaceId}/task-lists/${taskListId}/statuses`, payload); + await fetchTaskStatuses(spaceId, taskListId); return response.data; }; - const updateTaskStatus = async (spaceId, statusId, payload) => { - const response = await apiClient.put(`/api/v1/spaces/${spaceId}/task-statuses/${statusId}`, payload); - await fetchTaskStatuses(spaceId); + const updateTaskStatus = async (spaceId, taskListId, statusId, payload) => { + const response = await apiClient.put(`/api/v1/spaces/${spaceId}/task-lists/${taskListId}/statuses/${statusId}`, payload); + await fetchTaskStatuses(spaceId, taskListId); return response.data; }; - const deleteTaskStatus = async (spaceId, statusId) => { - await apiClient.delete(`/api/v1/spaces/${spaceId}/task-statuses/${statusId}`); - await fetchTaskStatuses(spaceId); + const deleteTaskStatus = async (spaceId, taskListId, statusId) => { + await apiClient.delete(`/api/v1/spaces/${spaceId}/task-lists/${taskListId}/statuses/${statusId}`); + await fetchTaskStatuses(spaceId, taskListId); }; - const reorderTaskStatuses = async (spaceId, orderedStatusIds) => { - const response = await apiClient.put(`/api/v1/spaces/${spaceId}/task-statuses/reorder`, { + const reorderTaskStatuses = async (spaceId, taskListId, orderedStatusIds) => { + const response = await apiClient.put(`/api/v1/spaces/${spaceId}/task-lists/${taskListId}/statuses/reorder`, { ordered_status_ids: orderedStatusIds, }); taskStatuses.value = response.data || [];