diff --git a/packages/twenty-front/src/modules/activities/emails/components/EmailThreadMessage.tsx b/packages/twenty-front/src/modules/activities/emails/components/EmailThreadMessage.tsx index e5cd3507d9..ccd1150722 100644 --- a/packages/twenty-front/src/modules/activities/emails/components/EmailThreadMessage.tsx +++ b/packages/twenty-front/src/modules/activities/emails/components/EmailThreadMessage.tsx @@ -6,6 +6,7 @@ import { EmailThreadMessageBodyPreview } from '@/activities/emails/components/Em import { EmailThreadMessageReceivers } from '@/activities/emails/components/EmailThreadMessageReceivers'; import { EmailThreadMessageSender } from '@/activities/emails/components/EmailThreadMessageSender'; import { type EmailThreadMessageParticipant } from '@/activities/emails/types/EmailThreadMessageParticipant'; +import { MessageParticipantRole } from 'twenty-shared/types'; const StyledThreadMessage = styled.div` border-bottom: 1px solid ${({ theme }) => theme.border.color.light}; @@ -45,7 +46,7 @@ export const EmailThreadMessage = ({ const [isOpen, setIsOpen] = useState(isExpanded); const receivers = participants.filter( - (participant) => participant.role !== 'from', + (participant) => participant.role !== MessageParticipantRole.FROM, ); if (!sender || receivers.length === 0) { diff --git a/packages/twenty-front/src/modules/activities/emails/types/EmailParticipantRole.ts b/packages/twenty-front/src/modules/activities/emails/types/EmailParticipantRole.ts deleted file mode 100644 index f852b3ac07..0000000000 --- a/packages/twenty-front/src/modules/activities/emails/types/EmailParticipantRole.ts +++ /dev/null @@ -1 +0,0 @@ -export type EmailParticipantRole = 'from' | 'to' | 'cc' | 'bcc'; diff --git a/packages/twenty-front/src/modules/activities/emails/types/EmailThreadMessageParticipant.ts b/packages/twenty-front/src/modules/activities/emails/types/EmailThreadMessageParticipant.ts index 937e988aab..f531b38672 100644 --- a/packages/twenty-front/src/modules/activities/emails/types/EmailThreadMessageParticipant.ts +++ b/packages/twenty-front/src/modules/activities/emails/types/EmailThreadMessageParticipant.ts @@ -1,12 +1,12 @@ -import { type EmailParticipantRole } from '@/activities/emails/types/EmailParticipantRole'; import { type Person } from '@/people/types/Person'; import { type WorkspaceMember } from '@/workspace-member/types/WorkspaceMember'; +import { type MessageParticipantRole } from 'twenty-shared/types'; export type EmailThreadMessageParticipant = { id: string; displayName: string; handle: string; - role: EmailParticipantRole; + role: MessageParticipantRole; messageId: string; person: Person; workspaceMember: WorkspaceMember; diff --git a/packages/twenty-front/src/modules/activities/emails/utils/__tests__/getDisplayNameFromParticipant.test.ts b/packages/twenty-front/src/modules/activities/emails/utils/__tests__/getDisplayNameFromParticipant.test.ts index ae0e2455c5..a343a658c3 100644 --- a/packages/twenty-front/src/modules/activities/emails/utils/__tests__/getDisplayNameFromParticipant.test.ts +++ b/packages/twenty-front/src/modules/activities/emails/utils/__tests__/getDisplayNameFromParticipant.test.ts @@ -1,5 +1,6 @@ import { type EmailThreadMessageParticipant } from '@/activities/emails/types/EmailThreadMessageParticipant'; +import { MessageParticipantRole } from 'twenty-shared/types'; import { getDisplayNameFromParticipant } from '../getDisplayNameFromParticipant'; describe('getDisplayNameFromParticipant', () => { @@ -8,7 +9,7 @@ describe('getDisplayNameFromParticipant', () => { __typename: 'EmailThreadMessageParticipant', displayName: '', handle: '', - role: 'from', + role: MessageParticipantRole.FROM, messageId: '638f52d1-fd55-4a2b-b0f3-9858ea3b2e91', person: { __typename: 'Person', @@ -51,23 +52,23 @@ describe('getDisplayNameFromParticipant', () => { }, }; - const participantWithHandle: any = { + const participantWithHandle = { displayName: '', handle: 'user_handle', - role: 'from', - }; + role: MessageParticipantRole.FROM, + } as EmailThreadMessageParticipant; - const participantWithDisplayName: any = { + const participantWithDisplayName = { displayName: 'User123', handle: '', - role: 'from', - }; + role: MessageParticipantRole.FROM, + } as EmailThreadMessageParticipant; - const participantWithoutInfo: any = { + const participantWithoutInfo = { displayName: '', handle: '', - role: 'from', - }; + role: MessageParticipantRole.FROM, + } as EmailThreadMessageParticipant; it('should return full name when shouldUseFullName is true', () => { expect( diff --git a/packages/twenty-front/src/modules/command-menu/pages/message-thread/hooks/__tests__/useEmailThreadInCommandMenu.test.tsx b/packages/twenty-front/src/modules/command-menu/pages/message-thread/hooks/__tests__/useEmailThreadInCommandMenu.test.tsx index bcc2d2ad34..5fded4ab5c 100644 --- a/packages/twenty-front/src/modules/command-menu/pages/message-thread/hooks/__tests__/useEmailThreadInCommandMenu.test.tsx +++ b/packages/twenty-front/src/modules/command-menu/pages/message-thread/hooks/__tests__/useEmailThreadInCommandMenu.test.tsx @@ -7,6 +7,7 @@ import { QUERY_DEFAULT_LIMIT_RECORDS, QUERY_MAX_RECORDS, } from 'twenty-shared/constants'; +import { MessageParticipantRole } from 'twenty-shared/types'; import { generateEmptyJestRecordNode } from '~/testing/jest/generateEmptyJestRecordNode'; import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper'; import { useEmailThreadInCommandMenu } from '../useEmailThreadInCommandMenu'; @@ -313,7 +314,10 @@ const mocks = [ } `, variables: { - filter: { messageId: { in: ['1', '2'] }, role: { eq: 'from' } }, + filter: { + messageId: { in: ['1', '2'] }, + role: { eq: MessageParticipantRole.FROM }, + }, orderBy: undefined, lastCursor: undefined, limit: QUERY_DEFAULT_LIMIT_RECORDS, @@ -328,7 +332,7 @@ const mocks = [ objectNameSingular: 'messageParticipant', input: { id: 'messageParticipant-1', - role: 'from', + role: MessageParticipantRole.FROM, messageId: '1', }, }), @@ -339,7 +343,7 @@ const mocks = [ objectNameSingular: 'messageParticipant', input: { id: 'messageParticipant-2', - role: 'from', + role: MessageParticipantRole.FROM, messageId: '2', }, }), @@ -401,7 +405,7 @@ describe('useEmailThreadInCommandMenu', () => { id: 'messageParticipant-1', messageId: '1', person: null, - role: 'from', + role: MessageParticipantRole.FROM, workspaceMember: null, }, subject: '', @@ -422,7 +426,7 @@ describe('useEmailThreadInCommandMenu', () => { id: 'messageParticipant-2', messageId: '2', person: null, - role: 'from', + role: MessageParticipantRole.FROM, workspaceMember: null, }, subject: '', diff --git a/packages/twenty-front/src/modules/command-menu/pages/message-thread/hooks/useEmailThreadInCommandMenu.ts b/packages/twenty-front/src/modules/command-menu/pages/message-thread/hooks/useEmailThreadInCommandMenu.ts index 09c2d40df4..806255016d 100644 --- a/packages/twenty-front/src/modules/command-menu/pages/message-thread/hooks/useEmailThreadInCommandMenu.ts +++ b/packages/twenty-front/src/modules/command-menu/pages/message-thread/hooks/useEmailThreadInCommandMenu.ts @@ -14,6 +14,7 @@ import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord'; import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore'; import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; +import { MessageParticipantRole } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; // to improve - https://github.com/twentyhq/twenty/issues/12190 @@ -82,7 +83,7 @@ export const useEmailThreadInCommandMenu = () => { in: messages.map(({ id }) => id), }, role: { - eq: 'from', + eq: MessageParticipantRole.FROM, }, }, objectNameSingular: CoreObjectNameSingular.MessageParticipant, diff --git a/packages/twenty-front/src/pages/settings/data-model/utils/computeMetadataNameFromLabel.ts b/packages/twenty-front/src/pages/settings/data-model/utils/computeMetadataNameFromLabel.ts index aaad22006b..f0d5f276c9 100644 --- a/packages/twenty-front/src/pages/settings/data-model/utils/computeMetadataNameFromLabel.ts +++ b/packages/twenty-front/src/pages/settings/data-model/utils/computeMetadataNameFromLabel.ts @@ -4,7 +4,7 @@ import { computeMetadataNameFromLabel as computeMetadataNameFromLabelCore } from // This is needed for form validation and UI components that prefer graceful degradation export const computeMetadataNameFromLabel = (label: string): string => { try { - return computeMetadataNameFromLabelCore(label); + return computeMetadataNameFromLabelCore({ label }); } catch { return ''; } diff --git a/packages/twenty-front/src/testing/mock-data/generated/mock-metadata-query-result.ts b/packages/twenty-front/src/testing/mock-data/generated/mock-metadata-query-result.ts index 196b199812..6168eb358d 100644 --- a/packages/twenty-front/src/testing/mock-data/generated/mock-metadata-query-result.ts +++ b/packages/twenty-front/src/testing/mock-data/generated/mock-metadata-query-result.ts @@ -3877,7 +3877,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = "isUnique": false, "createdAt": "2025-06-09T18:53:47.000Z", "updatedAt": "2025-06-09T18:53:47.000Z", - "defaultValue": "'from'", + "defaultValue": "'FROM'", "options": [ { "id": "edc6f452-4ecc-4ff7-b30c-18575dbe0bfd", @@ -8594,7 +8594,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = "__typename": "Field", "id": "21f219f9-c531-4ab5-9640-69230baa83d0", "type": "TEXT", - "name": "iCalUID", + "name": "iCalUid", "isCustom": false, "isActive": true, "isSystem": false, diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-13/1-13-migrate-standard-invalid-entities.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-13/1-13-migrate-standard-invalid-entities.command.ts new file mode 100644 index 0000000000..cf0d10b1c6 --- /dev/null +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-13/1-13-migrate-standard-invalid-entities.command.ts @@ -0,0 +1,380 @@ +import { InjectRepository } from '@nestjs/typeorm'; + +import { Command } from 'nest-commander'; +import { STANDARD_OBJECT_IDS } from 'twenty-shared/metadata'; +import { + FieldMetadataComplexOption, + FieldMetadataDefaultValue, + FieldMetadataType, + FromTo, +} from 'twenty-shared/types'; +import { isDefined } from 'twenty-shared/utils'; +import { type Repository } from 'typeorm'; + +import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner'; +import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner'; +import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; +import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; +import { FieldMetadataService } from 'src/engine/metadata-modules/field-metadata/services/field-metadata.service'; +import { MetadataFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/metadata-flat-entity-maps.type'; +import { findManyFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-many-flat-entity-by-id-in-flat-entity-maps-or-throw.util'; +import { isFlatFieldMetadataOfType } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-flat-field-metadata-of-type.util'; +import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; +import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service'; +import { AllStandardObjectFieldName } from 'src/engine/workspace-manager/twenty-standard-application/types/all-standard-object-field-name.type'; +import { AllStandardObjectName } from 'src/engine/workspace-manager/twenty-standard-application/types/all-standard-object-name.type'; +import { + CALENDAR_EVENT_STANDARD_FIELD_IDS, + MESSAGE_CHANNEL_STANDARD_FIELD_IDS, + MESSAGE_PARTICIPANT_STANDARD_FIELD_IDS, + MESSAGE_STANDARD_FIELD_IDS, + WORKSPACE_MEMBER_STANDARD_FIELD_IDS, +} from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids'; + +const findStandardFlatObjectMetadataOrThrow = ({ + flatObjectMetadataMaps, + standardId, +}: { + flatObjectMetadataMaps: MetadataFlatEntityMaps<'objectMetadata'>; + standardId: string; +}): FlatObjectMetadata => { + const flatObjectMetadata = Object.values(flatObjectMetadataMaps.byId).find( + (flatObjectMetadata) => flatObjectMetadata?.standardId === standardId, + ); + + if (!isDefined(flatObjectMetadata)) { + throw new Error(`Could not find standard object ${standardId}`); + } + + return flatObjectMetadata; +}; + +const findStandardFlatFieldMetadatawOrThrow = ({ + flatFieldMetadataMaps, + flatObjectMetadata, + standardId, +}: { + standardId: string; + flatObjectMetadata: FlatObjectMetadata; + flatFieldMetadataMaps: MetadataFlatEntityMaps<'fieldMetadata'>; +}) => { + const objectFlatFieldMetadata = findManyFlatEntityByIdInFlatEntityMapsOrThrow( + { + flatEntityIds: flatObjectMetadata.fieldMetadataIds, + flatEntityMaps: flatFieldMetadataMaps, + }, + ); + + const flatFieldMetadata = objectFlatFieldMetadata.find( + (flatFieldMetadata) => flatFieldMetadata.standardId === standardId, + ); + + if (!isDefined(flatFieldMetadata)) { + throw new Error(`Could not find standard field ${standardId}`); + } + + return flatFieldMetadata; +}; + +const ENUM_FIELD_OPTIONS_TO_MUTATE = { + messageChannelMessageAssociation: { + fieldStandardId: MESSAGE_STANDARD_FIELD_IDS.direction, + field: 'direction', + defaultValue: { + fromDefaultValue: "'incoming'", + toDefaultValue: "'INCOMING'", + }, + options: [ + { + fromOption: { + value: 'incoming', + label: 'Incoming', + position: 0, + color: 'green', + }, + toOption: { + value: 'INCOMING', + label: 'Incoming', + position: 0, + color: 'green', + }, + }, + { + fromOption: { + value: 'outgoing', + label: 'Outgoing', + position: 1, + color: 'blue', + }, + toOption: { + value: 'OUTGOING', + label: 'Outgoing', + position: 1, + color: 'blue', + }, + }, + ], + }, + messageChannel: { + fieldStandardId: MESSAGE_CHANNEL_STANDARD_FIELD_IDS.type, + field: 'type', + defaultValue: { fromDefaultValue: "'email'", toDefaultValue: "'EMAIL'" }, + options: [ + { + fromOption: { + value: 'email', + label: 'Email', + position: 0, + color: 'green', + }, + toOption: { + value: 'EMAIL', + label: 'Email', + position: 0, + color: 'green', + }, + }, + { + fromOption: { value: 'sms', label: 'SMS', position: 1, color: 'blue' }, + toOption: { value: 'SMS', label: 'SMS', position: 1, color: 'blue' }, + }, + ], + }, + messageParticipant: { + fieldStandardId: MESSAGE_PARTICIPANT_STANDARD_FIELD_IDS.role, + field: 'role', + defaultValue: { fromDefaultValue: "'from'", toDefaultValue: "'FROM'" }, + options: [ + { + fromOption: { + value: 'from', + label: 'From', + position: 0, + color: 'green', + }, + toOption: { value: 'FROM', label: 'From', position: 0, color: 'green' }, + }, + { + fromOption: { value: 'to', label: 'To', position: 1, color: 'blue' }, + toOption: { value: 'TO', label: 'To', position: 1, color: 'blue' }, + }, + { + fromOption: { value: 'cc', label: 'Cc', position: 2, color: 'orange' }, + toOption: { value: 'CC', label: 'Cc', position: 2, color: 'orange' }, + }, + { + fromOption: { value: 'bcc', label: 'Bcc', position: 3, color: 'red' }, + toOption: { value: 'BCC', label: 'Bcc', position: 3, color: 'red' }, + }, + ], + }, + workspaceMember: { + fieldStandardId: WORKSPACE_MEMBER_STANDARD_FIELD_IDS.numberFormat, + field: 'numberFormat', + defaultValue: { fromDefaultValue: "'system'", toDefaultValue: "'SYSTEM'" }, + options: [ + { + fromOption: { + value: 'SYSTEM', + label: 'System', + position: 0, + color: 'turquoise', + }, + toOption: { + value: 'SYSTEM', + label: 'System', + position: 0, + color: 'turquoise', + }, + }, + { + fromOption: { + value: 'COMMAS_AND_DOT', + label: 'Commas and dot (1,234.56)', + position: 1, + color: 'blue', + }, + toOption: { + value: 'COMMAS_AND_DOT', + label: 'Commas and dot', + position: 1, + color: 'blue', + }, + }, + { + fromOption: { + value: 'SPACES_AND_COMMA', + label: 'Spaces and comma (1 234,56)', + position: 2, + color: 'green', + }, + toOption: { + value: 'SPACES_AND_COMMA', + label: 'Spaces and comma', + position: 2, + color: 'green', + }, + }, + { + fromOption: { + value: 'DOTS_AND_COMMA', + label: 'Dots and comma (1.234,56)', + position: 3, + color: 'orange', + }, + toOption: { + value: 'DOTS_AND_COMMA', + label: 'Dots and comma', + position: 3, + color: 'orange', + }, + }, + { + fromOption: { + value: 'APOSTROPHE_AND_DOT', + label: "Apostrophe and dot (1'234.56)", + position: 4, + color: 'purple', + }, + toOption: { + value: 'APOSTROPHE_AND_DOT', + label: 'Apostrophe and dot', + position: 4, + color: 'purple', + }, + }, + ], + }, +} as const satisfies { + [P in AllStandardObjectName]?: { + fieldStandardId: string; + field: AllStandardObjectFieldName

; + options: FromTo[]; + defaultValue: FromTo; + }; +}; + +@Command({ + name: 'upgrade:1-13:migrate-standard-invalid-entities', + description: 'Migrate standard invalid entities', +}) +export class MigrateStandardInvalidEntitiesCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner { + constructor( + @InjectRepository(WorkspaceEntity) + protected readonly workspaceRepository: Repository, + protected readonly twentyORMGlobalManager: TwentyORMGlobalManager, + protected readonly dataSourceService: DataSourceService, + private readonly workspaceCacheService: WorkspaceCacheService, + private readonly fieldMetadataService: FieldMetadataService, + ) { + super(workspaceRepository, twentyORMGlobalManager, dataSourceService); + } + + override async runOnWorkspace({ + options, + workspaceId, + }: RunOnWorkspaceArgs): Promise { + const isDryRun = options.dryRun || false; + + this.logger.log( + `MigrateStandardInvalidEntitiesCommand starting for workspace ${workspaceId}`, + ); + const { flatFieldMetadataMaps, flatObjectMetadataMaps } = + await this.workspaceCacheService.getOrRecompute(workspaceId, [ + 'flatFieldMetadataMaps', + 'flatObjectMetadataMaps', + ]); + + const flatCalendarEventObject = findStandardFlatObjectMetadataOrThrow({ + standardId: STANDARD_OBJECT_IDS.calendarEvent, + flatObjectMetadataMaps, + }); + + const iCalUidFieldId = findStandardFlatFieldMetadatawOrThrow({ + flatFieldMetadataMaps, + flatObjectMetadata: flatCalendarEventObject, + standardId: CALENDAR_EVENT_STANDARD_FIELD_IDS.iCalUid, + }).id; + + const allAffectedObjectNames = Object.keys( + ENUM_FIELD_OPTIONS_TO_MUTATE, + ).filter(isDefined) as (keyof typeof ENUM_FIELD_OPTIONS_TO_MUTATE)[]; + + const flatEnumFieldMetadatasToUpdate = allAffectedObjectNames.flatMap<{ + id: string; + options: FieldMetadataComplexOption[]; + defaultValue: FieldMetadataDefaultValue; + }>((objectName) => { + const { defaultValue, field, options, fieldStandardId } = + ENUM_FIELD_OPTIONS_TO_MUTATE[objectName]; + + const objectStandardId = STANDARD_OBJECT_IDS[objectName]; + + const flatObjectMetadata = findStandardFlatObjectMetadataOrThrow({ + flatObjectMetadataMaps, + standardId: objectStandardId, + }); + + const flatFieldMetadata = findStandardFlatFieldMetadatawOrThrow({ + flatFieldMetadataMaps, + flatObjectMetadata, + standardId: fieldStandardId, + }); + + if ( + flatFieldMetadata.name !== field || + !isFlatFieldMetadataOfType(flatFieldMetadata, FieldMetadataType.SELECT) + ) { + throw new Error( + `Retrieved flat field is not the one expected expected ${field} got ${flatFieldMetadata.name} in workspace ${workspaceId}`, + ); + } + + const updatedOptions = flatFieldMetadata.options.map((option) => { + const matchingNewOption = options.find( + ({ fromOption }) => fromOption.value === option.value, + ); + + if (!isDefined(matchingNewOption)) { + return option; + } + + return { + ...matchingNewOption.toOption, + id: option.id, + }; + }); + + const updatedDefaultValue: FieldMetadataDefaultValue = + flatFieldMetadata.defaultValue === defaultValue.fromDefaultValue + ? defaultValue.toDefaultValue + : flatFieldMetadata.defaultValue; + + return { + id: flatFieldMetadata.id, + options: updatedOptions, + defaultValue: updatedDefaultValue, + }; + }); + + const allUpdates = [ + { + id: iCalUidFieldId, + name: 'iCalUid', + }, + ...flatEnumFieldMetadatasToUpdate, + ]; + + if (!isDryRun) { + for (const updateFieldInput of allUpdates) { + await this.fieldMetadataService.updateOneField({ + updateFieldInput, + workspaceId, + isSystemBuild: true, + }); + } + } + this.logger.log('Migrated standard invalid entities'); + } +} diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-13/1-13-upgrade-version-command.module.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-13/1-13-upgrade-version-command.module.ts index b05f4db5f0..c3b4b954bf 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-13/1-13-upgrade-version-command.module.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-13/1-13-upgrade-version-command.module.ts @@ -5,6 +5,7 @@ import { BackfillPageLayoutUniversalIdentifiersCommand } from 'src/database/comm import { BackfillViewMainGroupByFieldMetadataIdCommand } from 'src/database/commands/upgrade-version-command/1-13/1-13-backfill-view-main-group-by-field-metadata-id.command'; import { CleanEmptyStringNullInTextFieldsCommand } from 'src/database/commands/upgrade-version-command/1-13/1-13-clean-empty-string-null-in-text-fields.command'; import { DeduplicateRoleTargetsCommand } from 'src/database/commands/upgrade-version-command/1-13/1-13-deduplicate-role-targets.command'; +import { MigrateStandardInvalidEntitiesCommand } from 'src/database/commands/upgrade-version-command/1-13/1-13-migrate-standard-invalid-entities.command'; import { RenameIndexNameCommand } from 'src/database/commands/upgrade-version-command/1-13/1-13-rename-unique-index.command'; import { UpdateRoleTargetsUniqueConstraintMigrationCommand } from 'src/database/commands/upgrade-version-command/1-13/1-13-update-role-targets-unique-constraint-migration.command'; import { ApplicationModule } from 'src/engine/core-modules/application/application.module'; @@ -12,6 +13,7 @@ import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature- import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module'; import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { FieldMetadataModule } from 'src/engine/metadata-modules/field-metadata/field-metadata.module'; import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { PageLayoutTabEntity } from 'src/engine/metadata-modules/page-layout/entities/page-layout-tab.entity'; @@ -40,6 +42,7 @@ import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache DataSourceModule, ApplicationModule, WorkspaceCacheModule, + FieldMetadataModule, ], providers: [ CleanEmptyStringNullInTextFieldsCommand, @@ -47,6 +50,7 @@ import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache BackfillPageLayoutUniversalIdentifiersCommand, DeduplicateRoleTargetsCommand, RenameIndexNameCommand, + MigrateStandardInvalidEntitiesCommand, UpdateRoleTargetsUniqueConstraintMigrationCommand, ], exports: [ @@ -55,6 +59,7 @@ import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache BackfillPageLayoutUniversalIdentifiersCommand, DeduplicateRoleTargetsCommand, RenameIndexNameCommand, + MigrateStandardInvalidEntitiesCommand, UpdateRoleTargetsUniqueConstraintMigrationCommand, ], }) diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/upgrade.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/upgrade.command.ts index 84cd350622..b8d4f1a85f 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/upgrade.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/upgrade.command.ts @@ -29,6 +29,7 @@ import { SetStandardApplicationNotUninstallableCommand } from 'src/database/comm import { WorkspaceCustomApplicationIdNonNullableCommand } from 'src/database/commands/upgrade-version-command/1-12/1-12-workspace-custom-application-id-non-nullable-migration.command'; import { BackfillPageLayoutUniversalIdentifiersCommand } from 'src/database/commands/upgrade-version-command/1-13/1-13-backfill-page-layout-universal-identifiers.command'; import { DeduplicateRoleTargetsCommand } from 'src/database/commands/upgrade-version-command/1-13/1-13-deduplicate-role-targets.command'; +import { MigrateStandardInvalidEntitiesCommand } from 'src/database/commands/upgrade-version-command/1-13/1-13-migrate-standard-invalid-entities.command'; import { UpdateRoleTargetsUniqueConstraintMigrationCommand } from 'src/database/commands/upgrade-version-command/1-13/1-13-update-role-targets-unique-constraint-migration.command'; import { FixLabelIdentifierPositionAndVisibilityCommand } from 'src/database/commands/upgrade-version-command/1-6/1-6-fix-label-identifier-position-and-visibility.command'; import { BackfillWorkflowManualTriggerAvailabilityCommand } from 'src/database/commands/upgrade-version-command/1-7/1-7-backfill-workflow-manual-trigger-availability.command'; @@ -100,6 +101,7 @@ export class UpgradeCommand extends UpgradeCommandRunner { protected readonly deduplicateRoleTargetsCommand: DeduplicateRoleTargetsCommand, protected readonly updateRoleTargetsUniqueConstraintMigrationCommand: UpdateRoleTargetsUniqueConstraintMigrationCommand, protected readonly backfillPageLayoutUniversalIdentifiersCommand: BackfillPageLayoutUniversalIdentifiersCommand, + protected readonly migrateStandardInvalidEntitiesCommand: MigrateStandardInvalidEntitiesCommand, ) { super( workspaceRepository, @@ -172,6 +174,7 @@ export class UpgradeCommand extends UpgradeCommandRunner { this.deduplicateRoleTargetsCommand, this.updateRoleTargetsUniqueConstraintMigrationCommand, this.backfillPageLayoutUniversalIdentifiersCommand, + this.migrateStandardInvalidEntitiesCommand, ], afterSyncMetadata: [], }; diff --git a/packages/twenty-server/src/engine/core-modules/messaging/services/timeline-messaging.service.ts b/packages/twenty-server/src/engine/core-modules/messaging/services/timeline-messaging.service.ts index a6c86d149d..adbd2bfa2b 100644 --- a/packages/twenty-server/src/engine/core-modules/messaging/services/timeline-messaging.service.ts +++ b/packages/twenty-server/src/engine/core-modules/messaging/services/timeline-messaging.service.ts @@ -1,6 +1,7 @@ import { Injectable } from '@nestjs/common'; import { In } from 'typeorm'; +import { MessageParticipantRole } from 'twenty-shared/types'; import { type TimelineThreadDTO } from 'src/engine/core-modules/messaging/dtos/timeline-thread.dto'; import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; @@ -115,7 +116,9 @@ export class TimelineMessagingService { .where('message.messageThreadId = ANY(:messageThreadIds)', { messageThreadIds, }) - .andWhere('messageParticipant.role = :role', { role: 'from' }) + .andWhere('messageParticipant.role = :role', { + role: MessageParticipantRole.FROM, + }) .orderBy('message.messageThreadId') .distinctOn(['message.messageThreadId', 'messageParticipant.handle']) .getMany(); diff --git a/packages/twenty-server/src/engine/core-modules/messaging/utils/filter-active-participants.util.ts b/packages/twenty-server/src/engine/core-modules/messaging/utils/filter-active-participants.util.ts index 2bd414f838..0d624a0a21 100644 --- a/packages/twenty-server/src/engine/core-modules/messaging/utils/filter-active-participants.util.ts +++ b/packages/twenty-server/src/engine/core-modules/messaging/utils/filter-active-participants.util.ts @@ -1,7 +1,11 @@ +import { MessageParticipantRole } from 'twenty-shared/types'; + import { type MessageParticipantWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-participant.workspace-entity'; export const filterActiveParticipants = ( participants: MessageParticipantWorkspaceEntity[], ): MessageParticipantWorkspaceEntity[] => { - return participants.filter((participant) => participant.role === 'from'); + return participants.filter( + (participant) => participant.role === MessageParticipantRole.FROM, + ); }; diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent/utils/from-create-agent-input-to-flat-agent.util.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent/utils/from-create-agent-input-to-flat-agent.util.ts index 5a38cbd6c8..7a6fcadc50 100644 --- a/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent/utils/from-create-agent-input-to-flat-agent.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent/utils/from-create-agent-input-to-flat-agent.util.ts @@ -1,10 +1,10 @@ import { isNonEmptyString } from '@sniptt/guards'; +import { computeMetadataNameFromLabel } from 'twenty-shared/metadata'; import { isDefined, trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties, } from 'twenty-shared/utils'; import { v4 } from 'uuid'; -import { computeMetadataNameFromLabel } from 'twenty-shared/metadata'; import { type CreateAgentInput } from 'src/engine/metadata-modules/ai/ai-agent/dtos/create-agent.input'; import { type FlatAgent } from 'src/engine/metadata-modules/flat-agent/types/flat-agent.type'; @@ -48,7 +48,7 @@ export const fromCreateAgentInputToFlatAgent = ({ standardId, name: isNonEmptyString(createAgentInput.name) ? createAgentInput.name - : computeMetadataNameFromLabel(createAgentInput.label), + : computeMetadataNameFromLabel({ label: createAgentInput.label }), label: createAgentInput.label, icon: createAgentInput.icon ?? null, description: createAgentInput.description ?? null, diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata.service.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata.service.ts index feb781313c..1e626ea5f2 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata.service.ts @@ -181,6 +181,7 @@ export class FieldMetadataService extends TypeOrmQueryService; // Note: Standard override is way too complex we should land a smoother implemenentation once we standardize // them across every flat entities @@ -34,11 +35,13 @@ export const computeFlatFieldToUpdateAndRelatedFlatFieldToUpdate = ({ rawUpdateFieldInput, flatFieldMetadataMaps, flatObjectMetadata, + isSystemBuild, }: ComputeFlatFieldToUpdateAndRelatedFlatFieldToUpdateArgs): ComputeFlatFieldToUpdateAndRelatedFlatFieldToUpdateReturnType => { const { standardOverrides, updatedEditableFieldProperties } = sanitizeRawUpdateFieldInput({ existingFlatFieldMetadata: fromFlatFieldMetadata, rawUpdateFieldInput, + isSystemBuild, }); const isStandardField = isStandardMetadata(fromFlatFieldMetadata); @@ -48,7 +51,7 @@ export const computeFlatFieldToUpdateAndRelatedFlatFieldToUpdate = ({ existing: fromFlatFieldMetadata, properties: FLAT_FIELD_METADATA_EDITABLE_PROPERTIES[ - isStandardField ? 'standard' : 'custom' + isStandardField && !isSystemBuild ? 'standard' : 'custom' ], update: updatedEditableFieldProperties, }), @@ -104,7 +107,7 @@ export const computeFlatFieldToUpdateAndRelatedFlatFieldToUpdate = ({ >((relatedFlatFieldMetadataFrom) => { const relatedMorphPropertiesToUpdateTo = FLAT_FIELD_METADATA_MORPH_RELATION_EDITABLE_PROPERTIES_ON_SIBLING_MORPH_RELATION_UPDATE_CONSTANT[ - isStandardField ? 'standard' : 'custom' + isStandardField && !isSystemBuild ? 'standard' : 'custom' ]; const relatedFlatFieldMetadataTo = { ...mergeUpdateInExistingRecord({ diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-update-field-input-to-flat-field-metadata.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-update-field-input-to-flat-field-metadata.util.ts index 388ada54cf..a8ff395b02 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-update-field-input-to-flat-field-metadata.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/from-update-field-input-to-flat-field-metadata.util.ts @@ -24,6 +24,7 @@ import { isFlatFieldMetadataOfType } from 'src/engine/metadata-modules/flat-fiel type FromUpdateFieldInputToFlatFieldMetadataArgs = { updateFieldInput: UpdateFieldInput; workspaceCustomApplicationId: string; + isSystemBuild: boolean; } & Pick< AllFlatEntityMaps, | 'flatObjectMetadataMaps' @@ -49,6 +50,7 @@ export const fromUpdateFieldInputToFlatFieldMetadata = ({ flatViewGroupMaps, flatViewMaps, flatViewFieldMaps, + isSystemBuild, }: FromUpdateFieldInputToFlatFieldMetadataArgs): FieldInputTranspilationResult => { const updateFieldInputInformalProperties = extractAndSanitizeObjectStringFields(rawUpdateFieldInput, [ @@ -98,6 +100,7 @@ export const fromUpdateFieldInputToFlatFieldMetadata = ({ flatObjectMetadata, fromFlatFieldMetadata: existingFlatFieldMetadataToUpdate, rawUpdateFieldInput, + isSystemBuild, }); const { flatFieldMetadatasToCreate, flatIndexMetadatasToCreate } = diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/generate-morph-or-relation-flat-field-metadata-pair.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/generate-morph-or-relation-flat-field-metadata-pair.util.ts index 7b9b085811..fbec150898 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/generate-morph-or-relation-flat-field-metadata-pair.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/generate-morph-or-relation-flat-field-metadata-pair.util.ts @@ -1,10 +1,10 @@ +import { computeMetadataNameFromLabel } from 'twenty-shared/metadata'; import { FieldMetadataType, RelationOnDeleteAction, RelationType, } from 'twenty-shared/types'; import { v4 } from 'uuid'; -import { computeMetadataNameFromLabel } from 'twenty-shared/metadata'; import { type CreateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/create-field.input'; import { type MorphOrRelationFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/types/morph-or-relation-field-metadata-type.type'; @@ -100,7 +100,9 @@ export const generateMorphOrRelationFlatFieldMetadataPair = ({ label: relationCreationPayload.targetFieldLabel, name: targetFieldName ?? - computeMetadataNameFromLabel(relationCreationPayload.targetFieldLabel), + computeMetadataNameFromLabel({ + label: relationCreationPayload.targetFieldLabel, + }), objectMetadataId: targetFlatObjectMetadata.id, type: FieldMetadataType.RELATION, workspaceId, diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/is-flat-field-metadata-name-synced-with-label.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/is-flat-field-metadata-name-synced-with-label.util.ts index 884006091d..4279f0855b 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/is-flat-field-metadata-name-synced-with-label.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/is-flat-field-metadata-name-synced-with-label.util.ts @@ -2,13 +2,20 @@ import { computeMetadataNameFromLabel } from 'twenty-shared/metadata'; import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; -export const isFlatFieldMetadataNameSyncedWithLabel = ( +export const isFlatFieldMetadataNameSyncedWithLabel = ({ + flatFieldMetadata, + isSystemBuild, +}: { flatFieldMetadata: Pick< FlatFieldMetadata, 'name' | 'isLabelSyncedWithName' | 'label' - >, -) => { - const computedName = computeMetadataNameFromLabel(flatFieldMetadata.label); + >; + isSystemBuild: boolean; +}) => { + const computedName = computeMetadataNameFromLabel({ + label: flatFieldMetadata.label, + applyCustomSuffix: !isSystemBuild, + }); return flatFieldMetadata.name === computedName; }; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/sanitize-raw-update-field-input.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/sanitize-raw-update-field-input.ts index 1a5da3b5c8..a2cdd5a2a0 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/sanitize-raw-update-field-input.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/sanitize-raw-update-field-input.ts @@ -18,10 +18,12 @@ import { isStandardMetadata } from 'src/engine/metadata-modules/utils/is-standar type SanitizeRawUpdateFieldInputArgs = { rawUpdateFieldInput: UpdateFieldInput; existingFlatFieldMetadata: FlatFieldMetadata; + isSystemBuild: boolean; }; export const sanitizeRawUpdateFieldInput = ({ existingFlatFieldMetadata, rawUpdateFieldInput, + isSystemBuild, }: SanitizeRawUpdateFieldInputArgs) => { const isStandardField = isStandardMetadata(existingFlatFieldMetadata); const updatedEditableFieldProperties = extractAndSanitizeObjectStringFields( @@ -43,7 +45,7 @@ export const sanitizeRawUpdateFieldInput = ({ ...option, })); - if (!isStandardField) { + if (!isStandardField || isSystemBuild) { return { updatedEditableFieldProperties, standardOverrides: null, diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/are-flat-object-metadata-names-synced-with-labels.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/are-flat-object-metadata-names-synced-with-labels.util.ts index fd62de4f88..1785eeceef 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/are-flat-object-metadata-names-synced-with-labels.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/utils/are-flat-object-metadata-names-synced-with-labels.util.ts @@ -2,16 +2,25 @@ import { computeMetadataNameFromLabel } from 'twenty-shared/metadata'; import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; -export const areFlatObjectMetadataNamesSyncedWithLabels = ( +export const areFlatObjectMetadataNamesSyncedWithLabels = ({ + flatObjectdMetadata, + isSystemBuild, +}: { + isSystemBuild: boolean; flatObjectdMetadata: Pick< FlatObjectMetadata, 'namePlural' | 'nameSingular' | 'labelPlural' | 'labelSingular' - >, -) => { + >; +}) => { const [computedSingularName, computedPluralName] = [ flatObjectdMetadata.labelSingular, flatObjectdMetadata.labelPlural, - ].map(computeMetadataNameFromLabel); + ].map((label) => + computeMetadataNameFromLabel({ + label, + applyCustomSuffix: !isSystemBuild, + }), + ); return ( flatObjectdMetadata.nameSingular === computedSingularName && diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/validators/utils/validate-flat-object-metadata-name-and-labels.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/validators/utils/validate-flat-object-metadata-name-and-labels.util.ts index db087b92dd..ff6097a913 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/validators/utils/validate-flat-object-metadata-name-and-labels.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/validators/utils/validate-flat-object-metadata-name-and-labels.util.ts @@ -8,6 +8,7 @@ import { areFlatObjectMetadataNamesSyncedWithLabels } from 'src/engine/metadata- import { validateFlatObjectMetadataLabel } from 'src/engine/metadata-modules/flat-object-metadata/validators/utils/validate-flat-object-metadata-label.util'; import { validateFlatObjectMetadataNames } from 'src/engine/metadata-modules/flat-object-metadata/validators/utils/validate-flat-object-metadata-name.util'; import { ObjectMetadataExceptionCode } from 'src/engine/metadata-modules/object-metadata/object-metadata.exception'; +import { type WorkspaceMigrationBuilderOptions } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-builder-options.type'; type ValidateNoOtherObjectWithSameNameExistsOrThrowsParams = { objectMetadataNameSingular: string; @@ -36,9 +37,11 @@ export const doesOtherObjectWithSameNameExists = ({ export const validateFlatObjectMetadataNameAndLabels = ({ optimisticFlatObjectMetadataMaps, flatObjectMetadataToValidate, + buildOptions, }: { flatObjectMetadataToValidate: FlatObjectMetadata; optimisticFlatObjectMetadataMaps: FlatEntityMaps; + buildOptions: WorkspaceMigrationBuilderOptions; }): FlatObjectMetadataValidationError[] => { const errors: FlatObjectMetadataValidationError[] = []; @@ -58,7 +61,10 @@ export const validateFlatObjectMetadataNameAndLabels = ({ if ( flatObjectMetadataToValidate.isLabelSyncedWithName && - !areFlatObjectMetadataNamesSyncedWithLabels(flatObjectMetadataToValidate) + !areFlatObjectMetadataNamesSyncedWithLabels({ + flatObjectdMetadata: flatObjectMetadataToValidate, + isSystemBuild: buildOptions.isSystemBuild, + }) ) { errors.push({ code: ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT, diff --git a/packages/twenty-server/src/engine/metadata-modules/utils/compute-metadata-name-from-label-or-throw.util.ts b/packages/twenty-server/src/engine/metadata-modules/utils/compute-metadata-name-from-label-or-throw.util.ts index e4f5405e7d..14fccce18d 100644 --- a/packages/twenty-server/src/engine/metadata-modules/utils/compute-metadata-name-from-label-or-throw.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/utils/compute-metadata-name-from-label-or-throw.util.ts @@ -17,7 +17,7 @@ export const computeMetadataNameFromLabelOrThrow = (label: string): string => { } try { - return computeMetadataNameFromLabelCore(label); + return computeMetadataNameFromLabelCore({ label }); } catch (error) { if (error instanceof Error) { throw new InvalidMetadataException( diff --git a/packages/twenty-server/src/engine/twenty-orm/decorators/workspace-field.decorator.ts b/packages/twenty-server/src/engine/twenty-orm/decorators/workspace-field.decorator.ts index 410832b64d..2ec99939c6 100644 --- a/packages/twenty-server/src/engine/twenty-orm/decorators/workspace-field.decorator.ts +++ b/packages/twenty-server/src/engine/twenty-orm/decorators/workspace-field.decorator.ts @@ -82,7 +82,8 @@ export function WorkspaceField( generateDefaultValue(options.type)) as FieldMetadataDefaultValue | null; const name = propertyKey.toString(); const label = options.label.message ?? ''; - const isLabelSyncedWithName = computeMetadataNameFromLabel(label) === name; + const isLabelSyncedWithName = + computeMetadataNameFromLabel({ label }) === name; metadataArgsStorage.addFields({ target: object.constructor, diff --git a/packages/twenty-server/src/engine/twenty-orm/decorators/workspace-relation.decorator.ts b/packages/twenty-server/src/engine/twenty-orm/decorators/workspace-relation.decorator.ts index f58e20abe7..a8f68ebcb4 100644 --- a/packages/twenty-server/src/engine/twenty-orm/decorators/workspace-relation.decorator.ts +++ b/packages/twenty-server/src/engine/twenty-orm/decorators/workspace-relation.decorator.ts @@ -1,9 +1,9 @@ import { type MessageDescriptor } from '@lingui/core'; import { isDefined, isUUID } from 'class-validator'; +import { computeMetadataNameFromLabel } from 'twenty-shared/metadata'; +import { type RelationOnDeleteAction } from 'twenty-shared/types'; import { CustomError } from 'twenty-shared/utils'; import { type ObjectType } from 'typeorm'; -import { type RelationOnDeleteAction } from 'twenty-shared/types'; -import { computeMetadataNameFromLabel } from 'twenty-shared/metadata'; import { type RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; @@ -88,7 +88,8 @@ export function WorkspaceRelation( ); const name = propertyKey.toString(); const label = options.label.message ?? ''; - const isLabelSyncedWithName = computeMetadataNameFromLabel(label) === name; + const isLabelSyncedWithName = + computeMetadataNameFromLabel({ label }) === name; if (options.isMorphRelation && !isDefined(options.morphId)) { throw new CustomError( diff --git a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/data/constants/calendar-event-data-seeds.constant.ts b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/data/constants/calendar-event-data-seeds.constant.ts index 2d1016380f..4681fe2180 100644 --- a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/data/constants/calendar-event-data-seeds.constant.ts +++ b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/data/constants/calendar-event-data-seeds.constant.ts @@ -9,7 +9,7 @@ type CalendarEventDataSeed = { externalUpdatedAt: string; description: string; location: string; - iCalUID: string; + iCalUid: string; conferenceSolution: string; conferenceLinkPrimaryLinkLabel: string; conferenceLinkPrimaryLinkUrl: string; @@ -27,7 +27,7 @@ export const CALENDAR_EVENT_DATA_SEED_COLUMNS: (keyof CalendarEventDataSeed)[] = 'externalUpdatedAt', 'description', 'location', - 'iCalUID', + 'iCalUid', 'conferenceSolution', 'conferenceLinkPrimaryLinkLabel', 'conferenceLinkPrimaryLinkUrl', @@ -217,7 +217,7 @@ const GENERATE_CALENDAR_EVENT_SEEDS = (): CalendarEventDataSeed[] => { ).toISOString(), description: TEMPLATE.description, location: LOCATION, - iCalUID: `event${INDEX}@calendar.twentycrm.com`, + iCalUid: `event${INDEX}@calendar.twentycrm.com`, conferenceSolution: CONFERENCE_SOLUTION, conferenceLinkPrimaryLinkLabel: CONFERENCE_LINK, conferenceLinkPrimaryLinkUrl: CONFERENCE_LINK, diff --git a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/data/constants/message-channel-data-seeds.constant.ts b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/data/constants/message-channel-data-seeds.constant.ts index 1d1e1dd0b2..3b7dd728a1 100644 --- a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/data/constants/message-channel-data-seeds.constant.ts +++ b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/data/constants/message-channel-data-seeds.constant.ts @@ -1,6 +1,7 @@ import { CONNECTED_ACCOUNT_DATA_SEED_IDS } from 'src/engine/workspace-manager/dev-seeder/data/constants/connected-account-data-seeds.constant'; import { MessageChannelSyncStage, + MessageChannelType, MessageChannelVisibility, } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity'; @@ -10,7 +11,7 @@ type MessageChannelDataSeed = { updatedAt: Date; deletedAt: Date | null; isContactAutoCreationEnabled: boolean; - type: string; + type: MessageChannelType; connectedAccountId: string; handle: string; isSyncEnabled: boolean; @@ -54,7 +55,7 @@ export const MESSAGE_CHANNEL_DATA_SEEDS: MessageChannelDataSeed[] = [ updatedAt: new Date(), deletedAt: null, isContactAutoCreationEnabled: true, - type: 'email', + type: MessageChannelType.EMAIL, connectedAccountId: CONNECTED_ACCOUNT_DATA_SEED_IDS.TIM, handle: 'tim@apple.dev', isSyncEnabled: true, @@ -67,7 +68,7 @@ export const MESSAGE_CHANNEL_DATA_SEEDS: MessageChannelDataSeed[] = [ updatedAt: new Date(), deletedAt: null, isContactAutoCreationEnabled: true, - type: 'email', + type: MessageChannelType.EMAIL, connectedAccountId: CONNECTED_ACCOUNT_DATA_SEED_IDS.JONY, handle: 'jony.ive@apple.dev', isSyncEnabled: true, @@ -80,7 +81,7 @@ export const MESSAGE_CHANNEL_DATA_SEEDS: MessageChannelDataSeed[] = [ updatedAt: new Date(), deletedAt: null, isContactAutoCreationEnabled: true, - type: 'email', + type: MessageChannelType.EMAIL, connectedAccountId: CONNECTED_ACCOUNT_DATA_SEED_IDS.PHIL, handle: 'phil.schiler@apple.dev', isSyncEnabled: true, @@ -93,7 +94,7 @@ export const MESSAGE_CHANNEL_DATA_SEEDS: MessageChannelDataSeed[] = [ updatedAt: new Date(), deletedAt: null, isContactAutoCreationEnabled: true, - type: 'email', + type: MessageChannelType.EMAIL, connectedAccountId: CONNECTED_ACCOUNT_DATA_SEED_IDS.TIM, // Use TIM's connected account for shared inbox handle: 'support@apple.dev', isSyncEnabled: true, @@ -106,7 +107,7 @@ export const MESSAGE_CHANNEL_DATA_SEEDS: MessageChannelDataSeed[] = [ updatedAt: new Date(), deletedAt: null, isContactAutoCreationEnabled: true, - type: 'email', + type: MessageChannelType.EMAIL, connectedAccountId: CONNECTED_ACCOUNT_DATA_SEED_IDS.TIM, // Use TIM's connected account for shared inbox handle: 'sales@apple.dev', isSyncEnabled: true, diff --git a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/data/constants/message-participant-data-seeds.constant.ts b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/data/constants/message-participant-data-seeds.constant.ts index b6aea1e228..ce702d879c 100644 --- a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/data/constants/message-participant-data-seeds.constant.ts +++ b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/data/constants/message-participant-data-seeds.constant.ts @@ -1,3 +1,5 @@ +import { MessageParticipantRole } from 'twenty-shared/types'; + import { MESSAGE_DATA_SEED_IDS } from 'src/engine/workspace-manager/dev-seeder/data/constants/message-data-seeds.constant'; import { PERSON_DATA_SEED_IDS } from 'src/engine/workspace-manager/dev-seeder/data/constants/person-data-seeds.constant'; import { @@ -14,7 +16,7 @@ export type MessageParticipantDataSeed = { personId: string; displayName: string; handle: string; - role: string; + role: MessageParticipantRole; messageId: string; }; @@ -226,7 +228,9 @@ const CREATE_MESSAGE_PARTICIPANTS = ( for (let I = 0; I < TOTAL_PARTICIPANTS; I++) { const IS_SENDER = I === 0; - const ROLE = IS_SENDER ? 'from' : 'to'; + const ROLE = IS_SENDER + ? MessageParticipantRole.FROM + : MessageParticipantRole.TO; const HANDLE = IS_SENDER ? 'outgoing' : 'incoming'; // Random date within the last 3 months diff --git a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/constants/standard-object.constant.ts b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/constants/standard-object.constant.ts index 839aeab20c..d05a1e327a 100644 --- a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/constants/standard-object.constant.ts +++ b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/constants/standard-object.constant.ts @@ -271,8 +271,8 @@ export const STANDARD_OBJECTS = { location: { universalIdentifier: CALENDAR_EVENT_STANDARD_FIELD_IDS.location, }, - iCalUID: { - universalIdentifier: CALENDAR_EVENT_STANDARD_FIELD_IDS.iCalUID, + iCalUid: { + universalIdentifier: CALENDAR_EVENT_STANDARD_FIELD_IDS.iCalUid, }, conferenceSolution: { universalIdentifier: diff --git a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/compute-calendar-event-standard-flat-field-metadata.util.ts b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/compute-calendar-event-standard-flat-field-metadata.util.ts index 09b5e893d0..3f8d1a5761 100644 --- a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/compute-calendar-event-standard-flat-field-metadata.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/compute-calendar-event-standard-flat-field-metadata.util.ts @@ -210,11 +210,11 @@ export const buildCalendarEventStandardFlatFieldMetadatas = ({ }, standardFieldMetadataIdByObjectAndFieldName, }), - iCalUID: createStandardFieldFlatMetadata({ + iCalUid: createStandardFieldFlatMetadata({ objectName: 'calendarEvent', workspaceId, options: { - fieldName: 'iCalUID', + fieldName: 'iCalUid', type: FieldMetadataType.TEXT, label: 'iCal UID', description: 'iCal UID', diff --git a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/compute-message-channel-message-association-standard-flat-field-metadata.util.ts b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/compute-message-channel-message-association-standard-flat-field-metadata.util.ts index 065b7fc882..aa4e00617b 100644 --- a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/compute-message-channel-message-association-standard-flat-field-metadata.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/compute-message-channel-message-association-standard-flat-field-metadata.util.ts @@ -9,6 +9,7 @@ import { type AllStandardObjectFieldName } from 'src/engine/workspace-manager/tw import { createStandardFieldFlatMetadata } from 'src/engine/workspace-manager/twenty-standard-application/utils/create-standard-field-flat-metadata.util'; import { createStandardRelationFieldFlatMetadata } from 'src/engine/workspace-manager/twenty-standard-application/utils/create-standard-relation-field-flat-metadata.util'; import { type StandardFieldMetadataIdByObjectAndFieldName } from 'src/engine/workspace-manager/twenty-standard-application/utils/get-standard-field-metadata-id-by-object-and-field-name.util'; +import { MessageDirection } from 'src/modules/messaging/common/enums/message-direction.enum'; export const buildMessageChannelMessageAssociationStandardFlatFieldMetadatas = ({ @@ -128,10 +129,20 @@ export const buildMessageChannelMessageAssociationStandardFlatFieldMetadatas = description: 'Message Direction', icon: 'IconDirection', isNullable: false, - defaultValue: "'incoming'", + defaultValue: `'${MessageDirection.INCOMING}'`, options: [ - { value: 'incoming', label: 'Incoming', position: 0, color: 'green' }, - { value: 'outgoing', label: 'Outgoing', position: 1, color: 'blue' }, + { + value: MessageDirection.INCOMING, + label: 'Incoming', + position: 0, + color: 'green', + }, + { + value: MessageDirection.OUTGOING, + label: 'Outgoing', + position: 1, + color: 'blue', + }, ], createdAt, }, diff --git a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/compute-message-channel-standard-flat-field-metadata.util.ts b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/compute-message-channel-standard-flat-field-metadata.util.ts index f9e638577b..f60c92641e 100644 --- a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/compute-message-channel-standard-flat-field-metadata.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/compute-message-channel-standard-flat-field-metadata.util.ts @@ -9,6 +9,7 @@ import { type AllStandardObjectFieldName } from 'src/engine/workspace-manager/tw import { createStandardFieldFlatMetadata } from 'src/engine/workspace-manager/twenty-standard-application/utils/create-standard-field-flat-metadata.util'; import { createStandardRelationFieldFlatMetadata } from 'src/engine/workspace-manager/twenty-standard-application/utils/create-standard-relation-field-flat-metadata.util'; import { type StandardFieldMetadataIdByObjectAndFieldName } from 'src/engine/workspace-manager/twenty-standard-application/utils/get-standard-field-metadata-id-by-object-and-field-name.util'; +import { MessageChannelType } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity'; export const buildMessageChannelStandardFlatFieldMetadatas = ({ createdAt, @@ -138,10 +139,20 @@ export const buildMessageChannelStandardFlatFieldMetadatas = ({ description: 'Channel Type', icon: 'IconMessage', isNullable: false, - defaultValue: "'email'", + defaultValue: `'${MessageChannelType.EMAIL}'`, options: [ - { value: 'email', label: 'Email', position: 0, color: 'green' }, - { value: 'sms', label: 'SMS', position: 1, color: 'blue' }, + { + value: MessageChannelType.EMAIL, + label: 'Email', + position: 0, + color: 'green', + }, + { + value: MessageChannelType.SMS, + label: 'SMS', + position: 1, + color: 'blue', + }, ], createdAt, }, diff --git a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/compute-message-participant-standard-flat-field-metadata.util.ts b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/compute-message-participant-standard-flat-field-metadata.util.ts index 83ebc1671f..8a787aa646 100644 --- a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/compute-message-participant-standard-flat-field-metadata.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/compute-message-participant-standard-flat-field-metadata.util.ts @@ -1,5 +1,6 @@ import { FieldMetadataType, + MessageParticipantRole, RelationOnDeleteAction, RelationType, } from 'twenty-shared/types'; @@ -99,12 +100,32 @@ export const buildMessageParticipantStandardFlatFieldMetadatas = ({ description: 'Role', icon: 'IconAt', isNullable: false, - defaultValue: "'from'", + defaultValue: `'${MessageParticipantRole.FROM}'`, options: [ - { value: 'from', label: 'From', position: 0, color: 'green' }, - { value: 'to', label: 'To', position: 1, color: 'blue' }, - { value: 'cc', label: 'Cc', position: 2, color: 'orange' }, - { value: 'bcc', label: 'Bcc', position: 3, color: 'red' }, + { + value: MessageParticipantRole.FROM, + label: 'From', + position: 0, + color: 'green', + }, + { + value: MessageParticipantRole.TO, + label: 'To', + position: 1, + color: 'blue', + }, + { + value: MessageParticipantRole.CC, + label: 'Cc', + position: 2, + color: 'orange', + }, + { + value: MessageParticipantRole.BCC, + label: 'Bcc', + position: 3, + color: 'red', + }, ], createdAt, }, diff --git a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/compute-workspace-member-standard-flat-field-metadata.util.ts b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/compute-workspace-member-standard-flat-field-metadata.util.ts index ae310b2d09..2a4d952e03 100644 --- a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/compute-workspace-member-standard-flat-field-metadata.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/compute-workspace-member-standard-flat-field-metadata.util.ts @@ -9,6 +9,7 @@ import { type AllStandardObjectFieldName } from 'src/engine/workspace-manager/tw import { createStandardFieldFlatMetadata } from 'src/engine/workspace-manager/twenty-standard-application/utils/create-standard-field-flat-metadata.util'; import { createStandardRelationFieldFlatMetadata } from 'src/engine/workspace-manager/twenty-standard-application/utils/create-standard-relation-field-flat-metadata.util'; import { type StandardFieldMetadataIdByObjectAndFieldName } from 'src/engine/workspace-manager/twenty-standard-application/utils/get-standard-field-metadata-id-by-object-and-field-name.util'; +import { WorkspaceMemberNumberFormatEnum } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; export const buildWorkspaceMemberStandardFlatFieldMetadatas = ({ createdAt, @@ -296,30 +297,35 @@ export const buildWorkspaceMemberStandardFlatFieldMetadatas = ({ icon: 'IconNumbers', isSystem: true, isNullable: false, - defaultValue: "'SYSTEM'", + defaultValue: `'${WorkspaceMemberNumberFormatEnum.SYSTEM}'`, options: [ - { value: 'SYSTEM', label: 'System', position: 0, color: 'turquoise' }, { - value: 'COMMAS_AND_DOT', - label: 'Commas and dot (1,234.56)', + value: WorkspaceMemberNumberFormatEnum.SYSTEM, + label: 'System', + position: 0, + color: 'turquoise', + }, + { + value: WorkspaceMemberNumberFormatEnum.COMMAS_AND_DOT, + label: 'Commas and dot', position: 1, color: 'blue', }, { - value: 'SPACES_AND_COMMA', - label: 'Spaces and comma (1 234,56)', + value: WorkspaceMemberNumberFormatEnum.SPACES_AND_COMMA, + label: 'Spaces and comma', position: 2, color: 'green', }, { - value: 'DOTS_AND_COMMA', - label: 'Dots and comma (1.234,56)', + value: WorkspaceMemberNumberFormatEnum.DOTS_AND_COMMA, + label: 'Dots and comma', position: 3, color: 'orange', }, { - value: 'APOSTROPHE_AND_DOT', - label: "Apostrophe and dot (1'234.56)", + value: WorkspaceMemberNumberFormatEnum.APOSTROPHE_AND_DOT, + label: 'Apostrophe and dot', position: 4, color: 'purple', }, diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-field-metadata-validator.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-field-metadata-validator.service.ts index 5a230abcb3..6023ca3d9b 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-field-metadata-validator.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-field-metadata-validator.service.ts @@ -146,7 +146,10 @@ export class FlatFieldMetadataValidatorService { if ( flatFieldMetadataToValidate.isLabelSyncedWithName && - !isFlatFieldMetadataNameSyncedWithLabel(flatFieldMetadataToValidate) + !isFlatFieldMetadataNameSyncedWithLabel({ + flatFieldMetadata: flatFieldMetadataToValidate, + isSystemBuild: buildOptions.isSystemBuild, + }) ) { validationResult.errors.push({ code: FieldMetadataExceptionCode.FIELD_MUTATION_NOT_ALLOWED, @@ -318,7 +321,10 @@ export class FlatFieldMetadataValidatorService { if ( flatFieldMetadataToValidate.isLabelSyncedWithName && - !isFlatFieldMetadataNameSyncedWithLabel(flatFieldMetadataToValidate) + !isFlatFieldMetadataNameSyncedWithLabel({ + flatFieldMetadata: flatFieldMetadataToValidate, + isSystemBuild: buildOptions.isSystemBuild, + }) ) { validationResult.errors.push({ code: FieldMetadataExceptionCode.NAME_NOT_SYNCED_WITH_LABEL, diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-object-metadata-validator.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-object-metadata-validator.service.ts index f0d15a9efc..1a9cc78ae4 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-object-metadata-validator.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-object-metadata-validator.service.ts @@ -65,6 +65,7 @@ export class FlatObjectMetadataValidatorService { ...validateFlatObjectMetadataNameAndLabels({ optimisticFlatObjectMetadataMaps, flatObjectMetadataToValidate: updatedFlatObjectMetadata, + buildOptions, }), ); @@ -202,6 +203,7 @@ export class FlatObjectMetadataValidatorService { ...validateFlatObjectMetadataNameAndLabels({ optimisticFlatObjectMetadataMaps, flatObjectMetadataToValidate, + buildOptions, }), ); diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids.ts index 4fda5be8fc..12e5cf20c1 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids.ts @@ -102,7 +102,7 @@ export const CALENDAR_EVENT_STANDARD_FIELD_IDS = { externalUpdatedAt: '20202020-b355-4c18-8825-ef42c8a5a755', description: '20202020-52c4-4266-a98f-e90af0b4d271', location: '20202020-641a-4ffe-960d-c3c186d95b17', - iCalUID: '20202020-f24b-45f4-b6a3-d2f9fcb98714', + iCalUid: '20202020-f24b-45f4-b6a3-d2f9fcb98714', conferenceSolution: '20202020-1c3f-4b5a-b526-5411a82179eb', conferenceLink: '20202020-35da-43ef-9ca0-e936e9dc237b', calendarChannelEventAssociations: '20202020-bdf8-4572-a2cc-ecbb6bcc3a02', diff --git a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/caldav/lib/caldav.client.ts b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/caldav/lib/caldav.client.ts index 6299f85743..3aff9b2197 100644 --- a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/caldav/lib/caldav.client.ts +++ b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/caldav/lib/caldav.client.ts @@ -272,7 +272,7 @@ export class CalDAVClient { return { id: objectUrl, title: event.summary || 'Untitled Event', - iCalUID: event.uid || '', + iCalUid: event.uid || '', description: event.description || '', startsAt: event.start.toISOString(), endsAt: event.end.toISOString(), diff --git a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/google-calendar/utils/__tests__/format-google-calendar-event.util.spec.ts b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/google-calendar/utils/__tests__/format-google-calendar-event.util.spec.ts index e03542029d..ca06a8a365 100644 --- a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/google-calendar/utils/__tests__/format-google-calendar-event.util.spec.ts +++ b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/google-calendar/utils/__tests__/format-google-calendar-event.util.spec.ts @@ -120,6 +120,6 @@ describe('formatGoogleCalendarEvents', () => { mockGoogleEventWithImproperUcalid, ]); - expect(result[0].iCalUID).toBe(context.expected); + expect(result[0].iCalUid).toBe(context.expected); }); }); diff --git a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/google-calendar/utils/format-google-calendar-event.util.ts b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/google-calendar/utils/format-google-calendar-event.util.ts index 8a47e446b9..2fdd5ee715 100644 --- a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/google-calendar/utils/format-google-calendar-event.util.ts +++ b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/google-calendar/utils/format-google-calendar-event.util.ts @@ -38,7 +38,7 @@ const formatGoogleCalendarEvent = ( externalUpdatedAt: event.updated ?? '', description: event.description ?? '', location: event.location ?? '', - iCalUID: event.iCalUID ?? '', + iCalUid: event.iCalUID ?? '', conferenceSolution: event.conferenceData?.conferenceSolution?.key?.type ?? '', conferenceLinkLabel: event.conferenceData?.entryPoints?.[0]?.uri ?? '', @@ -63,7 +63,7 @@ const formatGoogleCalendarEvent = ( 'externalUpdatedAt', 'description', 'location', - 'iCalUID', + 'iCalUid', 'conferenceSolution', 'conferenceLinkLabel', 'conferenceLinkUrl', diff --git a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/microsoft-calendar/utils/__tests__/format-microsoft-calendar-event.util.spec.ts b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/microsoft-calendar/utils/__tests__/format-microsoft-calendar-event.util.spec.ts index 516fa38f8d..fa917da63e 100644 --- a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/microsoft-calendar/utils/__tests__/format-microsoft-calendar-event.util.spec.ts +++ b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/microsoft-calendar/utils/__tests__/format-microsoft-calendar-event.util.spec.ts @@ -100,7 +100,7 @@ describe('formatMicrosoftCalendarEvents', () => { mockMicrosoftEventWithImproperData2, ]); - expect(result[0].iCalUID).toBe('eventStrange@microsoft.com'); - expect(result[1].iCalUID).toBe('>\u0015-;_�^�W&�p\u001f�'); + expect(result[0].iCalUid).toBe('eventStrange@microsoft.com'); + expect(result[1].iCalUid).toBe('>\u0015-;_�^�W&�p\u001f�'); }); }); diff --git a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/microsoft-calendar/utils/format-microsoft-calendar-event.util.ts b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/microsoft-calendar/utils/format-microsoft-calendar-event.util.ts index 3a55f9f777..fb737b2fdf 100644 --- a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/microsoft-calendar/utils/format-microsoft-calendar-event.util.ts +++ b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/microsoft-calendar/utils/format-microsoft-calendar-event.util.ts @@ -42,7 +42,7 @@ const formatMicrosoftCalendarEvent = (event: Event): FetchedCalendarEvent => { externalUpdatedAt: event.lastModifiedDateTime ?? '', description: event.body?.content ?? '', location: event.location?.displayName ?? '', - iCalUID: event.iCalUId ?? '', + iCalUid: event.iCalUId ?? '', conferenceSolution: event.onlineMeetingProvider ?? '', conferenceLinkLabel: event.onlineMeeting?.joinUrl ?? '', conferenceLinkUrl: event.onlineMeeting?.joinUrl ?? '', @@ -66,7 +66,7 @@ const formatMicrosoftCalendarEvent = (event: Event): FetchedCalendarEvent => { 'externalUpdatedAt', 'description', 'location', - 'iCalUID', + 'iCalUid', 'conferenceSolution', 'conferenceLinkLabel', 'conferenceLinkUrl', diff --git a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/services/calendar-save-events.service.ts b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/services/calendar-save-events.service.ts index 1f83c1e544..3cb8e2cf0b 100644 --- a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/services/calendar-save-events.service.ts +++ b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/services/calendar-save-events.service.ts @@ -44,21 +44,21 @@ export class CalendarSaveEventsService { const existingCalendarEvents = await calendarEventRepository.find({ where: { - iCalUID: Any( - fetchedCalendarEvents.map((event) => event.iCalUID as string), + iCalUid: Any( + fetchedCalendarEvents.map((event) => event.iCalUid as string), ), }, }); const fetchedCalendarEventsWithDBEvents: FetchedCalendarEventWithDBEvent[] = fetchedCalendarEvents.map((event): FetchedCalendarEventWithDBEvent => { - const existingEventWithSameiCalUID = existingCalendarEvents.find( - (existingEvent) => existingEvent.iCalUID === event.iCalUID, + const existingEventWithSameiCalUid = existingCalendarEvents.find( + (existingEvent) => existingEvent.iCalUid === event.iCalUid, ); return { fetchedCalendarEvent: event, - existingCalendarEvent: existingEventWithSameiCalUID ?? null, + existingCalendarEvent: existingEventWithSameiCalUid ?? null, newlyCreatedCalendarEvent: null, }; }); @@ -78,7 +78,7 @@ export class CalendarSaveEventsService { .map( ({ fetchedCalendarEvent }) => ({ - iCalUID: fetchedCalendarEvent.iCalUID, + iCalUid: fetchedCalendarEvent.iCalUid, title: fetchedCalendarEvent.title, description: fetchedCalendarEvent.description, startsAt: fetchedCalendarEvent.startsAt, @@ -113,7 +113,7 @@ export class CalendarSaveEventsService { ({ fetchedCalendarEvent, existingCalendarEvent }) => { const savedCalendarEvent = savedCalendarEvents.find( (savedCalendarEvent) => - savedCalendarEvent.iCalUID === fetchedCalendarEvent.iCalUID, + savedCalendarEvent.iCalUid === fetchedCalendarEvent.iCalUid, ); return { @@ -132,13 +132,13 @@ export class CalendarSaveEventsService { .map(({ fetchedCalendarEvent, existingCalendarEvent }) => { if (!existingCalendarEvent) { throw new Error( - `Existing calendar event with iCalUID ${fetchedCalendarEvent.iCalUID} not found - should never happen`, + `Existing calendar event with iCalUid ${fetchedCalendarEvent.iCalUid} not found - should never happen`, ); } return { id: existingCalendarEvent.id, - iCalUID: fetchedCalendarEvent.iCalUID, + iCalUid: fetchedCalendarEvent.iCalUid, title: fetchedCalendarEvent.title, description: fetchedCalendarEvent.description, startsAt: fetchedCalendarEvent.startsAt, @@ -184,7 +184,7 @@ export class CalendarSaveEventsService { if (!calendarEventId) { throw new Error( - `Calendar event id not found for event with iCalUID ${fetchedCalendarEvent.iCalUID} - should never happen`, + `Calendar event id not found for event with iCalUid ${fetchedCalendarEvent.iCalUid} - should never happen`, ); } @@ -213,7 +213,7 @@ export class CalendarSaveEventsService { .flatMap(({ newlyCreatedCalendarEvent, fetchedCalendarEvent }) => { if (!newlyCreatedCalendarEvent?.id) { throw new Error( - `Newly created calendar event with iCalUID ${fetchedCalendarEvent.iCalUID} not found - should never happen`, + `Newly created calendar event with iCalUid ${fetchedCalendarEvent.iCalUid} not found - should never happen`, ); } @@ -233,7 +233,7 @@ export class CalendarSaveEventsService { .flatMap(({ fetchedCalendarEvent, existingCalendarEvent }) => { if (!existingCalendarEvent?.id) { throw new Error( - `Existing calendar event with iCalUID ${fetchedCalendarEvent.iCalUID} not found - should never happen`, + `Existing calendar event with iCalUid ${fetchedCalendarEvent.iCalUid} not found - should never happen`, ); } diff --git a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/utils/calendar-event-mapper.util.ts b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/utils/calendar-event-mapper.util.ts index a098150532..6536cd61d2 100644 --- a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/utils/calendar-event-mapper.util.ts +++ b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/utils/calendar-event-mapper.util.ts @@ -5,7 +5,7 @@ export const mapCalendarEventsByICalUID = ( ): Map => { return new Map( existingCalendarEvents.map((calendarEvent) => [ - calendarEvent.iCalUID ?? '', + calendarEvent.iCalUid ?? '', calendarEvent.id, ]), ); diff --git a/packages/twenty-server/src/modules/calendar/common/query-hooks/calendar-event/services/apply-calendar-events-visibility-restrictions.service.spec.ts b/packages/twenty-server/src/modules/calendar/common/query-hooks/calendar-event/services/apply-calendar-events-visibility-restrictions.service.spec.ts index 662aba6e42..485f44c40b 100644 --- a/packages/twenty-server/src/modules/calendar/common/query-hooks/calendar-event/services/apply-calendar-events-visibility-restrictions.service.spec.ts +++ b/packages/twenty-server/src/modules/calendar/common/query-hooks/calendar-event/services/apply-calendar-events-visibility-restrictions.service.spec.ts @@ -31,7 +31,7 @@ const createMockCalendarEvent = ( deletedAt: null, createdAt: '2024-03-20T09:00:00Z', updatedAt: '2024-03-20T09:00:00Z', - iCalUID: '', + iCalUid: '', conferenceSolution: '', calendarChannelEventAssociations: [], calendarEventParticipants: [], diff --git a/packages/twenty-server/src/modules/calendar/common/standard-objects/calendar-event.workspace-entity.ts b/packages/twenty-server/src/modules/calendar/common/standard-objects/calendar-event.workspace-entity.ts index 3ff9042db8..500e4ad968 100644 --- a/packages/twenty-server/src/modules/calendar/common/standard-objects/calendar-event.workspace-entity.ts +++ b/packages/twenty-server/src/modules/calendar/common/standard-objects/calendar-event.workspace-entity.ts @@ -127,14 +127,14 @@ export class CalendarEventWorkspaceEntity extends BaseWorkspaceEntity { location: string | null; @WorkspaceField({ - standardId: CALENDAR_EVENT_STANDARD_FIELD_IDS.iCalUID, + standardId: CALENDAR_EVENT_STANDARD_FIELD_IDS.iCalUid, type: FieldMetadataType.TEXT, label: msg`iCal UID`, description: msg`iCal UID`, icon: 'IconKey', }) @WorkspaceIsNullable() - iCalUID: string | null; + iCalUid: string | null; @WorkspaceField({ standardId: CALENDAR_EVENT_STANDARD_FIELD_IDS.conferenceSolution, diff --git a/packages/twenty-server/src/modules/calendar/common/types/fetched-calendar-event.ts b/packages/twenty-server/src/modules/calendar/common/types/fetched-calendar-event.ts index 85239d585b..e183454139 100644 --- a/packages/twenty-server/src/modules/calendar/common/types/fetched-calendar-event.ts +++ b/packages/twenty-server/src/modules/calendar/common/types/fetched-calendar-event.ts @@ -8,7 +8,7 @@ export type FetchedCalendarEventParticipant = { export type FetchedCalendarEvent = { id: string; title: string; - iCalUID: string; + iCalUid: string; description: string; startsAt: string; endsAt: string; diff --git a/packages/twenty-server/src/modules/messaging/blocklist-manager/jobs/messaging-blocklist-item-delete-messages.job.ts b/packages/twenty-server/src/modules/messaging/blocklist-manager/jobs/messaging-blocklist-item-delete-messages.job.ts index 12ad56e0af..aedc03c2d1 100644 --- a/packages/twenty-server/src/modules/messaging/blocklist-manager/jobs/messaging-blocklist-item-delete-messages.job.ts +++ b/packages/twenty-server/src/modules/messaging/blocklist-manager/jobs/messaging-blocklist-item-delete-messages.job.ts @@ -2,6 +2,7 @@ import { Scope } from '@nestjs/common'; import { isDefined } from 'twenty-shared/utils'; import { And, Any, ILike, In, Not, Or } from 'typeorm'; +import { MessageParticipantRole } from 'twenty-shared/types'; import { type ObjectRecordCreateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-create.event'; import { Process } from 'src/engine/core-modules/message-queue/decorators/process.decorator'; @@ -87,7 +88,10 @@ export class BlocklistItemDeleteMessagesJob { continue; } - const rolesToDelete: ('from' | 'to')[] = ['from', 'to']; + const rolesToDelete = [ + MessageParticipantRole.FROM, + MessageParticipantRole.TO, + ] as const; const messageChannels = await messageChannelRepository.find({ select: { diff --git a/packages/twenty-server/src/modules/messaging/common/standard-objects/message-channel.workspace-entity.ts b/packages/twenty-server/src/modules/messaging/common/standard-objects/message-channel.workspace-entity.ts index 33f17e8538..d2f4ce6d0d 100644 --- a/packages/twenty-server/src/modules/messaging/common/standard-objects/message-channel.workspace-entity.ts +++ b/packages/twenty-server/src/modules/messaging/common/standard-objects/message-channel.workspace-entity.ts @@ -47,8 +47,8 @@ export enum MessageChannelVisibility { } export enum MessageChannelType { - EMAIL = 'email', - SMS = 'sms', + EMAIL = 'EMAIL', + SMS = 'SMS', } export enum MessageChannelContactAutoCreationPolicy { diff --git a/packages/twenty-server/src/modules/messaging/common/standard-objects/message-participant.workspace-entity.ts b/packages/twenty-server/src/modules/messaging/common/standard-objects/message-participant.workspace-entity.ts index 76e1701ff2..3b435fddc3 100644 --- a/packages/twenty-server/src/modules/messaging/common/standard-objects/message-participant.workspace-entity.ts +++ b/packages/twenty-server/src/modules/messaging/common/standard-objects/message-participant.workspace-entity.ts @@ -1,6 +1,10 @@ import { msg } from '@lingui/core/macro'; import { STANDARD_OBJECT_IDS } from 'twenty-shared/metadata'; -import { FieldMetadataType, RelationOnDeleteAction } from 'twenty-shared/types'; +import { + FieldMetadataType, + MessageParticipantRole, + RelationOnDeleteAction, +} from 'twenty-shared/types'; import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface'; @@ -39,14 +43,34 @@ export class MessageParticipantWorkspaceEntity extends BaseWorkspaceEntity { description: msg`Role`, icon: 'IconAt', options: [ - { value: 'from', label: 'From', position: 0, color: 'green' }, - { value: 'to', label: 'To', position: 1, color: 'blue' }, - { value: 'cc', label: 'Cc', position: 2, color: 'orange' }, - { value: 'bcc', label: 'Bcc', position: 3, color: 'red' }, + { + value: MessageParticipantRole.FROM, + label: 'From', + position: 0, + color: 'green', + }, + { + value: MessageParticipantRole.TO, + label: 'To', + position: 1, + color: 'blue', + }, + { + value: MessageParticipantRole.CC, + label: 'Cc', + position: 2, + color: 'orange', + }, + { + value: MessageParticipantRole.BCC, + label: 'Bcc', + position: 3, + color: 'red', + }, ], - defaultValue: "'from'", + defaultValue: `'${MessageParticipantRole.FROM}'`, }) - role: string; + role: MessageParticipantRole; @WorkspaceField({ standardId: MESSAGE_PARTICIPANT_STANDARD_FIELD_IDS.handle, diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/types/gmail-message.type.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/types/gmail-message.type.ts index ee87048f71..9f8c04d9a8 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/types/gmail-message.type.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/types/gmail-message.type.ts @@ -1,3 +1,5 @@ +import { type MessageParticipantRole } from 'twenty-shared/types'; + export type GmailMessage = { historyId: string; externalId: string; @@ -13,7 +15,7 @@ export type GmailMessage = { }; export type Participant = { - role: 'from' | 'to' | 'cc' | 'bcc'; + role: MessageParticipantRole; handle: string; displayName: string; }; diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/utils/parse-and-format-gmail-message.util.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/utils/parse-and-format-gmail-message.util.ts index 3e1d31debd..40f006569f 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/utils/parse-and-format-gmail-message.util.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/utils/parse-and-format-gmail-message.util.ts @@ -1,5 +1,6 @@ import { type gmail_v1 as gmailV1 } from 'googleapis'; import planer from 'planer'; +import { MessageParticipantRole } from 'twenty-shared/types'; import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; import { computeMessageDirection } from 'src/modules/messaging/message-import-manager/drivers/gmail/utils/compute-message-direction.util'; @@ -43,17 +44,28 @@ export const parseAndFormatGmailMessage = ( const participants = [ ...(from - ? formatAddressObjectAsParticipants([{ address: from }], 'from') + ? formatAddressObjectAsParticipants( + [{ address: from }], + MessageParticipantRole.FROM, + ) : []), ...(toParticipants ? formatAddressObjectAsParticipants( [{ address: toParticipants, name: '' }], - 'to', + MessageParticipantRole.TO, + ) + : []), + ...(cc + ? formatAddressObjectAsParticipants( + [{ address: cc }], + MessageParticipantRole.CC, ) : []), - ...(cc ? formatAddressObjectAsParticipants([{ address: cc }], 'cc') : []), ...(bcc - ? formatAddressObjectAsParticipants([{ address: bcc }], 'bcc') + ? formatAddressObjectAsParticipants( + [{ address: bcc }], + MessageParticipantRole.BCC, + ) : []), ]; diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/imap/services/imap-get-messages.service.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/imap/services/imap-get-messages.service.ts index ed50476aa0..343ad54072 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/imap/services/imap-get-messages.service.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/imap/services/imap-get-messages.service.ts @@ -2,6 +2,7 @@ import { Injectable, Logger } from '@nestjs/common'; import { type ImapFlow } from 'imapflow'; import { Address, type Email as ParsedMail } from 'postal-mime'; +import { MessageParticipantRole } from 'twenty-shared/types'; import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; import { computeMessageDirection } from 'src/modules/messaging/message-import-manager/drivers/gmail/utils/compute-message-direction.util'; @@ -203,11 +204,11 @@ export class ImapGetMessagesService { private extractParticipants(parsed: ParsedMail) { const addressFields = [ - { field: parsed.from, role: 'from' as const }, - { field: parsed.to, role: 'to' as const }, - { field: parsed.cc, role: 'cc' as const }, - { field: parsed.bcc, role: 'bcc' as const }, - ]; + { field: parsed.from, role: MessageParticipantRole.FROM }, + { field: parsed.to, role: MessageParticipantRole.TO }, + { field: parsed.cc, role: MessageParticipantRole.CC }, + { field: parsed.bcc, role: MessageParticipantRole.BCC }, + ] as const; return addressFields.flatMap(({ field, role }) => formatAddressObjectAsParticipants(this.extractAddresses(field), role), diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/microsoft/services/microsoft-get-messages.service.spec.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/microsoft/services/microsoft-get-messages.service.spec.ts index 1ec09e6f02..da35e1f39f 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/microsoft/services/microsoft-get-messages.service.spec.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/microsoft/services/microsoft-get-messages.service.spec.ts @@ -2,7 +2,10 @@ import { Logger } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { Test, type TestingModule } from '@nestjs/testing'; -import { ConnectedAccountProvider } from 'twenty-shared/types'; +import { + ConnectedAccountProvider, + MessageParticipantRole, +} from 'twenty-shared/types'; import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; import { GoogleOAuth2ClientManagerService } from 'src/modules/connected-account/oauth2-client-manager/drivers/google/google-oauth2-client-manager.service'; @@ -97,12 +100,12 @@ describe('Microsoft get messages service', () => { { displayName: 'John l', handle: 'john.l@outlook.fr', - role: 'from', + role: MessageParticipantRole.FROM, }, { displayName: 'Walker', handle: 'walker@felixacme.onmicrosoft.com', - role: 'to', + role: MessageParticipantRole.TO, }, ], attachments: [], @@ -130,22 +133,22 @@ describe('Microsoft get messages service', () => { { displayName: 'Microsoft', handle: 'microsoft-noreply@microsoft.com', - role: 'from', + role: MessageParticipantRole.FROM, }, { displayName: 'Walker', handle: 'walker@felixacme.onmicrosoft.com', - role: 'to', + role: MessageParticipantRole.TO, }, { displayName: 'Antoine', handle: 'antoine@gmail.com', - role: 'cc', + role: MessageParticipantRole.CC, }, { displayName: 'Cyril@acme2.com', handle: 'cyril@acme2.com', - role: 'cc', + role: MessageParticipantRole.CC, }, ], attachments: [], @@ -184,7 +187,7 @@ describe('Microsoft get messages service', () => { displayName: responseExample.body.sender.emailAddress.name, handle: responseExample.body.sender.emailAddress.address.toLowerCase(), - role: 'from', + role: MessageParticipantRole.FROM, }, ], attachments: [], diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/microsoft/services/microsoft-get-messages.service.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/microsoft/services/microsoft-get-messages.service.ts index cf9b4e016a..021957bee9 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/microsoft/services/microsoft-get-messages.service.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/microsoft/services/microsoft-get-messages.service.ts @@ -2,6 +2,7 @@ import { Injectable, Logger } from '@nestjs/common'; import { type EmailAddress } from 'addressparser'; import { isDefined } from 'twenty-shared/utils'; +import { MessageParticipantRole } from 'twenty-shared/types'; import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; import { MessageDirection } from 'src/modules/messaging/common/enums/message-direction.enum'; @@ -109,16 +110,28 @@ export class MicrosoftGetMessagesService { const participants = [ ...(safeParseFrom - ? formatAddressObjectAsParticipants(safeParseFrom, 'from') + ? formatAddressObjectAsParticipants( + safeParseFrom, + MessageParticipantRole.FROM, + ) : []), ...(safeParseTo - ? formatAddressObjectAsParticipants(safeParseTo, 'to') + ? formatAddressObjectAsParticipants( + safeParseTo, + MessageParticipantRole.TO, + ) : []), ...(safeParseCc - ? formatAddressObjectAsParticipants(safeParseCc, 'cc') + ? formatAddressObjectAsParticipants( + safeParseCc, + MessageParticipantRole.CC, + ) : []), ...(safeParseBcc - ? formatAddressObjectAsParticipants(safeParseBcc, 'bcc') + ? formatAddressObjectAsParticipants( + safeParseBcc, + MessageParticipantRole.BCC, + ) : []), ]; diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-delete-group-email-messages.service.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-delete-group-email-messages.service.ts index a2a6209a1c..74d289b798 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-delete-group-email-messages.service.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-delete-group-email-messages.service.ts @@ -2,6 +2,7 @@ import { Injectable, Logger } from '@nestjs/common'; import chunk from 'lodash.chunk'; import { isDefined } from 'twenty-shared/utils'; +import { MessageParticipantRole } from 'twenty-shared/types'; import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; import { MessageChannelMessageAssociationWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel-message-association.workspace-entity'; @@ -55,7 +56,7 @@ export class MessagingDeleteGroupEmailMessagesService { 'message.messageParticipants', 'participant', 'participant.role = :role', - { role: 'from' }, + { role: MessageParticipantRole.FROM }, ) .where('mcma.messageChannelId = :messageChannelId', { messageChannelId, diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-save-messages-and-enqueue-contact-creation.service.spec.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-save-messages-and-enqueue-contact-creation.service.spec.ts index 71a74f9daf..ccc804670b 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-save-messages-and-enqueue-contact-creation.service.spec.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-save-messages-and-enqueue-contact-creation.service.spec.ts @@ -1,7 +1,7 @@ import { Test, type TestingModule } from '@nestjs/testing'; import { getRepositoryToken } from '@nestjs/typeorm'; -import { FieldActorSource } from 'twenty-shared/types'; +import { FieldActorSource, MessageParticipantRole } from 'twenty-shared/types'; import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants'; import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service'; @@ -56,8 +56,16 @@ describe('MessagingSaveMessagesAndEnqueueContactCreationService', () => { messageThreadExternalId: 'thread-1', direction: MessageDirection.OUTGOING, participants: [ - { role: 'from', handle: 'test@example.com', displayName: 'Test User' }, - { role: 'to', handle: 'contact@company.com', displayName: 'Contact' }, + { + role: MessageParticipantRole.FROM, + handle: 'test@example.com', + displayName: 'Test User', + }, + { + role: MessageParticipantRole.TO, + handle: 'contact@company.com', + displayName: 'Contact', + }, ], }, { @@ -70,11 +78,23 @@ describe('MessagingSaveMessagesAndEnqueueContactCreationService', () => { messageThreadExternalId: 'thread-1', direction: MessageDirection.INCOMING, participants: [ - { role: 'from', handle: 'contact@company.com', displayName: 'Contact' }, - { role: 'to', handle: 'test@example.com', displayName: 'Test User' }, - { role: 'to', handle: 'personal@gmail.com', displayName: 'Personal' }, { - role: 'to', + role: MessageParticipantRole.FROM, + handle: 'contact@company.com', + displayName: 'Contact', + }, + { + role: MessageParticipantRole.TO, + handle: 'test@example.com', + displayName: 'Test User', + }, + { + role: MessageParticipantRole.TO, + handle: 'personal@gmail.com', + displayName: 'Personal', + }, + { + role: MessageParticipantRole.TO, handle: 'team@lists.company.com', displayName: 'Group email', }, @@ -199,7 +219,7 @@ describe('MessagingSaveMessagesAndEnqueueContactCreationService', () => { ...mockMessages[1], participants: [ { - role: 'from', + role: MessageParticipantRole.FROM, handle: 'tim@apple.com', displayName: 'participant email', }, @@ -221,7 +241,7 @@ describe('MessagingSaveMessagesAndEnqueueContactCreationService', () => { { handle: 'tim@apple.com', displayName: 'participant email', - role: 'from', + role: MessageParticipantRole.FROM, shouldCreateContact: true, messageId: 'db-message-id-2', }, @@ -237,7 +257,7 @@ describe('MessagingSaveMessagesAndEnqueueContactCreationService', () => { ...mockMessages[0], participants: [ { - role: 'from', + role: MessageParticipantRole.FROM, handle: 'test@gmail.com', displayName: 'participant personal email', }, @@ -265,7 +285,7 @@ describe('MessagingSaveMessagesAndEnqueueContactCreationService', () => { ...mockMessages[0], participants: [ { - role: 'from', + role: MessageParticipantRole.FROM, handle: 'connected@account.com', displayName: 'participant that is the Connected Account', }, diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-save-messages-and-enqueue-contact-creation.service.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-save-messages-and-enqueue-contact-creation.service.ts index 1b71c9868c..bffa6656d4 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-save-messages-and-enqueue-contact-creation.service.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-save-messages-and-enqueue-contact-creation.service.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common'; -import { FieldActorSource } from 'twenty-shared/types'; +import { FieldActorSource, MessageParticipantRole } from 'twenty-shared/types'; import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator'; import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants'; @@ -66,8 +66,9 @@ export class MessagingSaveMessagesAndEnqueueContactCreationService { return messageId ? message.participants.map((participant: Participant) => { const fromHandle = - message.participants.find((p) => p.role === 'from')?.handle || - ''; + message.participants.find( + (p) => p.role === MessageParticipantRole.FROM, + )?.handle || ''; const isMessageSentByConnectedAccount = handleAliases.includes(fromHandle) || diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/utils/__mocks__/messages.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/utils/__mocks__/messages.ts index 4cc2ead8f9..761a876235 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/utils/__mocks__/messages.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/utils/__mocks__/messages.ts @@ -1,3 +1,6 @@ +import { MessageParticipantRole } from 'twenty-shared/types'; + +import { MessageDirection } from 'src/modules/messaging/common/enums/message-direction.enum'; import { type MessageWithParticipants } from 'src/modules/messaging/message-import-manager/types/message'; export const messagingGetMessagesServiceGetMessages = [ @@ -8,15 +11,15 @@ export const messagingGetMessagesServiceGetMessages = [ text: 'Just a test', headerMessageId: '', messageThreadExternalId: 'AAQkAGZlMDQ1NjU5Lk=', - direction: 'OUTGOING', + direction: MessageDirection.OUTGOING, participants: [ { - role: 'from', + role: MessageParticipantRole.FROM, handle: 'from@acme.com', displayName: 'From', }, { - role: 'to', + role: MessageParticipantRole.TO, handle: 'to@acme.com', displayName: 'To', }, @@ -30,15 +33,15 @@ export const messagingGetMessagesServiceGetMessages = [ text: 'Just a test', headerMessageId: '', messageThreadExternalId: 'AAQkAGZlMDQ1NjU5Lk=', - direction: 'OUTGOING', + direction: MessageDirection.OUTGOING, participants: [ { - role: 'from', + role: MessageParticipantRole.FROM, handle: 'from@acme.com', displayName: 'From', }, { - role: 'to', + role: MessageParticipantRole.TO, handle: 'to@external.com', displayName: 'To', }, @@ -52,19 +55,19 @@ export const messagingGetMessagesServiceGetMessages = [ text: 'Learn with interactive sandboxes, curated collections, and bookmarks', headerMessageId: '', messageThreadExternalId: 'AAQkAGZlMDQ1NNc=', - direction: 'INCOMING', + direction: MessageDirection.INCOMING, participants: [ { - role: 'from', + role: MessageParticipantRole.FROM, handle: 'learn@mails.microsoft.com', displayName: 'Microsoft Learn', }, { - role: 'to', + role: MessageParticipantRole.TO, handle: 'to@gmail.com', displayName: 'To', }, ], attachments: [], }, -] as MessageWithParticipants[]; +] satisfies MessageWithParticipants[]; diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/utils/__tests__/filter-emails.util.spec.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/utils/__tests__/filter-emails.util.spec.ts index e11678741c..8b3ab767bd 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/utils/__tests__/filter-emails.util.spec.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/utils/__tests__/filter-emails.util.spec.ts @@ -1,3 +1,5 @@ +import { MessageParticipantRole } from 'twenty-shared/types'; + import { MessageDirection } from 'src/modules/messaging/common/enums/message-direction.enum'; import { type MessageWithParticipants } from 'src/modules/messaging/message-import-manager/types/message'; import { messagingGetMessagesServiceGetMessages } from 'src/modules/messaging/message-import-manager/utils/__mocks__/messages'; @@ -64,7 +66,7 @@ describe('filterEmails', () => { direction: MessageDirection.INCOMING, participants: [ { - role: 'from', + role: MessageParticipantRole.FROM, handle: 'noreply@example.com', displayName: 'No Reply', }, @@ -81,7 +83,7 @@ describe('filterEmails', () => { direction: MessageDirection.INCOMING, participants: [ { - role: 'from', + role: MessageParticipantRole.FROM, handle: 'support@company.com', displayName: 'Support Team', }, @@ -98,7 +100,7 @@ describe('filterEmails', () => { direction: MessageDirection.INCOMING, participants: [ { - role: 'from', + role: MessageParticipantRole.FROM, handle: 'john@example.com', displayName: 'John Doe', }, @@ -126,7 +128,7 @@ describe('filterEmails', () => { direction: MessageDirection.INCOMING, participants: [ { - role: 'from', + role: MessageParticipantRole.FROM, handle: 'noreply@example.com', displayName: 'No Reply', }, @@ -174,7 +176,7 @@ describe('filterEmails', () => { direction: MessageDirection.INCOMING, participants: [ { - role: 'from', + role: MessageParticipantRole.FROM, handle: 'john.doe@example.com', displayName: 'John Doe', }, diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/utils/__tests__/format-address-object-as-participants.util.spec.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/utils/__tests__/format-address-object-as-participants.util.spec.ts index cff04d2dae..79d72f3161 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/utils/__tests__/format-address-object-as-participants.util.spec.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/utils/__tests__/format-address-object-as-participants.util.spec.ts @@ -1,3 +1,5 @@ +import { MessageParticipantRole } from 'twenty-shared/types'; + import { formatAddressObjectAsParticipants } from 'src/modules/messaging/message-import-manager/utils/format-address-object-as-participants.util'; describe('formatAddressObjectAsParticipants', () => { @@ -7,16 +9,19 @@ describe('formatAddressObjectAsParticipants', () => { { name: 'Jane Smith', address: 'jane.smith@example.com ' }, ]; - const result = formatAddressObjectAsParticipants(addresses, 'from'); + const result = formatAddressObjectAsParticipants( + addresses, + MessageParticipantRole.FROM, + ); expect(result).toEqual([ { - role: 'from', + role: MessageParticipantRole.FROM, handle: 'john.doe@example.com', displayName: 'John Doe', }, { - role: 'from', + role: MessageParticipantRole.FROM, handle: 'jane.smith@example.com', displayName: 'Jane Smith', }, @@ -29,7 +34,10 @@ describe('formatAddressObjectAsParticipants', () => { address: 'john.doe', }; - const result = formatAddressObjectAsParticipants([addressObject], 'to'); + const result = formatAddressObjectAsParticipants( + [addressObject], + MessageParticipantRole.TO, + ); expect(result).toEqual([]); }); @@ -40,7 +48,10 @@ describe('formatAddressObjectAsParticipants', () => { address: '', }; - const result = formatAddressObjectAsParticipants([addressObject], 'to'); + const result = formatAddressObjectAsParticipants( + [addressObject], + MessageParticipantRole.TO, + ); expect(result).toEqual([]); }); @@ -51,11 +62,14 @@ describe('formatAddressObjectAsParticipants', () => { address: 'John.Doe@example.com', }; - const result = formatAddressObjectAsParticipants([addressObject], 'to'); + const result = formatAddressObjectAsParticipants( + [addressObject], + MessageParticipantRole.TO, + ); expect(result).toEqual([ { - role: 'to', + role: MessageParticipantRole.TO, handle: 'john.doe@example.com', displayName: 'John Doe', }, diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/utils/filter-out-group-emails.util.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/utils/filter-out-group-emails.util.ts index bd6b98c032..09f0190ef8 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/utils/filter-out-group-emails.util.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/utils/filter-out-group-emails.util.ts @@ -1,3 +1,5 @@ +import { MessageParticipantRole } from 'twenty-shared/types'; + import { type MessageWithParticipants } from 'src/modules/messaging/message-import-manager/types/message'; import { isGroupEmail } from 'src/modules/messaging/message-import-manager/utils/is-group-email'; @@ -8,7 +10,7 @@ export const filterOutGroupEmails = (messages: MessageWithParticipants[]) => { } const fromParticipant = message.participants.find( - (participant) => participant.role === 'from', + (participant) => participant.role === MessageParticipantRole.FROM, ); if (!fromParticipant || !fromParticipant.handle) { diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/utils/format-address-object-as-participants.util.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/utils/format-address-object-as-participants.util.ts index 1745f9f917..38bf0af196 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/utils/format-address-object-as-participants.util.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/utils/format-address-object-as-participants.util.ts @@ -1,4 +1,5 @@ import { isDefined } from 'twenty-shared/utils'; +import { type MessageParticipantRole } from 'twenty-shared/types'; import { type Participant } from 'src/modules/messaging/message-import-manager/drivers/gmail/types/gmail-message.type'; import { type EmailAddress } from 'src/modules/messaging/message-import-manager/types/email-address'; @@ -9,7 +10,7 @@ const removeSpacesAndLowerCase = (email: string): string => { export const formatAddressObjectAsParticipants = ( addressObjects: EmailAddress[], - role: 'from' | 'to' | 'cc' | 'bcc', + role: MessageParticipantRole, ): Participant[] => { const participants = addressObjects.map((addressObject) => { const address = addressObject.address; diff --git a/packages/twenty-server/src/modules/workspace-member/standard-objects/workspace-member.workspace-entity.ts b/packages/twenty-server/src/modules/workspace-member/standard-objects/workspace-member.workspace-entity.ts index 294ec08943..50f5ee9ed8 100644 --- a/packages/twenty-server/src/modules/workspace-member/standard-objects/workspace-member.workspace-entity.ts +++ b/packages/twenty-server/src/modules/workspace-member/standard-objects/workspace-member.workspace-entity.ts @@ -410,25 +410,25 @@ export class WorkspaceMemberWorkspaceEntity extends BaseWorkspaceEntity { }, { value: WorkspaceMemberNumberFormatEnum.COMMAS_AND_DOT, - label: 'Commas and dot (1,234.56)', + label: 'Commas and dot', position: 1, color: 'blue', }, { value: WorkspaceMemberNumberFormatEnum.SPACES_AND_COMMA, - label: 'Spaces and comma (1 234,56)', + label: 'Spaces and comma', position: 2, color: 'green', }, { value: WorkspaceMemberNumberFormatEnum.DOTS_AND_COMMA, - label: 'Dots and comma (1.234,56)', + label: 'Dots and comma', position: 3, color: 'orange', }, { value: WorkspaceMemberNumberFormatEnum.APOSTROPHE_AND_DOT, - label: "Apostrophe and dot (1'234.56)", + label: 'Apostrophe and dot', position: 4, color: 'purple', }, diff --git a/packages/twenty-shared/src/metadata/sanitize-reserved-keyword.util.ts b/packages/twenty-shared/src/metadata/add-custom-suffix-if-reserved.util.ts similarity index 57% rename from packages/twenty-shared/src/metadata/sanitize-reserved-keyword.util.ts rename to packages/twenty-shared/src/metadata/add-custom-suffix-if-reserved.util.ts index 60b10a0083..c3e4b8534e 100644 --- a/packages/twenty-shared/src/metadata/sanitize-reserved-keyword.util.ts +++ b/packages/twenty-shared/src/metadata/add-custom-suffix-if-reserved.util.ts @@ -1,10 +1,9 @@ -import { capitalize } from '../utils'; import { RESERVED_METADATA_NAME_KEYWORDS } from './reserved-metadata-name-keywords.constant'; -export const sanitizeReservedKeyword = (name: string): string => { +export const addCustomSuffixIfIsReserved = (name: string): string => { if (!name) return name; return RESERVED_METADATA_NAME_KEYWORDS.includes(name) - ? `${name}${capitalize('custom')}` + ? `${name}Custom` : name; }; diff --git a/packages/twenty-shared/src/metadata/compute-metadata-name-from-label.util.ts b/packages/twenty-shared/src/metadata/compute-metadata-name-from-label.util.ts index ba612410c2..0b935b5917 100644 --- a/packages/twenty-shared/src/metadata/compute-metadata-name-from-label.util.ts +++ b/packages/twenty-shared/src/metadata/compute-metadata-name-from-label.util.ts @@ -1,9 +1,14 @@ +import { addCustomSuffixIfIsReserved } from '@/metadata/add-custom-suffix-if-reserved.util'; import camelCase from 'lodash.camelcase'; import { slugify } from 'transliteration'; -import { sanitizeReservedKeyword } from './sanitize-reserved-keyword.util'; - -export const computeMetadataNameFromLabel = (label: string): string => { +export const computeMetadataNameFromLabel = ({ + label, + applyCustomSuffix = true, +}: { + label: string; + applyCustomSuffix?: boolean; +}): string => { if (!label) return ''; const prefixedLabel = /^\d/.test(label) ? `n${label}` : label; @@ -22,5 +27,7 @@ export const computeMetadataNameFromLabel = (label: string): string => { const computedName = camelCase(formattedString); - return sanitizeReservedKeyword(computedName); + return applyCustomSuffix + ? addCustomSuffixIfIsReserved(computedName) + : computedName; }; diff --git a/packages/twenty-shared/src/metadata/index.ts b/packages/twenty-shared/src/metadata/index.ts index 9310bde475..7b83687eee 100644 --- a/packages/twenty-shared/src/metadata/index.ts +++ b/packages/twenty-shared/src/metadata/index.ts @@ -7,6 +7,7 @@ * |___/ */ +export { addCustomSuffixIfIsReserved } from './add-custom-suffix-if-reserved.util'; export { ALL_METADATA_NAME } from './all-metadata-name.constant'; export type { AllMetadataName } from './all-metadata-name.type'; export { computeMetadataNameFromLabel } from './compute-metadata-name-from-label.util'; @@ -20,5 +21,4 @@ export { CORE_OBJECT_NAMES, RESERVED_METADATA_NAME_KEYWORDS, } from './reserved-metadata-name-keywords.constant'; -export { sanitizeReservedKeyword } from './sanitize-reserved-keyword.util'; export { STANDARD_OBJECT_IDS } from './standard-object-ids'; diff --git a/packages/twenty-shared/src/types/MessageParticipantRole.ts b/packages/twenty-shared/src/types/MessageParticipantRole.ts new file mode 100644 index 0000000000..6f0739d706 --- /dev/null +++ b/packages/twenty-shared/src/types/MessageParticipantRole.ts @@ -0,0 +1,6 @@ +export enum MessageParticipantRole { + 'FROM' = 'FROM', + 'TO' = 'TO', + 'CC' = 'CC', + 'BCC' = 'BCC', +} diff --git a/packages/twenty-shared/src/types/index.ts b/packages/twenty-shared/src/types/index.ts index f1cbfddb6d..38d036572d 100644 --- a/packages/twenty-shared/src/types/index.ts +++ b/packages/twenty-shared/src/types/index.ts @@ -114,6 +114,7 @@ export type { FromTo } from './FromToType'; export { HTTPMethod } from './HttpMethod'; export type { IsEmptyRecord } from './IsEmptyRecord.type'; export type { IsExactly } from './IsExactly'; +export { MessageParticipantRole } from './MessageParticipantRole'; export type { ModifiedProperties } from './ModifiedProperties'; export type { NonNullableRequired } from './NonNullableRequired'; export type { Nullable } from './Nullable';