Merge pull request #2398 from basecamp/notifications-tray-api

Add JSON response support for `notifications/tray`
This commit is contained in:
Alp Keser
2026-02-18 13:19:59 +03:00
committed by GitHub
10 changed files with 68 additions and 4 deletions
@@ -1,9 +1,27 @@
class Notifications::TraysController < ApplicationController
MAX_ENTRIES_LIMIT = 100
def show
@notifications = Current.user.notifications.unread.preloaded.ordered.limit(100)
@notifications = unread_notifications
if include_read?
@notifications += read_notifications
end
# Invalidate on the whole set instead of the unread set since the max updated at in the unread set
# can stay the same when reading old notifications.
fresh_when Current.user.notifications
fresh_when etag: [ Current.user.notifications, include_read? ]
end
private
def unread_notifications
Current.user.notifications.unread.preloaded.ordered.limit(MAX_ENTRIES_LIMIT)
end
def read_notifications
Current.user.notifications.read.preloaded.ordered.limit(MAX_ENTRIES_LIMIT)
end
def include_read?
ActiveModel::Type::Boolean.new.cast(params[:include_read])
end
end
+7 -1
View File
@@ -10,7 +10,13 @@ class Notification < ApplicationRecord
scope :unread, -> { where(read_at: nil) }
scope :read, -> { where.not(read_at: nil) }
scope :ordered, -> { order(read_at: :desc, updated_at: :desc) }
scope :preloaded, -> { preload(:card, :creator, :account, source: [ :board, :creator, { eventable: [ :closure, :board, :assignments ] } ]) }
scope :preloaded, -> {
preload(
:creator, :account,
card: [ :board, :column, :closure, :not_now ],
source: [ :board, :creator, { eventable: [ :closure, :board, :assignments ] } ]
)
}
before_validation :set_card
after_create :bundle
+1
View File
@@ -8,6 +8,7 @@ json.cache! card do
json.tags card.tags.pluck(:title).sort
json.closed card.closed?
json.postponed card.postponed?
json.golden card.golden?
json.last_active_at card.last_active_at.utc
json.created_at card.created_at.utc
@@ -3,14 +3,19 @@ json.cache! notification do
json.read notification.read?
json.read_at notification.read_at&.utc
json.created_at notification.created_at.utc
json.source_type notification.source_type.underscore
json.partial! "notifications/notification/#{notification.source_type.underscore}/body", notification: notification
json.creator notification.creator, partial: "users/user", as: :user
json.card do
json.(notification.card, :id, :title, :status)
json.(notification.card, :id, :number, :title, :status)
json.board_name notification.card.board.name
json.closed notification.card.closed?
json.postponed notification.card.postponed?
json.url card_url(notification.card)
json.column notification.card.column, partial: "columns/column", as: :column if notification.card.column
end
json.url notification_url(notification)
@@ -0,0 +1 @@
json.array! @notifications, partial: "notifications/notification", as: :notification
+1
View File
@@ -5,4 +5,5 @@ json.cache! user do
json.created_at user.created_at.utc
json.url user_url(user)
json.avatar_url user_avatar_url(user)
end
@@ -176,6 +176,7 @@ class CardsControllerTest < ActionDispatch::IntegrationTest
assert_equal card.title, @response.parsed_body["title"]
assert_equal card.closed?, @response.parsed_body["closed"]
assert_equal card.postponed?, @response.parsed_body["postponed"]
assert_equal 2, @response.parsed_body["steps"].size
assert_equal card_comments_url(card), @response.parsed_body["comments_url"]
assert_equal card_reactions_url(card), @response.parsed_body["reactions_url"]
@@ -11,4 +11,24 @@ class Notifications::TraysControllerTest < ActionDispatch::IntegrationTest
assert_response :success
assert_select "div", text: /Layout is broken/
end
test "show as JSON" do
expected_ids = users(:kevin).notifications.unread.ordered.limit(100).pluck(:id)
get tray_notifications_path(format: :json)
assert_response :success
assert_equal expected_ids, @response.parsed_body.map { |s| s["id"] }
end
test "show as JSON with include_read includes read notifications" do
notifications = users(:kevin).notifications
expected_ids = notifications.unread.ordered.limit(100).pluck(:id) +
notifications.read.ordered.limit(100).pluck(:id)
get tray_notifications_path(format: :json, include_read: true)
assert_response :success
assert_equal expected_ids, @response.parsed_body.map { |s| s["id"] }
end
end
@@ -22,5 +22,13 @@ class NotificationsControllerTest < ActionDispatch::IntegrationTest
assert_not_nil notification["card"]
assert_not_nil notification["creator"]
assert_not_nil notification["unread_count"]
assert_not_nil notification.dig("creator", "avatar_url")
assert_not_nil notification.dig("card", "number")
assert_not_nil notification.dig("card", "board_name")
assert_not_nil notification.dig("card", "column")
card = notifications(:logo_assignment_kevin).card
assert_equal card.closed?, notification.dig("card", "closed")
assert_equal card.postponed?, notification.dig("card", "postponed")
end
end
+3
View File
@@ -5,6 +5,7 @@ logo_assignment_kevin:
card: logo_uuid
unread_count: 2
created_at: <%= 1.week.ago %>
updated_at: <%= 1.week.ago + 1.second %>
creator: david_uuid
account: 37s_uuid
@@ -15,6 +16,7 @@ layout_commented_kevin:
card: layout_uuid
unread_count: 1
created_at: <%= 1.week.ago %>
updated_at: <%= 1.week.ago + 2.seconds %>
creator: david_uuid
account: 37s_uuid
@@ -25,5 +27,6 @@ logo_mentioned_david:
card: logo_uuid
unread_count: 2
created_at: <%= 1.week.ago %>
updated_at: <%= 1.week.ago + 3.seconds %>
creator: david_uuid
account: 37s_uuid