Skip to content

Commit 39a29d3

Browse files
committed
Use server-side card column filtering
1 parent 2b6ac58 commit 39a29d3

5 files changed

Lines changed: 81 additions & 84 deletions

File tree

internal/commands/card.go

Lines changed: 12 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -57,34 +57,35 @@ var cardListCmd = &cobra.Command{
5757
params = append(params, "board_ids[]="+boardID)
5858
}
5959

60-
clientSideColumnFilter := ""
61-
clientSideTriage := false
6260
if columnFilter != "" {
6361
if pseudo, ok := parsePseudoColumnID(columnFilter); ok {
6462
switch pseudo.Kind {
6563
case "not_now":
6664
if effectiveIndexedBy != "" && effectiveIndexedBy != "not_now" {
67-
return errors.NewInvalidArgsError("cannot combine --indexed-by with --column maybe")
65+
return errors.NewInvalidArgsError("cannot combine --indexed-by with --column " + columnFilter)
6866
}
6967
effectiveIndexedBy = "not_now"
7068
case "closed":
7169
if effectiveIndexedBy != "" && effectiveIndexedBy != "closed" {
72-
return errors.NewInvalidArgsError("cannot combine --indexed-by with --column done")
70+
return errors.NewInvalidArgsError("cannot combine --indexed-by with --column " + columnFilter)
7371
}
7472
effectiveIndexedBy = "closed"
7573
case "triage":
76-
if effectiveIndexedBy != "" {
77-
return errors.NewInvalidArgsError("cannot combine --indexed-by with --column not-yet")
74+
if effectiveIndexedBy != "" && effectiveIndexedBy != "maybe" {
75+
return errors.NewInvalidArgsError("cannot combine --indexed-by with --column " + columnFilter)
7876
}
79-
clientSideTriage = true
77+
effectiveIndexedBy = "maybe"
8078
default:
81-
clientSideColumnFilter = columnFilter
79+
if effectiveIndexedBy != "" {
80+
return errors.NewInvalidArgsError("cannot combine --indexed-by with --column")
81+
}
82+
params = append(params, "column_ids[]="+columnFilter)
8283
}
8384
} else {
8485
if effectiveIndexedBy != "" {
8586
return errors.NewInvalidArgsError("cannot combine --indexed-by with --column")
8687
}
87-
clientSideColumnFilter = columnFilter
88+
params = append(params, "column_ids[]="+columnFilter)
8889
}
8990
}
9091

@@ -128,10 +129,6 @@ var cardListCmd = &cobra.Command{
128129
path += "?" + strings.Join(params, "&")
129130
}
130131

131-
if (clientSideTriage || clientSideColumnFilter != "") && !cardListAll && cardListPage == 0 {
132-
return errors.NewInvalidArgsError("Filtering by column requires --all (or --page) because it is applied client-side")
133-
}
134-
135132
var items any
136133
var linkNext string
137134

@@ -150,46 +147,6 @@ var cardListCmd = &cobra.Command{
150147
linkNext = parseSDKLinkNext(resp)
151148
}
152149

153-
if clientSideTriage || clientSideColumnFilter != "" {
154-
arr := toSliceAny(items)
155-
if arr == nil {
156-
return errors.NewError("Unexpected cards list response")
157-
}
158-
159-
filtered := make([]any, 0, len(arr))
160-
for _, item := range arr {
161-
card, ok := item.(map[string]any)
162-
if !ok {
163-
continue
164-
}
165-
166-
columnID := ""
167-
if v, ok := card["column_id"].(string); ok {
168-
columnID = v
169-
}
170-
if columnID == "" {
171-
if col, ok := card["column"].(map[string]any); ok {
172-
if id, ok := col["id"].(string); ok {
173-
columnID = id
174-
}
175-
}
176-
}
177-
178-
if clientSideTriage {
179-
if columnID == "" {
180-
filtered = append(filtered, item)
181-
}
182-
continue
183-
}
184-
185-
if clientSideColumnFilter != "" && columnID == clientSideColumnFilter {
186-
filtered = append(filtered, item)
187-
}
188-
}
189-
190-
items = filtered
191-
}
192-
193150
// Build summary
194151
count := dataCount(items)
195152
summary := fmt.Sprintf("%d cards", count)
@@ -1082,9 +1039,9 @@ func init() {
10821039

10831040
// List
10841041
cardListCmd.Flags().StringVar(&cardListBoard, "board", "", "Filter by board ID")
1085-
cardListCmd.Flags().StringVar(&cardListColumn, "column", "", "Filter by column ID or pseudo column (not-yet, maybe, done)")
1042+
cardListCmd.Flags().StringVar(&cardListColumn, "column", "", "Filter by column ID or pseudo column (not-now, maybe, done)")
10861043
cardListCmd.Flags().StringVar(&cardListTag, "tag", "", "Filter by tag ID")
1087-
cardListCmd.Flags().StringVar(&cardListIndexedBy, "indexed-by", "", "Filter by lane/index (all, closed, not_now, stalled, postponing_soon, golden)")
1044+
cardListCmd.Flags().StringVar(&cardListIndexedBy, "indexed-by", "", "Filter by lane/index (all, closed, maybe, not_now, stalled, postponing_soon, golden)")
10881045
cardListCmd.Flags().StringVar(&cardListIndexedBy, "status", "", "Alias for --indexed-by")
10891046
_ = cardListCmd.Flags().MarkDeprecated("status", "use --indexed-by")
10901047
cardListCmd.Flags().StringVar(&cardListAssignee, "assignee", "", "Filter by assignee ID")

internal/commands/card_test.go

Lines changed: 59 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -88,29 +88,45 @@ func TestCardList(t *testing.T) {
8888
}
8989
})
9090

91-
t.Run("requires --all for client-side triage filter", func(t *testing.T) {
91+
t.Run("filters by real column server-side without client-side filtering", func(t *testing.T) {
9292
mock := NewMockClient()
93-
SetTestModeWithSDK(mock)
93+
mock.GetWithPaginationResponse = &client.APIResponse{
94+
StatusCode: 200,
95+
Data: []any{
96+
map[string]any{"id": "1", "title": "Column 1", "column_id": "col-1"},
97+
map[string]any{"id": "2", "title": "Column 2", "column_id": "col-2"},
98+
},
99+
}
100+
101+
result := SetTestModeWithSDK(mock)
94102
SetTestConfig("token", "account", "https://api.example.com")
95103
defer resetTest()
96104

97-
cardListColumn = "maybe"
98-
cardListAll = false
99-
cardListPage = 0
105+
cardListColumn = "col-1"
100106
err := cardListCmd.RunE(cardListCmd, []string{})
101107
cardListColumn = ""
102108

103-
assertExitCode(t, err, errors.ExitInvalidArgs)
109+
assertExitCode(t, err, 0)
110+
if mock.GetWithPaginationCalls[0].Path != "/cards.json?column_ids[]=col-1" {
111+
t.Errorf("expected server-side column_ids filter, got '%s'", mock.GetWithPaginationCalls[0].Path)
112+
}
113+
114+
arr, ok := result.Response.Data.([]any)
115+
if !ok {
116+
t.Fatalf("expected array response data, got %T", result.Response.Data)
117+
}
118+
if len(arr) != 2 {
119+
t.Fatalf("expected server response to remain unfiltered client-side, got %d cards", len(arr))
120+
}
104121
})
105122

106-
t.Run("filters triage client-side with --all", func(t *testing.T) {
123+
t.Run("filters by pseudo column maybe server-side without all", func(t *testing.T) {
107124
mock := NewMockClient()
108125
mock.GetWithPaginationResponse = &client.APIResponse{
109126
StatusCode: 200,
110127
Data: []any{
111128
map[string]any{"id": "1", "title": "Triage", "column": nil},
112-
map[string]any{"id": "2", "title": "In Column", "column": map[string]any{"id": "col-1"}},
113-
map[string]any{"id": "3", "title": "In Column 2", "column_id": "col-2"},
129+
map[string]any{"id": "2", "title": "Unexpected extra", "column_id": "col-1"},
114130
},
115131
}
116132

@@ -119,26 +135,20 @@ func TestCardList(t *testing.T) {
119135
defer resetTest()
120136

121137
cardListColumn = "maybe"
122-
cardListAll = true
123138
err := cardListCmd.RunE(cardListCmd, []string{})
124139
cardListColumn = ""
125-
cardListAll = false
126140

127141
assertExitCode(t, err, 0)
128-
129-
if err != nil {
130-
t.Fatalf("unexpected error: %v", err)
142+
if mock.GetWithPaginationCalls[0].Path != "/cards.json?indexed_by=maybe" {
143+
t.Errorf("expected server-side maybe filter, got '%s'", mock.GetWithPaginationCalls[0].Path)
131144
}
145+
132146
arr, ok := result.Response.Data.([]any)
133147
if !ok {
134148
t.Fatalf("expected array response data, got %T", result.Response.Data)
135149
}
136-
if len(arr) != 1 {
137-
t.Fatalf("expected 1 triage card, got %d", len(arr))
138-
}
139-
card := arr[0].(map[string]any)
140-
if card["id"] != "1" {
141-
t.Errorf("expected triage card id '1', got '%v'", card["id"])
150+
if len(arr) != 2 {
151+
t.Fatalf("expected server response to remain unfiltered client-side, got %d cards", len(arr))
142152
}
143153
})
144154

@@ -332,6 +342,35 @@ func TestCardList(t *testing.T) {
332342
t.Errorf("expected path '%s', got '%s'", expected, path)
333343
}
334344
})
345+
346+
t.Run("combines column with other filters without changing command shape", func(t *testing.T) {
347+
mock := NewMockClient()
348+
mock.GetWithPaginationResponse = &client.APIResponse{
349+
StatusCode: 200,
350+
Data: []any{},
351+
}
352+
353+
SetTestModeWithSDK(mock)
354+
SetTestConfig("token", "account", "https://api.example.com")
355+
defer resetTest()
356+
357+
cardListBoard = "123"
358+
cardListColumn = "col-1"
359+
cardListTag = "tag-1"
360+
cardListAssignee = "user-1"
361+
err := cardListCmd.RunE(cardListCmd, []string{})
362+
cardListBoard = ""
363+
cardListColumn = ""
364+
cardListTag = ""
365+
cardListAssignee = ""
366+
367+
assertExitCode(t, err, 0)
368+
path := mock.GetWithPaginationCalls[0].Path
369+
expected := "/cards.json?board_ids[]=123&column_ids[]=col-1&tag_ids[]=tag-1&assignee_ids[]=user-1"
370+
if path != expected {
371+
t.Errorf("expected path '%s', got '%s'", expected, path)
372+
}
373+
})
335374
}
336375

337376
func TestCardShow(t *testing.T) {

internal/commands/pseudocolumns.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,10 @@ type pseudoColumn struct {
1111
var (
1212
// "Not Now" contains postponed cards (indexed_by=not_now)
1313
pseudoColumnNotNow = pseudoColumn{ID: "not-now", Name: "Not Now", Kind: "not_now"}
14-
// "Maybe?" contains triage/backlog cards (null column_id)
14+
// "Maybe?" contains triage/backlog cards (indexed_by=maybe)
1515
pseudoColumnMaybe = pseudoColumn{ID: "maybe", Name: "Maybe?", Kind: "triage"}
16-
pseudoColumnDone = pseudoColumn{ID: "done", Name: "Done", Kind: "closed"}
16+
// "Done" contains closed cards (indexed_by=closed)
17+
pseudoColumnDone = pseudoColumn{ID: "done", Name: "Done", Kind: "closed"}
1718
)
1819

1920
func pseudoColumnObject(c pseudoColumn) map[string]any {

internal/commands/search.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ func init() {
112112
searchCmd.Flags().StringVar(&searchBoard, "board", "", "Filter by board ID")
113113
searchCmd.Flags().StringVar(&searchTag, "tag", "", "Filter by tag ID")
114114
searchCmd.Flags().StringVar(&searchAssignee, "assignee", "", "Filter by assignee ID")
115-
searchCmd.Flags().StringVar(&searchIndexedBy, "indexed-by", "", "Filter by status (all, closed, not_now, golden)")
115+
searchCmd.Flags().StringVar(&searchIndexedBy, "indexed-by", "", "Filter by status (all, closed, maybe, not_now, golden)")
116116
searchCmd.Flags().StringVar(&searchSort, "sort", "", "Sort order: newest, oldest, or latest (default)")
117117
searchCmd.Flags().IntVar(&searchPage, "page", 0, "Page number")
118118
searchCmd.Flags().BoolVar(&searchAll, "all", false, "Fetch all pages")

skills/fizzy/SKILL.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ Full CLI coverage: boards, cards, columns, comments, steps, reactions, tags, use
7777
Need to find something?
7878
├── Know the board? → fizzy card list --board <id>
7979
├── Full-text search? → fizzy search "query"
80-
├── Filter by status? → fizzy card list --indexed-by closed|not_now|golden|stalled
80+
├── Filter by status? → fizzy card list --indexed-by maybe|closed|not_now|golden|stalled
8181
├── Filter by person? → fizzy card list --assignee <id>
8282
├── Filter by time? → fizzy card list --created today|thisweek|thismonth
8383
└── Cross-board? → fizzy search "query" (searches all boards)
@@ -360,9 +360,9 @@ Cards exist in different states. By default, `fizzy card list` returns **open ca
360360
You can also use pseudo-columns:
361361

362362
```bash
363-
fizzy card list --column done --all # Same as --indexed-by closed
364-
fizzy card list --column not-now --all # Same as --indexed-by not_now
365-
fizzy card list --column maybe --all # Cards in triage (no column assigned)
363+
fizzy card list --column done # Same as --indexed-by closed
364+
fizzy card list --column not-now # Same as --indexed-by not_now
365+
fizzy card list --column maybe # Same as --indexed-by maybe
366366
```
367367

368368
**Fetching all cards on a board:**
@@ -475,7 +475,7 @@ fizzy search QUERY [flags]
475475
--board ID # Filter by board
476476
--assignee ID # Filter by assignee user ID
477477
--tag ID # Filter by tag ID
478-
--indexed-by LANE # Filter: all, closed, not_now, golden
478+
--indexed-by LANE # Filter: all, closed, maybe, not_now, golden
479479
--sort ORDER # Sort: newest, oldest, or latest (default)
480480
--page N # Page number
481481
--all # Fetch all pages
@@ -559,7 +559,7 @@ fizzy card list [flags]
559559
--column ID # Filter by column ID or pseudo: not-now, maybe, done
560560
--assignee ID # Filter by assignee user ID
561561
--tag ID # Filter by tag ID
562-
--indexed-by LANE # Filter: all, closed, not_now, stalled, postponing_soon, golden
562+
--indexed-by LANE # Filter: all, closed, maybe, not_now, stalled, postponing_soon, golden
563563
--search "terms" # Search by text (space-separated for multiple terms)
564564
--sort ORDER # Sort: newest, oldest, or latest (default)
565565
--creator ID # Filter by creator user ID

0 commit comments

Comments
 (0)