Add typeahead + multi-select opt-ins for controlled compound members#7540
Add typeahead + multi-select opt-ins for controlled compound members#7540laritakr wants to merge 8 commits into
Conversation
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>
Test Results 17 files ± 0 17 suites ±0 3h 31m 10s ⏱️ + 11m 40s 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.♻️ This comment has been updated with latest results. |
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>
There was a problem hiding this comment.
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.multipleflags 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.
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>
`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>
|
I ran this locally and can confirm this is working. cc @laritakr |
Issue:
Summary
Adds two opt-in, per-member
form:flags for compoundcontrolledmetadata 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 onemultiplemember). 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):

Multi-select typeahead with several roles chosen (pills wrap in one box, no overflow):
Edit round-trip — one saved row's roles reopen as separate preselected entries:
Show pg
** MOBILE VIEW **
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.rb—normalize_subpropertyreadsform.autocomplete/form.multipleinto 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.erb—multiple→<select multiple>with[]name;autocomplete→data-hyrax-compound-controlled+ placeholder.app/assets/javascripts/hyrax/compound_metadata.js—bindControlledSelectsbinds 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— newcompound_fields.searchplaceholder 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 inenact_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: trueon a controlled compound member:<select multiple>; typing filters client-side;Name / Roleper entry), each faceting independently;select2 styling
select2 v3 copies the control's
.form-controlclasses onto its own container (Bootstrap border + fixed.form-control-smheight) while rendering its own bordered inner box, producing a "box within a box" and clipping a multi-select's chosen pills._compound_metadata.scssflattens 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).