[BUGFIX] GenerateDefaultRecordChipData returns RecordChipData (#11071)

# Introduction
closes https://github.com/twentyhq/twenty/issues/11030, might not fix
the issue as it seems to be related to the passed record to
`identifierChipGeneratorPerObject` about to dig deeper in this generator
code => found nothing revelant doubled check each `RecordChip`
invocation that could provide undefined record
Fixed wrong default generated `RecordChipData` signature

## Reproducibility
I've only been able to reproduce the bug in production using the very
same opportunity than within the issue, but not all the time

https://crm.twenty-internal.com/objects/opportunities?viewId=b709d3d1-2dd2-455d-ba73-784f3ab00883
```json
// Removed timelineActivities to prevent linking ids
{
  "data": {
    "opportunity": {
      "__typename": "Opportunity",
      "closeDate": null,
      "company": null,
      "companyId": null,
      "createdAt": "2024-06-17T09:45:22.357Z",
      "deletedAt": null,
      "id": "006a22dd-6bd6-4247-a24b-42fb164cd48c",
      "name": "test",
      "pointOfContact": null,
      "pointOfContactId": null,
      "position": 0,
      "probability": "0",
      "stage": "NEW_STAGE",
      "updatedAt": "2025-03-20T16:27:51.927Z",
      "amount": {
        "__typename": "Currency",
        "amountMicros": null,
        "currencyCode": "USD"
      },
      "attachments": {
        "__typename": "AttachmentConnection",
        "edges": []
      },
      "createdBy": {
        "__typename": "Actor",
        "source": "MANUAL",
        "workspaceMemberId": null,
        "name": "",
        "context": {}
      },
      "favorites": {
        "__typename": "FavoriteConnection",
        "edges": []
      },
      "taskTargets": {
        "__typename": "TaskTargetConnection",
        "edges": []
      },
      "noteTargets": {
        "__typename": "NoteTargetConnection",
        "edges": [
          {
            "__typename": "NoteTargetEdge",
            "node": {
              "__typename": "NoteTarget",
              "appEventId": null,
              "companyId": null,
              "createdAt": "2025-01-22T17:11:07.801Z",
              "deletedAt": null,
              "feedbackId": null,
              "id": "2e8eca1c-e2c2-425a-93fc-ef2aeb65f410",
              "issueId": null,
              "listingId": null,
              "noteId": "ab586b51-6931-4a4a-9c24-0d16226211b2",
              "opportunityId": "006a22dd-6bd6-4247-a24b-42fb164cd48c",
              "personId": null,
              "somethingId": null,
              "testId": null,
              "updatedAt": "2025-01-22T17:11:07.801Z"
            }
          }
        ]
      },
    }
  }
}
```
This commit is contained in:
Paul Rastoin
2025-03-20 18:38:19 +01:00
committed by GitHub
parent 95014b0ac5
commit 872c0e97f6
6 changed files with 93 additions and 53 deletions

View File

@ -8,9 +8,8 @@ export type ChipGeneratorPerObjectNameSingularPerFieldName = Record<
Record<string, (record: ObjectRecord) => RecordChipData>
>;
export type IdentifierChipGeneratorPerObject = Record<
string,
(record: ObjectRecord) => RecordChipData
export type IdentifierChipGeneratorPerObject = Partial<
Record<string, (record: ObjectRecord) => RecordChipData>
>;
export type PreComputedChipGeneratorsContextProps = {

View File

@ -1,15 +1,25 @@
import { RecordChipData } from '@/object-record/record-field/types/RecordChipData';
import { isFieldFullNameValue } from '@/object-record/record-field/types/guards/isFieldFullNameValue';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
export const generateDefaultRecordChipData = (record: ObjectRecord) => {
type GenerateDefaultRecordChipDataArgs = {
record: ObjectRecord;
objectNameSingular: string;
};
export const generateDefaultRecordChipData = ({
objectNameSingular,
record,
}: GenerateDefaultRecordChipDataArgs): RecordChipData => {
const name = isFieldFullNameValue(record.name)
? record.name.firstName + ' ' + record.name.lastName
? `${record.name.firstName} ${record.name.lastName}`
: (record.name ?? '');
return {
name,
avatarUrl: name,
avatarType: 'rounded',
linkToShowPage: false,
avatarUrl: name,
isLabelIdentifier: false,
name,
objectNameSingular,
recordId: record.id,
};
};

View File

@ -1,24 +1,37 @@
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';
import { isDefined } from 'twenty-shared';
type UseRecordChipDataArgs = {
objectNameSingular: string;
record: ObjectRecord;
};
type UseRecordChipDataReturnType = {
recordChipData: RecordChipData;
};
export const useRecordChipData = ({
objectNameSingular,
record,
}: {
objectNameSingular: string;
record: ObjectRecord;
}) => {
}: UseRecordChipDataArgs): UseRecordChipDataReturnType => {
const { identifierChipGeneratorPerObject } = useContext(
PreComputedChipGeneratorsContext,
);
const generateRecordChipData =
identifierChipGeneratorPerObject[objectNameSingular] ??
generateDefaultRecordChipData;
const identifierChipGenerator =
identifierChipGeneratorPerObject[objectNameSingular];
if (isDefined(identifierChipGenerator)) {
return {
recordChipData: identifierChipGenerator(record),
};
}
const recordChipData = generateRecordChipData(record);
return { recordChipData };
return {
recordChipData: generateDefaultRecordChipData({
objectNameSingular,
record,
}),
};
};

View File

@ -6,7 +6,7 @@ import { RecordChip } from '@/object-record/components/RecordChip';
import { useFieldFocus } from '@/object-record/record-field/hooks/useFieldFocus';
import { useRelationFromManyFieldDisplay } from '@/object-record/record-field/meta-types/hooks/useRelationFromManyFieldDisplay';
import { ExpandableList } from '@/ui/layout/expandable-list/components/ExpandableList';
import { isNull } from '@sniptt/guards';
import { isDefined } from 'twenty-shared';
export const RelationFromManyFieldDisplay = () => {
const { fieldValue, fieldDefinition } = useRelationFromManyFieldDisplay();
@ -48,43 +48,37 @@ export const RelationFromManyFieldDisplay = () => {
return (
<ExpandableList isChipCountDisplayed={isFocused}>
{fieldValue
.filter((record) => !isNull(record[relationFieldName]))
.map((record) => (
<RecordChip
key={record.id}
objectNameSingular={objectNameSingular}
record={record[relationFieldName]}
/>
))}
{fieldValue.filter(isDefined).map((record) => (
<RecordChip
key={record.id}
objectNameSingular={objectNameSingular}
record={record[relationFieldName]}
/>
))}
</ExpandableList>
);
} else if (isRelationFromActivityTargets) {
return (
<ExpandableList isChipCountDisplayed={isFocused}>
{activityTargetObjectRecords
.filter((record) => !isNull(record.targetObject))
.map((record) => (
<RecordChip
key={record.targetObject.id}
objectNameSingular={record.targetObjectMetadataItem.nameSingular}
record={record.targetObject}
/>
))}
{activityTargetObjectRecords.filter(isDefined).map((record) => (
<RecordChip
key={record.targetObject.id}
objectNameSingular={record.targetObjectMetadataItem.nameSingular}
record={record.targetObject}
/>
))}
</ExpandableList>
);
} else {
return (
<ExpandableList isChipCountDisplayed={isFocused}>
{fieldValue
.filter((record) => !isNull(record))
.map((record) => (
<RecordChip
key={record.id}
objectNameSingular={objectNameSingular}
record={record}
/>
))}
{fieldValue.filter(isDefined).map((record) => (
<RecordChip
key={record.id}
objectNameSingular={objectNameSingular}
record={record}
/>
))}
</ExpandableList>
);
}

View File

@ -44,14 +44,26 @@ export const useRelationFromManyFieldDisplay = () => {
? maxWidth - FIELD_EDIT_BUTTON_WIDTH
: maxWidth;
if (!isNonEmptyString(fieldDefinition.metadata.objectMetadataNameSingular)) {
if (
!isDefined(fieldDefinition.metadata.objectMetadataNameSingular) ||
!isNonEmptyString(fieldDefinition.metadata.objectMetadataNameSingular)
) {
throw new Error('Object metadata name singular is not a non-empty string');
}
const generateRecordChipData =
const fieldChipGenerator =
chipGeneratorPerObjectPerField[
fieldDefinition.metadata.objectMetadataNameSingular
]?.[fieldDefinition.metadata.fieldName] ?? generateDefaultRecordChipData;
]?.[fieldDefinition.metadata.fieldName];
const generateRecordChipData = isDefined(fieldChipGenerator)
? fieldChipGenerator
: (record: ObjectRecord) =>
generateDefaultRecordChipData({
record,
// @ts-expect-error Above assertions does not infer that fieldDefinition.metadata.objectMetadataNameSingular always defined
objectNameSingular:
fieldDefinition.metadata.objectMetadataNameSingular,
});
return {
fieldDefinition,

View File

@ -2,13 +2,13 @@ import { isNonEmptyString } from '@sniptt/guards';
import { useContext } from 'react';
import { PreComputedChipGeneratorsContext } from '@/object-metadata/contexts/PreComputedChipGeneratorsContext';
import { generateDefaultRecordChipData } from '@/object-metadata/utils/generateDefaultRecordChipData';
import { useRecordFieldValue } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { FIELD_EDIT_BUTTON_WIDTH } from '@/ui/field/display/constants/FieldEditButtonWidth';
import { isDefined } from 'twenty-shared';
import { FieldMetadataType } from '~/generated-metadata/graphql';
import { generateDefaultRecordChipData } from '@/object-metadata/utils/generateDefaultRecordChipData';
import { FieldContext } from '../../contexts/FieldContext';
import { assertFieldMetadata } from '../../types/guards/assertFieldMetadata';
import { isFieldRelation } from '../../types/guards/isFieldRelation';
@ -44,14 +44,26 @@ export const useRelationToOneFieldDisplay = () => {
? maxWidth - FIELD_EDIT_BUTTON_WIDTH
: maxWidth;
if (!isNonEmptyString(fieldDefinition.metadata.objectMetadataNameSingular)) {
if (
!isDefined(fieldDefinition.metadata.objectMetadataNameSingular) ||
!isNonEmptyString(fieldDefinition.metadata.objectMetadataNameSingular)
) {
throw new Error('Object metadata name singular is not a non-empty string');
}
const generateRecordChipData =
const fieldChipGenerator =
chipGeneratorPerObjectPerField[
fieldDefinition.metadata.objectMetadataNameSingular
]?.[fieldDefinition.metadata.fieldName] ?? generateDefaultRecordChipData;
]?.[fieldDefinition.metadata.fieldName];
const generateRecordChipData = isDefined(fieldChipGenerator)
? fieldChipGenerator
: (record: ObjectRecord) =>
generateDefaultRecordChipData({
record,
// @ts-expect-error Above assertions does not infer that fieldDefinition.metadata.objectMetadataNameSingular always defined
objectNameSingular:
fieldDefinition.metadata.objectMetadataNameSingular,
});
return {
fieldDefinition,