[Studio] Fix mapping rows showing a neighbouring row's values after add/remove - #679
Conversation
…dd/remove
`MappingItemWithFilter` watched its own mapping with a dynamic name path:
Form.useWatch(['mappingConfig', fieldIndex])
rc-field-form registers a watcher exactly once (the effect's dependency list is
[isValidForm]) and warns in development that a dynamic namePath is unsupported.
`Form.List` keeps `field.key` stable but shifts `field.name` on add/remove, and
`notifyWatch` fires synchronously before React re-renders — so every row below the
change reads the store at its previous index and keeps that value: adding a mapping
made the row below it show the new mapping's label, sources and data target, and
removing one shifted the displayed values of all rows below it up by one.
Subscribe through a selector that reads the index from a ref (keeping the single
registration valid for value edits) and read the value from the form during render,
so it always belongs to the current index. Structural changes always re-render this
component, so the fresh read cannot go stale.
`handleRemoveItem` called `remove(index)` inside the `setActiveFilter` updater. State updaters must be pure — React invokes them twice in StrictMode, so in a development build a single click on the delete icon removed two mappings. Read the current filter from the existing `activeFilterRef` instead and perform both the filter reset and the removal outside the updater.
|
There was a problem hiding this comment.
Pull request overview
Fixes stale mapping-row values after list insertions/removals and prevents duplicate deletions under React StrictMode.
Changes:
- Reads mapping values from the current form index while maintaining an index-aware watcher.
- Moves form mutation outside the state updater.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
mapping-step.tsx |
Makes mapping removal StrictMode-safe. |
mapping-item-with-filter.tsx |
Prevents stale values after index shifts. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
|
One or more custom setup steps configured for this repository failed during this Copilot code review run: Setup steps run before each review. If the review above is missing context, or no review was posted at all, the failing step above may be the cause. See the workflow run for failure details, fix your setup steps configuration, and re-request a review. Note You can configure setup steps for Copilot code review separately from Copilot cloud agent with a |



Bug
In the Studio Data Importer mapping step, the mapping rows show the wrong values as soon as the list structure changes:
It is a display-only problem — the form store stays correct, so nothing wrong is saved and the display heals itself as soon as any value changes again — but it makes configuring a mapping very confusing.
Steps to reproduce
Verified on 2026.1 and reproducible on
2026.2/2026.x(the file is identical on all three).Cause
MappingItemWithFilterwatched its own mapping with a dynamic name path:rc-field-form'suseWatchregisters its watcher exactly once — the effect's dependency list is[isValidForm]— and warns in development that this is unsupported:`useWatch` is not support dynamic `namePath`. Please provide static instead.Form.Listkeepsfield.keystable but shiftsfield.name(the array index) when an item is inserted or removed, andnotifyWatchfires synchronously, before React re-renders. So onadd(item, 0)the row that was at index 0 still hasfieldIndex === 0at notification time, readsmappingConfig[0]— the new item — and stores it. The subsequent re-render hands itfieldIndex === 1, but nothing re-reads the value, so the stale one stays on screen until the next store change.remove(index)shifts the values of all rows below it the same way.Fix
Subscribe through a selector that reads the index from a ref — that keeps the single registration valid for value edits at a stable index — and read the value from the form during render, so it always belongs to the current index:
Structural changes always re-render this component (
field.namechanges, soMappingsPanelContentrebuilds its item list), which means the fresh read cannot go stale — a missed or mis-targeted notification can now only cost one extra render, never show a wrong value.Watching the whole
mappingConfigarray instead would also be correct but would re-render and re-JSON.stringifyevery row on every keystroke, undoing the existing per-item subscription optimisation inMappingsPanel/MappingsPanelContent.Second commit (independent, easy to drop)
handleRemoveItemcalledremove(index)from inside thesetActiveFilterupdater. State updaters must be pure, and React invokes them twice underStrictMode(which Studio enables), so in a development build one click on the delete icon removed two mappings. The current filter is now read from the existingactiveFilterRef, with the filter reset and the removal both performed outside the updater.Notes
assets/studiohas no jest/vitest setup, so there is no automated test to add here, and the built assets undersrc/Resources/public/studio/build/are left to the automatic frontend build (thecommit-buildjob is skipped for fork PRs).Tracking issue
Fixes pimcore/platform-version#337