Field mappings UI on the Runsignup connection management page#2008
Closed
moveson wants to merge 5 commits into
Closed
Field mappings UI on the Runsignup connection management page#2008moveson wants to merge 5 commits into
moveson wants to merge 5 commits into
Conversation
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>
4 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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/runsignupnow shows a "Field Mappings" card between the EventGroup card and the per-event sync cards. Table layout:emergency_contact ▾comments ▾First Attempt/ suppress when:Nocomments ▾comments ▾(don't sync) ▾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
Connectors::Runsignup::FetchRaceQuestions(new)(question_id, question_text)tuples from a single page of/participants?include_questions=T. Result cached 5 minutes inRails.cache.Connectors::Runsignup::Models::Question(new)Struct.new(:id, :text)ConnectServicePresenter#race_questions,#field_mappings,#field_mapping_for(question_id). Privateevent_level_connectionshelper walks the EventGroup's events.PATCH /event_groups/:event_group_id/connect_service/:service_identifier/field_mappingsEventGroups::FieldMappingsController(new)updateaction normalizes form params (whitelists destination, coercessource_question_idto Integer, drops empty/unknown rows, omits blank optional fields), writes to every event-level Connection. Authorized via existingEventGroupPolicy#setup?.event_groups/connect_service/runsignup/_field_mappings_card.html.erb(new)event_groups/field_mappings/update.turbo_stream.erb(new)field_mapping_row_controller.js(new Stimulus)event_groups/connect_service/show.html.erbNo schema changes —
connections.field_mappingsexists 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_questionsreturns[]; view shows "No questions returned by RunSignup yet — has the race had any registrations?" copy and renders no form.service_identifierin URL → 404 (ActiveRecord::RecordNotFoundraised atset_service).[]for race_questions.value_when_present/suppress_whenstrings → 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_questionswith 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.Out of scope
🤖 Generated with Claude Code