Morph many to one picker (#14155)

This PR follows the multiSelect PR merged previously. It will enable
morph relation Many to One to be handled from the table, using a
singleSelect picker

Main point : I decided to change the singleSelect API to take an array
of **objectMetadataName** instead of only one to deal with both our
usecases.

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Guillim
2025-10-07 17:25:11 +02:00
committed by GitHub
parent 9f9c2d85ca
commit f60817a1e1
138 changed files with 2116 additions and 1763 deletions
@@ -0,0 +1,81 @@
import { FieldMetadataType, RelationType } from 'twenty-shared/types';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { getMorphNameFromMorphFieldMetadataName } from 'src/engine/metadata-modules/flat-object-metadata/utils/get-morph-name-from-morph-field-metadata-name.util';
describe('getMorphNameFromMorphFieldMetadataName', () => {
const createMorphRelationFlatFieldMetadataMock = (
name: string,
relationType: RelationType,
) =>
({
name,
type: FieldMetadataType.MORPH_RELATION,
label: name,
settings: { relationType },
}) as unknown as FlatFieldMetadata<FieldMetadataType.MORPH_RELATION>;
it('should extract base morph field name for MANY_TO_ONE (removes singular suffix)', () => {
const morphRelationFlatFieldMetadata =
createMorphRelationFlatFieldMetadataMock(
'contactDeal',
RelationType.MANY_TO_ONE,
);
const result = getMorphNameFromMorphFieldMetadataName({
morphRelationFlatFieldMetadata,
nameSingular: 'Deal',
namePlural: 'Deals',
});
expect(result).toBe('contact');
});
it('should also work extract base morph field name for MANY_TO_ONE if casing is not correct', () => {
const morphRelationFlatFieldMetadata =
createMorphRelationFlatFieldMetadataMock(
'contactDeal',
RelationType.MANY_TO_ONE,
);
const result = getMorphNameFromMorphFieldMetadataName({
morphRelationFlatFieldMetadata,
nameSingular: 'deal',
namePlural: 'deals',
});
expect(result).toBe('contact');
});
it('should extract base morph field name for ONE_TO_MANY (removes plural suffix)', () => {
const morphRelationFlatFieldMetadata =
createMorphRelationFlatFieldMetadataMock(
'contactDeals',
RelationType.ONE_TO_MANY,
);
const result = getMorphNameFromMorphFieldMetadataName({
morphRelationFlatFieldMetadata,
nameSingular: 'Deal',
namePlural: 'Deals',
});
expect(result).toBe('contact');
});
it('should return the original name when suffix does not match', () => {
const morphRelationFlatFieldMetadata =
createMorphRelationFlatFieldMetadataMock(
'randomName',
RelationType.MANY_TO_ONE,
);
const result = getMorphNameFromMorphFieldMetadataName({
morphRelationFlatFieldMetadata,
nameSingular: 'account',
namePlural: 'accounts',
});
expect(result).toBe('randomName');
});
});
@@ -0,0 +1,30 @@
import { type FieldMetadataType, RelationType } from 'twenty-shared/types';
import { capitalize } from 'twenty-shared/utils';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { searchAndReplaceLast } from 'src/engine/metadata-modules/flat-object-metadata/utils/search-and-replace-last.util';
export const getMorphNameFromMorphFieldMetadataName = ({
morphRelationFlatFieldMetadata,
nameSingular,
namePlural,
}: {
morphRelationFlatFieldMetadata: Pick<
FlatFieldMetadata<FieldMetadataType.MORPH_RELATION>,
'name' | 'settings'
>;
nameSingular: string;
namePlural: string;
}): string => {
const isManyToOneRelationType =
morphRelationFlatFieldMetadata.settings.relationType ===
RelationType.MANY_TO_ONE;
return searchAndReplaceLast({
source: morphRelationFlatFieldMetadata.name,
replace: '',
search: isManyToOneRelationType
? capitalize(nameSingular)
: capitalize(namePlural),
});
};
@@ -3,34 +3,15 @@ import {
type FieldMetadataType,
type FromTo,
} from 'twenty-shared/types';
import { computeMorphRelationFieldName } from 'twenty-shared/utils';
import { type AllFlatEntityMaps } from 'src/engine/core-modules/common/types/all-flat-entity-maps.type';
import { computeMorphOrRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-or-relation-field-join-column-name.util';
import { computeMorphRelationFieldName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-relation-field-name.util';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { findObjectFlatFieldMetadatasOrThrow } from 'src/engine/metadata-modules/flat-field-metadata/utils/find-object-fields-in-flat-field-metadata-maps-or-throw.util';
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import { getFlatObjectMetadataTargetMorphRelationFlatFieldMetadatasOrThrow } from 'src/engine/metadata-modules/flat-object-metadata/utils/get-flat-object-metadata-many-to-one-target-morph-relation-flat-field-metadatas-or-throw.util';
const searchAndReplaceLast = ({
replace,
search,
source,
}: {
source: string;
search: string;
replace: string;
}) => {
const lastIndex = source.lastIndexOf(search);
if (lastIndex === -1) return source;
return (
source.slice(0, lastIndex) +
replace +
source.slice(lastIndex + search.length)
);
};
import { getMorphNameFromMorphFieldMetadataName } from 'src/engine/metadata-modules/flat-object-metadata/utils/get-morph-name-from-morph-field-metadata-name.util';
type RenameRelatedMorphFieldOnObjectNamesUpdateArgs = FromTo<
FlatObjectMetadata,
@@ -59,17 +40,18 @@ export const renameRelatedMorphFieldOnObjectNamesUpdate = ({
const isManyToOneRelationType =
morphRelationFlatFieldMetadata.settings.relationType ===
RelationType.MANY_TO_ONE;
const initialMorphRelationFieldName = searchAndReplaceLast({
source: morphRelationFlatFieldMetadata.name,
replace: '',
search: isManyToOneRelationType
? fromFlatObjectMetadata.nameSingular
: fromFlatObjectMetadata.namePlural,
});
const initialMorphRelationFieldName =
getMorphNameFromMorphFieldMetadataName({
morphRelationFlatFieldMetadata,
nameSingular: fromFlatObjectMetadata.nameSingular,
namePlural: fromFlatObjectMetadata.namePlural,
});
const newMorphFieldName = computeMorphRelationFieldName({
fieldName: initialMorphRelationFieldName,
relationType: morphRelationFlatFieldMetadata.settings.relationType,
targetObjectMetadata: toFlatObjectMetadata,
targetObjectMetadataNameSingular: toFlatObjectMetadata.nameSingular,
targetObjectMetadataNamePlural: toFlatObjectMetadata.namePlural,
});
const newJoinColumnName = isManyToOneRelationType
? computeMorphOrRelationFieldJoinColumnName({
@@ -0,0 +1,19 @@
export const searchAndReplaceLast = ({
replace,
search,
source,
}: {
source: string;
search: string;
replace: string;
}) => {
const lastIndex = source.lastIndexOf(search);
if (lastIndex === -1) return source;
return (
source.slice(0, lastIndex) +
replace +
source.slice(lastIndex + search.length)
);
};