Skip to content

Commit

Permalink
Logs show page (#4611)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>

* Move db calls to a dedicated repository

* Add server-side tests

---------

Co-authored-by: Weiko <[email protected]>
  • Loading branch information
FelixMalfait and Weiko authored Mar 22, 2024
1 parent aee6d49 commit d876b40
Show file tree
Hide file tree
Showing 38 changed files with 488 additions and 95 deletions.
67 changes: 65 additions & 2 deletions packages/twenty-front/src/generated-metadata/graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ export type Billing = {
export type BillingSubscription = {
__typename?: 'BillingSubscription';
id: Scalars['ID']['output'];
interval?: Maybe<Scalars['String']['output']>;
status: Scalars['String']['output'];
};

Expand Down Expand Up @@ -263,7 +264,6 @@ export enum FieldMetadataType {
DateTime = 'DATE_TIME',
Email = 'EMAIL',
FullName = 'FULL_NAME',
Json = 'JSON',
Link = 'LINK',
MultiSelect = 'MULTI_SELECT',
Number = 'NUMBER',
Expand All @@ -272,6 +272,7 @@ export enum FieldMetadataType {
Position = 'POSITION',
Probability = 'PROBABILITY',
Rating = 'RATING',
RawJson = 'RAW_JSON',
Relation = 'RELATION',
Select = 'SELECT',
Text = 'TEXT',
Expand Down Expand Up @@ -341,6 +342,7 @@ export type Mutation = {
renewToken: AuthTokens;
signUp: LoginToken;
track: Analytics;
updateBillingSubscription: UpdateBillingEntity;
updateOneField: Field;
updateOneObject: Object;
updatePasswordViaResetToken: InvalidatePassword;
Expand Down Expand Up @@ -545,6 +547,8 @@ export type Query = {
fields: FieldConnection;
findWorkspaceFromInviteHash: Workspace;
getProductPrices: ProductPricesEntity;
getTimelineCalendarEventsFromCompanyId: TimelineCalendarEventsWithTotal;
getTimelineCalendarEventsFromPersonId: TimelineCalendarEventsWithTotal;
getTimelineThreadsFromCompanyId: TimelineThreadsWithTotal;
getTimelineThreadsFromPersonId: TimelineThreadsWithTotal;
object: Object;
Expand Down Expand Up @@ -591,6 +595,20 @@ export type QueryGetProductPricesArgs = {
};


export type QueryGetTimelineCalendarEventsFromCompanyIdArgs = {
companyId: Scalars['ID']['input'];
page: Scalars['Int']['input'];
pageSize: Scalars['Int']['input'];
};


export type QueryGetTimelineCalendarEventsFromPersonIdArgs = {
page: Scalars['Int']['input'];
pageSize: Scalars['Int']['input'];
personId: Scalars['ID']['input'];
};


export type QueryGetTimelineThreadsFromCompanyIdArgs = {
companyId: Scalars['ID']['input'];
page: Scalars['Int']['input'];
Expand Down Expand Up @@ -697,7 +715,7 @@ export type Sentry = {

export type SessionEntity = {
__typename?: 'SessionEntity';
url: Scalars['String']['output'];
url?: Maybe<Scalars['String']['output']>;
};

/** Sort Directions */
Expand All @@ -724,6 +742,45 @@ export type Telemetry = {
enabled: Scalars['Boolean']['output'];
};

export type TimelineCalendarEvent = {
__typename?: 'TimelineCalendarEvent';
attendees: Array<TimelineCalendarEventAttendee>;
conferenceSolution: Scalars['String']['output'];
conferenceUri: Scalars['String']['output'];
description: Scalars['String']['output'];
endsAt: Scalars['DateTime']['output'];
id: Scalars['ID']['output'];
isCanceled: Scalars['Boolean']['output'];
isFullDay: Scalars['Boolean']['output'];
location: Scalars['String']['output'];
startsAt: Scalars['DateTime']['output'];
title: Scalars['String']['output'];
visibility: TimelineCalendarEventVisibility;
};

export type TimelineCalendarEventAttendee = {
__typename?: 'TimelineCalendarEventAttendee';
avatarUrl: Scalars['String']['output'];
displayName: Scalars['String']['output'];
firstName: Scalars['String']['output'];
handle: Scalars['String']['output'];
lastName: Scalars['String']['output'];
personId?: Maybe<Scalars['ID']['output']>;
workspaceMemberId?: Maybe<Scalars['ID']['output']>;
};

/** Visibility of the calendar event */
export enum TimelineCalendarEventVisibility {
Metadata = 'METADATA',
ShareEverything = 'SHARE_EVERYTHING'
}

export type TimelineCalendarEventsWithTotal = {
__typename?: 'TimelineCalendarEventsWithTotal';
timelineCalendarEvents: Array<TimelineCalendarEvent>;
totalNumberOfCalendarEvents: Scalars['Int']['output'];
};

export type TimelineThread = {
__typename?: 'TimelineThread';
firstParticipant: TimelineThreadParticipant;
Expand Down Expand Up @@ -760,6 +817,12 @@ export type TransientToken = {
transientToken: AuthToken;
};

export type UpdateBillingEntity = {
__typename?: 'UpdateBillingEntity';
/** Boolean that confirms query was successful */
success: Scalars['Boolean']['output'];
};

export type UpdateFieldInput = {
defaultValue?: InputMaybe<Scalars['JSON']['input']>;
description?: InputMaybe<Scalars['String']['input']>;
Expand Down
46 changes: 1 addition & 45 deletions packages/twenty-front/src/generated/graphql.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,6 @@ export enum FieldMetadataType {
DateTime = 'DATE_TIME',
Email = 'EMAIL',
FullName = 'FULL_NAME',
Json = 'JSON',
Link = 'LINK',
MultiSelect = 'MULTI_SELECT',
Number = 'NUMBER',
Expand All @@ -193,6 +192,7 @@ export enum FieldMetadataType {
Position = 'POSITION',
Probability = 'PROBABILITY',
Rating = 'RATING',
RawJson = 'RAW_JSON',
Relation = 'RELATION',
Select = 'SELECT',
Text = 'TEXT',
Expand Down Expand Up @@ -255,7 +255,6 @@ export type Mutation = {
generateJWT: AuthTokens;
generateTransientToken: TransientToken;
impersonate: Verify;
removeWorkspaceMember: Scalars['String'];
renewToken: AuthTokens;
signUp: LoginToken;
track: Analytics;
Expand Down Expand Up @@ -314,11 +313,6 @@ export type MutationImpersonateArgs = {
};


export type MutationRemoveWorkspaceMemberArgs = {
memberId: Scalars['String'];
};


export type MutationRenewTokenArgs = {
refreshToken: Scalars['String'];
};
Expand Down Expand Up @@ -1098,13 +1092,6 @@ export type GetCurrentUserQueryVariables = Exact<{ [key: string]: never; }>;

export type GetCurrentUserQuery = { __typename?: 'Query', currentUser: { __typename?: 'User', id: string, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: string, colorScheme: string, avatarUrl?: string | null, locale: string, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, defaultWorkspace: { __typename?: 'Workspace', id: string, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, subscriptionStatus: string, activationStatus: string, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: string, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', status: string, interval?: string | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: string, displayName?: string | null, logo?: string | null, domainName?: string | null } | null }> } };

export type RemoveWorkspaceMemberMutationVariables = Exact<{
memberId: Scalars['String'];
}>;


export type RemoveWorkspaceMemberMutation = { __typename?: 'Mutation', removeWorkspaceMember: string };

export type ActivateWorkspaceMutationVariables = Exact<{
input: ActivateWorkspaceInput;
}>;
Expand Down Expand Up @@ -2311,37 +2298,6 @@ export function useGetCurrentUserLazyQuery(baseOptions?: Apollo.LazyQueryHookOpt
export type GetCurrentUserQueryHookResult = ReturnType<typeof useGetCurrentUserQuery>;
export type GetCurrentUserLazyQueryHookResult = ReturnType<typeof useGetCurrentUserLazyQuery>;
export type GetCurrentUserQueryResult = Apollo.QueryResult<GetCurrentUserQuery, GetCurrentUserQueryVariables>;
export const RemoveWorkspaceMemberDocument = gql`
mutation RemoveWorkspaceMember($memberId: String!) {
removeWorkspaceMember(memberId: $memberId)
}
`;
export type RemoveWorkspaceMemberMutationFn = Apollo.MutationFunction<RemoveWorkspaceMemberMutation, RemoveWorkspaceMemberMutationVariables>;

/**
* __useRemoveWorkspaceMemberMutation__
*
* To run a mutation, you first call `useRemoveWorkspaceMemberMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useRemoveWorkspaceMemberMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [removeWorkspaceMemberMutation, { data, loading, error }] = useRemoveWorkspaceMemberMutation({
* variables: {
* memberId: // value for 'memberId'
* },
* });
*/
export function useRemoveWorkspaceMemberMutation(baseOptions?: Apollo.MutationHookOptions<RemoveWorkspaceMemberMutation, RemoveWorkspaceMemberMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<RemoveWorkspaceMemberMutation, RemoveWorkspaceMemberMutationVariables>(RemoveWorkspaceMemberDocument, options);
}
export type RemoveWorkspaceMemberMutationHookResult = ReturnType<typeof useRemoveWorkspaceMemberMutation>;
export type RemoveWorkspaceMemberMutationResult = Apollo.MutationResult<RemoveWorkspaceMemberMutation>;
export type RemoveWorkspaceMemberMutationOptions = Apollo.BaseMutationOptions<RemoveWorkspaceMemberMutation, RemoveWorkspaceMemberMutationVariables>;
export const ActivateWorkspaceDocument = gql`
mutation ActivateWorkspace($input: ActivateWorkspaceInput!) {
activateWorkspace(data: $input) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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} />)}
</>
);
};
Original file line number Diff line number Diff line change
@@ -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>
</>
);
};
Original file line number Diff line number Diff line change
@@ -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 ?? []}
/>
);
};
Original file line number Diff line number Diff line change
@@ -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: '[email protected]',
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);
});
});
Original file line number Diff line number Diff line change
@@ -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[],
};
};
Loading

0 comments on commit d876b40

Please sign in to comment.