Implement TaskUpdateParams builder

- TaskUpdateParams struct with fluent setter methods
- NewTaskUpdate() constructor
- Setters: SetTitle, SetDescription, SetColor, SetOwner, SetCategory,
  SetPriority, SetScore, SetDueDate, SetStartDate, SetReference, SetTags
- Clear methods: ClearDueDate, ClearStartDate, ClearOwner, ClearCategory
- Only set fields are included in update request (partial updates)
- Client.UpdateTaskFromParams for fluent task updates
- Comprehensive test coverage
This commit is contained in:
Oliver Jakoubek 2026-01-15 18:33:20 +01:00
commit 0c5b7fd6e9
4 changed files with 451 additions and 1 deletions

View file

@ -4,7 +4,7 @@
{"id":"kanboard-api-2g1","title":"Implement JSON-RPC client foundation","description":"Implement the core JSON-RPC 2.0 client for Kanboard API communication.\n\n## Requirements\n- JSONRPCRequest struct with jsonrpc, method, id, params fields\n- JSONRPCResponse struct with jsonrpc, id, result, error fields \n- JSONRPCError struct with code and message\n- Generic `call` method to send requests and parse responses\n- Automatic `/jsonrpc.php` path appending to baseURL\n- Thread-safe request ID generation via atomic.Int64\n\n## Files to create\n- jsonrpc.go\n\n## Acceptance criteria\n- All JSON-RPC structs properly marshal/unmarshal\n- Request IDs increment atomically\n- Supports subdirectory installations (e.g. /kanboard/jsonrpc.php)","status":"closed","priority":0,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-15T17:34:53.232007312+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-15T18:10:29.68466887+01:00","closed_at":"2026-01-15T18:10:29.68466887+01:00","close_reason":"Closed"} {"id":"kanboard-api-2g1","title":"Implement JSON-RPC client foundation","description":"Implement the core JSON-RPC 2.0 client for Kanboard API communication.\n\n## Requirements\n- JSONRPCRequest struct with jsonrpc, method, id, params fields\n- JSONRPCResponse struct with jsonrpc, id, result, error fields \n- JSONRPCError struct with code and message\n- Generic `call` method to send requests and parse responses\n- Automatic `/jsonrpc.php` path appending to baseURL\n- Thread-safe request ID generation via atomic.Int64\n\n## Files to create\n- jsonrpc.go\n\n## Acceptance criteria\n- All JSON-RPC structs properly marshal/unmarshal\n- Request IDs increment atomically\n- Supports subdirectory installations (e.g. /kanboard/jsonrpc.php)","status":"closed","priority":0,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-15T17:34:53.232007312+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-15T18:10:29.68466887+01:00","closed_at":"2026-01-15T18:10:29.68466887+01:00","close_reason":"Closed"}
{"id":"kanboard-api-2ze","title":"Implement BoardScope fluent builder","description":"Implement BoardScope for fluent project-scoped operations.\n\n## Requirements\n- BoardScope struct with client and projectID\n- Client.Board(projectID int) *BoardScope method\n- BoardScope methods:\n - GetColumns(ctx) ([]Column, error)\n - GetCategories(ctx) ([]Category, error)\n - GetTasks(ctx, status TaskStatus) ([]Task, error)\n - SearchTasks(ctx, query string) ([]Task, error)\n - CreateTask(ctx, task *TaskParams) (*Task, error)\n\n## Files to create\n- board_scope.go\n\n## Example usage\n```go\ncolumns, _ := client.Board(1).GetColumns(ctx)\ntask, _ := client.Board(1).CreateTask(ctx, kanboard.NewTask(\"Title\"))\n```\n\n## Acceptance criteria\n- All methods delegate to direct Client methods\n- Proper error propagation","status":"closed","priority":1,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-15T17:35:40.044649709+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-15T18:29:29.648709021+01:00","closed_at":"2026-01-15T18:29:29.648709021+01:00","close_reason":"Closed","dependencies":[{"issue_id":"kanboard-api-2ze","depends_on_id":"kanboard-api-apl","type":"blocks","created_at":"2026-01-15T17:43:30.81063282+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"kanboard-api-2ze","depends_on_id":"kanboard-api-l9b","type":"blocks","created_at":"2026-01-15T17:43:30.874964284+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"kanboard-api-2ze","depends_on_id":"kanboard-api-0fz","type":"blocks","created_at":"2026-01-15T17:43:30.939377116+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"kanboard-api-2ze","depends_on_id":"kanboard-api-91a","type":"blocks","created_at":"2026-01-15T17:43:31.005026627+01:00","created_by":"Oliver Jakoubek"}]} {"id":"kanboard-api-2ze","title":"Implement BoardScope fluent builder","description":"Implement BoardScope for fluent project-scoped operations.\n\n## Requirements\n- BoardScope struct with client and projectID\n- Client.Board(projectID int) *BoardScope method\n- BoardScope methods:\n - GetColumns(ctx) ([]Column, error)\n - GetCategories(ctx) ([]Category, error)\n - GetTasks(ctx, status TaskStatus) ([]Task, error)\n - SearchTasks(ctx, query string) ([]Task, error)\n - CreateTask(ctx, task *TaskParams) (*Task, error)\n\n## Files to create\n- board_scope.go\n\n## Example usage\n```go\ncolumns, _ := client.Board(1).GetColumns(ctx)\ntask, _ := client.Board(1).CreateTask(ctx, kanboard.NewTask(\"Title\"))\n```\n\n## Acceptance criteria\n- All methods delegate to direct Client methods\n- Proper error propagation","status":"closed","priority":1,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-15T17:35:40.044649709+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-15T18:29:29.648709021+01:00","closed_at":"2026-01-15T18:29:29.648709021+01:00","close_reason":"Closed","dependencies":[{"issue_id":"kanboard-api-2ze","depends_on_id":"kanboard-api-apl","type":"blocks","created_at":"2026-01-15T17:43:30.81063282+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"kanboard-api-2ze","depends_on_id":"kanboard-api-l9b","type":"blocks","created_at":"2026-01-15T17:43:30.874964284+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"kanboard-api-2ze","depends_on_id":"kanboard-api-0fz","type":"blocks","created_at":"2026-01-15T17:43:30.939377116+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"kanboard-api-2ze","depends_on_id":"kanboard-api-91a","type":"blocks","created_at":"2026-01-15T17:43:31.005026627+01:00","created_by":"Oliver Jakoubek"}]}
{"id":"kanboard-api-3dc","title":"Implement Link API methods","description":"Implement direct API methods for task link operations.\n\n## Methods to implement\n- GetAllTaskLinks(ctx, taskID int) ([]TaskLink, error) - getAllTaskLinks\n- CreateTaskLink(ctx, taskID, oppositeTaskID, linkID int) (int, error) - createTaskLink\n- RemoveTaskLink(ctx, taskLinkID int) error - removeTaskLink (Nice-to-have)\n\n## TaskScope methods to add\n- GetLinks(ctx) ([]TaskLink, error)\n- LinkTo(ctx, oppositeTaskID, linkID int) error\n\n## Files to create\n- links.go\n- task_scope.go (extend)\n\n## Acceptance criteria\n- CreateTaskLink returns the link ID\n- Links include related task information","status":"open","priority":2,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-15T17:36:09.328552773+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-15T17:36:09.328552773+01:00","dependencies":[{"issue_id":"kanboard-api-3dc","depends_on_id":"kanboard-api-uls","type":"blocks","created_at":"2026-01-15T17:43:49.785710003+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"kanboard-api-3dc","depends_on_id":"kanboard-api-cyc","type":"blocks","created_at":"2026-01-15T17:43:49.886111429+01:00","created_by":"Oliver Jakoubek"}]} {"id":"kanboard-api-3dc","title":"Implement Link API methods","description":"Implement direct API methods for task link operations.\n\n## Methods to implement\n- GetAllTaskLinks(ctx, taskID int) ([]TaskLink, error) - getAllTaskLinks\n- CreateTaskLink(ctx, taskID, oppositeTaskID, linkID int) (int, error) - createTaskLink\n- RemoveTaskLink(ctx, taskLinkID int) error - removeTaskLink (Nice-to-have)\n\n## TaskScope methods to add\n- GetLinks(ctx) ([]TaskLink, error)\n- LinkTo(ctx, oppositeTaskID, linkID int) error\n\n## Files to create\n- links.go\n- task_scope.go (extend)\n\n## Acceptance criteria\n- CreateTaskLink returns the link ID\n- Links include related task information","status":"open","priority":2,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-15T17:36:09.328552773+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-15T17:36:09.328552773+01:00","dependencies":[{"issue_id":"kanboard-api-3dc","depends_on_id":"kanboard-api-uls","type":"blocks","created_at":"2026-01-15T17:43:49.785710003+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"kanboard-api-3dc","depends_on_id":"kanboard-api-cyc","type":"blocks","created_at":"2026-01-15T17:43:49.886111429+01:00","created_by":"Oliver Jakoubek"}]}
{"id":"kanboard-api-5fb","title":"Implement TaskUpdateParams builder","description":"Implement TaskUpdateParams for fluent task update configuration.\n\n## Requirements\n- TaskUpdateParams struct with pointer fields\n- NewTaskUpdate() *TaskUpdateParams constructor\n- Fluent setter methods:\n - SetTitle(title string) *TaskUpdateParams\n - SetDescription(desc string) *TaskUpdateParams\n - SetColor(colorID string) *TaskUpdateParams\n - SetOwner(ownerID int) *TaskUpdateParams\n - SetCategory(categoryID int) *TaskUpdateParams\n - SetPriority(priority int) *TaskUpdateParams\n - SetDueDate(date time.Time) *TaskUpdateParams\n- Internal method to convert to UpdateTaskRequest\n\n## Files to create\n- task_update_params.go\n\n## Example usage\n```go\nparams := kanboard.NewTaskUpdate().\n SetTitle(\"New Title\").\n SetPriority(2)\n```\n\n## Acceptance criteria\n- Only set fields are included in update request\n- All setters return *TaskUpdateParams for chaining","status":"open","priority":1,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-15T17:35:40.814955926+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-15T17:35:40.814955926+01:00","dependencies":[{"issue_id":"kanboard-api-5fb","depends_on_id":"kanboard-api-cyc","type":"blocks","created_at":"2026-01-15T17:43:31.134402453+01:00","created_by":"Oliver Jakoubek"}]} {"id":"kanboard-api-5fb","title":"Implement TaskUpdateParams builder","description":"Implement TaskUpdateParams for fluent task update configuration.\n\n## Requirements\n- TaskUpdateParams struct with pointer fields\n- NewTaskUpdate() *TaskUpdateParams constructor\n- Fluent setter methods:\n - SetTitle(title string) *TaskUpdateParams\n - SetDescription(desc string) *TaskUpdateParams\n - SetColor(colorID string) *TaskUpdateParams\n - SetOwner(ownerID int) *TaskUpdateParams\n - SetCategory(categoryID int) *TaskUpdateParams\n - SetPriority(priority int) *TaskUpdateParams\n - SetDueDate(date time.Time) *TaskUpdateParams\n- Internal method to convert to UpdateTaskRequest\n\n## Files to create\n- task_update_params.go\n\n## Example usage\n```go\nparams := kanboard.NewTaskUpdate().\n SetTitle(\"New Title\").\n SetPriority(2)\n```\n\n## Acceptance criteria\n- Only set fields are included in update request\n- All setters return *TaskUpdateParams for chaining","status":"closed","priority":1,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-15T17:35:40.814955926+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-15T18:33:14.78349244+01:00","closed_at":"2026-01-15T18:33:14.78349244+01:00","close_reason":"Closed","dependencies":[{"issue_id":"kanboard-api-5fb","depends_on_id":"kanboard-api-cyc","type":"blocks","created_at":"2026-01-15T17:43:31.134402453+01:00","created_by":"Oliver Jakoubek"}]}
{"id":"kanboard-api-70e","title":"Add GoDoc documentation to all public APIs","description":"Add comprehensive GoDoc comments to all exported types and functions.\n\n## Requirements\n- Package-level documentation in doc.go\n- All exported types documented\n- All exported functions documented with examples\n- All exported constants/variables documented\n\n## Documentation standards\n- Start with the name being documented\n- Include usage examples where helpful\n- Document error conditions\n- Note thread-safety characteristics\n\n## Files to create/modify\n- doc.go (package documentation)\n- All .go files with exported symbols\n\n## Acceptance criteria\n- `go doc` produces clean output\n- All exports have documentation\n- Examples are runnable","status":"open","priority":2,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-15T17:36:54.342657373+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-15T17:36:54.342657373+01:00","dependencies":[{"issue_id":"kanboard-api-70e","depends_on_id":"kanboard-api-uls","type":"blocks","created_at":"2026-01-15T17:46:55.508211397+01:00","created_by":"Oliver Jakoubek"}]} {"id":"kanboard-api-70e","title":"Add GoDoc documentation to all public APIs","description":"Add comprehensive GoDoc comments to all exported types and functions.\n\n## Requirements\n- Package-level documentation in doc.go\n- All exported types documented\n- All exported functions documented with examples\n- All exported constants/variables documented\n\n## Documentation standards\n- Start with the name being documented\n- Include usage examples where helpful\n- Document error conditions\n- Note thread-safety characteristics\n\n## Files to create/modify\n- doc.go (package documentation)\n- All .go files with exported symbols\n\n## Acceptance criteria\n- `go doc` produces clean output\n- All exports have documentation\n- Examples are runnable","status":"open","priority":2,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-15T17:36:54.342657373+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-15T17:36:54.342657373+01:00","dependencies":[{"issue_id":"kanboard-api-70e","depends_on_id":"kanboard-api-uls","type":"blocks","created_at":"2026-01-15T17:46:55.508211397+01:00","created_by":"Oliver Jakoubek"}]}
{"id":"kanboard-api-91a","title":"Implement Task API methods (CRUD)","description":"Implement direct API methods for core task operations.\n\n## Methods to implement\n- GetTask(ctx, taskID int) (*Task, error) - getTask\n- GetAllTasks(ctx, projectID int, status TaskStatus) ([]Task, error) - getAllTasks\n- CreateTask(ctx, req CreateTaskRequest) (*Task, error) - createTask\n- UpdateTask(ctx, req UpdateTaskRequest) error - updateTask\n- CloseTask(ctx, taskID int) error - closeTask\n- OpenTask(ctx, taskID int) error - openTask\n\n## Files to create\n- tasks.go\n\n## Acceptance criteria\n- CreateTask returns the created task\n- UpdateTask supports partial updates via pointer fields\n- Proper error types (ErrTaskNotFound, ErrTaskClosed, etc.)","status":"closed","priority":1,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-15T17:35:16.9962543+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-15T18:26:49.591860359+01:00","closed_at":"2026-01-15T18:26:49.591860359+01:00","close_reason":"Closed","dependencies":[{"issue_id":"kanboard-api-91a","depends_on_id":"kanboard-api-uls","type":"blocks","created_at":"2026-01-15T17:42:53.291442735+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"kanboard-api-91a","depends_on_id":"kanboard-api-cyc","type":"blocks","created_at":"2026-01-15T17:42:53.434459365+01:00","created_by":"Oliver Jakoubek"}]} {"id":"kanboard-api-91a","title":"Implement Task API methods (CRUD)","description":"Implement direct API methods for core task operations.\n\n## Methods to implement\n- GetTask(ctx, taskID int) (*Task, error) - getTask\n- GetAllTasks(ctx, projectID int, status TaskStatus) ([]Task, error) - getAllTasks\n- CreateTask(ctx, req CreateTaskRequest) (*Task, error) - createTask\n- UpdateTask(ctx, req UpdateTaskRequest) error - updateTask\n- CloseTask(ctx, taskID int) error - closeTask\n- OpenTask(ctx, taskID int) error - openTask\n\n## Files to create\n- tasks.go\n\n## Acceptance criteria\n- CreateTask returns the created task\n- UpdateTask supports partial updates via pointer fields\n- Proper error types (ErrTaskNotFound, ErrTaskClosed, etc.)","status":"closed","priority":1,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-15T17:35:16.9962543+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-15T18:26:49.591860359+01:00","closed_at":"2026-01-15T18:26:49.591860359+01:00","close_reason":"Closed","dependencies":[{"issue_id":"kanboard-api-91a","depends_on_id":"kanboard-api-uls","type":"blocks","created_at":"2026-01-15T17:42:53.291442735+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"kanboard-api-91a","depends_on_id":"kanboard-api-cyc","type":"blocks","created_at":"2026-01-15T17:42:53.434459365+01:00","created_by":"Oliver Jakoubek"}]}
{"id":"kanboard-api-9k8","title":"Implement TaskParams builder (Options Pattern)","description":"Implement TaskParams for fluent task creation configuration.\n\n## Requirements\n- TaskParams struct with private fields\n- NewTask(title string) *TaskParams constructor\n- Fluent setter methods:\n - WithDescription(desc string) *TaskParams\n - InColumn(columnID int) *TaskParams\n - WithCategory(categoryID int) *TaskParams\n - WithOwner(ownerID int) *TaskParams\n - WithColor(colorID string) *TaskParams\n - WithPriority(priority int) *TaskParams\n - WithDueDate(date time.Time) *TaskParams\n - WithTags(tags ...string) *TaskParams\n - WithReference(ref string) *TaskParams\n- Internal method to convert to CreateTaskRequest\n\n## Files to create\n- task_params.go\n\n## Example usage\n```go\nparams := kanboard.NewTask(\"My Task\").\n WithDescription(\"Details\").\n InColumn(2).\n WithTags(\"urgent\", \"backend\")\n```\n\n## Acceptance criteria\n- Pure configuration object (no I/O)\n- All setters return *TaskParams for chaining\n- Unset optional fields remain nil","status":"closed","priority":1,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-15T17:35:40.439513879+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-15T18:31:07.365703704+01:00","closed_at":"2026-01-15T18:31:07.365703704+01:00","close_reason":"Closed","dependencies":[{"issue_id":"kanboard-api-9k8","depends_on_id":"kanboard-api-cyc","type":"blocks","created_at":"2026-01-15T17:43:31.070100884+01:00","created_by":"Oliver Jakoubek"}]} {"id":"kanboard-api-9k8","title":"Implement TaskParams builder (Options Pattern)","description":"Implement TaskParams for fluent task creation configuration.\n\n## Requirements\n- TaskParams struct with private fields\n- NewTask(title string) *TaskParams constructor\n- Fluent setter methods:\n - WithDescription(desc string) *TaskParams\n - InColumn(columnID int) *TaskParams\n - WithCategory(categoryID int) *TaskParams\n - WithOwner(ownerID int) *TaskParams\n - WithColor(colorID string) *TaskParams\n - WithPriority(priority int) *TaskParams\n - WithDueDate(date time.Time) *TaskParams\n - WithTags(tags ...string) *TaskParams\n - WithReference(ref string) *TaskParams\n- Internal method to convert to CreateTaskRequest\n\n## Files to create\n- task_params.go\n\n## Example usage\n```go\nparams := kanboard.NewTask(\"My Task\").\n WithDescription(\"Details\").\n InColumn(2).\n WithTags(\"urgent\", \"backend\")\n```\n\n## Acceptance criteria\n- Pure configuration object (no I/O)\n- All setters return *TaskParams for chaining\n- Unset optional fields remain nil","status":"closed","priority":1,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-15T17:35:40.439513879+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-15T18:31:07.365703704+01:00","closed_at":"2026-01-15T18:31:07.365703704+01:00","close_reason":"Closed","dependencies":[{"issue_id":"kanboard-api-9k8","depends_on_id":"kanboard-api-cyc","type":"blocks","created_at":"2026-01-15T17:43:31.070100884+01:00","created_by":"Oliver Jakoubek"}]}

172
task_update_params.go Normal file
View file

@ -0,0 +1,172 @@
package kanboard
import "time"
// TaskUpdateParams is a fluent builder for task update configuration.
// Only set fields are included in the update request.
type TaskUpdateParams struct {
title *string
description *string
colorID *string
ownerID *int
categoryID *int
priority *int
score *int
dueDate *int64
startDate *int64
reference *string
tags []string
tagsSet bool // tracks whether tags were explicitly set (even to empty)
}
// NewTaskUpdate creates a new TaskUpdateParams.
func NewTaskUpdate() *TaskUpdateParams {
return &TaskUpdateParams{}
}
// SetTitle sets the task title.
func (p *TaskUpdateParams) SetTitle(title string) *TaskUpdateParams {
p.title = &title
return p
}
// SetDescription sets the task description.
func (p *TaskUpdateParams) SetDescription(desc string) *TaskUpdateParams {
p.description = &desc
return p
}
// SetColor sets the color ID for the task.
func (p *TaskUpdateParams) SetColor(colorID string) *TaskUpdateParams {
p.colorID = &colorID
return p
}
// SetOwner sets the owner (assignee) ID for the task.
func (p *TaskUpdateParams) SetOwner(ownerID int) *TaskUpdateParams {
p.ownerID = &ownerID
return p
}
// SetCategory sets the category ID for the task.
func (p *TaskUpdateParams) SetCategory(categoryID int) *TaskUpdateParams {
p.categoryID = &categoryID
return p
}
// SetPriority sets the priority for the task.
func (p *TaskUpdateParams) SetPriority(priority int) *TaskUpdateParams {
p.priority = &priority
return p
}
// SetScore sets the complexity score for the task.
func (p *TaskUpdateParams) SetScore(score int) *TaskUpdateParams {
p.score = &score
return p
}
// SetDueDate sets the due date for the task.
func (p *TaskUpdateParams) SetDueDate(date time.Time) *TaskUpdateParams {
ts := date.Unix()
p.dueDate = &ts
return p
}
// SetStartDate sets the start date for the task.
func (p *TaskUpdateParams) SetStartDate(date time.Time) *TaskUpdateParams {
ts := date.Unix()
p.startDate = &ts
return p
}
// SetReference sets the external reference for the task.
func (p *TaskUpdateParams) SetReference(ref string) *TaskUpdateParams {
p.reference = &ref
return p
}
// SetTags sets the tags for the task.
// This replaces all existing tags on the task.
// Call with no arguments to clear all tags.
func (p *TaskUpdateParams) SetTags(tags ...string) *TaskUpdateParams {
if tags == nil {
p.tags = []string{}
} else {
p.tags = tags
}
p.tagsSet = true
return p
}
// ClearDueDate clears the due date from the task.
func (p *TaskUpdateParams) ClearDueDate() *TaskUpdateParams {
zero := int64(0)
p.dueDate = &zero
return p
}
// ClearStartDate clears the start date from the task.
func (p *TaskUpdateParams) ClearStartDate() *TaskUpdateParams {
zero := int64(0)
p.startDate = &zero
return p
}
// ClearOwner removes the owner from the task.
func (p *TaskUpdateParams) ClearOwner() *TaskUpdateParams {
zero := 0
p.ownerID = &zero
return p
}
// ClearCategory removes the category from the task.
func (p *TaskUpdateParams) ClearCategory() *TaskUpdateParams {
zero := 0
p.categoryID = &zero
return p
}
// toUpdateTaskRequest converts TaskUpdateParams to an UpdateTaskRequest.
// The taskID is required and must be provided by the caller.
func (p *TaskUpdateParams) toUpdateTaskRequest(taskID int) UpdateTaskRequest {
req := UpdateTaskRequest{
ID: taskID,
}
if p.title != nil {
req.Title = p.title
}
if p.description != nil {
req.Description = p.description
}
if p.colorID != nil {
req.ColorID = p.colorID
}
if p.ownerID != nil {
req.OwnerID = p.ownerID
}
if p.categoryID != nil {
req.CategoryID = p.categoryID
}
if p.priority != nil {
req.Priority = p.priority
}
if p.score != nil {
req.Score = p.score
}
if p.dueDate != nil {
req.DateDue = p.dueDate
}
if p.startDate != nil {
req.DateStarted = p.startDate
}
if p.reference != nil {
req.Reference = p.reference
}
if p.tagsSet {
req.Tags = p.tags
}
return req
}

271
task_update_params_test.go Normal file
View file

@ -0,0 +1,271 @@
package kanboard
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"time"
)
func TestNewTaskUpdate(t *testing.T) {
params := NewTaskUpdate()
if params == nil {
t.Fatal("expected non-nil TaskUpdateParams")
}
if params.title != nil {
t.Error("expected title to be nil")
}
}
func TestTaskUpdateParams_Chaining(t *testing.T) {
dueDate := time.Date(2025, 12, 31, 0, 0, 0, 0, time.UTC)
params := NewTaskUpdate().
SetTitle("Updated Title").
SetDescription("New description").
SetColor("red").
SetOwner(10).
SetCategory(5).
SetPriority(2).
SetDueDate(dueDate).
SetReference("JIRA-456").
SetTags("urgent", "review")
if *params.title != "Updated Title" {
t.Errorf("expected title='Updated Title', got %s", *params.title)
}
if *params.description != "New description" {
t.Errorf("expected description='New description', got %s", *params.description)
}
if *params.colorID != "red" {
t.Errorf("expected colorID='red', got %s", *params.colorID)
}
if *params.ownerID != 10 {
t.Errorf("expected ownerID=10, got %d", *params.ownerID)
}
if *params.categoryID != 5 {
t.Errorf("expected categoryID=5, got %d", *params.categoryID)
}
if *params.priority != 2 {
t.Errorf("expected priority=2, got %d", *params.priority)
}
if *params.dueDate != dueDate.Unix() {
t.Errorf("expected dueDate=%d, got %d", dueDate.Unix(), *params.dueDate)
}
if *params.reference != "JIRA-456" {
t.Errorf("expected reference='JIRA-456', got %s", *params.reference)
}
if len(params.tags) != 2 || params.tags[0] != "urgent" {
t.Errorf("expected tags=['urgent', 'review'], got %v", params.tags)
}
}
func TestTaskUpdateParams_SetScore(t *testing.T) {
params := NewTaskUpdate().SetScore(5)
if *params.score != 5 {
t.Errorf("expected score=5, got %d", *params.score)
}
}
func TestTaskUpdateParams_SetStartDate(t *testing.T) {
startDate := time.Date(2025, 6, 15, 0, 0, 0, 0, time.UTC)
params := NewTaskUpdate().SetStartDate(startDate)
if *params.startDate != startDate.Unix() {
t.Errorf("expected startDate=%d, got %d", startDate.Unix(), *params.startDate)
}
}
func TestTaskUpdateParams_ClearDueDate(t *testing.T) {
params := NewTaskUpdate().ClearDueDate()
if params.dueDate == nil {
t.Fatal("expected dueDate to be set")
}
if *params.dueDate != 0 {
t.Errorf("expected dueDate=0, got %d", *params.dueDate)
}
}
func TestTaskUpdateParams_ClearStartDate(t *testing.T) {
params := NewTaskUpdate().ClearStartDate()
if *params.startDate != 0 {
t.Errorf("expected startDate=0, got %d", *params.startDate)
}
}
func TestTaskUpdateParams_ClearOwner(t *testing.T) {
params := NewTaskUpdate().ClearOwner()
if *params.ownerID != 0 {
t.Errorf("expected ownerID=0, got %d", *params.ownerID)
}
}
func TestTaskUpdateParams_ClearCategory(t *testing.T) {
params := NewTaskUpdate().ClearCategory()
if *params.categoryID != 0 {
t.Errorf("expected categoryID=0, got %d", *params.categoryID)
}
}
func TestTaskUpdateParams_toUpdateTaskRequest(t *testing.T) {
dueDate := time.Date(2025, 12, 31, 0, 0, 0, 0, time.UTC)
params := NewTaskUpdate().
SetTitle("Updated Title").
SetDescription("Details").
SetPriority(2).
SetDueDate(dueDate).
SetTags("urgent")
req := params.toUpdateTaskRequest(42)
if req.ID != 42 {
t.Errorf("expected ID=42, got %d", req.ID)
}
if *req.Title != "Updated Title" {
t.Errorf("expected Title='Updated Title', got %s", *req.Title)
}
if *req.Description != "Details" {
t.Errorf("expected Description='Details', got %s", *req.Description)
}
if *req.Priority != 2 {
t.Errorf("expected Priority=2, got %d", *req.Priority)
}
if *req.DateDue != dueDate.Unix() {
t.Errorf("expected DateDue=%d, got %d", dueDate.Unix(), *req.DateDue)
}
if len(req.Tags) != 1 || req.Tags[0] != "urgent" {
t.Errorf("expected Tags=['urgent'], got %v", req.Tags)
}
}
func TestTaskUpdateParams_toUpdateTaskRequest_PartialUpdate(t *testing.T) {
// Only set title, nothing else
params := NewTaskUpdate().SetTitle("New Title")
req := params.toUpdateTaskRequest(42)
if *req.Title != "New Title" {
t.Errorf("expected Title='New Title', got %s", *req.Title)
}
// Unset fields should be nil
if req.Description != nil {
t.Error("expected Description to be nil")
}
if req.Priority != nil {
t.Error("expected Priority to be nil")
}
if req.OwnerID != nil {
t.Error("expected OwnerID to be nil")
}
if req.Tags != nil {
t.Error("expected Tags to be nil")
}
}
func TestTaskUpdateParams_SetTags_Empty(t *testing.T) {
// Explicitly clearing tags
params := NewTaskUpdate().SetTags()
if !params.tagsSet {
t.Error("expected tagsSet to be true")
}
if len(params.tags) != 0 {
t.Errorf("expected empty tags, got %v", params.tags)
}
req := params.toUpdateTaskRequest(42)
if req.Tags == nil {
t.Error("expected Tags to be non-nil (empty slice)")
}
if len(req.Tags) != 0 {
t.Errorf("expected 0 tags, got %d", len(req.Tags))
}
}
func TestClient_UpdateTaskFromParams(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var req JSONRPCRequest
json.NewDecoder(r.Body).Decode(&req)
if req.Method != "updateTask" {
t.Errorf("expected method=updateTask, got %s", req.Method)
}
params := req.Params.(map[string]any)
if params["id"].(float64) != 42 {
t.Errorf("expected id=42, got %v", params["id"])
}
if params["title"] != "Updated via Params" {
t.Errorf("expected title='Updated via Params', got %v", params["title"])
}
if params["priority"].(float64) != 3 {
t.Errorf("expected priority=3, got %v", params["priority"])
}
resp := JSONRPCResponse{
JSONRPC: "2.0",
ID: req.ID,
Result: json.RawMessage(`true`),
}
json.NewEncoder(w).Encode(resp)
}))
defer server.Close()
client := NewClient(server.URL).WithAPIToken("test-token")
params := NewTaskUpdate().
SetTitle("Updated via Params").
SetPriority(3)
err := client.UpdateTaskFromParams(context.Background(), 42, params)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
}
func TestClient_UpdateTaskFromParams_OnlySetFieldsSent(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var req JSONRPCRequest
json.NewDecoder(r.Body).Decode(&req)
params := req.Params.(map[string]any)
// Only title should be present, not priority or other fields
if _, hasTitle := params["title"]; !hasTitle {
t.Error("expected title to be present")
}
if _, hasPriority := params["priority"]; hasPriority {
t.Error("expected priority to NOT be present")
}
if _, hasOwner := params["owner_id"]; hasOwner {
t.Error("expected owner_id to NOT be present")
}
resp := JSONRPCResponse{
JSONRPC: "2.0",
ID: req.ID,
Result: json.RawMessage(`true`),
}
json.NewEncoder(w).Encode(resp)
}))
defer server.Close()
client := NewClient(server.URL).WithAPIToken("test-token")
// Only set title
params := NewTaskUpdate().SetTitle("Only Title")
err := client.UpdateTaskFromParams(context.Background(), 42, params)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
}

View file

@ -67,6 +67,13 @@ func (c *Client) UpdateTask(ctx context.Context, req UpdateTaskRequest) error {
return nil return nil
} }
// UpdateTaskFromParams updates an existing task using TaskUpdateParams.
// This provides a fluent interface for task updates.
func (c *Client) UpdateTaskFromParams(ctx context.Context, taskID int, params *TaskUpdateParams) error {
req := params.toUpdateTaskRequest(taskID)
return c.UpdateTask(ctx, req)
}
// CloseTask closes a task (sets it to inactive). // CloseTask closes a task (sets it to inactive).
// Returns ErrTaskClosed if the task is already closed. // Returns ErrTaskClosed if the task is already closed.
func (c *Client) CloseTask(ctx context.Context, taskID int) error { func (c *Client) CloseTask(ctx context.Context, taskID int) error {