Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 23 additions & 1 deletion internal/commands/todos.go
Original file line number Diff line number Diff line change
Expand Up @@ -1164,7 +1164,29 @@ Clear a field by passing its --no- flag or an empty value:
return convertSDKError(err)
}
} else {
req := &basecamp.UpdateTodoRequest{}
// Fetch existing todo so we can preserve fields the user
// didn't change. The BC3 API clears fields by omission,
// so a partial PUT would wipe untouched fields.
existingTodo, err := app.Account().Todos().Get(cmd.Context(), todoID)
if err != nil {
return convertSDKError(err)
}

req := &basecamp.UpdateTodoRequest{
Content: existingTodo.Content,
Description: existingTodo.Description,
DueOn: existingTodo.DueOn,
StartsOn: existingTodo.StartsOn,
}
if len(existingTodo.Assignees) > 0 {
ids := make([]int64, len(existingTodo.Assignees))
for i, a := range existingTodo.Assignees {
ids[i] = a.ID
}
req.AssigneeIDs = ids
}

// Override with user-provided values.
if effectiveTitle != "" {
req.Content = effectiveTitle
}
Expand Down
47 changes: 47 additions & 0 deletions internal/commands/todos_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1810,6 +1810,53 @@ func TestTodosUpdateClearPreservesAssignees(t *testing.T) {
assert.Equal(t, float64(42), ids[0])
}

func TestTodosUpdateDueDatePreservesExistingFields(t *testing.T) {
transport := &mockTodoUpdateTransport{}
app := setupTodoUpdateApp(t, transport)

cmd := NewTodosCmd()
err := executeTodosCommand(cmd, app, "update", "999", "--due", "2026-04-12")
require.NoError(t, err)
require.NotEmpty(t, transport.capturedBody, "expected PUT body but got none")

var body map[string]any
err = json.Unmarshal(transport.capturedBody, &body)
require.NoError(t, err)

// The new due date is applied.
assert.Equal(t, "2026-04-12", body["due_on"])

// BC3 API clears fields by omission, so untouched fields from the
// existing todo must be preserved in the request body.
assert.Equal(t, "Test todo", body["content"])
assert.Equal(t, "Existing desc", body["description"])
assert.Equal(t, "2026-03-25", body["starts_on"])

ids, ok := body["assignee_ids"].([]any)
require.True(t, ok, "assignee_ids must be preserved")
require.Len(t, ids, 1)
assert.Equal(t, float64(42), ids[0])
}

func TestTodosUpdateTitlePreservesExistingFields(t *testing.T) {
transport := &mockTodoUpdateTransport{}
app := setupTodoUpdateApp(t, transport)

cmd := NewTodosCmd()
err := executeTodosCommand(cmd, app, "update", "999", "New title")
require.NoError(t, err)
require.NotEmpty(t, transport.capturedBody)

var body map[string]any
err = json.Unmarshal(transport.capturedBody, &body)
require.NoError(t, err)

assert.Equal(t, "New title", body["content"])
assert.Equal(t, "Existing desc", body["description"])
assert.Equal(t, "2026-04-01", body["due_on"])
assert.Equal(t, "2026-03-25", body["starts_on"])
}

// =============================================================================
// --assignee filtering tests (single-list path)
// =============================================================================
Expand Down
Loading