Skip to content

Field mappings UI on the Runsignup connection management page#2008

Closed
moveson wants to merge 5 commits into
masterfrom
oveson/claude/1998-field-mappings-ui
Closed

Field mappings UI on the Runsignup connection management page#2008
moveson wants to merge 5 commits into
masterfrom
oveson/claude/1998-field-mappings-ui

Conversation

@moveson

@moveson moveson commented May 8, 2026

Copy link
Copy Markdown
Collaborator

Resolves #1998.

Summary

Adds the per-race-shared field-mappings configuration UI so race directors can map Runsignup registration questions to Effort columns (comments / emergency_contact / emergency_phone) without touching the Rails console — completing the work started in #2002 / #2005.

What it looks like

The connection management page at /event_groups/:slug/connect_service/runsignup now shows a "Field Mappings" card between the EventGroup card and the per-event sync cards. Table layout:

Question Destination Override / Suppress (comments only)
Emergency Contact Name emergency_contact ▾ (hidden)
Is this your first attempt? comments ▾ override: First Attempt / suppress when: No
What name on bib? comments ▾ override: (empty) / suppress when: (empty)
Tell us a fun fact… comments ▾ (same)
Allergies (don't sync) ▾ (hidden)

Single Save button. Destination select toggles the override/suppress column visibility via a small Stimulus controller. Submit PATCHes via Turbo, controller writes the same JSON array to every event-level Connection under the EventGroup, response replaces the card via turbo_stream.

Layers changed

Layer Change
Connectors::Runsignup::FetchRaceQuestions (new) Extracts catalog of distinct (question_id, question_text) tuples from a single page of /participants?include_questions=T. Result cached 5 minutes in Rails.cache.
Connectors::Runsignup::Models::Question (new) Struct.new(:id, :text)
ConnectServicePresenter New #race_questions, #field_mappings, #field_mapping_for(question_id). Private event_level_connections helper walks the EventGroup's events.
Routes PATCH /event_groups/:event_group_id/connect_service/:service_identifier/field_mappings
EventGroups::FieldMappingsController (new) update action normalizes form params (whitelists destination, coerces source_question_id to Integer, drops empty/unknown rows, omits blank optional fields), writes to every event-level Connection. Authorized via existing EventGroupPolicy#setup?.
event_groups/connect_service/runsignup/_field_mappings_card.html.erb (new) The form view
event_groups/field_mappings/update.turbo_stream.erb (new) Replaces the card on save
field_mapping_row_controller.js (new Stimulus) Toggles comments-only column based on destination select
event_groups/connect_service/show.html.erb Renders the new card between event_group_card and events_list, gated to runsignup service

No schema changesconnections.field_mappings exists from #2002.

Design choice — per-race shared

Confirmed in plan: one form, one submit, same JSON written to every event-level Connection under the EventGroup. Matches the reality that questions are race-level on Runsignup, and matches the console snippet's behavior. Per-event independent mappings deferred — can revisit if a real use case appears.

Edge cases handled

  • Race with zero participants → race_questions returns []; view shows "No questions returned by RunSignup yet — has the race had any registrations?" copy and renders no form.
  • Unknown service_identifier in URL → 404 (ActiveRecord::RecordNotFound raised at set_service).
  • Connector API error during fetch → caught in presenter, error_message set, returns [] for race_questions.
  • Mapping form input with blank or unknown destination → silently dropped during normalization.
  • Empty value_when_present / suppress_when strings → omitted from persisted JSON (don't store empty values).

Test plan

  • spec/lib/connectors/runsignup/fetch_race_questions_spec.rb — 7 examples: distinct extraction, empty participants, missing question_responses, outer-array unwrapping, caching (memory_store override), trailing-whitespace stripping.
  • spec/presenters/connect_service_presenter_spec.rb — 6 new examples covering #field_mappings, #field_mapping_for, #race_questions with delegation + memoization + missing-connection fallback. (21 examples total in file, all green.)
  • spec/requests/event_groups/field_mappings_controller_spec.rb — 6 examples covering write to all event Connections, Integer coercion, drop blank/unknown destinations, omit empty optional fields, turbo_stream response shape, RecordNotFound on unknown service.
  • Rubocop clean on all touched files.
  • erb_lint clean on all touched views.
  • Full sync-related suite: 59 examples across 4 spec files green.
  • Manual end-to-end against quad-rock-2026 in dev: visit the connection page, toggle a destination, save, re-sync, verify effort.comments reflects the change.

Out of scope

🤖 Generated with Claude Code

moveson and others added 5 commits May 8, 2026 17:10
Resolves #1998. Adds the per-race-shared field-mappings configuration
UI so race directors can map Runsignup registration questions to
Effort columns (comments / emergency_contact / emergency_phone)
without touching the Rails console.

Layered changes:

- New Connectors::Runsignup::FetchRaceQuestions extracts the catalog
  of distinct (question_id, question_text) tuples from a single page
  of /participants?include_questions=T. Result cached for 5 minutes
  in Rails.cache so page navigation doesn't hammer the API.
- New Connectors::Runsignup::Models::Question struct (id, text).
- ConnectServicePresenter gains #race_questions, #field_mappings,
  #field_mapping_for(id), and a private event_level_connections
  helper that walks the EventGroup's events and collects their
  per-event Connections.
- New PATCH route at
  /event_groups/:event_group_id/connect_service/:service_identifier/field_mappings
  routed to the new EventGroups::FieldMappingsController#update.
  The action normalizes form params (whitelists destination,
  coerces source_question_id to Integer, drops empty rows / blank
  optional fields), then writes the same field_mappings array to
  every event-level Connection under the EventGroup. Authorized via
  the existing EventGroupPolicy#setup?.
- New view _field_mappings_card.html.erb under
  event_groups/connect_service/runsignup/ — table with one row per
  race question, destination select, and (visible when destination
  is comments) inline override / suppress text inputs.
- New FieldMappingRowController Stimulus controller toggles the
  comments-only inputs based on the destination select's value.
- Show view renders the new card between event_group_card and
  events_list, gated to runsignup service identifier.

Per-race design (single shared mapping applied to every event-level
Connection under the EventGroup) — questions are race-level on the
Runsignup side and the typical use case has every event configured
identically. Per-event override deferred per #1998 plan.

No schema changes — connections.field_mappings exists from #2002.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously the field-mappings card showed empty until at least one
event-level Runsignup Connection had been toggled on. Race directors
naturally expect to see the questions on first visit so they can
configure mappings BEFORE wiring up event connections.

The /participants endpoint requires an event_id. Now we prefer the
first event-level Connection's source_id (so we hit a known-good
Runsignup event the user already chose), but fall back to the first
event from FetchRaceEvents (already cached by the page's all_sources
lookup) when no event Connection exists yet.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Refactor FetchRaceQuestions to call /Rest/race/{race_id}?include_questions=T
instead of paginating through /participants. Discovered while answering
"are questions per-event or per-race?" — the docs show questions are
configured at the race level (with optional skip_for_event_ids per
event) and there's a dedicated GET endpoint that returns the question
definitions directly without participant enumeration.

Benefits over the old approach:

- One API call regardless of participant count (was: 1 + small page).
- Catalog comes back even when the race has zero participants registered.
- No need for an event_id, which means the field-mappings UI no longer
  needs to resolve a "which event_id do I use" question — just the
  Runsignup race_id from the EventGroup-level Connection.

Changes:

- Request::GetRace now accepts an include_questions: false kwarg and
  conditionally adds the param. Client#get_race accepts and forwards it.
- FetchRaceQuestions drops the event_id parameter from its signature.
- ConnectServicePresenter#race_questions no longer derives an event_id;
  the questions_event_id helper is removed.

Existing get_race callers (FetchRaceEvents) are unaffected — the new
kwarg defaults to false so the request shape is identical.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously the Field Mappings section only appeared after a full page
reload after the user entered the Runsignup race ID. Now:

- show.html.erb wraps the card in a stable slot div
  (id: dom_id(event_group, :field_mappings_card_slot)) that's always
  present even before any connection exists.
- connections/create.turbo_stream.erb appends a turbo_stream.update
  rendering the field_mappings_card into the slot (when the new
  connection is for runsignup and successful_connection? is now true).
- connections/destroy.turbo_stream.erb empties the slot when the
  user disconnects, so the now-orphaned card disappears.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The form save was a no-op when no per-event Connections existed yet —
the controller iterated event_level_connections and called update! on
each, but the array was empty for users who hadn't toggled on the
per-event connection switches. Production log showed the params
arriving correctly with no UPDATE statements following.

Move field_mappings from per-event Connections to the EventGroup-level
Race Connection (which always exists once the user has entered a race
ID). Three layers updated:

- EventGroups::FieldMappingsController#update writes to the Race
  Connection only; raises RecordNotFound if it doesn't exist.
- ConnectServicePresenter#field_mappings reads from a new
  race_connection helper (a memoized find_by, distinct from the
  existing find_or_initialize_by used by the form).
- Interactors::SyncRunsignupParticipants extracts a field_mappings
  helper that reads from the Race Connection once and forwards the
  same value to every per-event FetchEventParticipants call (vs the
  previous per-event read).

Plus a one-time data migration (db/data/20260509033127) that copies
existing per-event field_mappings to the Race Connection. Production
quad-rock-2026 currently has the canonical mapping on per-event
Connections (set via console snippets); after this lands, the mapping
flows to where the new code reads from. Idempotent — skips Race
Connections that already have field_mappings present.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@moveson

moveson commented May 9, 2026

Copy link
Copy Markdown
Collaborator Author

Superseded by the building-block split: #2009, #2010, #2011, #2012, #2013 — all merged. Closing without merging since master already has everything this branch contained.

@moveson moveson closed this May 9, 2026
@moveson moveson deleted the oveson/claude/1998-field-mappings-ui branch May 9, 2026 15:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Runsignup: configurable question→Effort field mapping (incl. comments composition)

1 participant