fix: make add-select-option work on record detail pages (#23420)

Fixes #23339

Follow-up to #23410, which fixed the neighbouring issue (#23341) for
users *without* the `DATA_MODEL` permission.

## Problem

In #23339 the reporter added a multiselect field to Person and then
found the inline "create option" prompt unresponsive. They clearly have
`DATA_MODEL` permission, since they just created the field, so the
permission check isn't what's blocking them.

The blocker is the *other* precondition. Both hooks read the object name
from the router:

```ts
const { objectNamePlural } = useParams();
```

`objectNamePlural` only exists on record index routes
(`/objects/:objectNamePlural`). Record **detail** pages are
`/object/:objectNameSingular/:objectRecordId`, so on a record page the
param is `undefined` and:

- `useCanAddSelectOption` returns false via
`isNonEmptyString(objectNamePlural)`
- `useAddSelectOption` bails out at `if (!fieldName ||
!objectNamePlural) return;`

So the action was dead on record pages for **every** user, admins
included, and the navigation target it needed was never reachable from
there.

## Fix

Resolve the field and its object from `fieldMetadataId`, which
`FieldDefinition` already carries, instead of reading the object name
off the URL:

```ts
const { fieldMetadataItem, objectMetadataItem } =
  useFieldMetadataItemById(fieldMetadataId);
```

This drops the route dependency entirely, so the action behaves the same
wherever the field is rendered, and `canAddSelectOption` now reflects
only the real permission check.

It also lets both hooks take a single `fieldMetadataId` argument:
`fieldMetadataItem.name` is the same value the callers were previously
passing as `fieldName`, so that parameter is no longer needed. Resolving
both values from one id means the guard and the action can't disagree
about which field they're describing.

`useFieldMetadataItemById` is used rather than
`useFieldMetadataItemByIdOrThrow` because a lookup miss should disable
the prompt, not crash the field input.

## Reproduction

On the code the reporter was running (immediately before #23410), as an
**admin** with full `DATA_MODEL`, on a company record page, typing a
value matching no option:

- `Add "…" to options` renders
- clicking it does nothing — URL unchanged, no navigation
- pressing <kbd>Enter</kbd> does nothing either

which matches #23339 exactly, including the note about the Enter
keypress.

After #23410 the same root cause shows up differently: the prompt is no
longer rendered at all on record pages, since the guard it's now gated
on is false there. Still broken, just silent.

## Testing

Verified manually on a local instance, swapping only these files between
three states and re-running the identical steps on the same cell.

| code state | user | route | result |
|---|---|---|---|
| before #23410 | Admin | `/object/company/:id` | prompt shown, click
and Enter both do nothing |
| current main | Admin | `/object/company/:id` | prompt not shown,
action unreachable |
| **this PR** | Admin | `/object/company/:id` | prompt shown, navigates
to `/settings/objects/companies/workPolicy?newOption=…` |
| **this PR** | Admin | `/objects/tasks` (`Status`, single select) |
still works, navigates to `/settings/objects/tasks/status?newOption=…` |
| **this PR** | Member (no `DATA_MODEL`) | `/object/company/:id` |
prompt not shown |

The settings form opens with the typed value prefilled alongside the
existing options, so the end-to-end flow works from a record page for
the first time.

The Member row confirms #23341 stays fixed: dropping the route
dependency doesn't weaken the permission gate. The single-select row
covers `SelectFieldInput`, which takes the same change.

`nx lint:diff-with-main twenty-front` and `nx typecheck twenty-front`
both pass.
This commit is contained in:
Thomas Trompette
2026-07-28 14:27:26 +02:00
committed by GitHub
parent 7b46c3ed31
commit 11447bd96f
4 changed files with 17 additions and 12 deletions
@@ -1,13 +1,14 @@
import { useFieldMetadataItemById } from '@/object-metadata/hooks/useFieldMetadataItemById';
import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState';
import { shouldNavigateBackToMemorizedUrlOnSaveState } from '@/ui/navigation/states/shouldNavigateBackToMemorizedUrlOnSaveState';
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
import { useCallback } from 'react';
import { useParams } from 'react-router-dom';
import { SettingsPath } from 'twenty-shared/types';
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
export const useAddSelectOption = (fieldName: string) => {
const { objectNamePlural } = useParams();
export const useAddSelectOption = (fieldMetadataId: string) => {
const { fieldMetadataItem, objectMetadataItem } =
useFieldMetadataItemById(fieldMetadataId);
const navigateSettings = useNavigateSettings();
const setNavigationMemorizedUrl = useSetAtomState(
navigationMemorizedUrlState,
@@ -17,6 +18,9 @@ export const useAddSelectOption = (fieldName: string) => {
shouldNavigateBackToMemorizedUrlOnSaveState,
);
const fieldName = fieldMetadataItem?.name;
const objectNamePlural = objectMetadataItem?.namePlural;
const addSelectOption = useCallback(
(optionName: string) => {
if (!fieldName || !objectNamePlural) return;
@@ -1,10 +1,11 @@
import { useFieldMetadataItemById } from '@/object-metadata/hooks/useFieldMetadataItemById';
import { useHasPermissionFlag } from '@/settings/roles/hooks/useHasPermissionFlag';
import { isNonEmptyString } from '@sniptt/guards';
import { useParams } from 'react-router-dom';
import { PermissionFlagType } from '~/generated-metadata/graphql';
export const useCanAddSelectOption = (fieldName: string) => {
const { objectNamePlural } = useParams();
export const useCanAddSelectOption = (fieldMetadataId: string) => {
const { fieldMetadataItem, objectMetadataItem } =
useFieldMetadataItemById(fieldMetadataId);
const userHasPermissionToEditDataModel = useHasPermissionFlag(
PermissionFlagType.DATA_MODEL,
@@ -12,8 +13,8 @@ export const useCanAddSelectOption = (fieldName: string) => {
const canAddSelectOption =
userHasPermissionToEditDataModel &&
isNonEmptyString(fieldName) &&
isNonEmptyString(objectNamePlural);
isNonEmptyString(fieldMetadataItem?.name) &&
isNonEmptyString(objectMetadataItem?.namePlural);
return { canAddSelectOption };
};
@@ -13,10 +13,10 @@ import { useContext } from 'react';
export const MultiSelectFieldInput = () => {
const { fieldDefinition, draftValue, setDraftValue } = useMultiSelectField();
const { addSelectOption } = useAddSelectOption(
fieldDefinition?.metadata?.fieldName,
fieldDefinition.fieldMetadataId,
);
const { canAddSelectOption } = useCanAddSelectOption(
fieldDefinition?.metadata?.fieldName,
fieldDefinition.fieldMetadataId,
);
const { onSubmit } = useContext(FieldInputEventContext);
@@ -19,10 +19,10 @@ import { type SelectOption } from 'twenty-ui/input';
export const SelectFieldInput = () => {
const { fieldDefinition, fieldValue } = useSelectField();
const { addSelectOption } = useAddSelectOption(
fieldDefinition?.metadata?.fieldName,
fieldDefinition.fieldMetadataId,
);
const { canAddSelectOption } = useCanAddSelectOption(
fieldDefinition?.metadata?.fieldName,
fieldDefinition.fieldMetadataId,
);
const { onCancel, onSubmit } = useContext(FieldInputEventContext);