From 11447bd96f0100e90d99c221a6b7c7af10ccba2b Mon Sep 17 00:00:00 2001 From: Thomas Trompette Date: Tue, 28 Jul 2026 14:27:26 +0200 Subject: [PATCH] fix: make add-select-option work on record detail pages (#23420) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Enter 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. --- .../ui/meta-types/hooks/useAddSelectOption.ts | 10 +++++++--- .../ui/meta-types/hooks/useCanAddSelectOption.ts | 11 ++++++----- .../input/components/MultiSelectFieldInput.tsx | 4 ++-- .../meta-types/input/components/SelectFieldInput.tsx | 4 ++-- 4 files changed, 17 insertions(+), 12 deletions(-) diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/hooks/useAddSelectOption.ts b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/hooks/useAddSelectOption.ts index d81bb191ba..b351d3c2b8 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/hooks/useAddSelectOption.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/hooks/useAddSelectOption.ts @@ -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; diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/hooks/useCanAddSelectOption.ts b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/hooks/useCanAddSelectOption.ts index 83df7c5465..ff678ebe83 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/hooks/useCanAddSelectOption.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/hooks/useCanAddSelectOption.ts @@ -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 }; }; diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/components/MultiSelectFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/components/MultiSelectFieldInput.tsx index 2ebd83940a..4b229656cf 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/components/MultiSelectFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/components/MultiSelectFieldInput.tsx @@ -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); diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/components/SelectFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/components/SelectFieldInput.tsx index 1571a8a104..0384de6a9a 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/components/SelectFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/components/SelectFieldInput.tsx @@ -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);