Describe the Bug
Copy Row → Paste Row on a Payload array (or block) field creates phantom rows the editor cannot delete when the array has 10 or more sibling rows AND the source row's index is a digit prefix of another row's index (e.g. row 1 vs rows 10-19, row 2 vs rows 20-29).
After paste, empty rows appear at out-of-bounds indices like .50, .51, .52. The field then fails validation and blocks save. The phantom rows cannot be removed via the UI.
A second variant of the same bug affects textual field-name prefixes (e.g. children vs childrenOther) in the fromRowToField cleanup branch.
Root cause
packages/ui/src/elements/ClipboardAction/mergeFormStateFromClipboard.ts filters form-state by key.startsWith(prefix) at three callsites without enforcing a path boundary:
reduceFormStateByPath's filter loop — builds the clipboard payload, so the leak originates here.
mergeFormStateFromClipboard's paste loop predicate — rewrites leaked keys to out-of-bounds paths instead of dropping them.
mergeFormStateFromClipboard's fromRowToField cleanup branch — can delete unrelated fields whose name shares a textual prefix.
'children.1'.startsWith('children.1') matches 'children.10', 'children.11', etc. 'children'.startsWith('children')matches 'childrenOther'.
Reproduces on @payloadcms/ui@3.84.1 and on current main (v4). PR with fix + four unit-level regression tests will be linked.
Link to the code that reproduces this issue
https://github.com/dawndarkness/payload/blob/fix/clipboard-prefix-collision/packages/ui/src/elements/ClipboardAction/mergeFormStateFromClipboard.spec.ts
Reproduction Steps
Unit-level reproduction (deterministic, no app needed):
- Check out https://github.com/payloadcms/payload at
main.
- Replace
packages/ui/src/elements/ClipboardAction/mergeFormStateFromClipboard.spec.ts
with the version from the linked branch (adds a new describe('prefix collision with multi-digit sibling indices') block; existing tests
untouched).
- Run
pnpm install && pnpm vitest run --project unit packages/ui/src/elements/ClipboardAction/mergeFormStateFromClipboard.spec.ts
- Observe: 4 new tests fail, 9 existing tests pass.
End-user UI reproduction:
- Collection with an array field permitting at least 13 rows.
- Populate 13 rows with distinctive titles.
- Row Copy on row 1 (zero-indexed).
- Paste into any other row.
- Phantom empty rows appear at out-of-bounds positions; field fails validation.
Which area(s) are affected?
area: ui
Environment Info
Payload version: 3.84.1 (bug also reproduces on `main` / v4)
@payloadcms/ui: 3.84.1
Node.js version: 24.15.0
Next.js version: 16.x
Database adapter: any (UI-only bug, DB-agnostic)
Describe the Bug
Copy Row → Paste Row on a Payload array (or block) field creates phantom rows the editor cannot delete when the array has 10 or more sibling rows AND the source row's index is a digit prefix of another row's index (e.g. row 1 vs rows 10-19, row 2 vs rows 20-29).
After paste, empty rows appear at out-of-bounds indices like
.50,.51,.52. The field then fails validation and blocks save. The phantom rows cannot be removed via the UI.A second variant of the same bug affects textual field-name prefixes (e.g.
childrenvschildrenOther) in thefromRowToFieldcleanup branch.Root cause
packages/ui/src/elements/ClipboardAction/mergeFormStateFromClipboard.tsfilters form-state bykey.startsWith(prefix)at three callsites without enforcing a path boundary:reduceFormStateByPath's filter loop — builds the clipboard payload, so the leak originates here.mergeFormStateFromClipboard's paste loop predicate — rewrites leaked keys to out-of-bounds paths instead of dropping them.mergeFormStateFromClipboard'sfromRowToFieldcleanup branch — can delete unrelated fields whose name shares a textual prefix.'children.1'.startsWith('children.1')matches'children.10','children.11', etc.'children'.startsWith('children')matches'childrenOther'.Reproduces on
@payloadcms/ui@3.84.1and on currentmain(v4). PR with fix + four unit-level regression tests will be linked.Link to the code that reproduces this issue
https://github.com/dawndarkness/payload/blob/fix/clipboard-prefix-collision/packages/ui/src/elements/ClipboardAction/mergeFormStateFromClipboard.spec.ts
Reproduction Steps
Unit-level reproduction (deterministic, no app needed):
main.packages/ui/src/elements/ClipboardAction/mergeFormStateFromClipboard.spec.tswith the version from the linked branch (adds a new
describe('prefix collision with multi-digit sibling indices')block; existing testsuntouched).
pnpm install && pnpm vitest run --project unit packages/ui/src/elements/ClipboardAction/mergeFormStateFromClipboard.spec.tsEnd-user UI reproduction:
Which area(s) are affected?
area: ui
Environment Info