From 08c7947b3b97d3ec9486b1131a518fba07750eca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Bosi?= <71827178+bosiraphael@users.noreply.github.com> Date: Thu, 15 Aug 2024 10:15:32 +0200 Subject: [PATCH 1/3] Use twentyORM in Timeline messaging (#6595) - Remove raw queries and replace them by using `twentyORM` - Refactor into services and utils --------- Co-authored-by: Charles Bochet --- .../src/hooks/useIsMatchingLocation.ts | 2 +- .../MessageThreadSubscribersChip.tsx | 2 +- .../utils/generateDefaultRecordChipData.ts | 2 +- .../components/MultiSelectFieldInput.tsx | 4 +- .../RecordDetailRelationSection.tsx | 2 +- .../utils/findUnmatchedRequiredFields.ts | 9 +- .../modules/ui/input/components/Toggle.tsx | 2 +- .../dtos/timeline-thread-participant.dto.ts | 6 +- .../services/get-messages.service.ts | 101 ++++ .../services/timeline-messaging.service.ts | 259 +++++++++ .../messaging/timeline-messaging.module.ts | 11 +- .../messaging/timeline-messaging.resolver.ts | 22 +- .../messaging/timeline-messaging.service.ts | 525 ------------------ .../utils/extract-participant-summary.util.ts | 54 ++ .../utils/filter-active-participants.util.ts | 7 + .../utils/format-thread-participant.util.ts | 30 + .../messaging/utils/format-threads.util.ts | 28 + .../company.workspace-entity.ts | 4 +- .../person.workspace-entity.ts | 2 +- 19 files changed, 513 insertions(+), 559 deletions(-) create mode 100644 packages/twenty-server/src/engine/core-modules/messaging/services/get-messages.service.ts create mode 100644 packages/twenty-server/src/engine/core-modules/messaging/services/timeline-messaging.service.ts delete mode 100644 packages/twenty-server/src/engine/core-modules/messaging/timeline-messaging.service.ts create mode 100644 packages/twenty-server/src/engine/core-modules/messaging/utils/extract-participant-summary.util.ts create mode 100644 packages/twenty-server/src/engine/core-modules/messaging/utils/filter-active-participants.util.ts create mode 100644 packages/twenty-server/src/engine/core-modules/messaging/utils/format-thread-participant.util.ts create mode 100644 packages/twenty-server/src/engine/core-modules/messaging/utils/format-threads.util.ts diff --git a/packages/twenty-front/src/hooks/useIsMatchingLocation.ts b/packages/twenty-front/src/hooks/useIsMatchingLocation.ts index aa57dbb432e3..044a344068a3 100644 --- a/packages/twenty-front/src/hooks/useIsMatchingLocation.ts +++ b/packages/twenty-front/src/hooks/useIsMatchingLocation.ts @@ -9,7 +9,7 @@ export const useIsMatchingLocation = () => { return useCallback( (path: string, basePath?: AppBasePath) => { const constructedPath = basePath - ? (new URL(basePath + path, document.location.origin).pathname ?? '') + ? new URL(basePath + path, document.location.origin).pathname ?? '' : path; return !!matchPath(constructedPath, location.pathname); diff --git a/packages/twenty-front/src/modules/activities/emails/components/MessageThreadSubscribersChip.tsx b/packages/twenty-front/src/modules/activities/emails/components/MessageThreadSubscribersChip.tsx index 6ef97d07b28f..d10827215835 100644 --- a/packages/twenty-front/src/modules/activities/emails/components/MessageThreadSubscribersChip.tsx +++ b/packages/twenty-front/src/modules/activities/emails/components/MessageThreadSubscribersChip.tsx @@ -45,7 +45,7 @@ export const MessageThreadSubscribersChip = ({ ? `+${numberOfMessageThreadSubscribers - MAX_NUMBER_OF_AVATARS}` : null; - const label = isPrivateThread ? privateLabel : (moreAvatarsLabel ?? ''); + const label = isPrivateThread ? privateLabel : moreAvatarsLabel ?? ''; return ( { const name = isFieldFullNameValue(record.name) ? record.name.firstName + ' ' + record.name.lastName - : (record.name ?? ''); + : record.name ?? ''; return { name, diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/MultiSelectFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/MultiSelectFieldInput.tsx index c6134f911a2d..d36b2b9fbfc4 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/MultiSelectFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/MultiSelectFieldInput.tsx @@ -42,8 +42,8 @@ export const MultiSelectFieldInput = ({ const [searchFilter, setSearchFilter] = useState(''); const containerRef = useRef(null); - const selectedOptions = fieldDefinition.metadata.options.filter((option) => - fieldValues?.includes(option.value), + const selectedOptions = fieldDefinition.metadata.options.filter( + (option) => fieldValues?.includes(option.value), ); const optionsInDropDown = fieldDefinition.metadata.options; diff --git a/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailRelationSection.tsx b/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailRelationSection.tsx index 426078f807b4..e59ca3be75fe 100644 --- a/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailRelationSection.tsx +++ b/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailRelationSection.tsx @@ -69,7 +69,7 @@ export const RecordDetailRelationSection = ({ const relationRecords: ObjectRecord[] = fieldValue && isToOneObject ? [fieldValue as ObjectRecord] - : ((fieldValue as ObjectRecord[]) ?? []); + : (fieldValue as ObjectRecord[]) ?? []; const relationRecordIds = relationRecords.map(({ id }) => id); diff --git a/packages/twenty-front/src/modules/spreadsheet-import/utils/findUnmatchedRequiredFields.ts b/packages/twenty-front/src/modules/spreadsheet-import/utils/findUnmatchedRequiredFields.ts index 71af1840263d..e7219f70bddf 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/utils/findUnmatchedRequiredFields.ts +++ b/packages/twenty-front/src/modules/spreadsheet-import/utils/findUnmatchedRequiredFields.ts @@ -6,10 +6,11 @@ export const findUnmatchedRequiredFields = ( columns: Columns, ) => fields - .filter((field) => - field.fieldValidationDefinitions?.some( - (validation) => validation.rule === 'required', - ), + .filter( + (field) => + field.fieldValidationDefinitions?.some( + (validation) => validation.rule === 'required', + ), ) .filter( (field) => diff --git a/packages/twenty-front/src/modules/ui/input/components/Toggle.tsx b/packages/twenty-front/src/modules/ui/input/components/Toggle.tsx index f723e93e1c5a..9aadab0eb028 100644 --- a/packages/twenty-front/src/modules/ui/input/components/Toggle.tsx +++ b/packages/twenty-front/src/modules/ui/input/components/Toggle.tsx @@ -16,7 +16,7 @@ type ContainerProps = { const StyledContainer = styled.div` align-items: center; background-color: ${({ theme, isOn, color }) => - isOn ? (color ?? theme.color.blue) : theme.background.quaternary}; + isOn ? color ?? theme.color.blue : theme.background.quaternary}; border-radius: 10px; cursor: pointer; display: flex; diff --git a/packages/twenty-server/src/engine/core-modules/messaging/dtos/timeline-thread-participant.dto.ts b/packages/twenty-server/src/engine/core-modules/messaging/dtos/timeline-thread-participant.dto.ts index 6444474155ad..d0845ddf0f81 100644 --- a/packages/twenty-server/src/engine/core-modules/messaging/dtos/timeline-thread-participant.dto.ts +++ b/packages/twenty-server/src/engine/core-modules/messaging/dtos/timeline-thread-participant.dto.ts @@ -1,14 +1,14 @@ -import { ObjectType, Field } from '@nestjs/graphql'; +import { Field, ObjectType } from '@nestjs/graphql'; import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; @ObjectType('TimelineThreadParticipant') export class TimelineThreadParticipant { @Field(() => UUIDScalarType, { nullable: true }) - personId: string; + personId: string | null; @Field(() => UUIDScalarType, { nullable: true }) - workspaceMemberId: string; + workspaceMemberId: string | null; @Field() firstName: string; diff --git a/packages/twenty-server/src/engine/core-modules/messaging/services/get-messages.service.ts b/packages/twenty-server/src/engine/core-modules/messaging/services/get-messages.service.ts new file mode 100644 index 000000000000..63e5bf51cd25 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/messaging/services/get-messages.service.ts @@ -0,0 +1,101 @@ +import { Injectable } from '@nestjs/common'; + +import { TIMELINE_THREADS_DEFAULT_PAGE_SIZE } from 'src/engine/core-modules/messaging/constants/messaging.constants'; +import { TimelineThreadsWithTotal } from 'src/engine/core-modules/messaging/dtos/timeline-threads-with-total.dto'; +import { TimelineMessagingService } from 'src/engine/core-modules/messaging/services/timeline-messaging.service'; +import { formatThreads } from 'src/engine/core-modules/messaging/utils/format-threads.util'; +import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager'; +import { PersonWorkspaceEntity } from 'src/modules/person/standard-objects/person.workspace-entity'; + +@Injectable() +export class GetMessagesService { + constructor( + private readonly twentyORMManager: TwentyORMManager, + private readonly timelineMessagingService: TimelineMessagingService, + ) {} + + async getMessagesFromPersonIds( + workspaceMemberId: string, + personIds: string[], + page = 1, + pageSize: number = TIMELINE_THREADS_DEFAULT_PAGE_SIZE, + ): Promise { + const offset = (page - 1) * pageSize; + + const { messageThreads, totalNumberOfThreads } = + await this.timelineMessagingService.getAndCountMessageThreads( + personIds, + offset, + pageSize, + ); + + if (!messageThreads) { + return { + totalNumberOfThreads: 0, + timelineThreads: [], + }; + } + + const messageThreadIds = messageThreads.map( + (messageThread) => messageThread.id, + ); + + const threadParticipantsByThreadId = + await this.timelineMessagingService.getThreadParticipantsByThreadId( + messageThreadIds, + ); + + const threadVisibilityByThreadId = + await this.timelineMessagingService.getThreadVisibilityByThreadId( + messageThreadIds, + workspaceMemberId, + ); + + return { + totalNumberOfThreads, + timelineThreads: formatThreads( + messageThreads, + threadParticipantsByThreadId, + threadVisibilityByThreadId, + ), + }; + } + + async getMessagesFromCompanyId( + workspaceMemberId: string, + companyId: string, + page = 1, + pageSize: number = TIMELINE_THREADS_DEFAULT_PAGE_SIZE, + ): Promise { + const personRepository = + await this.twentyORMManager.getRepository( + 'person', + ); + const personIds = ( + await personRepository.find({ + where: { + companyId, + }, + select: { + id: true, + }, + }) + ).map((person) => person.id); + + if (personIds.length === 0) { + return { + totalNumberOfThreads: 0, + timelineThreads: [], + }; + } + + const messageThreads = await this.getMessagesFromPersonIds( + workspaceMemberId, + personIds, + page, + pageSize, + ); + + return messageThreads; + } +} 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 new file mode 100644 index 000000000000..87a24166b5b6 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/messaging/services/timeline-messaging.service.ts @@ -0,0 +1,259 @@ +import { Injectable } from '@nestjs/common'; + +import { Any, Not } from 'typeorm'; + +import { TimelineThread } from 'src/engine/core-modules/messaging/dtos/timeline-thread.dto'; +import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager'; +import { MessageChannelVisibility } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity'; +import { MessageParticipantWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-participant.workspace-entity'; +import { MessageThreadWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-thread.workspace-entity'; + +@Injectable() +export class TimelineMessagingService { + constructor(private readonly twentyORMManager: TwentyORMManager) {} + + public async getAndCountMessageThreads( + personIds: string[], + offset: number, + pageSize: number, + ): Promise<{ + messageThreads: Omit< + TimelineThread, + | 'firstParticipant' + | 'lastTwoParticipants' + | 'participantCount' + | 'read' + | 'visibility' + >[]; + totalNumberOfThreads: number; + }> { + const messageThreadRepository = + await this.twentyORMManager.getRepository( + 'messageThread', + ); + + const [messageThreadIds, totalNumberOfThreads] = + await messageThreadRepository.findAndCount({ + select: { + id: true, + }, + where: { + messages: { + messageParticipants: { + personId: Any(personIds), + }, + }, + }, + skip: offset, + take: pageSize, + }); + + const messageThreads = await messageThreadRepository.find({ + select: { + id: true, + }, + where: { + id: Any(messageThreadIds.map((thread) => thread.id)), + }, + order: { + messages: { + receivedAt: 'DESC', + }, + }, + relations: ['messages'], + }); + + return { + messageThreads: messageThreads.map((messageThread) => { + const lastMessage = messageThread.messages[0]; + const firstMessage = + messageThread.messages[messageThread.messages.length - 1]; + + return { + id: messageThread.id, + subject: firstMessage.subject, + lastMessageBody: lastMessage.text, + lastMessageReceivedAt: lastMessage.receivedAt ?? new Date(), + numberOfMessagesInThread: messageThread.messages.length, + }; + }), + totalNumberOfThreads, + }; + } + + public async getThreadParticipantsByThreadId( + messageThreadIds: string[], + ): Promise<{ + [key: string]: MessageParticipantWorkspaceEntity[]; + }> { + const messageParticipantRepository = + await this.twentyORMManager.getRepository( + 'messageParticipant', + ); + const threadParticipants = await messageParticipantRepository + .createQueryBuilder() + .select('messageParticipant') + .addSelect('message.messageThreadId') + .addSelect('message.receivedAt') + .leftJoinAndSelect('messageParticipant.person', 'person') + .leftJoinAndSelect( + 'messageParticipant.workspaceMember', + 'workspaceMember', + ) + .leftJoin('messageParticipant.message', 'message') + .where('message.messageThreadId = ANY(:messageThreadIds)', { + messageThreadIds, + }) + .andWhere('messageParticipant.role = :role', { role: 'from' }) + .orderBy('message.messageThreadId') + .distinctOn(['message.messageThreadId', 'messageParticipant.handle']) + .getMany(); + + // This is because subqueries are not handled by twentyORM + const orderedThreadParticipants = threadParticipants.sort( + (a, b) => + (a.message.receivedAt ?? new Date()).getTime() - + (b.message.receivedAt ?? new Date()).getTime(), + ); + + // This is because composite fields are not handled correctly by the ORM + const threadParticipantsWithCompositeFields = orderedThreadParticipants.map( + (threadParticipant) => ({ + ...threadParticipant, + person: { + id: threadParticipant.person?.id, + name: { + //eslint-disable-next-line + //@ts-ignore + firstName: threadParticipant.person?.nameFirstName, + //eslint-disable-next-line + //@ts-ignore + lastName: threadParticipant.person?.nameLastName, + }, + avatarUrl: threadParticipant.person?.avatarUrl, + }, + workspaceMember: { + id: threadParticipant.workspaceMember?.id, + name: { + //eslint-disable-next-line + //@ts-ignore + firstName: threadParticipant.workspaceMember?.nameFirstName, + //eslint-disable-next-line + //@ts-ignore + lastName: threadParticipant.workspaceMember?.nameLastName, + }, + avatarUrl: threadParticipant.workspaceMember?.avatarUrl, + }, + }), + ); + + return threadParticipantsWithCompositeFields.reduce( + (threadParticipantsAcc, threadParticipant) => { + if (!threadParticipant.message.messageThreadId) + return threadParticipantsAcc; + + if (!threadParticipantsAcc[threadParticipant.message.messageThreadId]) + threadParticipantsAcc[threadParticipant.message.messageThreadId] = []; + + threadParticipantsAcc[threadParticipant.message.messageThreadId].push( + threadParticipant, + ); + + return threadParticipantsAcc; + }, + {}, + ); + } + + public async getThreadVisibilityByThreadId( + messageThreadIds: string[], + workspaceMemberId: string, + ): Promise<{ + [key: string]: MessageChannelVisibility; + }> { + const messageThreadRepository = + await this.twentyORMManager.getRepository( + 'messageThread', + ); + + const threadsWithoutWorkspaceMember = await messageThreadRepository.find({ + select: { + id: true, + }, + where: { + id: Any(messageThreadIds), + messages: { + messageChannelMessageAssociations: { + messageChannel: { + connectedAccount: { + accountOwnerId: Not(workspaceMemberId), + }, + }, + }, + }, + }, + }); + + const threadIdsWithoutWorkspaceMember = threadsWithoutWorkspaceMember.map( + (thread) => thread.id, + ); + + const threadVisibility = await messageThreadRepository + .createQueryBuilder() + .select('messageThread.id', 'id') + .addSelect('messageChannel.visibility', 'visibility') + .leftJoin('messageThread.messages', 'message') + .leftJoin( + 'message.messageChannelMessageAssociations', + 'messageChannelMessageAssociation', + ) + .leftJoin( + 'messageChannelMessageAssociation.messageChannel', + 'messageChannel', + ) + .where('messageThread.id = ANY(:messageThreadIds)', { + messageThreadIds: threadIdsWithoutWorkspaceMember, + }) + .getRawMany(); + + const visibilityValues = Object.values(MessageChannelVisibility); + + const threadVisibilityByThreadIdForWhichWorkspaceMemberIsNotInParticipants: + | { + [key: string]: MessageChannelVisibility; + } + | undefined = threadVisibility?.reduce( + (threadVisibilityAcc, threadVisibility) => { + threadVisibilityAcc[threadVisibility.id] = + visibilityValues[ + Math.max( + visibilityValues.indexOf(threadVisibility.visibility), + visibilityValues.indexOf( + threadVisibilityAcc[threadVisibility.id] ?? + MessageChannelVisibility.METADATA, + ), + ) + ]; + + return threadVisibilityAcc; + }, + {}, + ); + + const threadVisibilityByThreadId: { + [key: string]: MessageChannelVisibility; + } = messageThreadIds.reduce((threadVisibilityAcc, messageThreadId) => { + // If the workspace member is not in the participants of the thread, use the visibility value from the query + threadVisibilityAcc[messageThreadId] = + threadIdsWithoutWorkspaceMember.includes(messageThreadId) + ? (threadVisibilityByThreadIdForWhichWorkspaceMemberIsNotInParticipants?.[ + messageThreadId + ] ?? MessageChannelVisibility.METADATA) + : MessageChannelVisibility.SHARE_EVERYTHING; + + return threadVisibilityAcc; + }, {}); + + return threadVisibilityByThreadId; + } +} diff --git a/packages/twenty-server/src/engine/core-modules/messaging/timeline-messaging.module.ts b/packages/twenty-server/src/engine/core-modules/messaging/timeline-messaging.module.ts index 275f7f0df151..f34f1c4e4476 100644 --- a/packages/twenty-server/src/engine/core-modules/messaging/timeline-messaging.module.ts +++ b/packages/twenty-server/src/engine/core-modules/messaging/timeline-messaging.module.ts @@ -1,12 +1,17 @@ import { Module } from '@nestjs/common'; +import { GetMessagesService } from 'src/engine/core-modules/messaging/services/get-messages.service'; +import { TimelineMessagingService } from 'src/engine/core-modules/messaging/services/timeline-messaging.service'; import { TimelineMessagingResolver } from 'src/engine/core-modules/messaging/timeline-messaging.resolver'; -import { TimelineMessagingService } from 'src/engine/core-modules/messaging/timeline-messaging.service'; -import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module'; import { UserModule } from 'src/engine/core-modules/user/user.module'; +import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module'; @Module({ imports: [WorkspaceDataSourceModule, UserModule], exports: [], - providers: [TimelineMessagingResolver, TimelineMessagingService], + providers: [ + TimelineMessagingResolver, + TimelineMessagingService, + GetMessagesService, + ], }) export class TimelineMessagingModule {} diff --git a/packages/twenty-server/src/engine/core-modules/messaging/timeline-messaging.resolver.ts b/packages/twenty-server/src/engine/core-modules/messaging/timeline-messaging.resolver.ts index 6ed83dc9d215..33a3a3caeba4 100644 --- a/packages/twenty-server/src/engine/core-modules/messaging/timeline-messaging.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/messaging/timeline-messaging.resolver.ts @@ -1,18 +1,16 @@ -import { Args, Query, Resolver, Int, ArgsType, Field } from '@nestjs/graphql'; import { UseGuards } from '@nestjs/common'; +import { Args, ArgsType, Field, Int, Query, Resolver } from '@nestjs/graphql'; import { Max } from 'class-validator'; -import { JwtAuthGuard } from 'src/engine/guards/jwt.auth.guard'; -import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; -import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator'; -import { TimelineMessagingService } from 'src/engine/core-modules/messaging/timeline-messaging.service'; +import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; import { TIMELINE_THREADS_MAX_PAGE_SIZE } from 'src/engine/core-modules/messaging/constants/messaging.constants'; import { TimelineThreadsWithTotal } from 'src/engine/core-modules/messaging/dtos/timeline-threads-with-total.dto'; -import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator'; +import { GetMessagesService } from 'src/engine/core-modules/messaging/services/get-messages.service'; import { UserService } from 'src/engine/core-modules/user/services/user.service'; import { User } from 'src/engine/core-modules/user/user.entity'; -import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; +import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator'; +import { JwtAuthGuard } from 'src/engine/guards/jwt.auth.guard'; @ArgsType() class GetTimelineThreadsFromPersonIdArgs { @@ -44,13 +42,12 @@ class GetTimelineThreadsFromCompanyIdArgs { @Resolver(() => TimelineThreadsWithTotal) export class TimelineMessagingResolver { constructor( - private readonly timelineMessagingService: TimelineMessagingService, + private readonly getMessagesFromPersonIdsService: GetMessagesService, private readonly userService: UserService, ) {} @Query(() => TimelineThreadsWithTotal) async getTimelineThreadsFromPersonId( - @AuthWorkspace() { id: workspaceId }: Workspace, @AuthUser() user: User, @Args() { personId, page, pageSize }: GetTimelineThreadsFromPersonIdArgs, ) { @@ -61,9 +58,8 @@ export class TimelineMessagingResolver { } const timelineThreads = - await this.timelineMessagingService.getMessagesFromPersonIds( + await this.getMessagesFromPersonIdsService.getMessagesFromPersonIds( workspaceMember.id, - workspaceId, [personId], page, pageSize, @@ -74,7 +70,6 @@ export class TimelineMessagingResolver { @Query(() => TimelineThreadsWithTotal) async getTimelineThreadsFromCompanyId( - @AuthWorkspace() { id: workspaceId }: Workspace, @AuthUser() user: User, @Args() { companyId, page, pageSize }: GetTimelineThreadsFromCompanyIdArgs, ) { @@ -85,9 +80,8 @@ export class TimelineMessagingResolver { } const timelineThreads = - await this.timelineMessagingService.getMessagesFromCompanyId( + await this.getMessagesFromPersonIdsService.getMessagesFromCompanyId( workspaceMember.id, - workspaceId, companyId, page, pageSize, diff --git a/packages/twenty-server/src/engine/core-modules/messaging/timeline-messaging.service.ts b/packages/twenty-server/src/engine/core-modules/messaging/timeline-messaging.service.ts deleted file mode 100644 index 5982b0d3148d..000000000000 --- a/packages/twenty-server/src/engine/core-modules/messaging/timeline-messaging.service.ts +++ /dev/null @@ -1,525 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -import { TIMELINE_THREADS_DEFAULT_PAGE_SIZE } from 'src/engine/core-modules/messaging/constants/messaging.constants'; -import { TimelineThreadsWithTotal } from 'src/engine/core-modules/messaging/dtos/timeline-threads-with-total.dto'; -import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service'; -import { MessageChannelVisibility } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity'; - -type TimelineThreadParticipant = { - personId: string; - workspaceMemberId: string; - firstName: string; - lastName: string; - displayName: string; - avatarUrl: string; - handle: string; -}; - -@Injectable() -export class TimelineMessagingService { - constructor( - private readonly workspaceDataSourceService: WorkspaceDataSourceService, - ) {} - - async getMessagesFromPersonIds( - workspaceMemberId: string, - workspaceId: string, - personIds: string[], - page = 1, - pageSize: number = TIMELINE_THREADS_DEFAULT_PAGE_SIZE, - ): Promise { - const offset = (page - 1) * pageSize; - - const dataSourceSchema = - this.workspaceDataSourceService.getSchemaName(workspaceId); - - const messageThreads: - | { - id: string; - lastMessageReceivedAt: Date; - lastMessageId: string; - lastMessageBody: string; - rowNumber: number; - }[] - | undefined = await this.workspaceDataSourceService.executeRawQuery( - ` - SELECT id, - "lastMessageReceivedAt", - "lastMessageId", - "lastMessageBody" - FROM - (SELECT message."messageThreadId" AS id, - MAX(message."receivedAt") AS "lastMessageReceivedAt", - message.id AS "lastMessageId", - message.text AS "lastMessageBody", - ROW_NUMBER() OVER (PARTITION BY message."messageThreadId" ORDER BY MAX(message."receivedAt") DESC) AS "rowNumber" - FROM - ${dataSourceSchema}."message" message - LEFT JOIN - ${dataSourceSchema}."messageParticipant" "messageParticipant" ON "messageParticipant"."messageId" = message.id - WHERE - "messageParticipant"."personId" = ANY($1) - AND EXISTS ( - SELECT 1 - FROM ${dataSourceSchema}."messageChannelMessageAssociation" mcma - WHERE mcma."messageId" = message.id - ) - GROUP BY - message."messageThreadId", - message.id - ORDER BY - message."receivedAt" DESC - ) AS "messageThreads" - WHERE - "rowNumber" = 1 - LIMIT $2 - OFFSET $3 - `, - [personIds, pageSize, offset], - workspaceId, - ); - - if (!messageThreads) { - return { - totalNumberOfThreads: 0, - timelineThreads: [], - }; - } - - const messageThreadIds = messageThreads.map( - (messageThread) => messageThread.id, - ); - - const threadSubjects: - | { - id: string; - subject: string; - }[] - | undefined = await this.workspaceDataSourceService.executeRawQuery( - ` - SELECT * - FROM - (SELECT - message."messageThreadId" AS id, - message.subject, - ROW_NUMBER() OVER (PARTITION BY message."messageThreadId" ORDER BY MAX(message."receivedAt") ASC) AS "rowNumber" - FROM - ${dataSourceSchema}."message" message - WHERE - message."messageThreadId" = ANY($1) - GROUP BY - message."messageThreadId", - message.id - ORDER BY - message."receivedAt" DESC - ) AS "messageThreads" - WHERE - "rowNumber" = 1 - `, - [messageThreadIds], - workspaceId, - ); - - const numberOfMessagesInThread: - | { - id: string; - numberOfMessagesInThread: number; - }[] - | undefined = await this.workspaceDataSourceService.executeRawQuery( - ` - SELECT - message."messageThreadId" AS id, - COUNT(message.id) AS "numberOfMessagesInThread" - FROM - ${dataSourceSchema}."message" message - WHERE - message."messageThreadId" = ANY($1) - AND EXISTS ( - SELECT 1 - FROM ${dataSourceSchema}."messageChannelMessageAssociation" mcma - WHERE mcma."messageId" = message.id - ) - GROUP BY - message."messageThreadId" - `, - [messageThreadIds], - workspaceId, - ); - - const messageThreadsByMessageThreadId: { - [key: string]: { - id: string; - lastMessageReceivedAt: Date; - lastMessageBody: string; - }; - } = messageThreads.reduce((messageThreadAcc, messageThread) => { - messageThreadAcc[messageThread.id] = messageThread; - - return messageThreadAcc; - }, {}); - - const subjectsByMessageThreadId: - | { - [key: string]: { - id: string; - subject: string; - }; - } - | undefined = threadSubjects?.reduce( - (threadSubjectAcc, threadSubject) => { - threadSubjectAcc[threadSubject.id] = threadSubject; - - return threadSubjectAcc; - }, - {}, - ); - - const numberOfMessagesByMessageThreadId: - | { - [key: string]: { - id: string; - numberOfMessagesInThread: number; - }; - } - | undefined = numberOfMessagesInThread?.reduce( - (numberOfMessagesAcc, numberOfMessages) => { - numberOfMessagesAcc[numberOfMessages.id] = numberOfMessages; - - return numberOfMessagesAcc; - }, - {}, - ); - - const threadMessagesParticipants: - | { - id: string; - messageId: string; - receivedAt: Date; - body: string; - subject: string; - role: string; - personId: string; - workspaceMemberId: string; - handle: string; - personFirstName: string; - personLastName: string; - personAvatarUrl: string; - workspaceMemberFirstName: string; - workspaceMemberLastName: string; - workspaceMemberAvatarUrl: string; - messageDisplayName: string; - }[] - | undefined = await this.workspaceDataSourceService.executeRawQuery( - ` - SELECT DISTINCT message."messageThreadId" AS id, - message.id AS "messageId", - message."receivedAt", - message.text, - message."subject", - "messageParticipant"."role", - "messageParticipant"."personId", - "messageParticipant"."workspaceMemberId", - "messageParticipant".handle, - "person"."nameFirstName" as "personFirstName", - "person"."nameLastName" as "personLastName", - "person"."avatarUrl" as "personAvatarUrl", - "workspaceMember"."nameFirstName" as "workspaceMemberFirstName", - "workspaceMember"."nameLastName" as "workspaceMemberLastName", - "workspaceMember"."avatarUrl" as "workspaceMemberAvatarUrl", - "messageParticipant"."displayName" as "messageDisplayName" - FROM - ${dataSourceSchema}."message" message - LEFT JOIN - ${dataSourceSchema}."messageParticipant" "messageParticipant" ON "messageParticipant"."messageId" = message.id - LEFT JOIN - ${dataSourceSchema}."person" person ON person."id" = "messageParticipant"."personId" - LEFT JOIN - ${dataSourceSchema}."workspaceMember" "workspaceMember" ON "workspaceMember".id = "messageParticipant"."workspaceMemberId" - WHERE - message."messageThreadId" = ANY($1) - ORDER BY - message."receivedAt" DESC - `, - [messageThreadIds], - workspaceId, - ); - - const threadMessagesFromActiveParticipants = - threadMessagesParticipants?.filter( - (threadMessage) => threadMessage.role === 'from', - ); - - const totalNumberOfThreads = - await this.workspaceDataSourceService.executeRawQuery( - ` - SELECT COUNT(DISTINCT message."messageThreadId") - FROM - ${dataSourceSchema}."message" message - LEFT JOIN - ${dataSourceSchema}."messageParticipant" "messageParticipant" ON "messageParticipant"."messageId" = message.id - WHERE - "messageParticipant"."personId" = ANY($1) - AND EXISTS ( - SELECT 1 - FROM ${dataSourceSchema}."messageChannelMessageAssociation" mcma - WHERE mcma."messageId" = message.id - ) - `, - [personIds], - workspaceId, - ); - - const threadActiveParticipantsByThreadId: { - [key: string]: TimelineThreadParticipant[]; - } = messageThreadIds.reduce((messageThreadIdAcc, messageThreadId) => { - const threadMessages = threadMessagesFromActiveParticipants?.filter( - (threadMessage) => threadMessage.id === messageThreadId, - ); - - const threadActiveParticipants = threadMessages?.reduce( - ( - threadMessageAcc, - threadMessage, - ): { - [key: string]: TimelineThreadParticipant; - } => { - const threadParticipant = threadMessageAcc[threadMessage.handle]; - - const firstName = - threadMessage.personFirstName || - threadMessage.workspaceMemberFirstName || - ''; - - const lastName = - threadMessage.personLastName || - threadMessage.workspaceMemberLastName || - ''; - - const displayName = - firstName || - threadMessage.messageDisplayName || - threadMessage.handle; - - if (!threadParticipant) { - threadMessageAcc[threadMessage.handle] = { - personId: threadMessage.personId, - workspaceMemberId: threadMessage.workspaceMemberId, - firstName, - lastName, - displayName, - avatarUrl: - threadMessage.personAvatarUrl ?? - threadMessage.workspaceMemberAvatarUrl ?? - '', - handle: threadMessage.handle, - }; - } - - return threadMessageAcc; - }, - {}, - ); - - messageThreadIdAcc[messageThreadId] = threadActiveParticipants - ? Object.values(threadActiveParticipants) - : []; - - return messageThreadIdAcc; - }, {}); - - const messageThreadIdsForWhichWorkspaceMemberIsNotInParticipants = - messageThreadIds.reduce( - ( - messageThreadIdsForWhichWorkspaceMemberIsInNotParticipantsAcc: string[], - messageThreadId, - ) => { - const threadMessagesWithWorkspaceMemberInParticipants = - threadMessagesParticipants?.filter( - (threadMessage) => - threadMessage.id === messageThreadId && - threadMessage.workspaceMemberId === workspaceMemberId, - ) ?? []; - - if (threadMessagesWithWorkspaceMemberInParticipants.length === 0) - messageThreadIdsForWhichWorkspaceMemberIsInNotParticipantsAcc.push( - messageThreadId, - ); - - return messageThreadIdsForWhichWorkspaceMemberIsInNotParticipantsAcc; - }, - [], - ); - - const threadVisibility: - | { - id: string; - visibility: MessageChannelVisibility; - }[] - | undefined = await this.workspaceDataSourceService.executeRawQuery( - ` - SELECT - message."messageThreadId" AS id, - "messageChannel".visibility - FROM - ${dataSourceSchema}."message" message - LEFT JOIN - ${dataSourceSchema}."messageChannelMessageAssociation" "messageChannelMessageAssociation" ON "messageChannelMessageAssociation"."messageId" = message.id - LEFT JOIN - ${dataSourceSchema}."messageChannel" "messageChannel" ON "messageChannel".id = "messageChannelMessageAssociation"."messageChannelId" - WHERE - message."messageThreadId" = ANY($1) - `, - [messageThreadIdsForWhichWorkspaceMemberIsNotInParticipants], - workspaceId, - ); - - const visibilityValues = Object.values(MessageChannelVisibility); - - const threadVisibilityByThreadIdForWhichWorkspaceMemberIsNotInParticipants: - | { - [key: string]: MessageChannelVisibility; - } - | undefined = threadVisibility?.reduce( - (threadVisibilityAcc, threadVisibility) => { - threadVisibilityAcc[threadVisibility.id] = - visibilityValues[ - Math.max( - visibilityValues.indexOf(threadVisibility.visibility), - visibilityValues.indexOf( - threadVisibilityAcc[threadVisibility.id] ?? - MessageChannelVisibility.METADATA, - ), - ) - ]; - - return threadVisibilityAcc; - }, - {}, - ); - - const threadVisibilityByThreadId: { - [key: string]: MessageChannelVisibility; - } = messageThreadIds.reduce((threadVisibilityAcc, messageThreadId) => { - // If the workspace member is not in the participants of the thread, use the visibility value from the query - threadVisibilityAcc[messageThreadId] = - messageThreadIdsForWhichWorkspaceMemberIsNotInParticipants.includes( - messageThreadId, - ) - ? (threadVisibilityByThreadIdForWhichWorkspaceMemberIsNotInParticipants?.[ - messageThreadId - ] ?? MessageChannelVisibility.METADATA) - : MessageChannelVisibility.SHARE_EVERYTHING; - - return threadVisibilityAcc; - }, {}); - - const timelineThreads = messageThreadIds.map((messageThreadId) => { - const threadActiveParticipants = - threadActiveParticipantsByThreadId[messageThreadId]; - - const firstParticipant = threadActiveParticipants[0]; - - const threadActiveParticipantsWithoutFirstParticipant = - threadActiveParticipants.filter( - (threadParticipant) => - threadParticipant.handle !== firstParticipant.handle, - ); - - const lastTwoParticipants: TimelineThreadParticipant[] = []; - - const lastParticipant = - threadActiveParticipantsWithoutFirstParticipant.slice(-1)[0]; - - if (lastParticipant) { - lastTwoParticipants.push(lastParticipant); - - const threadActiveParticipantsWithoutFirstAndLastParticipants = - threadActiveParticipantsWithoutFirstParticipant.filter( - (threadParticipant) => - threadParticipant.handle !== lastParticipant.handle, - ); - - if (threadActiveParticipantsWithoutFirstAndLastParticipants.length > 0) - lastTwoParticipants.push( - threadActiveParticipantsWithoutFirstAndLastParticipants.slice( - -1, - )[0], - ); - } - - const thread = messageThreadsByMessageThreadId[messageThreadId]; - - const threadSubject = - subjectsByMessageThreadId?.[messageThreadId].subject ?? ''; - - const numberOfMessages = - numberOfMessagesByMessageThreadId?.[messageThreadId] - .numberOfMessagesInThread ?? 1; - - return { - id: messageThreadId, - read: true, - firstParticipant, - lastTwoParticipants, - lastMessageReceivedAt: thread.lastMessageReceivedAt, - lastMessageBody: thread.lastMessageBody, - visibility: - threadVisibilityByThreadId?.[messageThreadId] ?? - MessageChannelVisibility.METADATA, - subject: threadSubject, - numberOfMessagesInThread: numberOfMessages, - participantCount: threadActiveParticipants.length, - }; - }); - - return { - totalNumberOfThreads: totalNumberOfThreads[0]?.count ?? 0, - timelineThreads, - }; - } - - async getMessagesFromCompanyId( - workspaceMemberId: string, - workspaceId: string, - companyId: string, - page = 1, - pageSize: number = TIMELINE_THREADS_DEFAULT_PAGE_SIZE, - ): Promise { - const dataSourceSchema = - this.workspaceDataSourceService.getSchemaName(workspaceId); - - const personIds = await this.workspaceDataSourceService.executeRawQuery( - ` - SELECT - p."id" - FROM - ${dataSourceSchema}."person" p - WHERE - p."companyId" = $1 - `, - [companyId], - workspaceId, - ); - - if (!personIds) { - return { - totalNumberOfThreads: 0, - timelineThreads: [], - }; - } - - const formattedPersonIds = personIds.map( - (personId: { id: string }) => personId.id, - ); - - const messageThreads = await this.getMessagesFromPersonIds( - workspaceMemberId, - workspaceId, - formattedPersonIds, - page, - pageSize, - ); - - return messageThreads; - } -} diff --git a/packages/twenty-server/src/engine/core-modules/messaging/utils/extract-participant-summary.util.ts b/packages/twenty-server/src/engine/core-modules/messaging/utils/extract-participant-summary.util.ts new file mode 100644 index 000000000000..46d8b18f78b2 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/messaging/utils/extract-participant-summary.util.ts @@ -0,0 +1,54 @@ +import { TimelineThreadParticipant } from 'src/engine/core-modules/messaging/dtos/timeline-thread-participant.dto'; +import { filterActiveParticipants } from 'src/engine/core-modules/messaging/utils/filter-active-participants.util'; +import { formatThreadParticipant } from 'src/engine/core-modules/messaging/utils/format-thread-participant.util'; +import { MessageParticipantWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-participant.workspace-entity'; + +export const extractParticipantSummary = ( + messageParticipants: MessageParticipantWorkspaceEntity[], +): { + firstParticipant: TimelineThreadParticipant; + lastTwoParticipants: TimelineThreadParticipant[]; + participantCount: number; +} => { + const activeMessageParticipants = + filterActiveParticipants(messageParticipants); + + const firstParticipant = formatThreadParticipant( + activeMessageParticipants[0], + ); + + const activeMessageParticipantsWithoutFirstParticipant = + activeMessageParticipants.filter( + (threadParticipant) => + threadParticipant.handle !== firstParticipant.handle, + ); + + const lastTwoParticipants: TimelineThreadParticipant[] = []; + + const lastParticipant = + activeMessageParticipantsWithoutFirstParticipant.slice(-1)[0]; + + if (lastParticipant) { + lastTwoParticipants.push(formatThreadParticipant(lastParticipant)); + + const activeMessageParticipantsWithoutFirstAndLastParticipants = + activeMessageParticipantsWithoutFirstParticipant.filter( + (threadParticipant) => + threadParticipant.handle !== lastParticipant.handle, + ); + + if (activeMessageParticipantsWithoutFirstAndLastParticipants.length > 0) { + lastTwoParticipants.push( + formatThreadParticipant( + activeMessageParticipantsWithoutFirstAndLastParticipants.slice(-1)[0], + ), + ); + } + } + + return { + firstParticipant, + lastTwoParticipants, + participantCount: activeMessageParticipants.length, + }; +}; 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 new file mode 100644 index 000000000000..7e743b6a5eb7 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/messaging/utils/filter-active-participants.util.ts @@ -0,0 +1,7 @@ +import { 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'); +}; diff --git a/packages/twenty-server/src/engine/core-modules/messaging/utils/format-thread-participant.util.ts b/packages/twenty-server/src/engine/core-modules/messaging/utils/format-thread-participant.util.ts new file mode 100644 index 000000000000..4cf5f5ead1fb --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/messaging/utils/format-thread-participant.util.ts @@ -0,0 +1,30 @@ +import { TimelineThreadParticipant } from 'src/engine/core-modules/messaging/dtos/timeline-thread-participant.dto'; +import { MessageParticipantWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-participant.workspace-entity'; + +export const formatThreadParticipant = ( + threadParticipant: MessageParticipantWorkspaceEntity, +): TimelineThreadParticipant => ({ + personId: threadParticipant.personId, + workspaceMemberId: threadParticipant.workspaceMemberId, + firstName: + threadParticipant.person?.name?.firstName || + threadParticipant.workspaceMember?.name.firstName || + '', + lastName: + threadParticipant.person?.name?.lastName || + threadParticipant.workspaceMember?.name.lastName || + '', + displayName: + threadParticipant.person?.name?.firstName || + threadParticipant.person?.name?.lastName || + threadParticipant.workspaceMember?.name.firstName || + threadParticipant.workspaceMember?.name.lastName || + threadParticipant.displayName || + threadParticipant.handle || + '', + avatarUrl: + threadParticipant.person?.avatarUrl || + threadParticipant.workspaceMember?.avatarUrl || + '', + handle: threadParticipant.handle, +}); diff --git a/packages/twenty-server/src/engine/core-modules/messaging/utils/format-threads.util.ts b/packages/twenty-server/src/engine/core-modules/messaging/utils/format-threads.util.ts new file mode 100644 index 000000000000..a141cbca12ea --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/messaging/utils/format-threads.util.ts @@ -0,0 +1,28 @@ +import { TimelineThread } from 'src/engine/core-modules/messaging/dtos/timeline-thread.dto'; +import { extractParticipantSummary } from 'src/engine/core-modules/messaging/utils/extract-participant-summary.util'; +import { MessageChannelVisibility } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity'; +import { MessageParticipantWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-participant.workspace-entity'; + +export const formatThreads = ( + threads: Omit< + TimelineThread, + | 'firstParticipant' + | 'lastTwoParticipants' + | 'participantCount' + | 'read' + | 'visibility' + >[], + threadParticipantsByThreadId: { + [key: string]: MessageParticipantWorkspaceEntity[]; + }, + threadVisibilityByThreadId: { + [key: string]: MessageChannelVisibility; + }, +): TimelineThread[] => { + return threads.map((thread) => ({ + ...thread, + ...extractParticipantSummary(threadParticipantsByThreadId[thread.id]), + visibility: threadVisibilityByThreadId[thread.id], + read: true, + })); +}; diff --git a/packages/twenty-server/src/modules/company/standard-objects/company.workspace-entity.ts b/packages/twenty-server/src/modules/company/standard-objects/company.workspace-entity.ts index 3947cd94b92b..32e774d4ccd5 100644 --- a/packages/twenty-server/src/modules/company/standard-objects/company.workspace-entity.ts +++ b/packages/twenty-server/src/modules/company/standard-objects/company.workspace-entity.ts @@ -4,6 +4,7 @@ import { ActorMetadata, FieldActorSource, } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type'; +import { AddressMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/address.composite-type'; import { CurrencyMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/currency.composite-type'; import { LinksMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/links.composite-type'; import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; @@ -30,7 +31,6 @@ import { PersonWorkspaceEntity } from 'src/modules/person/standard-objects/perso import { TaskTargetWorkspaceEntity } from 'src/modules/task/standard-objects/task-target.workspace-entity'; import { TimelineActivityWorkspaceEntity } from 'src/modules/timeline/standard-objects/timeline-activity.workspace-entity'; import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; -import { AddressMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/address.composite-type'; @WorkspaceEntity({ standardId: STANDARD_OBJECT_IDS.company, @@ -132,7 +132,7 @@ export class CompanyWorkspaceEntity extends BaseWorkspaceEntity { }) @WorkspaceIsSystem() @WorkspaceIsNullable() - position: number | null; + position: number; @WorkspaceField({ standardId: COMPANY_STANDARD_FIELD_IDS.createdBy, diff --git a/packages/twenty-server/src/modules/person/standard-objects/person.workspace-entity.ts b/packages/twenty-server/src/modules/person/standard-objects/person.workspace-entity.ts index 46300a1451cc..ac8aa9f628a5 100644 --- a/packages/twenty-server/src/modules/person/standard-objects/person.workspace-entity.ts +++ b/packages/twenty-server/src/modules/person/standard-objects/person.workspace-entity.ts @@ -127,7 +127,7 @@ export class PersonWorkspaceEntity extends BaseWorkspaceEntity { }) @WorkspaceIsSystem() @WorkspaceIsNullable() - position: number | null; + position: number; @WorkspaceField({ standardId: PERSON_STANDARD_FIELD_IDS.createdBy, From 5404a8ba2d5b7ad5383b8c3a7f80bf423b34c630 Mon Sep 17 00:00:00 2001 From: NitinPSingh <71833171+NitinPSingh@users.noreply.github.com> Date: Fri, 16 Aug 2024 19:01:04 +0530 Subject: [PATCH 2/3] fixes #6499 alignment issue on workspace switcher (#6589) fix #6499 - fix the size of workspace switcher to 32px from 40px ![Screenshot 2024-08-09 140212](https://github.com/user-attachments/assets/425c9089-8969-4d59-82ef-10572cfa7027) - fix alignment issues ![Screenshot 2024-08-09 142357](https://github.com/user-attachments/assets/2e3e76f2-8a81-48e9-86ff-691c11215583) --------- Co-authored-by: Lucas Bordeau --- packages/twenty-front/src/hooks/useIsMatchingLocation.ts | 2 +- .../src/loading/components/LeftPanelSkeletonLoader.tsx | 2 +- .../emails/components/MessageThreadSubscribersChip.tsx | 2 +- .../utils/generateDefaultRecordChipData.ts | 2 +- .../input/components/MultiSelectFieldInput.tsx | 4 ++-- .../components/RecordDetailRelationSection.tsx | 2 +- .../utils/findUnmatchedRequiredFields.ts | 9 ++++----- .../src/modules/ui/input/components/Toggle.tsx | 2 +- .../navigation-drawer/components/NavigationDrawer.tsx | 2 +- .../components/NavigationDrawerHeader.tsx | 2 -- 10 files changed, 13 insertions(+), 16 deletions(-) diff --git a/packages/twenty-front/src/hooks/useIsMatchingLocation.ts b/packages/twenty-front/src/hooks/useIsMatchingLocation.ts index 044a344068a3..aa57dbb432e3 100644 --- a/packages/twenty-front/src/hooks/useIsMatchingLocation.ts +++ b/packages/twenty-front/src/hooks/useIsMatchingLocation.ts @@ -9,7 +9,7 @@ export const useIsMatchingLocation = () => { return useCallback( (path: string, basePath?: AppBasePath) => { const constructedPath = basePath - ? new URL(basePath + path, document.location.origin).pathname ?? '' + ? (new URL(basePath + path, document.location.origin).pathname ?? '') : path; return !!matchPath(constructedPath, location.pathname); diff --git a/packages/twenty-front/src/loading/components/LeftPanelSkeletonLoader.tsx b/packages/twenty-front/src/loading/components/LeftPanelSkeletonLoader.tsx index 9a94373cef34..38803c9efecf 100644 --- a/packages/twenty-front/src/loading/components/LeftPanelSkeletonLoader.tsx +++ b/packages/twenty-front/src/loading/components/LeftPanelSkeletonLoader.tsx @@ -17,7 +17,7 @@ const StyledItemsContainer = styled.div` align-items: center; display: flex; flex-direction: column; - gap: 12px; + gap: 14px; height: calc(100dvh - 32px); margin-bottom: auto; max-width: 204px; diff --git a/packages/twenty-front/src/modules/activities/emails/components/MessageThreadSubscribersChip.tsx b/packages/twenty-front/src/modules/activities/emails/components/MessageThreadSubscribersChip.tsx index d10827215835..6ef97d07b28f 100644 --- a/packages/twenty-front/src/modules/activities/emails/components/MessageThreadSubscribersChip.tsx +++ b/packages/twenty-front/src/modules/activities/emails/components/MessageThreadSubscribersChip.tsx @@ -45,7 +45,7 @@ export const MessageThreadSubscribersChip = ({ ? `+${numberOfMessageThreadSubscribers - MAX_NUMBER_OF_AVATARS}` : null; - const label = isPrivateThread ? privateLabel : moreAvatarsLabel ?? ''; + const label = isPrivateThread ? privateLabel : (moreAvatarsLabel ?? ''); return ( { const name = isFieldFullNameValue(record.name) ? record.name.firstName + ' ' + record.name.lastName - : record.name ?? ''; + : (record.name ?? ''); return { name, diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/MultiSelectFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/MultiSelectFieldInput.tsx index d36b2b9fbfc4..c6134f911a2d 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/MultiSelectFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/MultiSelectFieldInput.tsx @@ -42,8 +42,8 @@ export const MultiSelectFieldInput = ({ const [searchFilter, setSearchFilter] = useState(''); const containerRef = useRef(null); - const selectedOptions = fieldDefinition.metadata.options.filter( - (option) => fieldValues?.includes(option.value), + const selectedOptions = fieldDefinition.metadata.options.filter((option) => + fieldValues?.includes(option.value), ); const optionsInDropDown = fieldDefinition.metadata.options; diff --git a/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailRelationSection.tsx b/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailRelationSection.tsx index e59ca3be75fe..426078f807b4 100644 --- a/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailRelationSection.tsx +++ b/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailRelationSection.tsx @@ -69,7 +69,7 @@ export const RecordDetailRelationSection = ({ const relationRecords: ObjectRecord[] = fieldValue && isToOneObject ? [fieldValue as ObjectRecord] - : (fieldValue as ObjectRecord[]) ?? []; + : ((fieldValue as ObjectRecord[]) ?? []); const relationRecordIds = relationRecords.map(({ id }) => id); diff --git a/packages/twenty-front/src/modules/spreadsheet-import/utils/findUnmatchedRequiredFields.ts b/packages/twenty-front/src/modules/spreadsheet-import/utils/findUnmatchedRequiredFields.ts index e7219f70bddf..71af1840263d 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/utils/findUnmatchedRequiredFields.ts +++ b/packages/twenty-front/src/modules/spreadsheet-import/utils/findUnmatchedRequiredFields.ts @@ -6,11 +6,10 @@ export const findUnmatchedRequiredFields = ( columns: Columns, ) => fields - .filter( - (field) => - field.fieldValidationDefinitions?.some( - (validation) => validation.rule === 'required', - ), + .filter((field) => + field.fieldValidationDefinitions?.some( + (validation) => validation.rule === 'required', + ), ) .filter( (field) => diff --git a/packages/twenty-front/src/modules/ui/input/components/Toggle.tsx b/packages/twenty-front/src/modules/ui/input/components/Toggle.tsx index 9aadab0eb028..f723e93e1c5a 100644 --- a/packages/twenty-front/src/modules/ui/input/components/Toggle.tsx +++ b/packages/twenty-front/src/modules/ui/input/components/Toggle.tsx @@ -16,7 +16,7 @@ type ContainerProps = { const StyledContainer = styled.div` align-items: center; background-color: ${({ theme, isOn, color }) => - isOn ? color ?? theme.color.blue : theme.background.quaternary}; + isOn ? (color ?? theme.color.blue) : theme.background.quaternary}; border-radius: 10px; cursor: pointer; display: flex; diff --git a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawer.tsx b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawer.tsx index 406402377646..92b020a7dc7b 100644 --- a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawer.tsx +++ b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawer.tsx @@ -32,7 +32,7 @@ const StyledContainer = styled.div<{ isSubMenu?: boolean }>` box-sizing: border-box; display: flex; flex-direction: column; - gap: ${({ theme }) => theme.spacing(3)}; + gap: ${({ theme }) => theme.spacing(3.5)}; height: 100%; min-width: ${DESKTOP_NAV_DRAWER_WIDTHS.menu}px; padding: ${({ theme }) => theme.spacing(3, 2, 4)}; diff --git a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerHeader.tsx b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerHeader.tsx index d6bbf810089b..03ae1fcd0ada 100644 --- a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerHeader.tsx +++ b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerHeader.tsx @@ -15,8 +15,6 @@ const StyledContainer = styled.div<{ isMultiWorkspace: boolean }>` display: flex; gap: ${({ theme, isMultiWorkspace }) => !isMultiWorkspace ? theme.spacing(2) : null}; - padding: ${({ theme, isMultiWorkspace }) => - !isMultiWorkspace ? theme.spacing(1) : null}; height: ${({ theme }) => theme.spacing(8)}; user-select: none; `; From 20d84755bb37366c449e3890e48cbafa17bc7c56 Mon Sep 17 00:00:00 2001 From: Nabhag Motivaras <65061890+Nabhag8848@users.noreply.github.com> Date: Fri, 16 Aug 2024 19:48:45 +0530 Subject: [PATCH 3/3] fix: exclude tabler/icons-react from optimizeDeps to avoid crashing performance CI (#6621) ## ISSUE (Warning) - Fixes #6437 ### Things needed to investigate : - [x] Can we exclude @tabler-icons from Babel / wyw-in-js / the vite config of Storybook ? > Yes we can - [ ] Is there a reproducible difference between non-Linaria components loading time and Linaria components loading times ? > Couldn't find anything related to this, yet. but changes fix the problem. --- packages/twenty-front/vite.config.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/twenty-front/vite.config.ts b/packages/twenty-front/vite.config.ts index 924f9c9d7ee0..a711d05916f1 100644 --- a/packages/twenty-front/vite.config.ts +++ b/packages/twenty-front/vite.config.ts @@ -104,5 +104,8 @@ export default defineConfig(({ command, mode }) => { localsConvention: 'camelCaseOnly', }, }, + optimizeDeps: { + exclude: ['@tabler/icons-react'], + }, }; });