Skip to content

Add typeahead + multi-select opt-ins for controlled compound members#7540

Open
laritakr wants to merge 8 commits into
mainfrom
compound-controlled-typeahead-multiselect
Open

Add typeahead + multi-select opt-ins for controlled compound members#7540
laritakr wants to merge 8 commits into
mainfrom
compound-controlled-typeahead-multiselect

Conversation

@laritakr

@laritakr laritakr commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Issue:

Summary

Adds two opt-in, per-member form: flags for compound controlled metadata members in the M3 profile. Both default off, so existing controlled members render exactly as before.

  • autocomplete: true — binds a select2 typeahead over the member's <select>. The full option list is already in the DOM, so filtering is client-side (no extra request). Works for single- and multi-select.
  • multiple: true — renders a <select multiple>. At submit time each selected value fans out into its own compound entry, sharing the row's other members (cartesian product when a row has more than one multiple member). A contributor picked with three roles is stored as three single-role entries, not one entry holding an array.

Screenshots

Deposit form — Name and Role controls aligned (empty state):
pr-shot-1-empty-matched

Multi-select typeahead with several roles chosen (pills wrap in one box, no overflow):

image

Edit round-trip — one saved row's roles reopen as separate preselected entries:

pr-shot-3-edit-roundtrip-fixed

Show pg

image

** MOBILE VIEW **

pr-shot-3-mobile

Details

The key design decision is fan-out: the stored shape stays uniform — every member value is a scalar, whether or not the member is multi-select. Consumers never branch on array-vs-scalar, and each term indexes and facets independently. The array only exists transiently between form submit and fan-out; the indexer and show renderer were hardened to tolerate a transient array value because an invalid submission is echoed back before fan-out runs.

Changed (all form / index / render path — nothing touches persistence):

  • app/services/hyrax/compound_schema.rbnormalize_subproperty reads form.autocomplete / form.multiple into the spec (default false).
  • app/forms/concerns/hyrax/compound_field_behavior.rb — the load-bearing change: fans a multi-select row out into one entry per value.
  • app/views/hyrax/compounds/_compound_row.html.erbmultiple<select multiple> with [] name; autocompletedata-hyrax-compound-controlled + placeholder.
  • app/assets/javascripts/hyrax/compound_metadata.jsbindControlledSelects binds select2 v3 to native selects, on load and on cloned rows.
  • app/indexers/.../compound_indexer.rb, app/renderers/.../compound_attribute_renderer.rb, app/helpers/hyrax/compound_fields_helper.rb, app/services/hyrax/compound_subproperty_labeler.rb — tolerate/represent an array member value.
  • config/locales/hyrax.en.yml — new compound_fields.search placeholder key.
  • documentation/compound_fields.md — new section + type-table row.

Behavior holds in both flexible and non-flexible modes (the compound list comes from the effective schema either way). The Enact profile opt-in (adding these flags to contributor_role) is a separate follow-up in enact_knapsack, not part of this PR.

Testing

Unit/spec coverage added for: schema flag parsing, fan-out (several values → several entries; single value → one scalar; blank handling), array-tolerant indexer/renderer/helper/labeler, and a new view spec for the multi/autocomplete markup. Green on all CI test apps (koppie, dassie, allinson, sirenia).

Manual end-to-end verification in a flexible-mode deposit form (allinson), with autocomplete: true + multiple: true on a controlled compound member:

  • role control renders as a select2 typeahead over a native <select multiple>; typing filters client-side;
  • picking two roles for one participant, saving → the row fans out into two separate scalar entries on the show page (Name / Role per entry), each faceting independently;
  • reopening edit → the two entries re-collapse into their rows, each preselected;
  • "Add" clones a row whose select is also a searchable multi-select.

select2 styling

select2 v3 copies the control's .form-control classes onto its own container (Bootstrap border + fixed .form-control-sm height) while rendering its own bordered inner box, producing a "box within a box" and clipping a multi-select's chosen pills. _compound_metadata.scss flattens the inner box so only the form-control frame shows and lets the frame grow to fit the selected values. Verified in a flexible-mode deposit form (single clean box; multiple pills wrap without overflowing into the next field).

Add two opt-in `form:` flags for a compound `controlled` member:
`autocomplete: true` binds a select2 typeahead over the options
(filtered client-side, since the full list is already in the DOM),
and `multiple: true` renders a multi-select. Both default off, so
existing controlled members are unchanged.

A `multiple` member fans each selected value out into its own
compound entry at submit time, sharing the row's other members.
This keeps every stored member value a scalar — consumers never
branch on array-vs-scalar, and each term indexes and facets on its
own. The indexer and show renderer still tolerate a transient array
value, since an invalid submission is echoed back before fan-out.

WIP: specs are written but not yet run green in a stack; the Enact
profile opt-in and the PR are follow-ups.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@laritakr laritakr added the notes-minor Release Notes: Non-breaking features label Jul 15, 2026
@github-actions

github-actions Bot commented Jul 15, 2026

Copy link
Copy Markdown

Test Results

    17 files  ± 0      17 suites  ±0   3h 31m 10s ⏱️ + 11m 40s
 7 974 tests +17   7 666 ✅ +23  307 💤 ±0  1 ❌  - 6 
27 259 runs  +69  26 659 ✅ +75  599 💤 ±0  1 ❌  - 6 

For more details on these failures, see this check.

Results for commit b7b3b58. ± Comparison against base commit 16ddae4.

This pull request removes 442 and adds 459 tests. Note that renamed tests count towards both.
spec.abilities.ability_spec ‑ Hyrax::Ability AdminSets and PermissionTemplates a user without edit access is expected not to be able to create #<Hyrax::PermissionTemplate:0x00007f16dd0ca508>
spec.abilities.ability_spec ‑ Hyrax::Ability AdminSets and PermissionTemplates a user without edit access is expected not to be able to create #<Hyrax::PermissionTemplate:0x00007f78462bd5c0>
spec.abilities.ability_spec ‑ Hyrax::Ability AdminSets and PermissionTemplates a user without edit access is expected not to be able to create #<Hyrax::PermissionTemplate:0x00007fe00abea158>
spec.abilities.ability_spec ‑ Hyrax::Ability AdminSets and PermissionTemplates a user without edit access is expected not to be able to create #<Hyrax::PermissionTemplate:0x00007ff0cdc20b58>
spec.abilities.ability_spec ‑ Hyrax::Ability AdminSets and PermissionTemplates a user without edit access is expected not to be able to create #<Hyrax::PermissionTemplateAccess:0x00007f16e65add50>
spec.abilities.ability_spec ‑ Hyrax::Ability AdminSets and PermissionTemplates a user without edit access is expected not to be able to create #<Hyrax::PermissionTemplateAccess:0x00007f78463b5518>
spec.abilities.ability_spec ‑ Hyrax::Ability AdminSets and PermissionTemplates a user without edit access is expected not to be able to create #<Hyrax::PermissionTemplateAccess:0x00007fe00fce5b48>
spec.abilities.ability_spec ‑ Hyrax::Ability AdminSets and PermissionTemplates a user without edit access is expected not to be able to create #<Hyrax::PermissionTemplateAccess:0x00007ff0d3a34898>
spec.abilities.ability_spec ‑ Hyrax::Ability AdminSets and PermissionTemplates a user without edit access is expected not to be able to destroy AdminSet: 57b29613-4d45-4d36-afa0-e0c2f4a8096b
spec.abilities.ability_spec ‑ Hyrax::Ability AdminSets and PermissionTemplates a user without edit access is expected not to be able to destroy Hyrax::AdministrativeSet: 12cd0b15-4d7e-4e37-bf1a-2fe430682b7f
…
spec.abilities.ability_spec ‑ Hyrax::Ability AdminSets and PermissionTemplates a user without edit access is expected not to be able to create #<Hyrax::PermissionTemplate:0x00007f2bb0afa018>
spec.abilities.ability_spec ‑ Hyrax::Ability AdminSets and PermissionTemplates a user without edit access is expected not to be able to create #<Hyrax::PermissionTemplate:0x00007f4181787bd8>
spec.abilities.ability_spec ‑ Hyrax::Ability AdminSets and PermissionTemplates a user without edit access is expected not to be able to create #<Hyrax::PermissionTemplate:0x00007f4c5579a290>
spec.abilities.ability_spec ‑ Hyrax::Ability AdminSets and PermissionTemplates a user without edit access is expected not to be able to create #<Hyrax::PermissionTemplate:0x00007f967d26cbc0>
spec.abilities.ability_spec ‑ Hyrax::Ability AdminSets and PermissionTemplates a user without edit access is expected not to be able to create #<Hyrax::PermissionTemplateAccess:0x00007f2bb085f790>
spec.abilities.ability_spec ‑ Hyrax::Ability AdminSets and PermissionTemplates a user without edit access is expected not to be able to create #<Hyrax::PermissionTemplateAccess:0x00007f41848c9110>
spec.abilities.ability_spec ‑ Hyrax::Ability AdminSets and PermissionTemplates a user without edit access is expected not to be able to create #<Hyrax::PermissionTemplateAccess:0x00007f4c59281f48>
spec.abilities.ability_spec ‑ Hyrax::Ability AdminSets and PermissionTemplates a user without edit access is expected not to be able to create #<Hyrax::PermissionTemplateAccess:0x00007f967d2f12d0>
spec.abilities.ability_spec ‑ Hyrax::Ability AdminSets and PermissionTemplates a user without edit access is expected not to be able to destroy AdminSet: fa1cdc2a-a4fe-4290-bb75-f7cc02e4d6e1
spec.abilities.ability_spec ‑ Hyrax::Ability AdminSets and PermissionTemplates a user without edit access is expected not to be able to destroy Hyrax::AdministrativeSet: 0631d6e6-47c2-4535-8d84-1b207e9e01b0
…

♻️ This comment has been updated with latest results.

@ShanaLMoore ShanaLMoore changed the title 🚧 WIP: typeahead + multi-select for controlled compounds Add typeahead + multi-select opt-ins for controlled compound members Jul 15, 2026
select2 v3 copies the native control's `.form-control` classes onto its
container while rendering its own bordered inner box, producing a
"box within a box" and clipping a multi-select's chosen pills at the fixed
`.form-control-sm` height. Flatten the inner box so only the form-control
frame shows, size the empty control to match a sibling `.form-control-sm`
input, and let the frame grow to fit the selected values.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment thread app/assets/stylesheets/hyrax/_compound_metadata.scss Outdated
Comment thread app/assets/stylesheets/hyrax/_compound_metadata.scss Outdated
Comment thread app/assets/stylesheets/hyrax/_compound_metadata.scss Outdated
Comment thread app/assets/stylesheets/hyrax/_compound_metadata.scss Outdated

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds opt-in enhancements for compound controlled members so profiles can enable (1) a select2-backed typeahead over an existing <select> and (2) multi-select submission with server-side fan-out into scalar-valued compound entries, while preserving default/backward-compatible rendering when the flags are off.

Changes:

  • Parse new per-subproperty form.autocomplete / form.multiple flags into the compound schema spec and render controlled members accordingly.
  • Implement multi-select fan-out in compound form deserialization and harden indexing/show rendering paths to tolerate transient array values.
  • Add JS/CSS to bind select2 for controlled selects and adjust styling; add locale/docs + comprehensive specs.

Reviewed changes

Copilot reviewed 17 out of 17 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
spec/views/hyrax/compounds/_compound_row_controlled_multiple_spec.rb Adds view coverage for controlled multi-select + typeahead markup and backward-compat single-select behavior.
spec/services/hyrax/compound_schema_spec.rb Verifies schema parsing for the new form flags and updated normalized specs.
spec/renderers/hyrax/renderers/compound_attribute_renderer_spec.rb Covers show rendering when a controlled value is temporarily an array.
spec/indexers/hyrax/indexers/compound_indexer_spec.rb Covers indexing behavior when a member value is an array (flattening into facet values).
spec/helpers/hyrax/compound_fields_helper_spec.rb Adds helper coverage for array current values (options + forced-value detection).
spec/forms/concerns/hyrax/compound_field_behavior_spec.rb Adds coverage for multi-select fan-out behavior during deserialization.
documentation/compound_fields.md Documents the two new controlled-member opt-ins and fan-out semantics.
config/locales/hyrax.en.yml Adds i18n placeholder text for controlled select2 search.
app/views/hyrax/compounds/_compound_row.html.erb Renders multiple controlled selects ([] name), adds select2 hook + placeholder for autocomplete.
app/services/hyrax/compound_subproperty_labeler.rb Makes label resolution tolerant of array values by joining term labels.
app/services/hyrax/compound_schema.rb Normalizes form.autocomplete / form.multiple into subproperty specs (default false).
app/renderers/hyrax/renderers/compound_attribute_renderer.rb Renders array controlled values as comma-separated term markup.
app/indexers/hyrax/indexers/compound_indexer.rb Flattens array member values during indexing to avoid nested arrays in Solr fields.
app/helpers/hyrax/compound_fields_helper.rb Ensures option lists / forced-value logic handle array current values.
app/forms/concerns/hyrax/compound_field_behavior.rb Implements multi-select fan-out and normalization in compound deserialization.
app/assets/stylesheets/hyrax/_compound_metadata.scss Adds select2 styling adjustments for controlled members (to avoid “double box” + pill clipping).
app/assets/javascripts/hyrax/compound_metadata.js Adds select2 binding for controlled selects, including on cloned rows.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread app/forms/concerns/hyrax/compound_field_behavior.rb
Comment thread app/assets/stylesheets/hyrax/_compound_metadata.scss Outdated
ShanaLMoore and others added 4 commits July 15, 2026 15:03
Drop narration that restated the code and keep only non-obvious reasoning:
the select2 v3 form-control quirk, the Reform populator detail, fan-out's
cartesian product and nil-collapse edge case, why off-list values still
render, and the html_safe precondition.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Scope the select2 styling to a container class tagged by bindControlledSelects
so it no longer applies to other select2 controls (work_or_url / linked_record)
in the same compound row.

Fix scss-lint nits: property order, drop the element-qualified selector, and
avoid the 4-value margin shorthand; also match the empty multi-select's height
to a sibling form-control-sm input.

expand_multiple_members now only collapses `multiple` members to nil, so it
never wipes other members that happen to arrive as arrays and never leaks a
`[]` for an unselected `multiple` member.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Extend the select2 form-control flattening to every select2 control in a
compound row (Item / work_or_url, linked_record, and the new controlled
typeahead), bounded to `.hyrax-compound-row`, so the whole compound form reads
consistently instead of the work_or_url pickers keeping a nested border. This
drops the per-container marker class in favor of the row-scoped selector.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@ShanaLMoore ShanaLMoore requested a review from orangewolf July 16, 2026 18:15
kirkkwang and others added 2 commits July 16, 2026 12:23
`identifier` and `identifiers` (compound) both declared the same
predicate.  In Sirenia (Fedora) the read path resolves a predicate to
whichever attribute was registered first, which depends on filesystem
enumeration order of the metadata YAMLs.  The fix is to give the
`identifiers` (compound) proprety a unique predicate.

(cherry picked from commit 942d8d4)
Drop the remaining what-narration so the code speaks for itself; keep comments
only where the behavior is unusual: the select2 v3 box-in-box, the allowClear
v3 placeholder warning, the fan-out nil-collapse edge case, an array arriving
before fan-out, and why off-list values still render.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@ShanaLMoore ShanaLMoore marked this pull request as ready for review July 16, 2026 20:22
@ShanaLMoore

Copy link
Copy Markdown
Contributor

I ran this locally and can confirm this is working. cc @laritakr

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

notes-minor Release Notes: Non-breaking features

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants