Skip to content

Commit e6ecba1

Browse files
authored
Merge pull request #83 from basecamp/board-publication-support
Add board publication commands
2 parents d96fe55 + 25e9921 commit e6ecba1

6 files changed

Lines changed: 376 additions & 6 deletions

File tree

SURFACE.txt

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ CMD fizzy board create
1111
CMD fizzy board delete
1212
CMD fizzy board help
1313
CMD fizzy board list
14+
CMD fizzy board publish
1415
CMD fizzy board show
16+
CMD fizzy board unpublish
1517
CMD fizzy board update
1618
CMD fizzy card
1719
CMD fizzy card assign
@@ -296,6 +298,19 @@ FLAG fizzy board list --quiet type=bool
296298
FLAG fizzy board list --styled type=bool
297299
FLAG fizzy board list --token type=string
298300
FLAG fizzy board list --verbose type=bool
301+
FLAG fizzy board publish --agent type=bool
302+
FLAG fizzy board publish --api-url type=string
303+
FLAG fizzy board publish --count type=bool
304+
FLAG fizzy board publish --help type=bool
305+
FLAG fizzy board publish --ids-only type=bool
306+
FLAG fizzy board publish --json type=bool
307+
FLAG fizzy board publish --limit type=int
308+
FLAG fizzy board publish --markdown type=bool
309+
FLAG fizzy board publish --profile type=string
310+
FLAG fizzy board publish --quiet type=bool
311+
FLAG fizzy board publish --styled type=bool
312+
FLAG fizzy board publish --token type=string
313+
FLAG fizzy board publish --verbose type=bool
299314
FLAG fizzy board show --agent type=bool
300315
FLAG fizzy board show --api-url type=string
301316
FLAG fizzy board show --count type=bool
@@ -309,6 +324,19 @@ FLAG fizzy board show --quiet type=bool
309324
FLAG fizzy board show --styled type=bool
310325
FLAG fizzy board show --token type=string
311326
FLAG fizzy board show --verbose type=bool
327+
FLAG fizzy board unpublish --agent type=bool
328+
FLAG fizzy board unpublish --api-url type=string
329+
FLAG fizzy board unpublish --count type=bool
330+
FLAG fizzy board unpublish --help type=bool
331+
FLAG fizzy board unpublish --ids-only type=bool
332+
FLAG fizzy board unpublish --json type=bool
333+
FLAG fizzy board unpublish --limit type=int
334+
FLAG fizzy board unpublish --markdown type=bool
335+
FLAG fizzy board unpublish --profile type=string
336+
FLAG fizzy board unpublish --quiet type=bool
337+
FLAG fizzy board unpublish --styled type=bool
338+
FLAG fizzy board unpublish --token type=string
339+
FLAG fizzy board unpublish --verbose type=bool
312340
FLAG fizzy board update --agent type=bool
313341
FLAG fizzy board update --all_access type=string
314342
FLAG fizzy board update --api-url type=string
@@ -1853,7 +1881,9 @@ SUB fizzy board create
18531881
SUB fizzy board delete
18541882
SUB fizzy board help
18551883
SUB fizzy board list
1884+
SUB fizzy board publish
18561885
SUB fizzy board show
1886+
SUB fizzy board unpublish
18571887
SUB fizzy board update
18581888
SUB fizzy card
18591889
SUB fizzy card assign

e2e/tests/board_test.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,83 @@ func TestBoardCRUD(t *testing.T) {
142142
if id != boardID {
143143
t.Errorf("expected id %q, got %q", boardID, id)
144144
}
145+
146+
if publicURL := result.GetDataString("public_url"); publicURL != "" {
147+
t.Errorf("expected unpublished board to omit public_url, got %q", publicURL)
148+
}
149+
})
150+
151+
t.Run("publish and unpublish board", func(t *testing.T) {
152+
if boardID == "" {
153+
t.Skip("no board ID from create test")
154+
}
155+
156+
published := false
157+
publishResult := h.Run("board", "publish", boardID)
158+
159+
// Only unpublish on exit if publish succeeded
160+
t.Cleanup(func() {
161+
if published {
162+
h.Run("board", "unpublish", boardID)
163+
}
164+
})
165+
166+
if publishResult.ExitCode != harness.ExitSuccess {
167+
t.Fatalf("expected exit code %d, got %d\nstderr: %s\nstdout: %s",
168+
harness.ExitSuccess, publishResult.ExitCode, publishResult.Stderr, publishResult.Stdout)
169+
}
170+
171+
if publishResult.Response == nil {
172+
t.Fatal("expected JSON response from publish")
173+
}
174+
175+
if !publishResult.Response.OK {
176+
t.Fatalf("expected ok=true, error: %+v", publishResult.Response.Error)
177+
}
178+
179+
published = true
180+
181+
publicURL := publishResult.GetDataString("public_url")
182+
if publicURL == "" {
183+
t.Fatal("expected public_url in publish response")
184+
}
185+
186+
showPublished := h.Run("board", "show", boardID)
187+
if showPublished.ExitCode != harness.ExitSuccess {
188+
t.Fatalf("failed to show published board: %s", showPublished.Stderr)
189+
}
190+
if got := showPublished.GetDataString("public_url"); got != publicURL {
191+
t.Errorf("expected public_url %q after publish, got %q", publicURL, got)
192+
}
193+
194+
// Verify unpublish works (cleanup is skipped since we unpublish here)
195+
unpublishResult := h.Run("board", "unpublish", boardID)
196+
if unpublishResult.ExitCode != harness.ExitSuccess {
197+
t.Fatalf("expected exit code %d, got %d\nstderr: %s\nstdout: %s",
198+
harness.ExitSuccess, unpublishResult.ExitCode, unpublishResult.Stderr, unpublishResult.Stdout)
199+
}
200+
201+
if unpublishResult.Response == nil {
202+
t.Fatal("expected JSON response from unpublish")
203+
}
204+
205+
if !unpublishResult.Response.OK {
206+
t.Fatalf("expected ok=true, error: %+v", unpublishResult.Response.Error)
207+
}
208+
209+
published = false
210+
211+
if !unpublishResult.GetDataBool("unpublished") {
212+
t.Error("expected unpublished=true")
213+
}
214+
215+
showUnpublished := h.Run("board", "show", boardID)
216+
if showUnpublished.ExitCode != harness.ExitSuccess {
217+
t.Fatalf("failed to show unpublished board: %s", showUnpublished.Stderr)
218+
}
219+
if got := showUnpublished.GetDataString("public_url"); got != "" {
220+
t.Errorf("expected public_url to be removed after unpublish, got %q", got)
221+
}
145222
})
146223

147224
t.Run("update board name", func(t *testing.T) {

internal/commands/board.go

Lines changed: 78 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,12 +96,12 @@ var boardShowCmd = &cobra.Command{
9696

9797
boardID := args[0]
9898

99-
data, _, err := getSDK().Boards().Get(cmd.Context(), boardID)
99+
resp, err := getSDK().Get(cmd.Context(), "/boards/"+boardID+".json")
100100
if err != nil {
101101
return convertSDKError(err)
102102
}
103103

104-
items := normalizeAny(data)
104+
items := normalizeAny(resp.Data)
105105

106106
summary := "Board"
107107
if board, ok := items.(map[string]any); ok {
@@ -115,6 +115,13 @@ var boardShowCmd = &cobra.Command{
115115
breadcrumb("columns", fmt.Sprintf("fizzy column list --board %s", boardID), "List columns"),
116116
breadcrumb("create-card", fmt.Sprintf("fizzy card create --board %s --title \"title\"", boardID), "Create card"),
117117
}
118+
if board, ok := items.(map[string]any); ok {
119+
if publicURL, ok := board["public_url"].(string); ok && publicURL != "" {
120+
breadcrumbs = append(breadcrumbs, breadcrumb("unpublish", fmt.Sprintf("fizzy board unpublish %s", boardID), "Disable public board link"))
121+
} else {
122+
breadcrumbs = append(breadcrumbs, breadcrumb("publish", fmt.Sprintf("fizzy board publish %s", boardID), "Create public board link"))
123+
}
124+
}
118125

119126
printDetail(items, summary, breadcrumbs)
120127
return nil
@@ -272,6 +279,71 @@ var boardDeleteCmd = &cobra.Command{
272279
},
273280
}
274281

282+
var boardPublishCmd = &cobra.Command{
283+
Use: "publish BOARD_ID",
284+
Short: "Publish a board",
285+
Long: "Publishes a board and returns its public share URL.",
286+
Args: cobra.ExactArgs(1),
287+
RunE: func(cmd *cobra.Command, args []string) error {
288+
if err := requireAuthAndAccount(); err != nil {
289+
return err
290+
}
291+
292+
boardID := args[0]
293+
294+
client := getClient()
295+
resp, err := client.Post("/boards/"+boardID+"/publication.json", nil)
296+
if err != nil {
297+
return err
298+
}
299+
300+
breadcrumbs := []Breadcrumb{
301+
breadcrumb("show", fmt.Sprintf("fizzy board show %s", boardID), "View board"),
302+
breadcrumb("cards", fmt.Sprintf("fizzy card list --board %s", boardID), "List cards"),
303+
breadcrumb("unpublish", fmt.Sprintf("fizzy board unpublish %s", boardID), "Disable public board link"),
304+
}
305+
306+
data := resp.Data
307+
if data == nil {
308+
data = map[string]any{"published": true}
309+
}
310+
311+
printMutation(data, "", breadcrumbs)
312+
return nil
313+
},
314+
}
315+
316+
var boardUnpublishCmd = &cobra.Command{
317+
Use: "unpublish BOARD_ID",
318+
Short: "Unpublish a board",
319+
Long: "Removes a board's public share URL.",
320+
Args: cobra.ExactArgs(1),
321+
RunE: func(cmd *cobra.Command, args []string) error {
322+
if err := requireAuthAndAccount(); err != nil {
323+
return err
324+
}
325+
326+
boardID := args[0]
327+
328+
client := getClient()
329+
_, err := client.Delete("/boards/" + boardID + "/publication.json")
330+
if err != nil {
331+
return err
332+
}
333+
334+
breadcrumbs := []Breadcrumb{
335+
breadcrumb("show", fmt.Sprintf("fizzy board show %s", boardID), "View board"),
336+
breadcrumb("cards", fmt.Sprintf("fizzy card list --board %s", boardID), "List cards"),
337+
breadcrumb("publish", fmt.Sprintf("fizzy board publish %s", boardID), "Create public board link"),
338+
}
339+
340+
printMutation(map[string]any{
341+
"unpublished": true,
342+
}, "", breadcrumbs)
343+
return nil
344+
},
345+
}
346+
275347
func init() {
276348
rootCmd.AddCommand(boardCmd)
277349

@@ -297,4 +369,8 @@ func init() {
297369

298370
// Delete
299371
boardCmd.AddCommand(boardDeleteCmd)
372+
373+
// Publication
374+
boardCmd.AddCommand(boardPublishCmd)
375+
boardCmd.AddCommand(boardUnpublishCmd)
300376
}

0 commit comments

Comments
 (0)