Compare commits

...
Author SHA1 Message Date
Thomas Trompette 05d7525430 Handle morph relations in workflow steps 2026-03-27 17:27:15 +01:00
16 changed files with 389 additions and 40 deletions
@@ -8,6 +8,7 @@ type SortableFieldInput = {
name: string;
type: FieldMetadataType;
relation?: { type: RelationType } | null;
settings?: { relationType?: RelationType } | null;
};
export const filterSortableFieldMetadataItems = (field: SortableFieldInput) => {
@@ -18,8 +19,10 @@ export const filterSortableFieldMetadataItems = (field: SortableFieldInput) => {
);
const isRelationFieldSortable =
field.type === FieldMetadataType.RELATION &&
field.relation?.type === RelationType.MANY_TO_ONE;
(field.type === FieldMetadataType.RELATION &&
field.relation?.type === RelationType.MANY_TO_ONE) ||
(field.type === FieldMetadataType.MORPH_RELATION &&
field.settings?.relationType === RelationType.MANY_TO_ONE);
return (
!isHiddenSystemField(field) &&
@@ -15,10 +15,15 @@ export const getFilterFilterableFieldMetadataItems = ({
field.type === FieldMetadataType.RELATION &&
(field.name === 'workflow' || field.name === 'workflowVersion');
const isRelationFieldHandled = !(
field.type === FieldMetadataType.RELATION &&
field.relation?.type !== RelationType.MANY_TO_ONE
);
const isRelationFieldHandled =
!(
field.type === FieldMetadataType.RELATION &&
field.relation?.type !== RelationType.MANY_TO_ONE
) &&
!(
field.type === FieldMetadataType.MORPH_RELATION &&
field.settings?.relationType !== RelationType.MANY_TO_ONE
);
const isFieldTypeFilterable = [
FieldMetadataType.BOOLEAN,
@@ -31,6 +36,7 @@ export const getFilterFilterableFieldMetadataItems = ({
FieldMetadataType.FULL_NAME,
FieldMetadataType.ADDRESS,
FieldMetadataType.RELATION,
FieldMetadataType.MORPH_RELATION,
FieldMetadataType.SELECT,
FieldMetadataType.MULTI_SELECT,
FieldMetadataType.CURRENCY,
@@ -9,10 +9,15 @@ import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectab
import { useAtomComponentSelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentSelectorValue';
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { AdvancedFilterFieldSelectSearchInput } from '@/object-record/advanced-filter/components/AdvancedFilterFieldSelectSearchInput';
import { useAdvancedFilterFieldSelectDropdown } from '@/object-record/advanced-filter/hooks/useAdvancedFilterFieldSelectDropdown';
import { useSelectFieldUsedInAdvancedFilterDropdown } from '@/object-record/advanced-filter/hooks/useSelectFieldUsedInAdvancedFilterDropdown';
import { AdvancedFilterContext } from '@/object-record/advanced-filter/states/context/AdvancedFilterContext';
import {
type ExpandedFilterableField,
expandMorphRelationFieldMetadataItems,
} from '@/object-record/advanced-filter/utils/expandMorphRelationFieldMetadataItems';
import { ObjectFilterDropdownFilterSelectMenuItem } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelectMenuItem';
import { fieldMetadataItemIdUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemIdUsedInDropdownComponentState';
import { objectFilterDropdownIsSelectingCompositeFieldComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownIsSelectingCompositeFieldComponentState';
@@ -26,8 +31,8 @@ import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/Gene
import { useAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentState';
import { useSetAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentState';
import { useLingui } from '@lingui/react/macro';
import { useContext } from 'react';
import { getFilterTypeFromFieldType } from 'twenty-shared/utils';
import { useContext, useMemo } from 'react';
import { getFilterTypeFromFieldType, isDefined } from 'twenty-shared/utils';
type AdvancedFilterFieldSelectMenuProps = {
recordFilterId: string;
@@ -50,6 +55,17 @@ export const AdvancedFilterFieldSelectMenu = ({
const { filterableFieldMetadataItems: filterableFieldMetadataItems } =
useFilterableFieldMetadataItems(objectMetadataItem.id);
const { objectMetadataItems } = useObjectMetadataItems();
const expandedFilterableFields = useMemo(
() =>
expandMorphRelationFieldMetadataItems(
filterableFieldMetadataItems,
objectMetadataItems,
),
[filterableFieldMetadataItems, objectMetadataItems],
);
const visibleRecordFields = useAtomComponentSelectorValue(
visibleRecordFieldsComponentSelector,
);
@@ -59,7 +75,7 @@ export const AdvancedFilterFieldSelectMenu = ({
);
const filteredSearchInputFieldMetadataItems =
filterableFieldMetadataItems.filter((fieldMetadataItem) =>
expandedFilterableFields.filter((fieldMetadataItem) =>
fieldMetadataItem.label
.toLocaleLowerCase()
.includes(objectFilterDropdownSearchInput.toLocaleLowerCase()),
@@ -106,6 +122,12 @@ export const AdvancedFilterFieldSelectMenu = ({
const handleFieldMetadataItemSelect = (
selectedFieldMetadataItem: FieldMetadataItem,
) => {
const expandedField =
selectedFieldMetadataItem as ExpandedFilterableField;
const isMorphRelationTarget = isDefined(
expandedField.morphRelationTarget,
);
resetSelectedItem();
const filterType = getFilterTypeFromFieldType(
@@ -115,6 +137,9 @@ export const AdvancedFilterFieldSelectMenu = ({
selectFieldUsedInAdvancedFilterDropdown({
fieldMetadataItemId: selectedFieldMetadataItem.id,
recordFilterId,
fieldMetadataItemOverride: isMorphRelationTarget
? selectedFieldMetadataItem
: undefined,
});
if (isCompositeFilterableFieldType(filterType)) {
@@ -1,4 +1,5 @@
import { useFieldMetadataItemById } from '@/object-metadata/hooks/useFieldMetadataItemById';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { getCompositeSubFieldLabel } from '@/object-record/object-filter-dropdown/utils/getCompositeSubFieldLabel';
import { isCompositeFieldType } from '@/object-record/object-filter-dropdown/utils/isCompositeFieldType';
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
@@ -8,6 +9,39 @@ import { isNonEmptyString } from '@sniptt/guards';
import { isDefined } from 'twenty-shared/utils';
import { useIcons } from 'twenty-ui/display';
const useMorphRelationFieldMetadataItem = (fieldMetadataId: string) => {
const { objectMetadataItems } = useObjectMetadataItems();
for (const objectMetadataItem of objectMetadataItems) {
for (const field of objectMetadataItem.fields) {
if (!isDefined(field.morphRelations)) {
continue;
}
const morphRelation = field.morphRelations.find(
(relation) => relation.sourceFieldMetadata.id === fieldMetadataId,
);
if (isDefined(morphRelation)) {
const targetObjectMetadata = objectMetadataItems.find(
(item) => item.id === morphRelation.targetObjectMetadata.id,
);
return {
fieldMetadataItem: {
...field,
icon: targetObjectMetadata?.icon ?? field.icon,
relation: morphRelation,
},
morphRelation,
};
}
}
}
return { fieldMetadataItem: undefined, morphRelation: undefined };
};
export const useRecordFilterField = (recordFilterId: string) => {
const currentRecordFilters = useAtomComponentStateValue(
currentRecordFiltersComponentState,
@@ -17,9 +51,13 @@ export const useRecordFilterField = (recordFilterId: string) => {
(recordFilter) => recordFilter.id === recordFilterId,
);
const { fieldMetadataItem } = useFieldMetadataItemById(
recordFilter?.fieldMetadataId ?? '',
);
const { fieldMetadataItem: directFieldMetadataItem } =
useFieldMetadataItemById(recordFilter?.fieldMetadataId ?? '');
const { fieldMetadataItem: morphFieldMetadataItem } =
useMorphRelationFieldMetadataItem(recordFilter?.fieldMetadataId ?? '');
const fieldMetadataItem = directFieldMetadataItem ?? morphFieldMetadataItem;
const { getIcon } = useIcons();
@@ -1,3 +1,4 @@
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { useGetFieldMetadataItemByIdOrThrow } from '@/object-metadata/hooks/useGetFieldMetadataItemById';
import { useGetInitialFilterValue } from '@/object-record/object-filter-dropdown/hooks/useGetInitialFilterValue';
import { fieldMetadataItemIdUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemIdUsedInDropdownComponentState';
@@ -23,6 +24,7 @@ type SelectFilterParams = {
fieldMetadataItemId: string;
recordFilterId: string;
subFieldName?: CompositeFieldSubFieldName | null | undefined;
fieldMetadataItemOverride?: FieldMetadataItem;
};
export const useSelectFieldUsedInAdvancedFilterDropdown = () => {
@@ -62,11 +64,13 @@ export const useSelectFieldUsedInAdvancedFilterDropdown = () => {
fieldMetadataItemId,
recordFilterId,
subFieldName,
fieldMetadataItemOverride,
}: SelectFilterParams) => {
setFieldMetadataItemIdUsedInDropdown(fieldMetadataItemId);
const { fieldMetadataItem } =
getFieldMetadataItemByIdOrThrow(fieldMetadataItemId);
const fieldMetadataItem =
fieldMetadataItemOverride ??
getFieldMetadataItemByIdOrThrow(fieldMetadataItemId).fieldMetadataItem;
if (!isDefined(fieldMetadataItem)) {
return;
@@ -74,6 +78,7 @@ export const useSelectFieldUsedInAdvancedFilterDropdown = () => {
if (
fieldMetadataItem.type === 'RELATION' ||
fieldMetadataItem.type === 'MORPH_RELATION' ||
fieldMetadataItem.type === 'SELECT'
) {
pushFocusItemToFocusStack({
@@ -0,0 +1,52 @@
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { type FieldMetadataItemRelation } from '@/object-metadata/types/FieldMetadataItemRelation';
import { isDefined } from 'twenty-shared/utils';
import { FieldMetadataType, RelationType } from '~/generated-metadata/graphql';
export type ExpandedFilterableField = FieldMetadataItem & {
morphRelationTarget?: FieldMetadataItemRelation;
};
export const expandMorphRelationFieldMetadataItems = (
fieldMetadataItems: FieldMetadataItem[],
objectMetadataItems: EnrichedObjectMetadataItem[],
): ExpandedFilterableField[] => {
return fieldMetadataItems.flatMap((field) => {
if (
field.type !== FieldMetadataType.MORPH_RELATION ||
!isDefined(field.morphRelations) ||
field.morphRelations.length === 0
) {
return [field as ExpandedFilterableField];
}
return field.morphRelations
.filter(
(morphRelation) =>
morphRelation.type === RelationType.MANY_TO_ONE &&
isDefined(morphRelation.targetObjectMetadata),
)
.map((morphRelation) => {
const targetObjectMetadata = objectMetadataItems.find(
(item) => item.id === morphRelation.targetObjectMetadata.id,
);
const label =
targetObjectMetadata?.labelSingular ??
morphRelation.targetObjectMetadata.nameSingular;
const icon = targetObjectMetadata?.icon ?? field.icon;
return {
...field,
id: morphRelation.sourceFieldMetadata.id,
label,
icon,
type: FieldMetadataType.RELATION,
relation: morphRelation,
morphRelationTarget: morphRelation,
} satisfies ExpandedFilterableField;
});
});
};
@@ -25,6 +25,8 @@ export const getAdvancedFilterInputPlaceholderText = (
return 'Select actor';
case FieldMetadataType.RELATION:
return `Select ${fieldMetadataItem.relation?.targetObjectMetadata.nameSingular}`;
case FieldMetadataType.MORPH_RELATION:
return `Select ${fieldMetadataItem.label}`;
case FieldMetadataType.SELECT:
case FieldMetadataType.MULTI_SELECT:
return `Select ${fieldMetadataItem.label}`;
@@ -3,6 +3,8 @@ import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataIte
import { ObjectFilterDropdownComponentInstanceContext } from '@/object-record/object-filter-dropdown/states/contexts/ObjectFilterDropdownComponentInstanceContext';
import { fieldMetadataItemIdUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemIdUsedInDropdownComponentState';
import { createAtomComponentSelector } from '@/ui/utilities/state/jotai/utils/createAtomComponentSelector';
import { FieldMetadataType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
export const fieldMetadataItemUsedInDropdownComponentSelector =
createAtomComponentSelector<FieldMetadataItem | null | undefined>({
@@ -18,13 +20,41 @@ export const fieldMetadataItemUsedInDropdownComponentSelector =
const objectMetadataItems = get(objectMetadataItemsSelector);
const correspondingFieldMetadataItem = objectMetadataItems
.flatMap((objectMetadataItem) => objectMetadataItem.fields)
.find(
(fieldMetadataItem) =>
fieldMetadataItem.id === fieldMetadataItemIdUsedInDropdown,
const allFields = objectMetadataItems.flatMap(
(objectMetadataItem) => objectMetadataItem.fields,
);
const correspondingFieldMetadataItem = allFields.find(
(fieldMetadataItem) =>
fieldMetadataItem.id === fieldMetadataItemIdUsedInDropdown,
);
if (isDefined(correspondingFieldMetadataItem)) {
return correspondingFieldMetadataItem;
}
// Morph relation targets have IDs from morphRelations[].sourceFieldMetadata.id
// which don't exist as standalone fields — return a virtual RELATION field
for (const field of allFields) {
if (!isDefined(field.morphRelations)) {
continue;
}
const morphRelation = field.morphRelations.find(
(relation) =>
relation.sourceFieldMetadata.id ===
fieldMetadataItemIdUsedInDropdown,
);
return correspondingFieldMetadataItem;
if (isDefined(morphRelation)) {
return {
...field,
type: FieldMetadataType.RELATION,
relation: morphRelation,
};
}
}
return undefined;
},
});
@@ -8,7 +8,8 @@ export const getFieldMetadataTypeLabel = (fieldType: FieldMetadataType) => {
//TODO: Remove ?.label > .label when we have a proper type for field (issue #1097)
if (
isNonCompositeField(fieldType) ||
fieldType === FieldMetadataType.RELATION
fieldType === FieldMetadataType.RELATION ||
fieldType === FieldMetadataType.MORPH_RELATION
)
return SETTINGS_NON_COMPOSITE_FIELD_TYPE_CONFIGS[
fieldType as keyof typeof SETTINGS_NON_COMPOSITE_FIELD_TYPE_CONFIGS
@@ -17,8 +17,8 @@ import { useUserTimezone } from '@/ui/input/components/internal/date/hooks/useUs
import { useEffect, useState } from 'react';
import { Temporal } from 'temporal-polyfill';
import { isDefined } from 'twenty-shared/utils';
import { isDifferentZonedDateTime } from '~/utils/dates/isDifferentZonedDateTime';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import { isDifferentZonedDateTime } from '~/utils/dates/isDifferentZonedDateTime';
const StyledInputContainer = styled.div`
align-items: center;
@@ -38,7 +38,7 @@ const StyledInput = styled.input<{ hasError?: boolean }>`
font-weight: 500;
outline: none;
padding-left: ${themeCssVariables.spacing[2]};
width: 140px;
width: 100%;
`;
type DateTimePickerInputProps = {
@@ -63,6 +63,7 @@ const isFilterableFieldType = (
FieldMetadataType.ARRAY,
FieldMetadataType.UUID,
FieldMetadataType.RELATION,
FieldMetadataType.MORPH_RELATION,
...COMPOSITE_FIELD_METADATA_TYPES,
'array',
'boolean',
@@ -261,7 +261,7 @@ describe('generateRecordEventOutputSchema', () => {
});
});
it('should convert MORPH_RELATION fields to prefixed UUID id fields when MANY_TO_ONE', () => {
it('should convert MORPH_RELATION fields to per-target prefixed UUID id fields when MANY_TO_ONE', () => {
const objectMetadataItem = createMockObjectMetadataItem({
fields: [
{
@@ -275,6 +275,38 @@ describe('generateRecordEventOutputSchema', () => {
settings: {
relationType: RelationType.MANY_TO_ONE,
},
morphRelations: [
{
type: RelationType.MANY_TO_ONE,
targetObjectMetadata: {
id: 'person-id',
nameSingular: 'person',
namePlural: 'people',
},
sourceObjectMetadata: {
id: 'task-target-id',
nameSingular: 'taskTarget',
namePlural: 'taskTargets',
},
sourceFieldMetadata: { id: 's1', name: 'target' },
targetFieldMetadata: { id: 't1', name: 'taskTargets' },
},
{
type: RelationType.MANY_TO_ONE,
targetObjectMetadata: {
id: 'company-id',
nameSingular: 'company',
namePlural: 'companies',
},
sourceObjectMetadata: {
id: 'task-target-id',
nameSingular: 'taskTarget',
namePlural: 'taskTargets',
},
sourceFieldMetadata: { id: 's2', name: 'target' },
targetFieldMetadata: { id: 't2', name: 'taskTargets' },
},
],
},
] as any,
});
@@ -284,8 +316,24 @@ describe('generateRecordEventOutputSchema', () => {
DatabaseEventAction.CREATED,
);
expect(Object.keys(result.fields)).toContain('properties.after.targetId');
expect(result.fields['properties.after.targetId']).toMatchObject({
expect(Object.keys(result.fields)).not.toContain(
'properties.after.targetId',
);
expect(Object.keys(result.fields)).toContain(
'properties.after.targetPersonId',
);
expect(
result.fields['properties.after.targetPersonId'],
).toMatchObject({
isLeaf: true,
type: FieldMetadataType.UUID,
});
expect(Object.keys(result.fields)).toContain(
'properties.after.targetCompanyId',
);
expect(
result.fields['properties.after.targetCompanyId'],
).toMatchObject({
isLeaf: true,
type: FieldMetadataType.UUID,
});
@@ -233,7 +233,7 @@ describe('generateRecordOutputSchema', () => {
expect(result.object.icon).toBeUndefined();
});
it('should convert MORPH_RELATION fields to UUID id fields when MANY_TO_ONE', () => {
it('should convert MORPH_RELATION fields to per-target UUID id fields when MANY_TO_ONE', () => {
const objectMetadataItem = createMockObjectMetadataItem({
fields: [
{
@@ -247,6 +247,38 @@ describe('generateRecordOutputSchema', () => {
settings: {
relationType: RelationType.MANY_TO_ONE,
},
morphRelations: [
{
type: RelationType.MANY_TO_ONE,
targetObjectMetadata: {
id: 'person-id',
nameSingular: 'person',
namePlural: 'people',
},
sourceObjectMetadata: {
id: 'task-target-id',
nameSingular: 'taskTarget',
namePlural: 'taskTargets',
},
sourceFieldMetadata: { id: 's1', name: 'target' },
targetFieldMetadata: { id: 't1', name: 'taskTargets' },
},
{
type: RelationType.MANY_TO_ONE,
targetObjectMetadata: {
id: 'company-id',
nameSingular: 'company',
namePlural: 'companies',
},
sourceObjectMetadata: {
id: 'task-target-id',
nameSingular: 'taskTarget',
namePlural: 'taskTargets',
},
sourceFieldMetadata: { id: 's2', name: 'target' },
targetFieldMetadata: { id: 't2', name: 'taskTargets' },
},
],
},
] as any,
});
@@ -254,11 +286,20 @@ describe('generateRecordOutputSchema', () => {
const result = generateRecordOutputSchema(objectMetadataItem);
expect(result.fields).not.toHaveProperty('target');
expect(result.fields).toHaveProperty('targetId');
expect(result.fields.targetId).toMatchObject({
expect(result.fields).not.toHaveProperty('targetId');
expect(result.fields).toHaveProperty('targetPersonId');
expect(result.fields.targetPersonId).toMatchObject({
isLeaf: true,
type: FieldMetadataType.UUID,
label: 'Target Id',
label: 'Target Person Id',
});
expect(result.fields).toHaveProperty('targetCompanyId');
expect(result.fields.targetCompanyId).toMatchObject({
isLeaf: true,
type: FieldMetadataType.UUID,
label: 'Target Company Id',
});
});
@@ -275,6 +316,23 @@ describe('generateRecordOutputSchema', () => {
settings: {
relationType: RelationType.ONE_TO_MANY,
},
morphRelations: [
{
type: RelationType.ONE_TO_MANY,
targetObjectMetadata: {
id: 'person-id',
nameSingular: 'person',
namePlural: 'people',
},
sourceObjectMetadata: {
id: 'task-target-id',
nameSingular: 'taskTarget',
namePlural: 'taskTargets',
},
sourceFieldMetadata: { id: 's1', name: 'targets' },
targetFieldMetadata: { id: 't1', name: 'taskTargets' },
},
],
},
] as any,
});
@@ -283,6 +341,33 @@ describe('generateRecordOutputSchema', () => {
expect(result.fields).not.toHaveProperty('targets');
expect(result.fields).not.toHaveProperty('targetsId');
expect(result.fields).not.toHaveProperty('targetsPeopleId');
});
it('should handle MORPH_RELATION with empty morphRelations array', () => {
const objectMetadataItem = createMockObjectMetadataItem({
fields: [
{
id: 'morph-relation-id',
name: 'target',
label: 'Target',
type: FieldMetadataType.MORPH_RELATION,
isActive: true,
isSystem: false,
icon: 'IconLink',
settings: {
relationType: RelationType.MANY_TO_ONE,
},
morphRelations: [],
},
] as any,
});
const result = generateRecordOutputSchema(objectMetadataItem);
expect(result.fields).not.toHaveProperty('target');
expect(result.fields).not.toHaveProperty('targetId');
expect(Object.keys(result.fields)).toHaveLength(0);
});
it('should handle field without icon', () => {
@@ -11,7 +11,11 @@ import {
FieldMetadataType,
RelationType,
} from 'twenty-shared/types';
import { capitalize, isDefined } from 'twenty-shared/utils';
import {
capitalize,
computeMorphRelationFieldName,
isDefined,
} from 'twenty-shared/utils';
import { DatabaseEventAction } from '~/generated-metadata/graphql';
const camelToTitleCase = (camelCaseText: string): string =>
@@ -116,11 +120,33 @@ const generatePrefixedRecordFields = (
continue;
}
const isRelationField =
fieldMetadataItem.type === FieldMetadataType.RELATION ||
fieldMetadataItem.type === FieldMetadataType.MORPH_RELATION;
if (fieldMetadataItem.type === FieldMetadataType.MORPH_RELATION) {
for (const morphRelation of fieldMetadataItem.morphRelations ?? []) {
const morphFieldName = computeMorphRelationFieldName({
fieldName: fieldMetadataItem.name,
relationType: RelationType.MANY_TO_ONE,
targetObjectMetadataNameSingular:
morphRelation.targetObjectMetadata.nameSingular,
targetObjectMetadataNamePlural:
morphRelation.targetObjectMetadata.namePlural,
});
const relationIdFieldName = `${morphFieldName}Id`;
const relationIdFieldLabel = camelToTitleCase(relationIdFieldName);
if (isRelationField) {
result[`${prefix}.${relationIdFieldName}`] = {
isLeaf: true,
icon: fieldMetadataItem.icon ?? undefined,
type: FieldMetadataType.UUID,
label: relationIdFieldLabel,
value: generateFakeValue(
FieldMetadataType.UUID,
'FieldMetadataType',
),
fieldMetadataId: fieldMetadataItem.id,
isCompositeSubField: false,
};
}
} else if (fieldMetadataItem.type === FieldMetadataType.RELATION) {
const relationIdFieldName = `${fieldMetadataItem.name}Id`;
const relationIdFieldLabel = camelToTitleCase(relationIdFieldName);
@@ -11,7 +11,11 @@ import {
FieldMetadataType,
RelationType,
} from 'twenty-shared/types';
import { capitalize, isDefined } from 'twenty-shared/utils';
import {
capitalize,
computeMorphRelationFieldName,
isDefined,
} from 'twenty-shared/utils';
const camelToTitleCase = (camelCaseText: string): string =>
capitalize(
@@ -107,11 +111,33 @@ const generateRecordFields = (
continue;
}
const isRelationField =
fieldMetadataItem.type === FieldMetadataType.RELATION ||
fieldMetadataItem.type === FieldMetadataType.MORPH_RELATION;
if (fieldMetadataItem.type === FieldMetadataType.MORPH_RELATION) {
for (const morphRelation of fieldMetadataItem.morphRelations ?? []) {
const morphFieldName = computeMorphRelationFieldName({
fieldName: fieldMetadataItem.name,
relationType: RelationType.MANY_TO_ONE,
targetObjectMetadataNameSingular:
morphRelation.targetObjectMetadata.nameSingular,
targetObjectMetadataNamePlural:
morphRelation.targetObjectMetadata.namePlural,
});
const relationIdFieldName = `${morphFieldName}Id`;
const relationIdFieldLabel = camelToTitleCase(relationIdFieldName);
if (isRelationField) {
result[relationIdFieldName] = {
isLeaf: true,
icon: fieldMetadataItem.icon ?? undefined,
type: FieldMetadataType.UUID,
label: relationIdFieldLabel,
value: generateFakeValue(
FieldMetadataType.UUID,
'FieldMetadataType',
),
fieldMetadataId: fieldMetadataItem.id,
isCompositeSubField: false,
};
}
} else if (fieldMetadataItem.type === FieldMetadataType.RELATION) {
const relationIdFieldName = `${fieldMetadataItem.name}Id`;
const relationIdFieldLabel = camelToTitleCase(relationIdFieldName);
@@ -24,6 +24,7 @@ export const getFilterTypeFromFieldType = (
case FieldMetadataType.PHONES:
return 'PHONES';
case FieldMetadataType.RELATION:
case FieldMetadataType.MORPH_RELATION:
return 'RELATION';
case FieldMetadataType.SELECT:
return 'SELECT';