Few fixes before release 1.3.0 (#13791)

This commit is contained in:
Charles Bochet
2025-08-09 03:08:11 +02:00
committed by GitHub
parent 1916333eac
commit b55c46f4a3
13 changed files with 106 additions and 85 deletions
@@ -17,7 +17,9 @@ type SettingsOptionCardContentSelectProps = {
};
const StyledSelectContainer = styled.div`
justify-content: flex-end;
margin-left: auto;
max-width: 120px;
`;
export const SettingsOptionCardContentSelect = ({
@@ -41,7 +41,7 @@ export const SettingsDataModelFieldIsUniqueForm = ({
<SettingsOptionCardContentSelect
Icon={IconKey}
title={t`Unique`}
description={t`Prevent users from assigining the same value to different records`}
description={t`Prevent from assigining the same value to different records`}
>
<Toggle
toggleSize="small"
@@ -34,7 +34,7 @@ import {
import { FieldMetadataType } from '~/generated-metadata/graphql';
const isUniqueFieldFormSchema = z.object({
isUnique: z.boolean().nullable().optional(),
isUnique: z.boolean().nullable().default(false),
});
const booleanFieldFormSchema = z
@@ -4,7 +4,6 @@ import { z } from 'zod';
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { SettingsDataModelPreviewFormCard } from '@/settings/data-model/components/SettingsDataModelPreviewFormCard';
import { SettingsDataModelFieldIsUniqueForm } from '@/settings/data-model/fields/forms/components/SettingsDataModelFieldIsUniqueForm';
import {
settingsDataModelFieldMultiSelectFormSchema,
SettingsDataModelFieldSelectForm,
@@ -68,9 +67,6 @@ export const SettingsDataModelFieldSelectSettingsFormCard = ({
<SettingsDataModelFieldSelectForm
fieldMetadataItem={fieldMetadataItem}
/>
<SettingsDataModelFieldIsUniqueForm
fieldMetadataItem={fieldMetadataItem}
/>
</>
}
/>
@@ -6,6 +6,10 @@ import { FieldMetadataType } from 'twenty-shared/types';
export const canBeUnique = (
field: Pick<FieldMetadataItem, 'type' | 'isCustom'>,
) => {
if (field.isCustom === false) {
return false;
}
if (
[FieldMetadataType.MORPH_RELATION, FieldMetadataType.RELATION].includes(
field.type,
@@ -6,6 +6,7 @@ export const fetchMetadataFields = (
selector: Selectors,
) => {
const defaultFields = `
id
type
name
label
@@ -5,7 +5,7 @@ import {
type BeforeUpdateOneHook,
type UpdateOneInputType,
} from '@ptc-org/nestjs-query-graphql';
import { type APP_LOCALES, SOURCE_LOCALE } from 'twenty-shared/translations';
import { SOURCE_LOCALE, type APP_LOCALES } from 'twenty-shared/translations';
import { isDefined } from 'twenty-shared/utils';
import {
@@ -188,6 +188,9 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
throw new FieldMetadataException(
'Unique field cannot have a default value',
FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
{
userFriendlyMessage: t`Unique field cannot have a default value`,
},
);
}
@@ -832,6 +835,9 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
throw new FieldMetadataException(
'Unique field cannot have a default value',
FieldMetadataExceptionCode.INVALID_FIELD_INPUT,
{
userFriendlyMessage: t`Unique field cannot have a default value`,
},
);
if (fieldMetadataInput.isUnique !== true) return;
@@ -34,7 +34,7 @@ export function formatData<T>(
if (!fieldMetadata) {
throw new Error(
`Field metadata for field "${key}" is missing in object metadata`,
`Field metadata for field "${key}" is missing in object metadata ${objectMetadataItemWithFieldMaps.nameSingular}`,
);
}
@@ -1,4 +1,4 @@
import { Injectable } from '@nestjs/common';
import { Injectable, Logger } from '@nestjs/common';
import { isNonEmptyString } from '@sniptt/guards';
import { type gmail_v1 as gmailV1 } from 'googleapis';
@@ -24,6 +24,7 @@ import { assertNotNull } from 'src/utils/assert';
@Injectable()
export class GmailGetMessageListService {
private readonly logger = new Logger(GmailGetMessageListService.name);
constructor(
private readonly gmailClientProvider: GmailClientProvider,
private readonly gmailGetHistoryService: GmailGetHistoryService,
@@ -55,6 +56,13 @@ export class GmailGetMessageListService {
q: computeGmailCategoryExcludeSearchFilter(excludedCategories),
})
.catch((error) => {
this.logger.error(
`Connected account ${connectedAccount.id}: Error fetching message list: ${error.message}`,
);
this.logger.error(
`Connected account ${connectedAccount.id}: Error fetching message list: ${JSON.stringify(error)}`,
);
this.gmailHandleErrorService.handleGmailMessageListFetchError(error);
return {
@@ -1,8 +1,8 @@
import { Injectable } from '@nestjs/common';
import { Injectable, Logger } from '@nestjs/common';
import {
type PageCollection,
PageIterator,
type PageCollection,
type PageIteratorCallback,
} from '@microsoft/microsoft-graph-client';
import { isNonEmptyString } from '@sniptt/guards';
@@ -30,6 +30,7 @@ const MESSAGING_MICROSOFT_USERS_MESSAGES_LIST_MAX_RESULT = 999;
@Injectable()
export class MicrosoftGetMessageListService {
private readonly logger = new Logger(MicrosoftGetMessageListService.name);
constructor(
private readonly microsoftClientProvider: MicrosoftClientProvider,
private readonly microsoftHandleErrorService: MicrosoftHandleErrorService,
@@ -123,6 +124,9 @@ export class MicrosoftGetMessageListService {
})
.get()
.catch((error) => {
this.logger.error(
`Connected account ${connectedAccount.id}: Error fetching message list: ${JSON.stringify(error)}`,
);
if (isAccessTokenRefreshingError(error?.body)) {
throw new MessageImportDriverException(
error.message,
@@ -45,76 +45,71 @@ export class MessagingSaveMessagesAndEnqueueContactCreationService {
const workspaceDataSource = await this.twentyORMManager.getDatasource();
const createdMessagesWithParticipants =
await workspaceDataSource?.transaction(
async (transactionManager: WorkspaceEntityManager) => {
const { messageExternalIdsAndIdsMap, createdMessages } =
await this.messageService.saveMessagesWithinTransaction(
messagesToSave,
messageChannel.id,
transactionManager,
workspaceId,
);
const participantsWithMessageId: (ParticipantWithMessageId & {
shouldCreateContact: boolean;
})[] = messagesToSave.flatMap((message) => {
const messageId = messageExternalIdsAndIdsMap.get(
message.externalId,
);
return messageId
? message.participants.map((participant: Participant) => {
const fromHandle =
message.participants.find((p) => p.role === 'from')
?.handle || '';
const isMessageSentByConnectedAccount =
handleAliases.includes(fromHandle) ||
fromHandle === connectedAccount.handle;
const isParticipantConnectedAccount =
handleAliases.includes(participant.handle) ||
participant.handle === connectedAccount.handle;
const isExcludedByNonProfessionalEmails =
messageChannel.excludeNonProfessionalEmails &&
!isWorkEmail(participant.handle);
const isExcludedByGroupEmails =
messageChannel.excludeGroupEmails &&
isGroupEmail(participant.handle);
const shouldCreateContact =
!!participant.handle &&
!isParticipantConnectedAccount &&
!isExcludedByNonProfessionalEmails &&
!isExcludedByGroupEmails &&
(messageChannel.contactAutoCreationPolicy ===
MessageChannelContactAutoCreationPolicy.SENT_AND_RECEIVED ||
(messageChannel.contactAutoCreationPolicy ===
MessageChannelContactAutoCreationPolicy.SENT &&
isMessageSentByConnectedAccount));
return {
...participant,
messageId,
shouldCreateContact,
};
})
: [];
});
await this.messageParticipantService.saveMessageParticipants(
participantsWithMessageId,
const participantsWithMessageId = await workspaceDataSource?.transaction(
async (transactionManager: WorkspaceEntityManager) => {
const { messageExternalIdsAndIdsMap } =
await this.messageService.saveMessagesWithinTransaction(
messagesToSave,
messageChannel.id,
transactionManager,
workspaceId,
);
return { participantsWithMessageId, createdMessages };
},
);
const participantsWithMessageId: (ParticipantWithMessageId & {
shouldCreateContact: boolean;
})[] = messagesToSave.flatMap((message) => {
const messageId = messageExternalIdsAndIdsMap.get(message.externalId);
const { participantsWithMessageId } = createdMessagesWithParticipants;
return messageId
? message.participants.map((participant: Participant) => {
const fromHandle =
message.participants.find((p) => p.role === 'from')?.handle ||
'';
const isMessageSentByConnectedAccount =
handleAliases.includes(fromHandle) ||
fromHandle === connectedAccount.handle;
const isParticipantConnectedAccount =
handleAliases.includes(participant.handle) ||
participant.handle === connectedAccount.handle;
const isExcludedByNonProfessionalEmails =
messageChannel.excludeNonProfessionalEmails &&
!isWorkEmail(participant.handle);
const isExcludedByGroupEmails =
messageChannel.excludeGroupEmails &&
isGroupEmail(participant.handle);
const shouldCreateContact =
!!participant.handle &&
!isParticipantConnectedAccount &&
!isExcludedByNonProfessionalEmails &&
!isExcludedByGroupEmails &&
(messageChannel.contactAutoCreationPolicy ===
MessageChannelContactAutoCreationPolicy.SENT_AND_RECEIVED ||
(messageChannel.contactAutoCreationPolicy ===
MessageChannelContactAutoCreationPolicy.SENT &&
isMessageSentByConnectedAccount));
return {
...participant,
messageId,
shouldCreateContact,
};
})
: [];
});
await this.messageParticipantService.saveMessageParticipants(
participantsWithMessageId,
transactionManager,
);
return participantsWithMessageId;
},
);
if (messageChannel.isContactAutoCreationEnabled) {
const contactsToCreate = participantsWithMessageId.filter(
@@ -68,15 +68,20 @@ const filterOutInternals = (
}
const primaryHandleDomain = getDomainNameByEmail(primaryHandle);
const isAllHandlesFromSameDomain = message.participants
.filter((participant) => !!participant.handle)
.every(
(participant) =>
getDomainNameByEmail(participant.handle) === primaryHandleDomain,
);
if (isAllHandlesFromSameDomain) {
return false;
try {
const isAllHandlesFromSameDomain = message.participants
.filter((participant) => !!participant.handle)
.every(
(participant) =>
getDomainNameByEmail(participant.handle) === primaryHandleDomain,
);
if (isAllHandlesFromSameDomain) {
return false;
}
} catch {
return true;
}
return true;