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:
Félix Malfait
2024-03-22 14:01:16 +01:00
committed by GitHub
parent aee6d49ea9
commit d876b40056
38 changed files with 488 additions and 95 deletions

View File

@ -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} />)}
</>
);
};

View File

@ -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>
</>
);
};

View File

@ -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 ?? []}
/>
);
};

View File

@ -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);
});
});

View File

@ -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[],
};
};

View File

@ -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;
};

View File

@ -8,6 +8,7 @@ export enum CoreObjectNameSingular {
Comment = 'comment',
Company = 'company',
ConnectedAccount = 'connectedAccount',
Event = 'event',
Favorite = 'favorite',
Message = 'message',
MessageChannel = 'messageChannel',

View File

@ -34,6 +34,7 @@ export const mapFieldMetadataToGraphQLQuery = ({
'RATING',
'SELECT',
'POSITION',
'RAW_JSON',
] as FieldMetadataType[]
).includes(fieldType);

View File

@ -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'

View File

@ -17,4 +17,5 @@ export type FieldType =
| 'URL'
| 'UUID'
| 'MULTI_SELECT'
| 'NUMERIC';
| 'NUMERIC'
| 'RAW_JSON';

View File

@ -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: (

View File

@ -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';

View File

@ -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>
);
};