Fetch messages with hard coded thread id (#3613)
* Fetch messages with hard coded thread id * Fix test * Use first workspace member or person names --------- Co-authored-by: Thomas Trompette <thomast@twenty.com>
This commit is contained in:
@ -1,10 +1,10 @@
|
|||||||
import React, { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
import { EmailThreadMessageBody } from '@/activities/emails/components/EmailThreadMessageBody';
|
import { EmailThreadMessageBody } from '@/activities/emails/components/EmailThreadMessageBody';
|
||||||
import { EmailThreadMessageBodyPreview } from '@/activities/emails/components/EmailThreadMessageBodyPreview';
|
import { EmailThreadMessageBodyPreview } from '@/activities/emails/components/EmailThreadMessageBodyPreview';
|
||||||
import { EmailThreadMessageSender } from '@/activities/emails/components/EmailThreadMessageSender';
|
import { EmailThreadMessageSender } from '@/activities/emails/components/EmailThreadMessageSender';
|
||||||
import { MockedEmailUser } from '@/activities/emails/mocks/mockedEmailThreads';
|
import { EmailThreadMessageParticipant } from '@/activities/emails/types/EmailThreadMessageParticipant';
|
||||||
|
|
||||||
const StyledThreadMessage = styled.div`
|
const StyledThreadMessage = styled.div`
|
||||||
border-bottom: 1px solid ${({ theme }) => theme.border.color.light};
|
border-bottom: 1px solid ${({ theme }) => theme.border.color.light};
|
||||||
@ -22,21 +22,52 @@ const StyledThreadMessageHeader = styled.div`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
type EmailThreadMessageProps = {
|
type EmailThreadMessageProps = {
|
||||||
id: string;
|
|
||||||
body: string;
|
body: string;
|
||||||
sentAt: string;
|
sentAt: string;
|
||||||
from: MockedEmailUser;
|
participants: EmailThreadMessageParticipant[];
|
||||||
to: MockedEmailUser[];
|
};
|
||||||
|
|
||||||
|
const getDisplayNameFromParticipant = (
|
||||||
|
participant: EmailThreadMessageParticipant,
|
||||||
|
) => {
|
||||||
|
if (participant.person) {
|
||||||
|
return `${participant.person?.name?.firstName} ${participant.person?.name?.lastName}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (participant.workspaceMember) {
|
||||||
|
return `${participant.workspaceMember?.name?.firstName} ${participant.workspaceMember?.name?.lastName}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (participant.displayName) {
|
||||||
|
return participant.displayName;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (participant.handle) {
|
||||||
|
return participant.handle;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'Unknown';
|
||||||
};
|
};
|
||||||
|
|
||||||
export const EmailThreadMessage = ({
|
export const EmailThreadMessage = ({
|
||||||
body,
|
body,
|
||||||
sentAt,
|
sentAt,
|
||||||
from,
|
participants,
|
||||||
}: EmailThreadMessageProps) => {
|
}: EmailThreadMessageProps) => {
|
||||||
const { displayName, avatarUrl } = from;
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
|
const from = participants.find((participant) => participant.role === 'from');
|
||||||
|
const to = participants.filter((participant) => participant.role === 'to');
|
||||||
|
|
||||||
|
if (!from || to.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const displayName = getDisplayNameFromParticipant(from);
|
||||||
|
|
||||||
|
const avatarUrl =
|
||||||
|
from.person?.avatarUrl ?? from.workspaceMember?.avatarUrl ?? '';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledThreadMessage onClick={() => setIsOpen(!isOpen)}>
|
<StyledThreadMessage onClick={() => setIsOpen(!isOpen)}>
|
||||||
<StyledThreadMessageHeader>
|
<StyledThreadMessageHeader>
|
||||||
|
|||||||
@ -1,31 +1,13 @@
|
|||||||
import { DateTime } from 'luxon';
|
|
||||||
|
|
||||||
import { Scalars, TimelineThread } from '~/generated/graphql';
|
import { Scalars, TimelineThread } from '~/generated/graphql';
|
||||||
|
|
||||||
export type MockedThread = {
|
export type MockedThread = {
|
||||||
id: string;
|
id: string;
|
||||||
} & TimelineThread;
|
} & TimelineThread;
|
||||||
|
|
||||||
export type MockedEmailUser = {
|
|
||||||
avatarUrl: string;
|
|
||||||
displayName: string;
|
|
||||||
workspaceMemberId?: string;
|
|
||||||
personId?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type MockedMessage = {
|
|
||||||
id: string;
|
|
||||||
from: MockedEmailUser;
|
|
||||||
to: MockedEmailUser[];
|
|
||||||
subject: string;
|
|
||||||
body: string;
|
|
||||||
sentAt: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const mockedEmailThreads: MockedThread[] = [
|
export const mockedEmailThreads: MockedThread[] = [
|
||||||
{
|
{
|
||||||
__typename: 'TimelineThread',
|
__typename: 'TimelineThread',
|
||||||
id: '1',
|
id: '4e88ec1f-a386-4235-bd82-98f25f6d557e',
|
||||||
body: 'This is a test email' as Scalars['String'],
|
body: 'This is a test email' as Scalars['String'],
|
||||||
numberOfMessagesInThread: 5 as Scalars['Float'],
|
numberOfMessagesInThread: 5 as Scalars['Float'],
|
||||||
read: true as Scalars['Boolean'],
|
read: true as Scalars['Boolean'],
|
||||||
@ -36,7 +18,7 @@ export const mockedEmailThreads: MockedThread[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
__typename: 'TimelineThread',
|
__typename: 'TimelineThread',
|
||||||
id: '2',
|
id: '4e88ec1f-a386-4235-bd82-98f25f6d557e',
|
||||||
body: 'This is a second test email' as Scalars['String'],
|
body: 'This is a second test email' as Scalars['String'],
|
||||||
numberOfMessagesInThread: 5 as Scalars['Float'],
|
numberOfMessagesInThread: 5 as Scalars['Float'],
|
||||||
read: true as Scalars['Boolean'],
|
read: true as Scalars['Boolean'],
|
||||||
@ -46,65 +28,3 @@ export const mockedEmailThreads: MockedThread[] = [
|
|||||||
subject: 'Test email number 2' as Scalars['String'],
|
subject: 'Test email number 2' as Scalars['String'],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export const mockedMessagesByThread: Map<string, MockedMessage[]> = new Map([
|
|
||||||
[
|
|
||||||
'1',
|
|
||||||
Array.from({ length: 5 }).map((_, i) => ({
|
|
||||||
id: `id${i + 1}`,
|
|
||||||
from: {
|
|
||||||
avatarUrl: '',
|
|
||||||
displayName: `User ${i + 1}`,
|
|
||||||
workspaceMemberId: `workspaceMemberId${i + 1}`,
|
|
||||||
personId: `personId${i + 1}`,
|
|
||||||
},
|
|
||||||
to: [
|
|
||||||
{
|
|
||||||
avatarUrl: 'https://favicon.twenty.com/qonto.com',
|
|
||||||
displayName: `User ${i + 2}`,
|
|
||||||
workspaceMemberId: `workspaceMemberId${i + 1}`,
|
|
||||||
personId: `personId${i + 2}`,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
subject: `Subject ${i + 1}`,
|
|
||||||
body: `Body ${
|
|
||||||
i + 1
|
|
||||||
}. I am testing a very long body. I am adding more text.
|
|
||||||
I also want to test a new line. To see if it works.
|
|
||||||
|
|
||||||
I am adding a new paragraph.
|
|
||||||
|
|
||||||
Thomas`,
|
|
||||||
sentAt: DateTime.fromFormat('2021-03-12', 'yyyy-MM-dd').toISO() ?? '',
|
|
||||||
})),
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'2',
|
|
||||||
Array.from({ length: 5 }).map((_, i) => ({
|
|
||||||
id: `id${i + 10}`,
|
|
||||||
from: {
|
|
||||||
avatarUrl: '',
|
|
||||||
displayName: `Other user ${i + 1}`,
|
|
||||||
workspaceMemberId: `workspaceMemberId${i + 1}`,
|
|
||||||
personId: `personId${i + 1}`,
|
|
||||||
},
|
|
||||||
to: [
|
|
||||||
{
|
|
||||||
avatarUrl: 'https://favicon.twenty.com/qonto.com',
|
|
||||||
displayName: `Other user ${i + 2}`,
|
|
||||||
workspaceMemberId: `workspaceMemberId${i + 1}`,
|
|
||||||
personId: `personId${i + 2}`,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
subject: `Subject ${i + 1}`,
|
|
||||||
body: `Body ${
|
|
||||||
i + 1
|
|
||||||
}. Hello, I am testing a very long body. I am adding more text.
|
|
||||||
|
|
||||||
I am adding a new paragraph.
|
|
||||||
|
|
||||||
Thomas`,
|
|
||||||
sentAt: DateTime.fromFormat('2021-03-12', 'yyyy-MM-dd').toISO() ?? '',
|
|
||||||
})),
|
|
||||||
],
|
|
||||||
]);
|
|
||||||
|
|||||||
@ -4,8 +4,10 @@ import { useRecoilValue } from 'recoil';
|
|||||||
|
|
||||||
import { EmailThreadHeader } from '@/activities/emails/components/EmailThreadHeader';
|
import { EmailThreadHeader } from '@/activities/emails/components/EmailThreadHeader';
|
||||||
import { EmailThreadMessage } from '@/activities/emails/components/EmailThreadMessage';
|
import { EmailThreadMessage } from '@/activities/emails/components/EmailThreadMessage';
|
||||||
import { mockedMessagesByThread } from '@/activities/emails/mocks/mockedEmailThreads';
|
|
||||||
import { viewableEmailThreadState } from '@/activities/emails/state/viewableEmailThreadState';
|
import { viewableEmailThreadState } from '@/activities/emails/state/viewableEmailThreadState';
|
||||||
|
import { EmailThreadMessage as EmailThreadMessageType } from '@/activities/emails/types/EmailThreadMessage';
|
||||||
|
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||||
|
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
||||||
|
|
||||||
const StyledContainer = styled.div`
|
const StyledContainer = styled.div`
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
@ -20,27 +22,37 @@ const StyledContainer = styled.div`
|
|||||||
export const RightDrawerEmailThread = () => {
|
export const RightDrawerEmailThread = () => {
|
||||||
const viewableEmailThread = useRecoilValue(viewableEmailThreadState);
|
const viewableEmailThread = useRecoilValue(viewableEmailThreadState);
|
||||||
|
|
||||||
|
const { records: messages } = useFindManyRecords<EmailThreadMessageType>({
|
||||||
|
depth: 3,
|
||||||
|
filter: {
|
||||||
|
messageThreadId: {
|
||||||
|
eq: viewableEmailThread?.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
objectNameSingular: CoreObjectNameSingular.Message,
|
||||||
|
orderBy: {
|
||||||
|
receivedAt: 'DescNullsLast',
|
||||||
|
},
|
||||||
|
skip: !viewableEmailThread,
|
||||||
|
useRecordsWithoutConnection: true,
|
||||||
|
});
|
||||||
|
|
||||||
if (!viewableEmailThread) {
|
if (!viewableEmailThread) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const mockedMessages =
|
|
||||||
mockedMessagesByThread.get(viewableEmailThread.id) ?? [];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledContainer>
|
<StyledContainer>
|
||||||
<EmailThreadHeader
|
<EmailThreadHeader
|
||||||
subject={viewableEmailThread.subject}
|
subject={viewableEmailThread.subject}
|
||||||
lastMessageSentAt={viewableEmailThread.receivedAt}
|
lastMessageSentAt={viewableEmailThread.receivedAt}
|
||||||
/>
|
/>
|
||||||
{mockedMessages.map((message) => (
|
{messages.map((message) => (
|
||||||
<EmailThreadMessage
|
<EmailThreadMessage
|
||||||
key={message.id}
|
key={message.id}
|
||||||
id={message.id}
|
participants={message.messageParticipants}
|
||||||
from={message.from}
|
body={message.text}
|
||||||
to={message.to}
|
sentAt={message.receivedAt}
|
||||||
body={message.body}
|
|
||||||
sentAt={message.sentAt}
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</StyledContainer>
|
</StyledContainer>
|
||||||
|
|||||||
@ -0,0 +1,8 @@
|
|||||||
|
import { EmailThreadMessageParticipant } from '@/activities/emails/types/EmailThreadMessageParticipant';
|
||||||
|
|
||||||
|
export type EmailThreadMessage = {
|
||||||
|
id: string;
|
||||||
|
text: string;
|
||||||
|
receivedAt: string;
|
||||||
|
messageParticipants: EmailThreadMessageParticipant[];
|
||||||
|
};
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
import { Person } from '@/people/types/Person';
|
||||||
|
import { WorkspaceMember } from '@/workspace-member/types/WorkspaceMember';
|
||||||
|
|
||||||
|
export type EmailThreadMessageParticipant = {
|
||||||
|
displayName: string;
|
||||||
|
handle: string;
|
||||||
|
role: string;
|
||||||
|
person: Person;
|
||||||
|
workspaceMember: WorkspaceMember;
|
||||||
|
};
|
||||||
@ -27,12 +27,32 @@ const getOneToManyRelation = () => {
|
|||||||
label
|
label
|
||||||
url
|
url
|
||||||
}
|
}
|
||||||
|
accountOwner
|
||||||
|
{
|
||||||
|
id
|
||||||
|
}
|
||||||
linkedinLink
|
linkedinLink
|
||||||
{
|
{
|
||||||
label
|
label
|
||||||
url
|
url
|
||||||
}
|
}
|
||||||
|
attachments
|
||||||
|
{
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
domainName
|
domainName
|
||||||
|
opportunities
|
||||||
|
{
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
annualRecurringRevenue
|
annualRecurringRevenue
|
||||||
{
|
{
|
||||||
amountMicros
|
amountMicros
|
||||||
@ -41,6 +61,30 @@ domainName
|
|||||||
createdAt
|
createdAt
|
||||||
address
|
address
|
||||||
updatedAt
|
updatedAt
|
||||||
|
activityTargets
|
||||||
|
{
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
favorites
|
||||||
|
{
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
people
|
||||||
|
{
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
name
|
name
|
||||||
accountOwnerId
|
accountOwnerId
|
||||||
employees
|
employees
|
||||||
@ -89,9 +133,17 @@ const getOneToManyFromRelationField = () => {
|
|||||||
personId
|
personId
|
||||||
pointOfContactId
|
pointOfContactId
|
||||||
updatedAt
|
updatedAt
|
||||||
|
company
|
||||||
|
{
|
||||||
|
id
|
||||||
|
}
|
||||||
companyId
|
companyId
|
||||||
pipelineStepId
|
pipelineStepId
|
||||||
probability
|
probability
|
||||||
|
pipelineStep
|
||||||
|
{
|
||||||
|
id
|
||||||
|
}
|
||||||
closeDate
|
closeDate
|
||||||
amount
|
amount
|
||||||
{
|
{
|
||||||
@ -100,6 +152,14 @@ closeDate
|
|||||||
}
|
}
|
||||||
id
|
id
|
||||||
createdAt
|
createdAt
|
||||||
|
pointOfContact
|
||||||
|
{
|
||||||
|
id
|
||||||
|
}
|
||||||
|
person
|
||||||
|
{
|
||||||
|
id
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}`,
|
}`,
|
||||||
|
|||||||
@ -48,7 +48,6 @@ export const useMapFieldMetadataToGraphQLQuery = () => {
|
|||||||
{
|
{
|
||||||
id
|
id
|
||||||
${(relationMetadataItem?.fields ?? [])
|
${(relationMetadataItem?.fields ?? [])
|
||||||
.filter((field) => field.type !== 'RELATION')
|
|
||||||
.map((field) =>
|
.map((field) =>
|
||||||
mapFieldMetadataToGraphQLQuery(field, maxDepthForRelations - 1),
|
mapFieldMetadataToGraphQLQuery(field, maxDepthForRelations - 1),
|
||||||
)
|
)
|
||||||
@ -68,7 +67,6 @@ export const useMapFieldMetadataToGraphQLQuery = () => {
|
|||||||
{
|
{
|
||||||
id
|
id
|
||||||
${(relationMetadataItem?.fields ?? [])
|
${(relationMetadataItem?.fields ?? [])
|
||||||
.filter((field) => field.type !== 'RELATION')
|
|
||||||
.map((field) =>
|
.map((field) =>
|
||||||
mapFieldMetadataToGraphQLQuery(field, maxDepthForRelations - 1),
|
mapFieldMetadataToGraphQLQuery(field, maxDepthForRelations - 1),
|
||||||
)
|
)
|
||||||
@ -90,7 +88,6 @@ export const useMapFieldMetadataToGraphQLQuery = () => {
|
|||||||
node {
|
node {
|
||||||
id
|
id
|
||||||
${(relationMetadataItem?.fields ?? [])
|
${(relationMetadataItem?.fields ?? [])
|
||||||
.filter((field) => field.type !== 'RELATION')
|
|
||||||
.map((field) =>
|
.map((field) =>
|
||||||
mapFieldMetadataToGraphQLQuery(field, maxDepthForRelations - 1),
|
mapFieldMetadataToGraphQLQuery(field, maxDepthForRelations - 1),
|
||||||
)
|
)
|
||||||
|
|||||||
@ -32,11 +32,13 @@ export const useFindManyRecords = <T extends ObjectRecord = ObjectRecord>({
|
|||||||
onCompleted,
|
onCompleted,
|
||||||
skip,
|
skip,
|
||||||
useRecordsWithoutConnection = false,
|
useRecordsWithoutConnection = false,
|
||||||
|
depth,
|
||||||
}: ObjectMetadataItemIdentifier &
|
}: ObjectMetadataItemIdentifier &
|
||||||
ObjectRecordQueryVariables & {
|
ObjectRecordQueryVariables & {
|
||||||
onCompleted?: (data: ObjectRecordConnection<T>) => void;
|
onCompleted?: (data: ObjectRecordConnection<T>) => void;
|
||||||
skip?: boolean;
|
skip?: boolean;
|
||||||
useRecordsWithoutConnection?: boolean;
|
useRecordsWithoutConnection?: boolean;
|
||||||
|
depth?: number;
|
||||||
}) => {
|
}) => {
|
||||||
const findManyQueryStateIdentifier =
|
const findManyQueryStateIdentifier =
|
||||||
objectNameSingular +
|
objectNameSingular +
|
||||||
@ -56,9 +58,12 @@ export const useFindManyRecords = <T extends ObjectRecord = ObjectRecord>({
|
|||||||
isFetchingMoreRecordsFamilyState(findManyQueryStateIdentifier),
|
isFetchingMoreRecordsFamilyState(findManyQueryStateIdentifier),
|
||||||
);
|
);
|
||||||
|
|
||||||
const { objectMetadataItem, findManyRecordsQuery } = useObjectMetadataItem({
|
const { objectMetadataItem, findManyRecordsQuery } = useObjectMetadataItem(
|
||||||
objectNameSingular,
|
{
|
||||||
});
|
objectNameSingular,
|
||||||
|
},
|
||||||
|
depth,
|
||||||
|
);
|
||||||
|
|
||||||
const { enqueueSnackBar } = useSnackBar();
|
const { enqueueSnackBar } = useSnackBar();
|
||||||
const currentWorkspace = useRecoilValue(currentWorkspaceState);
|
const currentWorkspace = useRecoilValue(currentWorkspaceState);
|
||||||
|
|||||||
Reference in New Issue
Block a user