Thomas Trompette a22fdf1d8e fix: prevent duplicate junction rows from double-fired checkbox clicks in multi-select menu items (#22737)
## What

Fixes a bug where selecting a relation from the record picker created
**two** rows in a junction object instead of one (issue #22698).

## Root cause

Base UI's `Checkbox` renders a styled `<span role="checkbox">` plus a
hidden `<input>`. On click of the span it re-dispatches a second,
*bubbling* click on the hidden input (to keep the native input in sync)
without stopping propagation. Both the original span click and the
re-dispatched input click bubble up to the menu-item row, whose
`onClick` drives selection — so one physical click on the checkbox fired
the row's handler **twice**.

This was invisible until now because every consumer's handler was
idempotent:
- multi-select toggle: both calls pass the same `!selected`, net one
toggle
- relation attach: setting a foreign key twice is the same result

The junction relation feature is the first non-idempotent consumer: each
call creates a new junction record with a fresh UUID, so two calls
produced two rows.

## Fix

Extract a shared `MenuItemMultiSelectCheckbox` part that:
- drives selection through the checkbox's own `onCheckedChange` (events
up)
- wraps the checkbox so its click cannot propagate to the row's
`onClick`

The row stays clickable for the rest of the item; the checkbox click and
the row click are now two clean, single-fire event sources. Applied to
all three affected components (`MenuItemMultiSelect`,
`MenuItemMultiSelectAvatar`, `MenuItemMultiSelectTag`) so the whole
class of bug is fixed once, not patched per component. No change to the
shared `Checkbox` API.

## Notes for reviewer

- The `oxlint-disable` for the stopPropagation wrapper's `onClick` now
lives in exactly one place (the shared part), following the existing
precedent in `OverflowingTextWithTooltip`.
- Added a regression interaction test on the `MenuItemMultiSelectAvatar`
story (one checkbox click = one `onSelectChange`); it exercises the
shared part.
- `typecheck`, `oxlint`, and `oxfmt` pass. The Storybook vitest-browser
runner is currently broken locally for all stories, so the interaction
test was not run locally — it runs in CI.


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22737?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-07-09 15:54:55 +02:00
2026-06-11 11:02:28 +02:00

Twenty logo

The #1 Open-Source CRM

Website · Documentation · Roadmap · Discord · Figma

Twenty banner


Why Twenty

Twenty gives technical teams the building blocks for a custom CRM that meets complex business needs and quickly adapts as the business evolves. Twenty is the CRM you build, ship, and version like the rest of your stack.

Learn more about why we built Twenty


Installation

Cloud

The fastest way to get started. Sign up at twenty.com and spin up a workspace in under a minute, with no infrastructure to manage and always up to date.

Build an app

Scaffold a new app with the Twenty CLI:

npx create-twenty-app my-app

Define objects, fields, and views as code:

import { defineObject, FieldType } from 'twenty-sdk/define';

export default defineObject({
  nameSingular: 'deal',
  namePlural: 'deals',
  labelSingular: 'Deal',
  labelPlural: 'Deals',
  fields: [
    { name: 'name', label: 'Name', type: FieldType.TEXT },
    { name: 'amount', label: 'Amount', type: FieldType.CURRENCY },
    { name: 'closeDate', label: 'Close Date', type: FieldType.DATE_TIME },
  ],
});

Then ship it to your workspace:

npx twenty app:publish --private

See the app development guide for objects, views, agents, and logic functions.

Self-hosting

Run Twenty on your own infrastructure with Docker Compose, or contribute locally via the local setup guide.



Everything you need

Twenty gives you the building blocks of a modern CRM (objects, views, workflows, and agents) and lets you extend them as code. Here's a tour of what's in the box.

Want to go deeper? Read the User Guide for product walkthroughs, or the Documentation for developer reference.

Create your apps

Learn more about apps in doc

Stay on top with version control

Learn more about version control in doc

All the tools you need to build anything

Learn more about primitives in doc

Customize your layouts

Learn more about layouts in doc

AI agents and chats

Learn more about AI in doc

Plus all the tools of a good CRM

Learn more about CRM features in doc


Stack

Thanks

Greptile      Sentry      Crowdin

Thanks to these amazing services that we use and recommend for code review (Greptile), catching bugs (Sentry) and translating (Crowdin).

Join the Community

Star the repo · Discord · Feature requests · Releases · X · LinkedIn · Crowdin · Contribute

S
Description
The open alternative to Salesforce, designed for AI.
Readme AGPL-3.0 1.4 GiB
Languages
TypeScript 79.6%
MDX 17.3%
JavaScript 2.7%
Python 0.2%
SCSS 0.1%