Merge pull request #2367 from basecamp/pins-api

Pins API
This commit is contained in:
Denis Svara
2026-01-27 15:52:50 +01:00
committed by GitHub
6 changed files with 128 additions and 3 deletions
+10 -2
View File
@@ -9,14 +9,22 @@ class Cards::PinsController < ApplicationController
@pin = @card.pin_by Current.user
broadcast_add_pin_to_tray
render_pin_button_replacement
respond_to do |format|
format.turbo_stream { render_pin_button_replacement }
format.json { head :no_content }
end
end
def destroy
@pin = @card.unpin_by Current.user
broadcast_remove_pin_from_tray
render_pin_button_replacement
respond_to do |format|
format.turbo_stream { render_pin_button_replacement }
format.json { head :no_content }
end
end
private
+10 -1
View File
@@ -1,6 +1,15 @@
class My::PinsController < ApplicationController
def index
@pins = Current.user.pins.includes(:card).ordered.limit(20)
@pins = user_pins
fresh_when etag: [ @pins, @pins.collect(&:card) ]
end
private
def user_pins
Current.user.pins.includes(:card).ordered.limit(pins_limit)
end
def pins_limit
request.format.json? ? 100 : 20
end
end
+3
View File
@@ -0,0 +1,3 @@
json.array! @pins do |pin|
json.partial! "cards/card", card: pin.card
end
+73
View File
@@ -805,6 +805,79 @@ __Response:__
Returns `204 No Content` on success.
## Pins
Pins let users keep quick access to important cards.
### `POST /:account_slug/cards/:card_number/pin`
Pins a card for the current user.
__Response:__
Returns `204 No Content` on success.
### `DELETE /:account_slug/cards/:card_number/pin`
Unpins a card for the current user.
__Response:__
Returns `204 No Content` on success.
### `GET /my/pins`
Returns the current user's pinned cards. This endpoint is not paginated and returns up to 100 cards.
__Response:__
```json
[
{
"id": "03f5vaeq985jlvwv3arl4srq2",
"number": 1,
"title": "First!",
"status": "published",
"description": "Hello, World!",
"description_html": "<div class=\"action-text-content\"><p>Hello, World!</p></div>",
"image_url": null,
"tags": ["programming"],
"golden": false,
"last_active_at": "2025-12-05T19:38:48.553Z",
"created_at": "2025-12-05T19:38:48.540Z",
"url": "http://fizzy.localhost:3006/897362094/cards/4",
"board": {
"id": "03f5v9zkft4hj9qq0lsn9ohcm",
"name": "Fizzy",
"all_access": true,
"created_at": "2025-12-05T19:36:35.534Z",
"url": "http://fizzy.localhost:3006/897362094/boards/03f5v9zkft4hj9qq0lsn9ohcm",
"creator": {
"id": "03f5v9zjw7pz8717a4no1h8a7",
"name": "David Heinemeier Hansson",
"role": "owner",
"active": true,
"email_address": "david@example.com",
"created_at": "2025-12-05T19:36:35.401Z",
"url": "http://fizzy.localhost:3006/897362094/users/03f5v9zjw7pz8717a4no1h8a7",
"avatar_url": "http://fizzy.localhost:3006/897362094/users/03f5v9zjw7pz8717a4no1h8a7/avatar"
}
},
"creator": {
"id": "03f5v9zjw7pz8717a4no1h8a7",
"name": "David Heinemeier Hansson",
"role": "owner",
"active": true,
"email_address": "david@example.com",
"created_at": "2025-12-05T19:36:35.401Z",
"url": "http://fizzy.localhost:3006/897362094/users/03f5v9zjw7pz8717a4no1h8a7",
"avatar_url": "http://fizzy.localhost:3006/897362094/users/03f5v9zjw7pz8717a4no1h8a7/avatar"
},
"comments_url": "http://fizzy.localhost:3006/897362094/cards/4/comments"
}
]
```
## Comments
Comments are attached to cards and support rich text.
@@ -17,6 +17,17 @@ class Cards::PinsControllerTest < ActionDispatch::IntegrationTest
assert_response :success
end
test "create as JSON" do
card = cards(:layout)
assert_not card.pinned_by?(users(:kevin))
post card_pin_path(card), as: :json
assert_response :no_content
assert card.reload.pinned_by?(users(:kevin))
end
test "destroy" do
assert_changes -> { cards(:shipping).pinned_by?(users(:kevin)) }, from: true, to: false do
perform_enqueued_jobs do
@@ -28,4 +39,15 @@ class Cards::PinsControllerTest < ActionDispatch::IntegrationTest
assert_response :success
end
test "destroy as JSON" do
card = cards(:shipping)
assert card.pinned_by?(users(:kevin))
delete card_pin_path(card), as: :json
assert_response :no_content
assert_not card.reload.pinned_by?(users(:kevin))
end
end
@@ -11,4 +11,14 @@ class My::PinsControllerTest < ActionDispatch::IntegrationTest
assert_response :success
assert_select "div", text: /#{users(:kevin).pins.first.card.title}/
end
test "index as JSON" do
expected_ids = users(:kevin).pins.ordered.pluck(:card_id)
get my_pins_path(format: :json)
assert_response :success
assert_equal expected_ids.count, @response.parsed_body.count
assert_equal expected_ids, @response.parsed_body.map { |card| card["id"] }
end
end