morph Board mode (#15249)

Here's a concise PR description for your changes:

---

## Fix morph relation fields showing placeholder in kanban view

**Problem:** Morph relation fields always displayed placeholders in
kanban view even when records were selected, because
`useRecordFieldValue` was checking the base field name (e.g.,
`"favorite"`) instead of the computed morph field names (e.g.,
`"favoriteCompany"`).

**Solution:** Modified `useRecordFieldValue` to accept an optional
`fieldDefinition` parameter and added special handling for morph
relations that uses
`recordStoreMorphManyToOneValueWithObjectNameFamilySelector` and
`recordStoreMorphOneToManyValueWithObjectNameFamilySelector` to
correctly retrieve values. Updated `useIsFieldEmpty` to pass the field
definition through, enabling proper empty state detection for morph
relation fields.

**Impact:** Morph relation fields now correctly display their values in
kanban board cards instead of showing placeholders.

Fixes https://github.com/twentyhq/core-team-issues/issues/1323

<img width="681" height="420" alt="Screenshot 2025-10-22 at 14 11 07"
src="https://github.com/user-attachments/assets/de357379-ebff-42c6-bb93-1f0c43ce086d"
/>
This commit is contained in:
Guillim
2025-10-24 13:54:21 +02:00
committed by GitHub
parent 721e180fd0
commit b46dc44a95
32 changed files with 247 additions and 117 deletions
@@ -4,11 +4,12 @@ import {
FieldActorMetadata,
FieldFullNameMetadata,
FieldLinksMetadata,
FieldMorphRelationMetadata,
FieldRatingMetadata,
FieldSelectMetadata,
FieldTextMetadata,
} from '@/object-record/record-field/ui/types/FieldMetadata';
import { FieldMetadataType } from '~/generated-metadata/graphql';
import { FieldMetadataType, RelationType } from '~/generated-metadata/graphql';
import { generatedMockObjectMetadataItems } from '~/testing/utils/generatedMockObjectMetadataItems';
export const fieldMetadataId = 'fieldMetadataId';
@@ -129,3 +130,19 @@ export const linksFieldDefinition: FieldDefinition<FieldLinksMetadata> = {
settings: null,
},
};
export const morphRelationFieldDefinition: FieldDefinition<FieldMorphRelationMetadata> =
{
fieldMetadataId,
label: 'Attachments',
iconName: 'IconLink',
type: FieldMetadataType.MORPH_RELATION,
defaultValue: [],
metadata: {
fieldName: 'attachments',
objectMetadataNameSingular: 'company',
morphRelations: [],
relationType: RelationType.ONE_TO_MANY,
settings: null,
},
};
@@ -2,7 +2,7 @@ import { useContext } from 'react';
import { isFieldValueEmpty } from '@/object-record/record-field/ui/utils/isFieldValueEmpty';
import { useRecordFieldValue } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
import { useRecordFieldValue } from '@/object-record/record-store/hooks/useRecordFieldValue';
import { isDefined } from 'twenty-shared/utils';
import { FieldContext } from '../contexts/FieldContext';
@@ -13,6 +13,7 @@ export const useIsFieldEmpty = () => {
const fieldValue = useRecordFieldValue(
recordId,
fieldDefinition?.metadata?.fieldName ?? '',
fieldDefinition,
);
if (isDefined(overridenIsFieldEmpty)) {
@@ -14,7 +14,15 @@ export const MorphRelationOneToManyFieldDisplay = () => {
const { isFocused } = useFieldFocus();
const { disableChipClick, triggerEvent } = useContext(FieldContext);
if (!morphValuesWithObjectNameSingular) {
if (!isDefined(morphValuesWithObjectNameSingular)) {
return null;
}
const areMorphValuesWithObjectNameSingularEmpty =
morphValuesWithObjectNameSingular.every(
(morphValueWithObjectNameSingular) =>
morphValueWithObjectNameSingular.value.length === 0,
);
if (areMorphValuesWithObjectNameSingularEmpty) {
return null;
}
@@ -1,9 +1,9 @@
import { useContext } from 'react';
import { type FieldActorValue } from '@/object-record/record-field/ui/types/FieldMetadata';
import { useRecordFieldValue } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
import { AuthContext } from '@/auth/contexts/AuthContext';
import { useRecordFieldValue } from '@/object-record/record-store/hooks/useRecordFieldValue';
import { isDefined } from 'twenty-shared/utils';
import { type WorkspaceMember } from '~/generated-metadata/graphql';
import { FieldContext } from '../../contexts/FieldContext';
@@ -24,6 +24,7 @@ export const useActorFieldDisplay = (): ActorFieldDisplayValue | undefined => {
const fieldValue = useRecordFieldValue<FieldActorValue | undefined>(
recordId,
fieldName,
fieldDefinition,
);
if (!isDefined(fieldValue)) {
return undefined;
@@ -1,7 +1,6 @@
import { useContext } from 'react';
import { useRecordFieldValue } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
import { useRecordFieldValue } from '@/object-record/record-store/hooks/useRecordFieldValue';
import { FieldContext } from '../../contexts/FieldContext';
import { type FieldAddressValue } from '../../types/FieldMetadata';
@@ -13,6 +12,7 @@ export const useAddressFieldDisplay = () => {
const fieldValue = useRecordFieldValue<FieldAddressValue | undefined>(
recordId,
fieldName,
fieldDefinition,
);
return {
@@ -4,7 +4,8 @@ import {
type FieldArrayMetadata,
type FieldArrayValue,
} from '@/object-record/record-field/ui/types/FieldMetadata';
import { useRecordFieldValue } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
import { useRecordFieldValue } from '@/object-record/record-store/hooks/useRecordFieldValue';
import { useContext } from 'react';
export const useArrayFieldDisplay = () => {
@@ -15,6 +16,7 @@ export const useArrayFieldDisplay = () => {
const fieldValue = useRecordFieldValue<FieldArrayValue | undefined>(
recordId,
fieldName,
fieldDefinition,
);
return {
@@ -1,7 +1,6 @@
import { useContext } from 'react';
import { useRecordFieldValue } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
import { useRecordFieldValue } from '@/object-record/record-store/hooks/useRecordFieldValue';
import { FieldContext } from '../../contexts/FieldContext';
export const useBooleanFieldDisplay = () => {
@@ -12,6 +11,7 @@ export const useBooleanFieldDisplay = () => {
const fieldValue = useRecordFieldValue<boolean | undefined>(
recordId,
fieldName,
fieldDefinition,
);
return {
@@ -1,9 +1,8 @@
import { useContext } from 'react';
import { useRecordFieldValue } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
import { assertFieldMetadata } from '@/object-record/record-field/ui/types/guards/assertFieldMetadata';
import { isFieldCurrency } from '@/object-record/record-field/ui/types/guards/isFieldCurrency';
import { useRecordFieldValue } from '@/object-record/record-store/hooks/useRecordFieldValue';
import { FieldMetadataType } from 'twenty-shared/types';
import { FieldContext } from '../../contexts/FieldContext';
import { type FieldCurrencyValue } from '../../types/FieldMetadata';
@@ -22,6 +21,7 @@ export const useCurrencyFieldDisplay = () => {
const fieldValue = useRecordFieldValue<FieldCurrencyValue | undefined>(
recordId,
fieldName,
fieldDefinition,
);
return {
@@ -1,9 +1,8 @@
import { useContext } from 'react';
import { useRecordFieldValue } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
import { type FieldDefinition } from '@/object-record/record-field/ui/types/FieldDefinition';
import { type FieldDateMetadata } from '@/object-record/record-field/ui/types/FieldMetadata';
import { useRecordFieldValue } from '@/object-record/record-store/hooks/useRecordFieldValue';
import { FieldContext } from '../../contexts/FieldContext';
export const useDateFieldDisplay = () => {
@@ -14,6 +13,7 @@ export const useDateFieldDisplay = () => {
const fieldValue = useRecordFieldValue<string | undefined>(
recordId,
fieldName,
fieldDefinition,
);
return {
@@ -1,9 +1,8 @@
import { useContext } from 'react';
import { useRecordFieldValue } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
import { type FieldDefinition } from '@/object-record/record-field/ui/types/FieldDefinition';
import { type FieldDateTimeMetadata } from '@/object-record/record-field/ui/types/FieldMetadata';
import { useRecordFieldValue } from '@/object-record/record-store/hooks/useRecordFieldValue';
import { FieldContext } from '../../contexts/FieldContext';
export const useDateTimeFieldDisplay = () => {
@@ -14,6 +13,7 @@ export const useDateTimeFieldDisplay = () => {
const fieldValue = useRecordFieldValue<string | undefined>(
recordId,
fieldName,
fieldDefinition,
);
return {
@@ -1,8 +1,7 @@
import { useContext } from 'react';
import { useRecordFieldValue } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
import { type FieldEmailsValue } from '@/object-record/record-field/ui/types/FieldMetadata';
import { useRecordFieldValue } from '@/object-record/record-store/hooks/useRecordFieldValue';
import { FieldContext } from '../../contexts/FieldContext';
export const useEmailsFieldDisplay = () => {
@@ -13,6 +12,7 @@ export const useEmailsFieldDisplay = () => {
const fieldValue = useRecordFieldValue<FieldEmailsValue | undefined>(
recordId,
fieldName,
fieldDefinition,
);
return {
@@ -1,8 +1,8 @@
import { useContext } from 'react';
import { type FieldFullNameValue } from '@/object-record/record-field/ui/types/FieldMetadata';
import { useRecordFieldValue } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
import { useRecordFieldValue } from '@/object-record/record-store/hooks/useRecordFieldValue';
import { FieldContext } from '../../contexts/FieldContext';
export const useFullNameFieldDisplay = () => {
@@ -13,6 +13,7 @@ export const useFullNameFieldDisplay = () => {
const fieldValue = useRecordFieldValue<FieldFullNameValue | undefined>(
recordId,
fieldName,
fieldDefinition,
);
return {
@@ -1,9 +1,9 @@
import { useContext } from 'react';
import { type FieldJsonValue } from '@/object-record/record-field/ui/types/FieldMetadata';
import { useRecordFieldValue } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
import { useFormattedJsonFieldValue } from '@/object-record/record-field/ui/meta-types/hooks/useFormattedJsonFieldValue';
import { useRecordFieldValue } from '@/object-record/record-store/hooks/useRecordFieldValue';
import { FieldContext } from '../../contexts/FieldContext';
export const useJsonFieldDisplay = () => {
@@ -14,6 +14,7 @@ export const useJsonFieldDisplay = () => {
const fieldValue = useRecordFieldValue<FieldJsonValue | undefined>(
recordId,
fieldName,
fieldDefinition,
);
const formattedFieldValue = useFormattedJsonFieldValue({
@@ -1,8 +1,8 @@
import { useContext } from 'react';
import { type FieldLinksValue } from '@/object-record/record-field/ui/types/FieldMetadata';
import { useRecordFieldValue } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
import { useRecordFieldValue } from '@/object-record/record-store/hooks/useRecordFieldValue';
import { FieldContext } from '../../contexts/FieldContext';
export const useLinksFieldDisplay = () => {
@@ -13,6 +13,7 @@ export const useLinksFieldDisplay = () => {
const fieldValue = useRecordFieldValue<FieldLinksValue | undefined>(
recordId,
fieldName,
fieldDefinition,
);
return {
@@ -7,8 +7,8 @@ import { FieldMetadataType } from '~/generated-metadata/graphql';
import { isFieldMorphRelation } from '@/object-record/record-field/ui/types/guards/isFieldMorphRelation';
import { recordStoreMorphOneToManyValueWithObjectNameFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreMorphOneToManyValueWithObjectNameFamilySelector';
import { useRecoilValue } from 'recoil';
import { useRecordFieldValue } from '@/object-record/record-store/hooks/useRecordFieldValue';
import { type ObjectRecord } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { FieldContext } from '../../contexts/FieldContext';
import { assertFieldMetadata } from '../../types/guards/assertFieldMetadata';
@@ -32,12 +32,12 @@ export const useMorphRelationFromManyFieldDisplay = () => {
const button = fieldDefinition.editButtonIcon;
const morphValuesWithObjectNameSingular = useRecoilValue(
recordStoreMorphOneToManyValueWithObjectNameFamilySelector({
recordId,
morphRelations: fieldDefinition.metadata.morphRelations,
}),
);
const morphValuesWithObjectNameSingular = useRecordFieldValue<
{
objectNameSingular: string;
value: ObjectRecord;
}[]
>(recordId, fieldDefinition.metadata.fieldName, fieldDefinition);
const maxWidthForField =
isDefined(button) && isDefined(maxWidth)
@@ -11,8 +11,9 @@ import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldCont
import { assertFieldMetadata } from '@/object-record/record-field/ui/types/guards/assertFieldMetadata';
import { isFieldMorphRelation } from '@/object-record/record-field/ui/types/guards/isFieldMorphRelation';
import { recordStoreMorphManyToOneValueWithObjectNameFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreMorphManyToOneValueWithObjectNameFamilySelector';
import { useRecoilValue } from 'recoil';
import { useRecordFieldValue } from '@/object-record/record-store/hooks/useRecordFieldValue';
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
import { isDefined } from 'twenty-shared/utils';
export const useMorphRelationToOneFieldDisplay = () => {
@@ -33,12 +34,10 @@ export const useMorphRelationToOneFieldDisplay = () => {
const button = fieldDefinition.editButtonIcon;
const morphFieldValueWithObjectName = useRecoilValue(
recordStoreMorphManyToOneValueWithObjectNameFamilySelector({
recordId,
morphRelations: fieldDefinition.metadata.morphRelations,
}),
);
const morphFieldValueWithObjectName = useRecordFieldValue<{
objectNameSingular: string;
value: ObjectRecord;
}>(recordId, fieldDefinition.metadata.fieldName, fieldDefinition);
if (!isDefined(morphFieldValueWithObjectName)) {
return {
@@ -6,7 +6,7 @@ import {
type FieldMultiSelectMetadata,
type FieldMultiSelectValue,
} from '@/object-record/record-field/ui/types/FieldMetadata';
import { useRecordFieldValue } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
import { useRecordFieldValue } from '@/object-record/record-store/hooks/useRecordFieldValue';
export const useMultiSelectFieldDisplay = () => {
const { recordId, fieldDefinition } = useContext(FieldContext);
@@ -16,6 +16,7 @@ export const useMultiSelectFieldDisplay = () => {
const fieldValue = useRecordFieldValue<FieldMultiSelectValue | undefined>(
recordId,
fieldName,
fieldDefinition,
);
return {
@@ -1,8 +1,8 @@
import { useContext } from 'react';
import { useRecordFieldValue } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
import { FieldMetadataType } from '~/generated-metadata/graphql';
import { useRecordFieldValue } from '@/object-record/record-store/hooks/useRecordFieldValue';
import { FieldContext } from '../../contexts/FieldContext';
import { assertFieldMetadata } from '../../types/guards/assertFieldMetadata';
import { isFieldNumber } from '../../types/guards/isFieldNumber';
@@ -13,7 +13,11 @@ export const useNumberFieldDisplay = () => {
assertFieldMetadata(FieldMetadataType.NUMBER, isFieldNumber, fieldDefinition);
const fieldName = fieldDefinition.metadata.fieldName;
const fieldValue = useRecordFieldValue<number | null>(recordId, fieldName);
const fieldValue = useRecordFieldValue<number | null>(
recordId,
fieldName,
fieldDefinition,
);
return {
fieldDefinition,
@@ -1,8 +1,8 @@
import { useContext } from 'react';
import { type FieldPhonesValue } from '@/object-record/record-field/ui/types/FieldMetadata';
import { useRecordFieldValue } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
import { useRecordFieldValue } from '@/object-record/record-store/hooks/useRecordFieldValue';
import { FieldContext } from '../../contexts/FieldContext';
export const usePhonesFieldDisplay = () => {
@@ -13,6 +13,7 @@ export const usePhonesFieldDisplay = () => {
const fieldValue = useRecordFieldValue<FieldPhonesValue | undefined>(
recordId,
fieldName,
fieldDefinition,
);
return {
@@ -1,7 +1,6 @@
import { useContext } from 'react';
import { useRecordFieldValue } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
import { useRecordFieldValue } from '@/object-record/record-store/hooks/useRecordFieldValue';
import { type FieldRatingValue } from 'twenty-shared/types';
import { FieldContext } from '../../contexts/FieldContext';
@@ -10,7 +9,11 @@ export const useRatingFieldDisplay = () => {
const fieldName = fieldDefinition.metadata.fieldName;
const fieldValue = useRecordFieldValue<FieldRatingValue>(recordId, fieldName);
const fieldValue = useRecordFieldValue<FieldRatingValue>(
recordId,
fieldName,
fieldDefinition,
);
const rating = fieldValue ?? null;
@@ -3,11 +3,12 @@ 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 { type ObjectRecord } from '@/object-record/types/ObjectRecord';
import { FIELD_EDIT_BUTTON_WIDTH } from '@/ui/field/display/constants/FieldEditButtonWidth';
import { FieldMetadataType } from '~/generated-metadata/graphql';
import { useRecordFieldValue } from '@/object-record/record-store/hooks/useRecordFieldValue';
import { isDefined } from 'twenty-shared/utils';
import { FieldContext } from '../../contexts/FieldContext';
import { assertFieldMetadata } from '../../types/guards/assertFieldMetadata';
@@ -37,6 +38,7 @@ export const useRelationFromManyFieldDisplay = () => {
const fieldValue = useRecordFieldValue<ObjectRecord[] | undefined>(
recordId,
fieldName,
fieldDefinition,
);
const maxWidthForField =
@@ -2,7 +2,7 @@ import { isNonEmptyString } from '@sniptt/guards';
import { useContext } from 'react';
import { PreComputedChipGeneratorsContext } from '@/object-metadata/contexts/PreComputedChipGeneratorsContext';
import { useRecordFieldValue } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
import { FIELD_EDIT_BUTTON_WIDTH } from '@/ui/field/display/constants/FieldEditButtonWidth';
import { FieldMetadataType } from '~/generated-metadata/graphql';
@@ -11,6 +11,7 @@ import { generateDefaultRecordChipData } from '@/object-metadata/utils/generateD
import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext';
import { assertFieldMetadata } from '@/object-record/record-field/ui/types/guards/assertFieldMetadata';
import { isFieldRelation } from '@/object-record/record-field/ui/types/guards/isFieldRelation';
import { useRecordFieldValue } from '@/object-record/record-store/hooks/useRecordFieldValue';
import { isDefined } from 'twenty-shared/utils';
export const useRelationToOneFieldDisplay = () => {
@@ -37,6 +38,7 @@ export const useRelationToOneFieldDisplay = () => {
const fieldValue = useRecordFieldValue<ObjectRecord | undefined>(
recordId,
fieldName,
fieldDefinition,
);
const maxWidthForField =
@@ -1,10 +1,9 @@
import { useContext } from 'react';
import { useRecordFieldValue } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
import { type FieldRichTextValue } from '@/object-record/record-field/ui/types/FieldMetadata';
import { assertFieldMetadata } from '@/object-record/record-field/ui/types/guards/assertFieldMetadata';
import { isFieldRichText } from '@/object-record/record-field/ui/types/guards/isFieldRichText';
import { useRecordFieldValue } from '@/object-record/record-store/hooks/useRecordFieldValue';
import type { PartialBlock } from '@blocknote/core';
import { isDefined, parseJson } from 'twenty-shared/utils';
import { FieldMetadataType } from '~/generated-metadata/graphql';
@@ -24,6 +23,7 @@ export const useRichTextFieldDisplay = () => {
const fieldValue = useRecordFieldValue<FieldRichTextValue | undefined>(
recordId,
fieldName,
fieldDefinition,
);
const fieldValueParsed = isDefined(fieldValue)
@@ -1,10 +1,9 @@
import { useContext } from 'react';
import { useRecordFieldValue } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
import { type FieldRichTextV2Value } from '@/object-record/record-field/ui/types/FieldMetadata';
import { assertFieldMetadata } from '@/object-record/record-field/ui/types/guards/assertFieldMetadata';
import { isFieldRichTextV2 } from '@/object-record/record-field/ui/types/guards/isFieldRichTextV2';
import { useRecordFieldValue } from '@/object-record/record-store/hooks/useRecordFieldValue';
import { FieldMetadataType } from '~/generated-metadata/graphql';
import { FieldContext } from '../../contexts/FieldContext';
@@ -22,6 +21,7 @@ export const useRichTextV2FieldDisplay = () => {
const fieldValue = useRecordFieldValue<FieldRichTextV2Value | undefined>(
recordId,
fieldName,
fieldDefinition,
);
return {
@@ -1,8 +1,8 @@
import { useContext } from 'react';
import { type FieldDefinition } from '@/object-record/record-field/ui/types/FieldDefinition';
import { useRecordFieldValue } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
import { useRecordFieldValue } from '@/object-record/record-store/hooks/useRecordFieldValue';
import { FieldContext } from '../../contexts/FieldContext';
import {
type FieldSelectMetadata,
@@ -17,6 +17,7 @@ export const useSelectFieldDisplay = () => {
const fieldValue = useRecordFieldValue<FieldSelectValue | undefined>(
recordId,
fieldName,
fieldDefinition,
);
return {
@@ -1,7 +1,6 @@
import { useContext } from 'react';
import { useRecordFieldValue } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
import { useRecordFieldValue } from '@/object-record/record-store/hooks/useRecordFieldValue';
import { FieldContext } from '../../contexts/FieldContext';
export const useTextFieldDisplay = () => {
@@ -11,7 +10,11 @@ export const useTextFieldDisplay = () => {
const fieldName = fieldDefinition.metadata.fieldName;
const fieldValue =
useRecordFieldValue<string | undefined>(recordId, fieldName) ?? '';
useRecordFieldValue<string | undefined>(
recordId,
fieldName,
fieldDefinition,
) ?? '';
return {
fieldDefinition,
@@ -3,6 +3,7 @@ import {
fieldMetadataId,
fullNameFieldDefinition,
linksFieldDefinition,
morphRelationFieldDefinition,
relationFieldDefinition,
selectFieldDefinition,
} from '@/object-record/record-field/ui/__mocks__/fieldDefinitions';
@@ -213,4 +214,25 @@ describe('isFieldValueEmpty', () => {
}),
).toBe(false);
});
it('should return correct value for morph relation field', () => {
expect(
isFieldValueEmpty({
fieldDefinition: morphRelationFieldDefinition,
fieldValue: null,
}),
).toBe(true);
expect(
isFieldValueEmpty({
fieldDefinition: morphRelationFieldDefinition,
fieldValue: [{ value: null }, { value: [] }],
}),
).toBe(true);
expect(
isFieldValueEmpty({
fieldDefinition: morphRelationFieldDefinition,
fieldValue: [{ value: [{ id: '123' }] }],
}),
).toBe(false);
});
});
@@ -82,9 +82,23 @@ export const isFieldValueEmpty = ({
}
if (isFieldMorphRelation(fieldDefinition)) {
return isArray(fieldValue)
? !isNonEmptyArray(fieldValue)
: isValueEmpty(fieldValue);
if (!isArray(fieldValue)) {
return isValueEmpty(fieldValue);
}
const areValuesEmpty = fieldValue
.filter(isDefined)
.every((fieldValueWithObjectNameSingular) => {
if ('value' in fieldValueWithObjectNameSingular) {
const value = fieldValueWithObjectNameSingular?.value;
if (!isArray(value)) {
return isValueEmpty(value);
}
return !isNonEmptyArray(value);
}
return true;
});
return areValuesEmpty;
}
if (isFieldMultiSelect(fieldDefinition) || isFieldArray(fieldDefinition)) {
@@ -1,16 +0,0 @@
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
import { useRecoilValue } from 'recoil';
export const useRecordFieldValue = <T,>(
recordId: string,
fieldName: string,
) => {
const recordFieldValue = useRecoilValue(
recordStoreFamilySelector({
recordId,
fieldName,
}),
);
return recordFieldValue as T | undefined;
};
@@ -0,0 +1,23 @@
import { type FieldDefinition } from '@/object-record/record-field/ui/types/FieldDefinition';
import { type FieldMetadata } from '@/object-record/record-field/ui/types/FieldMetadata';
import { recordStoreFieldValueSelector } from '@/object-record/record-store/states/selectors/recordStoreFieldValueSelector';
import { useRecoilValue } from 'recoil';
export const useRecordFieldValue = <T extends unknown>(
recordId: string,
fieldName: string,
fieldDefinition: Pick<FieldDefinition<FieldMetadata>, 'type' | 'metadata'>,
) => {
const recordFieldValue = useRecoilValue(
recordStoreFieldValueSelector({
recordId,
fieldName,
fieldDefinition: {
type: fieldDefinition.type,
metadata: fieldDefinition.metadata,
},
}),
) as T | undefined;
return recordFieldValue;
};
@@ -0,0 +1,84 @@
import { selectorFamily } from 'recoil';
import { type FieldDefinition } from '@/object-record/record-field/ui/types/FieldDefinition';
import { type FieldMetadata } from '@/object-record/record-field/ui/types/FieldMetadata';
import { isFieldMorphRelation } from '@/object-record/record-field/ui/types/guards/isFieldMorphRelation';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { RelationType, type ObjectRecord } from 'twenty-shared/types';
import {
computeMorphRelationFieldName,
CustomError,
isDefined,
} from 'twenty-shared/utils';
export const recordStoreFieldValueSelector = selectorFamily({
key: 'recordStoreFieldValueSelector',
get:
({
recordId,
fieldName,
fieldDefinition,
}: {
recordId: string;
fieldName: string;
fieldDefinition: Pick<
FieldDefinition<FieldMetadata>,
'type' | 'metadata'
>;
}) =>
({ get }) => {
if (!isFieldMorphRelation(fieldDefinition)) {
return get(recordStoreFamilyState(recordId))?.[fieldName];
}
const morphRelations = fieldDefinition.metadata.morphRelations;
if (!Array.isArray(morphRelations) || morphRelations.length === 0) {
throw new CustomError(
'No morph relations found',
'NO_MORPH_RELATIONS_FOUND',
);
}
const morphValuesWithObjectName = morphRelations.map((morphRelation) => {
const computedFieldName = computeMorphRelationFieldName({
fieldName: morphRelation.sourceFieldMetadata.name,
relationType: morphRelation.type,
targetObjectMetadataNameSingular:
morphRelation.targetObjectMetadata.nameSingular,
targetObjectMetadataNamePlural:
morphRelation.targetObjectMetadata.namePlural,
});
return {
objectNameSingular: morphRelation.targetObjectMetadata.nameSingular,
value: get(recordStoreFamilyState(recordId))?.[computedFieldName],
};
});
const relationType = morphRelations[0].type;
if (relationType === RelationType.ONE_TO_MANY) {
return morphValuesWithObjectName as {
objectNameSingular: string;
value: ObjectRecord[];
}[];
}
if (relationType === RelationType.MANY_TO_ONE) {
const morphValueFiltered = morphValuesWithObjectName.filter(
(morphValue) => isDefined(morphValue.value),
);
return morphValueFiltered.length > 0
? (morphValueFiltered[0] as {
objectNameSingular: string;
value: ObjectRecord;
})
: null;
}
throw new CustomError(
`Unknown relation type: ${relationType}`,
'UNKNOWN_RELATION_TYPE',
);
},
});
@@ -1,45 +0,0 @@
import { selectorFamily } from 'recoil';
import { type FieldMetadataItemRelation } from '@/object-metadata/types/FieldMetadataItemRelation';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
import { computeMorphRelationFieldName, isDefined } from 'twenty-shared/utils';
export const recordStoreMorphManyToOneValueWithObjectNameFamilySelector =
selectorFamily({
key: 'recordStoreMorphManyToOneValueWithObjectNameFamilySelector',
get:
({
recordId,
morphRelations,
}: {
recordId: string;
morphRelations: FieldMetadataItemRelation[];
}) =>
({ get }) => {
const morphValuesWithObjectName = morphRelations.map(
(morphRelation) => {
const fieldName = computeMorphRelationFieldName({
fieldName: morphRelation.sourceFieldMetadata.name,
relationType: morphRelation.type,
targetObjectMetadataNameSingular:
morphRelation.targetObjectMetadata.nameSingular,
targetObjectMetadataNamePlural:
morphRelation.targetObjectMetadata.namePlural,
});
return {
objectNameSingular:
morphRelation.targetObjectMetadata.nameSingular,
value: get(recordStoreFamilyState(recordId))?.[
fieldName
] as ObjectRecord,
};
},
);
const morphValueFiltered = morphValuesWithObjectName.filter(
(morphValue) => isDefined(morphValue.value),
);
return morphValueFiltered.length > 0 ? morphValueFiltered[0] : null;
},
});