[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:
@ -8,9 +8,8 @@ export type ChipGeneratorPerObjectNameSingularPerFieldName = Record<
|
|||||||
Record<string, (record: ObjectRecord) => RecordChipData>
|
Record<string, (record: ObjectRecord) => RecordChipData>
|
||||||
>;
|
>;
|
||||||
|
|
||||||
export type IdentifierChipGeneratorPerObject = Record<
|
export type IdentifierChipGeneratorPerObject = Partial<
|
||||||
string,
|
Record<string, (record: ObjectRecord) => RecordChipData>
|
||||||
(record: ObjectRecord) => RecordChipData
|
|
||||||
>;
|
>;
|
||||||
|
|
||||||
export type PreComputedChipGeneratorsContextProps = {
|
export type PreComputedChipGeneratorsContextProps = {
|
||||||
|
|||||||
@ -1,15 +1,25 @@
|
|||||||
|
import { RecordChipData } from '@/object-record/record-field/types/RecordChipData';
|
||||||
import { isFieldFullNameValue } from '@/object-record/record-field/types/guards/isFieldFullNameValue';
|
import { isFieldFullNameValue } from '@/object-record/record-field/types/guards/isFieldFullNameValue';
|
||||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
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)
|
const name = isFieldFullNameValue(record.name)
|
||||||
? record.name.firstName + ' ' + record.name.lastName
|
? `${record.name.firstName} ${record.name.lastName}`
|
||||||
: (record.name ?? '');
|
: (record.name ?? '');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name,
|
|
||||||
avatarUrl: name,
|
|
||||||
avatarType: 'rounded',
|
avatarType: 'rounded',
|
||||||
linkToShowPage: false,
|
avatarUrl: name,
|
||||||
|
isLabelIdentifier: false,
|
||||||
|
name,
|
||||||
|
objectNameSingular,
|
||||||
|
recordId: record.id,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,24 +1,37 @@
|
|||||||
import { PreComputedChipGeneratorsContext } from '@/object-metadata/contexts/PreComputedChipGeneratorsContext';
|
import { PreComputedChipGeneratorsContext } from '@/object-metadata/contexts/PreComputedChipGeneratorsContext';
|
||||||
import { generateDefaultRecordChipData } from '@/object-metadata/utils/generateDefaultRecordChipData';
|
import { generateDefaultRecordChipData } from '@/object-metadata/utils/generateDefaultRecordChipData';
|
||||||
|
import { RecordChipData } from '@/object-record/record-field/types/RecordChipData';
|
||||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||||
import { useContext } from 'react';
|
import { useContext } from 'react';
|
||||||
|
import { isDefined } from 'twenty-shared';
|
||||||
|
|
||||||
|
type UseRecordChipDataArgs = {
|
||||||
|
objectNameSingular: string;
|
||||||
|
record: ObjectRecord;
|
||||||
|
};
|
||||||
|
type UseRecordChipDataReturnType = {
|
||||||
|
recordChipData: RecordChipData;
|
||||||
|
};
|
||||||
export const useRecordChipData = ({
|
export const useRecordChipData = ({
|
||||||
objectNameSingular,
|
objectNameSingular,
|
||||||
record,
|
record,
|
||||||
}: {
|
}: UseRecordChipDataArgs): UseRecordChipDataReturnType => {
|
||||||
objectNameSingular: string;
|
|
||||||
record: ObjectRecord;
|
|
||||||
}) => {
|
|
||||||
const { identifierChipGeneratorPerObject } = useContext(
|
const { identifierChipGeneratorPerObject } = useContext(
|
||||||
PreComputedChipGeneratorsContext,
|
PreComputedChipGeneratorsContext,
|
||||||
);
|
);
|
||||||
|
|
||||||
const generateRecordChipData =
|
const identifierChipGenerator =
|
||||||
identifierChipGeneratorPerObject[objectNameSingular] ??
|
identifierChipGeneratorPerObject[objectNameSingular];
|
||||||
generateDefaultRecordChipData;
|
if (isDefined(identifierChipGenerator)) {
|
||||||
|
return {
|
||||||
|
recordChipData: identifierChipGenerator(record),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const recordChipData = generateRecordChipData(record);
|
return {
|
||||||
|
recordChipData: generateDefaultRecordChipData({
|
||||||
return { recordChipData };
|
objectNameSingular,
|
||||||
|
record,
|
||||||
|
}),
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import { RecordChip } from '@/object-record/components/RecordChip';
|
|||||||
import { useFieldFocus } from '@/object-record/record-field/hooks/useFieldFocus';
|
import { useFieldFocus } from '@/object-record/record-field/hooks/useFieldFocus';
|
||||||
import { useRelationFromManyFieldDisplay } from '@/object-record/record-field/meta-types/hooks/useRelationFromManyFieldDisplay';
|
import { useRelationFromManyFieldDisplay } from '@/object-record/record-field/meta-types/hooks/useRelationFromManyFieldDisplay';
|
||||||
import { ExpandableList } from '@/ui/layout/expandable-list/components/ExpandableList';
|
import { ExpandableList } from '@/ui/layout/expandable-list/components/ExpandableList';
|
||||||
import { isNull } from '@sniptt/guards';
|
import { isDefined } from 'twenty-shared';
|
||||||
|
|
||||||
export const RelationFromManyFieldDisplay = () => {
|
export const RelationFromManyFieldDisplay = () => {
|
||||||
const { fieldValue, fieldDefinition } = useRelationFromManyFieldDisplay();
|
const { fieldValue, fieldDefinition } = useRelationFromManyFieldDisplay();
|
||||||
@ -48,43 +48,37 @@ export const RelationFromManyFieldDisplay = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ExpandableList isChipCountDisplayed={isFocused}>
|
<ExpandableList isChipCountDisplayed={isFocused}>
|
||||||
{fieldValue
|
{fieldValue.filter(isDefined).map((record) => (
|
||||||
.filter((record) => !isNull(record[relationFieldName]))
|
<RecordChip
|
||||||
.map((record) => (
|
key={record.id}
|
||||||
<RecordChip
|
objectNameSingular={objectNameSingular}
|
||||||
key={record.id}
|
record={record[relationFieldName]}
|
||||||
objectNameSingular={objectNameSingular}
|
/>
|
||||||
record={record[relationFieldName]}
|
))}
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</ExpandableList>
|
</ExpandableList>
|
||||||
);
|
);
|
||||||
} else if (isRelationFromActivityTargets) {
|
} else if (isRelationFromActivityTargets) {
|
||||||
return (
|
return (
|
||||||
<ExpandableList isChipCountDisplayed={isFocused}>
|
<ExpandableList isChipCountDisplayed={isFocused}>
|
||||||
{activityTargetObjectRecords
|
{activityTargetObjectRecords.filter(isDefined).map((record) => (
|
||||||
.filter((record) => !isNull(record.targetObject))
|
<RecordChip
|
||||||
.map((record) => (
|
key={record.targetObject.id}
|
||||||
<RecordChip
|
objectNameSingular={record.targetObjectMetadataItem.nameSingular}
|
||||||
key={record.targetObject.id}
|
record={record.targetObject}
|
||||||
objectNameSingular={record.targetObjectMetadataItem.nameSingular}
|
/>
|
||||||
record={record.targetObject}
|
))}
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</ExpandableList>
|
</ExpandableList>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<ExpandableList isChipCountDisplayed={isFocused}>
|
<ExpandableList isChipCountDisplayed={isFocused}>
|
||||||
{fieldValue
|
{fieldValue.filter(isDefined).map((record) => (
|
||||||
.filter((record) => !isNull(record))
|
<RecordChip
|
||||||
.map((record) => (
|
key={record.id}
|
||||||
<RecordChip
|
objectNameSingular={objectNameSingular}
|
||||||
key={record.id}
|
record={record}
|
||||||
objectNameSingular={objectNameSingular}
|
/>
|
||||||
record={record}
|
))}
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</ExpandableList>
|
</ExpandableList>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -44,14 +44,26 @@ export const useRelationFromManyFieldDisplay = () => {
|
|||||||
? maxWidth - FIELD_EDIT_BUTTON_WIDTH
|
? maxWidth - FIELD_EDIT_BUTTON_WIDTH
|
||||||
: maxWidth;
|
: 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');
|
throw new Error('Object metadata name singular is not a non-empty string');
|
||||||
}
|
}
|
||||||
|
|
||||||
const generateRecordChipData =
|
const fieldChipGenerator =
|
||||||
chipGeneratorPerObjectPerField[
|
chipGeneratorPerObjectPerField[
|
||||||
fieldDefinition.metadata.objectMetadataNameSingular
|
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 {
|
return {
|
||||||
fieldDefinition,
|
fieldDefinition,
|
||||||
|
|||||||
@ -2,13 +2,13 @@ import { isNonEmptyString } from '@sniptt/guards';
|
|||||||
import { useContext } from 'react';
|
import { useContext } from 'react';
|
||||||
|
|
||||||
import { PreComputedChipGeneratorsContext } from '@/object-metadata/contexts/PreComputedChipGeneratorsContext';
|
import { PreComputedChipGeneratorsContext } from '@/object-metadata/contexts/PreComputedChipGeneratorsContext';
|
||||||
import { generateDefaultRecordChipData } from '@/object-metadata/utils/generateDefaultRecordChipData';
|
|
||||||
import { useRecordFieldValue } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
|
import { useRecordFieldValue } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
|
||||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||||
import { FIELD_EDIT_BUTTON_WIDTH } from '@/ui/field/display/constants/FieldEditButtonWidth';
|
import { FIELD_EDIT_BUTTON_WIDTH } from '@/ui/field/display/constants/FieldEditButtonWidth';
|
||||||
import { isDefined } from 'twenty-shared';
|
import { isDefined } from 'twenty-shared';
|
||||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||||
|
|
||||||
|
import { generateDefaultRecordChipData } from '@/object-metadata/utils/generateDefaultRecordChipData';
|
||||||
import { FieldContext } from '../../contexts/FieldContext';
|
import { FieldContext } from '../../contexts/FieldContext';
|
||||||
import { assertFieldMetadata } from '../../types/guards/assertFieldMetadata';
|
import { assertFieldMetadata } from '../../types/guards/assertFieldMetadata';
|
||||||
import { isFieldRelation } from '../../types/guards/isFieldRelation';
|
import { isFieldRelation } from '../../types/guards/isFieldRelation';
|
||||||
@ -44,14 +44,26 @@ export const useRelationToOneFieldDisplay = () => {
|
|||||||
? maxWidth - FIELD_EDIT_BUTTON_WIDTH
|
? maxWidth - FIELD_EDIT_BUTTON_WIDTH
|
||||||
: maxWidth;
|
: 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');
|
throw new Error('Object metadata name singular is not a non-empty string');
|
||||||
}
|
}
|
||||||
|
|
||||||
const generateRecordChipData =
|
const fieldChipGenerator =
|
||||||
chipGeneratorPerObjectPerField[
|
chipGeneratorPerObjectPerField[
|
||||||
fieldDefinition.metadata.objectMetadataNameSingular
|
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 {
|
return {
|
||||||
fieldDefinition,
|
fieldDefinition,
|
||||||
|
|||||||
Reference in New Issue
Block a user