From 94227c7fcd2f21fe87b82b7823abd0fc38e70255 Mon Sep 17 00:00:00 2001 From: maebeale Date: Sat, 1 Aug 2026 07:04:39 -0400 Subject: [PATCH 01/14] Collapse org workshop settings & background info sections MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The org edit form's Workshop settings option grid and Background info block forced a long scroll on every edit even though they rarely change. Wrap each in a native
/ so they collapse to a one-line summary — the category summary shows the current selections ("None selected" / "Hospitals") so you can see the value without expanding. Background info stays open on new orgs and when the form has errors so required fields aren't hidden. Co-Authored-By: Claude Opus 4.8 (1M context) --- app/views/organizations/_form.html.erb | 50 +++++++++++++++++--------- 1 file changed, 33 insertions(+), 17 deletions(-) diff --git a/app/views/organizations/_form.html.erb b/app/views/organizations/_form.html.erb index 490e115f1c..6ac8925ef1 100644 --- a/app/views/organizations/_form.html.erb +++ b/app/views/organizations/_form.html.erb @@ -77,29 +77,45 @@ <%= hidden_field_tag "organization[primary_age_category_ids][]", "" %> <% @org_categories_grouped.each do |type, cats| %> <% is_age = type.name == "AgeRange" %> -
<%= type.display_label %>
-
- <% if is_age %> -

Check every age group served, then mark the primary ones.

- <% end %> -
- <% cats.each do |category| %> - <%= render "shared/category_checkbox", param_key: "organization", category: category, - checked: @organization.category_ids.include?(category.id), - is_age: is_age, - primary_checked: is_age && primary_age_ids.include?(category.id) %> + <%# Collapsed to a one-line summary of the current selections so the long + option grid doesn't have to be scrolled past every edit — these rarely + change. Native
/, no JavaScript. %> + <% selected_names = cats.select { |category| @organization.category_ids.include?(category.id) }.map(&:name) %> +
+ +
+ <%= type.display_label %>: + <%= selected_names.any? ? selected_names.to_sentence : "None selected" %> +
+ +
+
+ <% if is_age %> +

Check every age group served, then mark the primary ones.

<% end %> +
+ <% cats.each do |category| %> + <%= render "shared/category_checkbox", param_key: "organization", category: category, + checked: @organization.category_ids.include?(category.id), + is_age: is_age, + primary_checked: is_age && primary_age_ids.include?(category.id) %> + <% end %> +
-
+
<% end %> <% end %> -
-
- Background Info -
+ <%# Collapsible like Workshop settings above. Stays open on a new org (required + fields) or when the form has errors so nothing hides behind the summary. %> + <% background_open = !@organization.persisted? || @organization.errors.any? %> +
> + + Background Info + +
@@ -211,7 +227,7 @@
- +
From c2f5e100c94401d2572ec86e0163c743a47250f8 Mon Sep 17 00:00:00 2001 From: maebeale Date: Sat, 1 Aug 2026 07:20:47 -0400 Subject: [PATCH 02/14] Move disclosure chevron inline and collapse Addresses too MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Put the chevron directly beside each section's label/value (with a hover color) instead of far-right, where it was easy to miss as a click target. Also collapse the Addresses section the same way — usually there's just one address — summarizing to the single address, a count, or "None"; it stays open on new orgs and when an address or the org has validation errors. Co-Authored-By: Claude Opus 4.8 (1M context) --- app/views/organizations/_form.html.erb | 41 +++++++++++++++++--------- 1 file changed, 27 insertions(+), 14 deletions(-) diff --git a/app/views/organizations/_form.html.erb b/app/views/organizations/_form.html.erb index 6ac8925ef1..f1e3084e91 100644 --- a/app/views/organizations/_form.html.erb +++ b/app/views/organizations/_form.html.erb @@ -82,12 +82,10 @@ change. Native
/, no JavaScript. %> <% selected_names = cats.select { |category| @organization.category_ids.include?(category.id) }.map(&:name) %>
- -
- <%= type.display_label %>: - <%= selected_names.any? ? selected_names.to_sentence : "None selected" %> -
- + + <%= type.display_label %>: + <%= selected_names.any? ? selected_names.to_sentence : "None selected" %> +
<% if is_age %> @@ -112,9 +110,9 @@ fields) or when the form has errors so nothing hides behind the summary. %> <% background_open = !@organization.persisted? || @organization.errors.any? %>
> - - Background Info - + + Background Info +
@@ -229,10 +227,25 @@
-
-
- Addresses -
+ <%# Collapsible like the sections above — there's usually just one address, so + collapse to a one-line summary. Stays open on a new org or when an address + (or the org) has errors so validation isn't hidden. %> + <% org_addresses = f.object.addresses.reject(&:marked_for_destruction?) %> + <% addresses_open = !f.object.persisted? || f.object.errors.any? || + org_addresses.any? { |address| address.errors.any? } %> + <% address_summary = if org_addresses.none? + "None" + elsif org_addresses.one? + org_addresses.first.name + else + pluralize(org_addresses.size, "address") + end %> +
> + + Addresses: + <%= address_summary %> + +
@@ -251,7 +264,7 @@ }, class: "btn btn-secondary-outline" %>
-
+
From e2a1d0ef80e0295e50141d882d0060d2f1684d14 Mon Sep 17 00:00:00 2001 From: maebeale Date: Sat, 1 Aug 2026 07:54:45 -0400 Subject: [PATCH 03/14] Collapse config sections on org & person edit forms Wrap the rarely-changed configuration sections on both edit forms in native
/ so they collapse to a one-line summary with an inline chevron, cutting the scroll on every edit. Summaries show the current value (selected sectors/categories, addresses, emails, phones, hidden profile toggles as "Hide X"); Background/Addresses/Emails/Phones/Extras stay open on new records and when there are errors so required/invalid fields aren't hidden. Org: keep name, Windows audience (50/50 with Sectors), Sectors and Workshop settings in the left column beside the logo so it no longer leaves dead space, and move Associated records below Comments (now one row). Add PersonDecorator/OrganizationDecorator#profile_display_summary (+ specs) for the "Hide X" rollup. Co-Authored-By: Claude Opus 4.8 (1M context) --- app/decorators/organization_decorator.rb | 23 ++ app/decorators/person_decorator.rb | 32 +++ .../_associated_records.html.erb | 4 +- app/views/organizations/_form.html.erb | 162 +++++++------- app/views/people/_form.html.erb | 209 ++++++++++++------ .../decorators/organization_decorator_spec.rb | 16 ++ spec/decorators/person_decorator_spec.rb | 16 ++ spec/views/people/edit.html.erb_spec.rb | 12 +- 8 files changed, 324 insertions(+), 150 deletions(-) diff --git a/app/decorators/organization_decorator.rb b/app/decorators/organization_decorator.rb index 34d02d9671..457deca964 100644 --- a/app/decorators/organization_decorator.rb +++ b/app/decorators/organization_decorator.rb @@ -103,6 +103,29 @@ def facilitator_status_as_of(date) active ? :ongoing : :reinstated end + # Profile display toggles in form order, mapped to the noun used on each + # checkbox ("Show email" => "email"). Drives the collapsed form section's + # one-line summary. + PROFILE_DISPLAY_LABELS = { + profile_show_email: "email", + profile_show_phone: "phone", + profile_show_website: "website", + profile_show_description: "description", + profile_show_sectors: "sectors", + profile_show_workshops: "workshops", + profile_show_stories: "stories", + profile_show_events_registered: "events hosted", + profile_show_workshop_logs: "workshop logs" + }.freeze + + # One-line summary of the profile display preferences for the collapsed form + # section. Most orgs show everything, so it names only what's hidden + # ("Hide phone and website") and says "All shown" when nothing is hidden. + def profile_display_summary + hidden = PROFILE_DISPLAY_LABELS.reject { |attr, _| object.public_send(attr) }.values + hidden.any? ? "Hide #{hidden.to_sentence}" : "All shown" + end + def badges earliest = affiliations.minimum(:start_date) || start_date years = earliest ? (Time.zone.now.year - earliest.year) : nil diff --git a/app/decorators/person_decorator.rb b/app/decorators/person_decorator.rb index 09bb4d5421..deba17eda4 100644 --- a/app/decorators/person_decorator.rb +++ b/app/decorators/person_decorator.rb @@ -76,6 +76,38 @@ def badges @badges ||= compute_badges end + # Profile display toggles in form order, mapped to the noun used on each + # checkbox ("Show email" => "email"). Drives the collapsed form section's + # one-line summary. + PROFILE_DISPLAY_LABELS = { + profile_show_credentials: "credentials", + profile_show_pronouns: "pronouns", + profile_show_email: "email", + profile_show_phone: "phone", + profile_show_social_media: "social media", + profile_show_member_since: "facilitator since", + profile_show_bio: "bio", + profile_show_affiliations: "affiliations", + profile_show_sectors: "sectors", + profile_show_workshops: "workshops", + profile_show_workshop_variations: "workshop variations", + profile_show_stories: "stories", + profile_show_resources: "resources", + profile_show_events_registered: "registrations", + profile_show_story_ideas: "story ideas", + profile_show_workshop_ideas: "workshop ideas", + profile_show_workshop_variation_ideas: "variation ideas", + profile_show_workshop_logs: "workshop logs" + }.freeze + + # One-line summary of the profile display preferences for the collapsed form + # section. Most people show everything, so it names only what's hidden + # ("Hide phone and bio") and says "All shown" when nothing is hidden. + def profile_display_summary + hidden = PROFILE_DISPLAY_LABELS.reject { |attr, _| object.public_send(attr) }.values + hidden.any? ? "Hide #{hidden.to_sentence}" : "All shown" + end + def facilitator_since_date @facilitator_since_date ||= begin facilitator_affiliations = affiliations.facilitators diff --git a/app/views/organizations/_associated_records.html.erb b/app/views/organizations/_associated_records.html.erb index 7e83c7a073..324deeccc7 100644 --- a/app/views/organizations/_associated_records.html.erb +++ b/app/views/organizations/_associated_records.html.erb @@ -1,7 +1,9 @@ <% return unless organization.persisted? %> <% org_filter = { organization_id: organization.id } %> <% monthly_reports = MonthlyReport.where(org_filter) %> -
    +<%# One row on md+ (grid-flow-col + equal auto columns); wraps to a 2/3-col grid + on narrower screens. %> +
    • <%= index_button CommunityNews.where(org_filter), params: org_filter %>
    • <%= index_button Story.where(org_filter), params: org_filter %>
    • <%= index_button organization.workshop_logs, params: org_filter %>
    • diff --git a/app/views/organizations/_form.html.erb b/app/views/organizations/_form.html.erb index f1e3084e91..50a62a3f16 100644 --- a/app/views/organizations/_form.html.erb +++ b/app/views/organizations/_form.html.erb @@ -5,32 +5,36 @@ <% automanaged_notice = "Auto-managed by affiliations" %>
      - -
      - -
      -
      - <%= f.input :name, - label: "Organization Name", - as: :text, - required: true, - input_html: { - autofocus: true, - rows: 1, - class: "w-full rounded-md border-gray-300 shadow-sm - focus:border-blue-500 focus:ring focus:ring-blue-200" - } %> -
      -
      + +
      + + <%= f.input :name, + label: "Organization Name", + as: :text, + required: true, + input_html: { + autofocus: true, + rows: 1, + class: "w-full rounded-md border-gray-300 shadow-sm + focus:border-blue-500 focus:ring focus:ring-blue-200" + } %> - -
      -
      -
      - Sectors -
      + + <%# Sectors is collapsible — collapses to the list of tagged sectors. Open on + a new org or when there are errors. %> + <% sector_names = f.object.sectors.map(&:name).reject { |name| name == Sector::OTHER_SECTOR_NAME }.sort %> + <% sectors_open = !f.object.persisted? || f.object.errors.any? %> +
      +
      > + + Sectors: + <%= sector_names.any? ? sector_names.to_sentence : "None selected" %> + + -
      +
      <%= f.simple_fields_for :sectorable_items do |sfi| %> <%= render "shared/sectorable_item_fields", f: sfi %> <% end %> @@ -47,8 +51,9 @@ }, class: "btn btn-secondary-outline" %>
      -
      +
      +
      <%= f.association :windows_type, label: "Windows audience", @@ -59,6 +64,43 @@ } %>
      + + + <% if @org_categories_grouped.present? %> + <% primary_age_ids = @organization.primary_age_category_ids %> +
      + <%# Ensures the primary-age param is always submitted so unchecking every + toggle clears the primary flags. %> + <%= hidden_field_tag "organization[primary_age_category_ids][]", "" %> + <% @org_categories_grouped.each do |type, cats| %> + <% is_age = type.name == "AgeRange" %> + <%# Collapsed to a one-line summary of the current selections so the long + option grid doesn't have to be scrolled past every edit — these rarely + change. Native
      /, no JavaScript. %> + <% selected_names = cats.select { |category| @organization.category_ids.include?(category.id) }.map(&:name) %> +
      + + <%= type.display_label %>: + <%= selected_names.any? ? selected_names.to_sentence : "None selected" %> + + +
      + <% if is_age %> +

      Check every age group served, then mark the primary ones.

      + <% end %> +
      + <% cats.each do |category| %> + <%= render "shared/category_checkbox", param_key: "organization", category: category, + checked: @organization.category_ids.include?(category.id), + is_age: is_age, + primary_checked: is_age && primary_age_ids.include?(category.id) %> + <% end %> +
      +
      +
      + <% end %> +
      + <% end %>
      @@ -68,43 +110,6 @@
      - - <% if @org_categories_grouped.present? %> - <% primary_age_ids = @organization.primary_age_category_ids %> -
      - <%# Ensures the primary-age param is always submitted so unchecking every - toggle clears the primary flags. %> - <%= hidden_field_tag "organization[primary_age_category_ids][]", "" %> - <% @org_categories_grouped.each do |type, cats| %> - <% is_age = type.name == "AgeRange" %> - <%# Collapsed to a one-line summary of the current selections so the long - option grid doesn't have to be scrolled past every edit — these rarely - change. Native
      /, no JavaScript. %> - <% selected_names = cats.select { |category| @organization.category_ids.include?(category.id) }.map(&:name) %> -
      - - <%= type.display_label %>: - <%= selected_names.any? ? selected_names.to_sentence : "None selected" %> - - -
      - <% if is_age %> -

      Check every age group served, then mark the primary ones.

      - <% end %> -
      - <% cats.each do |category| %> - <%= render "shared/category_checkbox", param_key: "organization", category: category, - checked: @organization.category_ids.include?(category.id), - is_age: is_age, - primary_checked: is_age && primary_age_ids.include?(category.id) %> - <% end %> -
      -
      -
      - <% end %> -
      - <% end %> - <%# Collapsible like Workshop settings above. Stays open on a new org (required fields) or when the form has errors so nothing hides behind the summary. %> @@ -386,23 +391,14 @@
      - <% if f.object.persisted? %> -
      -
      - Associated records -
      -
      - <%= render "associated_records", organization: f.object %> -
      -
      - <% end %> - -
      -
      - Profile display preferences: -
      +
      + + Profile display preferences + <%= f.object.decorate.profile_display_summary %> + + -
      +
      <%= f.input :profile_show_email, label: "Show email" %> @@ -423,7 +419,7 @@
      -
      +
      <% if f.object.persisted? %> @@ -457,6 +453,14 @@
      <% end %> +
      +
      + Associated records +
      +
      + <%= render "associated_records", organization: f.object %> +
      +
      <% end %> <% if params[:admin] && allowed_to?(:manage?, Organization) %> diff --git a/app/views/people/_form.html.erb b/app/views/people/_form.html.erb index 948c079c1c..0c018063aa 100644 --- a/app/views/people/_form.html.erb +++ b/app/views/people/_form.html.erb @@ -21,8 +21,17 @@
      <%= f.input :first_name, input_html: { value: f.object.first_name || @user&.first_name } %><%= f.input :legal_first_name, label: "Legal First Name", hint: "If different from first name" %><%= f.input :last_name, input_html: { value: f.object.last_name || @user&.last_name } %><%= f.input :pronouns %>
      -
      -

      Emails

      + <%# Collapsible — collapses to the email(s) on file. Open on a new person + or when there are errors so the required primary email stays visible. %> + <% primary_email = f.object.user&.email.presence || f.object.email.presence %> + <% email_summary = [ primary_email, f.object.email_2.presence ].compact %> + <% emails_open = !f.object.persisted? || f.object.errors.any? %> +
      > + + Emails: + <%= email_summary.any? ? email_summary.to_sentence : "None" %> + +
      @@ -68,7 +77,7 @@ include_blank: true %>
      -
      +
@@ -77,12 +86,18 @@
-
-
- Phones -
- -
+ <%# Collapsible — collapses to the phone number(s) on file. Open on a new + person or when there are errors so an invalid number stays visible. %> + <% phone_values = f.object.contact_methods.reject(&:marked_for_destruction?).filter_map { |cm| cm.value.presence } %> + <% phones_open = !f.object.persisted? || f.object.errors.any? %> +
> + + Phones: + <%= phone_values.any? ? phone_values.to_sentence : "None" %> + + + +
<% if @person.best_time_to_call.present? || params[:admin] == "true" %> <%= f.input :best_time_to_call %> <% end %> @@ -97,19 +112,33 @@ partial: "contact_methods/contact_method_fields", class: "btn btn-secondary-outline" %>
-
+
<%# = f.input :primary_address, collection: [['Work', 1], ['Home', 2]], prompt: "Select Primary Address" %>
-
-
- Addresses -
- -
+ <%# Collapsible — usually just one address, so collapse to a one-line summary. + Stays open on a new person or when an address (or the person) has errors. %> + <% person_addresses = f.object.addresses.reject(&:marked_for_destruction?) %> + <% addresses_open = !f.object.persisted? || f.object.errors.any? || + person_addresses.any? { |address| address.errors.any? } %> + <% address_summary = if person_addresses.none? + "None" + elsif person_addresses.one? + person_addresses.first.name + else + pluralize(person_addresses.size, "address") + end %> +
> + + Addresses: + <%= address_summary %> + + + +
<%= f.simple_fields_for :addresses do |f| %> <%= render "addresses/address_fields", f: f %> <% end %> @@ -119,7 +148,7 @@ :addresses, class: "btn btn-secondary-outline" %>
-
+
@@ -135,21 +164,28 @@ <% (@managed_category_type_ids || []).each do |type_id| %> <%= hidden_field_tag "person[managed_category_type_ids][]", type_id %> <% end %> -
+ <%# Sectors and Age ranges collapse to a one-line summary of their tagged chips + — they rarely change. items-start (not stretch) so a collapsed column keeps + its natural height. Both open on a new person or when there are errors. %> +
-
-
- Sectors -
-
+ <% editable_sector_items = sectors_owner.sectorable_items_ordered.reject { |item| item.sector&.name == Sector::OTHER_SECTOR_NAME } %> + <% sector_names = editable_sector_items.reject(&:marked_for_destruction?).filter_map { |item| item.sector&.name } %> + <% sectors_open = !sectors_owner.persisted? || sectors_owner.errors.any? %> +
> + + Sectors: + <%= sector_names.any? ? sector_names.to_sentence : "None selected" %> + + +
- <% sectors_owner = f.object.respond_to?(:object) ? f.object.object : f.object %> - <%# "Other" is never a real sector tag (it's captured as an OtherResponse), - so drop any stray legacy tagging from the editable list. %> - <% editable_sector_items = sectors_owner.sectorable_items_ordered.reject { |item| item.sector&.name == Sector::OTHER_SECTOR_NAME } %> <%= f.simple_fields_for :sectorable_items, editable_sector_items do |sfi| %> <%= render "shared/sectorable_item_fields", f: sfi, show_admin_flags: true %> <% end %> @@ -165,20 +201,24 @@ class: "btn btn-secondary-outline" %> <%= render "people/other_sector_responses", responses: @person.other_sector_responses, dismissable: true, curatable: true, return_to: "person_edit" %>
-
+ <% if @age_ranges_collection.present? %> -
-
- Age ranges -
-
> + + Age ranges: + <%= age_range_names.any? ? age_range_names.to_sentence : "None selected" %> + + +
- <% age_owner = f.object.respond_to?(:object) ? f.object.object : f.object %> <%= f.simple_fields_for :age_range_categorizable_items, age_owner.age_range_items_ordered do |afi| %> <%= render "shared/age_range_item_fields", f: afi %> <% end %> @@ -192,7 +232,7 @@ .reject { |_, id| (@current_age_range_category_ids || []).include?(id) } } }, class: "btn btn-secondary-outline" %>
-
+ <% end %>
@@ -201,17 +241,26 @@ <% if other_category_types.present? %>
<% other_category_types.each do |type, cats| %> -
-

<%= type.display_label %>

-
- <% cats.each do |category| %> - <%= render "shared/category_checkbox", param_key: "person", category: category, - checked: @person.category_ids.include?(category.id), - is_age: false, - primary_checked: false %> - <% end %> + <%# Collapsed to a one-line summary of the current selections so the long + option grid doesn't have to be scrolled past every edit. %> + <% selected_names = cats.select { |category| @person.category_ids.include?(category.id) }.map(&:name) %> +
+ + <%= type.display_label %>: + <%= selected_names.any? ? selected_names.to_sentence : "None selected" %> + + +
+
+ <% cats.each do |category| %> + <%= render "shared/category_checkbox", param_key: "person", category: category, + checked: @person.category_ids.include?(category.id), + is_age: false, + primary_checked: false %> + <% end %> +
-
+ <% end %> <% workshop_other = @person.other_workshop_setting_responses %> <% if workshop_other.any? %> @@ -335,12 +384,22 @@
-
-
- Background -
+ <%# Collapsible like the sections above. Stays open on a new person or when the + form has errors so nothing hides behind the summary. %> + <% background_open = !f.object.persisted? || f.object.errors.any? %> +
> + + Background + <% if f.object.bio.present? %> + Bio present + <% end %> + <% if f.object.shoutout_text.present? %> + Shout-out present + <% end %> + + -
+
<%# Bio and Shout-out share one row, each half width. %>
@@ -371,13 +430,19 @@
-
- -
-
- Professional licenses -
-
+
+ + <%# Collapsible — collapses to a count of licenses on file. Open on a new person + or when there are errors so an invalid license stays visible. %> + <% licenses = f.object.professional_licenses.reject(&:marked_for_destruction?) %> + <% licenses_open = !f.object.persisted? || f.object.errors.any? %> +
> + + Professional licenses: + <%= licenses.any? ? pluralize(licenses.size, "license") : "None" %> + + +

Used for continuing-education credit. Once a license has CE registrations it can't be removed, and only an admin can edit it.

<%= f.simple_fields_for :professional_licenses do |license_form| %> <%= render "professional_licenses/professional_license_fields", f: license_form %> @@ -388,9 +453,18 @@ partial: "professional_licenses/professional_license_fields", class: "btn btn-secondary-outline" %>
-
- -
+ + + <%# Extras collapses the less-frequently-edited fields — birthday, time zone, + and the admin-only demographics/FileMaker block. Open on a new person or + when there are errors. %> + <% extras_open = !f.object.persisted? || f.object.errors.any? %> +
> + + Extras + + +
@@ -455,6 +529,7 @@
<% end %>
+
@@ -472,12 +547,14 @@
-
-
- Profile display preferences -
+
+ + Profile display preferences + <%= f.object.decorate.profile_display_summary %> + + -
+
<%= f.input :profile_is_searchable, @@ -525,7 +602,7 @@ <%= f.input :profile_show_workshop_logs, label: "Show workshop logs" %>
-
+
<% if f.object.persisted? %> <%# Communications — subjects are visible to the person; the body and the diff --git a/spec/decorators/organization_decorator_spec.rb b/spec/decorators/organization_decorator_spec.rb index 69502ce06d..ac134d1ce8 100644 --- a/spec/decorators/organization_decorator_spec.rb +++ b/spec/decorators/organization_decorator_spec.rb @@ -88,4 +88,20 @@ expect(organization.decorate.agency_type_option).to eq("") end end + + describe "#profile_display_summary" do + it "says everything is shown when no toggle is hidden" do + expect(create(:organization).decorate.profile_display_summary).to eq("All shown") + end + + it "names only the hidden items, prefixed with Hide" do + organization = create(:organization, profile_show_phone: false, profile_show_website: false) + expect(organization.decorate.profile_display_summary).to eq("Hide phone and website") + end + + it "uses the checkbox wording for a hidden item" do + organization = create(:organization, profile_show_events_registered: false) + expect(organization.decorate.profile_display_summary).to eq("Hide events hosted") + end + end end diff --git a/spec/decorators/person_decorator_spec.rb b/spec/decorators/person_decorator_spec.rb index 307b06189b..21498c8de1 100644 --- a/spec/decorators/person_decorator_spec.rb +++ b/spec/decorators/person_decorator_spec.rb @@ -52,4 +52,20 @@ expect(person.decorate.affiliated_since_date).to be_nil end end + + describe "#profile_display_summary" do + it "says everything is shown when no toggle is hidden" do + expect(create(:person).decorate.profile_display_summary).to eq("All shown") + end + + it "names only the hidden items, prefixed with Hide" do + person = create(:person, profile_show_phone: false, profile_show_bio: false) + expect(person.decorate.profile_display_summary).to eq("Hide phone and bio") + end + + it "uses the checkbox wording for a hidden item" do + person = create(:person, profile_show_member_since: false) + expect(person.decorate.profile_display_summary).to eq("Hide facilitator since") + end + end end diff --git a/spec/views/people/edit.html.erb_spec.rb b/spec/views/people/edit.html.erb_spec.rb index 450c3bbba3..b4965e8fc6 100644 --- a/spec/views/people/edit.html.erb_spec.rb +++ b/spec/views/people/edit.html.erb_spec.rb @@ -21,7 +21,9 @@ expect(rendered).to have_field('First name', with: person.first_name) expect(rendered).to have_field('Last name', with: person.last_name) expect(rendered).to have_field('Pronouns', with: person.pronouns) - expect(rendered).to have_checked_field('person_profile_show_pronouns') if person.profile_show_pronouns + # Profile display preferences collapse to a summary by default, so the + # toggle is present but not visible until the section is expanded. + expect(rendered).to have_checked_field('person_profile_show_pronouns', visible: :all) if person.profile_show_pronouns end it "has a link to the show page" do @@ -43,7 +45,9 @@ end it "displays an editable email type field" do - expect(rendered).to have_select('Primary email type') + # Emails collapse to a summary by default, so the field is present but not + # visible until the section is expanded. + expect(rendered).to have_select('Primary email type', visible: :all) end end @@ -56,11 +60,11 @@ end it "displays an editable email field" do - expect(rendered).to have_field('Primary email', with: person_without_user.email) + expect(rendered).to have_field('Primary email', with: person_without_user.email, visible: :all) end it "displays an editable email type field" do - expect(rendered).to have_select('Primary email type') + expect(rendered).to have_select('Primary email type', visible: :all) end end end From a4d206c81ff6c5aef15115f5c8ca523d30d54e46 Mon Sep 17 00:00:00 2001 From: maebeale Date: Sat, 1 Aug 2026 08:28:35 -0400 Subject: [PATCH 04/14] Refine collapsible form sections: layout, summaries, and fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Org: Windows audience is now a collapsible summary parallel (50/50) with Sectors; Associated records moved below the save button (edit page, one row); shrink the logo box via a new size: local on the shared image field. - Person: Emails and Phones sit parallel (50/50), secondary email wraps; name fields go 2-up on mobile; contact-method row flex-wraps in the narrow column; Licenses/Extras collapse; Affiliations/Comments/Communications are collapsible (expanded by default); the shared communications box is now a
. - Sector/age-range collapsed summaries mark the primary (bold + ⭐) and sector leader (👑 + "(sector leader)") via *Decorator#sectors_summary / PersonDecorator#age_ranges_summary (+ specs). - Fix: give the Affiliations/Addresses
named groups so the section's group no longer fires the inner tooltip/nested-field group-hover/open; add pointer-events-none to the affiliation hover tooltips so they can't block the collapse chevron. Co-Authored-By: Claude Opus 4.8 (1M context) --- app/decorators/application_decorator.rb | 22 ++ app/decorators/person_decorator.rb | 16 ++ .../_contact_method_fields.html.erb | 10 +- .../notifications/_communications.html.erb | 29 ++- app/views/organizations/_form.html.erb | 82 ++++--- app/views/organizations/edit.html.erb | 10 + app/views/people/_form.html.erb | 211 +++++++++--------- app/views/shared/_form_image_field.html.erb | 11 +- .../decorators/organization_decorator_spec.rb | 18 ++ spec/decorators/person_decorator_spec.rb | 34 +++ spec/system/event_registration_edit_spec.rb | 4 +- 11 files changed, 289 insertions(+), 158 deletions(-) diff --git a/app/decorators/application_decorator.rb b/app/decorators/application_decorator.rb index 28d7b6fc58..60f12d4b0d 100644 --- a/app/decorators/application_decorator.rb +++ b/app/decorators/application_decorator.rb @@ -23,4 +23,26 @@ def published? def external_link? false end + + # One-line summary of the tagged sectors for a collapsed form section, primary + # first. The primary sector is bold with a ⭐; a sector leader gets a 👑 and a + # trailing "(sector leader)". Excludes the "Other" catch-all. HTML-safe. + def sectors_summary + items = object.sectorable_items_primary_first.reject { |item| item.sector&.name == Sector::OTHER_SECTOR_NAME } + return "None selected" if items.empty? + + h.safe_join(items.map { |item| sector_summary_chip(item) }, ", ") + end + + private + + def sector_summary_chip(item) + name = item.sector&.name.to_s + pieces = [] + pieces << h.content_tag(:i, "", class: "fa-solid fa-crown text-lime-600") if item.is_leader? + pieces << h.content_tag(:i, "", class: "fa-solid fa-star text-amber-400") if item.is_primary? + pieces << (item.is_primary? ? h.content_tag(:strong, name) : name) + pieces << "(sector leader)" if item.is_leader? + h.safe_join(pieces, " ") + end end diff --git a/app/decorators/person_decorator.rb b/app/decorators/person_decorator.rb index deba17eda4..bc78616fd5 100644 --- a/app/decorators/person_decorator.rb +++ b/app/decorators/person_decorator.rb @@ -108,6 +108,22 @@ def profile_display_summary hidden.any? ? "Hide #{hidden.to_sentence}" : "All shown" end + # One-line summary of the tagged age ranges for a collapsed form section. The + # primary age group is bold with a ⭐ (age ranges have no leader flag). HTML-safe. + def age_ranges_summary + items = object.age_range_items_ordered + return "None selected" if items.empty? + + h.safe_join(items.map { |item| + name = item.category&.name.to_s + if item.is_primary? + h.safe_join([ h.content_tag(:i, "", class: "fa-solid fa-star text-amber-400"), h.content_tag(:strong, name) ], " ") + else + name + end + }, ", ") + end + def facilitator_since_date @facilitator_since_date ||= begin facilitator_affiliations = affiliations.facilitators diff --git a/app/views/contact_methods/_contact_method_fields.html.erb b/app/views/contact_methods/_contact_method_fields.html.erb index ee19ec2736..773d730901 100644 --- a/app/views/contact_methods/_contact_method_fields.html.erb +++ b/app/views/contact_methods/_contact_method_fields.html.erb @@ -1,8 +1,10 @@
<%= f.hidden_field :kind, value: :phone %> -
+ <%# Flex-wrap (not a fixed 5-col grid) so the row reflows to multiple lines in a + narrow container, e.g. the half-width Phones column on the person form. %> +
-
+
<%= f.input :contact_type, as: :select, required: true, @@ -12,7 +14,7 @@
-
+
<%= f.input :value, wrapper: false, label: "Value", @@ -39,7 +41,7 @@ input_html: { class: "mr-2 text-blue-600 focus:ring-blue-500" } %>
-
+
<%= link_to_remove_association "Remove", f, diff --git a/app/views/notifications/_communications.html.erb b/app/views/notifications/_communications.html.erb index 99aa780bcc..6985939f89 100644 --- a/app/views/notifications/_communications.html.erb +++ b/app/views/notifications/_communications.html.erb @@ -26,23 +26,28 @@ id) rather than the fresh query row, so an admin's unsaved inline edits survive a validation re-render instead of reverting to the saved values. %> <% own_notifications_by_id = admin ? record.notifications.index_by(&:id) : {} %> -
-
+<%# Collapsible (expanded by default) so long communication logs don't dominate + the form; the header row is the disclosure summary. %> +
+

<%= t(title_key) %>

- <% if email.present? && admin %> - <%= link_to notifications_path(email: email), - class: "ml-auto inline-flex items-center gap-1.5 text-xs font-medium text-gray-500 hover:text-gray-700 hover:underline", - target: "_blank", rel: "noopener" do %> - View all - - <% end %> - <% end %> -
+ +
+ <% if email.present? && admin %> +
+ <%= link_to notifications_path(email: email), + class: "inline-flex items-center gap-1.5 text-xs font-medium text-gray-500 hover:text-gray-700 hover:underline", + target: "_blank", rel: "noopener" do %> + View all + + <% end %> +
+ <% end %>
<% notifications.each do |notification| %> @@ -93,4 +98,4 @@
<% end %>
-
+
diff --git a/app/views/organizations/_form.html.erb b/app/views/organizations/_form.html.erb index 50a62a3f16..d2d4f402b1 100644 --- a/app/views/organizations/_form.html.erb +++ b/app/views/organizations/_form.html.erb @@ -24,13 +24,12 @@ <%# Sectors is collapsible — collapses to the list of tagged sectors. Open on a new org or when there are errors. %> - <% sector_names = f.object.sectors.map(&:name).reject { |name| name == Sector::OTHER_SECTOR_NAME }.sort %> <% sectors_open = !f.object.persisted? || f.object.errors.any? %>
> Sectors: - <%= sector_names.any? ? sector_names.to_sentence : "None selected" %> + <%= f.object.decorate.sectors_summary %> @@ -54,15 +53,23 @@
-
- <%= f.association :windows_type, - label: "Windows audience", - include_blank: true, - required: false, - input_html: { - class: "rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-200" - } %> -
+ <% windows_open = !f.object.persisted? || f.object.errors.any? %> +
> + + Windows audience: + <%= f.object.windows_type&.name.presence || "None selected" %> + + +
+ <%= f.association :windows_type, + label: false, + include_blank: true, + required: false, + input_html: { + class: "rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-200" + } %> +
+
@@ -106,7 +113,7 @@
-
<%= render "shared/form_image_field", f: f, field_name: :logo %>
+
<%= render "shared/form_image_field", f: f, field_name: :logo, size: "w-24 h-24" %>
@@ -245,11 +252,11 @@ else pluralize(org_addresses.size, "address") end %> -
> +
> Addresses: <%= address_summary %> - +
@@ -271,12 +278,17 @@
-
-
- Affiliations -
+
+ + Affiliations + <% affiliation_count = f.object.affiliations.reject(&:marked_for_destruction?).size %> + <% if affiliation_count.positive? %> + <%= pluralize(affiliation_count, "affiliation") %> + <% end %> + + -
+
<% if show_status_select %>
<%= f.input :organization_status_id, @@ -306,7 +318,7 @@ -
@@ -424,10 +436,17 @@ <% if f.object.persisted? %> <% if allowed_to?(:manage?, Comment) %> -
-
Comments
+
+ <% comment_count = f.object.comments.reject(&:marked_for_destruction?).size %> + + Comments + <% if comment_count.positive? %> + <%= pluralize(comment_count, "comment") %> + <% end %> + + -
+
<%= f.simple_fields_for :comments do |cf| %> <%= render "comments/comment_fields", f: cf %> <% end %> @@ -450,17 +469,8 @@ partial: "comments/comment_fields", class: "btn btn-secondary-outline" %>
-
+
<% end %> - -
-
- Associated records -
-
- <%= render "associated_records", organization: f.object %> -
-
<% end %> <% if params[:admin] && allowed_to?(:manage?, Organization) %> diff --git a/app/views/organizations/edit.html.erb b/app/views/organizations/edit.html.erb index 67331a1c8b..e8fa99ea70 100644 --- a/app/views/organizations/edit.html.erb +++ b/app/views/organizations/edit.html.erb @@ -20,5 +20,15 @@ <%= render "form" %> <%= render "shared/audit_info", resource: @organization %>
+ <% if @organization.persisted? %> +
+
+ Associated records +
+
+ <%= render "associated_records", organization: @organization %> +
+
+ <% end %>
diff --git a/app/views/people/_form.html.erb b/app/views/people/_form.html.erb index 0c018063aa..8390b7cd7c 100644 --- a/app/views/people/_form.html.erb +++ b/app/views/people/_form.html.erb @@ -19,65 +19,95 @@
-
<%= f.input :first_name, input_html: { value: f.object.first_name || @user&.first_name } %><%= f.input :legal_first_name, label: "Legal First Name", hint: "If different from first name" %><%= f.input :last_name, input_html: { value: f.object.last_name || @user&.last_name } %><%= f.input :pronouns %>
+ <%# 2-up on mobile (First | Legal, Last | Pronouns), 4-across on md+. %> +
<%= f.input :first_name, input_html: { value: f.object.first_name || @user&.first_name } %><%= f.input :legal_first_name, label: "Legal First Name", hint: "If different from first name" %><%= f.input :last_name, input_html: { value: f.object.last_name || @user&.last_name } %><%= f.input :pronouns %>
- <%# Collapsible — collapses to the email(s) on file. Open on a new person - or when there are errors so the required primary email stays visible. %> + <%# Emails and Phones sit parallel (50/50). Each collapses to the value(s) + on file; both open on a new person or when there are errors. %> <% primary_email = f.object.user&.email.presence || f.object.email.presence %> <% email_summary = [ primary_email, f.object.email_2.presence ].compact %> - <% emails_open = !f.object.persisted? || f.object.errors.any? %> -
> - - Emails: - <%= email_summary.any? ? email_summary.to_sentence : "None" %> - - - -
-
- <% if f.object.user %> -
- - <%= f.object.user&.email %> - <% if f.object.user.unconfirmed_email.present? && allowed_to?(:show_email_change?, f.object) %> -
- - Change to <%= f.object.user.unconfirmed_email %> awaiting confirmation -
- <% end %> - -

- Only - <% if allowed_to?(:edit?, f.object.user) %> - <%= link_to "editable by admins", - edit_user_path(f.object.user), - class: "underline" %> - <% else %> - editable by admins + <% contacts_open = !f.object.persisted? || f.object.errors.any? %> + <% phone_values = f.object.contact_methods.reject(&:marked_for_destruction?).filter_map { |cm| cm.value.presence } %> +

+
> + + Emails: + <%= email_summary.any? ? email_summary.to_sentence : "None" %> + + + +
+ <%# Two per row so the secondary email + type wrap to the next line + in the narrow half-width column. %> +
+ <% if f.object.user %> +
+ + <%= f.object.user&.email %> + <% if f.object.user.unconfirmed_email.present? && allowed_to?(:show_email_change?, f.object) %> +
+ + Change to <%= f.object.user.unconfirmed_email %> awaiting confirmation +
<% end %> -

-
- <% else %> - <%= f.input :email, label: "Primary email" %> - <% end %> - <%= f.input :email_type, - label: "Primary email type", - as: :select, - collection: Person::CONTACT_TYPES.compact.map { |type| [type.to_s.humanize, type] }, - selected: f.object.email_type || f.object.user&.email_type, - include_blank: true %> +

+ Only + <% if allowed_to?(:edit?, f.object.user) %> + <%= link_to "editable by admins", + edit_user_path(f.object.user), + class: "underline" %> + <% else %> + editable by admins + <% end %> +

+
+ <% else %> + <%= f.input :email, label: "Primary email" %> + <% end %> + + <%= f.input :email_type, + label: "Primary email type", + as: :select, + collection: Person::CONTACT_TYPES.compact.map { |type| [type.to_s.humanize, type] }, + selected: f.object.email_type || f.object.user&.email_type, + include_blank: true %> + + <%= f.input :email_2, label: "Secondary email" %> + + <%= f.input :email_2_type, + label: "Secondary email type", + as: :select, + collection: Person::CONTACT_TYPES.compact.map { |type| [type.to_s.humanize, type] }, + include_blank: true %> +
+
+
+ +
> + + Phones: + <%= phone_values.any? ? phone_values.to_sentence : "None" %> + + + +
+ <% if @person.best_time_to_call.present? || params[:admin] == "true" %> + <%= f.input :best_time_to_call %> + <% end %> - <%= f.input :email_2, label: "Secondary email" %> + <%= f.simple_fields_for :contact_methods do |cf| %> + <%= render "contact_methods/contact_method_fields", f: cf %> + <% end %> - <%= f.input :email_2_type, - label: "Secondary email type", - as: :select, - collection: Person::CONTACT_TYPES.compact.map { |type| [type.to_s.humanize, type] }, - include_blank: true %> + <%= link_to_add_association "➕ Add phone", + f, + :contact_methods, + partial: "contact_methods/contact_method_fields", + class: "btn btn-secondary-outline" %>
-
- + +
@@ -85,36 +115,6 @@
<%= render "shared/form_image_field", f: f, field_name: :avatar %>
-
- <%# Collapsible — collapses to the phone number(s) on file. Open on a new - person or when there are errors so an invalid number stays visible. %> - <% phone_values = f.object.contact_methods.reject(&:marked_for_destruction?).filter_map { |cm| cm.value.presence } %> - <% phones_open = !f.object.persisted? || f.object.errors.any? %> -
> - - Phones: - <%= phone_values.any? ? phone_values.to_sentence : "None" %> - - - -
- <% if @person.best_time_to_call.present? || params[:admin] == "true" %> - <%= f.input :best_time_to_call %> - <% end %> - - <%= f.simple_fields_for :contact_methods do |f| %> - <%= render "contact_methods/contact_method_fields", f: f %> - <% end %> - - <%= link_to_add_association "➕ Add phone", - f, - :contact_methods, - partial: "contact_methods/contact_method_fields", - class: "btn btn-secondary-outline" %> -
-
-
-
<%# = f.input :primary_address, collection: [['Work', 1], ['Home', 2]], prompt: "Select Primary Address" %>
@@ -131,11 +131,11 @@ else pluralize(person_addresses.size, "address") end %> -
> +
> Addresses: <%= address_summary %> - +
@@ -173,12 +173,11 @@ <%# "Other" is never a real sector tag (it's captured as an OtherResponse), so drop any stray legacy tagging from the editable list. %> <% editable_sector_items = sectors_owner.sectorable_items_ordered.reject { |item| item.sector&.name == Sector::OTHER_SECTOR_NAME } %> - <% sector_names = editable_sector_items.reject(&:marked_for_destruction?).filter_map { |item| item.sector&.name } %> <% sectors_open = !sectors_owner.persisted? || sectors_owner.errors.any? %>
> Sectors: - <%= sector_names.any? ? sector_names.to_sentence : "None selected" %> + <%= sectors_owner.decorate.sectors_summary %>
> Age ranges: - <%= age_range_names.any? ? age_range_names.to_sentence : "None selected" %> + <%= age_owner.decorate.age_ranges_summary %>
-
- Affiliations (only editable by admins) -
+
+ + Affiliations + <% affiliation_count = person.affiliations.reject(&:marked_for_destruction?).size %> + <% if affiliation_count.positive? %> + <%= pluralize(affiliation_count, "affiliation") %> + <% end %> + (only editable by admins) + + -
+
<% if allowed_to?(:manage?, Person) && params[:admin].present? %>
<%= f.input :member_since, @@ -305,7 +309,7 @@ -
<%# Collapsible like the sections above. Stays open on a new person or when the @@ -439,7 +443,7 @@
> Professional licenses: - <%= licenses.any? ? pluralize(licenses.size, "license") : "None" %> + <%= licenses.any? ? licenses.map(&:name).to_sentence : "None" %>
@@ -612,10 +616,17 @@ <% if allowed_to?(:manage?, Comment) %> -
-
Comments
+
+ <% comment_count = f.object.comments.reject(&:marked_for_destruction?).size %> + + Comments + <% if comment_count.positive? %> + <%= pluralize(comment_count, "comment") %> + <% end %> + + -
<%= f.simple_fields_for :comments do |cf| %> @@ -637,7 +648,7 @@ partial: "comments/comment_fields", class: "btn btn-secondary-outline" %>
-
+
<% end %> <% end %> diff --git a/app/views/shared/_form_image_field.html.erb b/app/views/shared/_form_image_field.html.erb index e9bb300a0e..f0ef6184f5 100644 --- a/app/views/shared/_form_image_field.html.erb +++ b/app/views/shared/_form_image_field.html.erb @@ -16,6 +16,9 @@ <% hint ||= nil %> <% rounded ||= false %> <% multiple ||= false %> +<%# Tailwind size classes for the preview/placeholder box; callers can pass a + smaller box (e.g. "w-24 h-24") to keep a section compact. %> +<% size ||= "w-32 h-32" %> <% file ||= nil %> <% use_profile_placeholder ||= false %> <% show_file_field = show_file_field.to_s.present? ? show_file_field : true %> @@ -52,7 +55,7 @@ id: image_dom_id, alt: "#{label}", data: { file_preview_target: "preview" }, - class: "w-32 h-32 object-cover border border-gray-300 shadow-sm #{rounded ? 'rounded-full' : ''}" %> + class: "#{size} object-cover border border-gray-300 shadow-sm #{rounded ? 'rounded-full' : ''}" %> <%# ---------- PDF PREVIEW ---------- %> <% elsif is_pdf && file.previewable? %> @@ -60,14 +63,14 @@ id: image_dom_id, alt: "#{label} PDF preview", data: { file_preview_target: "preview" }, - class: "w-32 h-32 object-contain border border-gray-300 shadow-sm" %> + class: "#{size} object-contain border border-gray-300 shadow-sm" %> <%# ---------- PLACEHOLDER ---------- %> <% else %> -
+
+ class="hidden <%= size %> object-cover border border-gray-300 shadow-sm">
Health/Medical", "(sector leader)") + expect(summary).to include("Housing") + end + end end diff --git a/spec/decorators/person_decorator_spec.rb b/spec/decorators/person_decorator_spec.rb index 21498c8de1..ade4361210 100644 --- a/spec/decorators/person_decorator_spec.rb +++ b/spec/decorators/person_decorator_spec.rb @@ -68,4 +68,38 @@ expect(person.decorate.profile_display_summary).to eq("Hide facilitator since") end end + + describe "#sectors_summary" do + it "is 'None selected' with no sectors" do + expect(create(:person).decorate.sectors_summary).to eq("None selected") + end + + it "bolds and stars the primary, crowns and labels the leader" do + person = create(:person) + health = create(:sector, name: "Health/Medical") + create(:sectorable_item, sectorable: person, sector: health, is_primary: true, is_leader: true) + + summary = person.reload.decorate.sectors_summary + expect(summary).to include("fa-star", "fa-crown", "Health/Medical", "(sector leader)") + end + end + + describe "#age_ranges_summary" do + it "is 'None selected' with no age ranges" do + expect(create(:person).decorate.age_ranges_summary).to eq("None selected") + end + + it "bolds and stars the primary age range (no crown)" do + person = create(:person) + age_type = create(:category_type, :published, name: "AgeRange") + kids = create(:category, :published, category_type: age_type, name: "Children (0-12)") + teens = create(:category, :published, category_type: age_type, name: "Teens (13-17)") + create(:categorizable_item, categorizable: person, category: kids, is_primary: true) + create(:categorizable_item, categorizable: person, category: teens) + + summary = person.reload.decorate.age_ranges_summary + expect(summary).to include("fa-star", "Children (0-12)", "Teens (13-17)") + expect(summary).not_to include("fa-crown") + end + end end diff --git a/spec/system/event_registration_edit_spec.rb b/spec/system/event_registration_edit_spec.rb index caffa473cd..8889c2c50a 100644 --- a/spec/system/event_registration_edit_spec.rb +++ b/spec/system/event_registration_edit_spec.rb @@ -248,7 +248,7 @@ sign_in(admin) visit edit_event_registration_path(registration) - within("section", text: "Registration communications") do + within("details", text: "Registration communications") do expect(page).to have_text("Event registration confirmed") expect(page).to have_no_link("Event registration confirmed") expect(page).to have_link("View all") @@ -259,7 +259,7 @@ sign_in(admin) visit edit_event_registration_path(registration) - within("section", text: "Registration communications") do + within("details", text: "Registration communications") do click_on "Add communication" fill_in "Subject", with: "Called about parking" end From c72a374152a6c226869438e76b400e4c793207f4 Mon Sep 17 00:00:00 2001 From: maebeale Date: Sat, 1 Aug 2026 11:49:31 -0400 Subject: [PATCH 05/14] Force org associated records onto one row The grid-flow-col approach still wrapped to two rows; use a flex row (each item flex-1, min-w-0 so labels truncate rather than wrap) so all associated-record buttons sit on a single equal-width row on md+, stacking only on mobile. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../organizations/_associated_records.html.erb | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/app/views/organizations/_associated_records.html.erb b/app/views/organizations/_associated_records.html.erb index 324deeccc7..c16cddc461 100644 --- a/app/views/organizations/_associated_records.html.erb +++ b/app/views/organizations/_associated_records.html.erb @@ -1,15 +1,15 @@ <% return unless organization.persisted? %> <% org_filter = { organization_id: organization.id } %> <% monthly_reports = MonthlyReport.where(org_filter) %> -<%# One row on md+ (grid-flow-col + equal auto columns); wraps to a 2/3-col grid - on narrower screens. %> -
    -
  • <%= index_button CommunityNews.where(org_filter), params: org_filter %>
  • -
  • <%= index_button Story.where(org_filter), params: org_filter %>
  • -
  • <%= index_button organization.workshop_logs, params: org_filter %>
  • -
  • <%= index_button organization.event_registrations, params: org_filter %>
  • -
  • <%= index_button StoryIdea.where(org_filter), params: org_filter %>
  • +<%# Stacked on mobile, a single equal-width row on md+ (each item flex-1, shrinks + with truncated labels rather than wrapping to a second row). %> +
      +
    • <%= index_button CommunityNews.where(org_filter), params: org_filter %>
    • +
    • <%= index_button Story.where(org_filter), params: org_filter %>
    • +
    • <%= index_button organization.workshop_logs, params: org_filter %>
    • +
    • <%= index_button organization.event_registrations, params: org_filter %>
    • +
    • <%= index_button StoryIdea.where(org_filter), params: org_filter %>
    • <% if monthly_reports.exists? %> -
    • <%= index_button monthly_reports, params: org_filter %>
    • +
    • <%= index_button monthly_reports, params: org_filter %>
    • <% end %>
    From d714e335124c3a39afccaf8d3c2891e58b5ba542 Mon Sep 17 00:00:00 2001 From: maebeale Date: Sat, 1 Aug 2026 11:55:45 -0400 Subject: [PATCH 06/14] Alphabetize org associated-record buttons Order by label so Stories and Story ideas land next to each other; keeps the single-row layout. Co-Authored-By: Claude Opus 4.8 (1M context) --- app/views/organizations/_associated_records.html.erb | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/app/views/organizations/_associated_records.html.erb b/app/views/organizations/_associated_records.html.erb index c16cddc461..58103b954d 100644 --- a/app/views/organizations/_associated_records.html.erb +++ b/app/views/organizations/_associated_records.html.erb @@ -2,14 +2,15 @@ <% org_filter = { organization_id: organization.id } %> <% monthly_reports = MonthlyReport.where(org_filter) %> <%# Stacked on mobile, a single equal-width row on md+ (each item flex-1, shrinks - with truncated labels rather than wrapping to a second row). %> + with truncated labels rather than wrapping to a second row). Alphabetical by + label, which keeps Stories and Story ideas adjacent. %>
    • <%= index_button CommunityNews.where(org_filter), params: org_filter %>
    • -
    • <%= index_button Story.where(org_filter), params: org_filter %>
    • -
    • <%= index_button organization.workshop_logs, params: org_filter %>
    • <%= index_button organization.event_registrations, params: org_filter %>
    • -
    • <%= index_button StoryIdea.where(org_filter), params: org_filter %>
    • <% if monthly_reports.exists? %>
    • <%= index_button monthly_reports, params: org_filter %>
    • <% end %> +
    • <%= index_button Story.where(org_filter), params: org_filter %>
    • +
    • <%= index_button StoryIdea.where(org_filter), params: org_filter %>
    • +
    • <%= index_button organization.workshop_logs, params: org_filter %>
    From 10b6c1b2a1f3ba75595314b26f0e7ddf70ba351b Mon Sep 17 00:00:00 2001 From: maebeale Date: Sat, 1 Aug 2026 20:25:51 -0400 Subject: [PATCH 07/14] Make org associated-record buttons two lines tall, equal width Add a wrap: option to index_button that renders a fixed two-line-tall button whose label wraps (line-clamp-2 + min-h) instead of truncating on one line, so the single-row org associated-records buttons all line up to the same height. Co-Authored-By: Claude Opus 4.8 (1M context) --- app/helpers/application_helper.rb | 12 ++++++++---- .../_associated_records.html.erb | 19 ++++++++++--------- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 99b790e99c..6144c9eb29 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -293,7 +293,10 @@ def form_field_option_source(field) # model identity (DomainTheme color + default label + default icon + count # + default index path). Pass `params:` for filter params, or `path:` to # override entirely (e.g. nested routes). - def index_button(collection, params: {}, path: nil, label: nil, icon: nil, hide_count: false, hide_icon: false, data: {}) + # wrap: renders a fixed two-line-tall button whose label wraps (rather than + # truncating on one line) — used where buttons share a row at equal width, so + # long labels stay readable and every button lines up to the same height. + def index_button(collection, params: {}, path: nil, label: nil, icon: nil, hide_count: false, hide_icon: false, wrap: false, data: {}) klass = collection.klass key = klass.name.underscore.pluralize.to_sym label ||= key.to_s.humanize @@ -308,24 +311,25 @@ def index_button(collection, params: {}, path: nil, label: nil, icon: nil, hide_ link_to path, data: { turbo_prefetch: false }.merge(data), class: "group flex items-center gap-3 w-full px-3 py-2 rounded-lg + #{'min-h-[3.5rem]' if wrap} border #{border} #{bg} #{hover_bg} transition-colors duration-200 shadow-sm" do icon_tag = if hide_icon "".html_safe else - content_tag(:span, class: "#{text} w-5 text-center") do + content_tag(:span, class: "#{text} w-5 text-center shrink-0") do content_tag(:i, "", class: "fa-solid #{icon}") end end - label_tag = content_tag(:span, label, class: "font-medium #{text} truncate") + label_tag = content_tag(:span, label, class: "font-medium #{text} #{wrap ? 'line-clamp-2' : 'truncate'}") count_tag = if hide_count "".html_safe else content_tag(:span, number_with_delimiter(collection.count), - class: "ml-auto inline-flex items-center justify-center min-w-[2.25rem] px-2 py-0.5 text-sm font-semibold rounded-full bg-white #{text} border #{border}") + class: "ml-auto shrink-0 inline-flex items-center justify-center min-w-[2.25rem] px-2 py-0.5 text-sm font-semibold rounded-full bg-white #{text} border #{border}") end icon_tag + label_tag + count_tag diff --git a/app/views/organizations/_associated_records.html.erb b/app/views/organizations/_associated_records.html.erb index 58103b954d..2d59e78d11 100644 --- a/app/views/organizations/_associated_records.html.erb +++ b/app/views/organizations/_associated_records.html.erb @@ -1,16 +1,17 @@ <% return unless organization.persisted? %> <% org_filter = { organization_id: organization.id } %> <% monthly_reports = MonthlyReport.where(org_filter) %> -<%# Stacked on mobile, a single equal-width row on md+ (each item flex-1, shrinks - with truncated labels rather than wrapping to a second row). Alphabetical by - label, which keeps Stories and Story ideas adjacent. %> +<%# Stacked on mobile, a single equal-width row on md+ (each item flex-1). Buttons + are two lines tall (wrap: true) so long labels wrap and every button lines up + to the same height. Alphabetical by label, which keeps Stories and Story ideas + adjacent. %>
      -
    • <%= index_button CommunityNews.where(org_filter), params: org_filter %>
    • -
    • <%= index_button organization.event_registrations, params: org_filter %>
    • +
    • <%= index_button CommunityNews.where(org_filter), params: org_filter, wrap: true %>
    • +
    • <%= index_button organization.event_registrations, params: org_filter, wrap: true %>
    • <% if monthly_reports.exists? %> -
    • <%= index_button monthly_reports, params: org_filter %>
    • +
    • <%= index_button monthly_reports, params: org_filter, wrap: true %>
    • <% end %> -
    • <%= index_button Story.where(org_filter), params: org_filter %>
    • -
    • <%= index_button StoryIdea.where(org_filter), params: org_filter %>
    • -
    • <%= index_button organization.workshop_logs, params: org_filter %>
    • +
    • <%= index_button Story.where(org_filter), params: org_filter, wrap: true %>
    • +
    • <%= index_button StoryIdea.where(org_filter), params: org_filter, wrap: true %>
    • +
    • <%= index_button organization.workshop_logs, params: org_filter, wrap: true %>
    From a70508a50fd96649eeb29c7454148ceb1d4497de Mon Sep 17 00:00:00 2001 From: maebeale Date: Sat, 1 Aug 2026 20:33:00 -0400 Subject: [PATCH 08/14] Break "Story ideas" associated-record button onto two lines Give the Story ideas button an explicit two-line label so it matches the two-lines-tall neighbors; the shared min-h already keeps every button (Stories included) the same two-line height. Co-Authored-By: Claude Opus 4.8 (1M context) --- app/views/organizations/_associated_records.html.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/organizations/_associated_records.html.erb b/app/views/organizations/_associated_records.html.erb index 2d59e78d11..8ee1312402 100644 --- a/app/views/organizations/_associated_records.html.erb +++ b/app/views/organizations/_associated_records.html.erb @@ -12,6 +12,6 @@
  • <%= index_button monthly_reports, params: org_filter, wrap: true %>
  • <% end %>
  • <%= index_button Story.where(org_filter), params: org_filter, wrap: true %>
  • -
  • <%= index_button StoryIdea.where(org_filter), params: org_filter, wrap: true %>
  • +
  • <%= index_button StoryIdea.where(org_filter), params: org_filter, wrap: true, label: safe_join([ "Story", tag.br, "ideas" ]) %>
  • <%= index_button organization.workshop_logs, params: org_filter, wrap: true %>
From 41e394649b2970b42b6671a021a9ffaef48c8620 Mon Sep 17 00:00:00 2001 From: maebeale Date: Sat, 1 Aug 2026 20:35:44 -0400 Subject: [PATCH 09/14] Make wrap: index buttons fill the row height so all match the tallest min-h-[3.5rem] was shorter than an actual two-line button, so single-line buttons (e.g. Stories) came up short and their count pill sat higher. Add h-full so each button stretches to the flex row's tallest sibling, aligning every box height and centering the counts in the same vertical space. Co-Authored-By: Claude Opus 4.8 (1M context) --- app/helpers/application_helper.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 6144c9eb29..db6f05f767 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -311,7 +311,7 @@ def index_button(collection, params: {}, path: nil, label: nil, icon: nil, hide_ link_to path, data: { turbo_prefetch: false }.merge(data), class: "group flex items-center gap-3 w-full px-3 py-2 rounded-lg - #{'min-h-[3.5rem]' if wrap} + #{'h-full min-h-[3.5rem]' if wrap} border #{border} #{bg} #{hover_bg} transition-colors duration-200 shadow-sm" do icon_tag = if hide_icon From 8ad35d982934240d62d54558d8be812b5609510b Mon Sep 17 00:00:00 2001 From: maebeale Date: Sat, 1 Aug 2026 20:38:07 -0400 Subject: [PATCH 10/14] Summarize org Background Info in its collapsed toggle Lead with the organization type (the specify-text when "Other"), then a pill for each filled optional field (Email, Website, Description, Mission/vision/values). FileMaker Project ID is left out since it's admin-only internal data and the summary is visible to org owners. Co-Authored-By: Claude Opus 4.8 (1M context) --- app/decorators/organization_decorator.rb | 25 +++++++++++++++++++ app/views/organizations/_form.html.erb | 5 ++-- .../decorators/organization_decorator_spec.rb | 20 +++++++++++++++ 3 files changed, 48 insertions(+), 2 deletions(-) diff --git a/app/decorators/organization_decorator.rb b/app/decorators/organization_decorator.rb index 457deca964..a2193132c4 100644 --- a/app/decorators/organization_decorator.rb +++ b/app/decorators/organization_decorator.rb @@ -56,6 +56,31 @@ def agency_type_option Organization::AGENCY_TYPE_OTHER end + # Optional Background-info fields, mapped to the short pill label shown in the + # collapsed section summary when they're filled in. + BACKGROUND_FIELD_LABELS = { + email: "Email", + website_url: "Website", + description: "Description", + mission_vision_values: "Mission/vision/values" + }.freeze + + # One-line summary for the collapsed Background Info section: the organization + # type (the specify-text when it's "Other"), followed by a pill for each filled + # optional field. FileMaker ID is intentionally left out — it's admin-only. + # HTML-safe. + def background_summary + type = agency_type_option.presence + type = "#{type}: #{object.agency_type_other}" if type == Organization::AGENCY_TYPE_OTHER && object.agency_type_other.present? + + parts = [ h.content_tag(:span, type || "Type not set", class: "text-gray-600") ] + BACKGROUND_FIELD_LABELS.each do |attr, pill_label| + next if object.public_send(attr).blank? + parts << h.content_tag(:span, pill_label, class: "text-xs font-normal px-2 py-0.5 rounded-full bg-gray-100 text-gray-600") + end + h.safe_join(parts, " ") + end + def detail(length: nil) length ? description&.truncate(length) : description end diff --git a/app/views/organizations/_form.html.erb b/app/views/organizations/_form.html.erb index d2d4f402b1..46026aaa41 100644 --- a/app/views/organizations/_form.html.erb +++ b/app/views/organizations/_form.html.erb @@ -122,8 +122,9 @@ fields) or when the form has errors so nothing hides behind the summary. %> <% background_open = !@organization.persisted? || @organization.errors.any? %>
> - - Background Info + + Background Info: + <%= f.object.decorate.background_summary %> diff --git a/spec/decorators/organization_decorator_spec.rb b/spec/decorators/organization_decorator_spec.rb index 7b632b3b07..661f268e83 100644 --- a/spec/decorators/organization_decorator_spec.rb +++ b/spec/decorators/organization_decorator_spec.rb @@ -105,6 +105,26 @@ end end + describe "#background_summary" do + it "leads with the organization type" do + org = create(:organization, agency_type: "501c3/nonprofit") + expect(org.decorate.background_summary).to include("501c3/nonprofit") + end + + it "shows the specify-text for an 'Other' type" do + org = create(:organization, agency_type: Organization::AGENCY_TYPE_OTHER, agency_type_other: "Co-op") + expect(org.decorate.background_summary).to include("Co-op") + end + + it "adds a pill for each filled optional field and omits blank ones" do + org = create(:organization, agency_type: "501c3/nonprofit", email: "hi@example.org", + description: "About us", website_url: "", mission_vision_values: "") + summary = org.decorate.background_summary + expect(summary).to include("Email", "Description") + expect(summary).not_to include("Website", "Mission/vision/values") + end + end + describe "#sectors_summary" do it "is 'None selected' with no sectors" do expect(create(:organization).decorate.sectors_summary).to eq("None selected") From 000f79904f0ffe80090905e17dc292ad906bf623 Mon Sep 17 00:00:00 2001 From: maebeale Date: Sat, 1 Aug 2026 20:58:35 -0400 Subject: [PATCH 11/14] Hanging-indent sector/age-range summaries; grey shout-out pill - Sectors (both forms) and Age ranges (person) now put the names in a flex column beside the title so wrapped lines indent under the names rather than the title. - Shout-out is in the general person params (not admin-only), so make its Background summary pill grey like Bio instead of admin-blue. Co-Authored-By: Claude Opus 4.8 (1M context) --- app/views/organizations/_form.html.erb | 9 +++++---- app/views/people/_form.html.erb | 16 +++++++--------- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/app/views/organizations/_form.html.erb b/app/views/organizations/_form.html.erb index 46026aaa41..49d6552920 100644 --- a/app/views/organizations/_form.html.erb +++ b/app/views/organizations/_form.html.erb @@ -27,10 +27,11 @@ <% sectors_open = !f.object.persisted? || f.object.errors.any? %>
> - - Sectors: - <%= f.object.decorate.sectors_summary %> - + <%# Title is a fixed column; the names flow in the flex-1 column and wrap + indented under themselves (hanging indent), chevron trailing. %> + + Sectors: + <%= f.object.decorate.sectors_summary %>
diff --git a/app/views/people/_form.html.erb b/app/views/people/_form.html.erb index 8390b7cd7c..0d941a6e42 100644 --- a/app/views/people/_form.html.erb +++ b/app/views/people/_form.html.erb @@ -175,10 +175,9 @@ <% editable_sector_items = sectors_owner.sectorable_items_ordered.reject { |item| item.sector&.name == Sector::OTHER_SECTOR_NAME } %> <% sectors_open = !sectors_owner.persisted? || sectors_owner.errors.any? %>
> - - Sectors: - <%= sectors_owner.decorate.sectors_summary %> - + + Sectors: + <%= sectors_owner.decorate.sectors_summary %>
<% age_ranges_open = !age_owner.persisted? || age_owner.errors.any? %>
> - - Age ranges: - <%= age_owner.decorate.age_ranges_summary %> - + + Age ranges: + <%= age_owner.decorate.age_ranges_summary %>
Bio present <% end %> <% if f.object.shoutout_text.present? %> - Shout-out present + Shout-out present <% end %>
From e0b860d4b5aeb7d0a62b46730edf475b0d93f506 Mon Sep 17 00:00:00 2001 From: maebeale Date: Sat, 1 Aug 2026 21:14:36 -0400 Subject: [PATCH 12/14] Keep each sector/age-range chip on one line in the summary Wrap each chip (icon + name + "(sector leader)") in a whitespace-nowrap span so it never splits across a wrap; line breaks fall between chips at the comma. Co-Authored-By: Claude Opus 4.8 (1M context) --- app/decorators/application_decorator.rb | 5 ++++- app/decorators/person_decorator.rb | 3 ++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/app/decorators/application_decorator.rb b/app/decorators/application_decorator.rb index 60f12d4b0d..69a87af4ea 100644 --- a/app/decorators/application_decorator.rb +++ b/app/decorators/application_decorator.rb @@ -36,6 +36,9 @@ def sectors_summary private + # A single sector's chip, kept on one line (whitespace-nowrap) so the icon, + # name, and "(sector leader)" never split across a wrap — breaks fall between + # sectors, at the joining comma. def sector_summary_chip(item) name = item.sector&.name.to_s pieces = [] @@ -43,6 +46,6 @@ def sector_summary_chip(item) pieces << h.content_tag(:i, "", class: "fa-solid fa-star text-amber-400") if item.is_primary? pieces << (item.is_primary? ? h.content_tag(:strong, name) : name) pieces << "(sector leader)" if item.is_leader? - h.safe_join(pieces, " ") + h.content_tag(:span, h.safe_join(pieces, " "), class: "whitespace-nowrap") end end diff --git a/app/decorators/person_decorator.rb b/app/decorators/person_decorator.rb index bc78616fd5..807f05e306 100644 --- a/app/decorators/person_decorator.rb +++ b/app/decorators/person_decorator.rb @@ -116,11 +116,12 @@ def age_ranges_summary h.safe_join(items.map { |item| name = item.category&.name.to_s - if item.is_primary? + inner = if item.is_primary? h.safe_join([ h.content_tag(:i, "", class: "fa-solid fa-star text-amber-400"), h.content_tag(:strong, name) ], " ") else name end + h.content_tag(:span, inner, class: "whitespace-nowrap") }, ", ") end From 5e188a07c2a3d188b59d081b46cc307a1f617c93 Mon Sep 17 00:00:00 2001 From: maebeale Date: Sat, 1 Aug 2026 21:16:44 -0400 Subject: [PATCH 13/14] Collapse Social media links; shrink person avatar to logo size - Social media links is now a collapsible section summarizing which platforms have a URL on file (grey pills, or "None") via PersonDecorator#social_media_summary. - Shrink the person avatar box to w-24 h-24 to match the org logo. Co-Authored-By: Claude Opus 4.8 (1M context) --- app/decorators/person_decorator.rb | 21 +++++++++++++++++++++ app/views/people/_form.html.erb | 16 +++++++++------- spec/decorators/person_decorator_spec.rb | 13 +++++++++++++ 3 files changed, 43 insertions(+), 7 deletions(-) diff --git a/app/decorators/person_decorator.rb b/app/decorators/person_decorator.rb index 807f05e306..2ca2d68c30 100644 --- a/app/decorators/person_decorator.rb +++ b/app/decorators/person_decorator.rb @@ -108,6 +108,27 @@ def profile_display_summary hidden.any? ? "Hide #{hidden.to_sentence}" : "All shown" end + # Social-media URL fields mapped to the platform label shown as a pill in the + # collapsed section summary when the field is filled in. + SOCIAL_MEDIA_LABELS = { + linked_in_url: "LinkedIn", + facebook_url: "Facebook", + instagram_url: "Instagram", + youtube_url: "YouTube", + twitter_url: "Twitter" + }.freeze + + # One-line summary of the social-media links for the collapsed form section: a + # grey pill for each platform with a URL on file, or "None". HTML-safe. + def social_media_summary + present = SOCIAL_MEDIA_LABELS.select { |attr, _| object.public_send(attr).present? }.values + return "None" if present.empty? + + h.safe_join(present.map { |label| + h.content_tag(:span, label, class: "text-xs font-normal px-2 py-0.5 rounded-full bg-gray-100 text-gray-600") + }, " ") + end + # One-line summary of the tagged age ranges for a collapsed form section. The # primary age group is bold with a ⭐ (age ranges have no leader flag). HTML-safe. def age_ranges_summary diff --git a/app/views/people/_form.html.erb b/app/views/people/_form.html.erb index 0d941a6e42..890c9b608e 100644 --- a/app/views/people/_form.html.erb +++ b/app/views/people/_form.html.erb @@ -112,7 +112,7 @@
-
<%= render "shared/form_image_field", f: f, field_name: :avatar %>
+
<%= render "shared/form_image_field", f: f, field_name: :avatar, size: "w-24 h-24" %>
@@ -533,12 +533,14 @@
-
-
- Social media links -
+
+ + Social media links + <%= @person.decorate.social_media_summary %> + + -
+
<%= f.input :linked_in_url, as: :text, input_html: { rows: 1 } %> <%= f.input :facebook_url, as: :text, input_html: { rows: 1 } %> @@ -547,7 +549,7 @@ <%= f.input :twitter_url, as: :text, input_html: { rows: 1 } %>
-
+
diff --git a/spec/decorators/person_decorator_spec.rb b/spec/decorators/person_decorator_spec.rb index ade4361210..6d8eeda9ce 100644 --- a/spec/decorators/person_decorator_spec.rb +++ b/spec/decorators/person_decorator_spec.rb @@ -69,6 +69,19 @@ end end + describe "#social_media_summary" do + it "is 'None' with no links" do + expect(create(:person).decorate.social_media_summary).to eq("None") + end + + it "shows a pill for each platform with a URL on file" do + person = create(:person, linked_in_url: "https://linkedin.com/in/x", youtube_url: "https://youtu.be/x") + summary = person.decorate.social_media_summary + expect(summary).to include("LinkedIn", "YouTube") + expect(summary).not_to include("Facebook", "Instagram", "Twitter") + end + end + describe "#sectors_summary" do it "is 'None selected' with no sectors" do expect(create(:person).decorate.sectors_summary).to eq("None selected") From 113fa2ac50393efc0531eed5622eb7b06189af12 Mon Sep 17 00:00:00 2001 From: maebeale Date: Sat, 1 Aug 2026 21:25:28 -0400 Subject: [PATCH 14/14] Even out spacing between collapsed sections MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit space-y-4 on the collapsible
put a 1rem margin-bottom on the summary (Tailwind space-y = margin on non-last child), which rendered even when collapsed — so sections with it were taller and their gaps larger than the profile-preferences-section ones. Drop space-y-4 from the section
(the open-state gap is handled by mt-2 on the content div, re-added where missing) so every collapsed row is the same height and the between-row spacing is uniform. Co-Authored-By: Claude Opus 4.8 (1M context) --- app/views/organizations/_form.html.erb | 16 ++++++++-------- app/views/people/_form.html.erb | 12 ++++++------ 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/app/views/organizations/_form.html.erb b/app/views/organizations/_form.html.erb index 49d6552920..045a6522a4 100644 --- a/app/views/organizations/_form.html.erb +++ b/app/views/organizations/_form.html.erb @@ -26,7 +26,7 @@ a new org or when there are errors. %> <% sectors_open = !f.object.persisted? || f.object.errors.any? %>
-
> +
> <%# Title is a fixed column; the names flow in the flex-1 column and wrap indented under themselves (hanging indent), chevron trailing. %> @@ -55,7 +55,7 @@ <% windows_open = !f.object.persisted? || f.object.errors.any? %> -
> +
> Windows audience: <%= f.object.windows_type&.name.presence || "None selected" %> @@ -122,14 +122,14 @@ <%# Collapsible like Workshop settings above. Stays open on a new org (required fields) or when the form has errors so nothing hides behind the summary. %> <% background_open = !@organization.persisted? || @organization.errors.any? %> -
> +
> Background Info: <%= f.object.decorate.background_summary %> -
+
@@ -254,14 +254,14 @@ else pluralize(org_addresses.size, "address") end %> -
> +
> Addresses: <%= address_summary %> -
+
<%= f.simple_fields_for :addresses do |sfi| %> <%= render "organizations/address_fields", f: sfi %> @@ -280,7 +280,7 @@
-
+
Affiliations <% affiliation_count = f.object.affiliations.reject(&:marked_for_destruction?).size %> @@ -438,7 +438,7 @@ <% if f.object.persisted? %> <% if allowed_to?(:manage?, Comment) %> -
+
<% comment_count = f.object.comments.reject(&:marked_for_destruction?).size %> Comments diff --git a/app/views/people/_form.html.erb b/app/views/people/_form.html.erb index 890c9b608e..2039296131 100644 --- a/app/views/people/_form.html.erb +++ b/app/views/people/_form.html.erb @@ -131,7 +131,7 @@ else pluralize(person_addresses.size, "address") end %> -
> +
> Addresses: <%= address_summary %> @@ -270,7 +270,7 @@ <% person = f.object.respond_to?(:object) ? f.object.object : f.object %> <% decorated = person.decorate %> -
+
Affiliations <% affiliation_count = person.affiliations.reject(&:marked_for_destruction?).size %> @@ -389,7 +389,7 @@ <%# Collapsible like the sections above. Stays open on a new person or when the form has errors so nothing hides behind the summary. %> <% background_open = !f.object.persisted? || f.object.errors.any? %> -
> +
> Background <% if f.object.bio.present? %> @@ -438,7 +438,7 @@ or when there are errors so an invalid license stays visible. %> <% licenses = f.object.professional_licenses.reject(&:marked_for_destruction?) %> <% licenses_open = !f.object.persisted? || f.object.errors.any? %> -
> +
> Professional licenses: <%= licenses.any? ? licenses.map(&:name).to_sentence : "None" %> @@ -461,7 +461,7 @@ and the admin-only demographics/FileMaker block. Open on a new person or when there are errors. %> <% extras_open = !f.object.persisted? || f.object.errors.any? %> -
> +
> Extras @@ -616,7 +616,7 @@ <% if allowed_to?(:manage?, Comment) %> -
+
<% comment_count = f.object.comments.reject(&:marked_for_destruction?).size %> Comments