diff --git a/app/controllers/boards/columns/cards_controller.rb b/app/controllers/boards/columns/cards_controller.rb
new file mode 100644
index 000000000..ae6602f14
--- /dev/null
+++ b/app/controllers/boards/columns/cards_controller.rb
@@ -0,0 +1,19 @@
+class Boards::Columns::CardsController < ApplicationController
+ include BoardScoped
+
+ before_action :set_column
+
+ def index
+ set_page_and_extract_portion_from @column.cards.active.latest.with_golden_first.preloaded
+ fresh_when etag: @page.records
+
+ respond_to do |format|
+ format.json
+ end
+ end
+
+ private
+ def set_column
+ @column = @board.columns.find(params[:column_id])
+ end
+end
diff --git a/app/views/boards/columns/cards/index.json.jbuilder b/app/views/boards/columns/cards/index.json.jbuilder
new file mode 100644
index 000000000..c1dc1dff1
--- /dev/null
+++ b/app/views/boards/columns/cards/index.json.jbuilder
@@ -0,0 +1 @@
+json.array! @page.records, partial: "cards/card", as: :card
diff --git a/app/views/columns/_column.json.jbuilder b/app/views/columns/_column.json.jbuilder
index 5bb56bdd6..16f5604d0 100644
--- a/app/views/columns/_column.json.jbuilder
+++ b/app/views/columns/_column.json.jbuilder
@@ -1,4 +1,5 @@
json.cache! column do
json.(column, :id, :name, :color)
json.created_at column.created_at.utc
+ json.cards_url board_column_cards_url(column.board, column)
end
diff --git a/config/routes.rb b/config/routes.rb
index 4d5acf988..55262e78e 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -39,7 +39,11 @@ Rails.application.routes.draw do
resource :closed
end
- resources :columns
+ resources :columns do
+ scope module: :columns do
+ resources :cards, only: :index
+ end
+ end
end
resources :cards, only: :create
diff --git a/docs/api/sections/columns.md b/docs/api/sections/columns.md
index 73bd1c728..c4fc35d3b 100644
--- a/docs/api/sections/columns.md
+++ b/docs/api/sections/columns.md
@@ -14,20 +14,22 @@ __Response:__
"id": "03f5v9zkft4hj9qq0lsn9ohcm",
"name": "Recording",
"color": "var(--color-card-default)",
- "created_at": "2025-12-05T19:36:35.534Z"
+ "created_at": "2025-12-05T19:36:35.534Z",
+ "cards_url": "http://fizzy.localhost:3006/897362094/boards/03f5v9zkft4hj9qq0lsn9ohcm/columns/03f5v9zkft4hj9qq0lsn9ohcm/cards"
},
{
"id": "03f5v9zkft4hj9qq0lsn9ohcn",
"name": "Published",
"color": "var(--color-card-4)",
- "created_at": "2025-12-05T19:36:35.534Z"
+ "created_at": "2025-12-05T19:36:35.534Z",
+ "cards_url": "http://fizzy.localhost:3006/897362094/boards/03f5v9zkft4hj9qq0lsn9ohcm/columns/03f5v9zkft4hj9qq0lsn9ohcn/cards"
}
]
```
## `GET /:account_slug/boards/:board_id/columns/:column_id`
-Returns the specified column.
+Returns the specified column metadata.
__Response:__
@@ -36,10 +38,93 @@ __Response:__
"id": "03f5v9zkft4hj9qq0lsn9ohcm",
"name": "In Progress",
"color": "var(--color-card-default)",
- "created_at": "2025-12-05T19:36:35.534Z"
+ "created_at": "2025-12-05T19:36:35.534Z",
+ "cards_url": "http://fizzy.localhost:3006/897362094/boards/03f5v9zkft4hj9qq0lsn9ohcm/columns/03f5v9zkft4hj9qq0lsn9ohcm/cards"
}
```
+## `GET /:account_slug/boards/:board_id/columns/:column_id/cards`
+
+Returns a paginated list of open cards in the specified workflow column.
+This only includes cards triaged into that column. Cards in "Maybe?", "Not Now", and "Done"
+are not included.
+
+The response items have the same shape as `GET /:account_slug/cards`.
+
+__Response:__
+
+```json
+[
+ {
+ "id": "03f5vaeq985jlvwv3arl4srq2",
+ "number": 1,
+ "title": "First!",
+ "status": "published",
+ "description": "Hello, World!",
+ "description_html": "
",
+ "image_url": null,
+ "has_attachments": false,
+ "tags": ["programming"],
+ "closed": false,
+ "postponed": false,
+ "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",
+ "auto_postpone_period_in_days": 30,
+ "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"
+ }
+ },
+ "column": {
+ "id": "03f5v9zkft4hj9qq0lsn9ohcn",
+ "name": "In Progress",
+ "color": "var(--color-card-4)",
+ "created_at": "2025-12-05T19:36:35.534Z",
+ "cards_url": "http://fizzy.localhost:3006/897362094/boards/03f5v9zkft4hj9qq0lsn9ohcm/columns/03f5v9zkft4hj9qq0lsn9ohcn/cards"
+ },
+ "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"
+ },
+ "assignees": [
+ {
+ "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"
+ }
+ ],
+ "has_more_assignees": false,
+ "comments_url": "http://fizzy.localhost:3006/897362094/cards/4/comments",
+ "reactions_url": "http://fizzy.localhost:3006/897362094/cards/4/reactions"
+ }
+]
+```
+
## `POST /:account_slug/boards/:board_id/columns`
Creates a new column on the board.
diff --git a/test/controllers/boards/columns/cards_controller_test.rb b/test/controllers/boards/columns/cards_controller_test.rb
new file mode 100644
index 000000000..bb65bdfb5
--- /dev/null
+++ b/test/controllers/boards/columns/cards_controller_test.rb
@@ -0,0 +1,29 @@
+require "test_helper"
+
+class Boards::Columns::CardsControllerTest < ActionDispatch::IntegrationTest
+ setup do
+ sign_in_as :kevin
+ end
+
+ test "index as JSON" do
+ column = columns(:writebook_in_progress)
+
+ get board_column_cards_path(column.board, column), as: :json
+
+ assert_response :success
+ assert_kind_of Array, @response.parsed_body
+ assert_equal [ cards(:text).number ], @response.parsed_body.pluck("number")
+ assert_equal "1", response.headers["X-Total-Count"]
+ end
+
+ test "cannot access cards on board without access as JSON" do
+ board = boards(:private)
+ column = board.columns.create!(name: "Secret")
+
+ logout_and_sign_in_as :jason
+
+ get board_column_cards_path(board, column), as: :json
+
+ assert_response :not_found
+ end
+end
diff --git a/test/controllers/boards/columns_controller_test.rb b/test/controllers/boards/columns_controller_test.rb
index 9fef675f5..664e91ea1 100644
--- a/test/controllers/boards/columns_controller_test.rb
+++ b/test/controllers/boards/columns_controller_test.rb
@@ -55,6 +55,7 @@ class Boards::ColumnsControllerTest < ActionDispatch::IntegrationTest
assert_response :success
assert_equal board.columns.count, @response.parsed_body.count
+ assert_equal board_column_cards_url(board, board.columns.sorted.first), @response.parsed_body.first["cards_url"]
end
test "show as JSON" do
@@ -64,6 +65,7 @@ class Boards::ColumnsControllerTest < ActionDispatch::IntegrationTest
assert_response :success
assert_equal column.id, @response.parsed_body["id"]
+ assert_equal board_column_cards_url(column.board, column), @response.parsed_body["cards_url"]
end
test "create as JSON" do
@@ -76,6 +78,7 @@ class Boards::ColumnsControllerTest < ActionDispatch::IntegrationTest
assert_response :created
assert_equal board_column_path(board, Column.last, format: :json), @response.headers["Location"]
assert_equal "New Column", @response.parsed_body["name"]
+ assert_equal board_column_cards_url(board, Column.last), @response.parsed_body["cards_url"]
end
test "update as JSON" do