3d1842dcb3
## Add Morph Relation Field Display Support for Tables (one to many & many to one) **Features:** - Display morph relation fields (one-to-many, many-to-one) in table cells - Multi-field record retrieval for improved performance - Enhanced table cell rendering with morph relation support **Components Added:** - `MorphRelationManyToOneFieldDisplay` - Shows many-to-one morph relations - `MorphRelationOneToManyFieldDisplay` - Shows one-to-many morph relations TODO : handle notes and tasks case if we choose these kind of objects as targets/source for a morph relation --------- Co-authored-by: Charles Bochet <charles@twenty.com>
307 lines
8.0 KiB
TypeScript
307 lines
8.0 KiB
TypeScript
import { mapObjectMetadataToGraphQLQuery } from '@/object-metadata/utils/mapObjectMetadataToGraphQLQuery';
|
|
import { FieldMetadataType, RelationType } from '~/generated-metadata/graphql';
|
|
|
|
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
|
import { getObjectPermissionsForObject } from '@/object-metadata/utils/getObjectPermissionsForObject';
|
|
import { type RecordGqlFields } from '@/object-record/graphql/types/RecordGqlFields';
|
|
import { isNonCompositeField } from '@/object-record/object-filter-dropdown/utils/isNonCompositeField';
|
|
import { type ObjectPermissions } from 'twenty-shared/types';
|
|
import {
|
|
computeMorphRelationFieldJoinColumnName,
|
|
computeMorphRelationFieldName,
|
|
isDefined,
|
|
} from 'twenty-shared/utils';
|
|
import { type FieldMetadataItem } from '../types/FieldMetadataItem';
|
|
|
|
type MapFieldMetadataToGraphQLQueryArgs = {
|
|
objectMetadataItems: ObjectMetadataItem[];
|
|
gqlField: string;
|
|
fieldMetadata: Pick<
|
|
FieldMetadataItem,
|
|
'name' | 'type' | 'relation' | 'morphRelations' | 'settings'
|
|
>;
|
|
relationRecordGqlFields?: RecordGqlFields;
|
|
computeReferences?: boolean;
|
|
objectPermissionsByObjectMetadataId: Record<
|
|
string,
|
|
ObjectPermissions & { objectMetadataId: string }
|
|
>;
|
|
};
|
|
// TODO: change ObjectMetadataItems mock before refactoring with relation computed field
|
|
export const mapFieldMetadataToGraphQLQuery = ({
|
|
objectMetadataItems,
|
|
gqlField,
|
|
fieldMetadata,
|
|
relationRecordGqlFields,
|
|
computeReferences = false,
|
|
objectPermissionsByObjectMetadataId,
|
|
}: MapFieldMetadataToGraphQLQueryArgs): string => {
|
|
const fieldType = fieldMetadata.type;
|
|
|
|
const fieldIsNonCompositeField = isNonCompositeField(fieldType);
|
|
|
|
if (fieldIsNonCompositeField) {
|
|
return gqlField;
|
|
}
|
|
|
|
if (
|
|
fieldType === FieldMetadataType.MORPH_RELATION &&
|
|
(fieldMetadata.settings?.relationType === RelationType.ONE_TO_MANY ||
|
|
fieldMetadata.settings?.relationType === RelationType.MANY_TO_ONE)
|
|
) {
|
|
let gqlMorphField = '';
|
|
for (const morphRelation of fieldMetadata.morphRelations ?? []) {
|
|
const relationFieldName = computeMorphRelationFieldName({
|
|
fieldName: fieldMetadata.name,
|
|
relationDirection: fieldMetadata.settings?.relationType,
|
|
nameSingular: morphRelation.targetObjectMetadata.nameSingular,
|
|
namePlural: morphRelation.targetObjectMetadata.namePlural,
|
|
});
|
|
const relationMetadataItem = objectMetadataItems.find(
|
|
(objectMetadataItem) =>
|
|
objectMetadataItem.id === morphRelation.targetObjectMetadata.id,
|
|
);
|
|
|
|
if (!isDefined(relationMetadataItem)) {
|
|
continue;
|
|
}
|
|
|
|
if (
|
|
isDefined(objectPermissionsByObjectMetadataId) &&
|
|
isDefined(relationMetadataItem.id)
|
|
) {
|
|
if (!isDefined(morphRelation.targetObjectMetadata.id)) {
|
|
throw new Error(
|
|
`Target object metadata id not found with field metadata ${fieldMetadata.name}`,
|
|
);
|
|
}
|
|
|
|
const objectPermission = getObjectPermissionsForObject(
|
|
objectPermissionsByObjectMetadataId,
|
|
morphRelation.targetObjectMetadata.id,
|
|
);
|
|
|
|
if (!objectPermission.canReadObjectRecords) {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if (fieldMetadata.settings?.relationType === RelationType.ONE_TO_MANY) {
|
|
gqlMorphField += `${relationFieldName}
|
|
{
|
|
edges {
|
|
node ${mapObjectMetadataToGraphQLQuery({
|
|
objectMetadataItems,
|
|
objectMetadataItem: relationMetadataItem,
|
|
recordGqlFields: relationRecordGqlFields,
|
|
computeReferences,
|
|
isRootLevel: false,
|
|
objectPermissionsByObjectMetadataId,
|
|
})}
|
|
}
|
|
}`;
|
|
}
|
|
|
|
if (fieldMetadata.settings?.relationType === RelationType.MANY_TO_ONE) {
|
|
const joinColumnName = computeMorphRelationFieldJoinColumnName({
|
|
name: fieldMetadata.name,
|
|
targetObjectMetadataNameSingular:
|
|
morphRelation.targetObjectMetadata.nameSingular,
|
|
});
|
|
if (gqlField === joinColumnName) {
|
|
gqlMorphField += `${gqlField}
|
|
`;
|
|
continue;
|
|
}
|
|
|
|
gqlMorphField += `${relationFieldName}
|
|
${mapObjectMetadataToGraphQLQuery({
|
|
objectMetadataItems,
|
|
objectMetadataItem: relationMetadataItem,
|
|
recordGqlFields: relationRecordGqlFields,
|
|
computeReferences,
|
|
isRootLevel: false,
|
|
objectPermissionsByObjectMetadataId,
|
|
})}`;
|
|
}
|
|
}
|
|
return `${gqlMorphField}`;
|
|
}
|
|
|
|
if (
|
|
fieldType === FieldMetadataType.RELATION &&
|
|
fieldMetadata.relation?.type === RelationType.MANY_TO_ONE
|
|
) {
|
|
const relationMetadataItem = objectMetadataItems.find(
|
|
(objectMetadataItem) =>
|
|
objectMetadataItem.id ===
|
|
fieldMetadata.relation?.targetObjectMetadata.id,
|
|
);
|
|
|
|
if (!isDefined(relationMetadataItem)) {
|
|
return '';
|
|
}
|
|
|
|
if (
|
|
isDefined(objectPermissionsByObjectMetadataId) &&
|
|
isDefined(relationMetadataItem.id)
|
|
) {
|
|
if (!isDefined(fieldMetadata.relation?.targetObjectMetadata.id)) {
|
|
throw new Error(
|
|
`Target object metadata id not found with field metadata ${fieldMetadata.name}`,
|
|
);
|
|
}
|
|
|
|
const objectPermission = getObjectPermissionsForObject(
|
|
objectPermissionsByObjectMetadataId,
|
|
fieldMetadata.relation?.targetObjectMetadata.id,
|
|
);
|
|
|
|
if (!objectPermission.canReadObjectRecords) {
|
|
return '';
|
|
}
|
|
}
|
|
|
|
if (gqlField === fieldMetadata.settings?.joinColumnName) {
|
|
return `${gqlField}`;
|
|
}
|
|
|
|
return `${gqlField}
|
|
${mapObjectMetadataToGraphQLQuery({
|
|
objectMetadataItems,
|
|
objectMetadataItem: relationMetadataItem,
|
|
recordGqlFields: relationRecordGqlFields,
|
|
computeReferences: computeReferences,
|
|
isRootLevel: false,
|
|
objectPermissionsByObjectMetadataId,
|
|
})}`;
|
|
}
|
|
|
|
if (
|
|
fieldType === FieldMetadataType.RELATION &&
|
|
fieldMetadata.relation?.type === RelationType.ONE_TO_MANY
|
|
) {
|
|
const relationMetadataItem = objectMetadataItems.find(
|
|
(objectMetadataItem) =>
|
|
objectMetadataItem.id ===
|
|
fieldMetadata.relation?.targetObjectMetadata.id,
|
|
);
|
|
|
|
if (!isDefined(relationMetadataItem)) {
|
|
return '';
|
|
}
|
|
|
|
if (
|
|
isDefined(objectPermissionsByObjectMetadataId) &&
|
|
isDefined(relationMetadataItem.id)
|
|
) {
|
|
if (!isDefined(fieldMetadata.relation?.targetObjectMetadata.id)) {
|
|
throw new Error(
|
|
`Target object metadata id not found with field metadata ${fieldMetadata.name}`,
|
|
);
|
|
}
|
|
|
|
const objectPermission = getObjectPermissionsForObject(
|
|
objectPermissionsByObjectMetadataId,
|
|
fieldMetadata.relation?.targetObjectMetadata.id,
|
|
);
|
|
|
|
if (!objectPermission.canReadObjectRecords) {
|
|
return '';
|
|
}
|
|
}
|
|
|
|
return `${gqlField}
|
|
{
|
|
edges {
|
|
node ${mapObjectMetadataToGraphQLQuery({
|
|
objectMetadataItems,
|
|
objectMetadataItem: relationMetadataItem,
|
|
recordGqlFields: relationRecordGqlFields,
|
|
computeReferences,
|
|
isRootLevel: false,
|
|
objectPermissionsByObjectMetadataId,
|
|
})}
|
|
}
|
|
}`;
|
|
}
|
|
|
|
if (fieldType === FieldMetadataType.LINKS) {
|
|
return `${gqlField}
|
|
{
|
|
primaryLinkUrl
|
|
primaryLinkLabel
|
|
secondaryLinks
|
|
}`;
|
|
}
|
|
|
|
if (fieldType === FieldMetadataType.CURRENCY) {
|
|
return `${gqlField}
|
|
{
|
|
amountMicros
|
|
currencyCode
|
|
}
|
|
`;
|
|
}
|
|
|
|
if (fieldType === FieldMetadataType.FULL_NAME) {
|
|
return `${gqlField}
|
|
{
|
|
firstName
|
|
lastName
|
|
}`;
|
|
}
|
|
|
|
if (fieldType === FieldMetadataType.ADDRESS) {
|
|
return `${gqlField}
|
|
{
|
|
addressStreet1
|
|
addressStreet2
|
|
addressCity
|
|
addressState
|
|
addressCountry
|
|
addressPostcode
|
|
addressLat
|
|
addressLng
|
|
}`;
|
|
}
|
|
|
|
if (fieldType === FieldMetadataType.ACTOR) {
|
|
return `${gqlField}
|
|
{
|
|
source
|
|
workspaceMemberId
|
|
name
|
|
context
|
|
}`;
|
|
}
|
|
|
|
if (fieldType === FieldMetadataType.EMAILS) {
|
|
return `${gqlField}
|
|
{
|
|
primaryEmail
|
|
additionalEmails
|
|
}`;
|
|
}
|
|
|
|
if (fieldType === FieldMetadataType.PHONES) {
|
|
return `${gqlField}
|
|
{
|
|
primaryPhoneNumber
|
|
primaryPhoneCountryCode
|
|
primaryPhoneCallingCode
|
|
additionalPhones
|
|
}`;
|
|
}
|
|
|
|
if (fieldType === FieldMetadataType.RICH_TEXT_V2) {
|
|
return `${gqlField}
|
|
{
|
|
blocknote
|
|
markdown
|
|
}`;
|
|
}
|
|
|
|
return '';
|
|
};
|