feat: Stimulus OrderedMultiselect — createable mode with inline item management#554
Open
feat: Stimulus OrderedMultiselect — createable mode with inline item management#554
Conversation
…management Add new Stimulus controller f-input-ordered-multiselect with stimulus_ordered_multiselect helper for managing has_many :through relations with inline CRUD (create, rename, delete) directly in the select dropdown. Uses Tom Select (single mode) for async search + html5sortable for drag-and-drop reordering. Reuses existing react_select autocomplete endpoints and adds three new API actions (create/update/destroy) with CanCan authorization and class validation.
…cal state - __no_results__ dummy option keeps dropdown open when async load returns empty - Usage labels overlay current record label based on form selection state - Delete confirm dialog prefers local loadedOptions over server response - Dropdown closes after deleting the last remaining option - onItemRemoveClick syncs loadedOptions usage_labels
Mechanical refactoring — no behavior changes: - Merge renderOptionWithActions + restoreOptionHtml into shared optionActionHtml - Unify onOptionRenameInput + onItemRenameInputCheck into shared checkRenameDuplicate - Extract fakeEvent helper in bindDropdownEvents - Condense verbose HTML templates and remove redundant blank lines/comments - Keep all section comments for readability
7 tasks
Author
…condition - Dropdown: cancel active rename before starting a new one (restoreOptionHtml) - Dropdown: stopImmediatePropagation on edit input mousedown/click for cursor positioning - Selected list: mousedown preventDefault on action buttons to prevent focus stealing - Selected list: renderList() + double _blurCancelTimer clear to safely switch renames - Cleanup: _blurCancelTimer in disconnect(), simplify onItemRenameCancel
Allows hiding "Použito v:" usage labels via show_usage: false. When disabled, items are vertically centered (--no-usage CSS modifier). Default: true (backwards compatible).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Motivation
Admin interfaces often include simple taxonomies — records with just a title and position, like tags or feature labels assigned to a product. These are typically created on the fly while editing a parent record, often several in quick succession. Admins also need to rename or delete them on the spot, with changes propagating to all records that reference them.
This PR adds a Stimulus-based
stimulus_ordered_multiselecthelper that provides this functionality. It reuses the existingreact_selectautocomplete API endpoints (thereact_select_*naming is a historical convention — these are plain JSON endpoints with no React dependency) and adds three new actions for create/update/destroy.Summary
New Stimulus controller
f-input-ordered-multiselect— a configurable ordered multiselect. Every feature is opt-in via flags, so the same component scales from a simple select+reorder widget to a full inline CRUD manager.Configuration modes
createable: false, sortable: false, show_usage: falsesortable: true(default)createable: trueshow_usage: true(default)show_usage: falseHelper signature
Features (when createable: true)
react_select_create→ item added to listreact_select_updateconfirmed=truedestroys) → animated removal_destroyvia hidden inputs (no DB delete)usage_labels_for_warningon the model, toggleable viashow_usage)__no_results__dummy option workaround for Tom Select async load (Tom Select doesn't showno_resultsrenderer for async).ts-dropdown-contentcapped at dynamicmax-height(fits between dropdown top and form footer) withoverflow-y: autoModel requirements (for createable mode)
The through-target model (e.g.
ComparisonFeature) should implement:to_console_label— display label (inherited from Folio conventions)usage_count_for_warning— integer, used in delete confirmationusage_labels_for_warning— array of strings, shown as usage hints and in delete warningProtective measures
authorize! :create/:update/:destroy)safe_constantize+< ActiveRecord::Baseguard prevents arbitrary class instantiationconfirmed=trueactually destroys_destroyin hidden inputs; actual deletion requires the trash icon + confirmation_needsReloadflag — marks Tom Select options as stale after mutations; dropdown refreshes on next open viaclearOptions()+load('')preventDropdownClose/restoreDropdownClose— monkey-patchestomSelect.close()during inline editing to prevent accidental dropdown dismissal, with document-level mousedown listener for outside-click detection and proper cleanupNo impact on existing components
OrderedMultiselectAppandreact_ordered_multiselecthelper are unchanged — both versions coexistreact_selectaction gains an optionalusage_labelsfield in its response — backwards compatible, only present when the model responds tousage_labels_for_warningFiles
app/assets/javascripts/folio/input/ordered_multiselect.jsapp/assets/stylesheets/folio/input/_ordered_multiselect.sassapp/assets/javascripts/folio/input.jsapp/assets/stylesheets/folio/_input.sassapp/helpers/folio/console/react_helper.rbstimulus_ordered_multiselectmethodapp/controllers/folio/console/api/autocompletes_controller.rbusage_labelsin response + 3 new actionsconfig/routes.rbKnown limitations
Dropdown opens downward only —
adjustDropdownMaxHeight()dynamically caps the dropdown height to fit between its top edge and the form footer (or viewport bottom), with a minimum of 80 px. The dropdown does not flip upward when space below is limited.Tom Select inline editing workarounds — inline rename/delete places interactive elements inside the Tom Select dropdown, which Tom Select was not designed for. This requires two coupled workarounds:
preventDropdownClosemonkey-patch — temporarily replacestomSelect.close()with a no-op during editing, with a document-levelmousedownlistener for outside-click detection and cleanup.isFocusedmanual reset — because the rename input lives inside the dropdown, Tom Select never fires itsonBlurhandler. After editing ends, we manually setisFocused = trueand callrefreshState().Both workarounds are tested across Escape, Enter, click-outside, and confirm flows.
Test plan
stimulus_ordered_multiselect(f, :tags, sortable: false, show_usage: false)renders minimal select + list, no drag handles, no usage labels, no CRUD buttonsstimulus_ordered_multiselect(f, :tags)renders select + reorder, no create/rename/delete buttons visible_destroyhidden input added, item reappears in dropdown<form>react_ordered_multiselectstill works identically🤖 Generated with Claude Code