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
3 changes: 2 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ This codebase (Rails 8.1)
| Directory | Purpose |
|---|---|
| `app/frontend/entrypoints/` | Vite entry points (application.js, application.css) |
| `app/frontend/javascript/controllers/` | Stimulus controllers (75) |
| `app/frontend/javascript/controllers/` | Stimulus controllers (76) |
| `app/frontend/javascript/rhino/` | Rich text editor customizations (mentions, grid) |
| `app/frontend/stylesheets/` | Tailwind CSS and component styles |

Expand Down Expand Up @@ -303,6 +303,7 @@ end
- `confirm_email` — Email confirmation UI
- `dirty_form` — Unsaved changes detection
- `dismiss` — Dismissable elements
- `details_row_sync` — Keeps a row of `<details>` cards in lockstep: opening/closing one mirrors onto the rest of the row (event dashboard breakdown cards)
- `dropdown` — Dropdown menus with keyboard/click-outside handling
- `edit_toggle` — Inline view/edit toggle for the comments and communications boxes (configurable view/edit CSS classes)
- `event_staff_bio` — Loads a selected person's read-only profile bio (with edit link) alongside the editable event-specific bio on the staff form
Expand Down
19 changes: 19 additions & 0 deletions app/frontend/javascript/controllers/details_row_sync_controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Controller } from "@hotwired/stimulus"

// Connects to data-controller="details-row-sync"
// Keeps a row of <details> cards in lockstep: opening or closing one mirrors its
// open/closed state onto the rest of the row. Each card fires a native `toggle`
// event (which does not bubble, so nested <details> inside a card are ignored);
// we copy the toggled card's `open` onto its siblings. Mirroring only sets cards
// whose state actually differs, so the cascade of toggle events it triggers
// converges immediately instead of looping.
export default class extends Controller {
static targets = ["card"]

sync(event) {
const open = event.target.open
this.cardTargets.forEach(card => {
if (card.open !== open) card.open = open
})
}
}
3 changes: 3 additions & 0 deletions app/frontend/javascript/controllers/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@ application.register("expandable-card", ExpandableCardController)
import ExpandableCardsController from "./expandable_cards_controller"
application.register("expandable-cards", ExpandableCardsController)

import DetailsRowSyncController from "./details_row_sync_controller"
application.register("details-row-sync", DetailsRowSyncController)

import DropdownController from "./dropdown_controller"
application.register("dropdown", DropdownController)

Expand Down
18 changes: 9 additions & 9 deletions app/views/events/dashboard.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -129,9 +129,9 @@

<%# The three addends: Registration fees + Scholarships + Continuing education fees. Each owns
paid/completed + outstanding sub-rows that expand to the registrants behind the figure. %>
<div class="flex flex-col md:flex-row md:flex-wrap md:items-stretch gap-3 md:gap-4">
<div class="flex flex-col md:flex-row md:flex-wrap md:items-stretch gap-3 md:gap-4" data-controller="details-row-sync">
<%# Payments = Paid + Outstanding %>
<details class="group/card flex-1 md:min-w-[16rem] rounded-xl border <%= DomainTheme.border_class_for(:payments, intensity: 200) %> bg-white p-4 shadow-sm">
<details class="group/card flex-1 md:min-w-[16rem] rounded-xl border <%= DomainTheme.border_class_for(:payments, intensity: 200) %> bg-white p-4 shadow-sm" data-details-row-sync-target="card" data-action="toggle->details-row-sync#sync">
<summary class="cursor-pointer select-none list-none [&::-webkit-details-marker]:hidden">
<div class="flex items-center justify-between gap-2">
<div class="flex items-center gap-1.5 <%= DomainTheme.text_class_for(:payments, intensity: 600) %> text-xs font-semibold uppercase tracking-wide">
Expand Down Expand Up @@ -171,7 +171,7 @@
</details>

<%# Scholarships = Completed + Outstanding %>
<details class="group/card flex-1 md:min-w-[16rem] rounded-xl border <%= DomainTheme.border_class_for(:scholarships, intensity: 200) %> bg-white p-4 shadow-sm">
<details class="group/card flex-1 md:min-w-[16rem] rounded-xl border <%= DomainTheme.border_class_for(:scholarships, intensity: 200) %> bg-white p-4 shadow-sm" data-details-row-sync-target="card" data-action="toggle->details-row-sync#sync">
<summary class="cursor-pointer select-none list-none [&::-webkit-details-marker]:hidden">
<div class="flex items-center justify-between gap-2">
<div class="flex items-center gap-1.5 <%= DomainTheme.text_class_for(:scholarships, intensity: 600) %> text-xs font-semibold uppercase tracking-wide">
Expand Down Expand Up @@ -203,7 +203,7 @@
</details>

<%# Continuing education fees = Paid + Outstanding %>
<details class="group/card flex-1 md:min-w-[16rem] rounded-xl border <%= DomainTheme.border_class_for(:continuing_education, intensity: 200) %> bg-white p-4 shadow-sm">
<details class="group/card flex-1 md:min-w-[16rem] rounded-xl border <%= DomainTheme.border_class_for(:continuing_education, intensity: 200) %> bg-white p-4 shadow-sm" data-details-row-sync-target="card" data-action="toggle->details-row-sync#sync">
<summary class="cursor-pointer select-none list-none [&::-webkit-details-marker]:hidden">
<div class="flex items-center justify-between gap-2">
<div class="flex items-center gap-1.5 <%= DomainTheme.text_class_for(:continuing_education, intensity: 600) %> text-xs font-semibold uppercase tracking-wide">
Expand Down Expand Up @@ -244,8 +244,8 @@
<% end %>

<%# Headcount cards — each expands to reveal its full list %>
<div class="grid grid-cols-2 lg:grid-cols-4 gap-3 sm:gap-4">
<details class="group/card bg-white rounded-xl border <%= DomainTheme.border_class_for(:people, intensity: 200) %> p-4 shadow-sm">
<div class="grid grid-cols-2 lg:grid-cols-4 gap-3 sm:gap-4" data-controller="details-row-sync">
<details class="group/card bg-white rounded-xl border <%= DomainTheme.border_class_for(:people, intensity: 200) %> p-4 shadow-sm" data-details-row-sync-target="card" data-action="toggle->details-row-sync#sync">
<summary class="cursor-pointer select-none list-none [&::-webkit-details-marker]:hidden">
<div class="flex items-center justify-between gap-2">
<h3 class="flex items-center gap-1.5 min-w-0 text-lg sm:text-xl font-bold <%= DomainTheme.text_class_for(:people, intensity: 700) %>">
Expand Down Expand Up @@ -276,7 +276,7 @@
</div>
</details>

<details class="group/card bg-white rounded-xl border <%= DomainTheme.border_class_for(:organizations, intensity: 200) %> p-4 shadow-sm">
<details class="group/card bg-white rounded-xl border <%= DomainTheme.border_class_for(:organizations, intensity: 200) %> p-4 shadow-sm" data-details-row-sync-target="card" data-action="toggle->details-row-sync#sync">
<summary class="cursor-pointer select-none list-none [&::-webkit-details-marker]:hidden">
<div class="flex items-center justify-between gap-2">
<h3 class="flex items-center gap-1.5 min-w-0 text-lg sm:text-xl font-bold <%= DomainTheme.text_class_for(:organizations, intensity: 700) %>">
Expand Down Expand Up @@ -314,7 +314,7 @@
</div>
</details>

<details class="group/card bg-white rounded-xl border <%= DomainTheme.border_class_for(:sectors, intensity: 200) %> p-4 shadow-sm">
<details class="group/card bg-white rounded-xl border <%= DomainTheme.border_class_for(:sectors, intensity: 200) %> p-4 shadow-sm" data-details-row-sync-target="card" data-action="toggle->details-row-sync#sync">
<summary class="cursor-pointer select-none list-none [&::-webkit-details-marker]:hidden">
<div class="flex items-center justify-between gap-2">
<h3 class="flex items-center gap-1.5 min-w-0 text-lg sm:text-xl font-bold <%= DomainTheme.text_class_for(:sectors, intensity: 700) %>">
Expand Down Expand Up @@ -346,7 +346,7 @@
</div>
</details>

<details class="group/card bg-white rounded-xl border <%= DomainTheme.border_class_for(:addresses, intensity: 200) %> p-4 shadow-sm">
<details class="group/card bg-white rounded-xl border <%= DomainTheme.border_class_for(:addresses, intensity: 200) %> p-4 shadow-sm" data-details-row-sync-target="card" data-action="toggle->details-row-sync#sync">
<summary class="cursor-pointer select-none list-none [&::-webkit-details-marker]:hidden">
<div class="flex items-center justify-between gap-2">
<h3 class="flex items-center gap-1.5 min-w-0 text-lg sm:text-xl font-bold <%= DomainTheme.text_class_for(:addresses, intensity: 700) %>">
Expand Down
30 changes: 30 additions & 0 deletions spec/system/event_dashboard_details_row_sync_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
require "rails_helper"

# The dashboard groups its breakdown cards into rows of native <details>. Opening
# or closing one card should mirror onto the rest of that row (details-row-sync
# controller).
RSpec.describe "Event dashboard details row sync", type: :system, js: true do
let(:admin) { create(:user, :admin) }
let(:event) { create(:event, :published, cost: 25) }

before { sign_in admin }

it "opens and closes every card in a row together" do
visit dashboard_event_path(event)

# The headcount row (Registrants / Organizations / Sectors / States) always
# renders regardless of the event's data. Grab it via a card unique to it.
sectors_card = find("details.group\\/card", text: "Sectors")
row = sectors_card.find(:xpath, "..")
card_count = row.all("details.group\\/card", minimum: 2).size

# All start collapsed.
expect(row).to have_no_css("details.group\\/card[open]")

sectors_card.find("summary").click
expect(row).to have_css("details.group\\/card[open]", count: card_count, wait: 5)

sectors_card.find("summary").click
expect(row).to have_no_css("details.group\\/card[open]", wait: 5)
end
end