Merge pull request #1509 from basecamp/event-creator-tag

Fix event description in webhooks
This commit is contained in:
Jorge Manrubia
2025-11-06 13:57:45 +01:00
committed by GitHub
7 changed files with 189 additions and 66 deletions
-64
View File
@@ -55,70 +55,6 @@ module EventsHelper
tag.div class: "events__time-block", style: "grid-area: #{row}/#{col}", &
end
def event_action_sentence(event)
if event.action.comment_created?
comment_event_action_sentence(event)
else
card_event_action_sentence(event)
end
end
def comment_event_action_sentence(event)
"#{event_creator_name(event)} commented on #{event_card_title(event.eventable.card)}".html_safe
end
def event_creator_name(event)
tag.span data: { creator_id: event.creator.id } do
safe_join([
tag.span("You", data: { only_visible_to_you: true }),
tag.span(h(event.creator.name), data: { only_visible_to_others: true })
])
end
end
def card_event_action_sentence(event)
card = event.eventable
case event.action
when "card_assigned"
if event.assignees.include?(Current.user)
"#{event_creator_name(event)} will handle #{event_card_title(card)}"
else
"#{event_creator_name(event)} assigned #{h event.assignees.pluck(:name).to_sentence} to #{event_card_title(card)}"
end
when "card_unassigned"
"#{event_creator_name(event)} unassigned #{h(event.assignees.include?(Current.user) ? "yourself" : event.assignees.pluck(:name).to_sentence)} from #{event_card_title(card)}"
when "card_published"
"#{event_creator_name(event)} added #{event_card_title(card)}"
when "card_closed"
"#{event_creator_name(event)} moved #{event_card_title(card)} to “Done”"
when "card_reopened"
"#{event_creator_name(event)} reopened #{event_card_title(card)}"
when "card_postponed"
"#{event_creator_name(event)} moved #{event_card_title(card)} to “Not Now”"
when "card_auto_postponed"
"#{event_card_title(card)} was closed as “Not Now” due to inactivity"
when "card_resumed"
"#{event_creator_name(event)} resumed #{event_card_title(card)}"
when "card_title_changed"
"#{event_creator_name(event)} renamed #{event_card_title(card)} (was: “#{h event.particulars.dig('particulars', 'old_title')}”)"
when "card_board_changed", "card_collection_changed"
"#{event_creator_name(event)} moved #{event_card_title(card)} to “#{h (event.particulars.dig('particulars', 'new_board') || event.particulars.dig('particulars', 'new_collection'))}"
when "card_triaged"
"#{event_creator_name(event)} moved #{event_card_title(card)} to “#{h event.particulars.dig('particulars', 'column')}"
when "card_sent_back_to_triage"
"#{event_creator_name(event)} moved #{event_card_title(card)} back to “Maybe?”"
end&.html_safe
end
def event_card_title(card)
tag.span card.title, class: "txt-underline"
end
def event_due_date(event)
event.particulars.dig("particulars", "due_date").to_date.strftime("%B %-d")
end
def event_action_icon(event)
case event.action
when "card_assigned"
+4
View File
@@ -22,6 +22,10 @@ class Event < ApplicationRecord
eventable
end
def description_for(user)
Event::Description.new(self, user)
end
private
def dispatch_webhooks
Event::WebhookDispatchJob.perform_later(self)
+108
View File
@@ -0,0 +1,108 @@
class Event::Description
include ActionView::Helpers::TagHelper
include ERB::Util
attr_reader :event, :user
def initialize(event, user)
@event = event
@user = user
end
def to_html
to_sentence(creator_tag, card_title_tag).html_safe
end
def to_plain_text
to_sentence(creator_text, card.title)
end
private
def to_sentence(creator, card_title)
if event.action.comment_created?
comment_sentence(creator, card_title)
else
action_sentence(creator, card_title)
end
end
def creator_tag
tag.span data: { creator_id: event.creator.id } do
tag.span("You", data: { only_visible_to_you: true }) +
tag.span(event.creator.name, data: { only_visible_to_others: true })
end
end
def card_title_tag
tag.span card.title, class: "txt-underline"
end
def creator_text
event.creator == user ? "You" : h(event.creator.name)
end
def card
@card ||= event.action.comment_created? ? event.eventable.card : event.eventable
end
def comment_sentence(creator, card_title)
"#{creator} commented on #{card_title}"
end
def action_sentence(creator, card_title)
case event.action
when "card_assigned"
assigned_sentence(creator, card_title)
when "card_unassigned"
unassigned_sentence(creator, card_title)
when "card_published"
"#{creator} added #{card_title}"
when "card_closed"
%(#{creator} moved #{card_title} to "Done")
when "card_reopened"
"#{creator} reopened #{card_title}"
when "card_postponed"
%(#{creator} moved #{card_title} to "Not Now")
when "card_auto_postponed"
%(#{card_title} was closed as "Not Now" due to inactivity)
when "card_resumed"
"#{creator} resumed #{card_title}"
when "card_title_changed"
renamed_sentence(creator, card_title)
when "card_board_changed", "card_collection_changed"
moved_sentence(creator, card_title)
when "card_triaged"
triaged_sentence(creator, card_title)
when "card_sent_back_to_triage"
%(#{creator} moved #{card_title} back to "Maybe?")
end
end
def assigned_sentence(creator, card_title)
if event.assignees.include?(user)
"#{creator} will handle #{card_title}"
else
"#{creator} assigned #{h event.assignees.pluck(:name).to_sentence} to #{card_title}"
end
end
def unassigned_sentence(creator, card_title)
assignees_text = event.assignees.include?(user) ? "yourself" : event.assignees.pluck(:name).to_sentence
"#{creator} unassigned #{h(assignees_text)} from #{card_title}"
end
def renamed_sentence(creator, card_title)
old_title = event.particulars.dig("particulars", "old_title")
%(#{creator} renamed #{card_title} (was: "#{h old_title}"))
end
def moved_sentence(creator, card_title)
new_location = event.particulars.dig("particulars", "new_board") || event.particulars.dig("particulars", "new_collection")
%(#{creator} moved #{card_title} to "#{h new_location}")
end
def triaged_sentence(creator, card_title)
column = event.particulars.dig("particulars", "column")
%(#{creator} moved #{card_title} to "#{h column}")
end
end
+1 -1
View File
@@ -32,7 +32,7 @@
<div class="flex flex-column min-width txt-small align-start txt-align-start" style="color: color-mix(in srgb, var(--card-color) 40%, var(--color-ink));">
<strong class="event__title txt-break overflow-line-clamp txt-tight-lines">
<%= event_action_sentence(event) %>
<%= event.description_for(Current.user).to_html %>
</strong>
<%= yield %>
+1 -1
View File
@@ -1,4 +1,4 @@
<%= event_action_sentence(@event) %>
<%= @event.description_for(Current.user).to_plain_text %>
<% if @event.eventable %>
<%= link_to "↗︎", polymorphic_url(@event.eventable) %>
<% end %>
+35
View File
@@ -0,0 +1,35 @@
require "test_helper"
class Event::DescriptionTest < ActiveSupport::TestCase
test "generates html description for card published event" do
description = events(:logo_published).description_for(users(:david))
assert_includes description.to_html, "added"
assert_includes description.to_html, "logo"
end
test "generates plain text description for card published event" do
description = events(:logo_published).description_for(users(:david))
assert_includes description.to_plain_text, "You added"
assert_includes description.to_plain_text, "logo"
end
test "generates description for comment event" do
description = events(:layout_commented).description_for(users(:jz))
assert_includes description.to_plain_text, "David commented on"
end
test "uses 'You' when event creator is the current user" do
description = events(:logo_published).description_for(users(:david))
assert_includes description.to_plain_text, "You added"
end
test "uses creator name when event creator is not the current user" do
description = events(:logo_published).description_for(users(:jz))
assert_includes description.to_plain_text, "David added"
end
end
+40
View File
@@ -218,4 +218,44 @@ class Webhook::DeliveryTest < ActiveSupport::TestCase
assert Webhook::Delivery.exists?(fresh_delivery.id)
assert_not Webhook::Delivery.exists?(stale_delivery.id)
end
test "renders 'You' when event creator is current user" do
webhook = Webhook.create!(
board: boards(:writebook),
name: "Basecamp",
url: "https://3.basecamp.com/123/integrations/webhook/buckets/456/chats/789/lines"
)
event = events(:logo_published)
delivery = Webhook::Delivery.create!(webhook: webhook, event: event)
Current.session = sessions(:david)
request_stub = stub_request(:post, webhook.url)
.with { |request| CGI.parse(request.body)["content"].first.include?("You added") }
.to_return(status: 200)
delivery.deliver
assert_requested request_stub
end
test "renders creator name when event creator is not current user" do
webhook = Webhook.create!(
board: boards(:writebook),
name: "Basecamp",
url: "https://3.basecamp.com/123/integrations/webhook/buckets/456/chats/789/lines"
)
event = events(:logo_published)
delivery = Webhook::Delivery.create!(webhook: webhook, event: event)
Current.session = sessions(:kevin)
request_stub = stub_request(:post, webhook.url)
.with { |request| CGI.parse(request.body)["content"].first.include?("David added") }
.to_return(status: 200)
delivery.deliver
assert_requested request_stub
end
end