Skip to content

Commit ec1b8f2

Browse files
authored
Merge pull request #2367 from basecamp/pins-api
Pins API
2 parents 0252734 + a75d56d commit ec1b8f2

File tree

6 files changed

+128
-3
lines changed

6 files changed

+128
-3
lines changed

app/controllers/cards/pins_controller.rb

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,22 @@ def create
99
@pin = @card.pin_by Current.user
1010

1111
broadcast_add_pin_to_tray
12-
render_pin_button_replacement
12+
13+
respond_to do |format|
14+
format.turbo_stream { render_pin_button_replacement }
15+
format.json { head :no_content }
16+
end
1317
end
1418

1519
def destroy
1620
@pin = @card.unpin_by Current.user
1721

1822
broadcast_remove_pin_from_tray
19-
render_pin_button_replacement
23+
24+
respond_to do |format|
25+
format.turbo_stream { render_pin_button_replacement }
26+
format.json { head :no_content }
27+
end
2028
end
2129

2230
private
Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
11
class My::PinsController < ApplicationController
22
def index
3-
@pins = Current.user.pins.includes(:card).ordered.limit(20)
3+
@pins = user_pins
44
fresh_when etag: [ @pins, @pins.collect(&:card) ]
55
end
6+
7+
private
8+
def user_pins
9+
Current.user.pins.includes(:card).ordered.limit(pins_limit)
10+
end
11+
12+
def pins_limit
13+
request.format.json? ? 100 : 20
14+
end
615
end
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
json.array! @pins do |pin|
2+
json.partial! "cards/card", card: pin.card
3+
end

docs/API.md

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -805,6 +805,79 @@ __Response:__
805805

806806
Returns `204 No Content` on success.
807807

808+
## Pins
809+
810+
Pins let users keep quick access to important cards.
811+
812+
### `POST /:account_slug/cards/:card_number/pin`
813+
814+
Pins a card for the current user.
815+
816+
__Response:__
817+
818+
Returns `204 No Content` on success.
819+
820+
### `DELETE /:account_slug/cards/:card_number/pin`
821+
822+
Unpins a card for the current user.
823+
824+
__Response:__
825+
826+
Returns `204 No Content` on success.
827+
828+
### `GET /my/pins`
829+
830+
Returns the current user's pinned cards. This endpoint is not paginated and returns up to 100 cards.
831+
832+
__Response:__
833+
834+
```json
835+
[
836+
{
837+
"id": "03f5vaeq985jlvwv3arl4srq2",
838+
"number": 1,
839+
"title": "First!",
840+
"status": "published",
841+
"description": "Hello, World!",
842+
"description_html": "<div class=\"action-text-content\"><p>Hello, World!</p></div>",
843+
"image_url": null,
844+
"tags": ["programming"],
845+
"golden": false,
846+
"last_active_at": "2025-12-05T19:38:48.553Z",
847+
"created_at": "2025-12-05T19:38:48.540Z",
848+
"url": "http://fizzy.localhost:3006/897362094/cards/4",
849+
"board": {
850+
"id": "03f5v9zkft4hj9qq0lsn9ohcm",
851+
"name": "Fizzy",
852+
"all_access": true,
853+
"created_at": "2025-12-05T19:36:35.534Z",
854+
"url": "http://fizzy.localhost:3006/897362094/boards/03f5v9zkft4hj9qq0lsn9ohcm",
855+
"creator": {
856+
"id": "03f5v9zjw7pz8717a4no1h8a7",
857+
"name": "David Heinemeier Hansson",
858+
"role": "owner",
859+
"active": true,
860+
"email_address": "david@example.com",
861+
"created_at": "2025-12-05T19:36:35.401Z",
862+
"url": "http://fizzy.localhost:3006/897362094/users/03f5v9zjw7pz8717a4no1h8a7",
863+
"avatar_url": "http://fizzy.localhost:3006/897362094/users/03f5v9zjw7pz8717a4no1h8a7/avatar"
864+
}
865+
},
866+
"creator": {
867+
"id": "03f5v9zjw7pz8717a4no1h8a7",
868+
"name": "David Heinemeier Hansson",
869+
"role": "owner",
870+
"active": true,
871+
"email_address": "david@example.com",
872+
"created_at": "2025-12-05T19:36:35.401Z",
873+
"url": "http://fizzy.localhost:3006/897362094/users/03f5v9zjw7pz8717a4no1h8a7",
874+
"avatar_url": "http://fizzy.localhost:3006/897362094/users/03f5v9zjw7pz8717a4no1h8a7/avatar"
875+
},
876+
"comments_url": "http://fizzy.localhost:3006/897362094/cards/4/comments"
877+
}
878+
]
879+
```
880+
808881
## Comments
809882

810883
Comments are attached to cards and support rich text.

test/controllers/cards/pins_controller_test.rb

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,17 @@ class Cards::PinsControllerTest < ActionDispatch::IntegrationTest
1717
assert_response :success
1818
end
1919

20+
test "create as JSON" do
21+
card = cards(:layout)
22+
23+
assert_not card.pinned_by?(users(:kevin))
24+
25+
post card_pin_path(card), as: :json
26+
27+
assert_response :no_content
28+
assert card.reload.pinned_by?(users(:kevin))
29+
end
30+
2031
test "destroy" do
2132
assert_changes -> { cards(:shipping).pinned_by?(users(:kevin)) }, from: true, to: false do
2233
perform_enqueued_jobs do
@@ -28,4 +39,15 @@ class Cards::PinsControllerTest < ActionDispatch::IntegrationTest
2839

2940
assert_response :success
3041
end
42+
43+
test "destroy as JSON" do
44+
card = cards(:shipping)
45+
46+
assert card.pinned_by?(users(:kevin))
47+
48+
delete card_pin_path(card), as: :json
49+
50+
assert_response :no_content
51+
assert_not card.reload.pinned_by?(users(:kevin))
52+
end
3153
end

test/controllers/my/pins_controller_test.rb

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,14 @@ class My::PinsControllerTest < ActionDispatch::IntegrationTest
1111
assert_response :success
1212
assert_select "div", text: /#{users(:kevin).pins.first.card.title}/
1313
end
14+
15+
test "index as JSON" do
16+
expected_ids = users(:kevin).pins.ordered.pluck(:card_id)
17+
18+
get my_pins_path(format: :json)
19+
20+
assert_response :success
21+
assert_equal expected_ids.count, @response.parsed_body.count
22+
assert_equal expected_ids, @response.parsed_body.map { |card| card["id"] }
23+
end
1424
end

0 commit comments

Comments
 (0)