fizzy-cli 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env +1 -0
- package/CHANGELOG.md +26 -0
- package/bin/fizzy +0 -0
- package/cmd/card_list.go +62 -1
- package/cmd/card_list_test.go +225 -0
- package/cmd/card_reaction.go +13 -0
- package/cmd/card_reaction_create.go +46 -0
- package/cmd/card_reaction_create_test.go +148 -0
- package/cmd/card_reaction_delete.go +46 -0
- package/cmd/card_reaction_delete_test.go +112 -0
- package/cmd/card_reaction_list.go +51 -0
- package/cmd/card_reaction_list_test.go +127 -0
- package/cmd/reaction.go +2 -2
- package/cmd/user.go +22 -0
- package/cmd/user_deactivate.go +40 -0
- package/cmd/user_deactivate_test.go +121 -0
- package/cmd/user_list.go +44 -0
- package/cmd/user_list_test.go +126 -0
- package/cmd/user_show.go +40 -0
- package/cmd/user_show_test.go +110 -0
- package/cmd/user_update.go +71 -0
- package/cmd/user_update_test.go +177 -0
- package/docs/API.md +63 -2
- package/internal/api/cards.go +40 -6
- package/internal/api/reactions.go +61 -0
- package/internal/api/types.go +5 -0
- package/internal/api/users.go +75 -0
- package/internal/ui/user_list.go +19 -0
- package/internal/ui/user_show.go +23 -0
- package/package.json +1 -1
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
package cmd
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"encoding/json"
|
|
6
|
+
"net/http"
|
|
7
|
+
"net/http/httptest"
|
|
8
|
+
"testing"
|
|
9
|
+
|
|
10
|
+
"github.com/rogeriopvl/fizzy/internal/api"
|
|
11
|
+
"github.com/rogeriopvl/fizzy/internal/app"
|
|
12
|
+
"github.com/rogeriopvl/fizzy/internal/testutil"
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
func TestUserShowCommand(t *testing.T) {
|
|
16
|
+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
17
|
+
if r.URL.Path != "/users/user-123" {
|
|
18
|
+
t.Errorf("expected /users/user-123, got %s", r.URL.Path)
|
|
19
|
+
}
|
|
20
|
+
if r.Method != http.MethodGet {
|
|
21
|
+
t.Errorf("expected GET, got %s", r.Method)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
auth := r.Header.Get("Authorization")
|
|
25
|
+
if auth != "Bearer test-token" {
|
|
26
|
+
t.Errorf("expected Bearer test-token, got %s", auth)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
w.Header().Set("Content-Type", "application/json")
|
|
30
|
+
response := api.User{
|
|
31
|
+
ID: "user-123",
|
|
32
|
+
Name: "John Doe",
|
|
33
|
+
Email: "john@example.com",
|
|
34
|
+
Role: "admin",
|
|
35
|
+
Active: true,
|
|
36
|
+
CreatedAt: "2025-01-01T00:00:00Z",
|
|
37
|
+
}
|
|
38
|
+
json.NewEncoder(w).Encode(response)
|
|
39
|
+
}))
|
|
40
|
+
defer server.Close()
|
|
41
|
+
|
|
42
|
+
client := testutil.NewTestClient(server.URL, "", "", "test-token")
|
|
43
|
+
testApp := &app.App{Client: client}
|
|
44
|
+
|
|
45
|
+
cmd := userShowCmd
|
|
46
|
+
cmd.SetContext(testApp.ToContext(context.Background()))
|
|
47
|
+
|
|
48
|
+
if err := handleShowUser(cmd, "user-123"); err != nil {
|
|
49
|
+
t.Fatalf("handleShowUser failed: %v", err)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
func TestUserShowCommandNotFound(t *testing.T) {
|
|
54
|
+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
55
|
+
w.WriteHeader(http.StatusNotFound)
|
|
56
|
+
w.Write([]byte("User not found"))
|
|
57
|
+
}))
|
|
58
|
+
defer server.Close()
|
|
59
|
+
|
|
60
|
+
client := testutil.NewTestClient(server.URL, "", "", "test-token")
|
|
61
|
+
testApp := &app.App{Client: client}
|
|
62
|
+
|
|
63
|
+
cmd := userShowCmd
|
|
64
|
+
cmd.SetContext(testApp.ToContext(context.Background()))
|
|
65
|
+
|
|
66
|
+
err := handleShowUser(cmd, "nonexistent-user")
|
|
67
|
+
if err == nil {
|
|
68
|
+
t.Errorf("expected error for user not found")
|
|
69
|
+
}
|
|
70
|
+
if err.Error() != "fetching user: unexpected status code 404: User not found" {
|
|
71
|
+
t.Errorf("expected user not found error, got %v", err)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
func TestUserShowCommandAPIError(t *testing.T) {
|
|
76
|
+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
77
|
+
w.WriteHeader(http.StatusInternalServerError)
|
|
78
|
+
w.Write([]byte("Internal Server Error"))
|
|
79
|
+
}))
|
|
80
|
+
defer server.Close()
|
|
81
|
+
|
|
82
|
+
client := testutil.NewTestClient(server.URL, "", "", "test-token")
|
|
83
|
+
testApp := &app.App{Client: client}
|
|
84
|
+
|
|
85
|
+
cmd := userShowCmd
|
|
86
|
+
cmd.SetContext(testApp.ToContext(context.Background()))
|
|
87
|
+
|
|
88
|
+
err := handleShowUser(cmd, "user-123")
|
|
89
|
+
if err == nil {
|
|
90
|
+
t.Errorf("expected error for API failure")
|
|
91
|
+
}
|
|
92
|
+
if err.Error() != "fetching user: unexpected status code 500: Internal Server Error" {
|
|
93
|
+
t.Errorf("expected API error, got %v", err)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
func TestUserShowCommandNoClient(t *testing.T) {
|
|
98
|
+
testApp := &app.App{}
|
|
99
|
+
|
|
100
|
+
cmd := userShowCmd
|
|
101
|
+
cmd.SetContext(testApp.ToContext(context.Background()))
|
|
102
|
+
|
|
103
|
+
err := handleShowUser(cmd, "user-123")
|
|
104
|
+
if err == nil {
|
|
105
|
+
t.Errorf("expected error when client not available")
|
|
106
|
+
}
|
|
107
|
+
if err.Error() != "API client not available" {
|
|
108
|
+
t.Errorf("expected 'client not available' error, got %v", err)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
package cmd
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"fmt"
|
|
6
|
+
|
|
7
|
+
"github.com/rogeriopvl/fizzy/internal/api"
|
|
8
|
+
"github.com/rogeriopvl/fizzy/internal/app"
|
|
9
|
+
"github.com/spf13/cobra"
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
var userUpdateCmd = &cobra.Command{
|
|
13
|
+
Use: "update <user_id>",
|
|
14
|
+
Short: "Update a user",
|
|
15
|
+
Long: `Update user settings such as name and avatar.
|
|
16
|
+
|
|
17
|
+
Avatar must be provided as a URL (e.g., https://example.com/avatar.jpg).
|
|
18
|
+
|
|
19
|
+
Example:
|
|
20
|
+
fizzy user update user-123 --name "John Doe"
|
|
21
|
+
fizzy user update user-123 --avatar https://example.com/avatar.jpg`,
|
|
22
|
+
Args: cobra.ExactArgs(1),
|
|
23
|
+
Run: func(cmd *cobra.Command, args []string) {
|
|
24
|
+
if err := handleUpdateUser(cmd, args[0]); err != nil {
|
|
25
|
+
fmt.Fprintf(cmd.OutOrStderr(), "Error: %v\n", err)
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
func handleUpdateUser(cmd *cobra.Command, userID string) error {
|
|
31
|
+
if !cmd.Flags().Changed("name") && !cmd.Flags().Changed("avatar") {
|
|
32
|
+
return fmt.Errorf("at least one flag must be provided (--name or --avatar)")
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
a := app.FromContext(cmd.Context())
|
|
36
|
+
if a == nil || a.Client == nil {
|
|
37
|
+
return fmt.Errorf("API client not available")
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
payload := api.UpdateUserPayload{}
|
|
41
|
+
|
|
42
|
+
if cmd.Flags().Changed("name") {
|
|
43
|
+
name, err := cmd.Flags().GetString("name")
|
|
44
|
+
if err != nil {
|
|
45
|
+
return fmt.Errorf("invalid name flag: %w", err)
|
|
46
|
+
}
|
|
47
|
+
payload.Name = name
|
|
48
|
+
}
|
|
49
|
+
if cmd.Flags().Changed("avatar") {
|
|
50
|
+
avatar, err := cmd.Flags().GetString("avatar")
|
|
51
|
+
if err != nil {
|
|
52
|
+
return fmt.Errorf("invalid avatar flag: %w", err)
|
|
53
|
+
}
|
|
54
|
+
payload.Avatar = avatar
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
err := a.Client.PutUser(context.Background(), userID, payload)
|
|
58
|
+
if err != nil {
|
|
59
|
+
return fmt.Errorf("updating user: %w", err)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
fmt.Fprintf(cmd.OutOrStdout(), "✓ User '%s' updated successfully\n", userID)
|
|
63
|
+
return nil
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
func init() {
|
|
67
|
+
userUpdateCmd.Flags().StringP("name", "n", "", "User name")
|
|
68
|
+
userUpdateCmd.Flags().String("avatar", "", "Avatar URL (e.g., https://example.com/avatar.jpg)")
|
|
69
|
+
|
|
70
|
+
userCmd.AddCommand(userUpdateCmd)
|
|
71
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
package cmd
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"encoding/json"
|
|
6
|
+
"io"
|
|
7
|
+
"net/http"
|
|
8
|
+
"net/http/httptest"
|
|
9
|
+
"testing"
|
|
10
|
+
|
|
11
|
+
"github.com/rogeriopvl/fizzy/internal/api"
|
|
12
|
+
"github.com/rogeriopvl/fizzy/internal/app"
|
|
13
|
+
"github.com/rogeriopvl/fizzy/internal/testutil"
|
|
14
|
+
"github.com/spf13/cobra"
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
func TestUserUpdateCommandSuccess(t *testing.T) {
|
|
18
|
+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
19
|
+
if r.URL.Path != "/users/user-123" {
|
|
20
|
+
t.Errorf("expected /users/user-123, got %s", r.URL.Path)
|
|
21
|
+
}
|
|
22
|
+
if r.Method != http.MethodPut {
|
|
23
|
+
t.Errorf("expected PUT, got %s", r.Method)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
auth := r.Header.Get("Authorization")
|
|
27
|
+
if auth != "Bearer test-token" {
|
|
28
|
+
t.Errorf("expected Bearer test-token, got %s", auth)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
body, _ := io.ReadAll(r.Body)
|
|
32
|
+
var payload map[string]api.UpdateUserPayload
|
|
33
|
+
if err := json.Unmarshal(body, &payload); err != nil {
|
|
34
|
+
t.Fatalf("failed to unmarshal request body: %v", err)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
userPayload := payload["user"]
|
|
38
|
+
if userPayload.Name != "Updated Name" {
|
|
39
|
+
t.Errorf("expected name 'Updated Name', got %s", userPayload.Name)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
w.WriteHeader(http.StatusNoContent)
|
|
43
|
+
}))
|
|
44
|
+
defer server.Close()
|
|
45
|
+
|
|
46
|
+
client := testutil.NewTestClient(server.URL, "", "", "test-token")
|
|
47
|
+
testApp := &app.App{Client: client}
|
|
48
|
+
|
|
49
|
+
cmd := userUpdateCmd
|
|
50
|
+
cmd.SetContext(testApp.ToContext(context.Background()))
|
|
51
|
+
cmd.ParseFlags([]string{"--name", "Updated Name"})
|
|
52
|
+
|
|
53
|
+
if err := handleUpdateUser(cmd, "user-123"); err != nil {
|
|
54
|
+
t.Fatalf("handleUpdateUser failed: %v", err)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
func TestUserUpdateCommandWithAllFlags(t *testing.T) {
|
|
59
|
+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
60
|
+
if r.Method != http.MethodPut {
|
|
61
|
+
t.Errorf("expected PUT, got %s", r.Method)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
body, _ := io.ReadAll(r.Body)
|
|
65
|
+
var payload map[string]api.UpdateUserPayload
|
|
66
|
+
json.Unmarshal(body, &payload)
|
|
67
|
+
|
|
68
|
+
userPayload := payload["user"]
|
|
69
|
+
if userPayload.Name != "Updated Name" {
|
|
70
|
+
t.Errorf("expected name 'Updated Name', got %s", userPayload.Name)
|
|
71
|
+
}
|
|
72
|
+
if userPayload.Avatar != "https://example.com/avatar.png" {
|
|
73
|
+
t.Errorf("expected avatar 'https://example.com/avatar.png', got %s", userPayload.Avatar)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
w.WriteHeader(http.StatusNoContent)
|
|
77
|
+
}))
|
|
78
|
+
defer server.Close()
|
|
79
|
+
|
|
80
|
+
client := testutil.NewTestClient(server.URL, "", "", "test-token")
|
|
81
|
+
testApp := &app.App{Client: client}
|
|
82
|
+
|
|
83
|
+
cmd := userUpdateCmd
|
|
84
|
+
cmd.SetContext(testApp.ToContext(context.Background()))
|
|
85
|
+
cmd.ParseFlags([]string{
|
|
86
|
+
"--name", "Updated Name",
|
|
87
|
+
"--avatar", "https://example.com/avatar.png",
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
if err := handleUpdateUser(cmd, "user-123"); err != nil {
|
|
91
|
+
t.Fatalf("handleUpdateUser failed: %v", err)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
func TestUserUpdateCommandNoFlags(t *testing.T) {
|
|
96
|
+
client := testutil.NewTestClient("http://localhost", "", "", "test-token")
|
|
97
|
+
testApp := &app.App{Client: client}
|
|
98
|
+
|
|
99
|
+
cmd := &cobra.Command{
|
|
100
|
+
Use: "update <user_id>",
|
|
101
|
+
Args: cobra.ExactArgs(1),
|
|
102
|
+
}
|
|
103
|
+
cmd.Flags().String("name", "", "User name")
|
|
104
|
+
cmd.Flags().String("avatar", "", "Avatar URL or file path")
|
|
105
|
+
|
|
106
|
+
cmd.SetContext(testApp.ToContext(context.Background()))
|
|
107
|
+
|
|
108
|
+
err := handleUpdateUser(cmd, "user-123")
|
|
109
|
+
if err == nil {
|
|
110
|
+
t.Errorf("expected error when no flags provided")
|
|
111
|
+
}
|
|
112
|
+
if err.Error() != "at least one flag must be provided (--name or --avatar)" {
|
|
113
|
+
t.Errorf("expected flag requirement error, got %v", err)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
func TestUserUpdateCommandNotFound(t *testing.T) {
|
|
118
|
+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
119
|
+
w.WriteHeader(http.StatusNotFound)
|
|
120
|
+
w.Write([]byte("User not found"))
|
|
121
|
+
}))
|
|
122
|
+
defer server.Close()
|
|
123
|
+
|
|
124
|
+
client := testutil.NewTestClient(server.URL, "", "", "test-token")
|
|
125
|
+
testApp := &app.App{Client: client}
|
|
126
|
+
|
|
127
|
+
cmd := userUpdateCmd
|
|
128
|
+
cmd.SetContext(testApp.ToContext(context.Background()))
|
|
129
|
+
cmd.ParseFlags([]string{"--name", "Updated Name"})
|
|
130
|
+
|
|
131
|
+
err := handleUpdateUser(cmd, "nonexistent-user")
|
|
132
|
+
if err == nil {
|
|
133
|
+
t.Errorf("expected error for user not found")
|
|
134
|
+
}
|
|
135
|
+
if err.Error() != "updating user: unexpected status code 404: User not found" {
|
|
136
|
+
t.Errorf("expected user not found error, got %v", err)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
func TestUserUpdateCommandAPIError(t *testing.T) {
|
|
141
|
+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
142
|
+
w.WriteHeader(http.StatusInternalServerError)
|
|
143
|
+
w.Write([]byte("Internal Server Error"))
|
|
144
|
+
}))
|
|
145
|
+
defer server.Close()
|
|
146
|
+
|
|
147
|
+
client := testutil.NewTestClient(server.URL, "", "", "test-token")
|
|
148
|
+
testApp := &app.App{Client: client}
|
|
149
|
+
|
|
150
|
+
cmd := userUpdateCmd
|
|
151
|
+
cmd.SetContext(testApp.ToContext(context.Background()))
|
|
152
|
+
cmd.ParseFlags([]string{"--name", "Updated Name"})
|
|
153
|
+
|
|
154
|
+
err := handleUpdateUser(cmd, "user-123")
|
|
155
|
+
if err == nil {
|
|
156
|
+
t.Errorf("expected error for API failure")
|
|
157
|
+
}
|
|
158
|
+
if err.Error() != "updating user: unexpected status code 500: Internal Server Error" {
|
|
159
|
+
t.Errorf("expected API error, got %v", err)
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
func TestUserUpdateCommandNoClient(t *testing.T) {
|
|
164
|
+
testApp := &app.App{}
|
|
165
|
+
|
|
166
|
+
cmd := userUpdateCmd
|
|
167
|
+
cmd.SetContext(testApp.ToContext(context.Background()))
|
|
168
|
+
cmd.ParseFlags([]string{"--name", "Updated Name"})
|
|
169
|
+
|
|
170
|
+
err := handleUpdateUser(cmd, "user-123")
|
|
171
|
+
if err == nil {
|
|
172
|
+
t.Errorf("expected error when client not available")
|
|
173
|
+
}
|
|
174
|
+
if err.Error() != "API client not available" {
|
|
175
|
+
t.Errorf("expected 'client not available' error, got %v", err)
|
|
176
|
+
}
|
|
177
|
+
}
|
package/docs/API.md
CHANGED
|
@@ -553,7 +553,8 @@ __Response:__
|
|
|
553
553
|
"created_at": "2025-12-05T19:36:35.401Z",
|
|
554
554
|
"url": "http://fizzy.localhost:3006/897362094/users/03f5v9zjw7pz8717a4no1h8a7"
|
|
555
555
|
},
|
|
556
|
-
"comments_url": "http://fizzy.localhost:3006/897362094/cards/4/comments"
|
|
556
|
+
"comments_url": "http://fizzy.localhost:3006/897362094/cards/4/comments",
|
|
557
|
+
"reactions_url": "http://fizzy.localhost:3006/897362094/cards/4/reactions"
|
|
557
558
|
},
|
|
558
559
|
]
|
|
559
560
|
```
|
|
@@ -614,6 +615,7 @@ __Response:__
|
|
|
614
615
|
"url": "http://fizzy.localhost:3006/897362094/users/03f5v9zjw7pz8717a4no1h8a7"
|
|
615
616
|
},
|
|
616
617
|
"comments_url": "http://fizzy.localhost:3006/897362094/cards/4/comments",
|
|
618
|
+
"reactions_url": "http://fizzy.localhost:3006/897362094/cards/4/reactions",
|
|
617
619
|
"steps": [
|
|
618
620
|
{
|
|
619
621
|
"id": "03f8huu0sog76g3s975963b5e",
|
|
@@ -928,7 +930,66 @@ __Response:__
|
|
|
928
930
|
|
|
929
931
|
Returns `204 No Content` on success.
|
|
930
932
|
|
|
931
|
-
## Reactions
|
|
933
|
+
## Card Reactions (Boosts)
|
|
934
|
+
|
|
935
|
+
Card reactions (also called "boosts") let users add short responses directly to cards. These are limited to 16 characters.
|
|
936
|
+
|
|
937
|
+
### `GET /:account_slug/cards/:card_number/reactions`
|
|
938
|
+
|
|
939
|
+
Returns a list of reactions on a card.
|
|
940
|
+
|
|
941
|
+
__Response:__
|
|
942
|
+
|
|
943
|
+
```json
|
|
944
|
+
[
|
|
945
|
+
{
|
|
946
|
+
"id": "03f5v9zo9qlcwwpyc0ascnikz",
|
|
947
|
+
"content": "👍",
|
|
948
|
+
"reacter": {
|
|
949
|
+
"id": "03f5v9zjw7pz8717a4no1h8a7",
|
|
950
|
+
"name": "David Heinemeier Hansson",
|
|
951
|
+
"role": "owner",
|
|
952
|
+
"active": true,
|
|
953
|
+
"email_address": "david@example.com",
|
|
954
|
+
"created_at": "2025-12-05T19:36:35.401Z",
|
|
955
|
+
"url": "http://fizzy.localhost:3006/897362094/users/03f5v9zjw7pz8717a4no1h8a7"
|
|
956
|
+
},
|
|
957
|
+
"url": "http://fizzy.localhost:3006/897362094/cards/3/reactions/03f5v9zo9qlcwwpyc0ascnikz"
|
|
958
|
+
}
|
|
959
|
+
]
|
|
960
|
+
```
|
|
961
|
+
|
|
962
|
+
### `POST /:account_slug/cards/:card_number/reactions`
|
|
963
|
+
|
|
964
|
+
Adds a reaction (boost) to a card.
|
|
965
|
+
|
|
966
|
+
| Parameter | Type | Required | Description |
|
|
967
|
+
|-----------|------|----------|-------------|
|
|
968
|
+
| `content` | string | Yes | The reaction text (max 16 characters) |
|
|
969
|
+
|
|
970
|
+
__Request:__
|
|
971
|
+
|
|
972
|
+
```json
|
|
973
|
+
{
|
|
974
|
+
"reaction": {
|
|
975
|
+
"content": "Great 👍"
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
```
|
|
979
|
+
|
|
980
|
+
__Response:__
|
|
981
|
+
|
|
982
|
+
Returns `201 Created` on success.
|
|
983
|
+
|
|
984
|
+
### `DELETE /:account_slug/cards/:card_number/reactions/:reaction_id`
|
|
985
|
+
|
|
986
|
+
Removes your reaction from a card. Only the reaction creator can remove their own reactions.
|
|
987
|
+
|
|
988
|
+
__Response:__
|
|
989
|
+
|
|
990
|
+
Returns `204 No Content` on success.
|
|
991
|
+
|
|
992
|
+
## Comment Reactions
|
|
932
993
|
|
|
933
994
|
Reactions are short (16-character max) responses to comments.
|
|
934
995
|
|
package/internal/api/cards.go
CHANGED
|
@@ -14,14 +14,48 @@ func (c *Client) GetCards(ctx context.Context, filters CardFilters) ([]Card, err
|
|
|
14
14
|
return nil, fmt.Errorf("failed to create get cards request: %w", err)
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
17
|
+
q := req.URL.Query()
|
|
18
|
+
|
|
19
|
+
for _, boardID := range filters.BoardIDs {
|
|
20
|
+
q.Add("board_ids[]", boardID)
|
|
21
|
+
}
|
|
22
|
+
for _, tagID := range filters.TagIDs {
|
|
23
|
+
q.Add("tag_ids[]", tagID)
|
|
24
|
+
}
|
|
25
|
+
for _, assigneeID := range filters.AssigneeIDs {
|
|
26
|
+
q.Add("assignee_ids[]", assigneeID)
|
|
27
|
+
}
|
|
28
|
+
for _, creatorID := range filters.CreatorIDs {
|
|
29
|
+
q.Add("creator_ids[]", creatorID)
|
|
30
|
+
}
|
|
31
|
+
for _, closerID := range filters.CloserIDs {
|
|
32
|
+
q.Add("closer_ids[]", closerID)
|
|
33
|
+
}
|
|
34
|
+
for _, cardID := range filters.CardIDs {
|
|
35
|
+
q.Add("card_ids[]", cardID)
|
|
36
|
+
}
|
|
37
|
+
for _, term := range filters.Terms {
|
|
38
|
+
q.Add("terms[]", term)
|
|
23
39
|
}
|
|
24
40
|
|
|
41
|
+
if filters.IndexedBy != "" {
|
|
42
|
+
q.Set("indexed_by", filters.IndexedBy)
|
|
43
|
+
}
|
|
44
|
+
if filters.SortedBy != "" {
|
|
45
|
+
q.Set("sorted_by", filters.SortedBy)
|
|
46
|
+
}
|
|
47
|
+
if filters.AssignmentStatus != "" {
|
|
48
|
+
q.Set("assignment_status", filters.AssignmentStatus)
|
|
49
|
+
}
|
|
50
|
+
if filters.CreationStatus != "" {
|
|
51
|
+
q.Set("creation", filters.CreationStatus)
|
|
52
|
+
}
|
|
53
|
+
if filters.ClosureStatus != "" {
|
|
54
|
+
q.Set("closure", filters.ClosureStatus)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
req.URL.RawQuery = q.Encode()
|
|
58
|
+
|
|
25
59
|
var response []Card
|
|
26
60
|
_, err = c.decodeResponse(req, &response)
|
|
27
61
|
if err != nil {
|
|
@@ -67,3 +67,64 @@ func (c *Client) DeleteCommentReaction(ctx context.Context, cardNumber int, comm
|
|
|
67
67
|
|
|
68
68
|
return true, nil
|
|
69
69
|
}
|
|
70
|
+
|
|
71
|
+
func (c *Client) GetCardReactions(ctx context.Context, cardNumber int) ([]Reaction, error) {
|
|
72
|
+
endpointURL := fmt.Sprintf("%s/cards/%d/reactions", c.AccountBaseURL, cardNumber)
|
|
73
|
+
|
|
74
|
+
req, err := c.newRequest(ctx, http.MethodGet, endpointURL, nil)
|
|
75
|
+
if err != nil {
|
|
76
|
+
return nil, fmt.Errorf("failed to create get card reactions request: %w", err)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
var response []Reaction
|
|
80
|
+
_, err = c.decodeResponse(req, &response)
|
|
81
|
+
if err != nil {
|
|
82
|
+
return nil, err
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return response, nil
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
func (c *Client) PostCardReaction(ctx context.Context, cardNumber int, content string) (*Reaction, error) {
|
|
89
|
+
endpointURL := fmt.Sprintf("%s/cards/%d/reactions", c.AccountBaseURL, cardNumber)
|
|
90
|
+
|
|
91
|
+
payload := map[string]map[string]string{
|
|
92
|
+
"reaction": {"content": content},
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
req, err := c.newRequest(ctx, http.MethodPost, endpointURL, payload)
|
|
96
|
+
if err != nil {
|
|
97
|
+
return nil, fmt.Errorf("failed to create post card reaction request: %w", err)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
res, err := c.HTTPClient.Do(req)
|
|
101
|
+
if err != nil {
|
|
102
|
+
return nil, fmt.Errorf("failed to make request: %w", err)
|
|
103
|
+
}
|
|
104
|
+
defer res.Body.Close()
|
|
105
|
+
|
|
106
|
+
if res.StatusCode != http.StatusCreated {
|
|
107
|
+
body, _ := io.ReadAll(res.Body)
|
|
108
|
+
return nil, fmt.Errorf("unexpected status code %d: %s", res.StatusCode, string(body))
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// API returns 201 Created with Location header, not a response body
|
|
112
|
+
// Return a Reaction object with the content field set for reference
|
|
113
|
+
return &Reaction{Content: content}, nil
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
func (c *Client) DeleteCardReaction(ctx context.Context, cardNumber int, reactionID string) (bool, error) {
|
|
117
|
+
endpointURL := fmt.Sprintf("%s/cards/%d/reactions/%s", c.AccountBaseURL, cardNumber, reactionID)
|
|
118
|
+
|
|
119
|
+
req, err := c.newRequest(ctx, http.MethodDelete, endpointURL, nil)
|
|
120
|
+
if err != nil {
|
|
121
|
+
return false, fmt.Errorf("failed to create delete card reaction request: %w", err)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
_, err = c.decodeResponse(req, nil, http.StatusNoContent)
|
|
125
|
+
if err != nil {
|
|
126
|
+
return false, err
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return true, nil
|
|
130
|
+
}
|
package/internal/api/types.go
CHANGED
|
@@ -121,6 +121,11 @@ type User struct {
|
|
|
121
121
|
URL string `json:"url"`
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
+
type UpdateUserPayload struct {
|
|
125
|
+
Name string `json:"name,omitempty"`
|
|
126
|
+
Avatar string `json:"avatar,omitempty"`
|
|
127
|
+
}
|
|
128
|
+
|
|
124
129
|
type Notification struct {
|
|
125
130
|
ID string `json:"id"`
|
|
126
131
|
Read bool `json:"read"`
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
package api
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"fmt"
|
|
6
|
+
"net/http"
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
func (c *Client) GetUsers(ctx context.Context) ([]User, error) {
|
|
10
|
+
endpointURL := c.AccountBaseURL + "/users"
|
|
11
|
+
|
|
12
|
+
req, err := c.newRequest(ctx, http.MethodGet, endpointURL, nil)
|
|
13
|
+
if err != nil {
|
|
14
|
+
return nil, fmt.Errorf("failed to create request: %w", err)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
var response []User
|
|
18
|
+
_, err = c.decodeResponse(req, &response)
|
|
19
|
+
if err != nil {
|
|
20
|
+
return nil, err
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return response, nil
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
func (c *Client) GetUser(ctx context.Context, userID string) (*User, error) {
|
|
27
|
+
endpointURL := c.AccountBaseURL + "/users/" + userID
|
|
28
|
+
|
|
29
|
+
req, err := c.newRequest(ctx, http.MethodGet, endpointURL, nil)
|
|
30
|
+
if err != nil {
|
|
31
|
+
return nil, fmt.Errorf("failed to create request: %w", err)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
var response User
|
|
35
|
+
_, err = c.decodeResponse(req, &response)
|
|
36
|
+
if err != nil {
|
|
37
|
+
return nil, err
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return &response, nil
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
func (c *Client) PutUser(ctx context.Context, userID string, payload UpdateUserPayload) error {
|
|
44
|
+
endpointURL := c.AccountBaseURL + "/users/" + userID
|
|
45
|
+
|
|
46
|
+
body := map[string]UpdateUserPayload{"user": payload}
|
|
47
|
+
|
|
48
|
+
req, err := c.newRequest(ctx, http.MethodPut, endpointURL, body)
|
|
49
|
+
if err != nil {
|
|
50
|
+
return fmt.Errorf("failed to create update user request: %w", err)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
_, err = c.decodeResponse(req, nil, http.StatusNoContent)
|
|
54
|
+
if err != nil {
|
|
55
|
+
return err
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return nil
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
func (c *Client) DeleteUser(ctx context.Context, userID string) error {
|
|
62
|
+
endpointURL := c.AccountBaseURL + "/users/" + userID
|
|
63
|
+
|
|
64
|
+
req, err := c.newRequest(ctx, http.MethodDelete, endpointURL, nil)
|
|
65
|
+
if err != nil {
|
|
66
|
+
return fmt.Errorf("failed to create delete user request: %w", err)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
_, err = c.decodeResponse(req, nil, http.StatusNoContent)
|
|
70
|
+
if err != nil {
|
|
71
|
+
return err
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return nil
|
|
75
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
package ui
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"fmt"
|
|
5
|
+
"io"
|
|
6
|
+
|
|
7
|
+
"github.com/rogeriopvl/fizzy/internal/api"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
func DisplayUsers(w io.Writer, users []api.User) error {
|
|
11
|
+
for _, user := range users {
|
|
12
|
+
status := "active"
|
|
13
|
+
if !user.Active {
|
|
14
|
+
status = "inactive"
|
|
15
|
+
}
|
|
16
|
+
fmt.Fprintf(w, "%s (%s) - %s [%s]\n", user.Name, user.Email, DisplayID(user.ID), status)
|
|
17
|
+
}
|
|
18
|
+
return nil
|
|
19
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
package ui
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"fmt"
|
|
5
|
+
"io"
|
|
6
|
+
|
|
7
|
+
"github.com/rogeriopvl/fizzy/internal/api"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
func DisplayUser(w io.Writer, user *api.User) error {
|
|
11
|
+
status := "active"
|
|
12
|
+
if !user.Active {
|
|
13
|
+
status = "inactive"
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
fmt.Fprintf(w, "User: %s\n", user.Name)
|
|
17
|
+
fmt.Fprintf(w, "ID: %s\n", user.ID)
|
|
18
|
+
fmt.Fprintf(w, "Email: %s\n", user.Email)
|
|
19
|
+
fmt.Fprintf(w, "Role: %s\n", user.Role)
|
|
20
|
+
fmt.Fprintf(w, "Status: %s\n", status)
|
|
21
|
+
fmt.Fprintf(w, "Created At: %s\n", FormatTime(user.CreatedAt))
|
|
22
|
+
return nil
|
|
23
|
+
}
|