-
Notifications
You must be signed in to change notification settings - Fork 24
WAIT: Support international addresses with country-driven state field #1855
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| import { Controller } from "@hotwired/stimulus" | ||
|
|
||
| // Toggles an address's state field between a US state dropdown (validated server | ||
| // side) and a free-text region input, based on the selected country. Only the | ||
| // active input stays enabled, so exactly one value submits under the shared | ||
| // `[state]` name. Each address block scopes its own controller instance, so this | ||
| // works for cocoon-added addresses too. | ||
| export default class extends Controller { | ||
| static targets = ["country", "stateSelect", "stateText"] | ||
| static values = { usCountry: String } | ||
|
|
||
| connect() { | ||
| this.update() | ||
| } | ||
|
|
||
| update() { | ||
| const isUs = this.countryTarget.value === "" || this.countryTarget.value === this.usCountryValue | ||
| this.activate(this.stateSelectTarget, isUs) | ||
| this.activate(this.stateTextTarget, !isUs) | ||
| } | ||
|
|
||
| // Show and enable the active field (and its wrapper), hide and disable the other | ||
| // so it neither submits nor blocks form validation while hidden. | ||
| activate(field, active) { | ||
| field.disabled = !active | ||
| const wrapper = field.closest("[data-address-region-wrapper]") || field | ||
| wrapper.classList.toggle("hidden", !active) | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,57 +1,26 @@ | ||
| module GeographyHelper | ||
| def us_states | ||
| [ | ||
| [ "Alabama", "AL" ], | ||
| [ "Alaska", "AK" ], | ||
| [ "Arizona", "AZ" ], | ||
| [ "Arkansas", "AR" ], | ||
| [ "California", "CA" ], | ||
| [ "Colorado", "CO" ], | ||
| [ "Connecticut", "CT" ], | ||
| [ "Delaware", "DE" ], | ||
| [ "District of Columbia", "DC" ], | ||
| [ "Florida", "FL" ], | ||
| [ "Georgia", "GA" ], | ||
| [ "Hawaii", "HI" ], | ||
| [ "Idaho", "ID" ], | ||
| [ "Illinois", "IL" ], | ||
| [ "Indiana", "IN" ], | ||
| [ "Iowa", "IA" ], | ||
| [ "Kansas", "KS" ], | ||
| [ "Kentucky", "KY" ], | ||
| [ "Louisiana", "LA" ], | ||
| [ "Maine", "ME" ], | ||
| [ "Maryland", "MD" ], | ||
| [ "Massachusetts", "MA" ], | ||
| [ "Michigan", "MI" ], | ||
| [ "Minnesota", "MN" ], | ||
| [ "Mississippi", "MS" ], | ||
| [ "Missouri", "MO" ], | ||
| [ "Montana", "MT" ], | ||
| [ "Nebraska", "NE" ], | ||
| [ "Nevada", "NV" ], | ||
| [ "New Hampshire", "NH" ], | ||
| [ "New Jersey", "NJ" ], | ||
| [ "New Mexico", "NM" ], | ||
| [ "New York", "NY" ], | ||
| [ "North Carolina", "NC" ], | ||
| [ "North Dakota", "ND" ], | ||
| [ "Ohio", "OH" ], | ||
| [ "Oklahoma", "OK" ], | ||
| [ "Oregon", "OR" ], | ||
| [ "Pennsylvania", "PA" ], | ||
| [ "Rhode Island", "RI" ], | ||
| [ "South Carolina", "SC" ], | ||
| [ "South Dakota", "SD" ], | ||
| [ "Tennessee", "TN" ], | ||
| [ "Texas", "TX" ], | ||
| [ "Utah", "UT" ], | ||
| [ "Vermont", "VT" ], | ||
| [ "Virginia", "VA" ], | ||
| [ "Washington", "WA" ], | ||
| [ "West Virginia", "WV" ], | ||
| [ "Wisconsin", "WI" ], | ||
| [ "Wyoming", "WY" ] | ||
| ] | ||
| UsState::ALL | ||
| end | ||
|
|
||
| # A <datalist> of US states for a free-text state/region input: it suggests US | ||
| # states (stored as abbreviations) while still letting the user type any | ||
| # international region or leave it blank. Reference it from an input via | ||
| # `list:` matching the given +id+. | ||
| def us_states_datalist(id) | ||
| options = us_states.map { |name, abbr| tag.option(value: abbr, label: name) } | ||
| tag.datalist(safe_join(options), id: id) | ||
| end | ||
|
|
||
| # Country names for the address country dropdown: the canonical "United States" | ||
| # first (it drives the US-state-vs-region toggle), then every other country | ||
| # alphabetically. The US is excluded from the gem list by code so the only US | ||
| # option is our canonical value, not the gem's verbose "United States of America". | ||
| def country_options | ||
| others = ISO3166::Country.all | ||
| .reject { |country| country.alpha2 == "US" } | ||
| .map(&:iso_short_name) | ||
| .sort | ||
| [ Address::US_COUNTRY ] + others | ||
| end | ||
| end |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| # Canonical list of US states (plus DC) shared by the address form dropdown and | ||
| # the Address state-inclusion validation, so the options offered and the values | ||
| # accepted can never drift apart. | ||
| module UsState | ||
| ALL = [ | ||
| [ "Alabama", "AL" ], | ||
| [ "Alaska", "AK" ], | ||
| [ "Arizona", "AZ" ], | ||
| [ "Arkansas", "AR" ], | ||
| [ "California", "CA" ], | ||
| [ "Colorado", "CO" ], | ||
| [ "Connecticut", "CT" ], | ||
| [ "Delaware", "DE" ], | ||
| [ "District of Columbia", "DC" ], | ||
| [ "Florida", "FL" ], | ||
| [ "Georgia", "GA" ], | ||
| [ "Hawaii", "HI" ], | ||
| [ "Idaho", "ID" ], | ||
| [ "Illinois", "IL" ], | ||
| [ "Indiana", "IN" ], | ||
| [ "Iowa", "IA" ], | ||
| [ "Kansas", "KS" ], | ||
| [ "Kentucky", "KY" ], | ||
| [ "Louisiana", "LA" ], | ||
| [ "Maine", "ME" ], | ||
| [ "Maryland", "MD" ], | ||
| [ "Massachusetts", "MA" ], | ||
| [ "Michigan", "MI" ], | ||
| [ "Minnesota", "MN" ], | ||
| [ "Mississippi", "MS" ], | ||
| [ "Missouri", "MO" ], | ||
| [ "Montana", "MT" ], | ||
| [ "Nebraska", "NE" ], | ||
| [ "Nevada", "NV" ], | ||
| [ "New Hampshire", "NH" ], | ||
| [ "New Jersey", "NJ" ], | ||
| [ "New Mexico", "NM" ], | ||
| [ "New York", "NY" ], | ||
| [ "North Carolina", "NC" ], | ||
| [ "North Dakota", "ND" ], | ||
| [ "Ohio", "OH" ], | ||
| [ "Oklahoma", "OK" ], | ||
| [ "Oregon", "OR" ], | ||
| [ "Pennsylvania", "PA" ], | ||
| [ "Rhode Island", "RI" ], | ||
| [ "South Carolina", "SC" ], | ||
| [ "South Dakota", "SD" ], | ||
| [ "Tennessee", "TN" ], | ||
| [ "Texas", "TX" ], | ||
| [ "Utah", "UT" ], | ||
| [ "Vermont", "VT" ], | ||
| [ "Virginia", "VA" ], | ||
| [ "Washington", "WA" ], | ||
| [ "West Virginia", "WV" ], | ||
| [ "Wisconsin", "WI" ], | ||
| [ "Wyoming", "WY" ] | ||
| ].freeze | ||
|
|
||
| ABBREVIATIONS = ALL.map(&:last).freeze | ||
| end |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -169,6 +169,7 @@ def find_matching_person(last_name:, email:) | |
| def create_mailing_address(person) | ||
| new_city = field_value("mailing_city")&.strip | ||
| new_state = field_value("mailing_state")&.strip | ||
| new_country = field_value("mailing_country")&.strip.presence | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🤖 From Claude: The form already collected |
||
|
|
||
| existing = person.addresses.find_by( | ||
| "LOWER(city) = ? AND LOWER(COALESCE(state, '')) = ?", | ||
|
|
@@ -179,6 +180,7 @@ def create_mailing_address(person) | |
| existing.update!( | ||
| street_address: field_value("mailing_street"), | ||
| zip_code: field_value("mailing_zip"), | ||
| country: new_country, | ||
| primary: true, | ||
| inactive: false | ||
| ) | ||
|
|
@@ -191,6 +193,7 @@ def create_mailing_address(person) | |
| street_address: field_value("mailing_street"), | ||
| city: new_city, | ||
| state: new_state, | ||
| country: new_country, | ||
| zip_code: field_value("mailing_zip"), | ||
| locality: "Unknown", | ||
| address_type: field_value("mailing_address_type")&.downcase || "unknown", | ||
|
|
@@ -243,6 +246,7 @@ def create_affiliation(person, organization) | |
| def create_agency_address(organization) | ||
| new_city = field_value("agency_city")&.strip | ||
| new_state = field_value("agency_state")&.strip | ||
| new_country = field_value("agency_country")&.strip.presence | ||
|
|
||
| existing = organization.addresses.find_by( | ||
| "LOWER(city) = ? AND LOWER(COALESCE(state, '')) = ?", | ||
|
|
@@ -253,6 +257,7 @@ def create_agency_address(organization) | |
| existing.update!( | ||
| street_address: field_value("agency_street"), | ||
| zip_code: field_value("agency_zip"), | ||
| country: new_country, | ||
| primary: true, | ||
| inactive: false | ||
| ) | ||
|
|
@@ -265,6 +270,7 @@ def create_agency_address(organization) | |
| street_address: field_value("agency_street"), | ||
| city: new_city, | ||
| state: new_state, | ||
| country: new_country, | ||
| zip_code: field_value("agency_zip"), | ||
| locality: "Unknown", | ||
| address_type: "work", | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,6 @@ | ||
| <div class="nested-fields rounded-lg bg-white border border-gray-200 p-6 mb-4"> | ||
| <div class="nested-fields rounded-lg bg-white border border-gray-200 p-6 mb-4" | ||
| data-controller="address-region" | ||
| data-address-region-us-country-value="<%= Address::US_COUNTRY %>"> | ||
| <div class="text-lg font-semibold text-gray-800 mb-4"> | ||
| Address <%= "#" + (f.index.to_i + 1).to_s if f.object.persisted? %><%= ": " + f.object.name if f.object.persisted? %> | ||
| </div> | ||
|
|
@@ -89,12 +91,32 @@ | |
| class: "rounded-md border-gray-300 shadow-sm focus:ring-blue-500 focus:border-blue-500" | ||
| } %> | ||
|
|
||
| <%= f.input :state, | ||
| as: :select, | ||
| collection: us_states, | ||
| input_html: { | ||
| class: "rounded-md border-gray-300 shadow-sm focus:ring-blue-500 focus:border-blue-500" | ||
| } %> | ||
| <%# State: a validated US dropdown for domestic addresses, a free-text region | ||
| input for international ones. The address-region controller shows whichever | ||
| matches the selected country; the hidden one is disabled so it never submits. %> | ||
| <% domestic = f.object.new_record? || f.object.united_states? %> | ||
| <div> | ||
| <div data-address-region-wrapper class="<%= "hidden" unless domestic %>"> | ||
| <%= f.label :state, "State", class: "block text-sm font-medium text-gray-700" %> | ||
| <%= f.select :state, | ||
| us_states, | ||
| { include_blank: "Select a state" }, | ||
| required: true, | ||
| disabled: !domestic, | ||
| data: { address_region_target: "stateSelect" }, | ||
| class: "block w-full rounded-md border-gray-300 shadow-sm focus:ring-blue-500 focus:border-blue-500" %> | ||
| </div> | ||
| <div data-address-region-wrapper class="<%= "hidden" if domestic %>"> | ||
| <%= f.label :state, "State / region", for: f.field_id(:state, :region), | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🤖 From Claude: Both state inputs bind to the same |
||
| class: "block text-sm font-medium text-gray-700" %> | ||
| <%= f.text_field :state, | ||
| id: f.field_id(:state, :region), | ||
| required: true, | ||
| disabled: domestic, | ||
| data: { address_region_target: "stateText" }, | ||
| class: "block w-full rounded-md border-gray-300 shadow-sm focus:ring-blue-500 focus:border-blue-500" %> | ||
| </div> | ||
| </div> | ||
| <%= f.input :zip_code, | ||
| label: "ZIP Code", | ||
| input_html: { | ||
|
|
@@ -116,11 +138,17 @@ | |
| class: "rounded-md border-gray-300 shadow-sm focus:ring-blue-500 focus:border-blue-500" | ||
| } %> | ||
|
|
||
| <%= f.input :country, | ||
| label: "Country", | ||
| input_html: { | ||
| class: "rounded-md border-gray-300 shadow-sm focus:ring-blue-500 focus:border-blue-500" | ||
| } %> | ||
| <%# Country drives the state field above: choosing anything other than the | ||
| United States swaps the state dropdown for a free-text region input. %> | ||
| <div> | ||
| <%= f.label :country, "Country", class: "block text-sm font-medium text-gray-700" %> | ||
| <%= f.select :country, | ||
| country_options, | ||
| { selected: f.object.country.presence || Address::US_COUNTRY }, | ||
| required: true, | ||
| data: { address_region_target: "country", action: "address-region#update" }, | ||
| class: "block w-full rounded-md border-gray-300 shadow-sm focus:ring-blue-500 focus:border-blue-500" %> | ||
| </div> | ||
| <%= f.input :phone, | ||
| label: "Phone", | ||
| input_html: { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🤖 From Claude: The US-state check only runs when
united_states?(blank/US country). International addresses just need a value, so a region like "Ontario" passes. The check itself is case-insensitive (seestate_is_a_us_state) to tolerate legacy lowercase abbreviations the dashboard already normalizes.