From 080548abc9ca33e6e218ec5be3628b86e388cbf8 Mon Sep 17 00:00:00 2001 From: Mae Beale Date: Mon, 12 Oct 2026 21:59:06 -0400 Subject: [PATCH 1/5] Start: name FileMaker archive records Co-Authored-By: Claude Opus 5.5 (1M context) From 6dcf682e9cc69fa8423f0b54f6e7210c2e0f0ba5 Mon Sep 17 00:00:00 2001 From: Mae Beale Date: Mon, 12 Oct 2026 22:01:25 -0400 Subject: [PATCH 2/5] Name FileMaker archive records so admins can tell rows apart The archive listed and headed records by ID and class name only, so finding the right org or person meant opening rows one by one. Co-Authored-By: Claude Opus 5.5 (1M context) --- app/controllers/fm_archives_controller.rb | 2 ++ app/models/concerns/fm_archive.rb | 10 +++++++ app/models/fm_activity.rb | 1 + app/models/fm_address.rb | 1 + app/models/fm_ancillary_data.rb | 1 + app/models/fm_art_purchase.rb | 1 + app/models/fm_art_registry.rb | 1 + app/models/fm_campaign.rb | 1 + app/models/fm_committee.rb | 1 + app/models/fm_committee_meeting.rb | 1 + app/models/fm_committee_member.rb | 1 + app/models/fm_donor_developer.rb | 1 + app/models/fm_email_address.rb | 1 + app/models/fm_event.rb | 1 + app/models/fm_exhibited_item.rb | 1 + app/models/fm_expenditure.rb | 1 + app/models/fm_form_field.rb | 1 + app/models/fm_form_letter.rb | 1 + app/models/fm_form_submission.rb | 1 + app/models/fm_forms_list.rb | 1 + app/models/fm_funding.rb | 1 + app/models/fm_group.rb | 1 + app/models/fm_invoice.rb | 1 + app/models/fm_line_item.rb | 1 + app/models/fm_note.rb | 1 + app/models/fm_organization.rb | 1 + app/models/fm_participant.rb | 1 + app/models/fm_payment.rb | 1 + app/models/fm_people_to_organization.rb | 1 + app/models/fm_personnel.rb | 1 + app/models/fm_phone_number.rb | 1 + app/models/fm_postal_code.rb | 1 + app/models/fm_project.rb | 1 + app/models/fm_quotation.rb | 1 + app/models/fm_reporting.rb | 1 + app/models/fm_rolodex.rb | 1 + app/models/fm_service.rb | 1 + app/models/fm_solicitation.rb | 1 + app/models/fm_volunteer.rb | 1 + app/models/fm_workshop.rb | 1 + app/models/fm_workshop_log.rb | 1 + app/views/fm_archives/index.html.erb | 4 +-- app/views/fm_archives/show.html.erb | 6 ++-- config/features.yml | 12 ++++++++ spec/models/concerns/fm_archive_spec.rb | 36 +++++++++++++++++++++++ spec/requests/fm_archives_spec.rb | 16 ++++++++++ 46 files changed, 120 insertions(+), 5 deletions(-) create mode 100644 spec/models/concerns/fm_archive_spec.rb diff --git a/app/controllers/fm_archives_controller.rb b/app/controllers/fm_archives_controller.rb index 2ce09502b6..901e525cc5 100644 --- a/app/controllers/fm_archives_controller.rb +++ b/app/controllers/fm_archives_controller.rb @@ -93,6 +93,8 @@ def show return end + @table_key = @record.class.table_name.delete_prefix("fm_") + @table_label = TABLES.dig(@table_key, :label) load_backlinks load_portal_records end diff --git a/app/models/concerns/fm_archive.rb b/app/models/concerns/fm_archive.rb index 74176bd060..c93e0ce5bc 100644 --- a/app/models/concerns/fm_archive.rb +++ b/app/models/concerns/fm_archive.rb @@ -8,6 +8,8 @@ module FmArchive PORTAL_COUNTERPARTS = {}.freeze PORTAL_FIELDS = {}.freeze USER_LOOKUP_FIELDS = [].freeze + # Data fields that name a record, tried in order; a nested list joins its parts with a space. + FM_NAME_FIELDS = [].freeze included do before_validation :set_defaults @@ -62,6 +64,14 @@ def records_listed_under(_label) [] end + def name + self.class::FM_NAME_FIELDS.each do |fields| + name = Array(fields).filter_map { |field| data&.dig(field).to_s.strip.presence }.join(" ") + return name if name.present? + end + fm_id + end + private def set_defaults diff --git a/app/models/fm_activity.rb b/app/models/fm_activity.rb index a6a2ecdeba..2af69ac17c 100644 --- a/app/models/fm_activity.rb +++ b/app/models/fm_activity.rb @@ -34,6 +34,7 @@ class FmActivity < ApplicationRecord include FmArchive FM_KEY_COLUMN = "ActivityLogID" + FM_NAME_FIELDS = %w[Subject FullName].freeze FM_LINKS = { "ID" => "fm_rolodexes", diff --git a/app/models/fm_address.rb b/app/models/fm_address.rb index e02402ac47..0a5e7d5f17 100644 --- a/app/models/fm_address.rb +++ b/app/models/fm_address.rb @@ -25,6 +25,7 @@ class FmAddress < ApplicationRecord include FmArchive FM_KEY_COLUMN = "AddrsID" + FM_NAME_FIELDS = %w[AddrsLine1 AddrsCity].freeze FM_LINKS = { "RolodexID" => "fm_rolodexes", diff --git a/app/models/fm_ancillary_data.rb b/app/models/fm_ancillary_data.rb index 54116ae4c2..d81d858309 100644 --- a/app/models/fm_ancillary_data.rb +++ b/app/models/fm_ancillary_data.rb @@ -16,6 +16,7 @@ class FmAncillaryData < ApplicationRecord include FmArchive FM_KEY_COLUMN = "RecordID" + FM_NAME_FIELDS = %w[FlagText Flag].freeze FM_LINKS = { "RolodexID" => "fm_rolodexes" diff --git a/app/models/fm_art_purchase.rb b/app/models/fm_art_purchase.rb index 175a210f6b..12a159c9ae 100644 --- a/app/models/fm_art_purchase.rb +++ b/app/models/fm_art_purchase.rb @@ -17,6 +17,7 @@ class FmArtPurchase < ApplicationRecord include FmArchive FM_KEY_COLUMN = "PurchaseID" + FM_NAME_FIELDS = %w[ArtTitle].freeze FM_LINKS = { "ArtRegistryID" => "fm_art_registries", diff --git a/app/models/fm_art_registry.rb b/app/models/fm_art_registry.rb index f2a57beee6..b61ef02222 100644 --- a/app/models/fm_art_registry.rb +++ b/app/models/fm_art_registry.rb @@ -42,6 +42,7 @@ class FmArtRegistry < ApplicationRecord include FmArchive FM_KEY_COLUMN = "ItemID" + FM_NAME_FIELDS = %w[ArtTitle].freeze FM_LINKS = { "ArtistID" => "fm_rolodexes", diff --git a/app/models/fm_campaign.rb b/app/models/fm_campaign.rb index bbe0d5b0ee..8295ba8101 100644 --- a/app/models/fm_campaign.rb +++ b/app/models/fm_campaign.rb @@ -19,6 +19,7 @@ class FmCampaign < ApplicationRecord include FmArchive FM_KEY_COLUMN = "CampaignID" + FM_NAME_FIELDS = %w[Campaign].freeze FM_LINKS = {}.freeze diff --git a/app/models/fm_committee.rb b/app/models/fm_committee.rb index c9f4b3f7f6..d43fb7932f 100644 --- a/app/models/fm_committee.rb +++ b/app/models/fm_committee.rb @@ -17,6 +17,7 @@ class FmCommittee < ApplicationRecord include FmArchive FM_KEY_COLUMN = "CommitteeID" + FM_NAME_FIELDS = %w[CommitteeName].freeze FM_LINKS = {}.freeze diff --git a/app/models/fm_committee_meeting.rb b/app/models/fm_committee_meeting.rb index e15490d317..5cc527e775 100644 --- a/app/models/fm_committee_meeting.rb +++ b/app/models/fm_committee_meeting.rb @@ -14,6 +14,7 @@ class FmCommitteeMeeting < ApplicationRecord include FmArchive FM_KEY_COLUMN = "CtMtgID" + FM_NAME_FIELDS = %w[MeetingDate].freeze FM_LINKS = { "CommitteeID" => "fm_committees" diff --git a/app/models/fm_committee_member.rb b/app/models/fm_committee_member.rb index cdeb129425..1c237c0fbe 100644 --- a/app/models/fm_committee_member.rb +++ b/app/models/fm_committee_member.rb @@ -17,6 +17,7 @@ class FmCommitteeMember < ApplicationRecord include FmArchive FM_KEY_COLUMN = "CMRecID" + FM_NAME_FIELDS = %w[Role].freeze FM_LINKS = { "CommitteeID" => "fm_committees", diff --git a/app/models/fm_donor_developer.rb b/app/models/fm_donor_developer.rb index dfcaf92c40..a32b54bd32 100644 --- a/app/models/fm_donor_developer.rb +++ b/app/models/fm_donor_developer.rb @@ -17,6 +17,7 @@ class FmDonorDeveloper < ApplicationRecord include FmArchive FM_KEY_COLUMN = "DonorDevRecID" + FM_NAME_FIELDS = %w[NameListing].freeze FM_LINKS = { "CampaignID" => "fm_campaigns", diff --git a/app/models/fm_email_address.rb b/app/models/fm_email_address.rb index 9b8badf616..88b100ce08 100644 --- a/app/models/fm_email_address.rb +++ b/app/models/fm_email_address.rb @@ -14,6 +14,7 @@ class FmEmailAddress < ApplicationRecord include FmArchive FM_KEY_COLUMN = "EmailAddrsID" + FM_NAME_FIELDS = %w[EmailAddress].freeze FM_LINKS = { "OrgID" => "fm_organizations" diff --git a/app/models/fm_event.rb b/app/models/fm_event.rb index 149a04a8a2..cdea583ae1 100644 --- a/app/models/fm_event.rb +++ b/app/models/fm_event.rb @@ -37,6 +37,7 @@ class FmEvent < ApplicationRecord include FmArchive FM_KEY_COLUMN = "EventID" + FM_NAME_FIELDS = %w[Title].freeze FM_LINKS = {}.freeze diff --git a/app/models/fm_exhibited_item.rb b/app/models/fm_exhibited_item.rb index d8daa3b492..ecb49b4e29 100644 --- a/app/models/fm_exhibited_item.rb +++ b/app/models/fm_exhibited_item.rb @@ -18,6 +18,7 @@ class FmExhibitedItem < ApplicationRecord include FmArchive FM_KEY_COLUMN = "RecordID" + FM_NAME_FIELDS = %w[ItemName].freeze FM_LINKS = { "ArtRegistryItemID" => "fm_art_registries", diff --git a/app/models/fm_expenditure.rb b/app/models/fm_expenditure.rb index 752ac8357c..5027f36b2e 100644 --- a/app/models/fm_expenditure.rb +++ b/app/models/fm_expenditure.rb @@ -19,6 +19,7 @@ class FmExpenditure < ApplicationRecord include FmArchive FM_KEY_COLUMN = "ExpendRecID" + FM_NAME_FIELDS = %w[Name Description].freeze FM_LINKS = { "ProjectID" => "fm_projects" diff --git a/app/models/fm_form_field.rb b/app/models/fm_form_field.rb index 81518402b1..5ebcc17157 100644 --- a/app/models/fm_form_field.rb +++ b/app/models/fm_form_field.rb @@ -16,6 +16,7 @@ class FmFormField < ApplicationRecord include FmArchive FM_KEY_COLUMN = "FieldNo" + FM_NAME_FIELDS = %w[FieldNameFM FieldNameWP].freeze FM_LINKS = {}.freeze end diff --git a/app/models/fm_form_letter.rb b/app/models/fm_form_letter.rb index 5f3657e7b3..ce1b3b6d45 100644 --- a/app/models/fm_form_letter.rb +++ b/app/models/fm_form_letter.rb @@ -24,6 +24,7 @@ class FmFormLetter < ApplicationRecord include FmArchive FM_KEY_COLUMN = "DocID" + FM_NAME_FIELDS = %w[DocumentName].freeze FM_LINKS = { "SignatureID" => "fm_rolodexes" diff --git a/app/models/fm_form_submission.rb b/app/models/fm_form_submission.rb index 754bd17714..340819540a 100644 --- a/app/models/fm_form_submission.rb +++ b/app/models/fm_form_submission.rb @@ -44,6 +44,7 @@ class FmFormSubmission < ApplicationRecord include FmArchive FM_KEY_COLUMN = "RecordID" + FM_NAME_FIELDS = [ %w[FirstName LastName], "OrgName", "FormTitle" ].freeze FM_LINKS = { "FormID" => "fm_forms_lists", diff --git a/app/models/fm_forms_list.rb b/app/models/fm_forms_list.rb index 4c60716c94..c5b9ea78e2 100644 --- a/app/models/fm_forms_list.rb +++ b/app/models/fm_forms_list.rb @@ -15,6 +15,7 @@ class FmFormsList < ApplicationRecord include FmArchive FM_KEY_COLUMN = "FormID" + FM_NAME_FIELDS = %w[FormTitle].freeze FM_LINKS = {}.freeze diff --git a/app/models/fm_funding.rb b/app/models/fm_funding.rb index d384fc43a0..7c6ee364be 100644 --- a/app/models/fm_funding.rb +++ b/app/models/fm_funding.rb @@ -49,6 +49,7 @@ class FmFunding < ApplicationRecord include FmArchive FM_KEY_COLUMN = "RecordID" + FM_NAME_FIELDS = %w[NameTemp Purpose].freeze FM_LINKS = { "FunderID" => "fm_rolodexes" diff --git a/app/models/fm_group.rb b/app/models/fm_group.rb index c0e9cd3497..041c9afc37 100644 --- a/app/models/fm_group.rb +++ b/app/models/fm_group.rb @@ -24,6 +24,7 @@ class FmGroup < ApplicationRecord include FmArchive FM_KEY_COLUMN = "GroupID" + FM_NAME_FIELDS = %w[GroupName].freeze FM_LINKS = {}.freeze diff --git a/app/models/fm_invoice.rb b/app/models/fm_invoice.rb index 08c3d450af..7d9922b87c 100644 --- a/app/models/fm_invoice.rb +++ b/app/models/fm_invoice.rb @@ -35,6 +35,7 @@ class FmInvoice < ApplicationRecord include FmArchive FM_KEY_COLUMN = "RecordID" + FM_NAME_FIELDS = %w[Name Company InvoiceNo].freeze FM_LINKS = { "OrgID" => "fm_organizations", diff --git a/app/models/fm_line_item.rb b/app/models/fm_line_item.rb index 7332475822..fe7e0015f1 100644 --- a/app/models/fm_line_item.rb +++ b/app/models/fm_line_item.rb @@ -20,6 +20,7 @@ class FmLineItem < ApplicationRecord include FmArchive FM_KEY_COLUMN = "ItemID" + FM_NAME_FIELDS = %w[Description].freeze FM_LINKS = { "InvoiceNo" => "fm_invoices" diff --git a/app/models/fm_note.rb b/app/models/fm_note.rb index 59b2593b5b..3d0fb7bd2b 100644 --- a/app/models/fm_note.rb +++ b/app/models/fm_note.rb @@ -24,6 +24,7 @@ class FmNote < ApplicationRecord include FmArchive FM_KEY_COLUMN = "NoteID" + FM_NAME_FIELDS = %w[Subject].freeze FM_LINKS = { "ProjectID" => "fm_projects", diff --git a/app/models/fm_organization.rb b/app/models/fm_organization.rb index 3cb475be96..4cf2c198cd 100644 --- a/app/models/fm_organization.rb +++ b/app/models/fm_organization.rb @@ -32,6 +32,7 @@ class FmOrganization < ApplicationRecord include FmArchive FM_KEY_COLUMN = "OrgID" + FM_NAME_FIELDS = %w[OrgName].freeze FM_LINKS = { "ParentID" => "fm_organizations" diff --git a/app/models/fm_participant.rb b/app/models/fm_participant.rb index de059807e9..d777a0c3ad 100644 --- a/app/models/fm_participant.rb +++ b/app/models/fm_participant.rb @@ -45,6 +45,7 @@ class FmParticipant < ApplicationRecord include FmArchive FM_KEY_COLUMN = "PRecID" + FM_NAME_FIELDS = %w[Name].freeze FM_LINKS = { "ParticipantID" => "fm_rolodexes", diff --git a/app/models/fm_payment.rb b/app/models/fm_payment.rb index 8afb482abe..f9258b0794 100644 --- a/app/models/fm_payment.rb +++ b/app/models/fm_payment.rb @@ -39,6 +39,7 @@ class FmPayment < ApplicationRecord include FmArchive FM_KEY_COLUMN = "RecordID" + FM_NAME_FIELDS = %w[Name].freeze FM_LINKS = { "RolodexID" => "fm_rolodexes", diff --git a/app/models/fm_people_to_organization.rb b/app/models/fm_people_to_organization.rb index 5d67cd7ef8..43711bc287 100644 --- a/app/models/fm_people_to_organization.rb +++ b/app/models/fm_people_to_organization.rb @@ -16,6 +16,7 @@ class FmPeopleToOrganization < ApplicationRecord include FmArchive FM_KEY_COLUMN = "RecordID" + FM_NAME_FIELDS = %w[Title].freeze FM_LINKS = { "PersonID" => "fm_rolodexes", diff --git a/app/models/fm_personnel.rb b/app/models/fm_personnel.rb index 6d0d3b108c..07640f4e95 100644 --- a/app/models/fm_personnel.rb +++ b/app/models/fm_personnel.rb @@ -23,6 +23,7 @@ class FmPersonnel < ApplicationRecord include FmArchive FM_KEY_COLUMN = "PrsnlRecID" + FM_NAME_FIELDS = %w[Name].freeze FM_LINKS = { "PersonID" => "fm_rolodexes", diff --git a/app/models/fm_phone_number.rb b/app/models/fm_phone_number.rb index 9681a34a1f..8666623cd4 100644 --- a/app/models/fm_phone_number.rb +++ b/app/models/fm_phone_number.rb @@ -18,6 +18,7 @@ class FmPhoneNumber < ApplicationRecord include FmArchive FM_KEY_COLUMN = "PhoneID" + FM_NAME_FIELDS = %w[PhoneNumber].freeze FM_LINKS = { "RolodexID" => "fm_rolodexes", diff --git a/app/models/fm_postal_code.rb b/app/models/fm_postal_code.rb index a64d6b8f54..98cf5b7241 100644 --- a/app/models/fm_postal_code.rb +++ b/app/models/fm_postal_code.rb @@ -22,6 +22,7 @@ class FmPostalCode < ApplicationRecord include FmArchive FM_KEY_COLUMN = "Zipcode" + FM_NAME_FIELDS = [ %w[City State] ].freeze FM_LINKS = {}.freeze end diff --git a/app/models/fm_project.rb b/app/models/fm_project.rb index f649a7ad48..faa3b3efb8 100644 --- a/app/models/fm_project.rb +++ b/app/models/fm_project.rb @@ -47,6 +47,7 @@ class FmProject < ApplicationRecord include FmArchive FM_KEY_COLUMN = "ProjectID" + FM_NAME_FIELDS = %w[ProjectName].freeze FM_LINKS = { "OrgID" => "fm_organizations", diff --git a/app/models/fm_quotation.rb b/app/models/fm_quotation.rb index 33a27f6a66..f4850d3b59 100644 --- a/app/models/fm_quotation.rb +++ b/app/models/fm_quotation.rb @@ -34,6 +34,7 @@ class FmQuotation < ApplicationRecord include FmArchive FM_KEY_COLUMN = "RecordID" + FM_NAME_FIELDS = %w[PersonName].freeze FM_LINKS = { "RolodexID" => "fm_rolodexes", diff --git a/app/models/fm_reporting.rb b/app/models/fm_reporting.rb index 234f6248d0..5f5bba8ea3 100644 --- a/app/models/fm_reporting.rb +++ b/app/models/fm_reporting.rb @@ -14,6 +14,7 @@ class FmReporting < ApplicationRecord include FmArchive FM_KEY_COLUMN = "ReportID" + FM_NAME_FIELDS = %w[ReportDescription].freeze FM_LINKS = { "FundingRecordID" => "fm_fundings" diff --git a/app/models/fm_rolodex.rb b/app/models/fm_rolodex.rb index fb37537941..6a39d4a9d7 100644 --- a/app/models/fm_rolodex.rb +++ b/app/models/fm_rolodex.rb @@ -92,6 +92,7 @@ class FmRolodex < ApplicationRecord include FmArchive FM_KEY_COLUMN = "ID" + FM_NAME_FIELDS = [ %w[FirstName LastName], "Organization" ].freeze FM_LINKS = { "OrgID" => "fm_organizations", diff --git a/app/models/fm_service.rb b/app/models/fm_service.rb index 7456bf23e6..05f65d47c9 100644 --- a/app/models/fm_service.rb +++ b/app/models/fm_service.rb @@ -24,6 +24,7 @@ class FmService < ApplicationRecord include FmArchive FM_KEY_COLUMN = "RecordID" + FM_NAME_FIELDS = [ %w[Month Year] ].freeze FM_LINKS = { "ProjectID" => "fm_projects" diff --git a/app/models/fm_solicitation.rb b/app/models/fm_solicitation.rb index 7e1f26e733..d63ca8235b 100644 --- a/app/models/fm_solicitation.rb +++ b/app/models/fm_solicitation.rb @@ -30,6 +30,7 @@ class FmSolicitation < ApplicationRecord include FmArchive FM_KEY_COLUMN = "SolicitationID" + FM_NAME_FIELDS = %w[NameListing].freeze FM_LINKS = { "CampaignID" => "fm_campaigns", diff --git a/app/models/fm_volunteer.rb b/app/models/fm_volunteer.rb index 324d5406e6..d94fa65048 100644 --- a/app/models/fm_volunteer.rb +++ b/app/models/fm_volunteer.rb @@ -22,6 +22,7 @@ class FmVolunteer < ApplicationRecord include FmArchive FM_KEY_COLUMN = "VolID" + FM_NAME_FIELDS = [ %w[FirstName LastName] ].freeze FM_LINKS = { "VRecID" => "fm_rolodexes", diff --git a/app/models/fm_workshop.rb b/app/models/fm_workshop.rb index 69d0b91c7c..ed4cb099a9 100644 --- a/app/models/fm_workshop.rb +++ b/app/models/fm_workshop.rb @@ -44,6 +44,7 @@ class FmWorkshop < ApplicationRecord include FmArchive FM_KEY_COLUMN = "WorkshopID" + FM_NAME_FIELDS = %w[WorkshopTitle].freeze FM_LINKS = { "AuthorID" => "fm_rolodexes" diff --git a/app/models/fm_workshop_log.rb b/app/models/fm_workshop_log.rb index 0788dc8779..be0d1bd01c 100644 --- a/app/models/fm_workshop_log.rb +++ b/app/models/fm_workshop_log.rb @@ -39,6 +39,7 @@ class FmWorkshopLog < ApplicationRecord include FmArchive FM_KEY_COLUMN = "RecordID" + FM_NAME_FIELDS = %w[WorkshopTitle LeaderName].freeze FM_LINKS = { "ProjectID" => "fm_projects", diff --git a/app/views/fm_archives/index.html.erb b/app/views/fm_archives/index.html.erb index 9ba54ddf41..4e7b28c14b 100644 --- a/app/views/fm_archives/index.html.erb +++ b/app/views/fm_archives/index.html.erb @@ -29,7 +29,7 @@ FM ID - Key + Name @@ -38,7 +38,7 @@ <%= link_to record.fm_id, fm_archive_path(record.fm_id, table: @selected), class: "text-primary hover:underline" %> - <%= record.fm_key_name %> + <%= truncate(record.name, length: 100) %> <% end %> diff --git a/app/views/fm_archives/show.html.erb b/app/views/fm_archives/show.html.erb index 9cb4bd018a..b36f00af01 100644 --- a/app/views/fm_archives/show.html.erb +++ b/app/views/fm_archives/show.html.erb @@ -3,10 +3,10 @@
-

<%= @record.class.name %>

-

FM ID: <%= @record.fm_id %> · Key: <%= @record.fm_key_name %>

+

<%= @record.name %>

+

<%= @table_label %> · FM ID: <%= @record.fm_id %> · Key: <%= @record.fm_key_name %>

- <%= link_to "Back to list", fm_archives_path(table: @record.class.table_name.sub("fm_", "")), class: button_classes(:primary_outline) %> + <%= link_to "Back to list", fm_archives_path(table: @table_key), class: button_classes(:primary_outline) %>
<% if @portal_records.any? %> diff --git a/config/features.yml b/config/features.yml index 7b479ba5b3..2705a89bd0 100644 --- a/config/features.yml +++ b/config/features.yml @@ -4304,3 +4304,15 @@ - "Wide images still fill their space edge to edge, as before." - "This covers the carousels, the story cards on the home and browse pages, the related stories on each story page, and the image carousel on each story." - "Cards in the home page topic sections now all stay the same size, whatever the shape of the image." + +- name: "FileMaker archive records show their names" + area: reporting + display_status: admin_facing + released_on: 2026-10-12 + pr_number: 2664 + action_path: "/fm_archives" + summary: >- + FileMaker archive lists and record pages now show each record's name — a + person, organization, event title, or similar — instead of only its ID. + pro_tips: + - "Records with nothing to name them by, like join rows, show their FileMaker ID instead." diff --git a/spec/models/concerns/fm_archive_spec.rb b/spec/models/concerns/fm_archive_spec.rb new file mode 100644 index 0000000000..b04bed1fab --- /dev/null +++ b/spec/models/concerns/fm_archive_spec.rb @@ -0,0 +1,36 @@ +require "rails_helper" + +RSpec.describe FmArchive do + describe "#name" do + it "joins a record's name parts" do + rolodex = FmRolodex.new(fm_id: "00006", data: { "FirstName" => "Archie", "LastName" => " Coded " }) + + expect(rolodex.name).to eq("Archie Coded") + end + + it "falls back to the next name field when the first is blank" do + agency = FmRolodex.new(fm_id: "00006", data: { "FirstName" => "", "Organization" => "Sunrise Shelter" }) + + expect(agency.name).to eq("Sunrise Shelter") + end + + it "falls back to the FM ID when no name field is filled" do + expect(FmRolodex.new(fm_id: "00006", data: {}).name).to eq("00006") + end + + it "falls back to the FM ID for tables without name fields" do + expect(FmContact.new(fm_id: "C1", data: { "Notes" => "Main contact" }).name).to eq("C1") + end + + it "names every archive table's records from fields it documents" do + FmArchivesController::TABLES.each_value do |config| + model = config[:model] + source = File.read(Rails.root.join("app/models/#{model.name.underscore}.rb")) + + model::FM_NAME_FIELDS.flatten.each do |field| + expect(source).to match(/^#\s+#{Regexp.escape(field)}\b/), "#{model} names records by undocumented field #{field}" + end + end + end + end +end diff --git a/spec/requests/fm_archives_spec.rb b/spec/requests/fm_archives_spec.rb index 5a7f5ba4af..353d1c9259 100644 --- a/spec/requests/fm_archives_spec.rb +++ b/spec/requests/fm_archives_spec.rb @@ -160,6 +160,14 @@ get fm_archive_path("P2775", table: "projects") expect(response.body).not_to include("Available fields") + + it "heads the page with the record's name and table" do + FmOrganization.create!(fm_id: "ORG1", data: { "OrgName" => "Sunrise Shelter" }) + + get fm_archive_path("ORG1", table: "organizations") + + expect(response.body).to match(%r{]*>Sunrise Shelter}) + expect(response.body).to include("Organizations · FM ID: ORG1") end it "links back to the portal people carrying the archive code" do @@ -291,6 +299,14 @@ expect(response.body).to include("People to organizations") end + it "lists each record by name" do + FmRolodex.create!(fm_id: "ROL1", data: { "FirstName" => "Archie", "LastName" => "Coded" }) + + get fm_archives_path(table: "rolodexes") + + expect(response.body).to include("Archie Coded") + end + it "marks the selected table as the current page" do get fm_archives_path(table: "rolodexes") From 0e7b01cdc42bd32667f4ff94ab8182aff82f34a6 Mon Sep 17 00:00:00 2001 From: Mae Beale Date: Tue, 13 Oct 2026 09:56:31 -0400 Subject: [PATCH 3/5] Word each FileMaker archive table's names its own way MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A purchase, its artwork, and the exhibit that showed it all carried the same title, and one person's payment, registration, and volunteer shift all read as just their name — so names alone couldn't tell records apart. Co-Authored-By: Claude Opus 5.5 (1M context) --- app/models/concerns/fm_archive.rb | 34 +++++++++++++++++++----- app/models/fm_activity.rb | 5 +++- app/models/fm_address.rb | 5 +++- app/models/fm_allocation.rb | 4 +++ app/models/fm_ancillary_data.rb | 5 +++- app/models/fm_art_purchase.rb | 5 +++- app/models/fm_art_registry.rb | 5 +++- app/models/fm_campaign.rb | 5 +++- app/models/fm_committee.rb | 5 +++- app/models/fm_committee_meeting.rb | 5 +++- app/models/fm_committee_member.rb | 5 +++- app/models/fm_contact.rb | 4 +++ app/models/fm_donor_developer.rb | 5 +++- app/models/fm_email_address.rb | 5 +++- app/models/fm_event.rb | 5 +++- app/models/fm_exhibited_item.rb | 5 +++- app/models/fm_expenditure.rb | 5 +++- app/models/fm_form_field.rb | 5 +++- app/models/fm_form_letter.rb | 5 +++- app/models/fm_form_submission.rb | 5 +++- app/models/fm_forms_list.rb | 5 +++- app/models/fm_funding.rb | 7 ++++- app/models/fm_group.rb | 5 +++- app/models/fm_group_member.rb | 4 +++ app/models/fm_invoice.rb | 5 +++- app/models/fm_line_item.rb | 5 +++- app/models/fm_match_donation.rb | 4 +++ app/models/fm_note.rb | 5 +++- app/models/fm_organization.rb | 5 +++- app/models/fm_participant.rb | 5 +++- app/models/fm_payment.rb | 5 +++- app/models/fm_people_to_organization.rb | 5 +++- app/models/fm_personnel.rb | 5 +++- app/models/fm_phone_number.rb | 5 +++- app/models/fm_postal_code.rb | 5 +++- app/models/fm_program_sponsorship.rb | 4 +++ app/models/fm_project.rb | 5 +++- app/models/fm_quotation.rb | 6 ++++- app/models/fm_reporting.rb | 5 +++- app/models/fm_rolodex.rb | 6 ++++- app/models/fm_service.rb | 5 +++- app/models/fm_solicitation.rb | 5 +++- app/models/fm_volunteer.rb | 5 +++- app/models/fm_workshop.rb | 5 +++- app/models/fm_workshop_log.rb | 5 +++- config/features.yml | 3 ++- spec/models/concerns/fm_archive_spec.rb | 35 ++++++++++++++----------- spec/requests/fm_archives_spec.rb | 1 + 48 files changed, 231 insertions(+), 61 deletions(-) diff --git a/app/models/concerns/fm_archive.rb b/app/models/concerns/fm_archive.rb index c93e0ce5bc..570684d9b0 100644 --- a/app/models/concerns/fm_archive.rb +++ b/app/models/concerns/fm_archive.rb @@ -8,8 +8,6 @@ module FmArchive PORTAL_COUNTERPARTS = {}.freeze PORTAL_FIELDS = {}.freeze USER_LOOKUP_FIELDS = [].freeze - # Data fields that name a record, tried in order; a nested list joins its parts with a space. - FM_NAME_FIELDS = [].freeze included do before_validation :set_defaults @@ -64,16 +62,40 @@ def records_listed_under(_label) [] end + # Each table words its names its own way, so records from different tables never share one. def name - self.class::FM_NAME_FIELDS.each do |fields| - name = Array(fields).filter_map { |field| data&.dig(field).to_s.strip.presence }.join(" ") - return name if name.present? - end fm_id end private + def fm_value(*fields) + fields.lazy.filter_map { |field| data&.dig(field).to_s.squish.presence }.first + end + + def fm_join(*fields) + fm_words(*fields.map { |field| fm_value(field) }) + end + + def fm_words(*parts) + parts.compact_blank.join(" ").presence + end + + def fm_with(preposition, value) + "#{preposition} #{value}" if value.present? + end + + def fm_dollars(*fields) + amounts = fields.filter_map { |field| Float(fm_value(field)&.delete("$,"), exception: false) } + MoneyFormatter.dollars_from_cents((amounts.sum * 100).round) if amounts.any? + end + + # "Subject — detail", where the detail carries the table's own wording. + def fm_phrase(subject, detail) + return detail&.upcase_first || fm_id if subject.blank? + [ subject, detail ].compact_blank.join(" — ") + end + def set_defaults self.data ||= {} end diff --git a/app/models/fm_activity.rb b/app/models/fm_activity.rb index 2af69ac17c..b0bd930096 100644 --- a/app/models/fm_activity.rb +++ b/app/models/fm_activity.rb @@ -34,10 +34,13 @@ class FmActivity < ApplicationRecord include FmArchive FM_KEY_COLUMN = "ActivityLogID" - FM_NAME_FIELDS = %w[Subject FullName].freeze FM_LINKS = { "ID" => "fm_rolodexes", "LinkID" => "fm_payments" }.freeze + + def name + fm_phrase(fm_value("Subject"), fm_words(fm_value("Type") || "Activity", fm_with("with", fm_value("FullName") || fm_join("FirstName", "LastName")))) + end end diff --git a/app/models/fm_address.rb b/app/models/fm_address.rb index 0a5e7d5f17..5fe6eab75c 100644 --- a/app/models/fm_address.rb +++ b/app/models/fm_address.rb @@ -25,10 +25,13 @@ class FmAddress < ApplicationRecord include FmArchive FM_KEY_COLUMN = "AddrsID" - FM_NAME_FIELDS = %w[AddrsLine1 AddrsCity].freeze FM_LINKS = { "RolodexID" => "fm_rolodexes", "OrgID" => "fm_organizations" }.freeze + + def name + [ fm_value("AddrsLine1"), fm_value("AddrsCity") ].compact.join(", ").presence || fm_id + end end diff --git a/app/models/fm_allocation.rb b/app/models/fm_allocation.rb index bd3b5ea93e..f5c7a53677 100644 --- a/app/models/fm_allocation.rb +++ b/app/models/fm_allocation.rb @@ -31,4 +31,8 @@ class FmAllocation < ApplicationRecord HAS_MANY = { "fm_participants" => { via: "AllocRecID", label: "Participants" } }.freeze + + def name + fm_words(fm_value("Year"), "allocation", fm_with("of", fm_dollars("AmountGeneral", "AmountSupplies", "AmountExhibit")), fm_with("to", fm_value("ProjectID"))).upcase_first + end end diff --git a/app/models/fm_ancillary_data.rb b/app/models/fm_ancillary_data.rb index d81d858309..dc2d3f2361 100644 --- a/app/models/fm_ancillary_data.rb +++ b/app/models/fm_ancillary_data.rb @@ -16,9 +16,12 @@ class FmAncillaryData < ApplicationRecord include FmArchive FM_KEY_COLUMN = "RecordID" - FM_NAME_FIELDS = %w[FlagText Flag].freeze FM_LINKS = { "RolodexID" => "fm_rolodexes" }.freeze + + def name + fm_phrase(fm_value("FlagText"), fm_words(fm_value("Flag"), "flag")) + end end diff --git a/app/models/fm_art_purchase.rb b/app/models/fm_art_purchase.rb index 12a159c9ae..eb7069fc99 100644 --- a/app/models/fm_art_purchase.rb +++ b/app/models/fm_art_purchase.rb @@ -17,10 +17,13 @@ class FmArtPurchase < ApplicationRecord include FmArchive FM_KEY_COLUMN = "PurchaseID" - FM_NAME_FIELDS = %w[ArtTitle].freeze FM_LINKS = { "ArtRegistryID" => "fm_art_registries", "BuyerID" => "fm_rolodexes" }.freeze + + def name + fm_phrase(fm_value("ArtTitle"), fm_words("purchased", fm_with("for", fm_dollars("AmountPaid")))) + end end diff --git a/app/models/fm_art_registry.rb b/app/models/fm_art_registry.rb index b61ef02222..89c7fe13c9 100644 --- a/app/models/fm_art_registry.rb +++ b/app/models/fm_art_registry.rb @@ -42,7 +42,6 @@ class FmArtRegistry < ApplicationRecord include FmArchive FM_KEY_COLUMN = "ItemID" - FM_NAME_FIELDS = %w[ArtTitle].freeze FM_LINKS = { "ArtistID" => "fm_rolodexes", @@ -54,4 +53,8 @@ class FmArtRegistry < ApplicationRecord "fm_art_purchases" => { via: "ArtRegistryID", label: "Art Purchases" }, "fm_exhibited_items" => { via: "ArtRegistryItemID", label: "Exhibited Items" } }.freeze + + def name + fm_phrase(fm_value("ArtTitle"), fm_with("by", fm_join("ArtistFirstName", "ArtistLastName"))) + end end diff --git a/app/models/fm_campaign.rb b/app/models/fm_campaign.rb index 8295ba8101..ee141f8581 100644 --- a/app/models/fm_campaign.rb +++ b/app/models/fm_campaign.rb @@ -19,7 +19,6 @@ class FmCampaign < ApplicationRecord include FmArchive FM_KEY_COLUMN = "CampaignID" - FM_NAME_FIELDS = %w[Campaign].freeze FM_LINKS = {}.freeze @@ -27,4 +26,8 @@ class FmCampaign < ApplicationRecord "fm_donor_developers" => { via: "CampaignID", label: "Donor Developers" }, "fm_solicitations" => { via: "CampaignID", label: "Solicitations" } }.freeze + + def name + fm_value("Campaign") || fm_id + end end diff --git a/app/models/fm_committee.rb b/app/models/fm_committee.rb index d43fb7932f..933bea1067 100644 --- a/app/models/fm_committee.rb +++ b/app/models/fm_committee.rb @@ -17,7 +17,6 @@ class FmCommittee < ApplicationRecord include FmArchive FM_KEY_COLUMN = "CommitteeID" - FM_NAME_FIELDS = %w[CommitteeName].freeze FM_LINKS = {}.freeze @@ -25,4 +24,8 @@ class FmCommittee < ApplicationRecord "fm_committee_meetings" => { via: "CommitteeID", label: "Meetings" }, "fm_committee_members" => { via: "CommitteeID", label: "Members" } }.freeze + + def name + fm_value("CommitteeName") || fm_id + end end diff --git a/app/models/fm_committee_meeting.rb b/app/models/fm_committee_meeting.rb index 5cc527e775..159a86febe 100644 --- a/app/models/fm_committee_meeting.rb +++ b/app/models/fm_committee_meeting.rb @@ -14,9 +14,12 @@ class FmCommitteeMeeting < ApplicationRecord include FmArchive FM_KEY_COLUMN = "CtMtgID" - FM_NAME_FIELDS = %w[MeetingDate].freeze FM_LINKS = { "CommitteeID" => "fm_committees" }.freeze + + def name + fm_words("Committee", fm_value("CommitteeID"), "meeting", fm_with("on", fm_value("MeetingDate"))) + end end diff --git a/app/models/fm_committee_member.rb b/app/models/fm_committee_member.rb index 1c237c0fbe..d967b914d7 100644 --- a/app/models/fm_committee_member.rb +++ b/app/models/fm_committee_member.rb @@ -17,10 +17,13 @@ class FmCommitteeMember < ApplicationRecord include FmArchive FM_KEY_COLUMN = "CMRecID" - FM_NAME_FIELDS = %w[Role].freeze FM_LINKS = { "CommitteeID" => "fm_committees", "MemberID" => "fm_rolodexes" }.freeze + + def name + fm_phrase(fm_value("MemberID"), fm_words(fm_value("Role") || "Member", "of committee", fm_value("CommitteeID"))) + end end diff --git a/app/models/fm_contact.rb b/app/models/fm_contact.rb index 866cdc7525..4d359dd1b9 100644 --- a/app/models/fm_contact.rb +++ b/app/models/fm_contact.rb @@ -21,4 +21,8 @@ class FmContact < ApplicationRecord FM_LINKS = { "ContacteeID" => "fm_rolodexes" }.freeze + + def name + fm_phrase(fm_value("RecordID"), fm_words("contact", fm_with("for", fm_value("ContacteeID")))) + end end diff --git a/app/models/fm_donor_developer.rb b/app/models/fm_donor_developer.rb index a32b54bd32..20a83a9578 100644 --- a/app/models/fm_donor_developer.rb +++ b/app/models/fm_donor_developer.rb @@ -17,7 +17,6 @@ class FmDonorDeveloper < ApplicationRecord include FmArchive FM_KEY_COLUMN = "DonorDevRecID" - FM_NAME_FIELDS = %w[NameListing].freeze FM_LINKS = { "CampaignID" => "fm_campaigns", @@ -27,4 +26,8 @@ class FmDonorDeveloper < ApplicationRecord HAS_MANY = { "fm_solicitations" => { via: "DonorDevRecID", label: "Solicitations" } }.freeze + + def name + fm_phrase(fm_value("NameListing"), fm_words("donor developer", fm_with("for", fm_value("CampaignID")))) + end end diff --git a/app/models/fm_email_address.rb b/app/models/fm_email_address.rb index 88b100ce08..913d4447e7 100644 --- a/app/models/fm_email_address.rb +++ b/app/models/fm_email_address.rb @@ -14,9 +14,12 @@ class FmEmailAddress < ApplicationRecord include FmArchive FM_KEY_COLUMN = "EmailAddrsID" - FM_NAME_FIELDS = %w[EmailAddress].freeze FM_LINKS = { "OrgID" => "fm_organizations" }.freeze + + def name + fm_value("EmailAddress") || fm_id + end end diff --git a/app/models/fm_event.rb b/app/models/fm_event.rb index cdea583ae1..d6241c60d5 100644 --- a/app/models/fm_event.rb +++ b/app/models/fm_event.rb @@ -37,7 +37,6 @@ class FmEvent < ApplicationRecord include FmArchive FM_KEY_COLUMN = "EventID" - FM_NAME_FIELDS = %w[Title].freeze FM_LINKS = {}.freeze @@ -48,4 +47,8 @@ class FmEvent < ApplicationRecord "fm_volunteers" => { via: "EventID", label: "Volunteers" }, "fm_quotations" => { via: "EventID", label: "Quotations" } }.freeze + + def name + fm_value("Title") || fm_id + end end diff --git a/app/models/fm_exhibited_item.rb b/app/models/fm_exhibited_item.rb index ecb49b4e29..18c5ff5657 100644 --- a/app/models/fm_exhibited_item.rb +++ b/app/models/fm_exhibited_item.rb @@ -18,10 +18,13 @@ class FmExhibitedItem < ApplicationRecord include FmArchive FM_KEY_COLUMN = "RecordID" - FM_NAME_FIELDS = %w[ItemName].freeze FM_LINKS = { "ArtRegistryItemID" => "fm_art_registries", "EventID" => "fm_events" }.freeze + + def name + fm_phrase(fm_value("ItemName"), fm_words("exhibited", fm_with("at", fm_value("EventID")))) + end end diff --git a/app/models/fm_expenditure.rb b/app/models/fm_expenditure.rb index 5027f36b2e..07d45510d1 100644 --- a/app/models/fm_expenditure.rb +++ b/app/models/fm_expenditure.rb @@ -19,9 +19,12 @@ class FmExpenditure < ApplicationRecord include FmArchive FM_KEY_COLUMN = "ExpendRecID" - FM_NAME_FIELDS = %w[Name Description].freeze FM_LINKS = { "ProjectID" => "fm_projects" }.freeze + + def name + fm_phrase(fm_value("Name", "Description"), fm_words(fm_dollars("Amount"), "expense")) + end end diff --git a/app/models/fm_form_field.rb b/app/models/fm_form_field.rb index 5ebcc17157..e49221b840 100644 --- a/app/models/fm_form_field.rb +++ b/app/models/fm_form_field.rb @@ -16,7 +16,10 @@ class FmFormField < ApplicationRecord include FmArchive FM_KEY_COLUMN = "FieldNo" - FM_NAME_FIELDS = %w[FieldNameFM FieldNameWP].freeze FM_LINKS = {}.freeze + + def name + fm_words(fm_value("FieldNameFM", "FieldNameWP"), "form field").upcase_first + end end diff --git a/app/models/fm_form_letter.rb b/app/models/fm_form_letter.rb index ce1b3b6d45..5866a37048 100644 --- a/app/models/fm_form_letter.rb +++ b/app/models/fm_form_letter.rb @@ -24,9 +24,12 @@ class FmFormLetter < ApplicationRecord include FmArchive FM_KEY_COLUMN = "DocID" - FM_NAME_FIELDS = %w[DocumentName].freeze FM_LINKS = { "SignatureID" => "fm_rolodexes" }.freeze + + def name + fm_value("DocumentName") || fm_id + end end diff --git a/app/models/fm_form_submission.rb b/app/models/fm_form_submission.rb index 340819540a..cbfd9a2490 100644 --- a/app/models/fm_form_submission.rb +++ b/app/models/fm_form_submission.rb @@ -44,7 +44,6 @@ class FmFormSubmission < ApplicationRecord include FmArchive FM_KEY_COLUMN = "RecordID" - FM_NAME_FIELDS = [ %w[FirstName LastName], "OrgName", "FormTitle" ].freeze FM_LINKS = { "FormID" => "fm_forms_lists", @@ -55,4 +54,8 @@ class FmFormSubmission < ApplicationRecord "PaymentID" => "fm_payments", "ParticRecID" => "fm_participants" }.freeze + + def name + fm_phrase(fm_join("FirstName", "LastName") || fm_value("OrgName"), fm_value("FormTitle") || "form submission") + end end diff --git a/app/models/fm_forms_list.rb b/app/models/fm_forms_list.rb index c5b9ea78e2..ad4f5230a4 100644 --- a/app/models/fm_forms_list.rb +++ b/app/models/fm_forms_list.rb @@ -15,11 +15,14 @@ class FmFormsList < ApplicationRecord include FmArchive FM_KEY_COLUMN = "FormID" - FM_NAME_FIELDS = %w[FormTitle].freeze FM_LINKS = {}.freeze HAS_MANY = { "fm_form_submissions" => { via: "FormID", label: "Form Submissions" } }.freeze + + def name + fm_value("FormTitle") || fm_id + end end diff --git a/app/models/fm_funding.rb b/app/models/fm_funding.rb index 7c6ee364be..b066d42937 100644 --- a/app/models/fm_funding.rb +++ b/app/models/fm_funding.rb @@ -49,7 +49,6 @@ class FmFunding < ApplicationRecord include FmArchive FM_KEY_COLUMN = "RecordID" - FM_NAME_FIELDS = %w[NameTemp Purpose].freeze FM_LINKS = { "FunderID" => "fm_rolodexes" @@ -61,4 +60,10 @@ class FmFunding < ApplicationRecord "fm_match_donations" => { via: "FundingID", label: "Match Donations" }, "fm_reportings" => { via: "FundingRecordID", label: "Reporting Obligations" } }.freeze + + def name + granted = fm_dollars("AmountGranted") + detail = granted ? "#{granted} grant" : fm_words(fm_dollars("RequestAmount"), "funding request") + fm_phrase(fm_value("NameTemp", "Purpose"), detail) + end end diff --git a/app/models/fm_group.rb b/app/models/fm_group.rb index 041c9afc37..e652dd9c29 100644 --- a/app/models/fm_group.rb +++ b/app/models/fm_group.rb @@ -24,11 +24,14 @@ class FmGroup < ApplicationRecord include FmArchive FM_KEY_COLUMN = "GroupID" - FM_NAME_FIELDS = %w[GroupName].freeze FM_LINKS = {}.freeze HAS_MANY = { "fm_group_members" => { via: "GroupID", label: "Members" } }.freeze + + def name + fm_value("GroupName") || fm_id + end end diff --git a/app/models/fm_group_member.rb b/app/models/fm_group_member.rb index 1f5e36a1dd..d96ae4e91f 100644 --- a/app/models/fm_group_member.rb +++ b/app/models/fm_group_member.rb @@ -23,4 +23,8 @@ class FmGroupMember < ApplicationRecord "GroupID" => "fm_groups", "ContactID" => "fm_rolodexes" }.freeze + + def name + fm_phrase(fm_value("ContactID"), fm_words("member of group", fm_value("GroupID"))) + end end diff --git a/app/models/fm_invoice.rb b/app/models/fm_invoice.rb index 7d9922b87c..e94aa4124f 100644 --- a/app/models/fm_invoice.rb +++ b/app/models/fm_invoice.rb @@ -35,7 +35,6 @@ class FmInvoice < ApplicationRecord include FmArchive FM_KEY_COLUMN = "RecordID" - FM_NAME_FIELDS = %w[Name Company InvoiceNo].freeze FM_LINKS = { "OrgID" => "fm_organizations", @@ -45,4 +44,8 @@ class FmInvoice < ApplicationRecord HAS_MANY = { "fm_line_items" => { via: "InvoiceNo", label: "Line Items" } }.freeze + + def name + fm_phrase(fm_words("Invoice", fm_value("InvoiceNo")), fm_value("Name", "Company")) + end end diff --git a/app/models/fm_line_item.rb b/app/models/fm_line_item.rb index fe7e0015f1..0a1f85ab4c 100644 --- a/app/models/fm_line_item.rb +++ b/app/models/fm_line_item.rb @@ -20,9 +20,12 @@ class FmLineItem < ApplicationRecord include FmArchive FM_KEY_COLUMN = "ItemID" - FM_NAME_FIELDS = %w[Description].freeze FM_LINKS = { "InvoiceNo" => "fm_invoices" }.freeze + + def name + fm_phrase(fm_value("Description"), fm_words("line item", fm_with("on invoice", fm_value("InvoiceNo")))) + end end diff --git a/app/models/fm_match_donation.rb b/app/models/fm_match_donation.rb index 9195121972..c5b4baf86b 100644 --- a/app/models/fm_match_donation.rb +++ b/app/models/fm_match_donation.rb @@ -19,4 +19,8 @@ class FmMatchDonation < ApplicationRecord "FundingID" => "fm_fundings", "RefDonationID" => "fm_payments" }.freeze + + def name + fm_words(fm_value("Match"), "match", fm_with("of", fm_dollars("Amount")), fm_with("for", fm_value("DonationID"))).upcase_first + end end diff --git a/app/models/fm_note.rb b/app/models/fm_note.rb index 3d0fb7bd2b..60b0c05843 100644 --- a/app/models/fm_note.rb +++ b/app/models/fm_note.rb @@ -24,7 +24,6 @@ class FmNote < ApplicationRecord include FmArchive FM_KEY_COLUMN = "NoteID" - FM_NAME_FIELDS = %w[Subject].freeze FM_LINKS = { "ProjectID" => "fm_projects", @@ -38,4 +37,8 @@ class FmNote < ApplicationRecord "Organizations" => "fm_organizations", "Rolodex" => "fm_rolodexes" }.freeze + + def name + fm_phrase(fm_value("Subject"), fm_words(fm_value("Context")&.singularize, "note")) + end end diff --git a/app/models/fm_organization.rb b/app/models/fm_organization.rb index 4cf2c198cd..bc76b9eb5a 100644 --- a/app/models/fm_organization.rb +++ b/app/models/fm_organization.rb @@ -32,7 +32,6 @@ class FmOrganization < ApplicationRecord include FmArchive FM_KEY_COLUMN = "OrgID" - FM_NAME_FIELDS = %w[OrgName].freeze FM_LINKS = { "ParentID" => "fm_organizations" @@ -52,4 +51,8 @@ class FmOrganization < ApplicationRecord "fm_invoices" => { via: "OrgID", label: "Invoices" }, "fm_people_to_organizations" => { via: "OrgID", label: "People Affiliations" } }.freeze + + def name + fm_value("OrgName") || fm_id + end end diff --git a/app/models/fm_participant.rb b/app/models/fm_participant.rb index d777a0c3ad..cbcb983b07 100644 --- a/app/models/fm_participant.rb +++ b/app/models/fm_participant.rb @@ -45,7 +45,6 @@ class FmParticipant < ApplicationRecord include FmArchive FM_KEY_COLUMN = "PRecID" - FM_NAME_FIELDS = %w[Name].freeze FM_LINKS = { "ParticipantID" => "fm_rolodexes", @@ -60,4 +59,8 @@ class FmParticipant < ApplicationRecord HAS_MANY = { "fm_payments" => { via: "ParticRecID", label: "Payments" } }.freeze + + def name + fm_phrase(fm_value("Name"), fm_words(fm_value("Role") || "Participant", fm_with("at", fm_value("EventID")))) + end end diff --git a/app/models/fm_payment.rb b/app/models/fm_payment.rb index f9258b0794..1f4096042a 100644 --- a/app/models/fm_payment.rb +++ b/app/models/fm_payment.rb @@ -39,7 +39,6 @@ class FmPayment < ApplicationRecord include FmArchive FM_KEY_COLUMN = "RecordID" - FM_NAME_FIELDS = %w[Name].freeze FM_LINKS = { "RolodexID" => "fm_rolodexes", @@ -61,4 +60,8 @@ class FmPayment < ApplicationRecord "fm_match_donations" => { via: "RefDonationID", label: "Match Donations" }, "fm_form_submissions" => { via: "PaymentID", label: "Form Submissions" } }.freeze + + def name + fm_phrase(fm_value("Name"), fm_words(fm_value("Type") || "Payment", fm_with("of", fm_dollars("Amount")))) + end end diff --git a/app/models/fm_people_to_organization.rb b/app/models/fm_people_to_organization.rb index 43711bc287..e228bb82a9 100644 --- a/app/models/fm_people_to_organization.rb +++ b/app/models/fm_people_to_organization.rb @@ -16,10 +16,13 @@ class FmPeopleToOrganization < ApplicationRecord include FmArchive FM_KEY_COLUMN = "RecordID" - FM_NAME_FIELDS = %w[Title].freeze FM_LINKS = { "PersonID" => "fm_rolodexes", "OrgID" => "fm_organizations" }.freeze + + def name + fm_phrase(fm_value("PersonID"), fm_words(fm_value("Title") || "Affiliated", fm_with("at", fm_value("OrgID")))) + end end diff --git a/app/models/fm_personnel.rb b/app/models/fm_personnel.rb index 07640f4e95..a2feccdc90 100644 --- a/app/models/fm_personnel.rb +++ b/app/models/fm_personnel.rb @@ -23,10 +23,13 @@ class FmPersonnel < ApplicationRecord include FmArchive FM_KEY_COLUMN = "PrsnlRecID" - FM_NAME_FIELDS = %w[Name].freeze FM_LINKS = { "PersonID" => "fm_rolodexes", "ProjectID" => "fm_projects" }.freeze + + def name + fm_phrase(fm_value("Name"), fm_words(fm_value("Role") || "Personnel", fm_with("on", fm_value("ProjectID")))) + end end diff --git a/app/models/fm_phone_number.rb b/app/models/fm_phone_number.rb index 8666623cd4..85c495833e 100644 --- a/app/models/fm_phone_number.rb +++ b/app/models/fm_phone_number.rb @@ -18,10 +18,13 @@ class FmPhoneNumber < ApplicationRecord include FmArchive FM_KEY_COLUMN = "PhoneID" - FM_NAME_FIELDS = %w[PhoneNumber].freeze FM_LINKS = { "RolodexID" => "fm_rolodexes", "OrgID" => "fm_organizations" }.freeze + + def name + fm_value("PhoneNumber") || fm_id + end end diff --git a/app/models/fm_postal_code.rb b/app/models/fm_postal_code.rb index 98cf5b7241..e062381093 100644 --- a/app/models/fm_postal_code.rb +++ b/app/models/fm_postal_code.rb @@ -22,7 +22,10 @@ class FmPostalCode < ApplicationRecord include FmArchive FM_KEY_COLUMN = "Zipcode" - FM_NAME_FIELDS = [ %w[City State] ].freeze FM_LINKS = {}.freeze + + def name + fm_words([ fm_value("City"), fm_value("State") ].compact.join(", "), fm_id) + end end diff --git a/app/models/fm_program_sponsorship.rb b/app/models/fm_program_sponsorship.rb index 7368850a6b..0a33840f25 100644 --- a/app/models/fm_program_sponsorship.rb +++ b/app/models/fm_program_sponsorship.rb @@ -28,4 +28,8 @@ class FmProgramSponsorship < ApplicationRecord "FundingID" => "fm_fundings", "ProgramID" => "fm_projects" }.freeze + + def name + fm_words("Sponsorship", fm_with("of", fm_value("ProgramID")), fm_with("by", fm_value("FunderID"))) + end end diff --git a/app/models/fm_project.rb b/app/models/fm_project.rb index faa3b3efb8..e3b879eca0 100644 --- a/app/models/fm_project.rb +++ b/app/models/fm_project.rb @@ -47,7 +47,6 @@ class FmProject < ApplicationRecord include FmArchive FM_KEY_COLUMN = "ProjectID" - FM_NAME_FIELDS = %w[ProjectName].freeze FM_LINKS = { "OrgID" => "fm_organizations", @@ -69,4 +68,8 @@ class FmProject < ApplicationRecord "fm_form_submissions" => { via: "ProjectID", label: "Form Submissions" }, "fm_quotations" => { via: "ProjectID", label: "Quotations" } }.freeze + + def name + fm_value("ProjectName") || fm_id + end end diff --git a/app/models/fm_quotation.rb b/app/models/fm_quotation.rb index f4850d3b59..d75295c840 100644 --- a/app/models/fm_quotation.rb +++ b/app/models/fm_quotation.rb @@ -34,7 +34,6 @@ class FmQuotation < ApplicationRecord include FmArchive FM_KEY_COLUMN = "RecordID" - FM_NAME_FIELDS = %w[PersonName].freeze FM_LINKS = { "RolodexID" => "fm_rolodexes", @@ -42,4 +41,9 @@ class FmQuotation < ApplicationRecord "ProjectID" => "fm_projects", "SourceID" => "fm_rolodexes" }.freeze + + def name + quote = fm_value("Quotation")&.truncate(60) + fm_phrase(quote && "“#{quote}”", fm_words("quote", fm_with("from", fm_value("PersonName")))) + end end diff --git a/app/models/fm_reporting.rb b/app/models/fm_reporting.rb index 5f5bba8ea3..e2b37850ac 100644 --- a/app/models/fm_reporting.rb +++ b/app/models/fm_reporting.rb @@ -14,9 +14,12 @@ class FmReporting < ApplicationRecord include FmArchive FM_KEY_COLUMN = "ReportID" - FM_NAME_FIELDS = %w[ReportDescription].freeze FM_LINKS = { "FundingRecordID" => "fm_fundings" }.freeze + + def name + fm_phrase(fm_value("ReportDescription"), fm_words(fm_with("due", fm_value("ReportDueDate")), fm_with("for", fm_value("FundingRecordID"))) || "funding report") + end end diff --git a/app/models/fm_rolodex.rb b/app/models/fm_rolodex.rb index 6a39d4a9d7..07edb40b08 100644 --- a/app/models/fm_rolodex.rb +++ b/app/models/fm_rolodex.rb @@ -92,7 +92,6 @@ class FmRolodex < ApplicationRecord include FmArchive FM_KEY_COLUMN = "ID" - FM_NAME_FIELDS = [ %w[FirstName LastName], "Organization" ].freeze FM_LINKS = { "OrgID" => "fm_organizations", @@ -199,6 +198,11 @@ class FmRolodex < ApplicationRecord "fm_ancillary_data" => { via: "RolodexID", label: "Ancillary Data" } }.freeze + def name + agency = fm_value("Organization") + fm_join("FirstName", "LastName") || (agency && "#{agency} (agency)") || fm_id + end + def phone_numbers @phone_numbers ||= contact_records(FmPhoneNumber, primary_ids: data.values_at("PrimaryPhoneID")) end diff --git a/app/models/fm_service.rb b/app/models/fm_service.rb index 05f65d47c9..78ae833a0b 100644 --- a/app/models/fm_service.rb +++ b/app/models/fm_service.rb @@ -24,9 +24,12 @@ class FmService < ApplicationRecord include FmArchive FM_KEY_COLUMN = "RecordID" - FM_NAME_FIELDS = [ %w[Month Year] ].freeze FM_LINKS = { "ProjectID" => "fm_projects" }.freeze + + def name + fm_words(fm_value("Month"), fm_value("Year"), "service", fm_with("at", fm_value("ProjectID"))).upcase_first + end end diff --git a/app/models/fm_solicitation.rb b/app/models/fm_solicitation.rb index d63ca8235b..1bfb6a5cfc 100644 --- a/app/models/fm_solicitation.rb +++ b/app/models/fm_solicitation.rb @@ -30,11 +30,14 @@ class FmSolicitation < ApplicationRecord include FmArchive FM_KEY_COLUMN = "SolicitationID" - FM_NAME_FIELDS = %w[NameListing].freeze FM_LINKS = { "CampaignID" => "fm_campaigns", "DonorDevRecID" => "fm_donor_developers", "DonorID" => "fm_rolodexes" }.freeze + + def name + fm_phrase(fm_value("NameListing"), fm_words("solicitation", fm_with("for", fm_value("CampaignID")))) + end end diff --git a/app/models/fm_volunteer.rb b/app/models/fm_volunteer.rb index d94fa65048..b55362f524 100644 --- a/app/models/fm_volunteer.rb +++ b/app/models/fm_volunteer.rb @@ -22,10 +22,13 @@ class FmVolunteer < ApplicationRecord include FmArchive FM_KEY_COLUMN = "VolID" - FM_NAME_FIELDS = [ %w[FirstName LastName] ].freeze FM_LINKS = { "VRecID" => "fm_rolodexes", "EventID" => "fm_events" }.freeze + + def name + fm_phrase(fm_join("FirstName", "LastName"), fm_words(fm_value("Role"), "volunteer", fm_with("at", fm_value("EventID")))) + end end diff --git a/app/models/fm_workshop.rb b/app/models/fm_workshop.rb index ed4cb099a9..0ead84f291 100644 --- a/app/models/fm_workshop.rb +++ b/app/models/fm_workshop.rb @@ -44,9 +44,12 @@ class FmWorkshop < ApplicationRecord include FmArchive FM_KEY_COLUMN = "WorkshopID" - FM_NAME_FIELDS = %w[WorkshopTitle].freeze FM_LINKS = { "AuthorID" => "fm_rolodexes" }.freeze + + def name + fm_value("WorkshopTitle") || fm_id + end end diff --git a/app/models/fm_workshop_log.rb b/app/models/fm_workshop_log.rb index be0d1bd01c..f97066d2f9 100644 --- a/app/models/fm_workshop_log.rb +++ b/app/models/fm_workshop_log.rb @@ -39,11 +39,14 @@ class FmWorkshopLog < ApplicationRecord include FmArchive FM_KEY_COLUMN = "RecordID" - FM_NAME_FIELDS = %w[WorkshopTitle LeaderName].freeze FM_LINKS = { "ProjectID" => "fm_projects", "LeaderID" => "fm_rolodexes", "AgencyID" => "fm_rolodexes" }.freeze + + def name + fm_phrase(fm_value("WorkshopTitle"), fm_words("report", fm_with("by", fm_value("LeaderName")), fm_with("on", fm_value("WorkshopDate")))) + end end diff --git a/config/features.yml b/config/features.yml index 2705a89bd0..598b91efac 100644 --- a/config/features.yml +++ b/config/features.yml @@ -4315,4 +4315,5 @@ FileMaker archive lists and record pages now show each record's name — a person, organization, event title, or similar — instead of only its ID. pro_tips: - - "Records with nothing to name them by, like join rows, show their FileMaker ID instead." + - "Each table words its names its own way, so a person's payment, registration, and volunteer shift never look alike, and an art sale reads like \"After the Rain — purchased for $250\"." + - "Link rows with no name of their own, like committee members, name the FileMaker IDs they connect." diff --git a/spec/models/concerns/fm_archive_spec.rb b/spec/models/concerns/fm_archive_spec.rb index b04bed1fab..404bd9ed85 100644 --- a/spec/models/concerns/fm_archive_spec.rb +++ b/spec/models/concerns/fm_archive_spec.rb @@ -2,35 +2,40 @@ RSpec.describe FmArchive do describe "#name" do - it "joins a record's name parts" do + it "names a person by their full name" do rolodex = FmRolodex.new(fm_id: "00006", data: { "FirstName" => "Archie", "LastName" => " Coded " }) expect(rolodex.name).to eq("Archie Coded") end - it "falls back to the next name field when the first is blank" do + it "names an agency rolodex record by its organization" do agency = FmRolodex.new(fm_id: "00006", data: { "FirstName" => "", "Organization" => "Sunrise Shelter" }) - expect(agency.name).to eq("Sunrise Shelter") + expect(agency.name).to eq("Sunrise Shelter (agency)") end - it "falls back to the FM ID when no name field is filled" do - expect(FmRolodex.new(fm_id: "00006", data: {}).name).to eq("00006") + it "tells an art purchase apart from the artwork by its price" do + purchase = FmArtPurchase.new(fm_id: "1", data: { "ArtTitle" => "After the Rain", "AmountPaid" => "$1,250.00" }) + artwork = FmArtRegistry.new(fm_id: "2", data: { "ArtTitle" => "After the Rain" }) + + expect(purchase.name).to eq("After the Rain — purchased for $1,250") + expect(artwork.name).to eq("After the Rain") + end + + it "words the detail alone when the subject is blank" do + expect(FmArtPurchase.new(fm_id: "1", data: { "AmountPaid" => "90" }).name).to eq("Purchased for $90") end - it "falls back to the FM ID for tables without name fields" do - expect(FmContact.new(fm_id: "C1", data: { "Notes" => "Main contact" }).name).to eq("C1") + it "falls back to the FM ID when nothing names the record" do + expect(FmRolodex.new(fm_id: "00006", data: {}).name).to eq("00006") end - it "names every archive table's records from fields it documents" do - FmArchivesController::TABLES.each_value do |config| - model = config[:model] - source = File.read(Rails.root.join("app/models/#{model.name.underscore}.rb")) + it "gives every seeded archive record its own name, though the seed reuses people and art across tables" do + expect { load Rails.root.join("db/seeds/dev/fm_archive.rb") }.to output.to_stdout + + names = FmArchivesController::TABLES.values.flat_map { |config| config[:model].all.map(&:name) } - model::FM_NAME_FIELDS.flatten.each do |field| - expect(source).to match(/^#\s+#{Regexp.escape(field)}\b/), "#{model} names records by undocumented field #{field}" - end - end + expect(names.tally.select { |_, count| count > 1 }.keys).to be_empty end end end diff --git a/spec/requests/fm_archives_spec.rb b/spec/requests/fm_archives_spec.rb index 353d1c9259..0f7667b9ac 100644 --- a/spec/requests/fm_archives_spec.rb +++ b/spec/requests/fm_archives_spec.rb @@ -160,6 +160,7 @@ get fm_archive_path("P2775", table: "projects") expect(response.body).not_to include("Available fields") + end it "heads the page with the record's name and table" do FmOrganization.create!(fm_id: "ORG1", data: { "OrgName" => "Sunrise Shelter" }) From 6dcccbf5e86c7e3d0e79945b91a74e5b466f93c1 Mon Sep 17 00:00:00 2001 From: Mae Beale Date: Tue, 13 Oct 2026 10:13:23 -0400 Subject: [PATCH 4/5] Show names beside the IDs the grouped rolodex cards link to The cards and related-records list showed bare FileMaker IDs, so admins had to open each one to see what it was; the decorators also rebuilt org and project names by hand. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../fm_people_to_organization_decorator.rb | 2 +- app/decorators/fm_personnel_decorator.rb | 4 ++-- app/helpers/filemaker_helper.rb | 11 ++++++++--- app/views/fm_archives/_listed_records.html.erb | 2 +- app/views/fm_archives/show.html.erb | 11 ++++++++--- config/features.yml | 1 + spec/helpers/filemaker_helper_spec.rb | 12 ++++++++++++ spec/requests/fm_archives_spec.rb | 8 ++++++++ 8 files changed, 41 insertions(+), 10 deletions(-) diff --git a/app/decorators/fm_people_to_organization_decorator.rb b/app/decorators/fm_people_to_organization_decorator.rb index c27e6237e8..775f99c0b2 100644 --- a/app/decorators/fm_people_to_organization_decorator.rb +++ b/app/decorators/fm_people_to_organization_decorator.rb @@ -5,6 +5,6 @@ def kind def summary org_id = data["OrgID"] - FmOrganization.find_by(fm_id: org_id)&.data&.dig("OrgName").presence || org_id + FmOrganization.find_by(fm_id: org_id)&.name || org_id end end diff --git a/app/decorators/fm_personnel_decorator.rb b/app/decorators/fm_personnel_decorator.rb index 769218c1e1..e88a498e76 100644 --- a/app/decorators/fm_personnel_decorator.rb +++ b/app/decorators/fm_personnel_decorator.rb @@ -13,7 +13,7 @@ def program project = FmProject.find_by(fm_id: data["ProjectID"]) return data["ProjectID"] unless project - organization = FmOrganization.find_by(fm_id: project.data["OrgID"])&.data&.dig("OrgName") - [ project.data["ProjectName"].presence || project.fm_id, organization ].compact_blank.join(" at ") + organization = FmOrganization.find_by(fm_id: project.data["OrgID"])&.name + [ project.name, organization ].compact_blank.join(" at ") end end diff --git a/app/helpers/filemaker_helper.rb b/app/helpers/filemaker_helper.rb index afbdfe495f..00ea4cea6f 100644 --- a/app/helpers/filemaker_helper.rb +++ b/app/helpers/filemaker_helper.rb @@ -41,13 +41,18 @@ def filemaker_portal_link(record) link_to text, path, class: button_classes(:primary) end - # An archive field's link to the record it names, struck through when the archive has no such record. + # An archive field's link to the record it names, followed by that record's name; struck through + # when the archive has no such record. def fm_archive_reference_link(fm_id, table) key = table.delete_prefix("fm_") model = FmArchivesController::TABLES.dig(key, :model) - return tag.span(fm_id, class: "line-through opacity-60", title: "No FM record found") if model && !model.exists?(fm_id: fm_id) + target = model&.find_by(fm_id: fm_id) + return tag.span(fm_id, class: "line-through opacity-60", title: "No FM record found") if model && !target - link_to fm_id, fm_archive_path(fm_id, table: key), class: "text-primary hover:underline" + link = link_to(fm_id, fm_archive_path(fm_id, table: key), class: "text-primary hover:underline") + return link if target.nil? || target.name == fm_id + + safe_join([ link, tag.span(target.name, class: "ml-1 text-gray-500") ]) end # The portal's name for a related table's counterpart, when the archive's label doesn't already lead with it. diff --git a/app/views/fm_archives/_listed_records.html.erb b/app/views/fm_archives/_listed_records.html.erb index 80652052ea..1ef2db6785 100644 --- a/app/views/fm_archives/_listed_records.html.erb +++ b/app/views/fm_archives/_listed_records.html.erb @@ -16,7 +16,7 @@ <% if record.persisted? %> - <%= link_to record.decorate.summary.presence || record.fm_id, + <%= link_to record.decorate.summary.presence || record.name, fm_archive_path(record.fm_id, table: record.class.table_name.delete_prefix("fm_")), class: "text-primary hover:underline" %> <% else %> diff --git a/app/views/fm_archives/show.html.erb b/app/views/fm_archives/show.html.erb index b36f00af01..7970017f47 100644 --- a/app/views/fm_archives/show.html.erb +++ b/app/views/fm_archives/show.html.erb @@ -65,9 +65,14 @@ <% end %>
<% info[:records].each do |record| %> - <%= link_to record.fm_id, fm_archive_path(record.fm_id, table: info[:table].sub("fm_", "")), - class: "text-xs font-mono text-primary hover:underline bg-gray-50 px-2 py-1 rounded", - data: { turbo_frame: "_top" } %> + <%= link_to fm_archive_path(record.fm_id, table: info[:table].sub("fm_", "")), + class: "text-xs text-primary hover:underline bg-gray-50 px-2 py-1 rounded", + title: record.name, data: { turbo_frame: "_top" } do %> + <%= record.fm_id %> + <% if record.name != record.fm_id %> + <%= truncate(record.name, length: 40) %> + <% end %> + <% end %> <% end %>
<% if info[:records].total_pages > 1 %> diff --git a/config/features.yml b/config/features.yml index 598b91efac..2c896ca7f0 100644 --- a/config/features.yml +++ b/config/features.yml @@ -4317,3 +4317,4 @@ pro_tips: - "Each table words its names its own way, so a person's payment, registration, and volunteer shift never look alike, and an art sale reads like \"After the Rain — purchased for $250\"." - "Link rows with no name of their own, like committee members, name the FileMaker IDs they connect." + - "An ID field that points at another archive record, like a rolodex OrgID, shows that record's name beside the link, and related records list their names beside their IDs." diff --git a/spec/helpers/filemaker_helper_spec.rb b/spec/helpers/filemaker_helper_spec.rb index 68977695d8..dd7af8d956 100644 --- a/spec/helpers/filemaker_helper_spec.rb +++ b/spec/helpers/filemaker_helper_spec.rb @@ -92,6 +92,18 @@ .to include(%(href="#{fm_archive_path("74001", table: "addresses")}")) end + it "follows the link with the record's name" do + FmOrganization.create!(fm_id: "ORG1", data: { "OrgName" => "Sample Shelter" }) + + expect(helper.fm_archive_reference_link("ORG1", "fm_organizations")).to match(%r{>ORG1]*>Sample Shelter
}) + end + + it "leaves the name off a record named only by its ID" do + FmAddress.create!(fm_id: "74001") + + expect(helper.fm_archive_reference_link("74001", "fm_addresses")).not_to include(" "Art supplies", "AmountGranted" => "500")) + + get fm_archive_path(rolodex.fm_id, table: "rolodexes") + + expect(response.body).to match(%r{>FND1\s*]*>Art supplies — \$500 grant}) + end end it "shows a rolodex record's fields under group headings, most important first" do From ce47cc279086bdf10e5641c577c000c663f8dcf0 Mon Sep 17 00:00:00 2001 From: Mae Beale Date: Tue, 13 Oct 2026 10:19:40 -0400 Subject: [PATCH 5/5] Lay out archive ID fields like the phone and address rows An ID field showed the ID as the value with the name trailing in gray; putting the ID beside the label and linking the name reads like the rolodex contact rows. Resolving targets in one place also fixes payment RefIDs never linking (their prefix map was misnamed) and note pages linking every field by Context instead of just LinkID. Co-Authored-By: Claude Opus 5.5 (1M context) --- app/helpers/filemaker_helper.rb | 37 ++++++++++------ app/models/concerns/fm_archive.rb | 11 +++++ app/models/fm_payment.rb | 4 +- app/views/fm_archives/_fields.html.erb | 20 ++++----- .../fm_archives/_listed_records.html.erb | 15 +------ config/features.yml | 3 +- spec/helpers/filemaker_helper_spec.rb | 43 +++++++++++++------ spec/requests/fm_archives_spec.rb | 37 ++++++++++++++++ 8 files changed, 119 insertions(+), 51 deletions(-) diff --git a/app/helpers/filemaker_helper.rb b/app/helpers/filemaker_helper.rb index 00ea4cea6f..a3135b8592 100644 --- a/app/helpers/filemaker_helper.rb +++ b/app/helpers/filemaker_helper.rb @@ -41,18 +41,31 @@ def filemaker_portal_link(record) link_to text, path, class: button_classes(:primary) end - # An archive field's link to the record it names, followed by that record's name; struck through - # when the archive has no such record. - def fm_archive_reference_link(fm_id, table) - key = table.delete_prefix("fm_") - model = FmArchivesController::TABLES.dig(key, :model) - target = model&.find_by(fm_id: fm_id) - return tag.span(fm_id, class: "line-through opacity-60", title: "No FM record found") if model && !target - - link = link_to(fm_id, fm_archive_path(fm_id, table: key), class: "text-primary hover:underline") - return link if target.nil? || target.name == fm_id - - safe_join([ link, tag.span(target.name, class: "ml-1 text-gray-500") ]) + # The archive record an ID field points at, or an unsaved placeholder when the archive has none. + def fm_archive_record(fm_id, table) + model = FmArchivesController::TABLES.fetch(table.delete_prefix("fm_"))[:model] + model.find_by(fm_id: fm_id) || model.new(fm_id: fm_id, data: {}) + end + + # A record's FileMaker ID, linked to its archive page, or struck through when the archive lacks it. + def fm_archive_id_tag(record) + unless record.persisted? + return tag.span(record.fm_id, class: "ml-1 font-mono text-xs font-normal text-gray-400 line-through", title: "No FM record found") + end + + link_to record.fm_id, fm_archive_record_path(record), class: "ml-1 font-mono text-xs font-normal text-primary hover:underline" + end + + # What a record is, linked to its archive page, or a not-found note when the archive lacks it. + def fm_archive_summary_tag(record) + return tag.span("Not found in FM data", class: "text-gray-500 italic") unless record.persisted? + + summary = record.class.decorator_class? ? record.decorate.try(:summary).presence : nil + link_to summary || record.name, fm_archive_record_path(record), class: "text-primary hover:underline" + end + + def fm_archive_record_path(record) + fm_archive_path(record.fm_id, table: record.class.table_name.delete_prefix("fm_")) end # The portal's name for a related table's counterpart, when the archive's label doesn't already lead with it. diff --git a/app/models/concerns/fm_archive.rb b/app/models/concerns/fm_archive.rb index 570684d9b0..ec65065e8f 100644 --- a/app/models/concerns/fm_archive.rb +++ b/app/models/concerns/fm_archive.rb @@ -8,6 +8,8 @@ module FmArchive PORTAL_COUNTERPARTS = {}.freeze PORTAL_FIELDS = {}.freeze USER_LOOKUP_FIELDS = [].freeze + CONTEXT_LINK_MAP = {}.freeze + PREFIX_LINK_MAP = {}.freeze included do before_validation :set_defaults @@ -62,6 +64,15 @@ def records_listed_under(_label) [] end + # The archive table a field's ID points at. A polymorphic field, mapped to nil in FM_LINKS, resolves + # by the record's Context or by the ID's prefix. + def reference_table(key) + value = data[key] + return if value.blank? || !self.class::FM_LINKS.key?(key) + + self.class::FM_LINKS[key] || self.class::CONTEXT_LINK_MAP[data["Context"]] || self.class::PREFIX_LINK_MAP[value.to_s[0]] + end + # Each table words its names its own way, so records from different tables never share one. def name fm_id diff --git a/app/models/fm_payment.rb b/app/models/fm_payment.rb index 1f4096042a..ac738c2584 100644 --- a/app/models/fm_payment.rb +++ b/app/models/fm_payment.rb @@ -45,10 +45,10 @@ class FmPayment < ApplicationRecord "OrgID" => "fm_organizations", "ProjectID" => "fm_projects", "ParticRecID" => "fm_participants", - "RefID" => nil # polymorphic — resolved by prefix via REFID_PREFIX_MAP + "RefID" => nil # polymorphic — resolved by prefix via PREFIX_LINK_MAP }.freeze - REFID_PREFIX_MAP = { + PREFIX_LINK_MAP = { "E" => "fm_events", "F" => "fm_fundings", "e" => "fm_events" diff --git a/app/views/fm_archives/_fields.html.erb b/app/views/fm_archives/_fields.html.erb index b49f4b53e1..139d945a07 100644 --- a/app/views/fm_archives/_fields.html.erb +++ b/app/views/fm_archives/_fields.html.erb @@ -1,18 +1,16 @@
<% fields.each do |key, value| %> + <% table = record.reference_table(key) %> + <% target = fm_archive_record(value, table) if table %>
-
<%= key %>
-
- <% link_target = record.class::FM_LINKS[key] %> - <% if link_target && value.present? %> - <%= fm_archive_reference_link(value, link_target) %> - <% elsif link_target.nil? && value.present? && record.class.const_defined?(:CONTEXT_LINK_MAP) && (ctx = record.data["Context"]) && (ctx_target = record.class::CONTEXT_LINK_MAP[ctx]) %> - <%= fm_archive_reference_link(value, ctx_target) %> - <% elsif link_target.nil? && value.present? && record.class.const_defined?(:PREFIX_LINK_MAP) && (prefix_target = record.class::PREFIX_LINK_MAP[value.to_s[0]]) %> - <%= fm_archive_reference_link(value, prefix_target) %> - <% else %> - <%= value %> +
+ <%= key %> + <% if target %> + <%= fm_archive_id_tag(target) %> <% end %> +
+
+ <%= target ? fm_archive_summary_tag(target) : value %>
<% end %> diff --git a/app/views/fm_archives/_listed_records.html.erb b/app/views/fm_archives/_listed_records.html.erb index 1ef2db6785..eeb8f79050 100644 --- a/app/views/fm_archives/_listed_records.html.erb +++ b/app/views/fm_archives/_listed_records.html.erb @@ -4,24 +4,13 @@
  • <%= record.decorate.kind %> - <% if record.persisted? %> - <%= link_to record.fm_id, fm_archive_path(record.fm_id, table: record.class.table_name.delete_prefix("fm_")), - class: "ml-1 font-mono text-xs font-normal text-primary hover:underline" %> - <% else %> - <%= record.fm_id %> - <% end %> + <%= fm_archive_id_tag(record) %> <% owner.decorate.contact_tags(record).each do |tag| %> <%= tag %> <% end %> - <% if record.persisted? %> - <%= link_to record.decorate.summary.presence || record.name, - fm_archive_path(record.fm_id, table: record.class.table_name.delete_prefix("fm_")), - class: "text-primary hover:underline" %> - <% else %> - Not found in FM data - <% end %> + <%= fm_archive_summary_tag(record) %>
  • <% end %> diff --git a/config/features.yml b/config/features.yml index 2c896ca7f0..4449d15688 100644 --- a/config/features.yml +++ b/config/features.yml @@ -4317,4 +4317,5 @@ pro_tips: - "Each table words its names its own way, so a person's payment, registration, and volunteer shift never look alike, and an art sale reads like \"After the Rain — purchased for $250\"." - "Link rows with no name of their own, like committee members, name the FileMaker IDs they connect." - - "An ID field that points at another archive record, like a rolodex OrgID, shows that record's name beside the link, and related records list their names beside their IDs." + - "An ID field that points at another archive record, like a rolodex OrgID, reads like the phone and address rows: the ID sits beside the field name and the record's name links to it. A struck-through ID means the archive has no such record." + - "A payment's RefID now links to the event or funding it was for." diff --git a/spec/helpers/filemaker_helper_spec.rb b/spec/helpers/filemaker_helper_spec.rb index dd7af8d956..970713661e 100644 --- a/spec/helpers/filemaker_helper_spec.rb +++ b/spec/helpers/filemaker_helper_spec.rb @@ -84,34 +84,53 @@ end end - describe "#fm_archive_reference_link" do - it "links an ID the archive holds to its record" do - FmAddress.create!(fm_id: "74001") + describe "#fm_archive_record" do + it "finds the record an ID points at" do + organization = FmOrganization.create!(fm_id: "ORG1") - expect(helper.fm_archive_reference_link("74001", "fm_addresses")) - .to include(%(href="#{fm_archive_path("74001", table: "addresses")}")) + expect(helper.fm_archive_record("ORG1", "fm_organizations")).to eq(organization) end - it "follows the link with the record's name" do - FmOrganization.create!(fm_id: "ORG1", data: { "OrgName" => "Sample Shelter" }) + it "stands in an unsaved placeholder for an ID the archive lacks" do + record = helper.fm_archive_record("ORG9", "fm_organizations") - expect(helper.fm_archive_reference_link("ORG1", "fm_organizations")).to match(%r{>ORG1]*>Sample Shelter}) + expect([ record.class, record.fm_id, record.persisted? ]).to eq([ FmOrganization, "ORG9", false ]) end + end - it "leaves the name off a record named only by its ID" do - FmAddress.create!(fm_id: "74001") + describe "#fm_archive_id_tag" do + it "links a record's ID to its archive page" do + html = helper.fm_archive_id_tag(FmAddress.create!(fm_id: "74001")) - expect(helper.fm_archive_reference_link("74001", "fm_addresses")).not_to include("]*href="#{Regexp.escape(fm_archive_path("74001", table: "addresses"))}"[^>]*>74001}) end it "strikes through an ID the archive has no record for" do - html = helper.fm_archive_reference_link("74999", "fm_addresses") + html = helper.fm_archive_id_tag(FmAddress.new(fm_id: "74999")) expect(html).to include("line-through", %(title="No FM record found"), "74999") expect(html).not_to include("href") end end + describe "#fm_archive_summary_tag" do + it "links a record by its decorator's summary" do + address = FmAddress.create!(fm_id: "74001", data: { "AddrsLine1" => "88 Rose Ave", "AddrsCity" => "Venice" }) + + expect(helper.fm_archive_summary_tag(address)).to include(">88 Rose Ave, Venice") + end + + it "links a record without a decorator by its name" do + organization = FmOrganization.create!(fm_id: "ORG1", data: { "OrgName" => "Sample Shelter" }) + + expect(helper.fm_archive_summary_tag(organization)).to include(">Sample Shelter") + end + + it "notes a record the archive lacks as not found" do + expect(helper.fm_archive_summary_tag(FmOrganization.new(fm_id: "ORG9"))).to include("Not found in FM data") + end + end + describe "#fm_portal_counterpart_name" do let(:rolodex) { FmRolodex.new } diff --git a/spec/requests/fm_archives_spec.rb b/spec/requests/fm_archives_spec.rb index dac8af5ea8..7107a9d748 100644 --- a/spec/requests/fm_archives_spec.rb +++ b/spec/requests/fm_archives_spec.rb @@ -170,6 +170,43 @@ expect(response.body).not_to include("Available fields") end + it "links a note's LinkID by its context, leaving its other fields unlinked" do + FmProject.create!(fm_id: "P1", data: { "ProjectName" => "Adult Windows" }) + FmNote.create!(fm_id: "N1", data: { "Context" => "Project", "LinkID" => "P1", "Subject" => "Site visit" }) + + get fm_archive_path("N1", table: "notes") + + expect(response.body).to include(fm_archive_path("P1", table: "projects")) + expect(response.body).not_to include("Site visit", "line-through") + end + + it "links a payment's RefID to the table its prefix names" do + FmEvent.create!(fm_id: "E1", data: { "Title" => "Spring Art Auction" }) + FmPayment.create!(fm_id: "D1", data: { "RefID" => "E1" }) + + get fm_archive_path("D1", table: "payments") + + expect(response.body).to include(fm_archive_path("E1", table: "events")) + end + + it "shows a linked field's ID beside its label and the record's name as the link" do + FmOrganization.create!(fm_id: "ORG1", data: { "OrgName" => "Sample Shelter" }) + FmProject.create!(fm_id: "P1", data: { "OrgID" => "ORG1" }) + + get fm_archive_path("P1", table: "projects") + + path = Regexp.escape(fm_archive_path("ORG1", table: "organizations")) + expect(response.body).to match(%r{OrgID\s*]*href="#{path}"[^>]*>ORG1\s*\s*]*>\s*]*href="#{path}"[^>]*>Sample Shelter}) + end + + it "marks a linked field whose record the archive lacks as not found" do + FmProject.create!(fm_id: "P1", data: { "OrgID" => "ORG9" }) + + get fm_archive_path("P1", table: "projects") + + expect(response.body).to match(%r{OrgID\s*]*line-through[^>]*>ORG9.*Not found in FM data}m) + end + it "heads the page with the record's name and table" do FmOrganization.create!(fm_id: "ORG1", data: { "OrgName" => "Sunrise Shelter" })