Skip to content

fix(dashboard): prevent duplicate items in return create form#15348

Merged
kodiakhq[bot] merged 6 commits into
medusajs:developfrom
Metbcy:fix/return-create-duplicate-items
Jun 11, 2026
Merged

fix(dashboard): prevent duplicate items in return create form#15348
kodiakhq[bot] merged 6 commits into
medusajs:developfrom
Metbcy:fix/return-create-duplicate-items

Conversation

@Metbcy

@Metbcy Metbcy commented May 9, 2026

Copy link
Copy Markdown
Contributor

Summary

What -- What changes are introduced in this PR?

Prevents duplicate return line items from being created when clicking "Add items" multiple times in the return create form.

Why -- Why are these changes relevant or necessary?

Fixes #14936. The useEffect that syncs previewItems to form fields uses items.findIndex() to check for existing entries. However, since items (from useFieldArray) does not update mid-render, calling append() multiple times in the same effect for the same item results in duplicates because findIndex keeps returning -1 for newly appended items.

How -- How have these changes been implemented?

Added a local Set<string> (appendedIds) inside the effect to track item IDs that have already been appended during the current iteration. Before calling append(), the code now checks both the existing items array and the appendedIds set, preventing duplicates.

Testing -- How have these changes been tested, or how can the reviewer test the feature?

  1. Start the admin dashboard locally
  2. Navigate to an order with multiple items
  3. Click "Create Return"
  4. In the return form, click "Add items" and select an item
  5. Click "Add items" again and select the same item
  6. Verify that the item appears only once in the return form (previously it would duplicate)

Checklist

  • I have added a changeset for this PR
  • The changes are covered by relevant tests
  • I have verified the code works as intended locally
  • I have linked the related issue(s) if applicable

Additional Context

The fix is minimal and contained to the useEffect in return-create-form.tsx. No API or backend changes needed since this is purely a frontend form state issue.

@Metbcy Metbcy requested a review from a team as a code owner May 9, 2026 20:11
@changeset-bot

changeset-bot Bot commented May 9, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 57f8799

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 79 packages
Name Type
@medusajs/dashboard Patch
@medusajs/admin-bundler Patch
@medusajs/medusa Patch
@medusajs/test-utils Patch
@medusajs/medusa-oas-cli Patch
integration-tests-http Patch
@medusajs/analytics Patch
@medusajs/api-key Patch
@medusajs/auth Patch
@medusajs/caching Patch
@medusajs/cart Patch
@medusajs/currency Patch
@medusajs/customer Patch
@medusajs/file Patch
@medusajs/fulfillment Patch
@medusajs/index Patch
@medusajs/inventory Patch
@medusajs/link-modules Patch
@medusajs/locking Patch
@medusajs/notification Patch
@medusajs/order Patch
@medusajs/payment Patch
@medusajs/pricing Patch
@medusajs/product Patch
@medusajs/promotion Patch
@medusajs/rbac Patch
@medusajs/region Patch
@medusajs/sales-channel Patch
@medusajs/settings Patch
@medusajs/stock-location Patch
@medusajs/store Patch
@medusajs/tax Patch
@medusajs/translation Patch
@medusajs/user Patch
@medusajs/workflow-engine-inmemory Patch
@medusajs/workflow-engine-redis Patch
@medusajs/draft-order Patch
@medusajs/loyalty-plugin Patch
@medusajs/oas-github-ci Patch
@medusajs/cache-inmemory Patch
@medusajs/cache-redis Patch
@medusajs/event-bus-local Patch
@medusajs/event-bus-redis Patch
@medusajs/analytics-local Patch
@medusajs/analytics-posthog Patch
@medusajs/auth-emailpass Patch
@medusajs/auth-github Patch
@medusajs/auth-google Patch
@medusajs/caching-redis Patch
@medusajs/file-local Patch
@medusajs/file-s3 Patch
@medusajs/fulfillment-manual Patch
@medusajs/locking-postgres Patch
@medusajs/locking-redis Patch
@medusajs/notification-local Patch
@medusajs/notification-sendgrid Patch
@medusajs/payment-stripe Patch
@medusajs/core-flows Patch
@medusajs/framework Patch
@medusajs/js-sdk Patch
@medusajs/modules-sdk Patch
@medusajs/orchestration Patch
@medusajs/types Patch
@medusajs/utils Patch
@medusajs/workflows-sdk Patch
@medusajs/http-types-generator Patch
@medusajs/cli Patch
@medusajs/deps Patch
@medusajs/eslint-plugin Patch
@medusajs/telemetry Patch
@medusajs/admin-sdk Patch
@medusajs/admin-shared Patch
@medusajs/admin-vite-plugin Patch
@medusajs/icons Patch
@medusajs/toolbox Patch
@medusajs/ui-preset Patch
create-medusa-app Patch
medusa-dev-cli Patch
@medusajs/ui Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel

vercel Bot commented May 9, 2026

Copy link
Copy Markdown

@Metbcy is attempting to deploy a commit to the medusajs Team on Vercel.

A member of the Team first needs to authorize it.

@medusa-os-bot

medusa-os-bot Bot commented May 9, 2026

Copy link
Copy Markdown

Thank you for your contribution, @Metbcy!

Could you please fill in the PR description following our pull request template? We need the Testing section completed so we can review your PR efficiently. The What, Why, and How sections look great — we just need to know how these changes have been tested (or how a reviewer can reproduce and verify the fix).

Once you've updated the description, another review will be triggered automatically.

Thanks!

Triggered by: new PR opened

@medusa-os-bot

medusa-os-bot Bot commented May 9, 2026

Copy link
Copy Markdown

Thank you for the update, @Metbcy!

After reviewing the latest commit, we still need one thing addressed before we can move forward:

Required changes:

  • The Testing section in the PR template is still missing. Please describe how these changes were tested, or provide steps a reviewer can follow to reproduce and verify the fix.

Concerns:

The changes in create-refund-form.tsx — rounding the payment amount before storing it as the form's default and updated value — appear unrelated to the issue being fixed (#14936, which is specifically about duplicate line items in the return form). The PR title and the linked issue don't mention this refund rounding behaviour.

Could you clarify whether this is intentional? If so, please add it to the What section of the PR description so reviewers understand the scope. Alternatively, consider moving it to a separate PR — bundling unrelated fixes makes reviews harder and complicates reverting if needed.

The core fix in return-create-form.tsx (using a local appendedItemIds Set to guard against duplicate append() calls in the same effect run) looks correct and addresses the reported issue.

Triggered by: new commit pushed

@medusa-os-bot

medusa-os-bot Bot commented May 9, 2026

Copy link
Copy Markdown

Thank you for the update, @Metbcy!

The Testing section is now complete — that concern is resolved.

One item from the previous review still needs to be addressed:

Required changes:

The fix in return-create-form.tsx — using a local appendedItemIds Set to guard against duplicate append() calls within the same effect run — looks correct and directly addresses the reported issue.

Triggered by: PR description updated

@Metbcy Metbcy force-pushed the fix/return-create-duplicate-items branch from 851039d to 02f83e8 Compare May 9, 2026 20:23
@cursor

cursor Bot commented May 9, 2026

Copy link
Copy Markdown

You have used all of your free Bugbot PR reviews.

To receive reviews on all of your PRs, visit the Cursor dashboard to activate Pro and start your 14-day free trial.

@medusa-os-bot

medusa-os-bot Bot commented May 9, 2026

Copy link
Copy Markdown

Thank you for the update, @Metbcy!

After reviewing the latest commit, both previously flagged concerns are resolved:

  • ✅ The Testing section is now complete
  • ✅ The unrelated create-refund-form.tsx changes have been removed — the PR is now scoped to only the duplicate-items fix

The core fix looks correct: initialising appendedItemIds as a Set<string> inside the useEffect and checking it before calling append() correctly handles the case where items (React state from useFieldArray) doesn't update synchronously mid-loop, preventing duplicate entries within the same effect run while leaving the cross-run deduplication via findIndex intact.

After an initial review, this PR looks good to us. Here's a summary:

✅ PR template is complete
✅ Linked to a verified issue (#14936)
✅ Follows contribution guidelines
✅ Changeset included with correct bump type and message format
✅ Follows Medusa's conventions

A team member will do a final review before this is merged. We appreciate your patience!

Triggered by: new commit pushed

@shahednasser shahednasser left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for opening this PR, but it doesn't fix the original issue. I can still reproduce it based on this comment. You should look into the selectedItems array, as mentioned in the OS bot comment

… after save

Filter selectedItems against current form items before calling addReturnItem
so already-present ordli IDs are not re-posted, and reset selectedItems
after save so stale checks cannot leak between dialog opens.
@Metbcy

Metbcy commented May 19, 2026

Copy link
Copy Markdown
Contributor Author

Good catch @shahednasser, you're right that the original fix was at the wrong layer. The duplicates come from onItemsSelected re-POSTing the full selectedItems array into addReturnItem — since the dialog re-opens with previously-added items pre-checked, hitting Save without unchecking them blindly re-adds every ID, creating duplicate return-line records pointing to the same ordli. Deduping in the useEffect couldn't see this because by then the backend had already minted two distinct retitem IDs.

Pushed 5d61e8d:

  • Filter selectedItems against the current form items before calling addReturnItem, so already-present ordli IDs are skipped
  • Reset selectedItems = [] after save so stale selections can't leak between dialog opens
  • Skip the call entirely when there's nothing new

Repro from #14936 (alunark's steps) no longer fires for me.

@medusa-os-bot

medusa-os-bot Bot commented May 19, 2026

Copy link
Copy Markdown

Thank you for the update, @Metbcy!

The new approach moves the fix from the useEffect to onItemsSelected, which is the correct layer — the duplicates originated from the addReturnItem API call replaying already-added IDs, not from a client-side render race. Filtering selectedItems against the current form items before calling the mutation, skipping the call when there's nothing new, and resetting selectedItems after save all look correct, and the diff is appropriately minimal.

After re-review, this PR still looks good to us:

✅ PR template is complete
✅ Linked to a verified issue (#14936)
✅ Follows contribution guidelines
✅ Changeset included with correct bump type and message format
✅ Follows Medusa's conventions
✅ Scoped to the duplicate-items fix only

A team member will do the final review before this is merged. We appreciate your patience!

Triggered by: new commit pushed

@NicolasGorga NicolasGorga left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @Metbcy thanks for the contribution, but I can still reproduce the issue. What I suggest is that we disable the checkbox for any already selected return item in the add return item table. This way, you use said table to select the items you want, which are added with the default 1 quantity amount- at this point, you can update the quantities from the main return modal. If the user then removes the item from there, they will be able to reselect it in the table.

I suggest the following:

// in /packages/admin/dashboard/src/routes/orders/order-create-return/components/add-return-items-table/add-return-items-table.tsx

// add this variable
const alreadyAdded = useMemo(() => new Set(selectedItems), [selectedItems])

...

const updater: OnChangeFn<RowSelectionState> = (fn) => {
    const newState: RowSelectionState =
      typeof fn === "function" ? fn(rowSelection) : fn

    setRowSelection(newState)
    onSelectionChange(Object.keys(newState).filter((id) => !alreadyAdded.has(id))) // add here to emit only new selections
  }

...

const { table } = useDataTable({
    data: queriedItems as AdminOrderLineItem[],
    columns: columns,
    count: queriedItems.length,
    enablePagination: true,
    getRowId: (row) => row.id,
    pageSize: PAGE_SIZE,
    enableRowSelection: (row) => {
      return getReturnableQuantity(row.original) > 0 && !alreadyAdded.has(row.original.id) // add this second condition to disable already selected ones
    },
    rowSelection: {
      state: rowSelection,
      updater,
    },
  })

With this, you can remove the deduplication in the file you updated and just keep the selectedItems = [] clearing (left comment in the file)

Comment on lines +289 to +299
const existingItemIds = new Set(items.map((i) => i.item_id))
const newItems = selectedItems.filter((id) => !existingItemIds.has(id))

if (newItems.length) {
addReturnItem({
items: newItems.map((id) => ({
id,
quantity: 1,
})),
})
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With what I suggested in the review, this could be rollbacked to what we previously had as it would be redundant. Keep the update in L301

Move dedup from the form into the add-return-items-table per
NicolasGorga's review: rows already in the return are now
disabled in the table (enableRowSelection guard), and the
updater only emits genuinely new ids. Rolls back the
existingItemIds filter in return-create-form since the table
can no longer emit duplicates.

The selectedItems = [] reset after save is kept so stale
selections don't leak between dialog opens.
@Metbcy

Metbcy commented May 21, 2026

Copy link
Copy Markdown
Contributor Author

Thanks @NicolasGorga, makes sense — disabling at the table is the right layer, the user gets a clear signal instead of a silent dedup. Pushed a58d62f:

  • Added alreadyAdded memo in add-return-items-table.tsx
  • updater now emits only ids not in alreadyAdded
  • enableRowSelection returns false for already-added rows
  • Rolled back the existingItemIds filter in return-create-form.tsx since the table can no longer emit dupes
  • Kept the selectedItems = [] reset after save

@medusa-os-bot

medusa-os-bot Bot commented May 21, 2026

Copy link
Copy Markdown

Thanks for iterating on this, @Metbcy!

The new commit moves the dedup from the form layer into AddReturnItemsTable, as suggested. The approach looks correct:

  • alreadyAdded (a Set over the incoming selectedItems prop) is used to disable rows for items already in the return form via enableRowSelection, so the user sees them pre-checked but cannot re-select them — clearer UX than a silent dedup.
  • updater filters already-added IDs out of onSelectionChange, so the parent's module-level selectedItems only ever holds genuinely new picks.
  • In return-create-form.tsx, onItemsSelected now guards on selectedItems.length and resets the module-level selectedItems after save — both sensible.

After re-review, this PR still looks good to us:

✅ PR template is complete
✅ Linked to a verified issue (#14936)
✅ Follows contribution guidelines
✅ Changeset included with correct bump type and message format
✅ Follows Medusa's conventions
✅ Scoped to the duplicate-items fix only

A team member will do the final review before this is merged. We appreciate your patience!

Triggered by: new commit pushed

@NicolasGorga NicolasGorga left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@NicolasGorga NicolasGorga requested a review from shahednasser June 5, 2026 04:16
@kodiakhq kodiakhq Bot merged commit 0cbecc6 into medusajs:develop Jun 11, 2026
30 of 31 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

3 participants