Fix storybook / chromatic tests flakyness and integration tests (#11687)

## Storybook flakyness

### Actor Display image flakyness

<img width="1512" alt="image"
src="https://github.com/user-attachments/assets/875c0738-5e31-4aba-9231-4ba5f78d1355"
/>

**Fix:** stop using a random usage

### Task Groups broken

<img width="1512" alt="image"
src="https://github.com/user-attachments/assets/c67e47a1-a027-43f1-9601-68d61a8052b4"
/>

**Fix:** add missing TabListComponentInstance

## Flaky dates

Add https://github.com/k35o/storybook-addon-mock-date

## Integration tests

Fix broken tests due to relation refactoring
This commit is contained in:
Charles Bochet
2025-04-23 01:57:36 +02:00
committed by GitHub
parent 8694840b92
commit fa5f758228
20 changed files with 153 additions and 155 deletions

View File

@ -183,6 +183,7 @@
"semver": "^7.5.4", "semver": "^7.5.4",
"sharp": "^0.32.1", "sharp": "^0.32.1",
"slash": "^5.1.0", "slash": "^5.1.0",
"storybook-addon-mock-date": "^0.6.0",
"stripe": "^17.3.1", "stripe": "^17.3.1",
"ts-key-enum": "^2.0.12", "ts-key-enum": "^2.0.12",
"tslib": "^2.3.0", "tslib": "^2.3.0",

View File

@ -48,6 +48,7 @@ const config: StorybookConfig = {
'storybook-dark-mode', 'storybook-dark-mode',
'storybook-addon-cookie', 'storybook-addon-cookie',
'storybook-addon-pseudo-states', 'storybook-addon-pseudo-states',
'storybook-addon-mock-date'
], ],
framework: { framework: {
name: '@storybook/react-vite', name: '@storybook/react-vite',

View File

@ -62,6 +62,7 @@ const preview: Preview = {
date: /Date$/, date: /Date$/,
}, },
}, },
mockingDate: new Date('2024-03-12T09:30:00.000Z'),
options: { options: {
storySort: { storySort: {
order: ['UI', 'Modules', 'Pages'], order: ['UI', 'Modules', 'Pages'],

View File

@ -12,11 +12,11 @@ const StyledContainer = styled.div`
overflow: auto; overflow: auto;
`; `;
export const ObjectTasks = ({ type ObjectTasksProps = {
targetableObject,
}: {
targetableObject: ActivityTargetableObject; targetableObject: ActivityTargetableObject;
}) => { };
export const ObjectTasks = ({ targetableObject }: ObjectTasksProps) => {
return ( return (
<StyledContainer> <StyledContainer>
<ObjectFilterDropdownComponentInstanceContext.Provider <ObjectFilterDropdownComponentInstanceContext.Provider

View File

@ -3,6 +3,7 @@ import { Meta, StoryObj } from '@storybook/react';
import { TaskGroups } from '@/activities/tasks/components/TaskGroups'; import { TaskGroups } from '@/activities/tasks/components/TaskGroups';
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity'; import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
import { ObjectFilterDropdownComponentInstanceContext } from '@/object-record/object-filter-dropdown/states/contexts/ObjectFilterDropdownComponentInstanceContext'; import { ObjectFilterDropdownComponentInstanceContext } from '@/object-record/object-filter-dropdown/states/contexts/ObjectFilterDropdownComponentInstanceContext';
import { TabListComponentInstanceContext } from '@/ui/layout/tab/states/contexts/TabListComponentInstanceContext';
import { ComponentWithRecoilScopeDecorator } from '~/testing/decorators/ComponentWithRecoilScopeDecorator'; import { ComponentWithRecoilScopeDecorator } from '~/testing/decorators/ComponentWithRecoilScopeDecorator';
import { ComponentWithRouterDecorator } from '~/testing/decorators/ComponentWithRouterDecorator'; import { ComponentWithRouterDecorator } from '~/testing/decorators/ComponentWithRouterDecorator';
import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator'; import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator';
@ -15,11 +16,15 @@ const meta: Meta<typeof TaskGroups> = {
component: TaskGroups, component: TaskGroups,
decorators: [ decorators: [
(Story) => ( (Story) => (
<ObjectFilterDropdownComponentInstanceContext.Provider <TabListComponentInstanceContext.Provider
value={{ instanceId: 'entity-tasks-filter-scope' }} value={{ instanceId: 'entity-tasks-filter-scope' }}
> >
<Story /> <ObjectFilterDropdownComponentInstanceContext.Provider
</ObjectFilterDropdownComponentInstanceContext.Provider> value={{ instanceId: 'entity-tasks-filter-scope' }}
>
<Story />
</ObjectFilterDropdownComponentInstanceContext.Provider>
</TabListComponentInstanceContext.Provider>
), ),
ComponentWithRouterDecorator, ComponentWithRouterDecorator,
ComponentWithRecoilScopeDecorator, ComponentWithRecoilScopeDecorator,

View File

@ -1,38 +1,39 @@
import { ActionConfig } from '@/action-menu/actions/types/ActionConfig'; import { ActionConfig } from '@/action-menu/actions/types/ActionConfig';
import { getActionLabel } from '@/action-menu/utils/getActionLabel'; import { getActionLabel } from '@/action-menu/utils/getActionLabel';
import { isNonEmptyString } from '@sniptt/guards'; import { isNonEmptyString } from '@sniptt/guards';
import { useDebounce } from 'use-debounce'; import { useCallback } from 'react';
const checkInShortcuts = (action: ActionConfig, search: string) => {
const concatenatedString = action.hotKeys?.join('') ?? '';
return concatenatedString.toLowerCase().includes(search.toLowerCase().trim());
};
const checkInLabels = (action: ActionConfig, search: string) => {
const actionLabel = getActionLabel(action.label);
if (isNonEmptyString(actionLabel)) {
return actionLabel.toLowerCase().includes(search.toLowerCase());
}
return false;
};
type UseFilterActionsWithCommandMenuSearchProps = {
commandMenuSearch: string;
};
export const useFilterActionsWithCommandMenuSearch = ({ export const useFilterActionsWithCommandMenuSearch = ({
commandMenuSearch, commandMenuSearch,
}: { }: UseFilterActionsWithCommandMenuSearchProps) => {
commandMenuSearch: string; const filterActionsWithCommandMenuSearch = useCallback(
}) => { (actions: ActionConfig[]) => {
const [deferredCommandMenuSearch] = useDebounce(commandMenuSearch, 300); // 200ms - 500ms return actions.filter((action) =>
commandMenuSearch.length > 0
const checkInShortcuts = (action: ActionConfig, search: string) => { ? checkInShortcuts(action, commandMenuSearch) ||
const concatenatedString = action.hotKeys?.join('') ?? ''; checkInLabels(action, commandMenuSearch)
return concatenatedString : true,
.toLowerCase() );
.includes(search.toLowerCase().trim()); },
}; [commandMenuSearch],
);
const checkInLabels = (action: ActionConfig, search: string) => {
const actionLabel = getActionLabel(action.label);
if (isNonEmptyString(actionLabel)) {
return actionLabel.toLowerCase().includes(search.toLowerCase());
}
return false;
};
const filterActionsWithCommandMenuSearch = (actions: ActionConfig[]) => {
return actions.filter((action) =>
deferredCommandMenuSearch.length > 0
? checkInShortcuts(action, deferredCommandMenuSearch) ||
checkInLabels(action, deferredCommandMenuSearch)
: true,
);
};
return { return {
filterActionsWithCommandMenuSearch, filterActionsWithCommandMenuSearch,

View File

@ -4,6 +4,7 @@ import { Field, FieldMetadataType } from '~/generated-metadata/graphql';
import { FieldMetadataItem } from '../types/FieldMetadataItem'; import { FieldMetadataItem } from '../types/FieldMetadataItem';
import { formatFieldMetadataItemInput } from '../utils/formatFieldMetadataItemInput'; import { formatFieldMetadataItemInput } from '../utils/formatFieldMetadataItemInput';
import { isDefined } from 'twenty-shared/utils';
import { useCreateOneFieldMetadataItem } from './useCreateOneFieldMetadataItem'; import { useCreateOneFieldMetadataItem } from './useCreateOneFieldMetadataItem';
import { useDeleteOneFieldMetadataItem } from './useDeleteOneFieldMetadataItem'; import { useDeleteOneFieldMetadataItem } from './useDeleteOneFieldMetadataItem';
import { useUpdateOneFieldMetadataItem } from './useUpdateOneFieldMetadataItem'; import { useUpdateOneFieldMetadataItem } from './useUpdateOneFieldMetadataItem';
@ -63,7 +64,8 @@ export const useFieldMetadataItem = () => {
}); });
const deleteMetadataField = (metadataField: FieldMetadataItem) => { const deleteMetadataField = (metadataField: FieldMetadataItem) => {
return metadataField.type === FieldMetadataType.RELATION return metadataField.type === FieldMetadataType.RELATION &&
!isDefined(metadataField.settings?.relationType)
? deleteOneRelationMetadataItem( ? deleteOneRelationMetadataItem(
metadataField.relationDefinition?.relationId, metadataField.relationDefinition?.relationId,
) )

View File

@ -15,16 +15,16 @@ import { tableColumnsComponentState } from '@/object-record/record-table/states/
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext'; import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext';
import { within } from '@storybook/test'; import { within } from '@storybook/test';
import {
ComponentDecorator,
getCanvasElementForDropdownTesting,
} from 'twenty-ui/testing';
import { ContextStoreDecorator } from '~/testing/decorators/ContextStoreDecorator'; import { ContextStoreDecorator } from '~/testing/decorators/ContextStoreDecorator';
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator'; import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
import { IconsProviderDecorator } from '~/testing/decorators/IconsProviderDecorator'; import { IconsProviderDecorator } from '~/testing/decorators/IconsProviderDecorator';
import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator'; import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator';
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator'; import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems'; import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems';
import {
ComponentDecorator,
getCanvasElementForDropdownTesting,
} from 'twenty-ui/testing';
const meta: Meta<typeof MultipleFiltersDropdownButton> = { const meta: Meta<typeof MultipleFiltersDropdownButton> = {
title: title:

View File

@ -37,7 +37,7 @@ export const Catalog: Story = {
}, },
{ {
name: 'avatarUrl', name: 'avatarUrl',
values: [null, 'https://picsum.photos/16'], values: [null, 'https://picsum.photos/id/237/16/16'],
props: (avatarUrl: string) => ({ avatarUrl }), props: (avatarUrl: string) => ({ avatarUrl }),
}, },
], ],

View File

@ -9,8 +9,6 @@ import { graphqlMocks } from '~/testing/graphqlMocks';
import { userEvent, within } from '@storybook/test'; import { userEvent, within } from '@storybook/test';
import { SettingsExperience } from '../profile/appearance/components/SettingsExperience'; import { SettingsExperience } from '../profile/appearance/components/SettingsExperience';
Date.now = () => new Date('2022-06-13T12:33:37.000Z').getTime();
const meta: Meta<PageDecoratorArgs> = { const meta: Meta<PageDecoratorArgs> = {
title: 'Pages/Settings/SettingsExperience', title: 'Pages/Settings/SettingsExperience',
component: SettingsExperience, component: SettingsExperience,
@ -44,15 +42,15 @@ export const DateTimeSettingsTimeFormat: Story = {
await canvas.findByText('Date and time'); await canvas.findByText('Date and time');
const timeFormatSelect = await canvas.findByText('24h (08:33)'); const timeFormatSelect = await canvas.findByText('24h (05:30)');
await userEvent.click(timeFormatSelect); await userEvent.click(timeFormatSelect);
const timeFormatOptions = await canvas.findByText('12h (8:33 AM)'); const timeFormatOptions = await canvas.findByText('12h (5:30 AM)');
await userEvent.click(timeFormatOptions); await userEvent.click(timeFormatOptions);
await canvas.findByText('12h (8:33 AM)'); await canvas.findByText('12h (5:30 AM)');
}, },
}; };
@ -84,14 +82,14 @@ export const DateTimeSettingsDateFormat: Story = {
await canvas.findByText('Date and time'); await canvas.findByText('Date and time');
const timeFormatSelect = await canvas.findByText('13 Jun, 2022'); const timeFormatSelect = await canvas.findByText('12 Mar, 2024');
await userEvent.click(timeFormatSelect); await userEvent.click(timeFormatSelect);
const timeFormatOptions = await canvas.findByText('Jun 13, 2022'); const timeFormatOptions = await canvas.findByText('Mar 12, 2024');
await userEvent.click(timeFormatOptions); await userEvent.click(timeFormatOptions);
await canvas.findByText('Jun 13, 2022'); await canvas.findByText('Mar 12, 2024');
}, },
}; };

View File

@ -37,11 +37,6 @@ export class WorkspaceSchemaFactory {
return new GraphQLSchema({}); return new GraphQLSchema({});
} }
const cachedIsNewRelationEnabled =
await this.workspaceCacheStorageService.getIsNewRelationEnabled(
authContext.workspace.id,
);
const isNewRelationEnabled = await this.featureFlagService.isFeatureEnabled( const isNewRelationEnabled = await this.featureFlagService.isFeatureEnabled(
FeatureFlagKey.IsNewRelationEnabled, FeatureFlagKey.IsNewRelationEnabled,
authContext.workspace.id, authContext.workspace.id,
@ -79,35 +74,6 @@ export class WorkspaceSchemaFactory {
); );
} }
// TODO: remove this after the feature flag is droped
if (
(isNewRelationEnabled && cachedIsNewRelationEnabled === undefined) ||
(isNewRelationEnabled !== cachedIsNewRelationEnabled &&
cachedIsNewRelationEnabled !== undefined)
) {
// eslint-disable-next-line no-console
console.log(
chalk.yellow('Recomputing due to new relation feature flag'),
{
isNewRelationEnabled,
},
);
await this.workspaceCacheStorageService.setIsNewRelationEnabled(
authContext.workspace.id,
isNewRelationEnabled,
);
await this.workspaceMetadataCacheService.recomputeMetadataCache({
workspaceId: authContext.workspace.id,
});
throw new GraphqlQueryRunnerException(
'Metadata cache recomputation required due to relation feature flag change',
GraphqlQueryRunnerExceptionCode.METADATA_CACHE_FEATURE_FLAG_RECOMPUTATION_REQUIRED,
);
}
const objectMetadataMaps = const objectMetadataMaps =
await this.workspaceCacheStorageService.getObjectMetadataMaps( await this.workspaceCacheStorageService.getObjectMetadataMaps(
authContext.workspace.id, authContext.workspace.id,

View File

@ -24,6 +24,7 @@ import { IsFieldMetadataOptions } from 'src/engine/metadata-modules/field-metada
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module'; import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module';
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module'; import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
import { RelationMetadataEntity } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/workspace-metadata-version/workspace-metadata-version.module'; import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/workspace-metadata-version/workspace-metadata-version.module';
import { WorkspaceMigrationModule } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.module'; import { WorkspaceMigrationModule } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.module';
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module'; import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
@ -41,7 +42,7 @@ import { UpdateFieldInput } from './dtos/update-field.input';
NestjsQueryGraphQLModule.forFeature({ NestjsQueryGraphQLModule.forFeature({
imports: [ imports: [
NestjsQueryTypeOrmModule.forFeature( NestjsQueryTypeOrmModule.forFeature(
[FieldMetadataEntity, ObjectMetadataEntity], [FieldMetadataEntity, ObjectMetadataEntity, RelationMetadataEntity],
'metadata', 'metadata',
), ),
WorkspaceMigrationModule, WorkspaceMigrationModule,

View File

@ -161,12 +161,6 @@ export class FieldMetadataResolver {
throw new ValidationError("Active fields can't be deleted"); throw new ValidationError("Active fields can't be deleted");
} }
if (fieldMetadata.type === FieldMetadataType.RELATION) {
throw new ValidationError(
"Relation fields can't be deleted, you need to delete the RelationMetadata instead",
);
}
try { try {
return await this.fieldMetadataService.deleteOneField(input, workspaceId); return await this.fieldMetadataService.deleteOneField(input, workspaceId);
} catch (error) { } catch (error) {

View File

@ -10,6 +10,8 @@ import { isDefined } from 'twenty-shared/utils';
import { DataSource, FindOneOptions, In, Repository } from 'typeorm'; import { DataSource, FindOneOptions, In, Repository } from 'typeorm';
import { v4 as uuidV4, v4 } from 'uuid'; import { v4 as uuidV4, v4 } from 'uuid';
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
import { TypeORMService } from 'src/database/typeorm/typeorm.service'; import { TypeORMService } from 'src/database/typeorm/typeorm.service';
import { settings } from 'src/engine/constants/settings'; import { settings } from 'src/engine/constants/settings';
import { generateMessageId } from 'src/engine/core-modules/i18n/utils/generateMessageId'; import { generateMessageId } from 'src/engine/core-modules/i18n/utils/generateMessageId';
@ -81,6 +83,8 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>, private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
@InjectRepository(ObjectMetadataEntity, 'metadata') @InjectRepository(ObjectMetadataEntity, 'metadata')
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>, private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
@InjectRepository(RelationMetadataEntity, 'metadata')
private readonly relationMetadataRepository: Repository<RelationMetadataEntity>,
private readonly workspaceMigrationFactory: WorkspaceMigrationFactory, private readonly workspaceMigrationFactory: WorkspaceMigrationFactory,
private readonly workspaceMigrationService: WorkspaceMigrationService, private readonly workspaceMigrationService: WorkspaceMigrationService,
private readonly workspaceMigrationRunnerService: WorkspaceMigrationRunnerService, private readonly workspaceMigrationRunnerService: WorkspaceMigrationRunnerService,
@ -350,9 +354,48 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
fieldMetadataId: fieldMetadata.id, fieldMetadataId: fieldMetadata.id,
}); });
await fieldMetadataRepository.delete(fieldMetadata.id); if (fieldMetadata.type === FieldMetadataType.RELATION) {
const isManyToManyRelation =
(fieldMetadata as FieldMetadataEntity<FieldMetadataType.RELATION>)
.settings?.relationType === RelationType.MANY_TO_ONE;
if (isCompositeFieldMetadataType(fieldMetadata.type)) { const targetFieldMetadata =
await this.fieldMetadataRepository.findOneBy({
id: fieldMetadata.relationTargetFieldMetadataId,
});
if (targetFieldMetadata) {
await this.relationMetadataRepository.delete({
fromFieldMetadataId: In([fieldMetadata.id, targetFieldMetadata.id]),
});
await this.relationMetadataRepository.delete({
toFieldMetadataId: In([fieldMetadata.id, targetFieldMetadata.id]),
});
await fieldMetadataRepository.delete({
id: In([fieldMetadata.id, targetFieldMetadata.id]),
});
await this.workspaceMigrationService.createCustomMigration(
generateMigrationName(`delete-${fieldMetadata.name}`),
workspaceId,
[
{
name: computeObjectTargetTable(objectMetadata),
action: WorkspaceMigrationTableActionType.ALTER,
columns: [
{
action: WorkspaceMigrationColumnActionType.DROP,
columnName: isManyToManyRelation
? `${(fieldMetadata as FieldMetadataEntity<FieldMetadataType.RELATION>).settings?.joinColumnName}`
: `${(targetFieldMetadata as FieldMetadataEntity<FieldMetadataType.RELATION>).settings?.joinColumnName}`,
} satisfies WorkspaceMigrationColumnDrop,
],
} satisfies WorkspaceMigrationTableAction,
],
);
}
} else if (isCompositeFieldMetadataType(fieldMetadata.type)) {
await fieldMetadataRepository.delete(fieldMetadata.id);
const compositeType = compositeTypeDefinitions.get(fieldMetadata.type); const compositeType = compositeTypeDefinitions.get(fieldMetadata.type);
if (!compositeType) { if (!compositeType) {
@ -383,6 +426,7 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
], ],
); );
} else { } else {
await fieldMetadataRepository.delete(fieldMetadata.id);
await this.workspaceMigrationService.createCustomMigration( await this.workspaceMigrationService.createCustomMigration(
generateMigrationName(`delete-${fieldMetadata.name}`), generateMigrationName(`delete-${fieldMetadata.name}`),
workspaceId, workspaceId,

View File

@ -310,17 +310,17 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
const updatedObject = await super.updateOne(inputId, inputPayload); const updatedObject = await super.updateOne(inputId, inputPayload);
const isNewRelationEnabled = await this.featureFlagService.isFeatureEnabled(
FeatureFlagKey.IsNewRelationEnabled,
workspaceId,
);
await this.handleObjectNameAndLabelUpdates( await this.handleObjectNameAndLabelUpdates(
existingObjectMetadata, existingObjectMetadata,
existingObjectMetadataCombinedWithUpdateInput, existingObjectMetadataCombinedWithUpdateInput,
inputPayload, inputPayload,
); );
const isNewRelationEnabled = await this.featureFlagService.isFeatureEnabled(
FeatureFlagKey.IsNewRelationEnabled,
workspaceId,
);
if (inputPayload.isActive !== undefined) { if (inputPayload.isActive !== undefined) {
// For new relation system, the active status is stitched to the field metadata // For new relation system, the active status is stitched to the field metadata
if (!isNewRelationEnabled) { if (!isNewRelationEnabled) {

View File

@ -6,6 +6,7 @@ import { capitalize } from 'twenty-shared/utils';
import { Repository } from 'typeorm'; import { Repository } from 'typeorm';
import { v4 as uuidV4 } from 'uuid'; import { v4 as uuidV4 } from 'uuid';
import { FieldMetadataDefaultSettings } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface';
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
@ -165,15 +166,30 @@ export class ObjectMetadataFieldRelationService {
objectMetadataId: targetObjectMetadata.id, objectMetadataId: targetObjectMetadata.id,
workspaceId: workspaceId, workspaceId: workspaceId,
}); });
const isTargetFieldMetadataManyToOneRelation =
(
targetFieldMetadataToUpdate as FieldMetadataEntity<FieldMetadataType.RELATION>
).settings?.relationType === RelationType.MANY_TO_ONE;
const targetFieldMetadata = await this.fieldMetadataRepository.save({ const targetFieldMetadata = await this.fieldMetadataRepository.save({
id: targetFieldMetadataToUpdate.id, id: targetFieldMetadataToUpdate.id,
...targetFieldMetadataUpdateData, ...targetFieldMetadataUpdateData,
settings: {
...(targetFieldMetadataToUpdate.settings as FieldMetadataDefaultSettings),
...(isTargetFieldMetadataManyToOneRelation
? {
joinColumnName: `${sourceObjectMetadata.nameSingular}Id`,
}
: {}),
},
}); });
const sourceFieldMetadataUpdateData = this.updateSourceFieldMetadata( const sourceFieldMetadataUpdateData = this.updateSourceFieldMetadata(
sourceObjectMetadata, sourceObjectMetadata,
targetObjectMetadata, targetObjectMetadata,
); );
const sourceFieldMetadataToUpdate = const sourceFieldMetadataToUpdate =
await this.fieldMetadataRepository.findOneByOrFail({ await this.fieldMetadataRepository.findOneByOrFail({
standardId: standardId:
@ -181,9 +197,23 @@ export class ObjectMetadataFieldRelationService {
objectMetadataId: sourceObjectMetadata.id, objectMetadataId: sourceObjectMetadata.id,
workspaceId: workspaceId, workspaceId: workspaceId,
}); });
const isSourceFieldMetadataManyToOneRelation =
(
sourceFieldMetadataToUpdate as FieldMetadataEntity<FieldMetadataType.RELATION>
).settings?.relationType === RelationType.MANY_TO_ONE;
const sourceFieldMetadata = await this.fieldMetadataRepository.save({ const sourceFieldMetadata = await this.fieldMetadataRepository.save({
id: sourceFieldMetadataToUpdate.id, id: sourceFieldMetadataToUpdate.id,
...sourceFieldMetadataUpdateData, ...sourceFieldMetadataUpdateData,
settings: {
...(sourceFieldMetadataToUpdate.settings as FieldMetadataDefaultSettings),
...(isSourceFieldMetadataManyToOneRelation
? {
joinColumnName: `${targetObjectMetadata.nameSingular}Id`,
}
: {}),
},
}); });
return { return {

View File

@ -276,7 +276,7 @@ export class ObjectMetadataMigrationService {
relationToDelete.toFieldMetadataId, relationToDelete.toFieldMetadataId,
]); ]);
if (relationToDelete.direction === 'from') { if (relationToDelete.direction === 'from' && !isNewRelationEnabled) {
await this.workspaceMigrationService.createCustomMigration( await this.workspaceMigrationService.createCustomMigration(
generateMigrationName( generateMigrationName(
`delete-${RELATION_MIGRATION_PRIORITY_PREFIX}-${relationToDelete.fromObjectName}-${relationToDelete.toObjectName}`, `delete-${RELATION_MIGRATION_PRIORITY_PREFIX}-${relationToDelete.fromObjectName}-${relationToDelete.toObjectName}`,

View File

@ -54,6 +54,7 @@ describe('SearchResolver', () => {
}, },
}, },
}); });
const listingObjectMetadata = objectsMetadata.find( const listingObjectMetadata = objectsMetadata.find(
(object) => object.nameSingular === LISTING_NAME_SINGULAR, (object) => object.nameSingular === LISTING_NAME_SINGULAR,
); );

View File

@ -4,7 +4,6 @@ import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object
import { findManyObjectMetadataQueryFactory } from 'test/integration/metadata/suites/object-metadata/utils/find-many-object-metadata-query-factory.util'; import { findManyObjectMetadataQueryFactory } from 'test/integration/metadata/suites/object-metadata/utils/find-many-object-metadata-query-factory.util';
import { updateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util'; import { updateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util';
import { createOneRelationMetadataFactory } from 'test/integration/metadata/suites/utils/create-one-relation-metadata-factory.util'; import { createOneRelationMetadataFactory } from 'test/integration/metadata/suites/utils/create-one-relation-metadata-factory.util';
import { deleteOneRelationMetadataItemFactory } from 'test/integration/metadata/suites/utils/delete-one-relation-metadata-factory.util';
import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util'; import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util';
import { FieldMetadataType } from 'twenty-shared/types'; import { FieldMetadataType } from 'twenty-shared/types';
@ -14,7 +13,6 @@ const LISTING_NAME_SINGULAR = 'listing';
describe('Custom object renaming', () => { describe('Custom object renaming', () => {
let listingObjectId = ''; let listingObjectId = '';
let customRelationId = '';
const STANDARD_OBJECT_RELATIONS = [ const STANDARD_OBJECT_RELATIONS = [
'noteTarget', 'noteTarget',
@ -108,10 +106,6 @@ describe('Custom object renaming', () => {
const fields = await makeMetadataAPIRequest(fieldsGraphqlOperation); const fields = await makeMetadataAPIRequest(fieldsGraphqlOperation);
const foreignKeyFieldsMetadataForListing = fields.body.data.fields.edges
.filter((field) => field.node.name === `${LISTING_NAME_SINGULAR}Id`)
.map((field) => field.node);
const relationFieldsMetadataForListing = fields.body.data.fields.edges const relationFieldsMetadataForListing = fields.body.data.fields.edges
.filter( .filter(
(field) => (field) =>
@ -120,21 +114,7 @@ describe('Custom object renaming', () => {
) )
.map((field) => field.node); .map((field) => field.node);
expect(foreignKeyFieldsMetadataForListing.length).toBe(5);
STANDARD_OBJECT_RELATIONS.forEach((relation) => { STANDARD_OBJECT_RELATIONS.forEach((relation) => {
// foreignKey field
const foreignKeyFieldMetadataId = foreignKeyFieldsMetadataForListing.find(
(field) =>
field.object.id ===
standardObjectRelationsMap[relation].objectMetadataId,
).id;
expect(foreignKeyFieldMetadataId).not.toBeUndefined();
standardObjectRelationsMap[relation].foreignKeyFieldMetadataId =
foreignKeyFieldMetadataId;
// relation field // relation field
const relationFieldMetadataId = relationFieldsMetadataForListing.find( const relationFieldMetadataId = relationFieldsMetadataForListing.find(
(field) => (field) =>
@ -189,8 +169,6 @@ describe('Custom object renaming', () => {
); );
// Assert // Assert
customRelationId = relationResponse.body.data.createOneRelationMetadata.id;
relationFieldMetadataOnPersonId = relationFieldMetadataOnPersonId =
relationResponse.body.data.createOneRelationMetadata.fromFieldMetadataId; relationResponse.body.data.createOneRelationMetadata.fromFieldMetadataId;
}); });
@ -233,29 +211,8 @@ describe('Custom object renaming', () => {
(field) => field.node, (field) => field.node,
); );
expect(
fieldsMetadata.find(
(field) => field.name === `${LISTING_NAME_SINGULAR}Id`,
),
).toBeUndefined();
// standard relations have been updated // standard relations have been updated
STANDARD_OBJECT_RELATIONS.forEach((relation) => { STANDARD_OBJECT_RELATIONS.forEach((relation) => {
// foreignKey field
const foreignKeyFieldMetadataId =
standardObjectRelationsMap[relation].foreignKeyFieldMetadataId;
const updatedForeignKeyFieldMetadata = fieldsMetadata.find(
(field) => field.id === foreignKeyFieldMetadataId,
);
expect(updatedForeignKeyFieldMetadata.name).toBe(
`${HOUSE_NAME_SINGULAR}Id`,
);
expect(updatedForeignKeyFieldMetadata.label).toBe(
'House ID (foreign key)',
);
// relation field // relation field
const relationFieldMetadataId = const relationFieldMetadataId =
standardObjectRelationsMap[relation].relationFieldMetadataId; standardObjectRelationsMap[relation].relationFieldMetadataId;
@ -276,19 +233,7 @@ describe('Custom object renaming', () => {
expect(updatedRelationFieldMetadata.name).toBe(RELATION_FROM_NAME); expect(updatedRelationFieldMetadata.name).toBe(RELATION_FROM_NAME);
}); });
it('4. should delete custom relation', async () => { it('4. should delete custom object', async () => {
const graphqlOperation = deleteOneRelationMetadataItemFactory({
idToDelete: customRelationId,
});
const response = await makeMetadataAPIRequest(graphqlOperation);
const deleteRelationResponse = response.body.data.deleteOneRelation;
expect(deleteRelationResponse.id).toBe(customRelationId);
});
it('5. should delete custom object', async () => {
const { data } = await deleteOneObjectMetadata({ const { data } = await deleteOneObjectMetadata({
input: { input: {
idToDelete: listingObjectId, idToDelete: listingObjectId,

View File

@ -52921,6 +52921,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"storybook-addon-mock-date@npm:^0.6.0":
version: 0.6.0
resolution: "storybook-addon-mock-date@npm:0.6.0"
checksum: 10c0/24c48d65a04eb1931e39495fe42692cea428db4b66874db86ab4e9e6f99cd0bba56cd25c56c9a56b42e53a14edfd38f79bcdef4c1f0e2758096528863d32d408
languageName: node
linkType: hard
"storybook-addon-pseudo-states@npm:^2.1.2": "storybook-addon-pseudo-states@npm:^2.1.2":
version: 2.2.1 version: 2.2.1
resolution: "storybook-addon-pseudo-states@npm:2.2.1" resolution: "storybook-addon-pseudo-states@npm:2.2.1"
@ -55501,6 +55508,7 @@ __metadata:
source-map-support: "npm:^0.5.20" source-map-support: "npm:^0.5.20"
storybook: "npm:^7.6.3" storybook: "npm:^7.6.3"
storybook-addon-cookie: "npm:^3.2.0" storybook-addon-cookie: "npm:^3.2.0"
storybook-addon-mock-date: "npm:^0.6.0"
storybook-addon-pseudo-states: "npm:^2.1.2" storybook-addon-pseudo-states: "npm:^2.1.2"
storybook-dark-mode: "npm:^3.0.3" storybook-dark-mode: "npm:^3.0.3"
stripe: "npm:^17.3.1" stripe: "npm:^17.3.1"