From 1d8b765bab4f14b40a599cc639c06669c59094a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denis=20=C5=A0vara?= Date: Wed, 14 Jan 2026 17:10:54 +0100 Subject: [PATCH 1/5] Add JSON support for pin/unpin on the card endpoints and add tests. --- app/controllers/cards/pins_controller.rb | 12 ++++++++-- .../controllers/cards/pins_controller_test.rb | 22 +++++++++++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/app/controllers/cards/pins_controller.rb b/app/controllers/cards/pins_controller.rb index f8da07da3..d46c6a48d 100644 --- a/app/controllers/cards/pins_controller.rb +++ b/app/controllers/cards/pins_controller.rb @@ -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 diff --git a/test/controllers/cards/pins_controller_test.rb b/test/controllers/cards/pins_controller_test.rb index 3bc569827..db26a12e7 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 From e82dc08f6cfe7e1a114489283139fb06ae124157 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denis=20=C5=A0vara?= Date: Wed, 14 Jan 2026 17:43:16 +0100 Subject: [PATCH 2/5] Add JSON support for GET `/my/pins` with no pagination and tests. --- app/controllers/my/pins_controller.rb | 11 ++++++++++- app/views/my/pins/index.json.jbuilder | 3 +++ test/controllers/my/pins_controller_test.rb | 10 ++++++++++ 3 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 app/views/my/pins/index.json.jbuilder diff --git a/app/controllers/my/pins_controller.rb b/app/controllers/my/pins_controller.rb index 4468c75d5..93b77df79 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 = pins_for_request fresh_when etag: [ @pins, @pins.collect(&:card) ] end + + private + def pins_for_request + if request.format.json? + Current.user.pins.includes(:card).ordered + else + Current.user.pins.includes(:card).ordered.limit(20) + end + 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 000000000..f3a39ae84 --- /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/test/controllers/my/pins_controller_test.rb b/test/controllers/my/pins_controller_test.rb index 3c8ca33e2..daff96354 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 From 90a7b15db3116a3072e35d6686b17054ebcf7671 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denis=20=C5=A0vara?= Date: Tue, 20 Jan 2026 12:32:45 +0100 Subject: [PATCH 3/5] Move pins scope into a variable. Keep web limit to 20 and set JSON to 100. --- app/controllers/my/pins_controller.rb | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/controllers/my/pins_controller.rb b/app/controllers/my/pins_controller.rb index 93b77df79..70e40ce98 100644 --- a/app/controllers/my/pins_controller.rb +++ b/app/controllers/my/pins_controller.rb @@ -6,10 +6,12 @@ class My::PinsController < ApplicationController private def pins_for_request + pins = Current.user.pins.includes(:card).ordered + if request.format.json? - Current.user.pins.includes(:card).ordered + pins.limit(100) else - Current.user.pins.includes(:card).ordered.limit(20) + pins.limit(20) end end end From 5384116f11862a017ebe1083d0597c6a7d83ebbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denis=20=C5=A0vara?= Date: Thu, 22 Jan 2026 16:44:30 +0100 Subject: [PATCH 4/5] Rename the helper to user_pins and extract a pins_limit method. --- app/controllers/my/pins_controller.rb | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/app/controllers/my/pins_controller.rb b/app/controllers/my/pins_controller.rb index 70e40ce98..18b58910f 100644 --- a/app/controllers/my/pins_controller.rb +++ b/app/controllers/my/pins_controller.rb @@ -1,17 +1,15 @@ class My::PinsController < ApplicationController def index - @pins = pins_for_request + @pins = user_pins fresh_when etag: [ @pins, @pins.collect(&:card) ] end private - def pins_for_request - pins = Current.user.pins.includes(:card).ordered + def user_pins + Current.user.pins.includes(:card).ordered.limit(pins_limit) + end - if request.format.json? - pins.limit(100) - else - pins.limit(20) - end + def pins_limit + request.format.json? ? 100 : 20 end end From 8e24988c6f4ac76fa882bf87ad5962ed304ced3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denis=20=C5=A0vara?= Date: Tue, 27 Jan 2026 15:37:07 +0100 Subject: [PATCH 5/5] Add a new Pins section to docs/API.md covering pin/unpin and the pinned cards list response. --- docs/API.md | 73 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/docs/API.md b/docs/API.md index bd4488127..9de1df786 100644 --- a/docs/API.md +++ b/docs/API.md @@ -803,6 +803,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.