POC timeline activity (#5697)
TODO: - remove WorkspaceIsNotAuditLogged decorators on activity/activityTarget to log task/note creations - handle attachments - fix css and remove unnecessary styled components or duplicates
This commit is contained in:
@ -44,9 +44,11 @@ export const EmailThreadHeader = ({
|
|||||||
<StyledContainer>
|
<StyledContainer>
|
||||||
<StyledHead>
|
<StyledHead>
|
||||||
<StyledHeading>{subject}</StyledHeading>
|
<StyledHeading>{subject}</StyledHeading>
|
||||||
|
{lastMessageSentAt && (
|
||||||
<StyledContent>
|
<StyledContent>
|
||||||
Last message {beautifyPastDateRelativeToNow(lastMessageSentAt)}
|
Last message {beautifyPastDateRelativeToNow(lastMessageSentAt)}
|
||||||
</StyledContent>
|
</StyledContent>
|
||||||
|
)}
|
||||||
</StyledHead>
|
</StyledHead>
|
||||||
</StyledContainer>
|
</StyledContainer>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -43,11 +43,14 @@ export const RightDrawerEmailThread = () => {
|
|||||||
|
|
||||||
useRegisterClickOutsideListenerCallback({
|
useRegisterClickOutsideListenerCallback({
|
||||||
callbackId:
|
callbackId:
|
||||||
'EmailThreadClickOutsideCallBack-' + (thread.id ?? 'no-thread-id'),
|
'EmailThreadClickOutsideCallBack-' + (thread?.id ?? 'no-thread-id'),
|
||||||
callbackFunction: useRecoilCallback(
|
callbackFunction: useRecoilCallback(
|
||||||
({ set }) =>
|
({ set }) =>
|
||||||
() => {
|
() => {
|
||||||
set(emailThreadIdWhenEmailThreadWasClosedState, thread.id);
|
set(
|
||||||
|
emailThreadIdWhenEmailThreadWasClosedState,
|
||||||
|
thread?.id ?? 'no-thread-id',
|
||||||
|
);
|
||||||
},
|
},
|
||||||
[thread],
|
[thread],
|
||||||
),
|
),
|
||||||
@ -71,14 +74,14 @@ export const RightDrawerEmailThread = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledContainer>
|
<StyledContainer>
|
||||||
<EmailThreadHeader
|
|
||||||
subject={thread.subject}
|
|
||||||
lastMessageSentAt={thread.lastMessageReceivedAt}
|
|
||||||
/>
|
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<EmailLoader loadingText="Loading thread" />
|
<EmailLoader loadingText="Loading thread" />
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
|
<EmailThreadHeader
|
||||||
|
subject={thread.subject}
|
||||||
|
lastMessageSentAt={lastMessage.receivedAt}
|
||||||
|
/>
|
||||||
{firstMessages.map((message) => (
|
{firstMessages.map((message) => (
|
||||||
<EmailThreadMessage
|
<EmailThreadMessage
|
||||||
key={message.id}
|
key={message.id}
|
||||||
|
|||||||
@ -3,9 +3,15 @@ import { renderHook } from '@testing-library/react';
|
|||||||
import { RecoilRoot } from 'recoil';
|
import { RecoilRoot } from 'recoil';
|
||||||
|
|
||||||
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
||||||
|
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
|
||||||
|
|
||||||
import { useRightDrawerEmailThread } from '../useRightDrawerEmailThread';
|
import { useRightDrawerEmailThread } from '../useRightDrawerEmailThread';
|
||||||
|
|
||||||
|
jest.mock('@/object-record/hooks/useFindOneRecord', () => ({
|
||||||
|
__esModule: true,
|
||||||
|
useFindOneRecord: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
jest.mock('@/object-record/hooks/useFindManyRecords', () => ({
|
jest.mock('@/object-record/hooks/useFindManyRecords', () => ({
|
||||||
__esModule: true,
|
__esModule: true,
|
||||||
useFindManyRecords: jest.fn(),
|
useFindManyRecords: jest.fn(),
|
||||||
@ -13,11 +19,21 @@ jest.mock('@/object-record/hooks/useFindManyRecords', () => ({
|
|||||||
|
|
||||||
describe('useRightDrawerEmailThread', () => {
|
describe('useRightDrawerEmailThread', () => {
|
||||||
it('should return correct values', async () => {
|
it('should return correct values', async () => {
|
||||||
|
const mockThread = { id: '1' };
|
||||||
|
|
||||||
const mockMessages = [
|
const mockMessages = [
|
||||||
{ id: '1', text: 'Message 1' },
|
{ id: '1', text: 'Message 1' },
|
||||||
{ id: '2', text: 'Message 2' },
|
{ id: '2', text: 'Message 2' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const mockFetchMoreRecords = jest.fn();
|
const mockFetchMoreRecords = jest.fn();
|
||||||
|
|
||||||
|
(useFindOneRecord as jest.Mock).mockReturnValue({
|
||||||
|
record: mockThread,
|
||||||
|
loading: false,
|
||||||
|
fetchMoreRecords: mockFetchMoreRecords,
|
||||||
|
});
|
||||||
|
|
||||||
(useFindManyRecords as jest.Mock).mockReturnValue({
|
(useFindManyRecords as jest.Mock).mockReturnValue({
|
||||||
records: mockMessages,
|
records: mockMessages,
|
||||||
loading: false,
|
loading: false,
|
||||||
|
|||||||
@ -1,26 +1,26 @@
|
|||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { useApolloClient } from '@apollo/client';
|
|
||||||
import gql from 'graphql-tag';
|
|
||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
|
|
||||||
import { fetchAllThreadMessagesOperationSignatureFactory } from '@/activities/emails/graphql/operation-signatures/factories/fetchAllThreadMessagesOperationSignatureFactory';
|
import { fetchAllThreadMessagesOperationSignatureFactory } from '@/activities/emails/graphql/operation-signatures/factories/fetchAllThreadMessagesOperationSignatureFactory';
|
||||||
|
import { EmailThread } from '@/activities/emails/types/EmailThread';
|
||||||
import { EmailThreadMessage as EmailThreadMessageType } from '@/activities/emails/types/EmailThreadMessage';
|
import { EmailThreadMessage as EmailThreadMessageType } from '@/activities/emails/types/EmailThreadMessage';
|
||||||
|
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||||
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
||||||
|
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
|
||||||
import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState';
|
import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState';
|
||||||
|
import { useSetRecordInStore } from '@/object-record/record-store/hooks/useSetRecordInStore';
|
||||||
|
|
||||||
export const useRightDrawerEmailThread = () => {
|
export const useRightDrawerEmailThread = () => {
|
||||||
const viewableRecordId = useRecoilValue(viewableRecordIdState);
|
const viewableRecordId = useRecoilValue(viewableRecordIdState);
|
||||||
|
const { setRecords } = useSetRecordInStore();
|
||||||
|
|
||||||
const apolloClient = useApolloClient();
|
const { record: thread } = useFindOneRecord<EmailThread>({
|
||||||
const thread = apolloClient.readFragment({
|
objectNameSingular: CoreObjectNameSingular.MessageThread,
|
||||||
id: `TimelineThread:${viewableRecordId}`,
|
objectRecordId: viewableRecordId ?? '',
|
||||||
fragment: gql`
|
recordGqlFields: {
|
||||||
fragment timelineThread on TimelineThread {
|
id: true,
|
||||||
id
|
},
|
||||||
subject
|
onCompleted: (record) => setRecords([record]),
|
||||||
lastMessageReceivedAt
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const FETCH_ALL_MESSAGES_OPERATION_SIGNATURE =
|
const FETCH_ALL_MESSAGES_OPERATION_SIGNATURE =
|
||||||
|
|||||||
@ -0,0 +1,8 @@
|
|||||||
|
import { EmailThreadMessage } from '@/activities/emails/types/EmailThreadMessage';
|
||||||
|
|
||||||
|
export type EmailThread = {
|
||||||
|
id: string;
|
||||||
|
subject: string;
|
||||||
|
messages: EmailThreadMessage[];
|
||||||
|
__typename: 'EmailThread';
|
||||||
|
};
|
||||||
@ -4,6 +4,8 @@ export type EmailThreadMessage = {
|
|||||||
id: string;
|
id: string;
|
||||||
text: string;
|
text: string;
|
||||||
receivedAt: string;
|
receivedAt: string;
|
||||||
|
subject: string;
|
||||||
|
messageThreadId: string;
|
||||||
messageParticipants: EmailThreadMessageParticipant[];
|
messageParticipants: EmailThreadMessageParticipant[];
|
||||||
__typename: 'EmailThreadMessage';
|
__typename: 'EmailThreadMessage';
|
||||||
};
|
};
|
||||||
|
|||||||
@ -0,0 +1,80 @@
|
|||||||
|
import { Meta, StoryObj } from '@storybook/react';
|
||||||
|
import { graphql, HttpResponse } from 'msw';
|
||||||
|
import { ComponentDecorator } from 'twenty-ui';
|
||||||
|
|
||||||
|
import { TimelineActivities } from '@/activities/timelineActivities/components/TimelineActivities';
|
||||||
|
import { TimelineActivityContext } from '@/activities/timelineActivities/contexts/TimelineActivityContext';
|
||||||
|
import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator';
|
||||||
|
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
|
||||||
|
import { mockedTimelineActivities } from '~/testing/mock-data/timeline-activities';
|
||||||
|
|
||||||
|
const meta: Meta<typeof TimelineActivities> = {
|
||||||
|
title: 'Modules/TimelineActivities/TimelineActivities',
|
||||||
|
component: TimelineActivities,
|
||||||
|
decorators: [
|
||||||
|
ComponentDecorator,
|
||||||
|
ObjectMetadataItemsDecorator,
|
||||||
|
SnackBarDecorator,
|
||||||
|
(Story) => {
|
||||||
|
return (
|
||||||
|
<TimelineActivityContext.Provider
|
||||||
|
value={{
|
||||||
|
labelIdentifierValue: 'Mock',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Story />
|
||||||
|
</TimelineActivityContext.Provider>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
],
|
||||||
|
args: {
|
||||||
|
targetableObject: {
|
||||||
|
id: '1',
|
||||||
|
targetObjectNameSingular: 'company',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
parameters: {
|
||||||
|
msw: {
|
||||||
|
handlers: [
|
||||||
|
graphql.query('FindManyActivities', () => {
|
||||||
|
return HttpResponse.json({
|
||||||
|
data: {
|
||||||
|
activities: {
|
||||||
|
edges: [],
|
||||||
|
pageInfo: {
|
||||||
|
hasNextPage: false,
|
||||||
|
hasPreviousPage: false,
|
||||||
|
startCursor: null,
|
||||||
|
endCursor: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
graphql.query('FindManyTimelineActivities', () => {
|
||||||
|
return HttpResponse.json({
|
||||||
|
data: {
|
||||||
|
timelineActivities: {
|
||||||
|
edges: mockedTimelineActivities.map((activity) => ({
|
||||||
|
node: activity,
|
||||||
|
cursor: activity.id,
|
||||||
|
})),
|
||||||
|
pageInfo: {
|
||||||
|
hasNextPage: false,
|
||||||
|
hasPreviousPage: false,
|
||||||
|
startCursor: null,
|
||||||
|
endCursor: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj<typeof TimelineActivities>;
|
||||||
|
|
||||||
|
export const Default: Story = {};
|
||||||
@ -25,7 +25,6 @@ const StyledTimelineContainer = styled.div`
|
|||||||
gap: ${({ theme }) => theme.spacing(1)};
|
gap: ${({ theme }) => theme.spacing(1)};
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
|
|
||||||
padding: ${({ theme }) => theme.spacing(4)};
|
|
||||||
width: calc(100% - ${({ theme }) => theme.spacing(8)});
|
width: calc(100% - ${({ theme }) => theme.spacing(8)});
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|||||||
@ -1,77 +1,46 @@
|
|||||||
import { Tooltip } from 'react-tooltip';
|
import { useContext } from 'react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import {
|
import { useRecoilValue } from 'recoil';
|
||||||
IconCheckbox,
|
|
||||||
IconCirclePlus,
|
|
||||||
IconEditCircle,
|
|
||||||
IconFocusCentered,
|
|
||||||
IconNotes,
|
|
||||||
useIcons,
|
|
||||||
} from 'twenty-ui';
|
|
||||||
|
|
||||||
import { useOpenActivityRightDrawer } from '@/activities/hooks/useOpenActivityRightDrawer';
|
|
||||||
import { useLinkedObject } from '@/activities/timeline/hooks/useLinkedObject';
|
import { useLinkedObject } from '@/activities/timeline/hooks/useLinkedObject';
|
||||||
import { EventUpdateProperty } from '@/activities/timelineActivities/components/EventUpdateProperty';
|
import { TimelineActivityContext } from '@/activities/timelineActivities/contexts/TimelineActivityContext';
|
||||||
|
import {
|
||||||
|
EventIconDynamicComponent,
|
||||||
|
EventRowDynamicComponent,
|
||||||
|
} from '@/activities/timelineActivities/rows/components/EventRowDynamicComponent';
|
||||||
import { TimelineActivity } from '@/activities/timelineActivities/types/TimelineActivity';
|
import { TimelineActivity } from '@/activities/timelineActivities/types/TimelineActivity';
|
||||||
|
import {
|
||||||
|
CurrentWorkspaceMember,
|
||||||
|
currentWorkspaceMemberState,
|
||||||
|
} from '@/auth/states/currentWorkspaceMemberState';
|
||||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||||
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
||||||
import {
|
import { beautifyPastDateRelativeToNow } from '~/utils/date-utils';
|
||||||
beautifyExactDateTime,
|
import { isDefined } from '~/utils/isDefined';
|
||||||
beautifyPastDateRelativeToNow,
|
|
||||||
} from '~/utils/date-utils';
|
|
||||||
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
|
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
|
||||||
|
|
||||||
const StyledIconContainer = styled.div`
|
const StyledIconContainer = styled.div`
|
||||||
align-items: center;
|
|
||||||
color: ${({ theme }) => theme.font.color.tertiary};
|
|
||||||
display: flex;
|
display: flex;
|
||||||
user-select: none;
|
align-items: center;
|
||||||
height: 16px;
|
|
||||||
margin: 5px;
|
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
text-decoration-line: underline;
|
color: ${({ theme }) => theme.font.color.tertiary};
|
||||||
|
height: 16px;
|
||||||
width: 16px;
|
width: 16px;
|
||||||
|
margin: 5px;
|
||||||
|
user-select: none;
|
||||||
|
text-decoration-line: underline;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
`;
|
align-self: normal;
|
||||||
|
|
||||||
const StyledActionName = styled.span`
|
|
||||||
overflow: hidden;
|
|
||||||
flex: none;
|
|
||||||
white-space: nowrap;
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledItemContainer = styled.div`
|
const StyledItemContainer = styled.div`
|
||||||
align-content: center;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
color: ${({ theme }) => theme.font.color.tertiary};
|
gap: ${({ theme }) => theme.spacing(1)};
|
||||||
display: flex;
|
|
||||||
flex: 1;
|
flex: 1;
|
||||||
gap: ${({ theme }) => theme.spacing(1)};
|
|
||||||
span {
|
|
||||||
color: ${({ theme }) => theme.font.color.secondary};
|
|
||||||
}
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledItemAuthorText = styled.span`
|
|
||||||
display: flex;
|
|
||||||
color: ${({ theme }) => theme.font.color.primary};
|
|
||||||
gap: ${({ theme }) => theme.spacing(1)};
|
|
||||||
white-space: nowrap;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const StyledItemTitle = styled.span`
|
|
||||||
display: flex;
|
|
||||||
flex-flow: row nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
white-space: nowrap;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const StyledLinkedObject = styled.span`
|
|
||||||
cursor: pointer;
|
|
||||||
text-decoration: underline;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const StyledItemTitleDate = styled.div`
|
const StyledItemTitleDate = styled.div`
|
||||||
align-items: center;
|
align-items: center;
|
||||||
color: ${({ theme }) => theme.font.color.tertiary};
|
color: ${({ theme }) => theme.font.color.tertiary};
|
||||||
@ -98,25 +67,10 @@ const StyledVerticalLine = styled.div`
|
|||||||
width: 2px;
|
width: 2px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledTooltip = styled(Tooltip)`
|
|
||||||
background-color: ${({ theme }) => theme.background.primary};
|
|
||||||
|
|
||||||
box-shadow: 0px 2px 4px 3px
|
|
||||||
${({ theme }) => theme.background.transparent.light};
|
|
||||||
|
|
||||||
box-shadow: 2px 4px 16px 6px
|
|
||||||
${({ theme }) => theme.background.transparent.light};
|
|
||||||
|
|
||||||
color: ${({ theme }) => theme.font.color.primary};
|
|
||||||
|
|
||||||
opacity: 1;
|
|
||||||
padding: ${({ theme }) => theme.spacing(2)};
|
|
||||||
`;
|
|
||||||
|
|
||||||
const StyledTimelineItemContainer = styled.div<{ isGap?: boolean }>`
|
const StyledTimelineItemContainer = styled.div<{ isGap?: boolean }>`
|
||||||
align-items: center;
|
|
||||||
align-self: stretch;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
gap: ${({ theme }) => theme.spacing(4)};
|
gap: ${({ theme }) => theme.spacing(4)};
|
||||||
height: ${({ isGap, theme }) =>
|
height: ${({ isGap, theme }) =>
|
||||||
isGap ? (useIsMobile() ? theme.spacing(6) : theme.spacing(3)) : 'auto'};
|
isGap ? (useIsMobile() ? theme.spacing(6) : theme.spacing(3)) : 'auto'};
|
||||||
@ -127,8 +81,9 @@ const StyledTimelineItemContainer = styled.div<{ isGap?: boolean }>`
|
|||||||
const StyledSummary = styled.summary`
|
const StyledSummary = styled.summary`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
flex-flow: row ${() => (useIsMobile() ? 'wrap' : 'nowrap')};
|
flex-direction: row;
|
||||||
gap: ${({ theme }) => theme.spacing(1)};
|
gap: ${({ theme }) => theme.spacing(1)};
|
||||||
|
align-items: center;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@ -138,135 +93,69 @@ type EventRowProps = {
|
|||||||
event: TimelineActivity;
|
event: TimelineActivity;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getAuthorFullName = (
|
||||||
|
event: TimelineActivity,
|
||||||
|
currentWorkspaceMember: CurrentWorkspaceMember,
|
||||||
|
) => {
|
||||||
|
if (isDefined(event.workspaceMember)) {
|
||||||
|
return currentWorkspaceMember.id === event.workspaceMember.id
|
||||||
|
? 'You'
|
||||||
|
: `${event.workspaceMember?.name.firstName} ${event.workspaceMember?.name.lastName}`;
|
||||||
|
}
|
||||||
|
return 'Twenty';
|
||||||
|
};
|
||||||
|
|
||||||
export const EventRow = ({
|
export const EventRow = ({
|
||||||
isLastEvent,
|
isLastEvent,
|
||||||
event,
|
event,
|
||||||
mainObjectMetadataItem,
|
mainObjectMetadataItem,
|
||||||
}: EventRowProps) => {
|
}: EventRowProps) => {
|
||||||
|
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
|
||||||
|
|
||||||
|
const { labelIdentifierValue } = useContext(TimelineActivityContext);
|
||||||
const beautifiedCreatedAt = beautifyPastDateRelativeToNow(event.createdAt);
|
const beautifiedCreatedAt = beautifyPastDateRelativeToNow(event.createdAt);
|
||||||
const exactCreatedAt = beautifyExactDateTime(event.createdAt);
|
const linkedObjectMetadataItem = useLinkedObject(
|
||||||
|
event.linkedObjectMetadataId,
|
||||||
|
);
|
||||||
|
|
||||||
const properties = JSON.parse(event.properties);
|
if (isUndefinedOrNull(currentWorkspaceMember)) {
|
||||||
const diff: Record<string, { before: any; after: any }> = properties?.diff;
|
return null;
|
||||||
|
|
||||||
const isEventType = (type: 'created' | 'updated') => {
|
|
||||||
if (event.name.includes('.')) {
|
|
||||||
return event.name.split('.')[1] === type;
|
|
||||||
}
|
}
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
const { getIcon } = useIcons();
|
const authorFullName = getAuthorFullName(event, currentWorkspaceMember);
|
||||||
|
|
||||||
const linkedObjectMetadata = useLinkedObject(event.linkedObjectMetadataId);
|
if (isUndefinedOrNull(mainObjectMetadataItem)) {
|
||||||
|
return null;
|
||||||
const linkedObjectLabel = event.name.includes('note')
|
|
||||||
? 'note'
|
|
||||||
: event.name.includes('task')
|
|
||||||
? 'task'
|
|
||||||
: linkedObjectMetadata?.labelSingular;
|
|
||||||
|
|
||||||
const ActivityIcon = event.linkedObjectMetadataId
|
|
||||||
? event.name.includes('note')
|
|
||||||
? IconNotes
|
|
||||||
: event.name.includes('task')
|
|
||||||
? IconCheckbox
|
|
||||||
: getIcon(linkedObjectMetadata?.icon)
|
|
||||||
: isEventType('created')
|
|
||||||
? IconCirclePlus
|
|
||||||
: isEventType('updated')
|
|
||||||
? IconEditCircle
|
|
||||||
: IconFocusCentered;
|
|
||||||
|
|
||||||
const author =
|
|
||||||
event.workspaceMember?.name.firstName +
|
|
||||||
' ' +
|
|
||||||
event.workspaceMember?.name.lastName;
|
|
||||||
|
|
||||||
const action = isEventType('created')
|
|
||||||
? 'created'
|
|
||||||
: isEventType('updated')
|
|
||||||
? 'updated'
|
|
||||||
: event.name;
|
|
||||||
|
|
||||||
let description;
|
|
||||||
|
|
||||||
if (!isUndefinedOrNull(linkedObjectMetadata)) {
|
|
||||||
description = 'a ' + linkedObjectLabel;
|
|
||||||
} else if (!event.linkedObjectMetadataId && isEventType('created')) {
|
|
||||||
description = `a new ${mainObjectMetadataItem?.labelSingular}`;
|
|
||||||
} else if (isEventType('updated')) {
|
|
||||||
const diffKeys = Object.keys(diff);
|
|
||||||
if (diffKeys.length === 0) {
|
|
||||||
description = `a ${mainObjectMetadataItem?.labelSingular}`;
|
|
||||||
} else if (diffKeys.length === 1) {
|
|
||||||
const [key, value] = Object.entries(diff)[0];
|
|
||||||
description = [
|
|
||||||
<EventUpdateProperty
|
|
||||||
propertyName={key}
|
|
||||||
after={value?.after as string}
|
|
||||||
/>,
|
|
||||||
];
|
|
||||||
} else if (diffKeys.length === 2) {
|
|
||||||
description =
|
|
||||||
mainObjectMetadataItem?.fields.find(
|
|
||||||
(field) => diffKeys[0] === field.name,
|
|
||||||
)?.label +
|
|
||||||
' and ' +
|
|
||||||
mainObjectMetadataItem?.fields.find(
|
|
||||||
(field) => diffKeys[1] === field.name,
|
|
||||||
)?.label;
|
|
||||||
} else if (diffKeys.length > 2) {
|
|
||||||
description =
|
|
||||||
diffKeys[0] + ' and ' + (diffKeys.length - 1) + ' other fields';
|
|
||||||
}
|
}
|
||||||
} else if (!isEventType('created') && !isEventType('updated')) {
|
|
||||||
description = JSON.stringify(diff);
|
|
||||||
}
|
|
||||||
const details = JSON.stringify(diff);
|
|
||||||
|
|
||||||
const openActivityRightDrawer = useOpenActivityRightDrawer();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<StyledTimelineItemContainer>
|
<StyledTimelineItemContainer>
|
||||||
<StyledIconContainer>
|
<StyledIconContainer>
|
||||||
<ActivityIcon />
|
<EventIconDynamicComponent
|
||||||
|
event={event}
|
||||||
|
linkedObjectMetadataItem={linkedObjectMetadataItem}
|
||||||
|
/>
|
||||||
</StyledIconContainer>
|
</StyledIconContainer>
|
||||||
<StyledItemContainer>
|
<StyledItemContainer>
|
||||||
<details>
|
|
||||||
<StyledSummary>
|
<StyledSummary>
|
||||||
<StyledItemAuthorText>{author}</StyledItemAuthorText>
|
<EventRowDynamicComponent
|
||||||
<StyledActionName>{action}</StyledActionName>
|
authorFullName={authorFullName}
|
||||||
<StyledItemTitle>{description}</StyledItemTitle>
|
labelIdentifierValue={labelIdentifierValue}
|
||||||
{isUndefinedOrNull(linkedObjectMetadata) ? (
|
event={event}
|
||||||
<></>
|
mainObjectMetadataItem={mainObjectMetadataItem}
|
||||||
) : (
|
linkedObjectMetadataItem={linkedObjectMetadataItem}
|
||||||
<StyledLinkedObject
|
/>
|
||||||
onClick={() => openActivityRightDrawer(event.linkedRecordId)}
|
|
||||||
>
|
|
||||||
{event.linkedRecordCachedName}
|
|
||||||
</StyledLinkedObject>
|
|
||||||
)}
|
|
||||||
</StyledSummary>
|
</StyledSummary>
|
||||||
{details}
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<StyledItemTitleDate id={`id-${event.id}`}>
|
<StyledItemTitleDate id={`id-${event.id}`}>
|
||||||
{beautifiedCreatedAt}
|
{beautifiedCreatedAt}
|
||||||
</StyledItemTitleDate>
|
</StyledItemTitleDate>
|
||||||
<StyledTooltip
|
|
||||||
anchorSelect={`#id-${event.id}`}
|
|
||||||
content={exactCreatedAt}
|
|
||||||
clickable
|
|
||||||
noArrow
|
|
||||||
/>
|
|
||||||
</StyledItemContainer>
|
</StyledItemContainer>
|
||||||
</StyledTimelineItemContainer>
|
</StyledTimelineItemContainer>
|
||||||
{!isLastEvent && (
|
{!isLastEvent && (
|
||||||
<StyledTimelineItemContainer isGap>
|
<StyledTimelineItemContainer isGap>
|
||||||
<StyledVerticalLineContainer>
|
<StyledVerticalLineContainer>
|
||||||
<StyledVerticalLine></StyledVerticalLine>
|
<StyledVerticalLine />
|
||||||
</StyledVerticalLineContainer>
|
</StyledVerticalLineContainer>
|
||||||
</StyledTimelineItemContainer>
|
</StyledTimelineItemContainer>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -45,6 +45,8 @@ const StyledMonthSeperator = styled.div`
|
|||||||
color: ${({ theme }) => theme.font.color.light};
|
color: ${({ theme }) => theme.font.color.light};
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: ${({ theme }) => theme.spacing(4)};
|
gap: ${({ theme }) => theme.spacing(4)};
|
||||||
|
font-weight: ${({ theme }) => theme.font.weight.semiBold};
|
||||||
|
font-size: ${({ theme }) => theme.font.size.xs};
|
||||||
`;
|
`;
|
||||||
const StyledMonthSeperatorLine = styled.div`
|
const StyledMonthSeperatorLine = styled.div`
|
||||||
background: ${({ theme }) => theme.border.color.light};
|
background: ${({ theme }) => theme.border.color.light};
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { isNonEmptyArray } from '@sniptt/guards';
|
import { isNonEmptyArray } from '@sniptt/guards';
|
||||||
|
|
||||||
|
import { FetchMoreLoader } from '@/activities/components/CustomResolverFetchMoreLoader';
|
||||||
import { TimelineCreateButtonGroup } from '@/activities/timeline/components/TimelineCreateButtonGroup';
|
import { TimelineCreateButtonGroup } from '@/activities/timeline/components/TimelineCreateButtonGroup';
|
||||||
import { EventList } from '@/activities/timelineActivities/components/EventList';
|
import { EventList } from '@/activities/timelineActivities/components/EventList';
|
||||||
import { useTimelineActivities } from '@/activities/timelineActivities/hooks/useTimelineActivities';
|
import { useTimelineActivities } from '@/activities/timelineActivities/hooks/useTimelineActivities';
|
||||||
@ -24,6 +25,11 @@ const StyledMainContainer = styled.div`
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
padding-top: ${({ theme }) => theme.spacing(6)};
|
||||||
|
padding-right: ${({ theme }) => theme.spacing(6)};
|
||||||
|
padding-bottom: ${({ theme }) => theme.spacing(16)};
|
||||||
|
padding-left: ${({ theme }) => theme.spacing(6)};
|
||||||
|
gap: ${({ theme }) => theme.spacing(4)};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const TimelineActivities = ({
|
export const TimelineActivities = ({
|
||||||
@ -31,7 +37,8 @@ export const TimelineActivities = ({
|
|||||||
}: {
|
}: {
|
||||||
targetableObject: ActivityTargetableObject;
|
targetableObject: ActivityTargetableObject;
|
||||||
}) => {
|
}) => {
|
||||||
const { timelineActivities } = useTimelineActivities(targetableObject);
|
const { timelineActivities, loading, fetchMoreRecords } =
|
||||||
|
useTimelineActivities(targetableObject);
|
||||||
|
|
||||||
if (!isNonEmptyArray(timelineActivities)) {
|
if (!isNonEmptyArray(timelineActivities)) {
|
||||||
return (
|
return (
|
||||||
@ -57,6 +64,7 @@ export const TimelineActivities = ({
|
|||||||
title="All"
|
title="All"
|
||||||
events={timelineActivities ?? []}
|
events={timelineActivities ?? []}
|
||||||
/>
|
/>
|
||||||
|
<FetchMoreLoader loading={loading} onLastRowVisible={fetchMoreRecords} />
|
||||||
</StyledMainContainer>
|
</StyledMainContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -0,0 +1,19 @@
|
|||||||
|
import { useActivities } from '@/activities/hooks/useActivities';
|
||||||
|
import { FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY } from '@/activities/timeline/constants/FindManyTimelineActivitiesOrderBy';
|
||||||
|
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
||||||
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
|
||||||
|
export const TimelineActivitiesQueryEffect = ({
|
||||||
|
targetableObject,
|
||||||
|
}: {
|
||||||
|
targetableObject: ActivityTargetableObject;
|
||||||
|
}) => {
|
||||||
|
useActivities({
|
||||||
|
targetableObjects: [targetableObject],
|
||||||
|
activitiesFilters: {},
|
||||||
|
activitiesOrderByVariables: FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY,
|
||||||
|
skip: !isDefined(targetableObject),
|
||||||
|
});
|
||||||
|
|
||||||
|
return <></>;
|
||||||
|
};
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
import { createContext } from 'react';
|
||||||
|
|
||||||
|
type TimelineActivityContextValue = {
|
||||||
|
labelIdentifierValue: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TimelineActivityContext =
|
||||||
|
createContext<TimelineActivityContextValue>({
|
||||||
|
labelIdentifierValue: '',
|
||||||
|
});
|
||||||
@ -12,7 +12,11 @@ export const useTimelineActivities = (
|
|||||||
nameSingular: targetableObject.targetObjectNameSingular,
|
nameSingular: targetableObject.targetObjectNameSingular,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { records: TimelineActivities } = useFindManyRecords({
|
const {
|
||||||
|
records: TimelineActivities,
|
||||||
|
loading,
|
||||||
|
fetchMoreRecords,
|
||||||
|
} = useFindManyRecords({
|
||||||
objectNameSingular: CoreObjectNameSingular.TimelineActivity,
|
objectNameSingular: CoreObjectNameSingular.TimelineActivity,
|
||||||
filter: {
|
filter: {
|
||||||
[targetableObjectFieldIdName]: {
|
[targetableObjectFieldIdName]: {
|
||||||
@ -22,10 +26,23 @@ export const useTimelineActivities = (
|
|||||||
orderBy: {
|
orderBy: {
|
||||||
createdAt: 'DescNullsFirst',
|
createdAt: 'DescNullsFirst',
|
||||||
},
|
},
|
||||||
fetchPolicy: 'network-only',
|
recordGqlFields: {
|
||||||
|
id: true,
|
||||||
|
createdAt: true,
|
||||||
|
linkedObjectMetadataId: true,
|
||||||
|
linkedRecordCachedName: true,
|
||||||
|
linkedRecordId: true,
|
||||||
|
name: true,
|
||||||
|
properties: true,
|
||||||
|
happensAt: true,
|
||||||
|
workspaceMember: true,
|
||||||
|
person: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
timelineActivities: TimelineActivities as TimelineActivity[],
|
timelineActivities: TimelineActivities as TimelineActivity[],
|
||||||
|
loading,
|
||||||
|
fetchMoreRecords,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@ -0,0 +1,40 @@
|
|||||||
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
|
import { useOpenActivityRightDrawer } from '@/activities/hooks/useOpenActivityRightDrawer';
|
||||||
|
import {
|
||||||
|
EventRowDynamicComponentProps,
|
||||||
|
StyledItemAction,
|
||||||
|
StyledItemAuthorText,
|
||||||
|
} from '@/activities/timelineActivities/rows/components/EventRowDynamicComponent';
|
||||||
|
|
||||||
|
type EventRowActivityProps = EventRowDynamicComponentProps;
|
||||||
|
|
||||||
|
const StyledLinkedActivity = styled.span`
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: underline;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const EventRowActivity: React.FC<EventRowActivityProps> = ({
|
||||||
|
event,
|
||||||
|
authorFullName,
|
||||||
|
}: EventRowActivityProps) => {
|
||||||
|
const [, eventAction] = event.name.split('.');
|
||||||
|
|
||||||
|
const openActivityRightDrawer = useOpenActivityRightDrawer();
|
||||||
|
|
||||||
|
if (!event.linkedRecordId) {
|
||||||
|
throw new Error('Could not find linked record id for event');
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<StyledItemAuthorText>{authorFullName}</StyledItemAuthorText>
|
||||||
|
<StyledItemAction>{eventAction}</StyledItemAction>
|
||||||
|
<StyledLinkedActivity
|
||||||
|
onClick={() => openActivityRightDrawer(event.linkedRecordId)}
|
||||||
|
>
|
||||||
|
{event.linkedRecordCachedName}
|
||||||
|
</StyledLinkedActivity>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,169 @@
|
|||||||
|
import styled from '@emotion/styled';
|
||||||
|
import { isUndefined } from '@sniptt/guards';
|
||||||
|
|
||||||
|
import { useOpenCalendarEventRightDrawer } from '@/activities/calendar/right-drawer/hooks/useOpenCalendarEventRightDrawer';
|
||||||
|
import { CalendarEvent } from '@/activities/calendar/types/CalendarEvent';
|
||||||
|
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||||
|
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
|
||||||
|
import { useSetRecordInStore } from '@/object-record/record-store/hooks/useSetRecordInStore';
|
||||||
|
import {
|
||||||
|
formatToHumanReadableDay,
|
||||||
|
formatToHumanReadableMonth,
|
||||||
|
formatToHumanReadableTime,
|
||||||
|
} from '~/utils';
|
||||||
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
|
||||||
|
|
||||||
|
const StyledEventCardCalendarEventContainer = styled.div`
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: ${({ theme }) => theme.spacing(2)};
|
||||||
|
width: 100%;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledCalendarEventContent = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: ${({ theme }) => theme.spacing(2)};
|
||||||
|
justify-content: center;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledCalendarEventTop = styled.div`
|
||||||
|
align-items: center;
|
||||||
|
align-self: stretch;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledCalendarEventTitle = styled.div`
|
||||||
|
color: ${({ theme }) => theme.font.color.primary};
|
||||||
|
font-weight: ${({ theme }) => theme.font.weight.medium};
|
||||||
|
display: flex;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledCalendarEventBody = styled.div`
|
||||||
|
align-items: flex-start;
|
||||||
|
color: ${({ theme }) => theme.font.color.tertiary};
|
||||||
|
display: flex;
|
||||||
|
flex: 1 0 0;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: ${({ theme }) => theme.spacing(2)};
|
||||||
|
|
||||||
|
justify-content: center;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledCalendarEventDateCard = styled.div`
|
||||||
|
display: flex;
|
||||||
|
padding: ${({ theme }) => theme.spacing(1)};
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: ${({ theme }) => theme.spacing(1)};
|
||||||
|
|
||||||
|
border-radius: ${({ theme }) => theme.spacing(1)};
|
||||||
|
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledCalendarEventDateCardMonth = styled.div`
|
||||||
|
color: ${({ theme }) => theme.font.color.danger};
|
||||||
|
font-size: ${({ theme }) => theme.font.size.xxs};
|
||||||
|
font-weight: ${({ theme }) => theme.font.weight.semiBold};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledCalendarEventDateCardDay = styled.div`
|
||||||
|
color: ${({ theme }) => theme.font.color.primary};
|
||||||
|
font-size: ${({ theme }) => theme.font.size.md};
|
||||||
|
font-weight: ${({ theme }) => theme.font.weight.medium};
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const EventCardCalendarEvent = ({
|
||||||
|
calendarEventId,
|
||||||
|
}: {
|
||||||
|
calendarEventId: string;
|
||||||
|
}) => {
|
||||||
|
const { setRecords } = useSetRecordInStore();
|
||||||
|
|
||||||
|
const {
|
||||||
|
record: calendarEvent,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
} = useFindOneRecord<CalendarEvent>({
|
||||||
|
objectNameSingular: CoreObjectNameSingular.CalendarEvent,
|
||||||
|
objectRecordId: calendarEventId,
|
||||||
|
recordGqlFields: {
|
||||||
|
id: true,
|
||||||
|
title: true,
|
||||||
|
startsAt: true,
|
||||||
|
endsAt: true,
|
||||||
|
},
|
||||||
|
onCompleted: (data) => {
|
||||||
|
setRecords([data]);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { openCalendarEventRightDrawer } = useOpenCalendarEventRightDrawer();
|
||||||
|
|
||||||
|
if (isDefined(error)) {
|
||||||
|
const shouldHideMessageContent = error.graphQLErrors.some(
|
||||||
|
(e) => e.extensions?.code === 'FORBIDDEN',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (shouldHideMessageContent) {
|
||||||
|
return <div>Calendar event not shared</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const shouldHandleNotFound = error.graphQLErrors.some(
|
||||||
|
(e) => e.extensions?.code === 'NOT_FOUND',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (shouldHandleNotFound) {
|
||||||
|
return <div>Calendar event not found</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div>Error loading message</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loading || isUndefined(calendarEvent)) {
|
||||||
|
return <div>Loading...</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const startsAtDate = calendarEvent?.startsAt;
|
||||||
|
const endsAtDate = calendarEvent?.endsAt;
|
||||||
|
|
||||||
|
if (isUndefinedOrNull(startsAtDate)) {
|
||||||
|
throw new Error("Can't render a calendarEvent without a start date");
|
||||||
|
}
|
||||||
|
|
||||||
|
const startsAtMonth = formatToHumanReadableMonth(startsAtDate);
|
||||||
|
|
||||||
|
const startsAtDay = formatToHumanReadableDay(startsAtDate);
|
||||||
|
|
||||||
|
const startsAtHour = formatToHumanReadableTime(startsAtDate);
|
||||||
|
const endsAtHour = endsAtDate ? formatToHumanReadableTime(endsAtDate) : null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledEventCardCalendarEventContainer
|
||||||
|
onClick={() => openCalendarEventRightDrawer(calendarEvent.id)}
|
||||||
|
>
|
||||||
|
<StyledCalendarEventDateCard>
|
||||||
|
<StyledCalendarEventDateCardMonth>
|
||||||
|
{startsAtMonth}
|
||||||
|
</StyledCalendarEventDateCardMonth>
|
||||||
|
<StyledCalendarEventDateCardDay>
|
||||||
|
{startsAtDay}
|
||||||
|
</StyledCalendarEventDateCardDay>
|
||||||
|
</StyledCalendarEventDateCard>
|
||||||
|
<StyledCalendarEventContent>
|
||||||
|
<StyledCalendarEventTop>
|
||||||
|
<StyledCalendarEventTitle>
|
||||||
|
{calendarEvent.title}
|
||||||
|
</StyledCalendarEventTitle>
|
||||||
|
</StyledCalendarEventTop>
|
||||||
|
<StyledCalendarEventBody>
|
||||||
|
{startsAtHour} {endsAtHour && <>→ {endsAtHour}</>}
|
||||||
|
</StyledCalendarEventBody>
|
||||||
|
</StyledCalendarEventContent>
|
||||||
|
</StyledEventCardCalendarEventContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,62 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
|
import { EventCardCalendarEvent } from '@/activities/timelineActivities/rows/calendar/components/EventCardCalendarEvent';
|
||||||
|
import {
|
||||||
|
EventCard,
|
||||||
|
EventCardToggleButton,
|
||||||
|
} from '@/activities/timelineActivities/rows/components/EventCard';
|
||||||
|
import {
|
||||||
|
EventRowDynamicComponentProps,
|
||||||
|
StyledItemAction,
|
||||||
|
StyledItemAuthorText,
|
||||||
|
} from '@/activities/timelineActivities/rows/components/EventRowDynamicComponent';
|
||||||
|
|
||||||
|
type EventRowCalendarEventProps = EventRowDynamicComponentProps;
|
||||||
|
|
||||||
|
const StyledEventRowCalendarEventContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: ${({ theme }) => theme.spacing(1)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledRowContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: ${({ theme }) => theme.spacing(1)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const EventRowCalendarEvent: React.FC<EventRowCalendarEventProps> = ({
|
||||||
|
event,
|
||||||
|
authorFullName,
|
||||||
|
labelIdentifierValue,
|
||||||
|
}: EventRowCalendarEventProps) => {
|
||||||
|
const [, eventAction] = event.name.split('.');
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
|
const renderRow = () => {
|
||||||
|
switch (eventAction) {
|
||||||
|
case 'linked': {
|
||||||
|
return (
|
||||||
|
<StyledItemAction>
|
||||||
|
linked a calendar event with {labelIdentifierValue}
|
||||||
|
</StyledItemAction>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
throw new Error('Invalid event action for calendarEvent event type.');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<StyledEventRowCalendarEventContainer>
|
||||||
|
<StyledRowContainer>
|
||||||
|
<StyledItemAuthorText>{authorFullName}</StyledItemAuthorText>
|
||||||
|
{renderRow()}
|
||||||
|
<EventCardToggleButton isOpen={isOpen} setIsOpen={setIsOpen} />
|
||||||
|
</StyledRowContainer>
|
||||||
|
<EventCard isOpen={isOpen}>
|
||||||
|
<EventCardCalendarEvent calendarEventId={event.linkedRecordId} />
|
||||||
|
</EventCard>
|
||||||
|
</StyledEventRowCalendarEventContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,70 @@
|
|||||||
|
import styled from '@emotion/styled';
|
||||||
|
import { IconChevronDown, IconChevronUp } from 'twenty-ui';
|
||||||
|
|
||||||
|
import { IconButton } from '@/ui/input/button/components/IconButton';
|
||||||
|
import { Card } from '@/ui/layout/card/components/Card';
|
||||||
|
|
||||||
|
type EventCardProps = {
|
||||||
|
children: React.ReactNode;
|
||||||
|
isOpen: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type EventCardToggleButtonProps = {
|
||||||
|
isOpen: boolean;
|
||||||
|
setIsOpen: (isOpen: boolean) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const StyledButtonContainer = styled.div`
|
||||||
|
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledCardContainer = styled.div`
|
||||||
|
align-items: flex-start;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-grow: 1;
|
||||||
|
gap: ${({ theme }) => theme.spacing(2)};
|
||||||
|
width: 400px;
|
||||||
|
padding: ${({ theme }) => theme.spacing(2)} 0px
|
||||||
|
${({ theme }) => theme.spacing(4)} 0px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledCard = styled(Card)`
|
||||||
|
background: ${({ theme }) => theme.background.secondary};
|
||||||
|
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||||
|
border-radius: ${({ theme }) => theme.border.radius.md};
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
padding: ${({ theme }) => theme.spacing(2)};
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: ${({ theme }) => theme.spacing(2)};
|
||||||
|
justify-content: center;
|
||||||
|
align-self: stretch;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const EventCard = ({ children, isOpen }: EventCardProps) => {
|
||||||
|
return (
|
||||||
|
isOpen && (
|
||||||
|
<StyledCardContainer>
|
||||||
|
<StyledCard fullWidth>{children}</StyledCard>
|
||||||
|
</StyledCardContainer>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const EventCardToggleButton = ({
|
||||||
|
isOpen,
|
||||||
|
setIsOpen,
|
||||||
|
}: EventCardToggleButtonProps) => {
|
||||||
|
return (
|
||||||
|
<StyledButtonContainer>
|
||||||
|
<IconButton
|
||||||
|
Icon={isOpen ? IconChevronUp : IconChevronDown}
|
||||||
|
onClick={() => setIsOpen(!isOpen)}
|
||||||
|
size="small"
|
||||||
|
variant="secondary"
|
||||||
|
/>
|
||||||
|
</StyledButtonContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,102 @@
|
|||||||
|
import styled from '@emotion/styled';
|
||||||
|
import { IconCirclePlus, IconEditCircle, useIcons } from 'twenty-ui';
|
||||||
|
|
||||||
|
import { EventRowActivity } from '@/activities/timelineActivities/rows/activity/components/EventRowActivity';
|
||||||
|
import { EventRowCalendarEvent } from '@/activities/timelineActivities/rows/calendar/components/EventRowCalendarEvent';
|
||||||
|
import { EventRowMainObject } from '@/activities/timelineActivities/rows/mainObject/components/EventRowMainObject';
|
||||||
|
import { EventRowMessage } from '@/activities/timelineActivities/rows/message/components/EventRowMessage';
|
||||||
|
import { TimelineActivity } from '@/activities/timelineActivities/types/TimelineActivity';
|
||||||
|
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||||
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
|
||||||
|
export interface EventRowDynamicComponentProps {
|
||||||
|
labelIdentifierValue: string;
|
||||||
|
event: TimelineActivity;
|
||||||
|
mainObjectMetadataItem: ObjectMetadataItem;
|
||||||
|
linkedObjectMetadataItem: ObjectMetadataItem | null;
|
||||||
|
authorFullName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const StyledItemColumn = styled.div`
|
||||||
|
align-items: center;
|
||||||
|
color: ${({ theme }) => theme.font.color.primary};
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: ${({ theme }) => theme.spacing(1)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const StyledItemAuthorText = styled(StyledItemColumn)``;
|
||||||
|
|
||||||
|
export const StyledItemLabelIdentifier = styled(StyledItemColumn)``;
|
||||||
|
|
||||||
|
export const StyledItemAction = styled(StyledItemColumn)`
|
||||||
|
color: ${({ theme }) => theme.font.color.secondary};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const eventRowComponentMap: {
|
||||||
|
[key: string]: React.FC<EventRowDynamicComponentProps>;
|
||||||
|
} = {
|
||||||
|
calendarEvent: EventRowCalendarEvent,
|
||||||
|
message: EventRowMessage,
|
||||||
|
task: EventRowActivity,
|
||||||
|
note: EventRowActivity,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const EventRowDynamicComponent = ({
|
||||||
|
labelIdentifierValue,
|
||||||
|
event,
|
||||||
|
mainObjectMetadataItem,
|
||||||
|
linkedObjectMetadataItem,
|
||||||
|
authorFullName,
|
||||||
|
}: EventRowDynamicComponentProps) => {
|
||||||
|
const [eventName] = event.name.split('.');
|
||||||
|
const EventRowComponent = eventRowComponentMap[eventName];
|
||||||
|
|
||||||
|
if (isDefined(EventRowComponent)) {
|
||||||
|
return (
|
||||||
|
<EventRowComponent
|
||||||
|
labelIdentifierValue={labelIdentifierValue}
|
||||||
|
event={event}
|
||||||
|
mainObjectMetadataItem={mainObjectMetadataItem}
|
||||||
|
linkedObjectMetadataItem={linkedObjectMetadataItem}
|
||||||
|
authorFullName={authorFullName}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (eventName === mainObjectMetadataItem?.nameSingular) {
|
||||||
|
return (
|
||||||
|
<EventRowMainObject
|
||||||
|
labelIdentifierValue={labelIdentifierValue}
|
||||||
|
event={event}
|
||||||
|
mainObjectMetadataItem={mainObjectMetadataItem}
|
||||||
|
linkedObjectMetadataItem={linkedObjectMetadataItem}
|
||||||
|
authorFullName={authorFullName}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Cannot find event component for event name ${eventName}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const EventIconDynamicComponent = ({
|
||||||
|
event,
|
||||||
|
linkedObjectMetadataItem,
|
||||||
|
}: {
|
||||||
|
event: TimelineActivity;
|
||||||
|
linkedObjectMetadataItem: ObjectMetadataItem | null;
|
||||||
|
}) => {
|
||||||
|
const { getIcon } = useIcons();
|
||||||
|
const [, eventAction] = event.name.split('.');
|
||||||
|
|
||||||
|
if (eventAction === 'created') {
|
||||||
|
return <IconCirclePlus />;
|
||||||
|
}
|
||||||
|
if (eventAction === 'updated') {
|
||||||
|
return <IconEditCircle />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const IconComponent = getIcon(linkedObjectMetadataItem?.icon);
|
||||||
|
|
||||||
|
return <IconComponent />;
|
||||||
|
};
|
||||||
@ -0,0 +1,51 @@
|
|||||||
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
|
import { EventFieldDiffLabel } from '@/activities/timelineActivities/rows/mainObject/components/EventFieldDiffLabel';
|
||||||
|
import { EventFieldDiffValue } from '@/activities/timelineActivities/rows/mainObject/components/EventFieldDiffValue';
|
||||||
|
import { EventFieldDiffValueEffect } from '@/activities/timelineActivities/rows/mainObject/components/EventFieldDiffValueEffect';
|
||||||
|
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||||
|
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||||
|
|
||||||
|
type EventFieldDiffProps = {
|
||||||
|
diffRecord: Record<string, any>;
|
||||||
|
mainObjectMetadataItem: ObjectMetadataItem;
|
||||||
|
fieldMetadataItem: FieldMetadataItem | undefined;
|
||||||
|
forgedRecordId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const StyledEventFieldDiffContainer = styled.div`
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: ${({ theme }) => theme.spacing(1)};
|
||||||
|
height: 24px;
|
||||||
|
width: 250px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const EventFieldDiff = ({
|
||||||
|
diffRecord,
|
||||||
|
mainObjectMetadataItem,
|
||||||
|
fieldMetadataItem,
|
||||||
|
forgedRecordId,
|
||||||
|
}: EventFieldDiffProps) => {
|
||||||
|
if (!fieldMetadataItem) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledEventFieldDiffContainer>
|
||||||
|
<EventFieldDiffLabel fieldMetadataItem={fieldMetadataItem} />→
|
||||||
|
<EventFieldDiffValueEffect
|
||||||
|
forgedRecordId={forgedRecordId}
|
||||||
|
mainObjectMetadataItem={mainObjectMetadataItem}
|
||||||
|
fieldMetadataItem={fieldMetadataItem}
|
||||||
|
diffRecord={diffRecord}
|
||||||
|
/>
|
||||||
|
<EventFieldDiffValue
|
||||||
|
forgedRecordId={forgedRecordId}
|
||||||
|
mainObjectMetadataItem={mainObjectMetadataItem}
|
||||||
|
fieldMetadataItem={fieldMetadataItem}
|
||||||
|
/>
|
||||||
|
</StyledEventFieldDiffContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,46 @@
|
|||||||
|
import styled from '@emotion/styled';
|
||||||
|
import { Icon123, useIcons } from 'twenty-ui';
|
||||||
|
|
||||||
|
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||||
|
|
||||||
|
type EventFieldDiffLabelProps = {
|
||||||
|
fieldMetadataItem: FieldMetadataItem;
|
||||||
|
};
|
||||||
|
|
||||||
|
const StyledUpdatedFieldContainer = styled.div`
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
gap: ${({ theme }) => theme.spacing(1)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledUpdatedFieldIconContainer = styled.div`
|
||||||
|
align-items: center;
|
||||||
|
color: ${({ theme }) => theme.font.color.tertiary};
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
height: 14px;
|
||||||
|
width: 14px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledUpdatedFieldLabel = styled.div``;
|
||||||
|
|
||||||
|
export const EventFieldDiffLabel = ({
|
||||||
|
fieldMetadataItem,
|
||||||
|
}: EventFieldDiffLabelProps) => {
|
||||||
|
const { getIcon } = useIcons();
|
||||||
|
|
||||||
|
const IconComponent = fieldMetadataItem?.icon
|
||||||
|
? getIcon(fieldMetadataItem?.icon)
|
||||||
|
: Icon123;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledUpdatedFieldContainer>
|
||||||
|
<StyledUpdatedFieldIconContainer>
|
||||||
|
<IconComponent />
|
||||||
|
</StyledUpdatedFieldIconContainer>
|
||||||
|
<StyledUpdatedFieldLabel>
|
||||||
|
{fieldMetadataItem.label}
|
||||||
|
</StyledUpdatedFieldLabel>
|
||||||
|
</StyledUpdatedFieldContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,53 @@
|
|||||||
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
|
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||||
|
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||||
|
import { isLabelIdentifierField } from '@/object-metadata/utils/isLabelIdentifierField';
|
||||||
|
import { FieldDisplay } from '@/object-record/record-field/components/FieldDisplay';
|
||||||
|
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
|
||||||
|
|
||||||
|
type EventFieldDiffValueProps = {
|
||||||
|
forgedRecordId: string;
|
||||||
|
mainObjectMetadataItem: ObjectMetadataItem;
|
||||||
|
fieldMetadataItem: FieldMetadataItem;
|
||||||
|
};
|
||||||
|
|
||||||
|
const StyledEventFieldDiffValue = styled.div`
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const EventFieldDiffValue = ({
|
||||||
|
forgedRecordId,
|
||||||
|
mainObjectMetadataItem,
|
||||||
|
fieldMetadataItem,
|
||||||
|
}: EventFieldDiffValueProps) => {
|
||||||
|
return (
|
||||||
|
<StyledEventFieldDiffValue>
|
||||||
|
<FieldContext.Provider
|
||||||
|
value={{
|
||||||
|
entityId: forgedRecordId,
|
||||||
|
isLabelIdentifier: isLabelIdentifierField({
|
||||||
|
fieldMetadataItem,
|
||||||
|
objectMetadataItem: mainObjectMetadataItem,
|
||||||
|
}),
|
||||||
|
fieldDefinition: {
|
||||||
|
type: fieldMetadataItem.type,
|
||||||
|
iconName: fieldMetadataItem?.icon || 'FieldIcon',
|
||||||
|
fieldMetadataId: fieldMetadataItem.id || '',
|
||||||
|
label: fieldMetadataItem.label,
|
||||||
|
metadata: {
|
||||||
|
fieldName: fieldMetadataItem.name,
|
||||||
|
objectMetadataNameSingular: mainObjectMetadataItem.nameSingular,
|
||||||
|
options: fieldMetadataItem.options ?? [],
|
||||||
|
},
|
||||||
|
defaultValue: fieldMetadataItem.defaultValue,
|
||||||
|
},
|
||||||
|
hotkeyScope: 'field-event-diff',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FieldDisplay />
|
||||||
|
</FieldContext.Provider>
|
||||||
|
</StyledEventFieldDiffValue>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,42 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
import { useSetRecoilState } from 'recoil';
|
||||||
|
|
||||||
|
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||||
|
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||||
|
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||||
|
|
||||||
|
export const EventFieldDiffValueEffect = ({
|
||||||
|
forgedRecordId,
|
||||||
|
diffRecord,
|
||||||
|
mainObjectMetadataItem,
|
||||||
|
fieldMetadataItem,
|
||||||
|
}: {
|
||||||
|
forgedRecordId: string;
|
||||||
|
diffRecord: Record<string, any> | undefined;
|
||||||
|
mainObjectMetadataItem: ObjectMetadataItem;
|
||||||
|
fieldMetadataItem: FieldMetadataItem;
|
||||||
|
}) => {
|
||||||
|
const setEntityFields = useSetRecoilState(
|
||||||
|
recordStoreFamilyState(forgedRecordId),
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!diffRecord) return;
|
||||||
|
|
||||||
|
const forgedObjectRecord = {
|
||||||
|
__typename: mainObjectMetadataItem.nameSingular,
|
||||||
|
id: forgedRecordId,
|
||||||
|
[fieldMetadataItem.name]: diffRecord,
|
||||||
|
};
|
||||||
|
|
||||||
|
setEntityFields(forgedObjectRecord);
|
||||||
|
}, [
|
||||||
|
diffRecord,
|
||||||
|
forgedRecordId,
|
||||||
|
fieldMetadataItem.name,
|
||||||
|
mainObjectMetadataItem.nameSingular,
|
||||||
|
setEntityFields,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return <></>;
|
||||||
|
};
|
||||||
@ -0,0 +1,52 @@
|
|||||||
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
|
import {
|
||||||
|
EventRowDynamicComponentProps,
|
||||||
|
StyledItemAction,
|
||||||
|
StyledItemAuthorText,
|
||||||
|
StyledItemLabelIdentifier,
|
||||||
|
} from '@/activities/timelineActivities/rows/components/EventRowDynamicComponent';
|
||||||
|
import { EventRowMainObjectUpdated } from '@/activities/timelineActivities/rows/mainObject/components/EventRowMainObjectUpdated';
|
||||||
|
|
||||||
|
type EventRowMainObjectProps = EventRowDynamicComponentProps;
|
||||||
|
|
||||||
|
const StyledMainContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: ${({ theme }) => theme.spacing(1)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const EventRowMainObject = ({
|
||||||
|
authorFullName,
|
||||||
|
labelIdentifierValue,
|
||||||
|
event,
|
||||||
|
mainObjectMetadataItem,
|
||||||
|
}: EventRowMainObjectProps) => {
|
||||||
|
const [, eventAction] = event.name.split('.');
|
||||||
|
|
||||||
|
switch (eventAction) {
|
||||||
|
case 'created': {
|
||||||
|
return (
|
||||||
|
<StyledMainContainer>
|
||||||
|
<StyledItemLabelIdentifier>
|
||||||
|
{labelIdentifierValue}
|
||||||
|
</StyledItemLabelIdentifier>
|
||||||
|
<StyledItemAction>was created by</StyledItemAction>
|
||||||
|
<StyledItemAuthorText>{authorFullName}</StyledItemAuthorText>
|
||||||
|
</StyledMainContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
case 'updated': {
|
||||||
|
return (
|
||||||
|
<EventRowMainObjectUpdated
|
||||||
|
authorFullName={authorFullName}
|
||||||
|
labelIdentifierValue={labelIdentifierValue}
|
||||||
|
event={event}
|
||||||
|
mainObjectMetadataItem={mainObjectMetadataItem}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -0,0 +1,125 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
|
import {
|
||||||
|
EventCard,
|
||||||
|
EventCardToggleButton,
|
||||||
|
} from '@/activities/timelineActivities/rows/components/EventCard';
|
||||||
|
import {
|
||||||
|
StyledItemAction,
|
||||||
|
StyledItemAuthorText,
|
||||||
|
} from '@/activities/timelineActivities/rows/components/EventRowDynamicComponent';
|
||||||
|
import { EventFieldDiff } from '@/activities/timelineActivities/rows/mainObject/components/EventFieldDiff';
|
||||||
|
import { TimelineActivity } from '@/activities/timelineActivities/types/TimelineActivity';
|
||||||
|
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||||
|
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||||
|
|
||||||
|
type EventRowMainObjectUpdatedProps = {
|
||||||
|
mainObjectMetadataItem: ObjectMetadataItem;
|
||||||
|
authorFullName: string;
|
||||||
|
labelIdentifierValue: string;
|
||||||
|
event: TimelineActivity;
|
||||||
|
};
|
||||||
|
|
||||||
|
const StyledRowContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: ${({ theme }) => theme.spacing(1)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledEventRowMainObjectUpdatedContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: ${({ theme }) => theme.spacing(1)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const renderUpdateDescription = (
|
||||||
|
mainObjectMetadataItem: ObjectMetadataItem,
|
||||||
|
diffKey: string,
|
||||||
|
diffValue: any,
|
||||||
|
eventId: string,
|
||||||
|
fieldMetadataItemMap: Record<string, FieldMetadataItem>,
|
||||||
|
) => {
|
||||||
|
const fieldMetadataItem = fieldMetadataItemMap[diffKey];
|
||||||
|
|
||||||
|
if (!fieldMetadataItem) {
|
||||||
|
throw new Error(
|
||||||
|
`Cannot find field metadata item for field name ${diffKey} on object ${mainObjectMetadataItem.nameSingular}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const forgedRecordId = eventId + '--' + fieldMetadataItem.id;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EventFieldDiff
|
||||||
|
key={forgedRecordId}
|
||||||
|
diffRecord={diffValue}
|
||||||
|
fieldMetadataItem={fieldMetadataItem}
|
||||||
|
mainObjectMetadataItem={mainObjectMetadataItem}
|
||||||
|
forgedRecordId={forgedRecordId}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const EventRowMainObjectUpdated = ({
|
||||||
|
authorFullName,
|
||||||
|
labelIdentifierValue,
|
||||||
|
event,
|
||||||
|
mainObjectMetadataItem,
|
||||||
|
}: EventRowMainObjectUpdatedProps) => {
|
||||||
|
const diff: Record<string, { before: any; after: any }> =
|
||||||
|
event.properties?.diff;
|
||||||
|
|
||||||
|
const [isOpen, setIsOpen] = useState(true);
|
||||||
|
|
||||||
|
const fieldMetadataItemMap: Record<string, FieldMetadataItem> =
|
||||||
|
mainObjectMetadataItem.fields.reduce(
|
||||||
|
(acc, field) => ({ ...acc, [field.name]: field }),
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
|
||||||
|
const diffEntries = Object.entries(diff);
|
||||||
|
if (diffEntries.length === 0) {
|
||||||
|
throw new Error('Cannot render update description without changes');
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledEventRowMainObjectUpdatedContainer>
|
||||||
|
<StyledRowContainer>
|
||||||
|
<StyledItemAuthorText>{authorFullName}</StyledItemAuthorText>
|
||||||
|
<StyledItemAction>
|
||||||
|
updated
|
||||||
|
{diffEntries.length === 1 &&
|
||||||
|
renderUpdateDescription(
|
||||||
|
mainObjectMetadataItem,
|
||||||
|
diffEntries[0][0],
|
||||||
|
diffEntries[0][1].after,
|
||||||
|
event.id,
|
||||||
|
fieldMetadataItemMap,
|
||||||
|
)}
|
||||||
|
{diffEntries.length > 1 && (
|
||||||
|
<>
|
||||||
|
<span>
|
||||||
|
{diffEntries.length} fields on {labelIdentifierValue}
|
||||||
|
</span>
|
||||||
|
<EventCardToggleButton isOpen={isOpen} setIsOpen={setIsOpen} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</StyledItemAction>
|
||||||
|
</StyledRowContainer>
|
||||||
|
{diffEntries.length > 1 && (
|
||||||
|
<EventCard isOpen={isOpen}>
|
||||||
|
{diffEntries.map(([diffKey, diffValue]) =>
|
||||||
|
renderUpdateDescription(
|
||||||
|
mainObjectMetadataItem,
|
||||||
|
diffKey,
|
||||||
|
diffValue.after,
|
||||||
|
event.id,
|
||||||
|
fieldMetadataItemMap,
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</EventCard>
|
||||||
|
)}
|
||||||
|
</StyledEventRowMainObjectUpdatedContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,127 @@
|
|||||||
|
import styled from '@emotion/styled';
|
||||||
|
import { isUndefined } from '@sniptt/guards';
|
||||||
|
import { OverflowingTextWithTooltip } from 'twenty-ui';
|
||||||
|
|
||||||
|
import { useEmailThread } from '@/activities/emails/hooks/useEmailThread';
|
||||||
|
import { EmailThreadMessage } from '@/activities/emails/types/EmailThreadMessage';
|
||||||
|
import { EventCardMessageNotShared } from '@/activities/timelineActivities/rows/message/components/EventCardMessageNotShared';
|
||||||
|
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||||
|
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
|
||||||
|
import { useSetRecordInStore } from '@/object-record/record-store/hooks/useSetRecordInStore';
|
||||||
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
|
||||||
|
const StyledEventCardMessageContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledEmailContent = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: ${({ theme }) => theme.spacing(4)};
|
||||||
|
justify-content: center;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledEmailTop = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: ${({ theme }) => theme.spacing(2)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledEmailTitle = styled.div`
|
||||||
|
color: ${({ theme }) => theme.font.color.primary};
|
||||||
|
font-weight: ${({ theme }) => theme.font.weight.medium};
|
||||||
|
display: flex;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledEmailParticipants = styled.div`
|
||||||
|
color: ${({ theme }) => theme.font.color.tertiary};
|
||||||
|
display: flex;
|
||||||
|
font-size: ${({ theme }) => theme.font.size.sm};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledEmailBody = styled.div`
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const EventCardMessage = ({
|
||||||
|
messageId,
|
||||||
|
authorFullName,
|
||||||
|
}: {
|
||||||
|
messageId: string;
|
||||||
|
authorFullName: string;
|
||||||
|
}) => {
|
||||||
|
const { setRecords } = useSetRecordInStore();
|
||||||
|
|
||||||
|
const {
|
||||||
|
record: message,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
} = useFindOneRecord<EmailThreadMessage>({
|
||||||
|
objectNameSingular: CoreObjectNameSingular.Message,
|
||||||
|
objectRecordId: messageId,
|
||||||
|
recordGqlFields: {
|
||||||
|
text: true,
|
||||||
|
subject: true,
|
||||||
|
direction: true,
|
||||||
|
messageThreadId: true,
|
||||||
|
messageParticipants: {
|
||||||
|
handle: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
onCompleted: (data) => {
|
||||||
|
setRecords([data]);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { openEmailThread } = useEmailThread();
|
||||||
|
|
||||||
|
if (isDefined(error)) {
|
||||||
|
const shouldHideMessageContent = error.graphQLErrors.some(
|
||||||
|
(e) => e.extensions?.code === 'FORBIDDEN',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (shouldHideMessageContent) {
|
||||||
|
return <EventCardMessageNotShared sharedByFullName={authorFullName} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const shouldHandleNotFound = error.graphQLErrors.some(
|
||||||
|
(e) => e.extensions?.code === 'NOT_FOUND',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (shouldHandleNotFound) {
|
||||||
|
return <div>Message not found</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div>Error loading message</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loading || isUndefined(message)) {
|
||||||
|
return <div>Loading...</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const messageParticipantHandles = message?.messageParticipants
|
||||||
|
.map((participant) => participant.handle)
|
||||||
|
.join(', ');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledEventCardMessageContainer>
|
||||||
|
<StyledEmailContent>
|
||||||
|
<StyledEmailTop>
|
||||||
|
<StyledEmailTitle>
|
||||||
|
<div>{message.subject}</div>
|
||||||
|
</StyledEmailTitle>
|
||||||
|
<StyledEmailParticipants>
|
||||||
|
<OverflowingTextWithTooltip text={messageParticipantHandles} />
|
||||||
|
</StyledEmailParticipants>
|
||||||
|
</StyledEmailTop>
|
||||||
|
<StyledEmailBody
|
||||||
|
onClick={() => openEmailThread(message.messageThreadId)}
|
||||||
|
>
|
||||||
|
{message.text}
|
||||||
|
</StyledEmailBody>
|
||||||
|
</StyledEmailContent>
|
||||||
|
</StyledEventCardMessageContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,86 @@
|
|||||||
|
import styled from '@emotion/styled';
|
||||||
|
import { IconLock } from 'twenty-ui';
|
||||||
|
|
||||||
|
const StyledEventCardMessageContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledEmailContent = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: ${({ theme }) => theme.spacing(4)};
|
||||||
|
justify-content: center;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledEmailTop = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: ${({ theme }) => theme.spacing(2)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledEmailTitle = styled.div`
|
||||||
|
color: ${({ theme }) => theme.font.color.primary};
|
||||||
|
font-weight: ${({ theme }) => theme.font.weight.medium};
|
||||||
|
display: flex;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledEmailBodyNotShareContainer = styled.div`
|
||||||
|
align-items: center;
|
||||||
|
align-self: stretch;
|
||||||
|
background: ${({ theme }) => theme.background.transparent.lighter};
|
||||||
|
border: 1px solid ${({ theme }) => theme.border.color.light};
|
||||||
|
border-radius: ${({ theme }) => theme.spacing(1)};
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: ${({ theme }) => theme.spacing(3)};
|
||||||
|
|
||||||
|
height: 80px;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0 ${({ theme }) => theme.spacing(1)};
|
||||||
|
|
||||||
|
color: ${({ theme }) => theme.font.color.light};
|
||||||
|
font-size: ${({ theme }) => theme.font.size.sm};
|
||||||
|
font-weight: ${({ theme }) => theme.font.weight.medium};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledEmailBodyNotSharedIconContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledEmailBodyNotShare = styled.div`
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
gap: ${({ theme }) => theme.spacing(1)};
|
||||||
|
padding: 0 ${({ theme }) => theme.spacing(1)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const EventCardMessageNotShared = ({
|
||||||
|
sharedByFullName,
|
||||||
|
}: {
|
||||||
|
sharedByFullName: string;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<StyledEventCardMessageContainer>
|
||||||
|
<StyledEmailContent>
|
||||||
|
<StyledEmailTop>
|
||||||
|
<StyledEmailTitle>
|
||||||
|
<span>Subject not shared</span>
|
||||||
|
</StyledEmailTitle>
|
||||||
|
</StyledEmailTop>
|
||||||
|
<StyledEmailBodyNotShareContainer>
|
||||||
|
<StyledEmailBodyNotShare>
|
||||||
|
<StyledEmailBodyNotSharedIconContainer>
|
||||||
|
<IconLock />
|
||||||
|
</StyledEmailBodyNotSharedIconContainer>
|
||||||
|
<span>Not shared by {sharedByFullName}</span>
|
||||||
|
</StyledEmailBodyNotShare>
|
||||||
|
</StyledEmailBodyNotShareContainer>
|
||||||
|
</StyledEmailContent>
|
||||||
|
</StyledEventCardMessageContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,70 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
|
import {
|
||||||
|
EventCard,
|
||||||
|
EventCardToggleButton,
|
||||||
|
} from '@/activities/timelineActivities/rows/components/EventCard';
|
||||||
|
import {
|
||||||
|
EventRowDynamicComponentProps,
|
||||||
|
StyledItemAction,
|
||||||
|
StyledItemAuthorText,
|
||||||
|
StyledItemLabelIdentifier,
|
||||||
|
} from '@/activities/timelineActivities/rows/components/EventRowDynamicComponent';
|
||||||
|
import { EventCardMessage } from '@/activities/timelineActivities/rows/message/components/EventCardMessage';
|
||||||
|
|
||||||
|
type EventRowMessageProps = EventRowDynamicComponentProps;
|
||||||
|
|
||||||
|
const StyledEventRowMessageContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: ${({ theme }) => theme.spacing(1)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledRowContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: ${({ theme }) => theme.spacing(1)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const EventRowMessage: React.FC<EventRowMessageProps> = ({
|
||||||
|
labelIdentifierValue,
|
||||||
|
event,
|
||||||
|
authorFullName,
|
||||||
|
}: EventRowMessageProps) => {
|
||||||
|
const [, eventAction] = event.name.split('.');
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
|
const renderRow = () => {
|
||||||
|
switch (eventAction) {
|
||||||
|
case 'linked': {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<StyledItemAuthorText>{authorFullName}</StyledItemAuthorText>
|
||||||
|
<StyledItemAction>linked an email with</StyledItemAction>
|
||||||
|
<StyledItemLabelIdentifier>
|
||||||
|
{labelIdentifierValue}
|
||||||
|
</StyledItemLabelIdentifier>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
throw new Error('Invalid event action for message event type.');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledEventRowMessageContainer>
|
||||||
|
<StyledRowContainer>
|
||||||
|
{renderRow()}
|
||||||
|
<EventCardToggleButton isOpen={isOpen} setIsOpen={setIsOpen} />
|
||||||
|
</StyledRowContainer>
|
||||||
|
<EventCard isOpen={isOpen}>
|
||||||
|
<EventCardMessage
|
||||||
|
messageId={event.linkedRecordId}
|
||||||
|
authorFullName={authorFullName}
|
||||||
|
/>
|
||||||
|
</EventCard>
|
||||||
|
</StyledEventRowMessageContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -7,13 +7,13 @@ describe('groupEventsByMonth', () => {
|
|||||||
const grouped = groupEventsByMonth(mockedTimelineActivities);
|
const grouped = groupEventsByMonth(mockedTimelineActivities);
|
||||||
|
|
||||||
expect(grouped).toHaveLength(2);
|
expect(grouped).toHaveLength(2);
|
||||||
expect(grouped[0].items).toHaveLength(1);
|
expect(grouped[0].items).toHaveLength(4);
|
||||||
expect(grouped[1].items).toHaveLength(1);
|
expect(grouped[1].items).toHaveLength(1);
|
||||||
|
|
||||||
expect(grouped[0].year).toBe(new Date().getFullYear());
|
expect(grouped[0].year).toBe(2023);
|
||||||
expect(grouped[1].year).toBe(2023);
|
expect(grouped[1].year).toBe(2022);
|
||||||
|
|
||||||
expect(grouped[0].month).toBe(new Date().getMonth());
|
expect(grouped[0].month).toBe(3);
|
||||||
expect(grouped[1].month).toBe(3);
|
expect(grouped[1].month).toBe(4);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -2,10 +2,13 @@ import { createState } from 'twenty-ui';
|
|||||||
|
|
||||||
import { WorkspaceMember } from '@/workspace-member/types/WorkspaceMember';
|
import { WorkspaceMember } from '@/workspace-member/types/WorkspaceMember';
|
||||||
|
|
||||||
export const currentWorkspaceMemberState = createState<Omit<
|
export type CurrentWorkspaceMember = Omit<
|
||||||
WorkspaceMember,
|
WorkspaceMember,
|
||||||
'createdAt' | 'updatedAt' | 'userId' | 'userEmail' | '__typename'
|
'createdAt' | 'updatedAt' | 'userId' | 'userEmail' | '__typename'
|
||||||
> | null>({
|
>;
|
||||||
|
|
||||||
|
export const currentWorkspaceMemberState =
|
||||||
|
createState<CurrentWorkspaceMember | null>({
|
||||||
key: 'currentWorkspaceMemberState',
|
key: 'currentWorkspaceMemberState',
|
||||||
defaultValue: null,
|
defaultValue: null,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -3618,6 +3618,42 @@ export const getObjectMetadataItemsMock = () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
__typename: 'object',
|
||||||
|
id: '20202020-049d-4d0c-9e7c-e74fee3f88b2',
|
||||||
|
dataSourceId: '20202020-7f63-47a9-b1b3-6c7290ca9fb1',
|
||||||
|
nameSingular: 'messageThread',
|
||||||
|
namePlural: 'messageThreads',
|
||||||
|
labelSingular: 'Message Thread',
|
||||||
|
labelPlural: 'Message Threads',
|
||||||
|
description: 'A webhook',
|
||||||
|
icon: 'IconMessage',
|
||||||
|
isCustom: false,
|
||||||
|
isRemote: false,
|
||||||
|
isActive: true,
|
||||||
|
isSystem: true,
|
||||||
|
createdAt: '2023-11-30T11:13:15.206Z',
|
||||||
|
updatedAt: '2023-11-30T11:13:15.206Z',
|
||||||
|
fields: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
__typename: 'object',
|
||||||
|
id: '20202020-049d-4d0c-9e7c-e74fee3f88b2',
|
||||||
|
dataSourceId: '20202020-7f63-47a9-b1b3-6c7290ca9fb1',
|
||||||
|
nameSingular: 'timelineActivity',
|
||||||
|
namePlural: 'timelineActivities',
|
||||||
|
labelSingular: 'Timeline Activitiy',
|
||||||
|
labelPlural: 'Timeline Activities',
|
||||||
|
description: 'A webhook',
|
||||||
|
icon: 'IconIconTimelineEvent',
|
||||||
|
isCustom: false,
|
||||||
|
isRemote: false,
|
||||||
|
isActive: true,
|
||||||
|
isSystem: true,
|
||||||
|
createdAt: '2023-11-30T11:13:15.206Z',
|
||||||
|
updatedAt: '2023-11-30T11:13:15.206Z',
|
||||||
|
fields: [],
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
// Todo fix typing here (the backend is not in sync with the frontend)
|
// Todo fix typing here (the backend is not in sync with the frontend)
|
||||||
|
|||||||
@ -18,6 +18,7 @@ import { ObjectTasks } from '@/activities/tasks/components/ObjectTasks';
|
|||||||
import { Timeline } from '@/activities/timeline/components/Timeline';
|
import { Timeline } from '@/activities/timeline/components/Timeline';
|
||||||
import { TimelineQueryEffect } from '@/activities/timeline/components/TimelineQueryEffect';
|
import { TimelineQueryEffect } from '@/activities/timeline/components/TimelineQueryEffect';
|
||||||
import { TimelineActivities } from '@/activities/timelineActivities/components/TimelineActivities';
|
import { TimelineActivities } from '@/activities/timelineActivities/components/TimelineActivities';
|
||||||
|
import { TimelineActivitiesQueryEffect } from '@/activities/timelineActivities/components/TimelineActivitiesQueryEffect';
|
||||||
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
||||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||||
import { TabList } from '@/ui/layout/tab/components/TabList';
|
import { TabList } from '@/ui/layout/tab/components/TabList';
|
||||||
@ -74,22 +75,21 @@ export const ShowPageRightContainer = ({
|
|||||||
);
|
);
|
||||||
const activeTabId = useRecoilValue(activeTabIdState);
|
const activeTabId = useRecoilValue(activeTabIdState);
|
||||||
|
|
||||||
const shouldDisplayCalendarTab =
|
const targetObjectNameSingular =
|
||||||
targetableObject.targetObjectNameSingular ===
|
targetableObject.targetObjectNameSingular as CoreObjectNameSingular;
|
||||||
CoreObjectNameSingular.Company ||
|
|
||||||
targetableObject.targetObjectNameSingular === CoreObjectNameSingular.Person;
|
|
||||||
|
|
||||||
|
const isCompanyOrPerson = [
|
||||||
|
CoreObjectNameSingular.Company,
|
||||||
|
CoreObjectNameSingular.Person,
|
||||||
|
].includes(targetObjectNameSingular);
|
||||||
|
|
||||||
|
const shouldDisplayCalendarTab = isCompanyOrPerson;
|
||||||
const shouldDisplayLogTab = useIsFeatureEnabled('IS_EVENT_OBJECT_ENABLED');
|
const shouldDisplayLogTab = useIsFeatureEnabled('IS_EVENT_OBJECT_ENABLED');
|
||||||
|
const shouldDisplayEmailsTab = emails && isCompanyOrPerson;
|
||||||
const shouldDisplayEmailsTab =
|
|
||||||
(emails &&
|
|
||||||
targetableObject.targetObjectNameSingular ===
|
|
||||||
CoreObjectNameSingular.Company) ||
|
|
||||||
targetableObject.targetObjectNameSingular === CoreObjectNameSingular.Person;
|
|
||||||
|
|
||||||
const isMobile = useIsMobile() || isRightDrawer;
|
const isMobile = useIsMobile() || isRightDrawer;
|
||||||
|
|
||||||
const TASK_TABS = [
|
const tabs = [
|
||||||
{
|
{
|
||||||
id: 'summary',
|
id: 'summary',
|
||||||
title: 'Summary',
|
title: 'Summary',
|
||||||
@ -102,24 +102,9 @@ export const ShowPageRightContainer = ({
|
|||||||
Icon: IconTimelineEvent,
|
Icon: IconTimelineEvent,
|
||||||
hide: !timeline || isRightDrawer,
|
hide: !timeline || isRightDrawer,
|
||||||
},
|
},
|
||||||
{
|
{ id: 'tasks', title: 'Tasks', Icon: IconCheckbox, hide: !tasks },
|
||||||
id: 'tasks',
|
{ id: 'notes', title: 'Notes', Icon: IconNotes, hide: !notes },
|
||||||
title: 'Tasks',
|
{ id: 'files', title: 'Files', Icon: IconPaperclip, hide: !notes },
|
||||||
Icon: IconCheckbox,
|
|
||||||
hide: !tasks,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'notes',
|
|
||||||
title: 'Notes',
|
|
||||||
Icon: IconNotes,
|
|
||||||
hide: !notes,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'files',
|
|
||||||
title: 'Files',
|
|
||||||
Icon: IconPaperclip,
|
|
||||||
hide: !notes,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: 'emails',
|
id: 'emails',
|
||||||
title: 'Emails',
|
title: 'Emails',
|
||||||
@ -132,48 +117,51 @@ export const ShowPageRightContainer = ({
|
|||||||
Icon: IconCalendarEvent,
|
Icon: IconCalendarEvent,
|
||||||
hide: !shouldDisplayCalendarTab,
|
hide: !shouldDisplayCalendarTab,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: 'logs',
|
|
||||||
title: 'Logs',
|
|
||||||
Icon: IconTimelineEvent,
|
|
||||||
hide: !shouldDisplayLogTab,
|
|
||||||
hasBetaPill: true,
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const renderActiveTabContent = () => {
|
||||||
|
switch (activeTabId) {
|
||||||
|
case 'timeline':
|
||||||
|
return shouldDisplayLogTab ? (
|
||||||
|
<>
|
||||||
|
<TimelineActivitiesQueryEffect
|
||||||
|
targetableObject={targetableObject}
|
||||||
|
/>
|
||||||
|
<TimelineActivities targetableObject={targetableObject} />
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<TimelineQueryEffect targetableObject={targetableObject} />
|
||||||
|
<Timeline loading={loading} targetableObject={targetableObject} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
case 'summary':
|
||||||
|
return summary;
|
||||||
|
case 'tasks':
|
||||||
|
return <ObjectTasks targetableObject={targetableObject} />;
|
||||||
|
case 'notes':
|
||||||
|
return <Notes targetableObject={targetableObject} />;
|
||||||
|
case 'files':
|
||||||
|
return <Attachments targetableObject={targetableObject} />;
|
||||||
|
case 'emails':
|
||||||
|
return <EmailThreads targetableObject={targetableObject} />;
|
||||||
|
case 'calendar':
|
||||||
|
return <Calendar targetableObject={targetableObject} />;
|
||||||
|
default:
|
||||||
|
return <></>;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledShowPageRightContainer isMobile={isMobile}>
|
<StyledShowPageRightContainer isMobile={isMobile}>
|
||||||
<StyledTabListContainer>
|
<StyledTabListContainer>
|
||||||
<TabList
|
<TabList
|
||||||
loading={loading}
|
loading={loading}
|
||||||
tabListId={TAB_LIST_COMPONENT_ID + isRightDrawer}
|
tabListId={TAB_LIST_COMPONENT_ID + isRightDrawer}
|
||||||
tabs={TASK_TABS}
|
tabs={tabs}
|
||||||
/>
|
/>
|
||||||
</StyledTabListContainer>
|
</StyledTabListContainer>
|
||||||
{activeTabId === 'summary' && summary}
|
{renderActiveTabContent()}
|
||||||
{activeTabId === 'timeline' && (
|
|
||||||
<>
|
|
||||||
<TimelineQueryEffect targetableObject={targetableObject} />
|
|
||||||
<Timeline loading={loading} targetableObject={targetableObject} />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{activeTabId === 'tasks' && (
|
|
||||||
<ObjectTasks targetableObject={targetableObject} />
|
|
||||||
)}
|
|
||||||
{activeTabId === 'notes' && <Notes targetableObject={targetableObject} />}
|
|
||||||
{activeTabId === 'files' && (
|
|
||||||
<Attachments targetableObject={targetableObject} />
|
|
||||||
)}
|
|
||||||
{activeTabId === 'emails' && (
|
|
||||||
<EmailThreads targetableObject={targetableObject} />
|
|
||||||
)}
|
|
||||||
{activeTabId === 'calendar' && (
|
|
||||||
<Calendar targetableObject={targetableObject} />
|
|
||||||
)}
|
|
||||||
{activeTabId === 'logs' && (
|
|
||||||
<TimelineActivities targetableObject={targetableObject} />
|
|
||||||
)}
|
|
||||||
{}
|
|
||||||
</StyledShowPageRightContainer>
|
</StyledShowPageRightContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { TimelineActivityContext } from '@/activities/timelineActivities/contexts/TimelineActivityContext';
|
||||||
import { RecordShowContainer } from '@/object-record/record-show/components/RecordShowContainer';
|
import { RecordShowContainer } from '@/object-record/record-show/components/RecordShowContainer';
|
||||||
import { useRecordShowPage } from '@/object-record/record-show/hooks/useRecordShowPage';
|
import { useRecordShowPage } from '@/object-record/record-show/hooks/useRecordShowPage';
|
||||||
import { RecordValueSetterEffect } from '@/object-record/record-store/components/RecordValueSetterEffect';
|
import { RecordValueSetterEffect } from '@/object-record/record-store/components/RecordValueSetterEffect';
|
||||||
@ -65,11 +66,17 @@ export const RecordShowPage = () => {
|
|||||||
</>
|
</>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
<PageBody>
|
<PageBody>
|
||||||
|
<TimelineActivityContext.Provider
|
||||||
|
value={{
|
||||||
|
labelIdentifierValue: pageName,
|
||||||
|
}}
|
||||||
|
>
|
||||||
<RecordShowContainer
|
<RecordShowContainer
|
||||||
objectNameSingular={objectNameSingular}
|
objectNameSingular={objectNameSingular}
|
||||||
objectRecordId={objectRecordId}
|
objectRecordId={objectRecordId}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
/>
|
/>
|
||||||
|
</TimelineActivityContext.Provider>
|
||||||
</PageBody>
|
</PageBody>
|
||||||
</PageContainer>
|
</PageContainer>
|
||||||
</RecordFieldValueSelectorContextProvider>
|
</RecordFieldValueSelectorContextProvider>
|
||||||
|
|||||||
@ -2,13 +2,13 @@ import { TimelineActivity } from '@/activities/timelineActivities/types/Timeline
|
|||||||
|
|
||||||
export const mockedTimelineActivities: Array<TimelineActivity> = [
|
export const mockedTimelineActivities: Array<TimelineActivity> = [
|
||||||
{
|
{
|
||||||
properties: '{"diff": {"address": {"after": "TEST", "before": ""}}}',
|
properties: null,
|
||||||
updatedAt: '2023-04-26T10:12:42.33625+00:00',
|
updatedAt: '2023-04-26T10:12:42.33625+00:00',
|
||||||
id: '79f84835-b2f9-4ab5-8ab9-17dbcc45dda3',
|
id: '79f84835-b2f9-4ab5-8ab9-17dbcc45dda3',
|
||||||
linkedObjectMetadataId: '1ad72a42-6ab4-4474-a62a-a57cae3c0298',
|
linkedObjectMetadataId: '1ad72a42-6ab4-4474-a62a-a57cae3c0298',
|
||||||
linkedRecordCachedName: 'Test',
|
linkedRecordCachedName: '',
|
||||||
linkedRecordId: 'ce40eca0-8f4b-4bba-ba91-5cbd870c64d0',
|
linkedRecordId: 'ce40eca0-8f4b-4bba-ba91-5cbd870c64d3',
|
||||||
name: 'updated.company',
|
name: 'calendarEvent.linked',
|
||||||
createdAt: '2023-04-26T10:12:42.33625+00:00',
|
createdAt: '2023-04-26T10:12:42.33625+00:00',
|
||||||
workspaceMember: {
|
workspaceMember: {
|
||||||
__typename: 'WorkspaceMember',
|
__typename: 'WorkspaceMember',
|
||||||
@ -27,15 +27,97 @@ export const mockedTimelineActivities: Array<TimelineActivity> = [
|
|||||||
__typename: 'TimelineActivity',
|
__typename: 'TimelineActivity',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
properties:
|
properties: null,
|
||||||
'{"after": {"id": "ce40eca0-8f4b-4bba-ba91-5cbd870c64d0", "name": "", "xLink": {"url": "", "label": ""}, "events": {"edges": [], "__typename": "eventConnection"}, "people": {"edges": [], "__typename": "personConnection"}, "address": "", "position": 0.5, "createdAt": "2024-03-24T21:33:45.765295", "employees": null, "favorites": {"edges": [], "__typename": "favoriteConnection"}, "updatedAt": "2024-03-24T21:33:45.765295", "__typename": "company", "domainName": "", "attachments": {"edges": [], "__typename": "attachmentConnection"}, "accountOwner": null, "linkedinLink": {"url": "", "label": ""}, "opportunities": {"edges": [], "__typename": "opportunityConnection"}, "accountOwnerId": null, "activityTargets": {"edges": [], "__typename": "activityTargetConnection"}, "idealCustomerProfile": false, "annualRecurringRevenue": {"amountMicros": null, "currencyCode": ""}}}',
|
updatedAt: '2023-04-26T10:12:42.33625+00:00',
|
||||||
updatedAt: new Date().toISOString(),
|
id: '79f84835-b2f9-4ab5-8ab9-17dbcc45dda3',
|
||||||
id: '1ad72a42-6ab4-4474-a62a-a57cae3c0298',
|
linkedObjectMetadataId: '1ad72a42-6ab4-4474-a62a-a57cae3c0298',
|
||||||
name: 'created.company',
|
linkedRecordCachedName: '',
|
||||||
|
linkedRecordId: 'ce40eca0-8f4b-4bba-ba91-5cbd870c64d5',
|
||||||
|
name: 'message.linked',
|
||||||
|
createdAt: '2023-04-26T10:12:42.33625+00:00',
|
||||||
|
workspaceMember: {
|
||||||
|
__typename: 'WorkspaceMember',
|
||||||
|
id: '20202020-0687-4c41-b707-ed1bfca972a7',
|
||||||
|
avatarUrl: '',
|
||||||
|
locale: 'en',
|
||||||
|
name: {
|
||||||
|
__typename: 'FullName',
|
||||||
|
firstName: 'Tim',
|
||||||
|
lastName: 'Apple',
|
||||||
|
},
|
||||||
|
colorScheme: 'Light',
|
||||||
|
},
|
||||||
|
workspaceMemberId: '20202020-0687-4c41-b707-ed1bfca972a7',
|
||||||
|
deletedAt: null,
|
||||||
|
__typename: 'TimelineActivity',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
properties: null,
|
||||||
|
updatedAt: '2023-04-26T10:12:42.33625+00:00',
|
||||||
|
id: '79f84835-b2f9-4ab5-8ab9-17dbcc45dda3',
|
||||||
|
linkedObjectMetadataId: '1ad72a42-6ab4-4474-a62a-a57cae3c0298',
|
||||||
|
linkedRecordCachedName: 'New Task',
|
||||||
|
linkedRecordId: 'ce40eca0-8f4b-4bba-ba91-5cbd870c64d2',
|
||||||
|
name: 'task.created',
|
||||||
|
createdAt: '2023-04-26T10:12:42.33625+00:00',
|
||||||
|
workspaceMember: {
|
||||||
|
__typename: 'WorkspaceMember',
|
||||||
|
id: '20202020-0687-4c41-b707-ed1bfca972a7',
|
||||||
|
avatarUrl: '',
|
||||||
|
locale: 'en',
|
||||||
|
name: {
|
||||||
|
__typename: 'FullName',
|
||||||
|
firstName: 'Tim',
|
||||||
|
lastName: 'Apple',
|
||||||
|
},
|
||||||
|
colorScheme: 'Light',
|
||||||
|
},
|
||||||
|
workspaceMemberId: '20202020-0687-4c41-b707-ed1bfca972a7',
|
||||||
|
deletedAt: null,
|
||||||
|
__typename: 'TimelineActivity',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
properties: {
|
||||||
|
diff: {
|
||||||
|
address: {
|
||||||
|
after: 'TEST',
|
||||||
|
before: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
updatedAt: '2023-04-26T10:12:42.33625+00:00',
|
||||||
|
id: '79f84835-b2f9-4ab5-8ab9-17dbcc45dda3',
|
||||||
linkedObjectMetadataId: '1ad72a42-6ab4-4474-a62a-a57cae3c0298',
|
linkedObjectMetadataId: '1ad72a42-6ab4-4474-a62a-a57cae3c0298',
|
||||||
linkedRecordCachedName: 'Test',
|
linkedRecordCachedName: 'Test',
|
||||||
linkedRecordId: 'ce40eca0-8f4b-4bba-ba91-5cbd870c64d0',
|
linkedRecordId: 'ce40eca0-8f4b-4bba-ba91-5cbd870c64d0',
|
||||||
createdAt: new Date().toISOString(),
|
name: 'company.updated',
|
||||||
|
createdAt: '2023-04-26T10:12:42.33625+00:00',
|
||||||
|
workspaceMember: {
|
||||||
|
__typename: 'WorkspaceMember',
|
||||||
|
id: '20202020-1553-45c6-a028-5a9064cce07f',
|
||||||
|
avatarUrl: '',
|
||||||
|
locale: 'en',
|
||||||
|
name: {
|
||||||
|
__typename: 'FullName',
|
||||||
|
firstName: 'Jane',
|
||||||
|
lastName: 'Doe',
|
||||||
|
},
|
||||||
|
colorScheme: 'Light',
|
||||||
|
},
|
||||||
|
workspaceMemberId: '20202020-1553-45c6-a028-5a9064cce07f',
|
||||||
|
deletedAt: null,
|
||||||
|
__typename: 'TimelineActivity',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
properties:
|
||||||
|
'{"after": {"id": "ce40eca0-8f4b-4bba-ba91-5cbd870c64d0", "name": "", "xLink": {"url": "", "label": ""}, "events": {"edges": [], "__typename": "eventConnection"}, "people": {"edges": [], "__typename": "personConnection"}, "address": "", "position": 0.5, "createdAt": "2024-03-24T21:33:45.765295", "employees": null, "favorites": {"edges": [], "__typename": "favoriteConnection"}, "updatedAt": "2024-03-24T21:33:45.765295", "__typename": "company", "domainName": "", "attachments": {"edges": [], "__typename": "attachmentConnection"}, "accountOwner": null, "linkedinLink": {"url": "", "label": ""}, "opportunities": {"edges": [], "__typename": "opportunityConnection"}, "accountOwnerId": null, "activityTargets": {"edges": [], "__typename": "activityTargetConnection"}, "idealCustomerProfile": false, "annualRecurringRevenue": {"amountMicros": null, "currencyCode": ""}}}',
|
||||||
|
updatedAt: '2023-05-26T10:12:42.33625+00:00',
|
||||||
|
id: '1ad72a42-6ab4-4474-a62a-a57cae3c0298',
|
||||||
|
name: 'company.created',
|
||||||
|
linkedObjectMetadataId: '1ad72a42-6ab4-4474-a62a-a57cae3c0298',
|
||||||
|
linkedRecordCachedName: 'Test',
|
||||||
|
linkedRecordId: 'ce40eca0-8f4b-4bba-ba91-5cbd870c64d0',
|
||||||
|
createdAt: '2022-05-26T10:12:42.33625+00:00',
|
||||||
workspaceMember: {
|
workspaceMember: {
|
||||||
__typename: 'WorkspaceMember',
|
__typename: 'WorkspaceMember',
|
||||||
id: '20202020-0687-4c41-b707-ed1bfca972a7',
|
id: '20202020-0687-4c41-b707-ed1bfca972a7',
|
||||||
|
|||||||
@ -22,6 +22,31 @@ export const formatToHumanReadableDateTime = (date: Date | string) => {
|
|||||||
}).format(parsedJSDate);
|
}).format(parsedJSDate);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const formatToHumanReadableMonth = (date: Date | string) => {
|
||||||
|
const parsedJSDate = parseDate(date).toJSDate();
|
||||||
|
|
||||||
|
return new Intl.DateTimeFormat(undefined, {
|
||||||
|
month: 'short',
|
||||||
|
}).format(parsedJSDate);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const formatToHumanReadableDay = (date: Date | string) => {
|
||||||
|
const parsedJSDate = parseDate(date).toJSDate();
|
||||||
|
|
||||||
|
return new Intl.DateTimeFormat(undefined, {
|
||||||
|
day: 'numeric',
|
||||||
|
}).format(parsedJSDate);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const formatToHumanReadableTime = (date: Date | string) => {
|
||||||
|
const parsedJSDate = parseDate(date).toJSDate();
|
||||||
|
|
||||||
|
return new Intl.DateTimeFormat(undefined, {
|
||||||
|
hour: 'numeric',
|
||||||
|
minute: 'numeric',
|
||||||
|
}).format(parsedJSDate);
|
||||||
|
};
|
||||||
|
|
||||||
export const sanitizeURL = (link: string | null | undefined) => {
|
export const sanitizeURL = (link: string | null | undefined) => {
|
||||||
return link
|
return link
|
||||||
? link.replace(/(https?:\/\/)|(www\.)/g, '').replace(/\/$/, '')
|
? link.replace(/(https?:\/\/)|(www\.)/g, '').replace(/\/$/, '')
|
||||||
|
|||||||
@ -33,7 +33,7 @@ export const seedFeatureFlags = async (
|
|||||||
{
|
{
|
||||||
key: FeatureFlagKeys.IsEventObjectEnabled,
|
key: FeatureFlagKeys.IsEventObjectEnabled,
|
||||||
workspaceId: workspaceId,
|
workspaceId: workspaceId,
|
||||||
value: true,
|
value: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: FeatureFlagKeys.IsStripeIntegrationEnabled,
|
key: FeatureFlagKeys.IsStripeIntegrationEnabled,
|
||||||
|
|||||||
@ -7,7 +7,6 @@ import { Repository } from 'typeorm';
|
|||||||
import { MessageQueue } from 'src/engine/integrations/message-queue/message-queue.constants';
|
import { MessageQueue } from 'src/engine/integrations/message-queue/message-queue.constants';
|
||||||
import { MessageQueueService } from 'src/engine/integrations/message-queue/services/message-queue.service';
|
import { MessageQueueService } from 'src/engine/integrations/message-queue/services/message-queue.service';
|
||||||
import { ObjectRecordCreateEvent } from 'src/engine/integrations/event-emitter/types/object-record-create.event';
|
import { ObjectRecordCreateEvent } from 'src/engine/integrations/event-emitter/types/object-record-create.event';
|
||||||
import { CreateAuditLogFromInternalEvent } from 'src/modules/timeline/jobs/create-audit-log-from-internal-event';
|
|
||||||
import {
|
import {
|
||||||
FeatureFlagEntity,
|
FeatureFlagEntity,
|
||||||
FeatureFlagKeys,
|
FeatureFlagKeys,
|
||||||
@ -16,6 +15,7 @@ import { objectRecordChangedValues } from 'src/engine/integrations/event-emitter
|
|||||||
import { ObjectRecordUpdateEvent } from 'src/engine/integrations/event-emitter/types/object-record-update.event';
|
import { ObjectRecordUpdateEvent } from 'src/engine/integrations/event-emitter/types/object-record-update.event';
|
||||||
import { ObjectRecordBaseEvent } from 'src/engine/integrations/event-emitter/types/object-record.base.event';
|
import { ObjectRecordBaseEvent } from 'src/engine/integrations/event-emitter/types/object-record.base.event';
|
||||||
import { UpsertTimelineActivityFromInternalEvent } from 'src/modules/timeline/jobs/upsert-timeline-activity-from-internal-event.job';
|
import { UpsertTimelineActivityFromInternalEvent } from 'src/modules/timeline/jobs/upsert-timeline-activity-from-internal-event.job';
|
||||||
|
import { CreateAuditLogFromInternalEvent } from 'src/modules/timeline/jobs/create-audit-log-from-internal-event';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class EntityEventsToDbListener {
|
export class EntityEventsToDbListener {
|
||||||
@ -48,7 +48,7 @@ export class EntityEventsToDbListener {
|
|||||||
// @OnEvent('*.restored') - TODO: implement when we soft delete has been implemented
|
// @OnEvent('*.restored') - TODO: implement when we soft delete has been implemented
|
||||||
// ....
|
// ....
|
||||||
|
|
||||||
private async handle(payload: ObjectRecordCreateEvent<any>) {
|
private async handle(payload: ObjectRecordBaseEvent) {
|
||||||
if (!payload.objectMetadata.isAuditLogged) {
|
if (!payload.objectMetadata.isAuditLogged) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,8 +23,9 @@ import { CalendarMessagingParticipantJobModule } from 'src/modules/calendar-mess
|
|||||||
import { CalendarCronJobModule } from 'src/modules/calendar/crons/jobs/calendar-cron-job.module';
|
import { CalendarCronJobModule } from 'src/modules/calendar/crons/jobs/calendar-cron-job.module';
|
||||||
import { CalendarJobModule } from 'src/modules/calendar/jobs/calendar-job.module';
|
import { CalendarJobModule } from 'src/modules/calendar/jobs/calendar-job.module';
|
||||||
import { AutoCompaniesAndContactsCreationJobModule } from 'src/modules/connected-account/auto-companies-and-contacts-creation/jobs/auto-companies-and-contacts-creation-job.module';
|
import { AutoCompaniesAndContactsCreationJobModule } from 'src/modules/connected-account/auto-companies-and-contacts-creation/jobs/auto-companies-and-contacts-creation-job.module';
|
||||||
import { TimelineJobModule } from 'src/modules/timeline/jobs/timeline-job.module';
|
import { CalendarModule } from 'src/modules/calendar/calendar.module';
|
||||||
import { MessagingModule } from 'src/modules/messaging/messaging.module';
|
import { MessagingModule } from 'src/modules/messaging/messaging.module';
|
||||||
|
import { TimelineJobModule } from 'src/modules/timeline/jobs/timeline-job.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@ -41,6 +42,7 @@ import { MessagingModule } from 'src/modules/messaging/messaging.module';
|
|||||||
CalendarEventParticipantModule,
|
CalendarEventParticipantModule,
|
||||||
TimelineActivityModule,
|
TimelineActivityModule,
|
||||||
StripeModule,
|
StripeModule,
|
||||||
|
CalendarModule,
|
||||||
// JobsModules
|
// JobsModules
|
||||||
WorkspaceQueryRunnerJobModule,
|
WorkspaceQueryRunnerJobModule,
|
||||||
CalendarMessagingParticipantJobModule,
|
CalendarMessagingParticipantJobModule,
|
||||||
|
|||||||
@ -9,7 +9,6 @@ import { OpportunityWorkspaceEntity } from 'src/modules/opportunity/standard-obj
|
|||||||
import { PersonWorkspaceEntity } from 'src/modules/person/standard-objects/person.workspace-entity';
|
import { PersonWorkspaceEntity } from 'src/modules/person/standard-objects/person.workspace-entity';
|
||||||
import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator';
|
import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator';
|
||||||
import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator';
|
import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator';
|
||||||
import { WorkspaceIsNotAuditLogged } from 'src/engine/twenty-orm/decorators/workspace-is-not-audit-logged.decorator';
|
|
||||||
import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator';
|
import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator';
|
||||||
import { WorkspaceDynamicRelation } from 'src/engine/twenty-orm/decorators/workspace-dynamic-relation.decorator';
|
import { WorkspaceDynamicRelation } from 'src/engine/twenty-orm/decorators/workspace-dynamic-relation.decorator';
|
||||||
import { RelationMetadataType } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
|
import { RelationMetadataType } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
|
||||||
@ -25,7 +24,6 @@ import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity
|
|||||||
icon: 'IconCheckbox',
|
icon: 'IconCheckbox',
|
||||||
})
|
})
|
||||||
@WorkspaceIsSystem()
|
@WorkspaceIsSystem()
|
||||||
@WorkspaceIsNotAuditLogged()
|
|
||||||
export class ActivityTargetWorkspaceEntity extends BaseWorkspaceEntity {
|
export class ActivityTargetWorkspaceEntity extends BaseWorkspaceEntity {
|
||||||
@WorkspaceRelation({
|
@WorkspaceRelation({
|
||||||
standardId: ACTIVITY_TARGET_STANDARD_FIELD_IDS.activity,
|
standardId: ACTIVITY_TARGET_STANDARD_FIELD_IDS.activity,
|
||||||
|
|||||||
@ -12,7 +12,6 @@ import { AttachmentWorkspaceEntity } from 'src/modules/attachment/standard-objec
|
|||||||
import { CommentWorkspaceEntity } from 'src/modules/activity/standard-objects/comment.workspace-entity';
|
import { CommentWorkspaceEntity } from 'src/modules/activity/standard-objects/comment.workspace-entity';
|
||||||
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
||||||
import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator';
|
import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator';
|
||||||
import { WorkspaceIsNotAuditLogged } from 'src/engine/twenty-orm/decorators/workspace-is-not-audit-logged.decorator';
|
|
||||||
import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator';
|
import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator';
|
||||||
import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator';
|
import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator';
|
||||||
import { WorkspaceRelation } from 'src/engine/twenty-orm/decorators/workspace-relation.decorator';
|
import { WorkspaceRelation } from 'src/engine/twenty-orm/decorators/workspace-relation.decorator';
|
||||||
@ -27,7 +26,6 @@ import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity
|
|||||||
description: 'An activity',
|
description: 'An activity',
|
||||||
icon: 'IconCheckbox',
|
icon: 'IconCheckbox',
|
||||||
})
|
})
|
||||||
@WorkspaceIsNotAuditLogged()
|
|
||||||
@WorkspaceIsSystem()
|
@WorkspaceIsSystem()
|
||||||
export class ActivityWorkspaceEntity extends BaseWorkspaceEntity {
|
export class ActivityWorkspaceEntity extends BaseWorkspaceEntity {
|
||||||
@WorkspaceField({
|
@WorkspaceField({
|
||||||
|
|||||||
@ -7,7 +7,6 @@ import { ActivityWorkspaceEntity } from 'src/modules/activity/standard-objects/a
|
|||||||
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
||||||
import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator';
|
import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator';
|
||||||
import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator';
|
import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator';
|
||||||
import { WorkspaceIsNotAuditLogged } from 'src/engine/twenty-orm/decorators/workspace-is-not-audit-logged.decorator';
|
|
||||||
import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator';
|
import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator';
|
||||||
import { WorkspaceRelation } from 'src/engine/twenty-orm/decorators/workspace-relation.decorator';
|
import { WorkspaceRelation } from 'src/engine/twenty-orm/decorators/workspace-relation.decorator';
|
||||||
import { RelationMetadataType } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
|
import { RelationMetadataType } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
|
||||||
@ -22,7 +21,6 @@ import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity
|
|||||||
icon: 'IconMessageCircle',
|
icon: 'IconMessageCircle',
|
||||||
})
|
})
|
||||||
@WorkspaceIsSystem()
|
@WorkspaceIsSystem()
|
||||||
@WorkspaceIsNotAuditLogged()
|
|
||||||
export class CommentWorkspaceEntity extends BaseWorkspaceEntity {
|
export class CommentWorkspaceEntity extends BaseWorkspaceEntity {
|
||||||
@WorkspaceField({
|
@WorkspaceField({
|
||||||
standardId: COMMENT_STANDARD_FIELD_IDS.body,
|
standardId: COMMENT_STANDARD_FIELD_IDS.body,
|
||||||
|
|||||||
@ -1,11 +1,27 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
|
||||||
|
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||||
|
import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repository/object-metadata-repository.module';
|
||||||
|
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
|
||||||
import { CalendarBlocklistListener } from 'src/modules/calendar/listeners/calendar-blocklist.listener';
|
import { CalendarBlocklistListener } from 'src/modules/calendar/listeners/calendar-blocklist.listener';
|
||||||
import { CalendarChannelListener } from 'src/modules/calendar/listeners/calendar-channel.listener';
|
import { CalendarChannelListener } from 'src/modules/calendar/listeners/calendar-channel.listener';
|
||||||
|
import { CalendarEventParticipantListener } from 'src/modules/calendar/listeners/calendar-event-participant.listener';
|
||||||
|
import { TimelineActivityWorkspaceEntity } from 'src/modules/timeline/standard-objects/timeline-activity.workspace-entity';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [],
|
imports: [
|
||||||
providers: [CalendarChannelListener, CalendarBlocklistListener],
|
WorkspaceDataSourceModule,
|
||||||
|
ObjectMetadataRepositoryModule.forFeature([
|
||||||
|
TimelineActivityWorkspaceEntity,
|
||||||
|
]),
|
||||||
|
TypeOrmModule.forFeature([ObjectMetadataEntity], 'metadata'),
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
CalendarChannelListener,
|
||||||
|
CalendarBlocklistListener,
|
||||||
|
CalendarEventParticipantListener,
|
||||||
|
],
|
||||||
exports: [],
|
exports: [],
|
||||||
})
|
})
|
||||||
export class CalendarModule {}
|
export class CalendarModule {}
|
||||||
|
|||||||
@ -8,6 +8,8 @@ import { CalendarEventParticipantRepository } from 'src/modules/calendar/reposit
|
|||||||
import { CalendarChannelWorkspaceEntity } from 'src/modules/calendar/standard-objects/calendar-channel.workspace-entity';
|
import { CalendarChannelWorkspaceEntity } from 'src/modules/calendar/standard-objects/calendar-channel.workspace-entity';
|
||||||
import { CalendarEventParticipantWorkspaceEntity } from 'src/modules/calendar/standard-objects/calendar-event-participant.workspace-entity';
|
import { CalendarEventParticipantWorkspaceEntity } from 'src/modules/calendar/standard-objects/calendar-event-participant.workspace-entity';
|
||||||
import { CreateCompanyAndContactService } from 'src/modules/connected-account/auto-companies-and-contacts-creation/services/create-company-and-contact.service';
|
import { CreateCompanyAndContactService } from 'src/modules/connected-account/auto-companies-and-contacts-creation/services/create-company-and-contact.service';
|
||||||
|
import { ConnectedAccountRepository } from 'src/modules/connected-account/repositories/connected-account.repository';
|
||||||
|
import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
|
||||||
|
|
||||||
export type CalendarCreateCompanyAndContactAfterSyncJobData = {
|
export type CalendarCreateCompanyAndContactAfterSyncJobData = {
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
@ -27,6 +29,8 @@ export class CalendarCreateCompanyAndContactAfterSyncJob
|
|||||||
private readonly calendarChannelService: CalendarChannelRepository,
|
private readonly calendarChannelService: CalendarChannelRepository,
|
||||||
@InjectObjectMetadataRepository(CalendarEventParticipantWorkspaceEntity)
|
@InjectObjectMetadataRepository(CalendarEventParticipantWorkspaceEntity)
|
||||||
private readonly calendarEventParticipantRepository: CalendarEventParticipantRepository,
|
private readonly calendarEventParticipantRepository: CalendarEventParticipantRepository,
|
||||||
|
@InjectObjectMetadataRepository(ConnectedAccountWorkspaceEntity)
|
||||||
|
private readonly connectedAccountRepository: ConnectedAccountRepository,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async handle(
|
async handle(
|
||||||
@ -48,12 +52,24 @@ export class CalendarCreateCompanyAndContactAfterSyncJob
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { handle, isContactAutoCreationEnabled } = calendarChannels[0];
|
const { handle, isContactAutoCreationEnabled, connectedAccountId } =
|
||||||
|
calendarChannels[0];
|
||||||
|
|
||||||
if (!isContactAutoCreationEnabled || !handle) {
|
if (!isContactAutoCreationEnabled || !handle) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const connectedAccount = await this.connectedAccountRepository.getById(
|
||||||
|
connectedAccountId,
|
||||||
|
workspaceId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!connectedAccount) {
|
||||||
|
throw new Error(
|
||||||
|
`Connected account with id ${connectedAccountId} not found in workspace ${workspaceId}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const calendarEventParticipantsWithoutPersonIdAndWorkspaceMemberId =
|
const calendarEventParticipantsWithoutPersonIdAndWorkspaceMemberId =
|
||||||
await this.calendarEventParticipantRepository.getByCalendarChannelIdWithoutPersonIdAndWorkspaceMemberId(
|
await this.calendarEventParticipantRepository.getByCalendarChannelIdWithoutPersonIdAndWorkspaceMemberId(
|
||||||
calendarChannelId,
|
calendarChannelId,
|
||||||
@ -61,7 +77,7 @@ export class CalendarCreateCompanyAndContactAfterSyncJob
|
|||||||
);
|
);
|
||||||
|
|
||||||
await this.createCompanyAndContactService.createCompaniesAndContactsAndUpdateParticipants(
|
await this.createCompanyAndContactService.createCompaniesAndContactsAndUpdateParticipants(
|
||||||
handle,
|
connectedAccount,
|
||||||
calendarEventParticipantsWithoutPersonIdAndWorkspaceMemberId,
|
calendarEventParticipantsWithoutPersonIdAndWorkspaceMemberId,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
);
|
);
|
||||||
|
|||||||
@ -0,0 +1,71 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { OnEvent } from '@nestjs/event-emitter';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
|
||||||
|
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||||
|
import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator';
|
||||||
|
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
|
||||||
|
import { CalendarEventParticipantWorkspaceEntity } from 'src/modules/calendar/standard-objects/calendar-event-participant.workspace-entity';
|
||||||
|
import { TimelineActivityRepository } from 'src/modules/timeline/repositiories/timeline-activity.repository';
|
||||||
|
import { TimelineActivityWorkspaceEntity } from 'src/modules/timeline/standard-objects/timeline-activity.workspace-entity';
|
||||||
|
import { ObjectRecord } from 'src/engine/workspace-manager/workspace-sync-metadata/types/object-record';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class CalendarEventParticipantListener {
|
||||||
|
constructor(
|
||||||
|
@InjectObjectMetadataRepository(TimelineActivityWorkspaceEntity)
|
||||||
|
private readonly timelineActivityRepository: TimelineActivityRepository,
|
||||||
|
private readonly workspaceDataSourceService: WorkspaceDataSourceService,
|
||||||
|
@InjectRepository(ObjectMetadataEntity, 'metadata')
|
||||||
|
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@OnEvent('calendarEventParticipant.matched')
|
||||||
|
public async handleCalendarEventParticipantMatchedEvent(payload: {
|
||||||
|
workspaceId: string;
|
||||||
|
userId: string;
|
||||||
|
calendarEventParticipants: ObjectRecord<CalendarEventParticipantWorkspaceEntity>[];
|
||||||
|
}): Promise<void> {
|
||||||
|
const calendarEventParticipants = payload.calendarEventParticipants ?? [];
|
||||||
|
|
||||||
|
// TODO: move to a job?
|
||||||
|
|
||||||
|
const dataSourceSchema = this.workspaceDataSourceService.getSchemaName(
|
||||||
|
payload.workspaceId,
|
||||||
|
);
|
||||||
|
|
||||||
|
const calendarEventObjectMetadata =
|
||||||
|
await this.objectMetadataRepository.findOneOrFail({
|
||||||
|
where: {
|
||||||
|
nameSingular: 'calendarEvent',
|
||||||
|
workspaceId: payload.workspaceId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const calendarEventParticipantsWithPersonId =
|
||||||
|
calendarEventParticipants.filter((participant) => participant.personId);
|
||||||
|
|
||||||
|
if (calendarEventParticipantsWithPersonId.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.timelineActivityRepository.insertTimelineActivitiesForObject(
|
||||||
|
'person',
|
||||||
|
calendarEventParticipantsWithPersonId.map((participant) => ({
|
||||||
|
dataSourceSchema,
|
||||||
|
name: 'calendarEvent.linked',
|
||||||
|
properties: null,
|
||||||
|
objectName: 'calendarEvent',
|
||||||
|
recordId: participant.personId,
|
||||||
|
workspaceMemberId: payload.userId,
|
||||||
|
workspaceId: payload.workspaceId,
|
||||||
|
linkedObjectMetadataId: calendarEventObjectMetadata.id,
|
||||||
|
linkedRecordId: participant.calendarEventId,
|
||||||
|
linkedRecordCachedName: '',
|
||||||
|
})),
|
||||||
|
payload.workspaceId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -51,6 +51,23 @@ export class CalendarEventParticipantRepository {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async updateParticipantsPersonIdAndReturn(
|
||||||
|
participantIds: string[],
|
||||||
|
personId: string,
|
||||||
|
workspaceId: string,
|
||||||
|
transactionManager?: EntityManager,
|
||||||
|
): Promise<ObjectRecord<CalendarEventParticipantWorkspaceEntity>[]> {
|
||||||
|
const dataSourceSchema =
|
||||||
|
this.workspaceDataSourceService.getSchemaName(workspaceId);
|
||||||
|
|
||||||
|
return await this.workspaceDataSourceService.executeRawQuery(
|
||||||
|
`UPDATE ${dataSourceSchema}."calendarEventParticipant" SET "personId" = $1 WHERE "id" = ANY($2) RETURNING *`,
|
||||||
|
[personId, participantIds],
|
||||||
|
workspaceId,
|
||||||
|
transactionManager,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public async updateParticipantsWorkspaceMemberId(
|
public async updateParticipantsWorkspaceMemberId(
|
||||||
participantIds: string[],
|
participantIds: string[],
|
||||||
workspaceMemberId: string,
|
workspaceMemberId: string,
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||||
|
|
||||||
import { EntityManager } from 'typeorm';
|
import { EntityManager } from 'typeorm';
|
||||||
|
|
||||||
@ -22,20 +23,21 @@ export class CalendarEventParticipantService {
|
|||||||
@InjectObjectMetadataRepository(PersonWorkspaceEntity)
|
@InjectObjectMetadataRepository(PersonWorkspaceEntity)
|
||||||
private readonly personRepository: PersonRepository,
|
private readonly personRepository: PersonRepository,
|
||||||
private readonly addPersonIdAndWorkspaceMemberIdService: AddPersonIdAndWorkspaceMemberIdService,
|
private readonly addPersonIdAndWorkspaceMemberIdService: AddPersonIdAndWorkspaceMemberIdService,
|
||||||
|
private readonly eventEmitter: EventEmitter2,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public async updateCalendarEventParticipantsAfterPeopleCreation(
|
public async updateCalendarEventParticipantsAfterPeopleCreation(
|
||||||
createdPeople: ObjectRecord<PersonWorkspaceEntity>[],
|
createdPeople: ObjectRecord<PersonWorkspaceEntity>[],
|
||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
transactionManager?: EntityManager,
|
transactionManager?: EntityManager,
|
||||||
): Promise<void> {
|
): Promise<ObjectRecord<CalendarEventParticipantWorkspaceEntity>[]> {
|
||||||
const participants =
|
const participants =
|
||||||
await this.calendarEventParticipantRepository.getByHandles(
|
await this.calendarEventParticipantRepository.getByHandles(
|
||||||
createdPeople.map((person) => person.email),
|
createdPeople.map((person) => person.email),
|
||||||
workspaceId,
|
workspaceId,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!participants) return;
|
if (!participants) return [];
|
||||||
|
|
||||||
const dataSourceSchema =
|
const dataSourceSchema =
|
||||||
this.workspaceDataSourceService.getSchemaName(workspaceId);
|
this.workspaceDataSourceService.getSchemaName(workspaceId);
|
||||||
@ -57,7 +59,7 @@ export class CalendarEventParticipantService {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (calendarEventParticipantsToUpdate.length === 0) return;
|
if (calendarEventParticipantsToUpdate.length === 0) return [];
|
||||||
|
|
||||||
const { flattenedValues, valuesString } =
|
const { flattenedValues, valuesString } =
|
||||||
getFlattenedValuesAndValuesStringForBatchRawQuery(
|
getFlattenedValuesAndValuesStringForBatchRawQuery(
|
||||||
@ -68,23 +70,26 @@ export class CalendarEventParticipantService {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
await this.workspaceDataSourceService.executeRawQuery(
|
await this.workspaceDataSourceService.executeRawQuery(
|
||||||
`UPDATE ${dataSourceSchema}."calendarEventParticipant" AS "calendarEventParticipant" SET "personId" = "data"."personId"
|
`UPDATE ${dataSourceSchema}."calendarEventParticipant" AS "calendarEventParticipant" SET "personId" = "data"."personId"
|
||||||
FROM (VALUES ${valuesString}) AS "data"("id", "personId")
|
FROM (VALUES ${valuesString}) AS "data"("id", "personId")
|
||||||
WHERE "calendarEventParticipant"."id" = "data"."id"`,
|
WHERE "calendarEventParticipant"."id" = "data"."id"
|
||||||
|
RETURNING *`,
|
||||||
flattenedValues,
|
flattenedValues,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
transactionManager,
|
transactionManager,
|
||||||
);
|
)
|
||||||
|
).flat();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async saveCalendarEventParticipants(
|
public async saveCalendarEventParticipants(
|
||||||
calendarEventParticipants: CalendarEventParticipant[],
|
calendarEventParticipants: CalendarEventParticipant[],
|
||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
transactionManager?: EntityManager,
|
transactionManager?: EntityManager,
|
||||||
): Promise<void> {
|
): Promise<ObjectRecord<CalendarEventParticipantWorkspaceEntity>[]> {
|
||||||
if (calendarEventParticipants.length === 0) {
|
if (calendarEventParticipants.length === 0) {
|
||||||
return;
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const dataSourceSchema =
|
const dataSourceSchema =
|
||||||
@ -111,8 +116,9 @@ export class CalendarEventParticipantService {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
await this.workspaceDataSourceService.executeRawQuery(
|
return await this.workspaceDataSourceService.executeRawQuery(
|
||||||
`INSERT INTO ${dataSourceSchema}."calendarEventParticipant" ("calendarEventId", "handle", "displayName", "isOrganizer", "responseStatus", "personId", "workspaceMemberId") VALUES ${valuesString}`,
|
`INSERT INTO ${dataSourceSchema}."calendarEventParticipant" ("calendarEventId", "handle", "displayName", "isOrganizer", "responseStatus", "personId", "workspaceMemberId") VALUES ${valuesString}
|
||||||
|
RETURNING *`,
|
||||||
flattenedValues,
|
flattenedValues,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
transactionManager,
|
transactionManager,
|
||||||
@ -135,11 +141,18 @@ export class CalendarEventParticipantService {
|
|||||||
calendarEventParticipantsToUpdate.map((participant) => participant.id);
|
calendarEventParticipantsToUpdate.map((participant) => participant.id);
|
||||||
|
|
||||||
if (personId) {
|
if (personId) {
|
||||||
await this.calendarEventParticipantRepository.updateParticipantsPersonId(
|
const updatedCalendarEventParticipants =
|
||||||
|
await this.calendarEventParticipantRepository.updateParticipantsPersonIdAndReturn(
|
||||||
calendarEventParticipantIdsToUpdate,
|
calendarEventParticipantIdsToUpdate,
|
||||||
personId,
|
personId,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.eventEmitter.emit(`calendarEventParticipant.matched`, {
|
||||||
|
workspaceId,
|
||||||
|
userId: null,
|
||||||
|
calendarEventParticipants: updatedCalendarEventParticipants,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
if (workspaceMemberId) {
|
if (workspaceMemberId) {
|
||||||
await this.calendarEventParticipantRepository.updateParticipantsWorkspaceMemberId(
|
await this.calendarEventParticipantRepository.updateParticipantsWorkspaceMemberId(
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { Injectable, Logger } from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||||
|
|
||||||
import { Repository } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
import { calendar_v3 as calendarV3 } from 'googleapis';
|
import { calendar_v3 as calendarV3 } from 'googleapis';
|
||||||
@ -33,9 +34,10 @@ import { InjectMessageQueue } from 'src/engine/integrations/message-queue/decora
|
|||||||
import { MessageQueue } from 'src/engine/integrations/message-queue/message-queue.constants';
|
import { MessageQueue } from 'src/engine/integrations/message-queue/message-queue.constants';
|
||||||
import { MessageQueueService } from 'src/engine/integrations/message-queue/services/message-queue.service';
|
import { MessageQueueService } from 'src/engine/integrations/message-queue/services/message-queue.service';
|
||||||
import {
|
import {
|
||||||
CreateCompanyAndContactJobData,
|
|
||||||
CreateCompanyAndContactJob,
|
CreateCompanyAndContactJob,
|
||||||
|
CreateCompanyAndContactJobData,
|
||||||
} from 'src/modules/connected-account/auto-companies-and-contacts-creation/jobs/create-company-and-contact.job';
|
} from 'src/modules/connected-account/auto-companies-and-contacts-creation/jobs/create-company-and-contact.job';
|
||||||
|
import { ObjectRecord } from 'src/engine/workspace-manager/workspace-sync-metadata/types/object-record';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class GoogleCalendarSyncService {
|
export class GoogleCalendarSyncService {
|
||||||
@ -64,6 +66,7 @@ export class GoogleCalendarSyncService {
|
|||||||
private readonly calendarEventParticipantsService: CalendarEventParticipantService,
|
private readonly calendarEventParticipantsService: CalendarEventParticipantService,
|
||||||
@InjectMessageQueue(MessageQueue.emailQueue)
|
@InjectMessageQueue(MessageQueue.emailQueue)
|
||||||
private readonly messageQueueService: MessageQueueService,
|
private readonly messageQueueService: MessageQueueService,
|
||||||
|
private readonly eventEmitter: EventEmitter2,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public async startGoogleCalendarSync(
|
public async startGoogleCalendarSync(
|
||||||
@ -389,7 +392,7 @@ export class GoogleCalendarSyncService {
|
|||||||
eventExternalId: string;
|
eventExternalId: string;
|
||||||
calendarChannelId: string;
|
calendarChannelId: string;
|
||||||
}[],
|
}[],
|
||||||
connectedAccount: ConnectedAccountWorkspaceEntity,
|
connectedAccount: ObjectRecord<ConnectedAccountWorkspaceEntity>,
|
||||||
calendarChannel: CalendarChannelWorkspaceEntity,
|
calendarChannel: CalendarChannelWorkspaceEntity,
|
||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
@ -409,8 +412,11 @@ export class GoogleCalendarSyncService {
|
|||||||
let startTime: number;
|
let startTime: number;
|
||||||
let endTime: number;
|
let endTime: number;
|
||||||
|
|
||||||
|
const savedCalendarEventParticipantsToEmit: ObjectRecord<CalendarEventParticipantWorkspaceEntity>[] =
|
||||||
|
[];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
dataSourceMetadata?.transaction(async (transactionManager) => {
|
await dataSourceMetadata?.transaction(async (transactionManager) => {
|
||||||
startTime = Date.now();
|
startTime = Date.now();
|
||||||
|
|
||||||
await this.calendarEventRepository.saveCalendarEvents(
|
await this.calendarEventRepository.saveCalendarEvents(
|
||||||
@ -484,12 +490,17 @@ export class GoogleCalendarSyncService {
|
|||||||
|
|
||||||
startTime = Date.now();
|
startTime = Date.now();
|
||||||
|
|
||||||
|
const savedCalendarEventParticipants =
|
||||||
await this.calendarEventParticipantsService.saveCalendarEventParticipants(
|
await this.calendarEventParticipantsService.saveCalendarEventParticipants(
|
||||||
participantsToSave,
|
participantsToSave,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
transactionManager,
|
transactionManager,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
savedCalendarEventParticipantsToEmit.push(
|
||||||
|
...savedCalendarEventParticipants,
|
||||||
|
);
|
||||||
|
|
||||||
endTime = Date.now();
|
endTime = Date.now();
|
||||||
|
|
||||||
this.logger.log(
|
this.logger.log(
|
||||||
@ -499,12 +510,18 @@ export class GoogleCalendarSyncService {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.eventEmitter.emit(`calendarEventParticipant.matched`, {
|
||||||
|
workspaceId,
|
||||||
|
userId: connectedAccount.accountOwnerId,
|
||||||
|
calendarEventParticipants: savedCalendarEventParticipantsToEmit,
|
||||||
|
});
|
||||||
|
|
||||||
if (calendarChannel.isContactAutoCreationEnabled) {
|
if (calendarChannel.isContactAutoCreationEnabled) {
|
||||||
await this.messageQueueService.add<CreateCompanyAndContactJobData>(
|
await this.messageQueueService.add<CreateCompanyAndContactJobData>(
|
||||||
CreateCompanyAndContactJob.name,
|
CreateCompanyAndContactJob.name,
|
||||||
{
|
{
|
||||||
workspaceId,
|
workspaceId,
|
||||||
connectedAccountHandle: connectedAccount.handle,
|
connectedAccount,
|
||||||
contactsToCreate: participantsToSave,
|
contactsToCreate: participantsToSave,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@ -2,11 +2,13 @@ import { Injectable } from '@nestjs/common';
|
|||||||
|
|
||||||
import { MessageQueueJob } from 'src/engine/integrations/message-queue/interfaces/message-queue-job.interface';
|
import { MessageQueueJob } from 'src/engine/integrations/message-queue/interfaces/message-queue-job.interface';
|
||||||
|
|
||||||
|
import { ObjectRecord } from 'src/engine/workspace-manager/workspace-sync-metadata/types/object-record';
|
||||||
import { CreateCompanyAndContactService } from 'src/modules/connected-account/auto-companies-and-contacts-creation/services/create-company-and-contact.service';
|
import { CreateCompanyAndContactService } from 'src/modules/connected-account/auto-companies-and-contacts-creation/services/create-company-and-contact.service';
|
||||||
|
import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
|
||||||
|
|
||||||
export type CreateCompanyAndContactJobData = {
|
export type CreateCompanyAndContactJobData = {
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
connectedAccountHandle: string;
|
connectedAccount: ObjectRecord<ConnectedAccountWorkspaceEntity>;
|
||||||
contactsToCreate: {
|
contactsToCreate: {
|
||||||
displayName: string;
|
displayName: string;
|
||||||
handle: string;
|
handle: string;
|
||||||
@ -22,10 +24,10 @@ export class CreateCompanyAndContactJob
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
async handle(data: CreateCompanyAndContactJobData): Promise<void> {
|
async handle(data: CreateCompanyAndContactJobData): Promise<void> {
|
||||||
const { workspaceId, connectedAccountHandle, contactsToCreate } = data;
|
const { workspaceId, connectedAccount, contactsToCreate } = data;
|
||||||
|
|
||||||
await this.createCompanyAndContactService.createCompaniesAndContactsAndUpdateParticipants(
|
await this.createCompanyAndContactService.createCompaniesAndContactsAndUpdateParticipants(
|
||||||
connectedAccountHandle,
|
connectedAccount,
|
||||||
contactsToCreate.map((contact) => ({
|
contactsToCreate.map((contact) => ({
|
||||||
handle: contact.handle,
|
handle: contact.handle,
|
||||||
displayName: contact.displayName,
|
displayName: contact.displayName,
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||||
|
|
||||||
import { EntityManager } from 'typeorm';
|
import { EntityManager } from 'typeorm';
|
||||||
import compact from 'lodash.compact';
|
import compact from 'lodash.compact';
|
||||||
@ -19,6 +20,9 @@ import { CalendarEventParticipantService } from 'src/modules/calendar/services/c
|
|||||||
import { filterOutContactsFromCompanyOrWorkspace } from 'src/modules/connected-account/auto-companies-and-contacts-creation/utils/filter-out-contacts-from-company-or-workspace.util';
|
import { filterOutContactsFromCompanyOrWorkspace } from 'src/modules/connected-account/auto-companies-and-contacts-creation/utils/filter-out-contacts-from-company-or-workspace.util';
|
||||||
import { ObjectRecord } from 'src/engine/workspace-manager/workspace-sync-metadata/types/object-record';
|
import { ObjectRecord } from 'src/engine/workspace-manager/workspace-sync-metadata/types/object-record';
|
||||||
import { MessagingMessageParticipantService } from 'src/modules/messaging/common/services/messaging-message-participant.service';
|
import { MessagingMessageParticipantService } from 'src/modules/messaging/common/services/messaging-message-participant.service';
|
||||||
|
import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
|
||||||
|
import { MessageParticipantWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-participant.workspace-entity';
|
||||||
|
import { CalendarEventParticipantWorkspaceEntity } from 'src/modules/calendar/standard-objects/calendar-event-participant.workspace-entity';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class CreateCompanyAndContactService {
|
export class CreateCompanyAndContactService {
|
||||||
@ -32,6 +36,7 @@ export class CreateCompanyAndContactService {
|
|||||||
private readonly workspaceDataSourceService: WorkspaceDataSourceService,
|
private readonly workspaceDataSourceService: WorkspaceDataSourceService,
|
||||||
private readonly messageParticipantService: MessagingMessageParticipantService,
|
private readonly messageParticipantService: MessagingMessageParticipantService,
|
||||||
private readonly calendarEventParticipantService: CalendarEventParticipantService,
|
private readonly calendarEventParticipantService: CalendarEventParticipantService,
|
||||||
|
private readonly eventEmitter: EventEmitter2,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async createCompaniesAndPeople(
|
async createCompaniesAndPeople(
|
||||||
@ -125,7 +130,7 @@ export class CreateCompanyAndContactService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async createCompaniesAndContactsAndUpdateParticipants(
|
async createCompaniesAndContactsAndUpdateParticipants(
|
||||||
connectedAccountHandle: string,
|
connectedAccount: ObjectRecord<ConnectedAccountWorkspaceEntity>,
|
||||||
contactsToCreate: Contacts,
|
contactsToCreate: Contacts,
|
||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
) {
|
) {
|
||||||
@ -134,21 +139,28 @@ export class CreateCompanyAndContactService {
|
|||||||
workspaceId,
|
workspaceId,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let updatedMessageParticipants: ObjectRecord<MessageParticipantWorkspaceEntity>[] =
|
||||||
|
[];
|
||||||
|
let updatedCalendarEventParticipants: ObjectRecord<CalendarEventParticipantWorkspaceEntity>[] =
|
||||||
|
[];
|
||||||
|
|
||||||
await workspaceDataSource?.transaction(
|
await workspaceDataSource?.transaction(
|
||||||
async (transactionManager: EntityManager) => {
|
async (transactionManager: EntityManager) => {
|
||||||
const createdPeople = await this.createCompaniesAndPeople(
|
const createdPeople = await this.createCompaniesAndPeople(
|
||||||
connectedAccountHandle,
|
connectedAccount.handle,
|
||||||
contactsToCreate,
|
contactsToCreate,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
transactionManager,
|
transactionManager,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
updatedMessageParticipants =
|
||||||
await this.messageParticipantService.updateMessageParticipantsAfterPeopleCreation(
|
await this.messageParticipantService.updateMessageParticipantsAfterPeopleCreation(
|
||||||
createdPeople,
|
createdPeople,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
transactionManager,
|
transactionManager,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
updatedCalendarEventParticipants =
|
||||||
await this.calendarEventParticipantService.updateCalendarEventParticipantsAfterPeopleCreation(
|
await this.calendarEventParticipantService.updateCalendarEventParticipantsAfterPeopleCreation(
|
||||||
createdPeople,
|
createdPeople,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
@ -156,5 +168,17 @@ export class CreateCompanyAndContactService {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.eventEmitter.emit(`messageParticipant.matched`, {
|
||||||
|
workspaceId,
|
||||||
|
userId: connectedAccount.accountOwnerId,
|
||||||
|
messageParticipants: updatedMessageParticipants,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.eventEmitter.emit(`calendarEventParticipant.matched`, {
|
||||||
|
workspaceId,
|
||||||
|
userId: connectedAccount.accountOwnerId,
|
||||||
|
calendarEventParticipants: updatedCalendarEventParticipants,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,64 @@
|
|||||||
|
import { ForbiddenException } from '@nestjs/common';
|
||||||
|
|
||||||
|
import groupBy from 'lodash.groupby';
|
||||||
|
|
||||||
|
import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator';
|
||||||
|
import { ConnectedAccountRepository } from 'src/modules/connected-account/repositories/connected-account.repository';
|
||||||
|
import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
|
||||||
|
import { MessageChannelRepository } from 'src/modules/messaging/common/repositories/message-channel.repository';
|
||||||
|
import { MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
|
||||||
|
import { WorkspaceMemberRepository } from 'src/modules/workspace-member/repositories/workspace-member.repository';
|
||||||
|
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
||||||
|
|
||||||
|
export class CanAccessMessageThreadService {
|
||||||
|
constructor(
|
||||||
|
@InjectObjectMetadataRepository(MessageChannelWorkspaceEntity)
|
||||||
|
private readonly messageChannelService: MessageChannelRepository,
|
||||||
|
@InjectObjectMetadataRepository(ConnectedAccountWorkspaceEntity)
|
||||||
|
private readonly connectedAccountRepository: ConnectedAccountRepository,
|
||||||
|
@InjectObjectMetadataRepository(WorkspaceMemberWorkspaceEntity)
|
||||||
|
private readonly workspaceMemberRepository: WorkspaceMemberRepository,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public async canAccessMessageThread(
|
||||||
|
userId: string,
|
||||||
|
workspaceId: string,
|
||||||
|
messageChannelMessageAssociations: any[],
|
||||||
|
) {
|
||||||
|
const messageChannels = await this.messageChannelService.getByIds(
|
||||||
|
messageChannelMessageAssociations.map(
|
||||||
|
(association) => association.messageChannelId,
|
||||||
|
),
|
||||||
|
workspaceId,
|
||||||
|
);
|
||||||
|
|
||||||
|
const messageChannelsGroupByVisibility = groupBy(
|
||||||
|
messageChannels,
|
||||||
|
(channel) => channel.visibility,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (messageChannelsGroupByVisibility.share_everything) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentWorkspaceMember =
|
||||||
|
await this.workspaceMemberRepository.getByIdOrFail(userId, workspaceId);
|
||||||
|
|
||||||
|
const messageChannelsConnectedAccounts =
|
||||||
|
await this.connectedAccountRepository.getByIds(
|
||||||
|
messageChannels.map((channel) => channel.connectedAccountId),
|
||||||
|
workspaceId,
|
||||||
|
);
|
||||||
|
|
||||||
|
const messageChannelsWorkspaceMemberIds =
|
||||||
|
messageChannelsConnectedAccounts.map(
|
||||||
|
(connectedAccount) => connectedAccount.accountOwnerId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (messageChannelsWorkspaceMemberIds.includes(currentWorkspaceMember.id)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new ForbiddenException();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,24 +1,16 @@
|
|||||||
import {
|
import {
|
||||||
BadRequestException,
|
BadRequestException,
|
||||||
ForbiddenException,
|
|
||||||
Injectable,
|
Injectable,
|
||||||
NotFoundException,
|
NotFoundException,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
|
|
||||||
import groupBy from 'lodash.groupby';
|
|
||||||
|
|
||||||
import { WorkspacePreQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-pre-query-hook/interfaces/workspace-pre-query-hook.interface';
|
import { WorkspacePreQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-pre-query-hook/interfaces/workspace-pre-query-hook.interface';
|
||||||
import { FindManyResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
import { FindManyResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
||||||
|
|
||||||
import { ConnectedAccountRepository } from 'src/modules/connected-account/repositories/connected-account.repository';
|
|
||||||
import { WorkspaceMemberRepository } from 'src/modules/workspace-member/repositories/workspace-member.repository';
|
|
||||||
import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator';
|
import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator';
|
||||||
import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
|
import { CanAccessMessageThreadService } from 'src/modules/messaging/common/query-hooks/message/can-access-message-thread.service';
|
||||||
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
|
||||||
import { MessageChannelMessageAssociationRepository } from 'src/modules/messaging/common/repositories/message-channel-message-association.repository';
|
import { MessageChannelMessageAssociationRepository } from 'src/modules/messaging/common/repositories/message-channel-message-association.repository';
|
||||||
import { MessageChannelRepository } from 'src/modules/messaging/common/repositories/message-channel.repository';
|
|
||||||
import { MessageChannelMessageAssociationWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel-message-association.workspace-entity';
|
import { MessageChannelMessageAssociationWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel-message-association.workspace-entity';
|
||||||
import { MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class MessageFindManyPreQueryHook implements WorkspacePreQueryHook {
|
export class MessageFindManyPreQueryHook implements WorkspacePreQueryHook {
|
||||||
@ -27,12 +19,7 @@ export class MessageFindManyPreQueryHook implements WorkspacePreQueryHook {
|
|||||||
MessageChannelMessageAssociationWorkspaceEntity,
|
MessageChannelMessageAssociationWorkspaceEntity,
|
||||||
)
|
)
|
||||||
private readonly messageChannelMessageAssociationService: MessageChannelMessageAssociationRepository,
|
private readonly messageChannelMessageAssociationService: MessageChannelMessageAssociationRepository,
|
||||||
@InjectObjectMetadataRepository(MessageChannelWorkspaceEntity)
|
private readonly canAccessMessageThreadService: CanAccessMessageThreadService,
|
||||||
private readonly messageChannelService: MessageChannelRepository,
|
|
||||||
@InjectObjectMetadataRepository(ConnectedAccountWorkspaceEntity)
|
|
||||||
private readonly connectedAccountRepository: ConnectedAccountRepository,
|
|
||||||
@InjectObjectMetadataRepository(WorkspaceMemberWorkspaceEntity)
|
|
||||||
private readonly workspaceMemberRepository: WorkspaceMemberRepository,
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async execute(
|
async execute(
|
||||||
@ -54,52 +41,10 @@ export class MessageFindManyPreQueryHook implements WorkspacePreQueryHook {
|
|||||||
throw new NotFoundException();
|
throw new NotFoundException();
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.canAccessMessageThread(
|
await this.canAccessMessageThreadService.canAccessMessageThread(
|
||||||
userId,
|
userId,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
messageChannelMessageAssociations,
|
messageChannelMessageAssociations,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async canAccessMessageThread(
|
|
||||||
userId: string,
|
|
||||||
workspaceId: string,
|
|
||||||
messageChannelMessageAssociations: any[],
|
|
||||||
) {
|
|
||||||
const messageChannels = await this.messageChannelService.getByIds(
|
|
||||||
messageChannelMessageAssociations.map(
|
|
||||||
(association) => association.messageChannelId,
|
|
||||||
),
|
|
||||||
workspaceId,
|
|
||||||
);
|
|
||||||
|
|
||||||
const messageChannelsGroupByVisibility = groupBy(
|
|
||||||
messageChannels,
|
|
||||||
(channel) => channel.visibility,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (messageChannelsGroupByVisibility.SHARE_EVERYTHING) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentWorkspaceMember =
|
|
||||||
await this.workspaceMemberRepository.getByIdOrFail(userId, workspaceId);
|
|
||||||
|
|
||||||
const messageChannelsConnectedAccounts =
|
|
||||||
await this.connectedAccountRepository.getByIds(
|
|
||||||
messageChannels.map((channel) => channel.connectedAccountId),
|
|
||||||
workspaceId,
|
|
||||||
);
|
|
||||||
|
|
||||||
const messageChannelsWorkspaceMemberIds =
|
|
||||||
messageChannelsConnectedAccounts.map(
|
|
||||||
(connectedAccount) => connectedAccount.accountOwnerId,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (messageChannelsWorkspaceMemberIds.includes(currentWorkspaceMember.id)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new ForbiddenException();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,16 +1,43 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||||
import { Injectable, MethodNotAllowedException } from '@nestjs/common';
|
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||||
|
|
||||||
import { WorkspacePreQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-pre-query-hook/interfaces/workspace-pre-query-hook.interface';
|
import { WorkspacePreQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-pre-query-hook/interfaces/workspace-pre-query-hook.interface';
|
||||||
import { FindOneResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
import { FindOneResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
||||||
|
|
||||||
|
import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator';
|
||||||
|
import { CanAccessMessageThreadService } from 'src/modules/messaging/common/query-hooks/message/can-access-message-thread.service';
|
||||||
|
import { MessageChannelMessageAssociationRepository } from 'src/modules/messaging/common/repositories/message-channel-message-association.repository';
|
||||||
|
import { MessageChannelMessageAssociationWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel-message-association.workspace-entity';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class MessageFindOnePreQueryHook implements WorkspacePreQueryHook {
|
export class MessageFindOnePreQueryHook implements WorkspacePreQueryHook {
|
||||||
|
constructor(
|
||||||
|
@InjectObjectMetadataRepository(
|
||||||
|
MessageChannelMessageAssociationWorkspaceEntity,
|
||||||
|
)
|
||||||
|
private readonly messageChannelMessageAssociationService: MessageChannelMessageAssociationRepository,
|
||||||
|
private readonly canAccessMessageThreadService: CanAccessMessageThreadService,
|
||||||
|
) {}
|
||||||
|
|
||||||
async execute(
|
async execute(
|
||||||
_userId: string,
|
userId: string,
|
||||||
_workspaceId: string,
|
workspaceId: string,
|
||||||
_payload: FindOneResolverArgs,
|
payload: FindOneResolverArgs,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
throw new MethodNotAllowedException('Method not allowed.');
|
const messageChannelMessageAssociations =
|
||||||
|
await this.messageChannelMessageAssociationService.getByMessageIds(
|
||||||
|
[payload?.filter?.id?.eq],
|
||||||
|
workspaceId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (messageChannelMessageAssociations.length === 0) {
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.canAccessMessageThreadService.canAccessMessageThread(
|
||||||
|
userId,
|
||||||
|
workspaceId,
|
||||||
|
messageChannelMessageAssociations,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { Module } from '@nestjs/common';
|
|||||||
import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repository/object-metadata-repository.module';
|
import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repository/object-metadata-repository.module';
|
||||||
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
||||||
import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
|
import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
|
||||||
|
import { CanAccessMessageThreadService } from 'src/modules/messaging/common/query-hooks/message/can-access-message-thread.service';
|
||||||
import { MessageFindManyPreQueryHook } from 'src/modules/messaging/common/query-hooks/message/message-find-many.pre-query.hook';
|
import { MessageFindManyPreQueryHook } from 'src/modules/messaging/common/query-hooks/message/message-find-many.pre-query.hook';
|
||||||
import { MessageFindOnePreQueryHook } from 'src/modules/messaging/common/query-hooks/message/message-find-one.pre-query-hook';
|
import { MessageFindOnePreQueryHook } from 'src/modules/messaging/common/query-hooks/message/message-find-one.pre-query-hook';
|
||||||
import { MessageChannelMessageAssociationWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel-message-association.workspace-entity';
|
import { MessageChannelMessageAssociationWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel-message-association.workspace-entity';
|
||||||
@ -18,6 +19,7 @@ import { MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/stan
|
|||||||
]),
|
]),
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
|
CanAccessMessageThreadService,
|
||||||
{
|
{
|
||||||
provide: MessageFindOnePreQueryHook.name,
|
provide: MessageFindOnePreQueryHook.name,
|
||||||
useClass: MessageFindOnePreQueryHook,
|
useClass: MessageFindOnePreQueryHook,
|
||||||
|
|||||||
@ -46,6 +46,23 @@ export class MessageParticipantRepository {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async updateParticipantsPersonIdAndReturn(
|
||||||
|
participantIds: string[],
|
||||||
|
personId: string,
|
||||||
|
workspaceId: string,
|
||||||
|
transactionManager?: EntityManager,
|
||||||
|
) {
|
||||||
|
const dataSourceSchema =
|
||||||
|
this.workspaceDataSourceService.getSchemaName(workspaceId);
|
||||||
|
|
||||||
|
return await this.workspaceDataSourceService.executeRawQuery(
|
||||||
|
`UPDATE ${dataSourceSchema}."messageParticipant" SET "personId" = $1 WHERE "id" = ANY($2) RETURNING *`,
|
||||||
|
[personId, participantIds],
|
||||||
|
workspaceId,
|
||||||
|
transactionManager,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public async updateParticipantsWorkspaceMemberId(
|
public async updateParticipantsWorkspaceMemberId(
|
||||||
participantIds: string[],
|
participantIds: string[],
|
||||||
workspaceMemberId: string,
|
workspaceMemberId: string,
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||||
|
|
||||||
import { EntityManager } from 'typeorm';
|
import { EntityManager } from 'typeorm';
|
||||||
|
|
||||||
@ -24,20 +25,21 @@ export class MessagingMessageParticipantService {
|
|||||||
@InjectObjectMetadataRepository(PersonWorkspaceEntity)
|
@InjectObjectMetadataRepository(PersonWorkspaceEntity)
|
||||||
private readonly personRepository: PersonRepository,
|
private readonly personRepository: PersonRepository,
|
||||||
private readonly addPersonIdAndWorkspaceMemberIdService: AddPersonIdAndWorkspaceMemberIdService,
|
private readonly addPersonIdAndWorkspaceMemberIdService: AddPersonIdAndWorkspaceMemberIdService,
|
||||||
|
private readonly eventEmitter: EventEmitter2,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public async updateMessageParticipantsAfterPeopleCreation(
|
public async updateMessageParticipantsAfterPeopleCreation(
|
||||||
createdPeople: ObjectRecord<PersonWorkspaceEntity>[],
|
createdPeople: ObjectRecord<PersonWorkspaceEntity>[],
|
||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
transactionManager?: EntityManager,
|
transactionManager?: EntityManager,
|
||||||
): Promise<void> {
|
): Promise<ObjectRecord<MessageParticipantWorkspaceEntity>[]> {
|
||||||
const participants = await this.messageParticipantRepository.getByHandles(
|
const participants = await this.messageParticipantRepository.getByHandles(
|
||||||
createdPeople.map((person) => person.email),
|
createdPeople.map((person) => person.email),
|
||||||
workspaceId,
|
workspaceId,
|
||||||
transactionManager,
|
transactionManager,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!participants) return;
|
if (!participants) return [];
|
||||||
|
|
||||||
const dataSourceSchema =
|
const dataSourceSchema =
|
||||||
this.workspaceDataSourceService.getSchemaName(workspaceId);
|
this.workspaceDataSourceService.getSchemaName(workspaceId);
|
||||||
@ -57,7 +59,7 @@ export class MessagingMessageParticipantService {
|
|||||||
)?.id,
|
)?.id,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
if (messageParticipantsToUpdate.length === 0) return;
|
if (messageParticipantsToUpdate.length === 0) return [];
|
||||||
|
|
||||||
const { flattenedValues, valuesString } =
|
const { flattenedValues, valuesString } =
|
||||||
getFlattenedValuesAndValuesStringForBatchRawQuery(
|
getFlattenedValuesAndValuesStringForBatchRawQuery(
|
||||||
@ -68,22 +70,25 @@ export class MessagingMessageParticipantService {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
await this.workspaceDataSourceService.executeRawQuery(
|
await this.workspaceDataSourceService.executeRawQuery(
|
||||||
`UPDATE ${dataSourceSchema}."messageParticipant" AS "messageParticipant" SET "personId" = "data"."personId"
|
`UPDATE ${dataSourceSchema}."messageParticipant" AS "messageParticipant" SET "personId" = "data"."personId"
|
||||||
FROM (VALUES ${valuesString}) AS "data"("id", "personId")
|
FROM (VALUES ${valuesString}) AS "data"("id", "personId")
|
||||||
WHERE "messageParticipant"."id" = "data"."id"`,
|
WHERE "messageParticipant"."id" = "data"."id"
|
||||||
|
RETURNING *`,
|
||||||
flattenedValues,
|
flattenedValues,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
transactionManager,
|
transactionManager,
|
||||||
);
|
)
|
||||||
|
).flat();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async saveMessageParticipants(
|
public async saveMessageParticipants(
|
||||||
participants: ParticipantWithMessageId[],
|
participants: ParticipantWithMessageId[],
|
||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
transactionManager?: EntityManager,
|
transactionManager?: EntityManager,
|
||||||
): Promise<void> {
|
): Promise<ObjectRecord<MessageParticipantWorkspaceEntity>[]> {
|
||||||
if (!participants) return;
|
if (!participants) return [];
|
||||||
|
|
||||||
const dataSourceSchema =
|
const dataSourceSchema =
|
||||||
this.workspaceDataSourceService.getSchemaName(workspaceId);
|
this.workspaceDataSourceService.getSchemaName(workspaceId);
|
||||||
@ -108,10 +113,10 @@ export class MessagingMessageParticipantService {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
if (messageParticipantsToSave.length === 0) return;
|
if (messageParticipantsToSave.length === 0) return [];
|
||||||
|
|
||||||
await this.workspaceDataSourceService.executeRawQuery(
|
return await this.workspaceDataSourceService.executeRawQuery(
|
||||||
`INSERT INTO ${dataSourceSchema}."messageParticipant" ("messageId", "role", "handle", "displayName", "personId", "workspaceMemberId") VALUES ${valuesString}`,
|
`INSERT INTO ${dataSourceSchema}."messageParticipant" ("messageId", "role", "handle", "displayName", "personId", "workspaceMemberId") VALUES ${valuesString} RETURNING *`,
|
||||||
flattenedValues,
|
flattenedValues,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
transactionManager,
|
transactionManager,
|
||||||
@ -135,11 +140,18 @@ export class MessagingMessageParticipantService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (personId) {
|
if (personId) {
|
||||||
await this.messageParticipantRepository.updateParticipantsPersonId(
|
const updatedMessageParticipants =
|
||||||
|
await this.messageParticipantRepository.updateParticipantsPersonIdAndReturn(
|
||||||
messageParticipantIdsToUpdate,
|
messageParticipantIdsToUpdate,
|
||||||
personId,
|
personId,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.eventEmitter.emit(`messageParticipant.matched`, {
|
||||||
|
workspaceId,
|
||||||
|
userId: null,
|
||||||
|
messageParticipants: updatedMessageParticipants,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
if (workspaceMemberId) {
|
if (workspaceMemberId) {
|
||||||
await this.messageParticipantRepository.updateParticipantsWorkspaceMemberId(
|
await this.messageParticipantRepository.updateParticipantsWorkspaceMemberId(
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { Injectable, Inject } from '@nestjs/common';
|
import { Injectable, Inject } from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||||
|
|
||||||
import { EntityManager, Repository } from 'typeorm';
|
import { EntityManager, Repository } from 'typeorm';
|
||||||
|
|
||||||
@ -19,10 +20,12 @@ import {
|
|||||||
import { MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
|
import { MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
|
||||||
import {
|
import {
|
||||||
GmailMessage,
|
GmailMessage,
|
||||||
|
Participant,
|
||||||
ParticipantWithMessageId,
|
ParticipantWithMessageId,
|
||||||
} from 'src/modules/messaging/message-import-manager/drivers/gmail/types/gmail-message';
|
} from 'src/modules/messaging/message-import-manager/drivers/gmail/types/gmail-message';
|
||||||
import { MessagingMessageService } from 'src/modules/messaging/common/services/messaging-message.service';
|
import { MessagingMessageService } from 'src/modules/messaging/common/services/messaging-message.service';
|
||||||
import { MessagingMessageParticipantService } from 'src/modules/messaging/common/services/messaging-message-participant.service';
|
import { MessagingMessageParticipantService } from 'src/modules/messaging/common/services/messaging-message-participant.service';
|
||||||
|
import { MessageParticipantWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-participant.workspace-entity';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class MessagingSaveMessagesAndEnqueueContactCreationService {
|
export class MessagingSaveMessagesAndEnqueueContactCreationService {
|
||||||
@ -34,6 +37,7 @@ export class MessagingSaveMessagesAndEnqueueContactCreationService {
|
|||||||
private readonly messageParticipantService: MessagingMessageParticipantService,
|
private readonly messageParticipantService: MessagingMessageParticipantService,
|
||||||
@InjectRepository(FeatureFlagEntity, 'core')
|
@InjectRepository(FeatureFlagEntity, 'core')
|
||||||
private readonly featureFlagRepository: Repository<FeatureFlagEntity>,
|
private readonly featureFlagRepository: Repository<FeatureFlagEntity>,
|
||||||
|
private readonly eventEmitter: EventEmitter2,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async saveMessagesAndEnqueueContactCreationJob(
|
async saveMessagesAndEnqueueContactCreationJob(
|
||||||
@ -57,6 +61,9 @@ export class MessagingSaveMessagesAndEnqueueContactCreationService {
|
|||||||
const isContactCreationForSentAndReceivedEmailsEnabled =
|
const isContactCreationForSentAndReceivedEmailsEnabled =
|
||||||
isContactCreationForSentAndReceivedEmailsEnabledFeatureFlag?.value;
|
isContactCreationForSentAndReceivedEmailsEnabledFeatureFlag?.value;
|
||||||
|
|
||||||
|
let savedMessageParticipants: ObjectRecord<MessageParticipantWorkspaceEntity>[] =
|
||||||
|
[];
|
||||||
|
|
||||||
const participantsWithMessageId = await workspaceDataSource?.transaction(
|
const participantsWithMessageId = await workspaceDataSource?.transaction(
|
||||||
async (transactionManager: EntityManager) => {
|
async (transactionManager: EntityManager) => {
|
||||||
const messageExternalIdsAndIdsMap =
|
const messageExternalIdsAndIdsMap =
|
||||||
@ -74,7 +81,7 @@ export class MessagingSaveMessagesAndEnqueueContactCreationService {
|
|||||||
const messageId = messageExternalIdsAndIdsMap.get(message.externalId);
|
const messageId = messageExternalIdsAndIdsMap.get(message.externalId);
|
||||||
|
|
||||||
return messageId
|
return messageId
|
||||||
? message.participants.map((participant) => ({
|
? message.participants.map((participant: Participant) => ({
|
||||||
...participant,
|
...participant,
|
||||||
messageId,
|
messageId,
|
||||||
shouldCreateContact:
|
shouldCreateContact:
|
||||||
@ -86,6 +93,7 @@ export class MessagingSaveMessagesAndEnqueueContactCreationService {
|
|||||||
: [];
|
: [];
|
||||||
});
|
});
|
||||||
|
|
||||||
|
savedMessageParticipants =
|
||||||
await this.messageParticipantService.saveMessageParticipants(
|
await this.messageParticipantService.saveMessageParticipants(
|
||||||
participantsWithMessageId,
|
participantsWithMessageId,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
@ -96,6 +104,12 @@ export class MessagingSaveMessagesAndEnqueueContactCreationService {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.eventEmitter.emit(`messageParticipant.matched`, {
|
||||||
|
workspaceId,
|
||||||
|
userId: connectedAccount.accountOwnerId,
|
||||||
|
messageParticipants: savedMessageParticipants,
|
||||||
|
});
|
||||||
|
|
||||||
if (messageChannel.isContactAutoCreationEnabled) {
|
if (messageChannel.isContactAutoCreationEnabled) {
|
||||||
const contactsToCreate = participantsWithMessageId.filter(
|
const contactsToCreate = participantsWithMessageId.filter(
|
||||||
(participant) => participant.shouldCreateContact,
|
(participant) => participant.shouldCreateContact,
|
||||||
@ -105,7 +119,7 @@ export class MessagingSaveMessagesAndEnqueueContactCreationService {
|
|||||||
CreateCompanyAndContactJob.name,
|
CreateCompanyAndContactJob.name,
|
||||||
{
|
{
|
||||||
workspaceId,
|
workspaceId,
|
||||||
connectedAccountHandle: connectedAccount.handle,
|
connectedAccount,
|
||||||
contactsToCreate,
|
contactsToCreate,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@ -15,6 +15,8 @@ import { MessageChannelRepository } from 'src/modules/messaging/common/repositor
|
|||||||
import { MessageParticipantRepository } from 'src/modules/messaging/common/repositories/message-participant.repository';
|
import { MessageParticipantRepository } from 'src/modules/messaging/common/repositories/message-participant.repository';
|
||||||
import { MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
|
import { MessageChannelWorkspaceEntity } 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 { MessageParticipantWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-participant.workspace-entity';
|
||||||
|
import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
|
||||||
|
import { ConnectedAccountRepository } from 'src/modules/connected-account/repositories/connected-account.repository';
|
||||||
|
|
||||||
export type MessagingCreateCompanyAndContactAfterSyncJobData = {
|
export type MessagingCreateCompanyAndContactAfterSyncJobData = {
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
@ -36,6 +38,8 @@ export class MessagingCreateCompanyAndContactAfterSyncJob
|
|||||||
private readonly messageParticipantRepository: MessageParticipantRepository,
|
private readonly messageParticipantRepository: MessageParticipantRepository,
|
||||||
@InjectRepository(FeatureFlagEntity, 'core')
|
@InjectRepository(FeatureFlagEntity, 'core')
|
||||||
private readonly featureFlagRepository: Repository<FeatureFlagEntity>,
|
private readonly featureFlagRepository: Repository<FeatureFlagEntity>,
|
||||||
|
@InjectObjectMetadataRepository(ConnectedAccountWorkspaceEntity)
|
||||||
|
private readonly connectedAccountRepository: ConnectedAccountRepository,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async handle(
|
async handle(
|
||||||
@ -51,12 +55,24 @@ export class MessagingCreateCompanyAndContactAfterSyncJob
|
|||||||
workspaceId,
|
workspaceId,
|
||||||
);
|
);
|
||||||
|
|
||||||
const { handle, isContactAutoCreationEnabled } = messageChannel[0];
|
const { isContactAutoCreationEnabled, connectedAccountId } =
|
||||||
|
messageChannel[0];
|
||||||
|
|
||||||
if (!isContactAutoCreationEnabled) {
|
if (!isContactAutoCreationEnabled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const connectedAccount = await this.connectedAccountRepository.getById(
|
||||||
|
connectedAccountId,
|
||||||
|
workspaceId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!connectedAccount) {
|
||||||
|
throw new Error(
|
||||||
|
`Connected account with id ${connectedAccountId} not found in workspace ${workspaceId}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const isContactCreationForSentAndReceivedEmailsEnabledFeatureFlag =
|
const isContactCreationForSentAndReceivedEmailsEnabledFeatureFlag =
|
||||||
await this.featureFlagRepository.findOneBy({
|
await this.featureFlagRepository.findOneBy({
|
||||||
workspaceId: workspaceId,
|
workspaceId: workspaceId,
|
||||||
@ -78,7 +94,7 @@ export class MessagingCreateCompanyAndContactAfterSyncJob
|
|||||||
);
|
);
|
||||||
|
|
||||||
await this.createCompanyAndContactService.createCompaniesAndContactsAndUpdateParticipants(
|
await this.createCompanyAndContactService.createCompaniesAndContactsAndUpdateParticipants(
|
||||||
handle,
|
connectedAccount,
|
||||||
contactsToCreate,
|
contactsToCreate,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
);
|
);
|
||||||
|
|||||||
@ -0,0 +1,72 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { OnEvent } from '@nestjs/event-emitter';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
|
||||||
|
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||||
|
import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator';
|
||||||
|
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
|
||||||
|
import { TimelineActivityRepository } from 'src/modules/timeline/repositiories/timeline-activity.repository';
|
||||||
|
import { TimelineActivityWorkspaceEntity } from 'src/modules/timeline/standard-objects/timeline-activity.workspace-entity';
|
||||||
|
import { ObjectRecord } from 'src/engine/workspace-manager/workspace-sync-metadata/types/object-record';
|
||||||
|
import { MessageParticipantWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-participant.workspace-entity';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class MessageParticipantListener {
|
||||||
|
constructor(
|
||||||
|
@InjectObjectMetadataRepository(TimelineActivityWorkspaceEntity)
|
||||||
|
private readonly timelineActivityRepository: TimelineActivityRepository,
|
||||||
|
private readonly workspaceDataSourceService: WorkspaceDataSourceService,
|
||||||
|
@InjectRepository(ObjectMetadataEntity, 'metadata')
|
||||||
|
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@OnEvent('messageParticipant.matched')
|
||||||
|
public async handleMessageParticipantMatched(payload: {
|
||||||
|
workspaceId: string;
|
||||||
|
userId: string;
|
||||||
|
messageParticipants: ObjectRecord<MessageParticipantWorkspaceEntity>[];
|
||||||
|
}): Promise<void> {
|
||||||
|
const messageParticipants = payload.messageParticipants ?? [];
|
||||||
|
|
||||||
|
// TODO: move to a job?
|
||||||
|
|
||||||
|
const dataSourceSchema = this.workspaceDataSourceService.getSchemaName(
|
||||||
|
payload.workspaceId,
|
||||||
|
);
|
||||||
|
|
||||||
|
const messageObjectMetadata =
|
||||||
|
await this.objectMetadataRepository.findOneOrFail({
|
||||||
|
where: {
|
||||||
|
nameSingular: 'message',
|
||||||
|
workspaceId: payload.workspaceId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const messageParticipantsWithPersonId = messageParticipants.filter(
|
||||||
|
(participant) => participant.personId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (messageParticipantsWithPersonId.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.timelineActivityRepository.insertTimelineActivitiesForObject(
|
||||||
|
'person',
|
||||||
|
messageParticipantsWithPersonId.map((participant) => ({
|
||||||
|
dataSourceSchema,
|
||||||
|
name: 'message.linked',
|
||||||
|
properties: null,
|
||||||
|
objectName: 'message',
|
||||||
|
recordId: participant.personId,
|
||||||
|
workspaceMemberId: payload.userId,
|
||||||
|
workspaceId: payload.workspaceId,
|
||||||
|
linkedObjectMetadataId: messageObjectMetadata.id,
|
||||||
|
linkedRecordId: participant.messageId,
|
||||||
|
linkedRecordCachedName: '',
|
||||||
|
})),
|
||||||
|
payload.workspaceId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -3,9 +3,14 @@ import { TypeOrmModule } from '@nestjs/typeorm';
|
|||||||
|
|
||||||
import { AnalyticsModule } from 'src/engine/core-modules/analytics/analytics.module';
|
import { AnalyticsModule } from 'src/engine/core-modules/analytics/analytics.module';
|
||||||
import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
|
import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
|
||||||
|
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||||
|
import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repository/object-metadata-repository.module';
|
||||||
|
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
|
||||||
import { AutoCompaniesAndContactsCreationModule } from 'src/modules/connected-account/auto-companies-and-contacts-creation/auto-companies-and-contacts-creation.module';
|
import { AutoCompaniesAndContactsCreationModule } from 'src/modules/connected-account/auto-companies-and-contacts-creation/auto-companies-and-contacts-creation.module';
|
||||||
import { MessagingGmailDriverModule } from 'src/modules/messaging/message-import-manager/drivers/gmail/messaging-gmail-driver.module';
|
import { MessagingGmailDriverModule } from 'src/modules/messaging/message-import-manager/drivers/gmail/messaging-gmail-driver.module';
|
||||||
import { MessagingCreateCompanyAndContactAfterSyncJob } from 'src/modules/messaging/message-participants-manager/jobs/messaging-create-company-and-contact-after-sync.job';
|
import { MessagingCreateCompanyAndContactAfterSyncJob } from 'src/modules/messaging/message-participants-manager/jobs/messaging-create-company-and-contact-after-sync.job';
|
||||||
|
import { MessageParticipantListener } from 'src/modules/messaging/message-participants-manager/listeners/message-participant.listener';
|
||||||
|
import { TimelineActivityWorkspaceEntity } from 'src/modules/timeline/standard-objects/timeline-activity.workspace-entity';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@ -13,12 +18,18 @@ import { MessagingCreateCompanyAndContactAfterSyncJob } from 'src/modules/messag
|
|||||||
AnalyticsModule,
|
AnalyticsModule,
|
||||||
MessagingGmailDriverModule,
|
MessagingGmailDriverModule,
|
||||||
AutoCompaniesAndContactsCreationModule,
|
AutoCompaniesAndContactsCreationModule,
|
||||||
|
WorkspaceDataSourceModule,
|
||||||
|
ObjectMetadataRepositoryModule.forFeature([
|
||||||
|
TimelineActivityWorkspaceEntity,
|
||||||
|
]),
|
||||||
|
TypeOrmModule.forFeature([ObjectMetadataEntity], 'metadata'),
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
{
|
{
|
||||||
provide: MessagingCreateCompanyAndContactAfterSyncJob.name,
|
provide: MessagingCreateCompanyAndContactAfterSyncJob.name,
|
||||||
useClass: MessagingCreateCompanyAndContactAfterSyncJob,
|
useClass: MessagingCreateCompanyAndContactAfterSyncJob,
|
||||||
},
|
},
|
||||||
|
MessageParticipantListener,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class MessaginParticipantsManagerModule {}
|
export class MessaginParticipantsManagerModule {}
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { EntityManager } from 'typeorm';
|
||||||
|
|
||||||
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
|
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
|
||||||
import { objectRecordDiffMerge } from 'src/engine/integrations/event-emitter/utils/object-record-diff-merge';
|
import { objectRecordDiffMerge } from 'src/engine/integrations/event-emitter/utils/object-record-diff-merge';
|
||||||
|
|
||||||
@ -74,17 +76,15 @@ export class TimelineActivityRepository {
|
|||||||
return this.workspaceDataSourceService.executeRawQuery(
|
return this.workspaceDataSourceService.executeRawQuery(
|
||||||
`SELECT * FROM ${dataSourceSchema}."timelineActivity"
|
`SELECT * FROM ${dataSourceSchema}."timelineActivity"
|
||||||
WHERE "${objectName}Id" = $1
|
WHERE "${objectName}Id" = $1
|
||||||
AND ("name" = $2 OR "name" = $3)
|
AND "name" = $2
|
||||||
AND "workspaceMemberId" = $4
|
AND "workspaceMemberId" = $3
|
||||||
AND "linkedRecordId" = $5
|
AND ${
|
||||||
|
linkedRecordId ? `"linkedRecordId" = $4` : `"linkedRecordId" IS NULL`
|
||||||
|
}
|
||||||
AND "createdAt" >= NOW() - interval '10 minutes'`,
|
AND "createdAt" >= NOW() - interval '10 minutes'`,
|
||||||
[
|
linkedRecordId
|
||||||
recordId,
|
? [recordId, name, workspaceMemberId, linkedRecordId]
|
||||||
name,
|
: [recordId, name, workspaceMemberId],
|
||||||
name.replace(/\.updated$/, '.created'),
|
|
||||||
workspaceMemberId,
|
|
||||||
linkedRecordId,
|
|
||||||
],
|
|
||||||
workspaceId,
|
workspaceId,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -133,4 +133,52 @@ export class TimelineActivityRepository {
|
|||||||
workspaceId,
|
workspaceId,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async insertTimelineActivitiesForObject(
|
||||||
|
objectName: string,
|
||||||
|
activities: {
|
||||||
|
name: string;
|
||||||
|
properties: Record<string, any> | null;
|
||||||
|
workspaceMemberId: string | undefined;
|
||||||
|
recordId: string;
|
||||||
|
linkedRecordCachedName: string;
|
||||||
|
linkedRecordId: string | undefined;
|
||||||
|
linkedObjectMetadataId: string | undefined;
|
||||||
|
}[],
|
||||||
|
workspaceId: string,
|
||||||
|
transactionManager?: EntityManager,
|
||||||
|
) {
|
||||||
|
if (activities.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dataSourceSchema =
|
||||||
|
this.workspaceDataSourceService.getSchemaName(workspaceId);
|
||||||
|
|
||||||
|
return this.workspaceDataSourceService.executeRawQuery(
|
||||||
|
`INSERT INTO ${dataSourceSchema}."timelineActivity"
|
||||||
|
("name", "properties", "workspaceMemberId", "${objectName}Id", "linkedRecordCachedName", "linkedRecordId", "linkedObjectMetadataId")
|
||||||
|
VALUES ${activities
|
||||||
|
.map(
|
||||||
|
(_, index) =>
|
||||||
|
`($${index * 7 + 1}, $${index * 7 + 2}, $${index * 7 + 3}, $${
|
||||||
|
index * 7 + 4
|
||||||
|
}, $${index * 7 + 5}, $${index * 7 + 6}, $${index * 7 + 7})`,
|
||||||
|
)
|
||||||
|
.join(',')}`,
|
||||||
|
activities
|
||||||
|
.map((activity) => [
|
||||||
|
activity.name,
|
||||||
|
activity.properties,
|
||||||
|
activity.workspaceMemberId,
|
||||||
|
activity.recordId,
|
||||||
|
activity.linkedRecordCachedName ?? '',
|
||||||
|
activity.linkedRecordId,
|
||||||
|
activity.linkedObjectMetadataId,
|
||||||
|
])
|
||||||
|
.flat(),
|
||||||
|
workspaceId,
|
||||||
|
transactionManager,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user