Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
05d7525430 |
+5
-2
@@ -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) &&
|
||||
|
||||
+10
-4
@@ -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,
|
||||
|
||||
+28
-3
@@ -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)) {
|
||||
|
||||
+41
-3
@@ -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();
|
||||
|
||||
|
||||
+7
-2
@@ -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({
|
||||
|
||||
+52
@@ -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;
|
||||
});
|
||||
});
|
||||
};
|
||||
+2
@@ -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}`;
|
||||
|
||||
+36
-6
@@ -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;
|
||||
},
|
||||
});
|
||||
|
||||
+2
-1
@@ -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
|
||||
|
||||
+2
-2
@@ -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 = {
|
||||
|
||||
+1
@@ -63,6 +63,7 @@ const isFilterableFieldType = (
|
||||
FieldMetadataType.ARRAY,
|
||||
FieldMetadataType.UUID,
|
||||
FieldMetadataType.RELATION,
|
||||
FieldMetadataType.MORPH_RELATION,
|
||||
...COMPOSITE_FIELD_METADATA_TYPES,
|
||||
'array',
|
||||
'boolean',
|
||||
|
||||
+51
-3
@@ -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,
|
||||
});
|
||||
|
||||
+89
-4
@@ -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', () => {
|
||||
|
||||
+31
-5
@@ -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);
|
||||
|
||||
|
||||
+31
-5
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user