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:
+18
-1
@@ -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
-1
@@ -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)) {
|
||||
|
||||
+9
-1
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
+2
-1
@@ -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;
|
||||
|
||||
+2
-2
@@ -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 {
|
||||
|
||||
+3
-1
@@ -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 {
|
||||
|
||||
+2
-2
@@ -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 {
|
||||
|
||||
+2
-2
@@ -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 {
|
||||
|
||||
+2
-2
@@ -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 {
|
||||
|
||||
+2
-2
@@ -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 {
|
||||
|
||||
+2
-2
@@ -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 {
|
||||
|
||||
+2
-1
@@ -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 {
|
||||
|
||||
+2
-1
@@ -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({
|
||||
|
||||
+2
-1
@@ -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 {
|
||||
|
||||
+8
-8
@@ -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)
|
||||
|
||||
+7
-8
@@ -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 {
|
||||
|
||||
+2
-1
@@ -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 {
|
||||
|
||||
+6
-2
@@ -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,
|
||||
|
||||
+2
-1
@@ -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 {
|
||||
|
||||
+6
-3
@@ -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
-1
@@ -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 =
|
||||
|
||||
+3
-1
@@ -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 =
|
||||
|
||||
+2
-2
@@ -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)
|
||||
|
||||
+2
-2
@@ -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 {
|
||||
|
||||
+2
-1
@@ -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 {
|
||||
|
||||
+6
-3
@@ -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,
|
||||
|
||||
+22
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
+17
-3
@@ -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)) {
|
||||
|
||||
-16
@@ -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;
|
||||
};
|
||||
+23
@@ -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;
|
||||
};
|
||||
+84
@@ -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',
|
||||
);
|
||||
},
|
||||
});
|
||||
-45
@@ -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;
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user