Logs show page (#4611)
* Being implementing events on the frontend * Rename JSON to RAW JSON * Fix handling of json field on frontend * Log user id * Add frontend tests * Update packages/twenty-server/src/engine/api/graphql/workspace-query-runner/jobs/save-event-to-db.job.ts Co-authored-by: Weiko <corentin@twenty.com> * Move db calls to a dedicated repository * Add server-side tests --------- Co-authored-by: Weiko <corentin@twenty.com>
This commit is contained in:
@ -0,0 +1,22 @@
|
||||
import { ReactElement } from 'react';
|
||||
|
||||
import { EventRow } from '@/activities/events/components/EventRow';
|
||||
import { Event } from '@/activities/events/types/Event';
|
||||
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
||||
|
||||
type EventListProps = {
|
||||
targetableObject: ActivityTargetableObject;
|
||||
title: string;
|
||||
events: Event[];
|
||||
button?: ReactElement | false;
|
||||
};
|
||||
|
||||
export const EventList = ({ events }: EventListProps) => {
|
||||
return (
|
||||
<>
|
||||
{events &&
|
||||
events.length > 0 &&
|
||||
events.map((event: Event) => <EventRow key={event.id} event={event} />)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,11 @@
|
||||
import { Event } from '@/activities/events/types/Event';
|
||||
|
||||
export const EventRow = ({ event }: { event: Event }) => {
|
||||
return (
|
||||
<>
|
||||
<p>
|
||||
{event.name}:<pre>{event.properties}</pre>
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,25 @@
|
||||
import { isNonEmptyArray } from '@sniptt/guards';
|
||||
|
||||
import { EventList } from '@/activities/events/components/EventList';
|
||||
import { useEvents } from '@/activities/events/hooks/useEvents';
|
||||
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
||||
|
||||
export const Events = ({
|
||||
targetableObject,
|
||||
}: {
|
||||
targetableObject: ActivityTargetableObject;
|
||||
}) => {
|
||||
const { events } = useEvents(targetableObject);
|
||||
|
||||
if (!isNonEmptyArray(events)) {
|
||||
return <div>No log yet</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<EventList
|
||||
targetableObject={targetableObject}
|
||||
title="All"
|
||||
events={events ?? []}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,91 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
|
||||
import { useEvents } from '@/activities/events/hooks/useEvents';
|
||||
|
||||
jest.mock('@/object-record/hooks/useFindManyRecords', () => ({
|
||||
useFindManyRecords: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('useEvent', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('fetches events correctly for a given targetableObject', () => {
|
||||
const mockEvents = [
|
||||
{
|
||||
__typename: 'Event',
|
||||
id: '166ec73f-26b1-4934-bb3b-c86c8513b99b',
|
||||
opportunityId: null,
|
||||
opportunity: null,
|
||||
personId: null,
|
||||
person: null,
|
||||
company: {
|
||||
__typename: 'Company',
|
||||
address: 'Paris',
|
||||
linkedinLink: {
|
||||
__typename: 'Link',
|
||||
label: '',
|
||||
url: '',
|
||||
},
|
||||
xLink: {
|
||||
__typename: 'Link',
|
||||
label: '',
|
||||
url: '',
|
||||
},
|
||||
position: 4,
|
||||
domainName: 'microsoft.com',
|
||||
employees: null,
|
||||
createdAt: '2024-03-21T16:01:41.809Z',
|
||||
annualRecurringRevenue: {
|
||||
__typename: 'Currency',
|
||||
amountMicros: 100000000,
|
||||
currencyCode: 'USD',
|
||||
},
|
||||
idealCustomerProfile: false,
|
||||
accountOwnerId: null,
|
||||
updatedAt: '2024-03-22T08:28:44.812Z',
|
||||
name: 'Microsoft',
|
||||
id: '460b6fb1-ed89-413a-b31a-962986e67bb4',
|
||||
},
|
||||
workspaceMember: {
|
||||
__typename: 'WorkspaceMember',
|
||||
locale: 'en',
|
||||
avatarUrl: '',
|
||||
updatedAt: '2024-03-21T16:01:41.839Z',
|
||||
name: {
|
||||
__typename: 'FullName',
|
||||
firstName: 'Tim',
|
||||
lastName: 'Apple',
|
||||
},
|
||||
id: '20202020-0687-4c41-b707-ed1bfca972a7',
|
||||
userEmail: 'tim@apple.dev',
|
||||
colorScheme: 'Light',
|
||||
createdAt: '2024-03-21T16:01:41.839Z',
|
||||
userId: '20202020-9e3b-46d4-a556-88b9ddc2b034',
|
||||
},
|
||||
workspaceMemberId: '20202020-0687-4c41-b707-ed1bfca972a7',
|
||||
createdAt: '2024-03-22T08:28:44.830Z',
|
||||
name: 'updated.company',
|
||||
companyId: '460b6fb1-ed89-413a-b31a-962986e67bb4',
|
||||
properties: '{"diff": {"address": {"after": "Paris", "before": ""}}}',
|
||||
updatedAt: '2024-03-22T08:28:44.830Z',
|
||||
},
|
||||
];
|
||||
const mockTargetableObject = {
|
||||
id: '1',
|
||||
targetObjectNameSingular: 'Opportunity',
|
||||
};
|
||||
|
||||
const useFindManyRecordsMock = jest.requireMock(
|
||||
'@/object-record/hooks/useFindManyRecords',
|
||||
);
|
||||
useFindManyRecordsMock.useFindManyRecords.mockReturnValue({
|
||||
records: mockEvents,
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useEvents(mockTargetableObject));
|
||||
|
||||
expect(result.current.events).toEqual(mockEvents);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,28 @@
|
||||
import { Event } from '@/activities/events/types/Event';
|
||||
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
||||
import { getActivityTargetObjectFieldIdName } from '@/activities/utils/getTargetObjectFilterFieldName';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
||||
|
||||
// do we need to test this?
|
||||
export const useEvents = (targetableObject: ActivityTargetableObject) => {
|
||||
const targetableObjectFieldIdName = getActivityTargetObjectFieldIdName({
|
||||
nameSingular: targetableObject.targetObjectNameSingular,
|
||||
});
|
||||
|
||||
const { records: events } = useFindManyRecords({
|
||||
objectNameSingular: CoreObjectNameSingular.Event,
|
||||
filter: {
|
||||
[targetableObjectFieldIdName]: {
|
||||
eq: targetableObject.id,
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'DescNullsFirst',
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
events: events as Event[],
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,12 @@
|
||||
export type Event = {
|
||||
id: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
deletedAt: string | null;
|
||||
opportunityId: string | null;
|
||||
companyId: string;
|
||||
personId: string;
|
||||
workspaceMemberId: string;
|
||||
properties: any;
|
||||
name: string;
|
||||
};
|
||||
@ -8,6 +8,7 @@ export enum CoreObjectNameSingular {
|
||||
Comment = 'comment',
|
||||
Company = 'company',
|
||||
ConnectedAccount = 'connectedAccount',
|
||||
Event = 'event',
|
||||
Favorite = 'favorite',
|
||||
Message = 'message',
|
||||
MessageChannel = 'messageChannel',
|
||||
|
||||
@ -34,6 +34,7 @@ export const mapFieldMetadataToGraphQLQuery = ({
|
||||
'RATING',
|
||||
'SELECT',
|
||||
'POSITION',
|
||||
'RAW_JSON',
|
||||
] as FieldMetadataType[]
|
||||
).includes(fieldType);
|
||||
|
||||
|
||||
@ -69,6 +69,11 @@ export type FieldRatingMetadata = {
|
||||
fieldName: string;
|
||||
};
|
||||
|
||||
export type FieldRawJsonMetadata = {
|
||||
objectMetadataNameSingular?: string;
|
||||
fieldName: string;
|
||||
};
|
||||
|
||||
export type FieldDefinitionRelationType =
|
||||
| 'FROM_MANY_OBJECTS'
|
||||
| 'FROM_ONE_OBJECT'
|
||||
|
||||
@ -17,4 +17,5 @@ export type FieldType =
|
||||
| 'URL'
|
||||
| 'UUID'
|
||||
| 'MULTI_SELECT'
|
||||
| 'NUMERIC';
|
||||
| 'NUMERIC'
|
||||
| 'RAW_JSON';
|
||||
|
||||
@ -10,6 +10,7 @@ import {
|
||||
FieldNumberMetadata,
|
||||
FieldPhoneMetadata,
|
||||
FieldRatingMetadata,
|
||||
FieldRawJsonMetadata,
|
||||
FieldRelationMetadata,
|
||||
FieldSelectMetadata,
|
||||
FieldTextMetadata,
|
||||
@ -47,7 +48,9 @@ type AssertFieldMetadataFunction = <
|
||||
? FieldTextMetadata
|
||||
: E extends 'UUID'
|
||||
? FieldUuidMetadata
|
||||
: never,
|
||||
: E extends 'RAW_JSON'
|
||||
? FieldRawJsonMetadata
|
||||
: never,
|
||||
>(
|
||||
fieldType: E,
|
||||
fieldTypeGuard: (
|
||||
|
||||
@ -0,0 +1,6 @@
|
||||
import { FieldDefinition } from '../FieldDefinition';
|
||||
import { FieldMetadata, FieldRawJsonMetadata } from '../FieldMetadata';
|
||||
|
||||
export const isFieldRawJson = (
|
||||
field: Pick<FieldDefinition<FieldMetadata>, 'type'>,
|
||||
): field is FieldDefinition<FieldRawJsonMetadata> => field.type === 'RAW_JSON';
|
||||
@ -3,6 +3,7 @@ import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { Calendar } from '@/activities/calendar/components/Calendar';
|
||||
import { EmailThreads } from '@/activities/emails/components/EmailThreads';
|
||||
import { Events } from '@/activities/events/components/Events';
|
||||
import { Attachments } from '@/activities/files/components/Attachments';
|
||||
import { Notes } from '@/activities/notes/components/Notes';
|
||||
import { ObjectTasks } from '@/activities/tasks/components/ObjectTasks';
|
||||
@ -65,6 +66,8 @@ export const ShowPageRightContainer = ({
|
||||
const activeTabId = useRecoilValue(activeTabIdState);
|
||||
|
||||
const shouldDisplayCalendarTab = useIsFeatureEnabled('IS_CALENDAR_ENABLED');
|
||||
const shouldDisplayLogTab = useIsFeatureEnabled('IS_EVENT_OBJECT_ENABLED');
|
||||
|
||||
const shouldDisplayEmailsTab =
|
||||
(emails &&
|
||||
targetableObject.targetObjectNameSingular ===
|
||||
@ -101,7 +104,6 @@ export const ShowPageRightContainer = ({
|
||||
title: 'Emails',
|
||||
Icon: IconMail,
|
||||
hide: !shouldDisplayEmailsTab,
|
||||
hasBetaPill: true,
|
||||
},
|
||||
{
|
||||
id: 'calendar',
|
||||
@ -109,6 +111,13 @@ export const ShowPageRightContainer = ({
|
||||
Icon: IconCalendarEvent,
|
||||
hide: !shouldDisplayCalendarTab,
|
||||
},
|
||||
{
|
||||
id: 'logs',
|
||||
title: 'Logs',
|
||||
Icon: IconTimelineEvent,
|
||||
hide: !shouldDisplayLogTab,
|
||||
hasBetaPill: true,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
@ -131,6 +140,7 @@ export const ShowPageRightContainer = ({
|
||||
)}
|
||||
{activeTabId === 'emails' && <EmailThreads entity={targetableObject} />}
|
||||
{activeTabId === 'calendar' && <Calendar />}
|
||||
{activeTabId === 'logs' && <Events targetableObject={targetableObject} />}
|
||||
</StyledShowPageRightContainer>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user