Skip to content
Merged
3 changes: 1 addition & 2 deletions app/helpers/application_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,7 @@ def credited_author_edit_button(record)
person_edit_button(person,
display_name: record.author_credit,
compact: true,
width_class: "inline-flex w-fit",
data: { turbo_frame: "_top" })
width_class: "inline-flex w-fit")
end

# The person an author picker should show. Only the record's own author counts —
Expand Down
3 changes: 2 additions & 1 deletion app/views/admin/ahoy_activities/_activity_row.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
<% if event.user %>
<%= link_to event.user.full_name,
user_path(event.user),
class: "text-indigo-600 hover:underline" %>
class: "text-indigo-600 hover:underline",
data: { turbo_frame: "_top" } %>
<% else %>
<span class="text-gray-400 italic">Guest</span>
<% end %>
Expand Down
2 changes: 1 addition & 1 deletion app/views/admin/ahoy_activities/_event_details.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
<% if row[:action] %><span class="text-gray-400"><%= row[:action] %></span><% end %>
<% if row[:link] %>
<% if row[:link][:path] %>
<%= link_to row[:link][:text], row[:link][:path], class: "text-indigo-600 hover:underline" %>
<%= link_to row[:link][:text], row[:link][:path], class: "text-indigo-600 hover:underline", data: { turbo_frame: "_top" } %>
<% else %>
<span class="text-gray-600"><%= row[:link][:text] %></span>
<% end %>
Expand Down
6 changes: 4 additions & 2 deletions app/views/admin/ahoy_activities/visit_results.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@
<% if visit.user %>
<%= link_to visit.user.full_name,
user_path(visit.user),
class: "text-indigo-600 hover:underline" %>
class: "text-indigo-600 hover:underline",
data: { turbo_frame: "_top" } %>
<% else %>
<span class="text-gray-400 italic">Guest</span>
<% end %>
Expand All @@ -61,7 +62,8 @@

<td class="px-4 py-3 text-gray-500">
<%= link_to visit.attributes["events_count"],
admin_activities_events_path(visit_id: visit.id) %>
admin_activities_events_path(visit_id: visit.id),
data: { turbo_frame: "_top" } %>
</td>

<td class="px-4 py-3 whitespace-nowrap text-gray-500">
Expand Down
3 changes: 2 additions & 1 deletion app/views/comments/_aggregated_comment.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
<div class="flex items-start justify-between gap-3">
<div class="flex flex-wrap items-center gap-2">
<%= link_to comment.source_label, comment.source_path,
class: "inline-flex items-center rounded-full border px-2 py-0.5 text-xs font-medium #{DomainTheme.border_class_for(comment.source_theme)} #{DomainTheme.bg_class_for(comment.source_theme, intensity: 100)} #{DomainTheme.text_class_for(comment.source_theme, intensity: 800)}" %>
class: "inline-flex items-center rounded-full border px-2 py-0.5 text-xs font-medium #{DomainTheme.border_class_for(comment.source_theme)} #{DomainTheme.bg_class_for(comment.source_theme, intensity: 100)} #{DomainTheme.text_class_for(comment.source_theme, intensity: 800)}",
data: { turbo_frame: "_top" } %>
<% if comment.topic.present? %>
<span class="text-xs font-bold uppercase tracking-wide text-gray-500"><%= comment.topic %></span>
<% end %>
Expand Down
1 change: 1 addition & 0 deletions app/views/events/_person_profile_button.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
{ return_to: "bulk_payments", event_id: local_assigns[:event]&.id, expand: local_assigns[:expand] } : {} %>
<%= link_to person_path(person, person_link_params),
title: "#{profile_person.name} — #{person.preferred_email}",
data: { turbo_frame: "_top" },
class: "inline-flex items-center gap-2 shrink-0 rounded-lg border border-sky-300 bg-sky-100 hover:bg-sky-200 px-3 py-2 shadow-sm transition-colors" do %>
<% if profile_person.avatar.present? %>
<%= image_tag profile_person.avatar.variant(:thumbnail),
Expand Down
17 changes: 10 additions & 7 deletions app/views/events/_registrant_roster.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@
<td class="block px-4 pt-3 pb-1 text-sm sm:table-cell sm:py-1.5 sm:whitespace-nowrap"
data-sort-first="<%= person.first_name.to_s.downcase %>"
data-sort-last="<%= person.last_name.to_s.downcase %>">
<%= link_to person.name, person_link, class: "font-bold text-gray-900 hover:underline text-base sm:text-sm" %>
<%= link_to person.name, person_link, class: "font-bold text-gray-900 hover:underline text-base sm:text-sm", data: { turbo_frame: "_top" } %>
</td>
<%# Primary sector + age group — one column, each independently
sortable (data-sort-sector / data-sort-age), mirroring the
Expand All @@ -116,7 +116,7 @@
<%= mobile_label.("Organization") %>
<div class="min-w-0 flex-1 break-words">
<% if person_orgs.any? %>
<% person_orgs.each_with_index do |org, i| %><%= ", " if i.positive? %><%= org.decorate.high_profile_icon %><%= link_to org.name, organization_path(org), class: "hover:underline", onclick: "event.stopPropagation()" %><% end %>
<% person_orgs.each_with_index do |org, i| %><%= ", " if i.positive? %><%= org.decorate.high_profile_icon %><%= link_to org.name, organization_path(org), class: "hover:underline", onclick: "event.stopPropagation()", data: { turbo_frame: "_top" } %><% end %>
<% else %>
<span class="text-gray-300">—</span>
<% end %>
Expand Down Expand Up @@ -174,7 +174,8 @@
<%= link_to recipients_event_path(target_event, anchor: ("participant-#{participant_slug}" if participant_slug)),
class: "inline-flex items-center gap-1 #{declined ? "text-gray-400 hover:text-gray-600" : "#{DomainTheme.text_class_for(:scholarships, intensity: 600)} hover:#{DomainTheme.text_class_for(:scholarships)}"}",
title: declined ? "Scholarship declined — view application" : (show_event_column ? "Scholarship from #{target_event.decorate.compact_label} — view application" : "Scholarship recipient — view application"),
onclick: "event.stopPropagation()" do %>
onclick: "event.stopPropagation()",
data: { turbo_frame: "_top" } do %>
<i class="fa-solid fa-graduation-cap"></i>
<% if show_event_column %><span class="text-xs font-medium text-gray-400"><%= target_event.decorate.compact_label %></span><% end %>
<%= scholarship.decorate.agreement_status_badge %>
Expand All @@ -192,7 +193,8 @@
<%= link_to edit_continuing_education_registration_path(ce_registration),
class: "inline-flex items-center gap-1 #{DomainTheme.text_class_for(:continuing_education, intensity: 600)} hover:#{DomainTheme.text_class_for(:continuing_education)}",
title: ce_event ? "Continuing education from #{ce_event.decorate.compact_label} — edit" : "Edit continuing education",
onclick: "event.stopPropagation()" do %>
onclick: "event.stopPropagation()",
data: { turbo_frame: "_top" } do %>
<i class="fa-solid fa-certificate"></i>
<% if ce_event %><span class="text-xs font-medium text-gray-400"><%= ce_event.decorate.compact_label %></span><% end %>
<% end %>
Expand All @@ -212,7 +214,8 @@
<%= link_to edit_event_registration_path(registration, return_to: row_return_to),
class: "inline-flex items-center gap-1.5 rounded-full border px-2 py-0.5 text-xs font-medium hover:underline #{attendance.attendance_status_classes}",
title: "Edit registration",
onclick: "event.stopPropagation()" do %>
onclick: "event.stopPropagation()",
data: { turbo_frame: "_top" } do %>
<i class="fa-solid <%= attendance.attendance_status_icon %>"></i>
<span><%= registration.attendance_status_label %></span>
<% end %>
Expand All @@ -238,7 +241,7 @@
<div class="min-w-0 flex-1">
<% if first_registration %>
<div class="flex items-center gap-1.5">
<%= link_to edit_event_registration_path(first_registration, return_to: "attendees"), class: event_link_class do %>
<%= link_to edit_event_registration_path(first_registration, return_to: "attendees"), class: event_link_class, data: { turbo_frame: "_top" } do %>
<%= first_registration.event.decorate.compact_label %>
<span class="font-normal text-gray-400"><%= first_registration.event.start_date&.strftime("%b %Y") %></span>
<% end %>
Expand All @@ -251,7 +254,7 @@
<% if registrations.size > 1 %>
<div class="mt-1 flex hidden flex-col gap-1" data-expandable-card-target="body">
<% registrations.drop(1).each do |registration| %>
<%= link_to edit_event_registration_path(registration, return_to: "attendees"), class: "text-xs #{DomainTheme.text_class_for(:event_registrations, intensity: 700)} hover:underline" do %>
<%= link_to edit_event_registration_path(registration, return_to: "attendees"), class: "text-xs #{DomainTheme.text_class_for(:event_registrations, intensity: 700)} hover:underline", data: { turbo_frame: "_top" } do %>
<%= registration.event.decorate.compact_label %>
<span class="text-gray-400"><%= registration.event.start_date&.strftime("%b %Y") %></span>
<% end %>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
class: "font-bold text-gray-800 hover:text-indigo-800 hover:underline" %>
</td>

<td class="px-6 py-4 text-sm text-gray-800"><%= credited_author_link(report, class: eyebrow_link_class) %></td>
<td class="px-6 py-4 text-sm text-gray-800"><%= credited_author_link(report, class: eyebrow_link_class, data: { turbo_frame: "_top" }) %></td>

<td class="px-6 py-4 text-sm text-gray-600"><%= report.organization&.name || "—" %></td>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@
<% end %>
</td>
<td class="px-4 py-2 text-center text-sm text-gray-800">
<%= credited_author_link(workshop_variation, class: "#{eyebrow_link_class}") %>
<%= credited_author_link(workshop_variation, class: "#{eyebrow_link_class}", data: { turbo_frame: "_top" }) %>
</td>
<td class="px-4 py-2 text-center text-sm whitespace-nowrap">
<% if workshop_variation.workshop_variation_idea %>
Expand Down
11 changes: 11 additions & 0 deletions spec/helpers/application_helper_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,17 @@
expect(result).to eq("AWBW Staff")
expect(result).not_to include("<a")
end

it "forwards a caller's data attrs (e.g. turbo_frame) onto the link so call sites can break out of a frame" do
allow(person).to receive(:profile_is_searchable).and_return(true)
workshop = create(:workshop, author_credit_preference: "full_name")
allow(workshop).to receive(:author).and_return(person)

link = Nokogiri::HTML.fragment(
helper.credited_author_link(workshop, data: { turbo_frame: "_top" })
).at_css("a")
expect(link["data-turbo-frame"]).to eq("_top")
end
end

describe "#credited_author_edit_button" do
Expand Down
14 changes: 14 additions & 0 deletions spec/helpers/organization_helper_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
require "rails_helper"

RSpec.describe OrganizationHelper, type: :helper do
describe "#organization_profile_button" do
let(:organization) { create(:organization) }

it "forwards a caller's data-turbo-frame onto the link so call sites can break out of a frame" do
link = Nokogiri::HTML(
helper.organization_profile_button(organization, data: { turbo_frame: "_top" })
).at_css("a")
expect(link["data-turbo-frame"]).to eq("_top")
end
end
end
23 changes: 23 additions & 0 deletions spec/helpers/person_helper_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,18 @@
expect(helper.person_profile_button(person)).not_to include("fa-triangle-exclamation")
end
end

context "frame breakout" do
let(:unconfirmed) { nil }
let(:authorized) { true }

it "forwards a caller's data-turbo-frame onto the link so call sites can break out of a frame" do
link = Nokogiri::HTML(
helper.person_profile_button(person, data: { turbo_frame: "_top" })
).at_css("a")
expect(link["data-turbo-frame"]).to eq("_top")
end
end
end

describe "#person_edit_button" do
Expand Down Expand Up @@ -71,6 +83,11 @@
expect(helper.person_edit_button(person, layout: :prefix)).to include("items-baseline gap-1.5")
expect(helper.person_edit_button(person, layout: :eyebrow)).not_to include("items-baseline gap-1.5")
end

it "forwards a caller's data-turbo-frame onto the link so call sites can break out of a frame" do
link = Nokogiri::HTML(helper.person_edit_button(person, data: { turbo_frame: "_top" })).at_css("a")
expect(link["data-turbo-frame"]).to eq("_top")
end
end

describe "#user_button" do
Expand All @@ -91,5 +108,11 @@
expect(html).not_to include("href=")
expect(html).to include("Cara Lang")
end

it "forwards a caller's data-turbo-frame onto the link so call sites can break out of a frame" do
allow(helper).to receive(:allowed_to?).with(:show?, user).and_return(true)
link = Nokogiri::HTML(helper.user_button(user, data: { turbo_frame: "_top" })).at_css("a")
expect(link["data-turbo-frame"]).to eq("_top")
end
end
end
13 changes: 13 additions & 0 deletions spec/requests/admin/ahoy_activities_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,12 @@
expect(response).to have_http_status(:ok)
end

it "breaks the row's user link out of the results frame" do
get index_path, params: { visit_id: visit_for_user.id }, headers: frame_headers

expect_frame_breakout(response.body, user_path(user))
end

it "filters by prefixes=auth" do
get index_path, params: { prefixes: "auth" }, headers: frame_headers

Expand Down Expand Up @@ -419,6 +425,13 @@
expect(response.body).to include(user.full_name)
end

it "breaks the visit's user and activities-count links out of the results frame" do
get visits_path, params: { user_id: user.id }, headers: visits_frame_headers

expect_frame_breakout(response.body, user_path(user))
expect_frame_breakout(response.body, "visit_id=#{visit_for_user.id}")
end

it "filters visits by visit_id" do
get visits_path, params: { visit_id: visit_for_user.id }, headers: visits_frame_headers

Expand Down
10 changes: 10 additions & 0 deletions spec/requests/comments_index_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,16 @@
expect(response.body).to include("One", "Two")
end

it "breaks the source chip out of the results frame so it doesn't Oopsie" do
sign_in admin
commentable = create(:person)
create(:comment, commentable: commentable, body: "Note")

get comments_path, headers: frame_headers

expect_frame_breakout(response.body, "/people/#{commentable.id}/edit")
end

it "filters to comments connected to a person" do
sign_in admin
target = create(:person)
Expand Down
10 changes: 10 additions & 0 deletions spec/requests/events/attendees_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,16 @@
# The charts are lazy-loaded into their own frame, only when the admin reveals them.
let(:charts_frame_headers) { { "Turbo-Frame" => "attendees_charts" } }

describe "roster away-links break out of the results frame" do
it "gives the registrant name link data-turbo-frame=_top so it doesn't Oopsie the frame" do
sign_in admin
get attendees_events_url, headers: frame_headers

expect(response).to have_http_status(:ok)
expect_frame_breakout(response.body, "/event_registrations/#{attendee_registration.id}/edit")
end
end

describe "GET /events/attendees" do
context "as a user who owns no events" do
it "redirects — there is nothing for them to report on" do
Expand Down
10 changes: 10 additions & 0 deletions spec/requests/monthly_reports_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,16 @@
expect(response.body).to include("Mar 2026")
end

it "breaks the credited-author byline out of the results frame" do
author = create(:person, first_name: "Lena", last_name: "Byline", profile_is_searchable: true)
create(:monthly_report, organization: organization, author: author,
author_credit_preference: "full_name", date: Date.new(2026, 4, 1))

get monthly_reports_url, headers: turbo_headers

expect_frame_breakout(response.body, person_path(author))
end

it "filters by organization_id when provided" do
create(:monthly_report, organization: organization, date: Date.new(2026, 4, 1))
create(:monthly_report, organization: other_organization, date: Date.new(2026, 3, 1))
Expand Down
20 changes: 20 additions & 0 deletions spec/requests/payments_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,26 @@

before { sign_in admin }

describe "GET /payments index frame" do
it "renders the lazy results frame without erroring" do
create(:payment, person: person, amount_cents: 5000, amount_cents_remaining: 5000)

get payments_path, headers: { "Turbo-Frame" => "payments_results" }

expect(response).to have_http_status(:ok)
expect(response.body).to include("payments_results")
expect(response.body).to include(person.name)
end

it "breaks the row's View link out of the results frame" do
payment = create(:payment, person: person, amount_cents: 5000, amount_cents_remaining: 5000)

get payments_path, headers: { "Turbo-Frame" => "payments_results" }

expect_frame_breakout(response.body, payment_path(payment))
end
end

describe "GET /payments/:id eyebrow" do
let(:event) { create(:event, cost_cents: 5000) }
let(:submission) { create(:form_submission, event: event, role: "bulk_payment") }
Expand Down
8 changes: 8 additions & 0 deletions spec/requests/people_all_comments_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,14 @@
expect(response.body).to include("Profile note", "Registration note", "Scholarship note", "CE note", "Account note")
end

it "breaks the source chip out of the results frame so it doesn't Oopsie" do
create(:comment, commentable: person, body: "Profile note")

get all_comments_person_path(person), headers: frame_headers

expect_frame_breakout(response.body, "/people/#{person.id}/edit")
end

it "filters to a single source" do
create(:comment, commentable: person, body: "Profile note")
registration = create(:event_registration, registrant: person)
Expand Down
1 change: 0 additions & 1 deletion spec/requests/people_notifications_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,6 @@
get edit_person_path(person)

doc = Nokogiri::HTML(response.body)
# Only admins reach the profile, so nothing inside the section carries the
# `admin-only` wash — buttons and cards alike render on the plain white card.
# (Per-author identity chips may independently be blue: chip_color hashes the
# author id onto a palette that includes bg-blue-100, so don't ban that class
Expand Down
5 changes: 5 additions & 0 deletions spec/requests/people_search_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@
expect(response).to have_http_status(:ok)
end

it "breaks the person profile button out of the results frame" do
get people_path, headers: turbo_headers
expect_frame_breakout(response.body, person_path(person_alice))
end

it "returns all people when no filters are applied" do
get people_path, headers: turbo_headers
expect(response).to have_http_status(:ok)
Expand Down
7 changes: 7 additions & 0 deletions spec/requests/professional_licenses_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,13 @@
expect(response.body).to include("555")
end

it "breaks the person edit button out of the results frame" do
get professional_licenses_path,
headers: { "Turbo-Frame" => "professional_licenses_results" }

expect_frame_breakout(response.body, edit_person_path(person))
end

it "filters by type" do
other = create(:professional_license, kind: "LCSW", number: "999")

Expand Down
9 changes: 9 additions & 0 deletions spec/requests/users_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,15 @@
get users_url
expect(response).to be_successful
end

it "renders the lazy results frame, including an account with no linked person" do
create(:user, person: nil)

get users_url, headers: { "Turbo-Frame" => "users_results" }

expect(response).to have_http_status(:ok)
expect(response.body).to include("users_results")
end
end

context "as regular_user" do
Expand Down
Loading