Repository: twentyhq/twenty
Pull request #11610: Support Full Name as Record Text Identifier
Author: login: b9aurav
Labels: name: -PR: awaiting author
Created: 2025-04-17T05:08:40Z | Merged: 2025-05-19T22:03:30Z

closes #11296 

[recording.webm](https://github.com/user-attachments/assets/da0f2587-a435-4bee-a802-81eb9ca92733)


Linked issue #11296: Support Full Name as Record Text Identifier
# Current behavior

1. Record Text identifier is not clickable when there is only one available field
2. Can't add a "full name" field as record identifier

![Image](https://github.com/user-attachments/assets/d1a8135c-1370-44c7-98d9-48cdb57e510c)


# Desired Behavior

Support "full name" as record text identifier

Implement this design when only one field is available as text identifier. (Keep the field clickable, do not disable it)

![Image](https://github.com/user-attachments/assets/cfbdf457-0c08-44fa-bf89-400281972012)

Diff:
diff --git a/packages/twenty-front/src/modules/object-metadata/constants/LabelIdentifierFieldMetadataTypes.ts b/packages/twenty-front/src/modules/object-metadata/constants/LabelIdentifierFieldMetadataTypes.ts
index 49e3d2984bf96..b164fc620a7cc 100644
--- a/packages/twenty-front/src/modules/object-metadata/constants/LabelIdentifierFieldMetadataTypes.ts
+++ b/packages/twenty-front/src/modules/object-metadata/constants/LabelIdentifierFieldMetadataTypes.ts
@@ -3,4 +3,5 @@ import { FieldMetadataType } from '~/generated-metadata/graphql';
 export const LABEL_IDENTIFIER_FIELD_METADATA_TYPES = [
   FieldMetadataType.NUMBER,
   FieldMetadataType.TEXT,
+  FieldMetadataType.FULL_NAME,
 ];
diff --git a/packages/twenty-front/src/modules/object-metadata/utils/getLabelIdentifierFieldValue.ts b/packages/twenty-front/src/modules/object-metadata/utils/getLabelIdentifierFieldValue.ts
index 2287dec2bcf3f..2f9254e959ac9 100644
--- a/packages/twenty-front/src/modules/object-metadata/utils/getLabelIdentifierFieldValue.ts
+++ b/packages/twenty-front/src/modules/object-metadata/utils/getLabelIdentifierFieldValue.ts
@@ -9,16 +9,16 @@ export const getLabelIdentifierFieldValue = (
   labelIdentifierFieldMetadataItem: FieldMetadataItem | undefined,
   objectNameSingular: string,
 ): string => {
+  if (!isDefined(labelIdentifierFieldMetadataItem)) {
+    return record.id;
+  }
+
   if (
     objectNameSingular === CoreObjectNameSingular.WorkspaceMember ||
-    labelIdentifierFieldMetadataItem?.type === FieldMetadataType.FULL_NAME
+    labelIdentifierFieldMetadataItem.type === FieldMetadataType.FULL_NAME
   ) {
-    return `${record.name?.firstName ?? ''} ${record.name?.lastName ?? ''}`;
-  }
-
-  if (isDefined(labelIdentifierFieldMetadataItem?.name)) {
-    return record[labelIdentifierFieldMetadataItem.name] ?? '';
+    return `${record[labelIdentifierFieldMetadataItem.name]?.firstName ?? ''} ${record[labelIdentifierFieldMetadataItem.name]?.lastName ?? ''}`;
   }
 
-  return '';
+  return record[labelIdentifierFieldMetadataItem.name] ?? '';
 };
diff --git a/packages/twenty-front/src/modules/object-record/hooks/useRecordChipData.ts b/packages/twenty-front/src/modules/object-record/hooks/useRecordChipData.ts
index f1e844cec10ed..99767a4e33481 100644
--- a/packages/twenty-front/src/modules/object-record/hooks/useRecordChipData.ts
+++ b/packages/twenty-front/src/modules/object-record/hooks/useRecordChipData.ts
@@ -1,5 +1,4 @@
 import { PreComputedChipGeneratorsContext } from '@/object-metadata/contexts/PreComputedChipGeneratorsContext';
-import { generateDefaultRecordChipData } from '@/object-metadata/utils/generateDefaultRecordChipData';
 import { RecordChipData } from '@/object-record/record-field/types/RecordChipData';
 import { ObjectRecord } from '@/object-record/types/ObjectRecord';
 import { useContext } from 'react';
@@ -23,16 +22,13 @@ export const useRecordChipData = ({
   const identifierChipGenerator =
     identifierChipGeneratorPerObject[objectNameSingular];
 
-  if (isDefined(identifierChipGenerator)) {
-    return {
-      recordChipData: identifierChipGenerator(record),
-    };
+  if (!isDefined(identifierChipGenerator)) {
+    throw new Error(
+      `No identifier chip generator found for object name singular: ${objectNameSingular}`,
+    );
   }
 
   return {
-    recordChipData: generateDefaultRecordChipData({
-      objectNameSingular,
-      record,
-    }),
+    recordChipData: identifierChipGenerator(record),
   };
 };
diff --git a/packages/twenty-front/src/modules/object-record/utils/getRecordChipGenerators.ts b/packages/twenty-front/src/modules/object-record/utils/getRecordChipGenerators.ts
index 5605920229ac3..52c05d951e27d 100644
--- a/packages/twenty-front/src/modules/object-record/utils/getRecordChipGenerators.ts
+++ b/packages/twenty-front/src/modules/object-record/utils/getRecordChipGenerators.ts
@@ -11,8 +11,8 @@ import { isLabelIdentifierField } from '@/object-metadata/utils/isLabelIdentifie
 import { isFieldIdentifierDisplay } from '@/object-record/record-field/meta-types/display/utils/isFieldIdentifierDisplay';
 import { RecordChipData } from '@/object-record/record-field/types/RecordChipData';
 import { ObjectRecord } from '@/object-record/types/ObjectRecord';
-import { FieldMetadataType } from '~/generated-metadata/graphql';
 import { isDefined } from 'twenty-shared/utils';
+import { FieldMetadataType } from '~/generated-metadata/graphql';
 
 export const getRecordChipGenerators = (
   objectMetadataItems: ObjectMetadataItem[],
diff --git a/packages/twenty-front/src/modules/settings/data-model/objects/forms/components/SettingsDataModelObjectIdentifiersForm.tsx b/packages/twenty-front/src/modules/settings/data-model/objects/forms/components/SettingsDataModelObjectIdentifiersForm.tsx
index a954b87c2bb17..eba3dee57b6d8 100644
--- a/packages/twenty-front/src/modules/settings/data-model/objects/forms/components/SettingsDataModelObjectIdentifiersForm.tsx
+++ b/packages/twenty-front/src/modules/settings/data-model/objects/forms/components/SettingsDataModelObjectIdentifiersForm.tsx
@@ -13,7 +13,8 @@ import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
 import { Select } from '@/ui/input/components/Select';
 import { zodResolver } from '@hookform/resolvers/zod';
 import { t } from '@lingui/core/macro';
-import { IconCircleOff, useIcons } from 'twenty-ui/display';
+import { useNavigate } from 'react-router-dom';
+import { IconCircleOff, IconPlus, useIcons } from 'twenty-ui/display';
 import { SelectOption } from 'twenty-ui/input';
 
 export const settingsDataModelObjectIdentifiersFormSchema =
@@ -100,6 +101,9 @@ export const SettingsDataModelObjectIdentifiersForm = ({
     label: 'None',
     value: null,
   };
+
+  const navigate = useNavigate();
+
   return (
     <StyledContainer>
       {[
@@ -124,12 +128,23 @@ export const SettingsDataModelObjectIdentifiersForm = ({
           render={({ field: { onChange, value } }) => (
             <Select
               label={label}
-              disabled={!objectMetadataItem.isCustom || !options.length}
               fullWidth
               dropdownId={`${fieldName}-select`}
               emptyOption={emptyOption}
               options={options}
               value={value}
+              withSearchInput={label === t`Record label`}
+              callToActionButton={
+                label === t`Record label`
+                  ? {
+                      text: 'Create Text Field',
+                      Icon: IconPlus,
+                      onClick: () => {
+                        navigate('./new-field/select');
+                      },
+                    }
+                  : undefined
+              }
               onChange={(value) => {
                 onChange(value);
                 formConfig.handleSubmit(handleSave)();
diff --git a/packages/twenty-server/src/engine/metadata-modules/utils/validate-metadata-name-is-not-reserved-keyword.ts b/packages/twenty-server/src/engine/metadata-modules/utils/validate-metadata-name-is-not-reserved-keyword.ts
index 1d0da0a19cfd4..70b4a02363bb6 100644
--- a/packages/twenty-server/src/engine/metadata-modules/utils/validate-metadata-name-is-not-reserved-keyword.ts
+++ b/packages/twenty-server/src/engine/metadata-modules/utils/validate-metadata-name-is-not-reserved-keyword.ts
@@ -50,7 +50,6 @@ const reservedKeywords = [
   'links',
   'currency',
   'currencies',
-  'fullName',
   'fullNames',
   'address',
   'addresses',


Reviews:
user: login: greptile-apps[bot]
body: 
## PR Summary

This PR adds support for using full name fields as record text identifiers, allowing them to be clickable and selectable in the settings form.

- Added `FieldMetadataType.FULL_NAME` to `LABEL_IDENTIFIER_FIELD_METADATA_TYPES` array in `/packages/twenty-front/src/modules/object-metadata/constants/LabelIdentifierFieldMetadataTypes.ts`
- Removed 'fullName' from reserved keywords in `/packages/twenty-server/src/engine/metadata-modules/utils/validate-metadata-name-is-not-reserved-keyword.ts`
- Enhanced `RecordChip` component in `/packages/twenty-front/src/modules/object-record/components/RecordChip.tsx` to handle full name concatenation
- Modified `ChipFieldDisplay` and `useChipFieldDisplay` to properly handle full name fields as identifiers
- Updated `SettingsDataModelObjectIdentifiersForm` to keep identifier fields clickable even with single field

<sub>💡 (5/5) You can turn off certain types of comments like style [here](https://app.greptile.com/review/github)!</sub>

<sub>6 file(s) reviewed, 2 comment(s)</sub>
<sub>[Edit PR Review Bot Settings](https://app.greptile.com/review/github) | [Greptile](https://greptile.com?utm_source=greptile_expert&utm_medium=github&utm_campaign=code_reviews&utm_content=twenty_11610)</sub>
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T05:09:48Z
user: login: charlesBochet
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-17T09:10:05Z
user: login: charlesBochet
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-17T09:10:42Z
user: login: charlesBochet
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-17T09:11:34Z
user: login: charlesBochet
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-17T09:11:47Z
user: login: charlesBochet
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-17T09:11:55Z
user: login: charlesBochet
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-17T09:12:07Z
user: login: charlesBochet
body: Hi @b9aurav, thanks for the PR
I have left comments :)
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-04-17T09:12:29Z
user: login: b9aurav
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T11:38:58Z
user: login: b9aurav
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T11:41:49Z
user: login: b9aurav
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T11:44:13Z
user: login: charlesBochet
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-12T12:49:09Z
user: login: charlesBochet
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-12T12:50:32Z
user: login: charlesBochet
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-12T12:51:18Z
user: login: charlesBochet
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-12T12:51:37Z
user: login: charlesBochet
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-12T12:52:57Z

Review comments:
diff_hunk: @@ -70,12 +73,18 @@ export const RecordChip = ({
         })
     : undefined;
 
+  let name = recordChipData.name;
+
+  if (field?.type === FieldMetadataType.FULL_NAME) {
+    name = `${record[field.name]?.firstName || ''} ${record[field.name]?.lastName || ''}`;
path: packages/twenty-front/src/modules/object-record/components/RecordChip.tsx
user: login: greptile-apps[bot]
body: style: Consider adding .trim() to remove extra space when either firstName or lastName is empty

```suggestion
    name = `${record[field.name]?.firstName || ''} ${record[field.name]?.lastName || ''}`.trim();
```
created_at: 2025-04-17T05:09:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -47,7 +47,6 @@ const reservedKeywords = [
   'links',
   'currency',
   'currencies',
-  'fullName',
   'fullNames',
path: packages/twenty-server/src/engine/metadata-modules/utils/validate-metadata-name-is-not-reserved-keyword.ts
user: login: greptile-apps[bot]
body: style: Consider whether 'fullNames' should also be removed for consistency since 'fullName' is now allowed
created_at: 2025-04-17T05:09:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -35,6 +37,7 @@ export const RecordChip = ({
   size,
   forceDisableClick = false,
   isLabelHidden = false,
+  field,
path: packages/twenty-front/src/modules/object-record/components/RecordChip.tsx
user: login: charlesBochet
body: we should not pass the field here, and modify useRecordChipData instead!
created_at: 2025-04-17T09:10:05Z
author_association: MEMBER
diff_hunk: @@ -70,12 +73,18 @@ export const RecordChip = ({
         })
     : undefined;
 
+  let name = recordChipData.name;
+
+  if (field?.type === FieldMetadataType.FULL_NAME) {
path: packages/twenty-front/src/modules/object-record/components/RecordChip.tsx
user: login: charlesBochet
body: see my comment above: this should be handled in useRecordChipData
created_at: 2025-04-17T09:10:42Z
author_association: MEMBER
diff_hunk: @@ -47,7 +47,6 @@ const reservedKeywords = [
   'links',
   'currency',
   'currencies',
-  'fullName',
path: packages/twenty-server/src/engine/metadata-modules/utils/validate-metadata-name-is-not-reserved-keyword.ts
user: login: charlesBochet
body: why?
created_at: 2025-04-17T09:11:34Z
author_association: MEMBER
diff_hunk: @@ -124,12 +128,23 @@ export const SettingsDataModelObjectIdentifiersForm = ({
           render={({ field: { onChange, value } }) => (
             <Select
               label={label}
-              disabled={!objectMetadataItem.isCustom || !options.length}
               fullWidth
               dropdownId={`${fieldName}-select`}
               emptyOption={emptyOption}
               options={options}
               value={value}
+              withSearchInput={label === t`Record label`}
path: packages/twenty-front/src/modules/settings/data-model/objects/forms/components/SettingsDataModelObjectIdentifiersForm.tsx
user: login: charlesBochet
body: I don't understand this change
created_at: 2025-04-17T09:11:47Z
author_association: MEMBER
diff_hunk: @@ -124,12 +128,23 @@ export const SettingsDataModelObjectIdentifiersForm = ({
           render={({ field: { onChange, value } }) => (
             <Select
               label={label}
-              disabled={!objectMetadataItem.isCustom || !options.length}
path: packages/twenty-front/src/modules/settings/data-model/objects/forms/components/SettingsDataModelObjectIdentifiersForm.tsx
user: login: charlesBochet
body: I don't understand this change either
created_at: 2025-04-17T09:11:55Z
author_association: MEMBER
diff_hunk: @@ -22,6 +23,10 @@ export const ChipFieldDisplay = () => {
       size={ChipSize.Small}
       to={labelIdentifierLink}
       isLabelHidden={isLabelIdentifierCompact}
+      field={{
path: packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/ChipFieldDisplay.tsx
user: login: charlesBochet
body: see comment above
created_at: 2025-04-17T09:12:07Z
author_association: MEMBER
diff_hunk: @@ -47,7 +47,6 @@ const reservedKeywords = [
   'links',
   'currency',
   'currencies',
-  'fullName',
path: packages/twenty-server/src/engine/metadata-modules/utils/validate-metadata-name-is-not-reserved-keyword.ts
user: login: b9aurav
body: Without removing it, Its not allowing to create field named 'Full Name' (As I created in demo clip above). It throws error: The name "fullName" is not available
created_at: 2025-04-17T11:38:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -124,12 +128,23 @@ export const SettingsDataModelObjectIdentifiersForm = ({
           render={({ field: { onChange, value } }) => (
             <Select
               label={label}
-              disabled={!objectMetadataItem.isCustom || !options.length}
               fullWidth
               dropdownId={`${fieldName}-select`}
               emptyOption={emptyOption}
               options={options}
               value={value}
+              withSearchInput={label === t`Record label`}
path: packages/twenty-front/src/modules/settings/data-model/objects/forms/components/SettingsDataModelObjectIdentifiersForm.tsx
user: login: b9aurav
body: In Issue, required change is only in 'Record Label' dropdown, Not in 'Record Image' dropdown.
created_at: 2025-04-17T11:41:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -124,12 +128,23 @@ export const SettingsDataModelObjectIdentifiersForm = ({
           render={({ field: { onChange, value } }) => (
             <Select
               label={label}
-              disabled={!objectMetadataItem.isCustom || !options.length}
path: packages/twenty-front/src/modules/settings/data-model/objects/forms/components/SettingsDataModelObjectIdentifiersForm.tsx
user: login: b9aurav
body: In issue, there's mentioned in Desired Behavior section: Implement this design when only one field is available as text identifier. **(Keep the field clickable, do not disable it)**
created_at: 2025-04-17T11:44:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -13,7 +13,7 @@ export const getLabelIdentifierFieldValue = (
     objectNameSingular === CoreObjectNameSingular.WorkspaceMember ||
     labelIdentifierFieldMetadataItem?.type === FieldMetadataType.FULL_NAME
   ) {
-    return `${record.name?.firstName ?? ''} ${record.name?.lastName ?? ''}`;
+    return `${record[labelIdentifierFieldMetadataItem?.name ?? '']?.firstName ?? ''} ${record[labelIdentifierFieldMetadataItem?.name ?? '']?.lastName ?? ''}`;
path: packages/twenty-front/src/modules/object-metadata/utils/getLabelIdentifierFieldValue.ts
user: login: charlesBochet
body: we should throw if we can't find the labelIdentifierFieldMetadataItem
created_at: 2025-05-12T12:49:09Z
author_association: MEMBER
diff_hunk: @@ -20,19 +22,26 @@ export const useRecordChipData = ({
     PreComputedChipGeneratorsContext,
   );
 
+  const { fieldDefinition } = useContext(FieldContext);
+  const updatedRecord = { ...record };
path: packages/twenty-front/src/modules/object-record/hooks/useRecordChipData.ts
user: login: charlesBochet
body: let's avoid creating a fake record having a "name", this will create confusion in the code. Instead, we can passe to identifierChipGenerator and generateDefaultRecordChipData what we need
created_at: 2025-05-12T12:50:32Z
author_association: MEMBER
diff_hunk: @@ -124,12 +128,23 @@ export const SettingsDataModelObjectIdentifiersForm = ({
           render={({ field: { onChange, value } }) => (
             <Select
               label={label}
-              disabled={!objectMetadataItem.isCustom || !options.length}
path: packages/twenty-front/src/modules/settings/data-model/objects/forms/components/SettingsDataModelObjectIdentifiersForm.tsx
user: login: charlesBochet
body: ok!
created_at: 2025-05-12T12:51:18Z
author_association: MEMBER
diff_hunk: @@ -124,12 +128,23 @@ export const SettingsDataModelObjectIdentifiersForm = ({
           render={({ field: { onChange, value } }) => (
             <Select
               label={label}
-              disabled={!objectMetadataItem.isCustom || !options.length}
path: packages/twenty-front/src/modules/settings/data-model/objects/forms/components/SettingsDataModelObjectIdentifiersForm.tsx
user: login: charlesBochet
body: but we should still disable it for standard object as we don't let the user change the labelIdentifier
created_at: 2025-05-12T12:51:36Z
author_association: MEMBER
diff_hunk: @@ -47,7 +47,6 @@ const reservedKeywords = [
   'links',
   'currency',
   'currencies',
-  'fullName',
path: packages/twenty-server/src/engine/metadata-modules/utils/validate-metadata-name-is-not-reserved-keyword.ts
user: login: charlesBochet
body: this is on purpose as this could conflict with fieldType, let's keep it
created_at: 2025-05-12T12:52:57Z
author_association: MEMBER

PR comments:
user: login: b9aurav
created_at: 2025-04-17T11:47:01Z
body: @charlesBochet Thanks for your comments, I'll take a look at requested changes.
author_association: CONTRIBUTOR
user: login: charlesBochet
created_at: 2025-05-19T22:02:56Z
body: Thank you @b9aurav, I've made some small modifications to remove previous technical debt
author_association: MEMBER
user: login: github-actions[bot]
created_at: 2025-05-19T22:03:22Z
body: <!-- PR_PREVIEW_ENV -->
🚀 **Preview Environment Ready!**

Your preview environment is available at: http://bore.pub:31034

This environment will automatically shut down when the PR is closed or after 5 hours.
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-05-19T22:04:30Z
body: Thanks @b9aurav for your contribution!
This marks your **10th** PR on the repo. You're **top 4%** of all our contributors 🎉
[See contributor page](https://twenty.com/contributors/b9aurav) - [Share on LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https://twenty.com/contributors/b9aurav) - [Share on Twitter](https://www.twitter.com/share?url=https://twenty.com/contributors/b9aurav)

![Contributions](https://twenty.com/api/contributors/b9aurav/og.png)
author_association: CONTRIBUTOR