Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@ end
- `ModelDeduper` — Deduplication logic
- `RichTextMigrator` — Rich text migration utility
- `DisplayImagePresenter` — Image display logic
- `RelatedComments` — Gathers all comments related to a commentable (type-aware: registrant/user/active orgs, etc.) for the comments overview page

### Event Registrations

Expand Down
53 changes: 38 additions & 15 deletions app/controllers/comments_controller.rb
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
class CommentsController < ApplicationController
before_action :set_commentable
before_action :require_commentable, only: :create

def index
authorize!
@comments = @commentable.comments.newest_first.paginate(page: params[:page], per_page: 10)

respond_to do |format|
format.html { render partial: "comments/list", locals: { commentable: @commentable, comments: @comments } }
format.html do
if turbo_frame_request? && @commentable
@comments = @commentable.comments.newest_first.paginate(page: params[:page], per_page: 10)
render partial: "comments/list", locals: { commentable: @commentable, comments: @comments }
else
@comments = index_comments.paginate(page: params[:page], per_page: 10)
end
end
end
end

Expand All @@ -33,20 +40,36 @@ def create

private

# Scoped to a commentable: every comment related to that record (see
# RelatedComments). Top-level /comments: a preview of every comment in the
# system alongside the record each is about.
def index_comments

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 From Claude: Three modes share this action: scoped related overview (full page), global /comments preview (no commentable), and the existing bare turbo-frame list for embeds/create — kept intact so the inline comment widgets don't change.

return all_comments unless @commentable

RelatedComments.new(@commentable).comments
end

def all_comments
Comment.includes(:commentable, created_by: :person, updated_by: :person).newest_first
end

def set_commentable
if params[:person_id]
@commentable = Person.find(params[:person_id])
elsif params[:user_id]
@commentable = User.find(params[:user_id])
elsif params[:organization_id]
@commentable = Organization.find(params[:organization_id])
elsif params[:event_registration_id]
@commentable = EventRegistration.find(params[:event_registration_id])
elsif params[:workshop_id]
@commentable = Workshop.find(params[:workshop_id])
else
redirect_to root_path, alert: "Invalid commentable resource"
end
@commentable =
if params[:person_id]
Person.find(params[:person_id])
elsif params[:user_id]
User.find(params[:user_id])
elsif params[:organization_id]
Organization.find(params[:organization_id])
elsif params[:event_registration_id]
EventRegistration.find(params[:event_registration_id])
elsif params[:workshop_id]
Workshop.find(params[:workshop_id])
end
end

def require_commentable
redirect_to root_path, alert: "Invalid commentable resource" unless @commentable
end

def comment_params
Expand Down
39 changes: 39 additions & 0 deletions app/helpers/comments_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
module CommentsHelper
# One-line description of what a record's related-comments overview pulls in,
# mirroring the logic in RelatedComments.
def related_comments_summary(commentable)
case commentable
when EventRegistration then "Includes the registrant, their user account, and active organizations."
when Person then "Includes their user account and active organizations."
when Organization then "Includes people with an active affiliation."
when User then "Includes the linked person and their active organizations."
when Workshop then "Includes the workshop's creator and their organizations."
else "Comments on this record."
end
end

# Maps a comment's polymorphic commentable to the label, name, link, and badge
# styling used in the "About" column of the full-page comments index.
def comment_subject(commentable)
case commentable
when EventRegistration
{ type: "Registration", name: commentable.event&.title || "Registration",
path: edit_event_registration_path(commentable), badge: "bg-blue-100 text-blue-800" }
when Person
{ type: "Person", name: commentable.full_name,
path: person_path(commentable), badge: "bg-green-100 text-green-800" }
when Organization
{ type: "Organization", name: commentable.name,
path: organization_path(commentable), badge: "bg-purple-100 text-purple-800" }
when User
{ type: "User account", name: commentable.person&.name || commentable.name.presence || commentable.email,
path: user_path(commentable), badge: "bg-amber-100 text-amber-800" }
when Workshop
{ type: "Workshop", name: commentable.try(:title).presence || commentable.try(:name) || "Workshop",
path: edit_workshop_path(commentable), badge: "bg-rose-100 text-rose-800" }
else
{ type: commentable.class.name, name: commentable.try(:name) || "##{commentable.id}",
path: nil, badge: "bg-gray-100 text-gray-800" }
end
end
end
70 changes: 70 additions & 0 deletions app/services/related_comments.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# Collects every comment "related" to a commentable, where the meaning of
# related depends on the commentable's type. The type is the signal of which
# comment box the user opened the overview from (each box links to its own
# polymorphic comments path), so this is the single place that decides what
# context to surface for each kind of record.
class RelatedComments
def initialize(commentable)
@commentable = commentable
end

def comments
commentable_groups
.map { |type, ids| Comment.where(commentable_type: type, commentable_id: ids) }
.reduce(:or)
.includes(:commentable, created_by: :person, updated_by: :person)
.newest_first
end

private

def commentable_groups
([ @commentable ] + related_records).compact.uniq
.group_by { |record| record.class.base_class.name }
.transform_values { |records| records.map(&:id).uniq }
end

def related_records

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 From Claude: The commentable's type is the only signal of which comment box the button was clicked from — each box links to its own polymorphic *_comments_path — so this case statement is the single place that defines "related" per type. Add a when to extend it.

case @commentable
when EventRegistration then registration_related
when Person then person_related
when Organization then organization_related
when User then user_related
when Workshop then workshop_related
else []
end
end

def registration_related
person = @commentable.registrant
return [] unless person

[ person, person.user, *active_organizations(person) ]
end

def person_related
[ @commentable.user, *active_organizations(@commentable) ]
end

def organization_related
@commentable.people.merge(Affiliation.active).distinct.to_a
end

def user_related
person = @commentable.person
return [] unless person

[ person, *active_organizations(person) ]
end

def workshop_related
creator = @commentable.created_by
return [] unless creator

[ creator, creator.person, *(creator.person ? active_organizations(creator.person) : []) ]
end

def active_organizations(person)
person.organizations.merge(Affiliation.active).distinct.to_a
end
end
6 changes: 6 additions & 0 deletions app/views/comments/_view_all_link.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<%= link_to polymorphic_path([ commentable, :comments ]),
class: "inline-flex items-center gap-1.5 text-xs font-medium text-gray-500 hover:text-gray-700 hover:underline whitespace-nowrap",
target: "_blank", rel: "noopener" do %>
View all related comments
<i class="fa-solid fa-arrow-up-right-from-square text-[0.6rem]"></i>
<% end %>
69 changes: 68 additions & 1 deletion app/views/comments/index.html.erb
Original file line number Diff line number Diff line change
@@ -1,2 +1,69 @@
<% content_for(:page_bg_class, "admin-only bg-blue-100") %>
<%= render "comments/list", commentable: @commentable, comments: @comments %>
<div class="max-w-6xl mx-auto">
<div class="mb-6">
<% if @commentable %>
<% subject = comment_subject(@commentable) %>
<% if subject[:path] %>
<%= link_to "← Back", subject[:path], class: "text-sm text-gray-500 hover:text-gray-700 mb-2 inline-block" %>
<% end %>
<h1 class="text-2xl font-semibold text-gray-900">Related comments</h1>
<p class="text-gray-600 mt-1">
<span class="inline-block <%= subject[:badge] %> text-xs px-2 py-0.5 rounded align-middle"><%= subject[:type] %></span>
<%= subject[:name] %>
</p>
<p class="text-sm text-gray-500 mt-1"><%= related_comments_summary(@commentable) %></p>
<% else %>
<h1 class="text-2xl font-semibold text-gray-900">All comments</h1>
<p class="text-sm text-gray-500 mt-1">Every comment in the system, with the record each one is about.</p>
<% end %>
</div>

<% if @comments.any? %>
<div class="overflow-x-auto animate-fade">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Date</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">About</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Author</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Comment</th>
</tr>
</thead>

<tbody class="bg-white divide-y divide-gray-200">
<% @comments.each do |comment| %>
<% subject = comment_subject(comment.commentable) %>
<% author = comment.created_by&.person&.name || comment.created_by&.name %>
<% editor = comment.updated_by && comment.updated_by != comment.created_by ? (comment.updated_by.person&.name || comment.updated_by.name) : nil %>
<tr class="hover:bg-gray-50 align-top">
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500"><%= comment.created_at.strftime("%-m/%-d/%Y %-I:%M %p") %></td>
<td class="px-6 py-4 whitespace-nowrap text-sm">
<span class="inline-block <%= subject[:badge] %> text-xs px-2 py-0.5 rounded mb-1"><%= subject[:type] %></span>
<div>
<% if subject[:path] %>
<%= link_to subject[:name], subject[:path], class: "text-blue-600 hover:text-blue-800", data: { turbo_frame: "_top" } %>
<% else %>
<span class="text-gray-900"><%= subject[:name] %></span>
<% end %>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
<%= author.presence || "—" %>
<% if editor %>
<span class="block text-xs text-gray-400">edited by <%= editor %></span>
<% end %>
</td>
<td class="px-6 py-4 text-sm text-gray-900 max-w-xl"><%= simple_format(comment.body, {}, sanitize: true) %></td>
</tr>
<% end %>
</tbody>
</table>
</div>

<% if @comments.total_pages > 1 %>
<div class="pagination flex justify-center mt-6"><%= tailwind_paginate @comments %></div>
<% end %>
<% else %>
<p class="text-gray-500 text-center py-6">There are no comments to show yet.</p>
<% end %>
</div>
9 changes: 3 additions & 6 deletions app/views/event_registrations/_form.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -255,12 +255,9 @@
<i class="fa-solid fa-comments"></i>
</span>
<h2 class="text-sm font-semibold text-gray-700">Registration comments</h2>
<%= link_to event_registration_comments_path(f.object),
class: "ml-auto inline-flex items-center gap-1.5 text-xs font-medium text-gray-500 hover:text-gray-700 hover:underline",
target: "_blank", rel: "noopener" do %>
View all
<i class="fa-solid fa-arrow-up-right-from-square text-[0.6rem]"></i>
<% end %>
<div class="ml-auto">
<%= render "comments/view_all_link", commentable: f.object %>
</div>
</div>

<div class="space-y-4 p-4" data-controller="paginated-fields" data-paginated-fields-per-page-value="5">
Expand Down
5 changes: 4 additions & 1 deletion app/views/organizations/_form.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -380,7 +380,10 @@
<!-- Comments -->
<% if allowed_to?(:manage?, Comment) %>
<div class="form-group space-y-4 mb-8" id="comments-section">
<div class="font-semibold text-gray-700 mb-2">Comments</div>
<div class="flex items-center justify-between mb-2">
<span class="font-semibold text-gray-700">Comments</span>
<%= render "comments/view_all_link", commentable: f.object %>
</div>

<div class="admin-only bg-blue-50 rounded-lg border border-gray-200 p-4 mb-4 shadow-sm" data-controller="paginated-fields comment-edit-toggle" data-paginated-fields-per-page-value="5">
<%= f.simple_fields_for :comments do |cf| %>
Expand Down
5 changes: 4 additions & 1 deletion app/views/people/_form.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -380,7 +380,10 @@
<!-- Comments -->
<% if allowed_to?(:manage?, Comment) %>
<div class="admin-only bg-blue-100 form-group space-y-4" id="comments-section">
<div class="font-semibold text-gray-700 mb-2">Comments</div>
<div class="flex items-center justify-between mb-2">
<span class="font-semibold text-gray-700">Comments</span>
<%= render "comments/view_all_link", commentable: f.object %>
</div>

<div class="admin-only bg-blue-100 rounded-lg border border-gray-200 p-4 mb-4 shadow-sm"
data-controller="paginated-fields comment-edit-toggle"
Expand Down
5 changes: 4 additions & 1 deletion app/views/users/_form.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,10 @@
<% if f.object.persisted? %>
<!-- Comments -->
<div class="bg-white border border-gray-200 rounded-xl shadow-sm p-6" id="comments-section">
<h4 class="text-sm font-semibold uppercase text-gray-700 mb-4">Comments</h4>
<div class="flex items-center justify-between mb-4">
<h4 class="text-sm font-semibold uppercase text-gray-700">Comments</h4>
<%= render "comments/view_all_link", commentable: f.object %>
</div>

<div class="rounded-lg border border-gray-200 bg-gray-50 p-4 shadow-sm"
data-controller="paginated-fields comment-edit-toggle"
Expand Down
5 changes: 4 additions & 1 deletion app/views/workshops/_form.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -478,7 +478,10 @@
<% if f.object.persisted? %>
<% if allowed_to?(:manage?, Comment) %>
<div class="admin-only bg-blue-100 form-group p-3 space-y-4 mt-6 rounded-md" id="comments-section">
<div class="font-semibold text-gray-700 mb-2">Comments</div>
<div class="flex items-center justify-between mb-2">
<span class="font-semibold text-gray-700">Comments</span>
<%= render "comments/view_all_link", commentable: f.object %>
</div>

<div class="admin-only bg-blue-100 rounded-lg border border-gray-200 p-4 mb-4 shadow-sm"
data-controller="paginated-fields comment-edit-toggle"
Expand Down
4 changes: 4 additions & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,10 @@
post :revert, on: :member
end

# Global preview of every comment and the record it's about. Per-record,
# context-aware overviews live under each commentable's nested :comments.
resources :comments, only: [ :index ]

resources :refunds, only: [ :new, :create, :show ]
resources :organization_statuses
resources :affiliations
Expand Down
Loading