2026-01-15 18:34:38 +01:00
|
|
|
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))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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)
|
|
|
|
|
}
|
2026-01-15 18:36:40 +01:00
|
|
|
|
|
|
|
|
// 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
|
|
|
|
|
}
|