Morph-front-display-table (#13979)
## 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>
This commit is contained in:
+2
@@ -229,3 +229,5 @@ idealCustomerProfile
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// todo @guillim add a test for the morph relation
|
||||
|
||||
+92
-5
@@ -1,5 +1,4 @@
|
||||
import { mapObjectMetadataToGraphQLQuery } from '@/object-metadata/utils/mapObjectMetadataToGraphQLQuery';
|
||||
import { isUndefined } from '@sniptt/guards';
|
||||
import { FieldMetadataType, RelationType } from '~/generated-metadata/graphql';
|
||||
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
@@ -7,7 +6,11 @@ import { getObjectPermissionsForObject } from '@/object-metadata/utils/getObject
|
||||
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 { isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
computeMorphRelationFieldJoinColumnName,
|
||||
computeMorphRelationFieldName,
|
||||
isDefined,
|
||||
} from 'twenty-shared/utils';
|
||||
import { type FieldMetadataItem } from '../types/FieldMetadataItem';
|
||||
|
||||
type MapFieldMetadataToGraphQLQueryArgs = {
|
||||
@@ -15,7 +18,7 @@ type MapFieldMetadataToGraphQLQueryArgs = {
|
||||
gqlField: string;
|
||||
fieldMetadata: Pick<
|
||||
FieldMetadataItem,
|
||||
'name' | 'type' | 'relation' | 'settings'
|
||||
'name' | 'type' | 'relation' | 'morphRelations' | 'settings'
|
||||
>;
|
||||
relationRecordGqlFields?: RecordGqlFields;
|
||||
computeReferences?: boolean;
|
||||
@@ -41,6 +44,90 @@ export const mapFieldMetadataToGraphQLQuery = ({
|
||||
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
|
||||
@@ -51,7 +138,7 @@ export const mapFieldMetadataToGraphQLQuery = ({
|
||||
fieldMetadata.relation?.targetObjectMetadata.id,
|
||||
);
|
||||
|
||||
if (isUndefined(relationMetadataItem)) {
|
||||
if (!isDefined(relationMetadataItem)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
@@ -100,7 +187,7 @@ ${mapObjectMetadataToGraphQLQuery({
|
||||
fieldMetadata.relation?.targetObjectMetadata.id,
|
||||
);
|
||||
|
||||
if (isUndefined(relationMetadataItem)) {
|
||||
if (!isDefined(relationMetadataItem)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
|
||||
+64
-11
@@ -5,7 +5,7 @@ import { shouldFieldBeQueried } from '@/object-metadata/utils/shouldFieldBeQueri
|
||||
import { type RecordGqlFields } from '@/object-record/graphql/types/RecordGqlFields';
|
||||
import { isRecordGqlFieldsNode } from '@/object-record/graphql/utils/isRecordGraphlFieldsNode';
|
||||
import { FieldMetadataType, type ObjectPermissions } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { computeMorphRelationFieldName, isDefined } from 'twenty-shared/utils';
|
||||
|
||||
type MapObjectMetadataToGraphQLQueryArgs = {
|
||||
objectMetadataItems: ObjectMetadataItem[];
|
||||
@@ -46,22 +46,75 @@ export const mapObjectMetadataToGraphQLQuery = ({
|
||||
|
||||
const manyToOneRelationFields = objectMetadataItem?.readableFields
|
||||
.filter((field) => field.isActive)
|
||||
.filter((field) => field.type === FieldMetadataType.RELATION)
|
||||
.filter(
|
||||
(field) =>
|
||||
field.type === FieldMetadataType.RELATION ||
|
||||
field.type === FieldMetadataType.MORPH_RELATION,
|
||||
)
|
||||
.filter((field) => isDefined(field.settings?.joinColumnName));
|
||||
|
||||
const manyToOneRelationGqlFieldWithFieldMetadata =
|
||||
manyToOneRelationFields.map((field) => ({
|
||||
gqlField: field.settings?.joinColumnName,
|
||||
fieldMetadata: field,
|
||||
manyToOneRelationFields.flatMap((field) => {
|
||||
const isMorphRelation = field.type === FieldMetadataType.MORPH_RELATION;
|
||||
if (!isMorphRelation) {
|
||||
return {
|
||||
gqlField: field.settings?.joinColumnName,
|
||||
fieldMetadata: field,
|
||||
};
|
||||
}
|
||||
|
||||
if (!isDefined(field.morphRelations)) {
|
||||
throw new Error(
|
||||
`Field ${field.name} is missing, please refresh the page. If the problem persists, please contact support.`,
|
||||
);
|
||||
}
|
||||
|
||||
return field.morphRelations.map((morphRelation) => ({
|
||||
gqlField: computeMorphRelationFieldName({
|
||||
fieldName: field.name,
|
||||
relationDirection: morphRelation.type,
|
||||
nameSingular: morphRelation.targetObjectMetadata.nameSingular,
|
||||
namePlural: morphRelation.targetObjectMetadata.namePlural,
|
||||
}),
|
||||
fieldMetadata: field,
|
||||
}));
|
||||
});
|
||||
|
||||
const readableFields = objectMetadataItem.readableFields.filter(
|
||||
(fieldMetadata) => fieldMetadata.isActive,
|
||||
);
|
||||
|
||||
const activeReadableFields = readableFields.flatMap((fieldMetadata) => {
|
||||
const isMorphRelation =
|
||||
fieldMetadata.type === FieldMetadataType.MORPH_RELATION;
|
||||
if (!isMorphRelation) {
|
||||
return [
|
||||
{
|
||||
gqlField: fieldMetadata.name,
|
||||
fieldMetadata,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
if (!isDefined(fieldMetadata.morphRelations)) {
|
||||
throw new Error(
|
||||
`Field ${fieldMetadata.name} is missing, please refresh the page. If the problem persists, please contact support.`,
|
||||
);
|
||||
}
|
||||
|
||||
return fieldMetadata.morphRelations.map((morphRelation) => ({
|
||||
gqlField: computeMorphRelationFieldName({
|
||||
fieldName: fieldMetadata.name,
|
||||
relationDirection: morphRelation.type,
|
||||
nameSingular: morphRelation.targetObjectMetadata.nameSingular,
|
||||
namePlural: morphRelation.targetObjectMetadata.namePlural,
|
||||
}),
|
||||
fieldMetadata,
|
||||
}));
|
||||
});
|
||||
|
||||
const gqlFieldWithFieldMetadataThatCouldBeQueried = [
|
||||
...objectMetadataItem.readableFields
|
||||
.filter((fieldMetadata) => fieldMetadata.isActive)
|
||||
.map((fieldMetadata) => ({
|
||||
gqlField: fieldMetadata.name,
|
||||
fieldMetadata,
|
||||
})),
|
||||
...activeReadableFields,
|
||||
...manyToOneRelationGqlFieldWithFieldMetadata,
|
||||
].sort((gqlFieldWithFieldMetadataA, gqlFieldWithFieldMetadataB) =>
|
||||
gqlFieldWithFieldMetadataA.gqlField.localeCompare(
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { type RecordGqlOperationGqlRecordFields } from '@/object-record/graphql/types/RecordGqlOperationGqlRecordFields';
|
||||
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
|
||||
|
||||
import { isFieldMorphRelation } from '@/object-record/record-field/ui/types/guards/isFieldMorphRelation';
|
||||
import { isFieldRelation } from '@/object-record/record-field/ui/types/guards/isFieldRelation';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { type FieldMetadataItem } from '../types/FieldMetadataItem';
|
||||
@@ -18,16 +18,21 @@ export const shouldFieldBeQueried = ({
|
||||
recordGqlFields?: RecordGqlOperationGqlRecordFields;
|
||||
}): any => {
|
||||
const isJoinColumn: boolean =
|
||||
isFieldRelation(fieldMetadata) &&
|
||||
(isFieldRelation(fieldMetadata) || isFieldMorphRelation(fieldMetadata)) &&
|
||||
fieldMetadata.settings.joinColumnName === gqlField;
|
||||
|
||||
if (
|
||||
isUndefinedOrNull(recordGqlFields) &&
|
||||
(fieldMetadata.type !== FieldMetadataType.RELATION || isJoinColumn)
|
||||
!isFieldRelation(fieldMetadata) &&
|
||||
!isFieldMorphRelation(fieldMetadata)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (isUndefinedOrNull(recordGqlFields) && isJoinColumn) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
isDefined(recordGqlFields) &&
|
||||
isDefined(recordGqlFields[gqlField]) &&
|
||||
|
||||
Reference in New Issue
Block a user