diff --git a/app/controllers/cards/pins_controller.rb b/app/controllers/cards/pins_controller.rb index f8da07da31..d46c6a48df 100644 --- a/app/controllers/cards/pins_controller.rb +++ b/app/controllers/cards/pins_controller.rb @@ -9,14 +9,22 @@ def create @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 diff --git a/app/controllers/my/pins_controller.rb b/app/controllers/my/pins_controller.rb index 4468c75d5c..18b58910f2 100644 --- a/app/controllers/my/pins_controller.rb +++ b/app/controllers/my/pins_controller.rb @@ -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 diff --git a/app/views/my/pins/index.json.jbuilder b/app/views/my/pins/index.json.jbuilder new file mode 100644 index 0000000000..f3a39ae845 --- /dev/null +++ b/app/views/my/pins/index.json.jbuilder @@ -0,0 +1,3 @@ +json.array! @pins do |pin| + json.partial! "cards/card", card: pin.card +end diff --git a/docs/API.md b/docs/API.md index f7bdacb306..3a68c46dde 100644 --- a/docs/API.md +++ b/docs/API.md @@ -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": "

Hello, World!

", + "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. diff --git a/test/controllers/cards/pins_controller_test.rb b/test/controllers/cards/pins_controller_test.rb index 3bc5698277..db26a12e71 100644 --- a/test/controllers/cards/pins_controller_test.rb +++ b/test/controllers/cards/pins_controller_test.rb @@ -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 diff --git a/test/controllers/my/pins_controller_test.rb b/test/controllers/my/pins_controller_test.rb index 3c8ca33e22..daff963549 100644 --- a/test/controllers/my/pins_controller_test.rb +++ b/test/controllers/my/pins_controller_test.rb @@ -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