Skip to content
Merged
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
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,10 @@ fizzy card delete 42
fizzy card close 42
fizzy card reopen 42

# Move to a different board
fizzy card move 42 --to BOARD_ID
fizzy card move 42 -t BOARD_ID

# Move to "Not Now"
fizzy card postpone 42

Expand Down
49 changes: 49 additions & 0 deletions internal/commands/card.go
Original file line number Diff line number Diff line change
Expand Up @@ -442,6 +442,51 @@ var cardPostponeCmd = &cobra.Command{
},
}

// Card move flags
var cardMoveBoard string

var cardMoveCmd = &cobra.Command{
Use: "move CARD_NUMBER",
Short: "Move card to a different board",
Long: "Moves a card to a different board.",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
if err := requireAuthAndAccount(); err != nil {
exitWithError(err)
}

if cardMoveBoard == "" {
exitWithError(newRequiredFlagError("to"))
}

body := map[string]interface{}{
"board_id": cardMoveBoard,
}

client := getClient()
_, err := client.Patch("/cards/"+args[0]+"/board.json", body)
if err != nil {
exitWithError(err)
}

// Fetch the updated card to show confirmation with title
resp, err := client.Get("/cards/" + args[0] + ".json")
if err != nil {
exitWithError(err)
}

// Build summary with card title if available
summary := fmt.Sprintf("Card #%s moved to board %s", args[0], cardMoveBoard)
if card, ok := resp.Data.(map[string]interface{}); ok {
if title, ok := card["title"].(string); ok {
summary = fmt.Sprintf("Card #%s \"%s\" moved to board %s", args[0], title, cardMoveBoard)
}
}

printSuccessWithSummary(resp.Data, summary)
},
}

// Card column flags
var cardColumnColumn string

Expand Down Expand Up @@ -782,6 +827,10 @@ func init() {
cardCmd.AddCommand(cardReopenCmd)
cardCmd.AddCommand(cardPostponeCmd)

// Move to different board
cardMoveCmd.Flags().StringVarP(&cardMoveBoard, "to", "t", "", "Target board ID (required)")
cardCmd.AddCommand(cardMoveCmd)

// Column
cardColumnCmd.Flags().StringVar(&cardColumnColumn, "column", "", "Column ID (required)")
cardCmd.AddCommand(cardColumnCmd)
Expand Down
84 changes: 84 additions & 0 deletions internal/commands/card_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1184,3 +1184,87 @@ func TestCardUngolden(t *testing.T) {
}
})
}

func TestCardMove(t *testing.T) {
t.Run("moves card to different board", func(t *testing.T) {
mock := NewMockClient()
mock.PatchResponse = &client.APIResponse{
StatusCode: 204,
Data: nil,
}
mock.GetResponse = &client.APIResponse{
StatusCode: 200,
Data: map[string]interface{}{
"id": "abc",
"number": float64(42),
"title": "Test Card",
"board_id": "board-456",
},
}

result := SetTestMode(mock)
SetTestConfig("token", "account", "https://api.example.com")
defer ResetTestMode()

cardMoveBoard = "board-456"
RunTestCommand(func() {
cardMoveCmd.Run(cardMoveCmd, []string{"42"})
})
cardMoveBoard = ""

if result.ExitCode != 0 {
t.Errorf("expected exit code 0, got %d", result.ExitCode)
}
if len(mock.PatchCalls) != 1 {
t.Errorf("expected 1 patch call, got %d", len(mock.PatchCalls))
}
if mock.PatchCalls[0].Path != "/cards/42/board.json" {
t.Errorf("expected path '/cards/42/board.json', got '%s'", mock.PatchCalls[0].Path)
}

body := mock.PatchCalls[0].Body.(map[string]interface{})
if body["board_id"] != "board-456" {
t.Errorf("expected board_id 'board-456', got '%v'", body["board_id"])
}

// Verify it fetched the card after moving
if len(mock.GetCalls) != 1 || mock.GetCalls[0].Path != "/cards/42.json" {
t.Errorf("expected get call to '/cards/42.json', got %+v", mock.GetCalls)
}
})

t.Run("requires --to flag", func(t *testing.T) {
mock := NewMockClient()
result := SetTestMode(mock)
SetTestConfig("token", "account", "https://api.example.com")
defer ResetTestMode()

cardMoveBoard = ""
RunTestCommand(func() {
cardMoveCmd.Run(cardMoveCmd, []string{"42"})
})

if result.ExitCode != errors.ExitInvalidArgs {
t.Errorf("expected exit code %d, got %d", errors.ExitInvalidArgs, result.ExitCode)
}
})

t.Run("handles not found error", func(t *testing.T) {
mock := NewMockClient()
mock.PatchError = errors.NewNotFoundError("Card not found")

result := SetTestMode(mock)
SetTestConfig("token", "account", "https://api.example.com")
defer ResetTestMode()

cardMoveBoard = "board-456"
RunTestCommand(func() {
cardMoveCmd.Run(cardMoveCmd, []string{"999"})
})
cardMoveBoard = ""

if result.ExitCode != errors.ExitNotFound {
t.Errorf("expected exit code %d, got %d", errors.ExitNotFound, result.ExitCode)
}
})
}