kanboard-api/task_scope.go

303 lines
8.8 KiB
Go
Raw Normal View History

package kanboard
import "context"
// TaskScope provides fluent task-scoped operations.
type TaskScope struct {
client *Client
taskID int
}
// Task returns a TaskScope for fluent task-scoped operations.
func (c *Client) Task(taskID int) *TaskScope {
return &TaskScope{
client: c,
taskID: taskID,
}
}
// Get returns the task.
func (t *TaskScope) Get(ctx context.Context) (*Task, error) {
return t.client.GetTask(ctx, t.taskID)
}
// Close closes the task (sets it to inactive).
func (t *TaskScope) Close(ctx context.Context) error {
return t.client.CloseTask(ctx, t.taskID)
}
// Open opens the task (sets it to active).
func (t *TaskScope) Open(ctx context.Context) error {
return t.client.OpenTask(ctx, t.taskID)
}
// MoveToColumn moves the task to a different column.
// The task is placed at the end of the column (position=0).
// Requires the project ID to be fetched from the task.
func (t *TaskScope) MoveToColumn(ctx context.Context, columnID int) error {
task, err := t.Get(ctx)
if err != nil {
return err
}
return t.client.MoveTaskPosition(ctx, int(task.ProjectID), t.taskID, columnID, 0, int(task.SwimlaneID))
}
// MoveToNextColumn moves the task to the next column in the workflow.
// Columns are ordered by their position field.
// Returns ErrAlreadyInLastColumn if the task is already in the last column.
func (t *TaskScope) MoveToNextColumn(ctx context.Context) error {
// Get task to find current column and project
task, err := t.Get(ctx)
if err != nil {
return err
}
// Get all columns for the project (already sorted by position)
columns, err := t.client.GetColumns(ctx, int(task.ProjectID))
if err != nil {
return err
}
if len(columns) == 0 {
return ErrAlreadyInLastColumn
}
// Find the current column index
currentColumnID := int(task.ColumnID)
currentIndex := -1
for i, col := range columns {
if int(col.ID) == currentColumnID {
currentIndex = i
break
}
}
// If current column not found or is the last column
if currentIndex == -1 || currentIndex >= len(columns)-1 {
return ErrAlreadyInLastColumn
}
// Move to the next column
nextColumn := columns[currentIndex+1]
return t.client.MoveTaskPosition(ctx, int(task.ProjectID), t.taskID, int(nextColumn.ID), 0, int(task.SwimlaneID))
}
// MoveToPreviousColumn moves the task to the previous column in the workflow.
// Columns are ordered by their position field.
// Returns ErrAlreadyInFirstColumn if the task is already in the first column.
func (t *TaskScope) MoveToPreviousColumn(ctx context.Context) error {
// Get task to find current column and project
task, err := t.Get(ctx)
if err != nil {
return err
}
// Get all columns for the project (already sorted by position)
columns, err := t.client.GetColumns(ctx, int(task.ProjectID))
if err != nil {
return err
}
if len(columns) == 0 {
return ErrAlreadyInFirstColumn
}
// Find the current column index
currentColumnID := int(task.ColumnID)
currentIndex := -1
for i, col := range columns {
if int(col.ID) == currentColumnID {
currentIndex = i
break
}
}
// If current column not found or is the first column
if currentIndex <= 0 {
return ErrAlreadyInFirstColumn
}
// Move to the previous column
prevColumn := columns[currentIndex-1]
return t.client.MoveTaskPosition(ctx, int(task.ProjectID), t.taskID, int(prevColumn.ID), 0, int(task.SwimlaneID))
}
// MoveToProject moves the task to a different project.
func (t *TaskScope) MoveToProject(ctx context.Context, projectID int) error {
return t.client.MoveTaskToProject(ctx, t.taskID, projectID)
}
// Update updates the task using TaskUpdateParams.
func (t *TaskScope) Update(ctx context.Context, params *TaskUpdateParams) error {
return t.client.UpdateTaskFromParams(ctx, t.taskID, params)
}
// GetTags returns the tags assigned to this task as a map of tagID to tag name.
func (t *TaskScope) GetTags(ctx context.Context) (map[int]string, error) {
return t.client.GetTaskTags(ctx, t.taskID)
}
// SetTags sets the tags for this task, replacing all existing tags.
// Tags are specified by name. Non-existent tags will be auto-created.
//
// WARNING: This operation is not atomic. Concurrent tag modifications may cause data loss.
func (t *TaskScope) SetTags(ctx context.Context, tags ...string) error {
task, err := t.Get(ctx)
if err != nil {
return err
}
return t.client.SetTaskTags(ctx, int(task.ProjectID), t.taskID, tags)
}
// ClearTags removes all tags from this task.
//
// WARNING: This operation is not atomic. Concurrent tag modifications may cause data loss.
func (t *TaskScope) ClearTags(ctx context.Context) error {
return t.SetTags(ctx)
}
// AddTag adds a tag to this task using a read-modify-write pattern.
// If the tag already exists on the task, this is a no-op (idempotent).
//
// WARNING: This operation is not atomic. Concurrent tag modifications may cause data loss.
func (t *TaskScope) AddTag(ctx context.Context, tag string) error {
// Get task info for project_id
task, err := t.Get(ctx)
if err != nil {
return err
}
// Get current tags
currentTags, err := t.client.GetTaskTags(ctx, t.taskID)
if err != nil {
return err
}
// Check if tag already exists (idempotent)
for _, existingTag := range currentTags {
if existingTag == tag {
return nil
}
}
// Build new tag list
tagNames := make([]string, 0, len(currentTags)+1)
for _, name := range currentTags {
tagNames = append(tagNames, name)
}
tagNames = append(tagNames, tag)
// Set updated tags
return t.client.SetTaskTags(ctx, int(task.ProjectID), t.taskID, tagNames)
}
// RemoveTag removes a tag from this task using a read-modify-write pattern.
// If the tag doesn't exist on the task, this is a no-op (idempotent).
//
// WARNING: This operation is not atomic. Concurrent tag modifications may cause data loss.
func (t *TaskScope) RemoveTag(ctx context.Context, tag string) error {
// Get task info for project_id
task, err := t.Get(ctx)
if err != nil {
return err
}
// Get current tags
currentTags, err := t.client.GetTaskTags(ctx, t.taskID)
if err != nil {
return err
}
// Filter out the tag to remove
tagNames := make([]string, 0, len(currentTags))
found := false
for _, name := range currentTags {
if name == tag {
found = true
continue
}
tagNames = append(tagNames, name)
}
// If tag wasn't found, nothing to do (idempotent)
if !found {
return nil
}
// Set updated tags
return t.client.SetTaskTags(ctx, int(task.ProjectID), t.taskID, tagNames)
}
// HasTag checks if this task has a specific tag.
func (t *TaskScope) HasTag(ctx context.Context, tag string) (bool, error) {
tags, err := t.client.GetTaskTags(ctx, t.taskID)
if err != nil {
return false, err
}
for _, name := range tags {
if name == tag {
return true, nil
}
}
return false, nil
}
// GetComments returns all comments for this task.
func (t *TaskScope) GetComments(ctx context.Context) ([]Comment, error) {
return t.client.GetAllComments(ctx, t.taskID)
}
// AddComment adds a comment to this task and returns the created comment.
// The userID is the ID of the user creating the comment.
func (t *TaskScope) AddComment(ctx context.Context, userID int, content string) (*Comment, error) {
return t.client.CreateComment(ctx, t.taskID, userID, content)
}
// GetLinks returns all links for this task.
func (t *TaskScope) GetLinks(ctx context.Context) ([]TaskLink, error) {
return t.client.GetAllTaskLinks(ctx, t.taskID)
}
// LinkTo creates a link from this task to another task.
// The linkID specifies the type of relationship (e.g., "blocks", "is blocked by").
func (t *TaskScope) LinkTo(ctx context.Context, oppositeTaskID, linkID int) error {
_, err := t.client.CreateTaskLink(ctx, t.taskID, oppositeTaskID, linkID)
return err
}
// GetFiles returns all files attached to this task.
func (t *TaskScope) GetFiles(ctx context.Context) ([]TaskFile, error) {
return t.client.GetAllTaskFiles(ctx, t.taskID)
}
// UploadFile uploads a file to this task and returns the file ID.
// The file content is automatically base64 encoded.
func (t *TaskScope) UploadFile(ctx context.Context, filename string, content []byte) (int, error) {
task, err := t.Get(ctx)
if err != nil {
return 0, err
}
return t.client.CreateTaskFile(ctx, int(task.ProjectID), t.taskID, filename, content)
}
// GetFile returns a file's metadata by ID.
func (t *TaskScope) GetFile(ctx context.Context, fileID int) (*TaskFile, error) {
return t.client.GetTaskFile(ctx, fileID)
}
// RemoveFile removes a file by ID.
func (t *TaskScope) RemoveFile(ctx context.Context, fileID int) error {
return t.client.RemoveTaskFile(ctx, fileID)
}
// DownloadFile downloads a file's content by ID.
func (t *TaskScope) DownloadFile(ctx context.Context, fileID int) ([]byte, error) {
return t.client.DownloadTaskFile(ctx, fileID)
}
// RemoveAllFiles removes all files from this task.
func (t *TaskScope) RemoveAllFiles(ctx context.Context) error {
return t.client.RemoveAllTaskFiles(ctx, t.taskID)
}