feat: OrderedMultiselect — createable mode with inline item management#553
Closed
feat: OrderedMultiselect — createable mode with inline item management#553
Conversation
Add opt-in `createable: true` option to `react_ordered_multiselect` helper for inline creation of new records directly from the multiselect. - New `react_select_create` API endpoint in autocompletes controller - OrderedMultiselectApp passes createable + onCreateOption to Select - Select component forwards onCreateOption to AsyncCreatableSelect - Rails helper generates data-createable and data-create-url attributes
…tableSelect
The createable loadOptions path was using formatOptions() which wraps
each API response object as {value: obj, label: obj}, causing react-select
to crash when trying to render objects as strings. Now properly extracts
value and label fields from the API response, matching the AsyncPaginate path.
…te) for createable mode When createable: true, dropdown options now show a three-dot menu (⋮) with Rename and Delete actions. Rename opens inline input directly in the dropdown. Delete checks usage count via API and shows a warning if the record is used by other records. New API endpoints: PATCH react_select_update, DELETE react_select_destroy. Custom OptionWithActions component for react-select with green + icon for create option and sub-menu for existing options.
… menu Override innerProps on Option component to check for data-owa-action attribute before propagating mouse events to react-select. This prevents the dropdown from closing when clicking the three-dot menu or sub-menu items.
Rename state was lost when dropdown closed (Option components unmount). Now OptionWithActions only signals onStartRename to parent, and OrderedMultiselectApp renders the rename input in place of the Select. Enter submits, Escape cancels, blur submits.
Replace three-dot sub-menu with inline edit/delete icons visible on hover. Rename now happens inline within the open dropdown — clicking the edit icon transforms the label into an input field, Enter confirms, Escape cancels. Removed rename-outside-select approach.
- Fix rename: add e.preventDefault() on mouseDown to keep dropdown open - Fix rename input: add onMouseDown stopPropagation to prevent blur - Delete dialog: use deleteFromDbConfirm for simple case, deleteWarning with count for shared records - Icons: match Folio patterns (height=16, opacity 0.35, transition-base) - Add deleteFromDbConfirm translations (cs/en)
- Keep menu open during rename by passing menuIsOpen=true when editing - Rename input now appears inside the focused dropdown option - Add rename icon to list items (already-added entries) - Fix icon opacity/hover styles to match Folio conventions
… dirty state - Menu stays open after rename (closes only on explicit close) - Add checkmark confirm button next to rename inputs - Debounce createable loadOptions to prevent flicker on each keystroke - Don't fire change event on rename (saved via API immediately)
Track inlineEditing flag separately so onMenuClose ignores react-select's close request while rename input holds focus.
- Use option.value (not option.id) for rename recordId — fixes order change when renaming server-loaded items (id = join table, value = record) - Remove forceSelectRefresh() from onRenameSubmit — no more flash; dropdown shows new label immediately via local renamedLabel state - isValidNewOption now checks by label (not value) to prevent duplicate create options appearing - Add existingLabels prop so isValidNewOption can check already-added items - Add duplicate check in onCreateOption with alreadyExists translation - Add onBlur to dropdown rename input (submit when clicking outside) - Pass onMenuClose only when createable
- is-invalid red border + invalid-feedback error text on rename inputs (both in dropdown options and list items) when duplicate detected - Rename stays in edit mode on Enter if duplicate (doesn't close) - Create input: noOptionsMessage shows alreadyExists when duplicate typed - Fix Enter key causing form submission in createable select
… detection Track API response in Select state and pass loadedOptions to SelectComponent so OptionWithActions can check rename/create against unselected dropdown options.
…ons guard - Use stopPropagation instead of preventDefault for creatable Enter key: react-select v5 skips its own handler when defaultPrevented is true, so Enter was not triggering onCreateOption - isValidNewOption also checks this.state.loadedOptions (last API response) to catch duplicates during debounce timing gap - noOptionsMessage also checks loadedOptions for 'already exists' case - onCreateOption also checks loadedOptions before calling API - Pass loadedOptions state up to App via onLoadedOptionsChange callback
… reducer - duplicateDetection.test.js: tests for checkDuplicate function and isValidNewOption logic including the timing-gap scenario - orderedMultiselect.test.js: tests for addItem, removeItem, renameItem, removeDeletedItem, updateItems reducers - Export checkDuplicate from OptionWithActions to make it testable
…count - _translations.slim: add missing keys (alreadyExists, rename, deleteFromDb, deleteFromDbConfirm, deleteWarning, addPlaceholder) — they were in YAML but never rendered to window.FolioConsole.translations - Select/index.js: fix Enter page reload for createable — stopPropagation was preventing bubbling but not browser default action (form submit). react-select v3 also skips selectOption when defaultPrevented is true. Solution: do nothing in onKeyDown for createable, let parent wrapper call preventDefault after react-select has already processed the event - OrderedMultiselectApp: add onKeyDown on wrapper div to preventDefault on Enter — fires after react-select's handler (event bubbles up), so onCreateOption is called first, then form submission is blocked
…e detection Pass loadedOptions from App state down to Item component so renaming a selected item is also checked against unselected DB items in the dropdown, not only against other already-selected items.
…ing with labels list - Refactor action buttons in Item and OptionWithActions to use btn btn-none p-0 - Return usage_labels from autocompletes API alongside usage_count - Format delete warning as multi-line with bullet list and count - Add deleteWarningWithLabels locale key (CS + EN)
…avoid JS SyntaxError
…ve duplication - Extract buildAtomSettingsParams() to eliminate duplicated URL building in Select - Extract isDuplicateLabel() to shared module, used by both Item and OptionWithActions - Deduplicate async loadOptions error/empty-result handling into selectedAsFallback() - Simplify onChange, remove verbose comments - Fix document.querySelector → wrapRef.current for change event dispatch - Remove useDebounceTimeout indirection
…ve isDuplicateLabel to utils - Extract InlineRenameInput shared component (used by Item and OptionWithActions) - Move isDuplicateLabel to utils/, update all imports - Replace window.alert/confirm with FolioConsole.Ui.Flash where applicable - Use extractIdFromValue in onSelect, remove inline duplication - Convert CRUD handlers to async/await - Remove fragile renamedLabel local state from OptionWithActions - Add RENAME_ITEM + REMOVE_DELETED_ITEM to saga trigger list - Remove dead yield $wrap line in saga - Fix data-createable to emit "0" instead of nil for consistency with data-sortable - Fix test import after isDuplicateLabel rename
Contributor
|
@vdedek nevím jestli máme v plánu dál rozvíjet react funkcionalitu. Nešlo by to udělat přes stimulus? 📞 kouzelné sluchátko... |
Author
|
@jirkamotejl Jde to udělat ve Stimulusu — pro async search je Tom Select, pro drag & drop SortableJS, inline editace je čistý DOM. Ale znamenalo by to buď:
Tady rozšiřuju existující React komponentu o opt-in režim. Pokud je ale dlouhodobý plán migrovat do stimulusu, tak to asi dává smysl napsat celé v něm. Je to, myslíš, žádoucí? |
Author
|
Udělám to ve stimulusu. Je to i příjemnější s tím pracovat a zároveň výrazně méně kódu. |
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.
Without
createablemode, the component only supports selecting existing records. Any creation, renaming, or deletion requires navigating to a separate CRUD page and coming back. For such simple entities, that's unnecessary friction. Inline management directly in the select field keeps the admin in context and reduces the workflow to a minimum.Summary
Extends the existing
react_ordered_multiselecthelper with an opt-increateable: truemode. The original (non-createable) behaviour isunchanged.
New in createable mode:
btn btn-noneAlso fixes:
deleteWarningWithLabelstranslation (\nin double-quoted YAML caused JS SyntaxError in_translations.slim)Test plan