Optimize metadata queries (#7013)

In this PR:

1. Refactor guards to avoid duplicated queries: WorkspaceAuthGuard and
UserAuthGuard only check for existence of workspace and user in the
request without querying the database
This commit is contained in:
Charles Bochet
2024-09-13 19:11:32 +02:00
committed by Charles Bochet
parent cf8b1161cc
commit 523df5398a
132 changed files with 818 additions and 6372 deletions

View File

@ -1,62 +0,0 @@
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { RelationType } from '@/settings/data-model/types/RelationType';
import {
FieldMetadataType,
RelationMetadataType,
} from '~/generated-metadata/graphql';
export const getRelationDefinition = ({
objectMetadataItems,
fieldMetadataItemOnSourceRecord,
}: {
objectMetadataItems: ObjectMetadataItem[];
fieldMetadataItemOnSourceRecord: FieldMetadataItem;
}) => {
if (fieldMetadataItemOnSourceRecord.type !== FieldMetadataType.Relation) {
return null;
}
const relationMetadataItem =
fieldMetadataItemOnSourceRecord.fromRelationMetadata ||
fieldMetadataItemOnSourceRecord.toRelationMetadata;
if (!relationMetadataItem) return null;
const relationSourceFieldMetadataItemId =
'toFieldMetadataId' in relationMetadataItem
? relationMetadataItem.toFieldMetadataId
: relationMetadataItem.fromFieldMetadataId;
if (!relationSourceFieldMetadataItemId) return null;
// TODO: precise naming, is it relationTypeFromTargetPointOfView or relationTypeFromSourcePointOfView ?
const relationType =
relationMetadataItem.relationType === RelationMetadataType.OneToMany &&
fieldMetadataItemOnSourceRecord.toRelationMetadata
? ('MANY_TO_ONE' satisfies RelationType)
: (relationMetadataItem.relationType as RelationType);
const targetObjectMetadataNameSingular =
'toObjectMetadata' in relationMetadataItem
? relationMetadataItem.toObjectMetadata.nameSingular
: relationMetadataItem.fromObjectMetadata.nameSingular;
const targetObjectMetadataItem = objectMetadataItems.find(
(item) => item.nameSingular === targetObjectMetadataNameSingular,
);
if (!targetObjectMetadataItem) return null;
const fieldMetadataItemOnTargetRecord = targetObjectMetadataItem.fields.find(
(field) => field.id === relationSourceFieldMetadataItemId,
);
if (!fieldMetadataItemOnTargetRecord) return null;
return {
fieldMetadataItemOnTargetRecord,
targetObjectMetadataItem,
relationType,
};
};

View File

@ -1,6 +1,5 @@
import { ApolloCache } from '@apollo/client';
import { getRelationDefinition } from '@/apollo/optimistic-effect/utils/getRelationDefinition';
import { triggerAttachRelationOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerAttachRelationOptimisticEffect';
import { triggerDeleteRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDeleteRecordsOptimisticEffect';
import { triggerDetachRelationOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDetachRelationOptimisticEffect';
@ -45,16 +44,23 @@ export const triggerUpdateRelationsOptimisticEffect = ({
return;
}
const relationDefinition = getRelationDefinition({
fieldMetadataItemOnSourceRecord,
objectMetadataItems,
});
const relationDefinition =
fieldMetadataItemOnSourceRecord.relationDefinition;
if (!relationDefinition) {
return;
}
const { targetObjectMetadataItem, fieldMetadataItemOnTargetRecord } =
relationDefinition;
const { targetObjectMetadata, targetFieldMetadata } = relationDefinition;
const fullTargetObjectMetadataItem = objectMetadataItems.find(
({ nameSingular }) =>
nameSingular === targetObjectMetadata.nameSingular,
);
if (!fullTargetObjectMetadataItem) {
return;
}
const currentFieldValueOnSourceRecord:
| RecordGqlConnection
@ -80,7 +86,7 @@ export const triggerUpdateRelationsOptimisticEffect = ({
// it's an object record connection (we can still check it though as a safeguard)
const currentFieldValueOnSourceRecordIsARecordConnection =
isObjectRecordConnection(
targetObjectMetadataItem.nameSingular,
targetObjectMetadata.nameSingular,
currentFieldValueOnSourceRecord,
);
@ -93,7 +99,7 @@ export const triggerUpdateRelationsOptimisticEffect = ({
const updatedFieldValueOnSourceRecordIsARecordConnection =
isObjectRecordConnection(
targetObjectMetadataItem.nameSingular,
targetObjectMetadata.nameSingular,
updatedFieldValueOnSourceRecord,
);
@ -112,13 +118,13 @@ export const triggerUpdateRelationsOptimisticEffect = ({
// Instead of hardcoding it here
const shouldCascadeDeleteTargetRecords =
CORE_OBJECT_NAMES_TO_DELETE_ON_TRIGGER_RELATION_DETACH.includes(
targetObjectMetadataItem.nameSingular as CoreObjectNameSingular,
targetObjectMetadata.nameSingular as CoreObjectNameSingular,
);
if (shouldCascadeDeleteTargetRecords) {
triggerDeleteRecordsOptimisticEffect({
cache,
objectMetadataItem: targetObjectMetadataItem,
objectMetadataItem: fullTargetObjectMetadataItem,
recordsToDelete: targetRecordsToDetachFrom,
objectMetadataItems,
});
@ -128,8 +134,8 @@ export const triggerUpdateRelationsOptimisticEffect = ({
cache,
sourceObjectNameSingular: sourceObjectMetadataItem.nameSingular,
sourceRecordId: currentSourceRecord.id,
fieldNameOnTargetRecord: fieldMetadataItemOnTargetRecord.name,
targetObjectNameSingular: targetObjectMetadataItem.nameSingular,
fieldNameOnTargetRecord: targetFieldMetadata.name,
targetObjectNameSingular: targetObjectMetadata.nameSingular,
targetRecordId: targetRecordToDetachFrom.id,
});
});
@ -145,8 +151,8 @@ export const triggerUpdateRelationsOptimisticEffect = ({
cache,
sourceObjectNameSingular: sourceObjectMetadataItem.nameSingular,
sourceRecordId: updatedSourceRecord.id,
fieldNameOnTargetRecord: fieldMetadataItemOnTargetRecord.name,
targetObjectNameSingular: targetObjectMetadataItem.nameSingular,
fieldNameOnTargetRecord: targetFieldMetadata.name,
targetObjectNameSingular: targetObjectMetadata.nameSingular,
targetRecordId: targetRecordToAttachTo.id,
}),
);

View File

@ -11,7 +11,7 @@ import { NavigationDrawerSection } from '@/ui/navigation/navigation-drawer/compo
import { NavigationDrawerSectionTitle } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSectionTitle';
import { useNavigationSection } from '@/ui/navigation/navigation-drawer/hooks/useNavigationSection';
import { currentUserState } from '@/auth/states/currentUserState';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { useFavorites } from '../hooks/useFavorites';
const StyledContainer = styled(NavigationDrawerSection)`
@ -35,7 +35,7 @@ const StyledNavigationDrawerItem = styled(NavigationDrawerItem)`
`;
export const CurrentWorkspaceMemberFavorites = () => {
const currentUser = useRecoilValue(currentUserState);
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
const { favorites, handleReorderFavorite } = useFavorites();
const loading = useIsPrefetchLoading();
@ -44,12 +44,12 @@ export const CurrentWorkspaceMemberFavorites = () => {
useNavigationSection('Favorites');
const isNavigationSectionOpen = useRecoilValue(isNavigationSectionOpenState);
if (loading && isDefined(currentUser)) {
if (loading && isDefined(currentWorkspaceMember)) {
return <FavoritesSkeletonLoader />;
}
const currentWorkspaceMemberFavorites = favorites.filter(
(favorite) => favorite.workspaceMemberId === currentUser?.id,
(favorite) => favorite.workspaceMemberId === currentWorkspaceMember?.id,
);
if (

View File

@ -9,5 +9,6 @@ export type Favorite = {
avatarType: AvatarType;
link: string;
recordId: string;
workspaceMemberId: string;
__typename: 'Favorite';
};

View File

@ -19,8 +19,8 @@ export const sortFavorites = (
const relationObject = favorite[relationField.name];
const relationObjectNameSingular =
relationField.toRelationMetadata?.fromObjectMetadata.nameSingular ??
'';
relationField.relationDefinition?.targetObjectMetadata
.nameSingular ?? '';
const objectRecordIdentifier =
getObjectRecordIdentifierByNameSingular(
@ -38,6 +38,7 @@ export const sortFavorites = (
link: hasLinkToShowPage
? objectRecordIdentifier.linkToShowPage
: '',
workspaceMemberId: favorite.workspaceMemberId,
} as Favorite;
}
}

View File

@ -39,32 +39,6 @@ export const FIND_MANY_OBJECT_METADATA_ITEMS = gql`
isNullable
createdAt
updatedAt
fromRelationMetadata {
id
relationType
toObjectMetadata {
id
dataSourceId
nameSingular
namePlural
isSystem
isRemote
}
toFieldMetadataId
}
toRelationMetadata {
id
relationType
fromObjectMetadata {
id
dataSourceId
nameSingular
namePlural
isSystem
isRemote
}
fromFieldMetadataId
}
defaultValue
options
relationDefinition {

View File

@ -39,29 +39,27 @@ export const query = gql`
isNullable
createdAt
updatedAt
fromRelationMetadata {
id
relationType
toObjectMetadata {
relationDefinition {
relationId
direction
sourceObjectMetadata {
id
dataSourceId
nameSingular
namePlural
isSystem
}
toFieldMetadataId
}
toRelationMetadata {
id
relationType
fromObjectMetadata {
sourceFieldMetadata {
id
name
}
targetObjectMetadata {
id
dataSourceId
nameSingular
namePlural
isSystem
}
fromFieldMetadataId
targetFieldMetadata {
id
name
}
}
defaultValue
options

View File

@ -1,10 +1,10 @@
import { ReactNode } from 'react';
import { MockedProvider } from '@apollo/client/testing';
import { act, renderHook } from '@testing-library/react';
import { ReactNode } from 'react';
import { RecoilRoot } from 'recoil';
import { useCreateOneRelationMetadataItem } from '@/object-metadata/hooks/useCreateOneRelationMetadataItem';
import { RelationMetadataType } from '~/generated/graphql';
import { RelationDefinitionType } from '~/generated/graphql';
import {
query,
@ -42,7 +42,7 @@ describe('useCreateOneRelationMetadataItem', () => {
await act(async () => {
const res = await result.current.createOneRelationMetadataItem({
relationType: RelationMetadataType.OneToOne,
relationType: RelationDefinitionType.OneToOne,
field: {
label: 'label',
},

View File

@ -1,11 +1,7 @@
import { useRecoilCallback } from 'recoil';
import { objectMetadataItemFamilySelector } from '@/object-metadata/states/objectMetadataItemFamilySelector';
import { RelationType } from '@/settings/data-model/types/RelationType';
import {
FieldMetadataType,
RelationMetadataType,
} from '~/generated-metadata/graphql';
import { FieldMetadataType } from '~/generated-metadata/graphql';
import { FieldMetadataItem } from '../types/FieldMetadataItem';
@ -17,39 +13,19 @@ export const useGetRelationMetadata = () =>
}: {
fieldMetadataItem: Pick<
FieldMetadataItem,
'fromRelationMetadata' | 'toRelationMetadata' | 'type'
'type' | 'relationDefinition'
>;
}) => {
if (fieldMetadataItem.type !== FieldMetadataType.Relation) return null;
const relationMetadata =
fieldMetadataItem.fromRelationMetadata ||
fieldMetadataItem.toRelationMetadata;
const relationDefinition = fieldMetadataItem.relationDefinition;
if (!relationMetadata) return null;
const relationFieldMetadataId =
'toFieldMetadataId' in relationMetadata
? relationMetadata.toFieldMetadataId
: relationMetadata.fromFieldMetadataId;
if (!relationFieldMetadataId) return null;
const relationType =
relationMetadata.relationType === RelationMetadataType.OneToMany &&
fieldMetadataItem.toRelationMetadata
? 'MANY_TO_ONE'
: (relationMetadata.relationType as RelationType);
const relationObjectMetadataNameSingular =
'toObjectMetadata' in relationMetadata
? relationMetadata.toObjectMetadata.nameSingular
: relationMetadata.fromObjectMetadata.nameSingular;
if (!relationDefinition) return null;
const relationObjectMetadataItem = snapshot
.getLoadable(
objectMetadataItemFamilySelector({
objectName: relationObjectMetadataNameSingular,
objectName: relationDefinition.targetObjectMetadata.nameSingular,
objectNameType: 'singular',
}),
)
@ -59,7 +35,7 @@ export const useGetRelationMetadata = () =>
const relationFieldMetadataItem =
relationObjectMetadataItem.fields.find(
(field) => field.id === relationFieldMetadataId,
(field) => field.id === relationDefinition.targetFieldMetadata.id,
);
if (!relationFieldMetadataItem) return null;
@ -67,7 +43,7 @@ export const useGetRelationMetadata = () =>
return {
relationFieldMetadataItem,
relationObjectMetadataItem,
relationType,
relationType: relationDefinition.direction,
};
},
[],

View File

@ -3,7 +3,6 @@ import { ThemeColor } from 'twenty-ui';
import {
Field,
Object as MetadataObject,
Relation,
RelationDefinition,
RelationDefinitionType,
} from '~/generated-metadata/graphql';
@ -18,31 +17,9 @@ export type FieldMetadataItemOption = {
export type FieldMetadataItem = Omit<
Field,
| '__typename'
| 'fromRelationMetadata'
| 'toRelationMetadata'
| 'defaultValue'
| 'options'
| 'settings'
| 'relationDefinition'
'__typename' | 'defaultValue' | 'options' | 'settings' | 'relationDefinition'
> & {
__typename?: string;
fromRelationMetadata?:
| (Pick<Relation, 'id' | 'toFieldMetadataId' | 'relationType'> & {
toObjectMetadata: Pick<
Relation['toObjectMetadata'],
'id' | 'nameSingular' | 'namePlural' | 'isSystem' | 'isRemote'
>;
})
| null;
toRelationMetadata?:
| (Pick<Relation, 'id' | 'fromFieldMetadataId' | 'relationType'> & {
fromObjectMetadata: Pick<
Relation['fromObjectMetadata'],
'id' | 'nameSingular' | 'namePlural' | 'isSystem' | 'isRemote'
>;
})
| null;
defaultValue?: any;
options?: FieldMetadataItemOption[] | null;
relationDefinition?: {

View File

@ -1,5 +1,4 @@
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { parseFieldRelationType } from '@/object-metadata/utils/parseFieldRelationType';
import { FieldDefinition } from '@/object-record/record-field/types/FieldDefinition';
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
import { getFieldButtonIcon } from '@/object-record/record-field/utils/getFieldButtonIcon';
@ -20,17 +19,15 @@ export const formatFieldMetadataItemAsFieldDefinition = ({
labelWidth,
}: FieldMetadataItemAsFieldDefinitionProps): FieldDefinition<FieldMetadata> => {
const relationObjectMetadataItem =
field.toRelationMetadata?.fromObjectMetadata ||
field.fromRelationMetadata?.toObjectMetadata;
field.relationDefinition?.targetObjectMetadata;
const relationFieldMetadataId =
field.toRelationMetadata?.fromFieldMetadataId ||
field.fromRelationMetadata?.toFieldMetadataId;
field.relationDefinition?.targetFieldMetadata.id;
const fieldDefintionMetadata = {
fieldName: field.name,
placeHolder: field.label,
relationType: parseFieldRelationType(field),
relationType: field.relationDefinition?.direction,
relationFieldMetadataId,
relationObjectMetadataNameSingular:
relationObjectMetadataItem?.nameSingular ?? '',

View File

@ -2,6 +2,7 @@ import { RelationType } from '@/settings/data-model/types/RelationType';
import {
CreateRelationInput,
Field,
RelationDefinitionType,
RelationMetadataType,
} from '~/generated-metadata/graphql';
@ -24,8 +25,8 @@ export const formatRelationMetadataInput = (
// => Transform into ONE_TO_MANY and invert "from" and "to" data.
const isManyToOne = input.relationType === 'MANY_TO_ONE';
const relationType = isManyToOne
? RelationMetadataType.OneToMany
: (input.relationType as RelationMetadataType);
? RelationDefinitionType.OneToMany
: (input.relationType as RelationDefinitionType);
const { field: fromField, objectMetadataId: fromObjectMetadataId } =
isManyToOne ? input.connect : input;
const { field: toField, objectMetadataId: toObjectMetadataId } = isManyToOne
@ -51,7 +52,7 @@ export const formatRelationMetadataInput = (
fromLabel,
fromName,
fromObjectMetadataId,
relationType,
relationType: relationType as unknown as RelationMetadataType,
toDescription,
toIcon,
toLabel,

View File

@ -4,7 +4,7 @@ import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { mapObjectMetadataToGraphQLQuery } from '@/object-metadata/utils/mapObjectMetadataToGraphQLQuery';
import {
FieldMetadataType,
RelationMetadataType,
RelationDefinitionType,
} from '~/generated-metadata/graphql';
import { FieldMetadataItem } from '../types/FieldMetadataItem';
@ -17,10 +17,7 @@ export const mapFieldMetadataToGraphQLQuery = ({
computeReferences = false,
}: {
objectMetadataItems: ObjectMetadataItem[];
field: Pick<
FieldMetadataItem,
'name' | 'type' | 'toRelationMetadata' | 'fromRelationMetadata'
>;
field: Pick<FieldMetadataItem, 'name' | 'type' | 'relationDefinition'>;
relationrecordFields?: Record<string, any>;
computeReferences?: boolean;
}): any => {
@ -49,12 +46,12 @@ export const mapFieldMetadataToGraphQLQuery = ({
if (
fieldType === FieldMetadataType.Relation &&
field.toRelationMetadata?.relationType === RelationMetadataType.OneToMany
field.relationDefinition?.direction === RelationDefinitionType.ManyToOne
) {
const relationMetadataItem = objectMetadataItems.find(
(objectMetadataItem) =>
objectMetadataItem.id ===
(field.toRelationMetadata as any)?.fromObjectMetadata?.id,
field.relationDefinition?.targetObjectMetadata.id,
);
if (isUndefined(relationMetadataItem)) {
@ -73,12 +70,12 @@ ${mapObjectMetadataToGraphQLQuery({
if (
fieldType === FieldMetadataType.Relation &&
field.fromRelationMetadata?.relationType === RelationMetadataType.OneToMany
field.relationDefinition?.direction === RelationDefinitionType.OneToMany
) {
const relationMetadataItem = objectMetadataItems.find(
(objectMetadataItem) =>
objectMetadataItem.id ===
(field.fromRelationMetadata as any)?.toObjectMetadata?.id,
field.relationDefinition?.targetObjectMetadata.id,
);
if (isUndefined(relationMetadataItem)) {

View File

@ -1,55 +0,0 @@
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { FieldDefinitionRelationType } from '@/object-record/record-field/types/FieldDefinition';
import {
FieldMetadataType,
RelationMetadataType,
} from '~/generated-metadata/graphql';
import { isDefined } from '~/utils/isDefined';
export const parseFieldRelationType = (
field: FieldMetadataItem | undefined,
): FieldDefinitionRelationType | undefined => {
if (!field || field.type !== FieldMetadataType.Relation) return;
const config: Record<
RelationMetadataType,
{ from: FieldDefinitionRelationType; to: FieldDefinitionRelationType }
> = {
[RelationMetadataType.ManyToMany]: {
from: 'FROM_MANY_OBJECTS',
to: 'TO_MANY_OBJECTS',
},
[RelationMetadataType.OneToMany]: {
from: 'FROM_MANY_OBJECTS',
to: 'TO_ONE_OBJECT',
},
[RelationMetadataType.ManyToOne]: {
from: 'TO_ONE_OBJECT',
to: 'FROM_MANY_OBJECTS',
},
[RelationMetadataType.OneToOne]: {
from: 'FROM_ONE_OBJECT',
to: 'TO_ONE_OBJECT',
},
};
if (
isDefined(field.fromRelationMetadata) &&
field.fromRelationMetadata.relationType in config
) {
return config[field.fromRelationMetadata.relationType].from;
}
if (
isDefined(field.toRelationMetadata) &&
field.toRelationMetadata.relationType in config
) {
return config[field.toRelationMetadata.relationType].to;
}
throw new Error(
`Cannot determine field relation type for field : ${JSON.stringify(
field,
)}.`,
);
};

View File

@ -6,7 +6,6 @@ import { metadataLabelSchema } from '@/object-metadata/validation-schemas/metada
import {
FieldMetadataType,
RelationDefinitionType,
RelationMetadataType,
} from '~/generated-metadata/graphql';
import { camelCaseStringSchema } from '~/utils/validation-schemas/camelCaseStringSchema';
@ -16,24 +15,6 @@ export const fieldMetadataItemSchema = (existingLabels?: string[]) => {
createdAt: z.string().datetime(),
defaultValue: z.any().optional(),
description: z.string().trim().nullable().optional(),
fromRelationMetadata: z
.object({
__typename: z.literal('relation').optional(),
id: z.string().uuid(),
relationType: z.nativeEnum(RelationMetadataType),
toFieldMetadataId: z.string().uuid(),
toObjectMetadata: z.object({
__typename: z.literal('object').optional(),
dataSourceId: z.string().uuid(),
id: z.string().uuid(),
isRemote: z.boolean(),
isSystem: z.boolean(),
namePlural: z.string().trim().min(1),
nameSingular: z.string().trim().min(1),
}),
})
.nullable()
.optional(),
icon: z.string().startsWith('Icon').trim().nullable(),
id: z.string().uuid(),
isActive: z.boolean(),
@ -84,24 +65,6 @@ export const fieldMetadataItemSchema = (existingLabels?: string[]) => {
})
.nullable()
.optional(),
toRelationMetadata: z
.object({
__typename: z.literal('relation').optional(),
id: z.string().uuid(),
relationType: z.nativeEnum(RelationMetadataType),
fromFieldMetadataId: z.string().uuid(),
fromObjectMetadata: z.object({
__typename: z.literal('object').optional(),
id: z.string().uuid(),
dataSourceId: z.string().uuid(),
isRemote: z.boolean(),
isSystem: z.boolean(),
namePlural: z.string().trim().min(1),
nameSingular: z.string().trim().min(1),
}),
})
.nullable()
.optional(),
type: z.nativeEnum(FieldMetadataType),
updatedAt: z.string().datetime(),
}) satisfies z.ZodType<FieldMetadataItem>;

View File

@ -4,17 +4,6 @@ import { FieldMetadataType } from '~/generated-metadata/graphql';
import { FieldMetadata } from './FieldMetadata';
export type FieldDefinitionRelationType =
| 'FROM_MANY_OBJECTS'
| 'FROM_ONE_OBJECT'
| 'TO_MANY_OBJECTS'
| 'TO_ONE_OBJECT';
export type RelationDirections = {
from: FieldDefinitionRelationType;
to: FieldDefinitionRelationType;
};
export type FieldDefinition<T extends FieldMetadata> = {
fieldMetadataId: string;
label: string;

View File

@ -3,8 +3,8 @@ import { ThemeColor } from 'twenty-ui';
import { RATING_VALUES } from '@/object-record/record-field/meta-types/constants/RatingValues';
import { ZodHelperLiteral } from '@/object-record/record-field/types/ZodHelperLiteral';
import { EntityForSelect } from '@/object-record/relation-picker/types/EntityForSelect';
import { WithNarrowedStringLiteralProperty } from '~/types/WithNarrowedStringLiteralProperty';
import { RelationDefinitionType } from '~/generated-metadata/graphql';
import { CurrencyCode } from './CurrencyCode';
export type FieldUuidMetadata = {
@ -110,35 +110,17 @@ export type FieldPositionMetadata = {
fieldName: string;
};
export type FieldDefinitionRelationType =
| 'FROM_MANY_OBJECTS'
| 'FROM_ONE_OBJECT'
| 'TO_MANY_OBJECTS'
| 'TO_ONE_OBJECT';
export type FieldRelationMetadata = {
fieldName: string;
objectMetadataNameSingular?: string;
relationFieldMetadataId: string;
relationObjectMetadataNamePlural: string;
relationObjectMetadataNameSingular: string;
relationType?: FieldDefinitionRelationType;
relationType?: RelationDefinitionType;
targetFieldMetadataName?: string;
useEditButton?: boolean;
};
export type FieldRelationOneMetadata = WithNarrowedStringLiteralProperty<
FieldRelationMetadata,
'relationType',
'TO_ONE_OBJECT'
>;
export type FieldRelationManyMetadata = WithNarrowedStringLiteralProperty<
FieldRelationMetadata,
'relationType',
'FROM_MANY_OBJECTS'
>;
export type FieldSelectMetadata = {
objectMetadataNameSingular?: string;
fieldName: string;

View File

@ -1,9 +1,11 @@
import { isFieldRelation } from '@/object-record/record-field/types/guards/isFieldRelation';
import { RelationDefinitionType } from '~/generated-metadata/graphql';
import { FieldDefinition } from '../FieldDefinition';
import { FieldMetadata, FieldRelationManyMetadata } from '../FieldMetadata';
import { FieldMetadata } from '../FieldMetadata';
export const isFieldRelationFromManyObjects = (
field: Pick<FieldDefinition<FieldMetadata>, 'type' | 'metadata'>,
): field is FieldDefinition<FieldRelationManyMetadata> =>
isFieldRelation(field) && field.metadata.relationType === 'FROM_MANY_OBJECTS';
): field is FieldDefinition<FieldMetadata> =>
isFieldRelation(field) &&
field.metadata.relationType === RelationDefinitionType.OneToMany;

View File

@ -1,9 +1,11 @@
import { isFieldRelation } from '@/object-record/record-field/types/guards/isFieldRelation';
import { RelationDefinitionType } from '~/generated-metadata/graphql';
import { FieldDefinition } from '../FieldDefinition';
import { FieldMetadata, FieldRelationOneMetadata } from '../FieldMetadata';
import { FieldMetadata } from '../FieldMetadata';
export const isFieldRelationToOneObject = (
field: Pick<FieldDefinition<FieldMetadata>, 'type' | 'metadata'>,
): field is FieldDefinition<FieldRelationOneMetadata> =>
isFieldRelation(field) && field.metadata.relationType === 'TO_ONE_OBJECT';
): field is FieldDefinition<FieldMetadata> =>
isFieldRelation(field) &&
field.metadata.relationType === RelationDefinitionType.ManyToOne;

View File

@ -1,6 +1,7 @@
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
import { RelationDefinitionType } from '~/generated-metadata/graphql';
import {
csvDownloader,
displayedExportProgress,
@ -35,7 +36,10 @@ describe('generateCsv', () => {
{ label: 'Nested', metadata: { fieldName: 'nested' } },
{
label: 'Relation',
metadata: { fieldName: 'relation', relationType: 'TO_ONE_OBJECT' },
metadata: {
fieldName: 'relation',
relationType: RelationDefinitionType.ManyToOne,
},
},
] as ColumnDefinition<FieldMetadata>[];
const rows = [

View File

@ -9,6 +9,7 @@ import {
} from '@/object-record/record-index/options/hooks/useTableData';
import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { RelationDefinitionType } from '~/generated-metadata/graphql';
import { FieldMetadataType } from '~/generated/graphql';
import { isDefined } from '~/utils/isDefined';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
@ -43,7 +44,7 @@ export const generateCsv: GenerateExport = ({
const columnsToExport = columns.filter(
(col) =>
!('relationType' in col.metadata && col.metadata.relationType) ||
col.metadata.relationType === 'TO_ONE_OBJECT',
col.metadata.relationType === RelationDefinitionType.ManyToOne,
);
const objectIdColumn: ColumnDefinition<FieldMetadata> = {

View File

@ -7,6 +7,7 @@ import { Task } from '@/activities/types/Task';
import { InformationBannerDeletedRecord } from '@/information-banner/components/deleted-record/InformationBannerDeletedRecord';
import { useLabelIdentifierFieldMetadataItem } from '@/object-metadata/hooks/useLabelIdentifierFieldMetadataItem';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { formatFieldMetadataItemAsColumnDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsColumnDefinition';
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
@ -56,6 +57,8 @@ export const RecordShowContainer = ({
objectNameSingular,
});
const { objectMetadataItems } = useObjectMetadataItems();
const { labelIdentifierFieldMetadataItem } =
useLabelIdentifierFieldMetadataItem({
objectNameSingular,
@ -119,7 +122,7 @@ export const RecordShowContainer = ({
const availableFieldMetadataItems = objectMetadataItem.fields
.filter(
(fieldMetadataItem) =>
isFieldCellSupported(fieldMetadataItem) &&
isFieldCellSupported(fieldMetadataItem, objectMetadataItems) &&
fieldMetadataItem.id !== labelIdentifierFieldMetadataItem?.id,
)
.sort((fieldMetadataItemA, fieldMetadataItemB) =>

View File

@ -1,7 +1,7 @@
import { useCallback, useContext } from 'react';
import { css } from '@emotion/react';
import styled from '@emotion/styled';
import { motion } from 'framer-motion';
import { useCallback, useContext } from 'react';
import {
IconChevronDown,
IconComponent,
@ -11,6 +11,7 @@ import {
} from 'twenty-ui';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { formatFieldMetadataItemAsColumnDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsColumnDefinition';
import { RecordChip } from '@/object-record/components/RecordChip';
@ -37,6 +38,7 @@ import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { DropdownScope } from '@/ui/layout/dropdown/scopes/DropdownScope';
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
import { AnimatedEaseInOut } from '@/ui/utilities/animation/components/AnimatedEaseInOut';
import { RelationDefinitionType } from '~/generated-metadata/graphql';
const StyledListItem = styled(RecordDetailRecordsListItem)<{
isDropdownOpen?: boolean;
@ -89,12 +91,14 @@ export const RecordDetailRelationRecordsListItem = ({
relationType,
} = fieldDefinition.metadata as FieldRelationMetadata;
const isToOneObject = relationType === 'TO_ONE_OBJECT';
const isToOneObject = relationType === RelationDefinitionType.ManyToOne;
const { objectMetadataItem: relationObjectMetadataItem } =
useObjectMetadataItem({
objectNameSingular: relationObjectMetadataNameSingular,
});
const { objectMetadataItems } = useObjectMetadataItems();
const persistField = usePersistField();
const { updateOneRecord: updateOneRelationRecord } = useUpdateOneRecord({
@ -111,7 +115,7 @@ export const RecordDetailRelationRecordsListItem = ({
const availableRelationFieldMetadataItems = relationObjectMetadataItem.fields
.filter(
(fieldMetadataItem) =>
isFieldCellSupported(fieldMetadataItem) &&
isFieldCellSupported(fieldMetadataItem, objectMetadataItems) &&
fieldMetadataItem.id !==
relationObjectMetadataItem.labelIdentifierFieldMetadataId &&
fieldMetadataItem.id !== relationFieldMetadataId,

View File

@ -32,6 +32,7 @@ import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { DropdownScope } from '@/ui/layout/dropdown/scopes/DropdownScope';
import { FilterQueryParams } from '@/views/hooks/internal/useViewFromQueryParams';
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
import { RelationDefinitionType } from '~/generated-metadata/graphql';
type RecordDetailRelationSectionProps = {
loading: boolean;
@ -67,8 +68,8 @@ export const RecordDetailRelationSection = ({
>(recordStoreFamilySelector({ recordId, fieldName }));
// TODO: use new relation type
const isToOneObject = relationType === 'TO_ONE_OBJECT';
const isFromManyObjects = relationType === 'FROM_MANY_OBJECTS';
const isToOneObject = relationType === RelationDefinitionType.ManyToOne;
const isToManyObjects = RelationDefinitionType.OneToMany;
const relationRecords: ObjectRecord[] =
fieldValue && isToOneObject
@ -160,7 +161,7 @@ export const RecordDetailRelationSection = ({
<RecordDetailSectionHeader
title={fieldDefinition.label}
link={
isFromManyObjects
isToManyObjects
? {
to: filterLinkHref,
label: `All (${relationRecords.length})`,

View File

@ -34,22 +34,6 @@ export const mockPerformance = {
isNullable: true,
createdAt: '2024-05-16T10:54:27.788Z',
updatedAt: '2024-05-16T10:54:27.788Z',
fromRelationMetadata: null,
toRelationMetadata: {
__typename: 'relation',
id: '0cf72416-3d94-4d94-abf3-7dc9d734435b',
relationType: 'ONE_TO_MANY',
fromObjectMetadata: {
__typename: 'object',
id: '79c2d29c-76f6-432f-91c9-df1259b73d95',
dataSourceId: '0fd9fd54-0e8d-4f78-911c-76b33436a768',
nameSingular: 'company',
namePlural: 'companies',
isSystem: false,
isRemote: false,
},
fromFieldMetadataId: '7b281010-5f47-4771-b3f5-f4bcd24ed1b5',
},
defaultValue: null,
options: null,
relationDefinition: {
@ -94,8 +78,6 @@ export const mockPerformance = {
isNullable: false,
createdAt: '2024-05-16T10:54:27.788Z',
updatedAt: '2024-05-16T10:54:27.788Z',
fromRelationMetadata: null,
toRelationMetadata: null,
defaultValue: "''",
options: null,
relationDefinition: null,
@ -114,8 +96,6 @@ export const mockPerformance = {
isNullable: false,
createdAt: '2024-05-16T10:54:27.788Z',
updatedAt: '2024-05-16T10:54:27.788Z',
fromRelationMetadata: null,
toRelationMetadata: null,
defaultValue: "''",
options: null,
relationDefinition: null,
@ -134,22 +114,6 @@ export const mockPerformance = {
isNullable: true,
createdAt: '2024-05-16T10:54:27.788Z',
updatedAt: '2024-05-16T10:54:27.788Z',
fromRelationMetadata: {
__typename: 'relation',
id: 'd76f949d-023d-4b45-a71e-f39e3b1562ba',
relationType: 'ONE_TO_MANY',
toObjectMetadata: {
__typename: 'object',
id: '82222ca2-dd40-44ec-b8c5-eb0eca9ec625',
dataSourceId: '0fd9fd54-0e8d-4f78-911c-76b33436a768',
nameSingular: 'activityTarget',
namePlural: 'activityTargets',
isSystem: true,
isRemote: false,
},
toFieldMetadataId: 'f5f515cc-6d8a-44c3-b2d4-f04b9868a9c5',
},
toRelationMetadata: null,
defaultValue: null,
options: null,
relationDefinition: {
@ -194,22 +158,6 @@ export const mockPerformance = {
isNullable: true,
createdAt: '2024-05-16T10:54:27.788Z',
updatedAt: '2024-05-16T10:54:27.788Z',
fromRelationMetadata: {
__typename: 'relation',
id: 'a5a61d23-8ac9-4014-9441-ec3a1781a661',
relationType: 'ONE_TO_MANY',
toObjectMetadata: {
__typename: 'object',
id: '494b9b7c-a44e-4d52-b274-cdfb0e322165',
dataSourceId: '0fd9fd54-0e8d-4f78-911c-76b33436a768',
nameSingular: 'opportunity',
namePlural: 'opportunities',
isSystem: false,
isRemote: false,
},
toFieldMetadataId: '86559a6f-6afc-4d5c-9bed-fc74d063791b',
},
toRelationMetadata: null,
defaultValue: null,
options: null,
relationDefinition: {
@ -254,22 +202,6 @@ export const mockPerformance = {
isNullable: true,
createdAt: '2024-05-16T10:54:27.788Z',
updatedAt: '2024-05-16T10:54:27.788Z',
fromRelationMetadata: {
__typename: 'relation',
id: '456f7875-b48c-4795-a0c7-a69d7339afee',
relationType: 'ONE_TO_MANY',
toObjectMetadata: {
__typename: 'object',
id: 'eba13fca-57b7-470c-8c23-a0e640e04ffb',
dataSourceId: '0fd9fd54-0e8d-4f78-911c-76b33436a768',
nameSingular: 'calendarEventParticipant',
namePlural: 'calendarEventParticipants',
isSystem: true,
isRemote: false,
},
toFieldMetadataId: 'c1cdebda-b514-4487-9b9c-aa59d8fca8eb',
},
toRelationMetadata: null,
defaultValue: null,
options: null,
relationDefinition: {
@ -314,8 +246,6 @@ export const mockPerformance = {
isNullable: false,
createdAt: '2024-05-16T10:54:27.788Z',
updatedAt: '2024-05-16T10:54:27.788Z',
fromRelationMetadata: null,
toRelationMetadata: null,
defaultValue: 'now',
options: null,
relationDefinition: null,
@ -334,22 +264,6 @@ export const mockPerformance = {
isNullable: true,
createdAt: '2024-05-16T10:54:27.788Z',
updatedAt: '2024-05-16T10:54:27.788Z',
fromRelationMetadata: {
__typename: 'relation',
id: '31542774-fb15-4d01-b00b-8fc94887f458',
relationType: 'ONE_TO_MANY',
toObjectMetadata: {
__typename: 'object',
id: 'f08422e2-14cd-4966-9cd3-bce0302cc56f',
dataSourceId: '0fd9fd54-0e8d-4f78-911c-76b33436a768',
nameSingular: 'favorite',
namePlural: 'favorites',
isSystem: true,
isRemote: false,
},
toFieldMetadataId: '67d28b17-ff3c-49b4-a6da-1354be9634b0',
},
toRelationMetadata: null,
defaultValue: null,
options: null,
relationDefinition: {
@ -394,8 +308,6 @@ export const mockPerformance = {
isNullable: true,
createdAt: '2024-05-16T10:54:27.788Z',
updatedAt: '2024-05-16T10:54:27.788Z',
fromRelationMetadata: null,
toRelationMetadata: null,
defaultValue: {
primaryLinkUrl: "''",
primaryLinkLabel: "''",
@ -417,22 +329,6 @@ export const mockPerformance = {
isNullable: true,
createdAt: '2024-05-16T10:54:27.788Z',
updatedAt: '2024-05-16T10:54:27.788Z',
fromRelationMetadata: {
__typename: 'relation',
id: 'c0cc3456-afa4-46e0-820d-2db0b63a8273',
relationType: 'ONE_TO_MANY',
toObjectMetadata: {
__typename: 'object',
id: '0e3c9a9d-8a60-4671-a466-7b840a422da2',
dataSourceId: '0fd9fd54-0e8d-4f78-911c-76b33436a768',
nameSingular: 'attachment',
namePlural: 'attachments',
isSystem: true,
isRemote: false,
},
toFieldMetadataId: 'a920a0d6-8e71-4ab8-90b9-ab540e04732a',
},
toRelationMetadata: null,
defaultValue: null,
options: null,
relationDefinition: {
@ -477,8 +373,6 @@ export const mockPerformance = {
isNullable: false,
createdAt: '2024-05-16T10:54:27.788Z',
updatedAt: '2024-05-16T10:54:27.788Z',
fromRelationMetadata: null,
toRelationMetadata: null,
defaultValue: "''",
options: null,
relationDefinition: null,
@ -497,8 +391,6 @@ export const mockPerformance = {
isNullable: true,
createdAt: '2024-05-16T10:54:27.788Z',
updatedAt: '2024-05-16T10:54:27.788Z',
fromRelationMetadata: null,
toRelationMetadata: null,
defaultValue: null,
options: null,
relationDefinition: null,
@ -517,8 +409,6 @@ export const mockPerformance = {
isNullable: false,
createdAt: '2024-05-16T10:54:27.788Z',
updatedAt: '2024-05-16T10:54:27.788Z',
fromRelationMetadata: null,
toRelationMetadata: null,
defaultValue: "''",
options: null,
relationDefinition: null,
@ -537,8 +427,6 @@ export const mockPerformance = {
isNullable: false,
createdAt: '2024-05-16T10:54:27.788Z',
updatedAt: '2024-05-16T10:54:27.788Z',
fromRelationMetadata: null,
toRelationMetadata: null,
defaultValue: "''",
options: null,
relationDefinition: null,
@ -557,8 +445,6 @@ export const mockPerformance = {
isNullable: false,
createdAt: '2024-05-16T10:54:27.788Z',
updatedAt: '2024-05-16T10:54:27.788Z',
fromRelationMetadata: null,
toRelationMetadata: null,
defaultValue: 'now',
options: null,
relationDefinition: null,
@ -577,8 +463,6 @@ export const mockPerformance = {
isNullable: true,
createdAt: '2024-05-16T10:54:27.788Z',
updatedAt: '2024-05-16T10:54:27.788Z',
fromRelationMetadata: null,
toRelationMetadata: null,
defaultValue: null,
options: null,
relationDefinition: null,
@ -597,22 +481,6 @@ export const mockPerformance = {
isNullable: true,
createdAt: '2024-05-16T10:54:27.788Z',
updatedAt: '2024-05-16T10:54:27.788Z',
fromRelationMetadata: {
__typename: 'relation',
id: '25150feb-fcd7-407e-b5fa-ffe58a0450ac',
relationType: 'ONE_TO_MANY',
toObjectMetadata: {
__typename: 'object',
id: '83b5ff3e-975e-4dc9-ba4d-c645a0d8afb2',
dataSourceId: '0fd9fd54-0e8d-4f78-911c-76b33436a768',
nameSingular: 'timelineActivity',
namePlural: 'timelineActivities',
isSystem: true,
isRemote: false,
},
toFieldMetadataId: '556a12d4-ef0a-4232-963f-0f317f4c5ef5',
},
toRelationMetadata: null,
defaultValue: null,
options: null,
relationDefinition: {
@ -657,8 +525,6 @@ export const mockPerformance = {
isNullable: true,
createdAt: '2024-05-16T10:54:27.788Z',
updatedAt: '2024-05-16T10:54:27.788Z',
fromRelationMetadata: null,
toRelationMetadata: null,
defaultValue: {
lastName: "''",
firstName: "''",
@ -680,8 +546,6 @@ export const mockPerformance = {
isNullable: true,
createdAt: '2024-05-16T10:54:27.788Z',
updatedAt: '2024-05-16T10:54:27.788Z',
fromRelationMetadata: null,
toRelationMetadata: null,
defaultValue: {
primaryLinkUrl: "''",
primaryLinkLabel: "''",
@ -703,22 +567,6 @@ export const mockPerformance = {
isNullable: true,
createdAt: '2024-05-16T10:54:27.788Z',
updatedAt: '2024-05-16T10:54:27.788Z',
fromRelationMetadata: {
__typename: 'relation',
id: 'e2eb7156-6e65-4bf8-922b-670179744f27',
relationType: 'ONE_TO_MANY',
toObjectMetadata: {
__typename: 'object',
id: 'ffd8e640-84b7-4ed6-99e9-14def0f9d82b',
dataSourceId: '0fd9fd54-0e8d-4f78-911c-76b33436a768',
nameSingular: 'messageParticipant',
namePlural: 'messageParticipants',
isSystem: true,
isRemote: false,
},
toFieldMetadataId: '8c4593a1-ad40-4681-92fe-43ad4fe60205',
},
toRelationMetadata: null,
defaultValue: null,
options: null,
relationDefinition: {
@ -763,8 +611,6 @@ export const mockPerformance = {
isNullable: false,
createdAt: '2024-05-16T10:54:27.788Z',
updatedAt: '2024-05-16T10:54:27.788Z',
fromRelationMetadata: null,
toRelationMetadata: null,
defaultValue: 'uuid',
options: null,
relationDefinition: null,

View File

@ -6,7 +6,10 @@ import { useOpenSpreadsheetImportDialog } from '@/spreadsheet-import/hooks/useOp
import { SpreadsheetImportDialogOptions } from '@/spreadsheet-import/types';
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { FieldMetadataType } from '~/generated-metadata/graphql';
import {
FieldMetadataType,
RelationDefinitionType,
} from '~/generated-metadata/graphql';
export const useOpenObjectRecordsSpreasheetImportDialog = (
objectNameSingular: string,
@ -37,7 +40,8 @@ export const useOpenObjectRecordsSpreasheetImportDialog = (
(!fieldMetadataItem.isSystem || fieldMetadataItem.name === 'id') &&
fieldMetadataItem.name !== 'createdAt' &&
(fieldMetadataItem.type !== FieldMetadataType.Relation ||
fieldMetadataItem.toRelationMetadata),
fieldMetadataItem.relationDefinition?.direction ===
RelationDefinitionType.ManyToOne),
)
.sort((fieldMetadataItemA, fieldMetadataItemB) =>
fieldMetadataItemA.name.localeCompare(fieldMetadataItemB.name),

View File

@ -2,14 +2,14 @@ import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata'
import { isFieldRelation } from '@/object-record/record-field/types/guards/isFieldRelation';
import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
import { TABLE_COLUMNS_DENY_LIST } from '@/object-record/relation-picker/constants/TableColumnsDenyList';
import { RelationDefinitionType } from '~/generated-metadata/graphql';
export const filterAvailableTableColumns = (
columnDefinition: ColumnDefinition<FieldMetadata>,
): boolean => {
if (
isFieldRelation(columnDefinition) &&
columnDefinition.metadata?.relationType !== 'TO_ONE_OBJECT' &&
columnDefinition.metadata?.relationType !== 'FROM_MANY_OBJECTS'
columnDefinition.metadata?.relationType !== RelationDefinitionType.ManyToOne
) {
return false;
}

View File

@ -4,10 +4,7 @@ import { generateEmptyFieldValue } from '@/object-record/utils/generateEmptyFiel
import { v4 } from 'uuid';
export const generateDefaultFieldValue = (
fieldMetadataItem: Pick<
FieldMetadataItem,
'defaultValue' | 'type' | 'fromRelationMetadata'
>,
fieldMetadataItem: Pick<FieldMetadataItem, 'defaultValue' | 'type'>,
) => {
const defaultValue = isFieldValueEmpty({
fieldValue: fieldMetadataItem.defaultValue,

View File

@ -1,10 +1,11 @@
import { isNonEmptyString } from '@sniptt/guards';
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { FieldMetadataType } from '~/generated-metadata/graphql';
import {
FieldMetadataType,
RelationDefinitionType,
} from '~/generated-metadata/graphql';
export const generateEmptyFieldValue = (
fieldMetadataItem: Pick<FieldMetadataItem, 'type' | 'fromRelationMetadata'>,
fieldMetadataItem: Pick<FieldMetadataItem, 'type' | 'relationDefinition'>,
) => {
switch (fieldMetadataItem.type) {
case FieldMetadataType.Email:
@ -62,10 +63,8 @@ export const generateEmptyFieldValue = (
}
case FieldMetadataType.Relation: {
if (
!isNonEmptyString(
fieldMetadataItem.fromRelationMetadata?.toObjectMetadata
?.nameSingular,
)
fieldMetadataItem.relationDefinition?.direction ===
RelationDefinitionType.ManyToOne
) {
return null;
}

View File

@ -1,12 +1,16 @@
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { isObjectMetadataAvailableForRelation } from '@/object-metadata/utils/isObjectMetadataAvailableForRelation';
import {
FieldMetadataType,
RelationMetadataType,
RelationDefinitionType,
} from '~/generated-metadata/graphql';
export const isFieldCellSupported = (fieldMetadataItem: FieldMetadataItem) => {
export const isFieldCellSupported = (
fieldMetadataItem: FieldMetadataItem,
objectMetadataItems: ObjectMetadataItem[],
) => {
if (
[
FieldMetadataType.Uuid,
@ -18,17 +22,17 @@ export const isFieldCellSupported = (fieldMetadataItem: FieldMetadataItem) => {
}
if (fieldMetadataItem.type === FieldMetadataType.Relation) {
const relationMetadata =
fieldMetadataItem.fromRelationMetadata ??
fieldMetadataItem.toRelationMetadata;
const relationObjectMetadataItem =
fieldMetadataItem.fromRelationMetadata?.toObjectMetadata ??
fieldMetadataItem.toRelationMetadata?.fromObjectMetadata;
const relationObjectMetadataItemId =
fieldMetadataItem.relationDefinition?.targetObjectMetadata.id;
const relationObjectMetadataItem = objectMetadataItems.find(
(item) => item.id === relationObjectMetadataItemId,
);
// Hack to display targets on Notes and Tasks
if (
fieldMetadataItem.fromRelationMetadata?.toObjectMetadata?.nameSingular ===
CoreObjectNameSingular.NoteTarget &&
fieldMetadataItem.relationDefinition?.targetObjectMetadata
?.nameSingular === CoreObjectNameSingular.NoteTarget &&
fieldMetadataItem.relationDefinition?.sourceObjectMetadata
.nameSingular === CoreObjectNameSingular.Note
) {
@ -36,8 +40,8 @@ export const isFieldCellSupported = (fieldMetadataItem: FieldMetadataItem) => {
}
if (
fieldMetadataItem.fromRelationMetadata?.toObjectMetadata?.nameSingular ===
CoreObjectNameSingular.TaskTarget &&
fieldMetadataItem.relationDefinition?.targetObjectMetadata
?.nameSingular === CoreObjectNameSingular.TaskTarget &&
fieldMetadataItem.relationDefinition?.sourceObjectMetadata
.nameSingular === CoreObjectNameSingular.Task
) {
@ -45,9 +49,10 @@ export const isFieldCellSupported = (fieldMetadataItem: FieldMetadataItem) => {
}
if (
!relationMetadata ||
!fieldMetadataItem.relationDefinition ||
// TODO: Many to many relations are not supported yet.
relationMetadata.relationType === RelationMetadataType.ManyToMany ||
fieldMetadataItem.relationDefinition.direction ===
RelationDefinitionType.ManyToMany ||
!relationObjectMetadataItem ||
!isObjectMetadataAvailableForRelation(relationObjectMetadataItem)
) {

View File

@ -1,8 +1,8 @@
import { isString } from '@sniptt/guards';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { isFieldRelationToOneValue } from '@/object-record/record-field/types/guards/isFieldRelationToOneValue';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { RelationDefinitionType } from '~/generated-metadata/graphql';
import { FieldMetadataType } from '~/generated/graphql';
import { isDefined } from '~/utils/isDefined';
import { getUrlHostName } from '~/utils/url/getUrlHostName';
@ -29,7 +29,8 @@ export const sanitizeRecordInput = ({
if (
fieldMetadataItem.type === FieldMetadataType.Relation &&
isFieldRelationToOneValue(fieldValue)
fieldMetadataItem.relationDefinition?.direction ===
RelationDefinitionType.ManyToOne
) {
const relationIdFieldName = `${fieldMetadataItem.name}Id`;
const relationIdFieldMetadataItem = objectMetadataItem.fields.find(
@ -41,6 +42,14 @@ export const sanitizeRecordInput = ({
: undefined;
}
if (
fieldMetadataItem.type === FieldMetadataType.Relation &&
fieldMetadataItem.relationDefinition?.direction ===
RelationDefinitionType.OneToMany
) {
return undefined;
}
return [fieldName, fieldValue];
})
.filter(isDefined),

View File

@ -1,12 +1,12 @@
import {
IconComponent,
IconRelationManyToMany,
IconRelationManyToOne,
IconRelationOneToMany,
IconRelationOneToOne,
} from 'twenty-ui';
import { RelationMetadataType } from '~/generated-metadata/graphql';
import { RelationDefinitionType } from '~/generated-metadata/graphql';
import OneToManySvg from '../assets/OneToMany.svg';
import OneToOneSvg from '../assets/OneToOne.svg';
import { RelationType } from '../types/RelationType';
@ -20,20 +20,27 @@ export const RELATION_TYPES: Record<
isImageFlipped?: boolean;
}
> = {
[RelationMetadataType.OneToMany]: {
[RelationDefinitionType.OneToMany]: {
label: 'Has many',
Icon: IconRelationOneToMany,
imageSrc: OneToManySvg,
},
[RelationMetadataType.OneToOne]: {
[RelationDefinitionType.OneToOne]: {
label: 'Has one',
Icon: IconRelationOneToOne,
imageSrc: OneToOneSvg,
},
MANY_TO_ONE: {
[RelationDefinitionType.ManyToOne]: {
label: 'Belongs to one',
Icon: IconRelationManyToOne,
imageSrc: OneToManySvg,
isImageFlipped: true,
},
// Not supported yet
[RelationDefinitionType.ManyToMany]: {
label: 'Belongs to many',
Icon: IconRelationManyToMany,
imageSrc: OneToManySvg,
isImageFlipped: true,
},
};

View File

@ -1,5 +1,5 @@
import { Controller, useFormContext } from 'react-hook-form';
import styled from '@emotion/styled';
import { Controller, useFormContext } from 'react-hook-form';
import { useIcons } from 'twenty-ui';
import { z } from 'zod';
@ -14,6 +14,7 @@ import { RelationType } from '@/settings/data-model/types/RelationType';
import { IconPicker } from '@/ui/input/components/IconPicker';
import { Select } from '@/ui/input/components/Select';
import { TextInput } from '@/ui/input/components/TextInput';
import { RelationDefinitionType } from '~/generated-metadata/graphql';
export const settingsDataModelFieldRelationFormSchema = z.object({
relation: z.object({
@ -23,7 +24,10 @@ export const settingsDataModelFieldRelationFormSchema = z.object({
}),
objectMetadataId: z.string().uuid(),
type: z.enum(
Object.keys(RELATION_TYPES) as [RelationType, ...RelationType[]],
Object.keys(RELATION_TYPES) as [
RelationDefinitionType,
...RelationDefinitionType[],
],
),
}),
});
@ -33,10 +37,7 @@ export type SettingsDataModelFieldRelationFormValues = z.infer<
>;
type SettingsDataModelFieldRelationFormProps = {
fieldMetadataItem: Pick<
FieldMetadataItem,
'fromRelationMetadata' | 'toRelationMetadata' | 'type'
>;
fieldMetadataItem: Pick<FieldMetadataItem, 'type'>;
};
const StyledContainer = styled.div`

View File

@ -1,5 +1,5 @@
import { useFormContext } from 'react-hook-form';
import styled from '@emotion/styled';
import { useFormContext } from 'react-hook-form';
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';

View File

@ -5,15 +5,12 @@ import { useGetRelationMetadata } from '@/object-metadata/hooks/useGetRelationMe
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { isObjectMetadataAvailableForRelation } from '@/object-metadata/utils/isObjectMetadataAvailableForRelation';
import { RelationMetadataType } from '~/generated-metadata/graphql';
import { RelationDefinitionType } from '~/generated-metadata/graphql';
export const useRelationSettingsFormInitialValues = ({
fieldMetadataItem,
}: {
fieldMetadataItem?: Pick<
FieldMetadataItem,
'fromRelationMetadata' | 'toRelationMetadata' | 'type'
>;
fieldMetadataItem?: Pick<FieldMetadataItem, 'type' | 'relationDefinition'>;
}) => {
const { objectMetadataItems } = useFilteredObjectMetadataItems();
@ -39,7 +36,7 @@ export const useRelationSettingsFormInitialValues = ({
);
const initialRelationType =
relationTypeFromFieldMetadata ?? RelationMetadataType.OneToMany;
relationTypeFromFieldMetadata ?? RelationDefinitionType.OneToMany;
return {
disableFieldEdition:

View File

@ -1,7 +1,7 @@
import { useEffect } from 'react';
import { Edge, Node } from 'reactflow';
import dagre from '@dagrejs/dagre';
import { useTheme } from '@emotion/react';
import { useEffect } from 'react';
import { Edge, Node } from 'reactflow';
import { useRecoilValue } from 'recoil';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
@ -43,10 +43,10 @@ export const SettingsDataModelOverviewEffect = ({
for (const field of object.fields) {
if (
isDefined(field.toRelationMetadata) &&
isDefined(field.relationDefinition) &&
isDefined(
items.find(
(x) => x.id === field.toRelationMetadata?.fromObjectMetadata.id,
(x) => x.id === field.relationDefinition?.targetObjectMetadata.id,
),
)
) {
@ -59,8 +59,8 @@ export const SettingsDataModelOverviewEffect = ({
id: `${sourceObj}-${targetObj}`,
source: object.namePlural,
sourceHandle: `${field.id}-right`,
target: field.toRelationMetadata.fromObjectMetadata.namePlural,
targetHandle: `${field.toRelationMetadata.fromFieldMetadataId}-left`,
target: field.relationDefinition.targetObjectMetadata.namePlural,
targetHandle: `${field.relationDefinition.targetObjectMetadata}-left`,
type: 'smoothstep',
style: {
strokeWidth: 1,
@ -70,8 +70,8 @@ export const SettingsDataModelOverviewEffect = ({
markerStart: 'marker',
data: {
sourceField: field.id,
targetField: field.toRelationMetadata.fromFieldMetadataId,
relation: field.toRelationMetadata.relationType,
targetField: field.relationDefinition.targetFieldMetadata.id,
relation: field.relationDefinition.direction,
sourceObject: sourceObj,
targetObject: targetObj,
},

View File

@ -6,6 +6,7 @@ import { useIcons } from 'twenty-ui';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { RelationDefinitionType } from '~/generated-metadata/graphql';
type ObjectFieldRowProps = {
field: FieldMetadataItem;
@ -42,21 +43,33 @@ export const ObjectFieldRow = ({ field }: ObjectFieldRowProps) => {
{Icon && <Icon size={theme.icon.size.md} />}
<StyledFieldName>{relatedObject?.labelPlural ?? ''}</StyledFieldName>
<Handle
type={field.toRelationMetadata ? 'source' : 'target'}
type={
field.relationDefinition?.direction ===
RelationDefinitionType.OneToMany
? 'source'
: 'target'
}
position={Position.Right}
id={`${field.id}-right`}
className={
field.fromRelationMetadata
field.relationDefinition?.direction ===
RelationDefinitionType.OneToMany
? 'right-handle source-handle'
: 'right-handle target-handle'
}
/>
<Handle
type={field.toRelationMetadata ? 'source' : 'target'}
type={
field.relationDefinition?.direction ===
RelationDefinitionType.OneToMany
? 'source'
: 'target'
}
position={Position.Left}
id={`${field.id}-left`}
className={
field.fromRelationMetadata
field.relationDefinition?.direction ===
RelationDefinitionType.OneToMany
? 'left-handle source-handle'
: 'left-handle target-handle'
}

View File

@ -29,7 +29,7 @@ import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMe
import { View } from '@/views/types/View';
import { useNavigate } from 'react-router-dom';
import { useRecoilState } from 'recoil';
import { RelationMetadataType } from '~/generated-metadata/graphql';
import { RelationDefinitionType } from '~/generated-metadata/graphql';
import { SettingsObjectDetailTableItem } from '~/pages/settings/data-model/types/SettingsObjectDetailTableItem';
import { SettingsObjectFieldDataType } from './SettingsObjectFieldDataType';
@ -224,8 +224,8 @@ export const SettingsObjectFieldItemTableRow = ({
<SettingsObjectFieldDataType
Icon={RelationIcon}
label={
relationType === RelationMetadataType.ManyToOne ||
relationType === RelationMetadataType.OneToOne
relationType === RelationDefinitionType.ManyToOne ||
relationType === RelationDefinitionType.OneToOne
? relationObjectMetadataItem?.labelSingular
: relationObjectMetadataItem?.labelPlural
}

View File

@ -1,5 +1,3 @@
import { RelationMetadataType } from '~/generated-metadata/graphql';
import { RelationDefinitionType } from '~/generated-metadata/graphql';
export type RelationType =
| Exclude<RelationMetadataType, 'MANY_TO_MANY'>
| 'MANY_TO_ONE';
export type RelationType = RelationDefinitionType;

View File

@ -2,7 +2,10 @@ import { COMPANY_LABEL_IDENTIFIER_FIELD_METADATA_ID } from '@/object-metadata/ut
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
import { filterAvailableTableColumns } from '@/object-record/utils/filterAvailableTableColumns';
import { FieldMetadataType } from '~/generated-metadata/graphql';
import {
FieldMetadataType,
RelationDefinitionType,
} from '~/generated-metadata/graphql';
export const SIGN_IN_BACKGROUND_MOCK_COLUMN_DEFINITIONS = (
[
@ -65,7 +68,7 @@ export const SIGN_IN_BACKGROUND_MOCK_COLUMN_DEFINITIONS = (
type: FieldMetadataType.Relation,
metadata: {
fieldName: 'favorites',
relationType: 'FROM_MANY_OBJECTS',
relationType: RelationDefinitionType.OneToMany,
relationObjectMetadataNameSingular: '',
relationObjectMetadataNamePlural: '',
objectMetadataNameSingular: 'company',
@ -99,7 +102,7 @@ export const SIGN_IN_BACKGROUND_MOCK_COLUMN_DEFINITIONS = (
type: FieldMetadataType.Relation,
metadata: {
fieldName: 'accountOwner',
relationType: 'TO_ONE_OBJECT',
relationType: RelationDefinitionType.ManyToOne,
relationObjectMetadataNameSingular: 'workspaceMember',
relationObjectMetadataNamePlural: 'workspaceMembers',
objectMetadataNameSingular: 'company',
@ -116,7 +119,7 @@ export const SIGN_IN_BACKGROUND_MOCK_COLUMN_DEFINITIONS = (
type: FieldMetadataType.Relation,
metadata: {
fieldName: 'people',
relationType: 'FROM_MANY_OBJECTS',
relationType: RelationDefinitionType.OneToMany,
relationObjectMetadataNameSingular: '',
relationObjectMetadataNamePlural: '',
objectMetadataNameSingular: 'company',
@ -133,7 +136,7 @@ export const SIGN_IN_BACKGROUND_MOCK_COLUMN_DEFINITIONS = (
type: FieldMetadataType.Relation,
metadata: {
fieldName: 'attachments',
relationType: 'FROM_MANY_OBJECTS',
relationType: RelationDefinitionType.OneToMany,
relationObjectMetadataNameSingular: '',
relationObjectMetadataNamePlural: '',
objectMetadataNameSingular: 'company',
@ -201,7 +204,7 @@ export const SIGN_IN_BACKGROUND_MOCK_COLUMN_DEFINITIONS = (
type: FieldMetadataType.Relation,
metadata: {
fieldName: 'opportunities',
relationType: 'FROM_MANY_OBJECTS',
relationType: RelationDefinitionType.OneToMany,
relationObjectMetadataNameSingular: '',
relationObjectMetadataNamePlural: '',
objectMetadataNameSingular: 'company',
@ -235,7 +238,7 @@ export const SIGN_IN_BACKGROUND_MOCK_COLUMN_DEFINITIONS = (
type: FieldMetadataType.Relation,
metadata: {
fieldName: 'activityTargets',
relationType: 'FROM_MANY_OBJECTS',
relationType: RelationDefinitionType.OneToMany,
relationObjectMetadataNameSingular: '',
relationObjectMetadataNamePlural: '',
objectMetadataNameSingular: 'company',

View File

@ -1,8 +1,8 @@
import { useMemo } from 'react';
import { useParams, useSearchParams } from 'react-router-dom';
import { useApolloClient } from '@apollo/client';
import { isNonEmptyString } from '@sniptt/guards';
import qs from 'qs';
import { useMemo } from 'react';
import { useParams, useSearchParams } from 'react-router-dom';
import { useRecoilCallback, useRecoilValue } from 'recoil';
import z from 'zod';
@ -92,12 +92,12 @@ export const useViewFromQueryParams = () => {
if (isUndefinedOrNull(filterDefinition)) return null;
const relationObjectMetadataNameSingular =
fieldMetadataItem.toRelationMetadata?.fromObjectMetadata
.nameSingular;
fieldMetadataItem.relationDefinition?.targetObjectMetadata
?.nameSingular;
const relationObjectMetadataNamePlural =
fieldMetadataItem.toRelationMetadata?.fromObjectMetadata
.namePlural;
fieldMetadataItem.relationDefinition?.targetObjectMetadata
?.namePlural;
const relationObjectMetadataItem =
relationObjectMetadataNameSingular