Activity as standard object (#6219)

In this PR I layout the first steps to migrate Activity to a traditional
Standard objects

Since this is a big transition, I'd rather split it into several
deployments / PRs

<img width="1512" alt="image"
src="https://github.com/user-attachments/assets/012e2bbf-9d1b-4723-aaf6-269ef588b050">

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
Co-authored-by: bosiraphael <71827178+bosiraphael@users.noreply.github.com>
Co-authored-by: Weiko <corentin@twenty.com>
Co-authored-by: Faisal-imtiyaz123 <142205282+Faisal-imtiyaz123@users.noreply.github.com>
Co-authored-by: Prateek Jain <prateekj1171998@gmail.com>
This commit is contained in:
Félix Malfait
2024-07-31 15:36:11 +02:00
committed by GitHub
parent defcee2a02
commit 80c0fc7ff1
239 changed files with 18418 additions and 8671 deletions

View File

@ -6,6 +6,7 @@ import { RecoilRoot, useSetRecoilState } from 'recoil';
import { useActivities } from '@/activities/hooks/useActivities';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope';
import { mockWorkspaceMembers } from '~/testing/mock-data/workspace-members';
@ -20,16 +21,14 @@ const mockActivityTarget = {
};
const mockActivity = {
__typename: 'Activity',
__typename: 'Note',
updatedAt: '2021-08-03T19:20:06.000Z',
createdAt: '2021-08-03T19:20:06.000Z',
completedAt: '2021-08-03T19:20:06.000Z',
status: 'DONE',
reminderAt: '2021-08-03T19:20:06.000Z',
title: 'title',
authorId: '1',
body: 'body',
dueAt: '2021-08-03T19:20:06.000Z',
type: 'type',
assigneeId: '1',
id: '234',
};
@ -102,13 +101,13 @@ const mocks: MockedResponse[] = [
{
request: {
query: gql`
query FindManyActivities(
$filter: ActivityFilterInput
$orderBy: [ActivityOrderByInput]
query FindManyTasks(
$filter: TaskFilterInput
$orderBy: [TaskOrderByInput]
$lastCursor: String
$limit: Int
) {
activities(
tasks(
filter: $filter
orderBy: $orderBy
first: $limit
@ -117,17 +116,13 @@ const mocks: MockedResponse[] = [
edges {
node {
__typename
createdAt
reminderAt
authorId
title
completedAt
updatedAt
body
dueAt
type
id
assigneeId
updatedAt
createdAt
body
status
dueAt
}
cursor
}
@ -178,6 +173,7 @@ describe('useActivities', () => {
const { result } = renderHook(
() =>
useActivities({
objectNameSingular: CoreObjectNameSingular.Task,
targetableObjects: [],
activitiesFilters: {},
activitiesOrderByVariables: [{}],
@ -200,6 +196,7 @@ describe('useActivities', () => {
);
const activities = useActivities({
objectNameSingular: CoreObjectNameSingular.Task,
targetableObjects: [
{ targetObjectNameSingular: 'company', id: '123' },
],

View File

@ -7,8 +7,8 @@ import { RecoilRoot, useSetRecoilState } from 'recoil';
import { useActivityTargetObjectRecords } from '@/activities/hooks/useActivityTargetObjectRecords';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { getObjectMetadataItemsMock } from '@/object-metadata/utils/getObjectMetadataItemsMock';
import { getRecordFromRecordNode } from '@/object-record/cache/utils/getRecordFromRecordNode';
import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope';
import { JestObjectMetadataItemSetter } from '~/testing/jest/JestObjectMetadataItemSetter';
import { mockWorkspaceMembers } from '~/testing/mock-data/workspace-members';
@ -16,108 +16,95 @@ const mockObjectMetadataItems = getObjectMetadataItemsMock();
const cache = new InMemoryCache();
const activityNode = {
id: '3ecaa1be-aac7-463a-a38e-64078dd451d5',
const taskTarget = {
id: '89bb825c-171e-4bcc-9cf7-43448d6fb300',
createdAt: '2023-04-26T10:12:42.33625+00:00',
updatedAt: '2023-04-26T10:23:42.33625+00:00',
reminderAt: null,
title: 'My very first note',
type: 'Note',
body: '',
dueAt: '2023-04-26T10:12:42.33625+00:00',
completedAt: null,
author: null,
assignee: null,
assigneeId: null,
authorId: null,
comments: {
edges: [],
companyId: null,
company: null,
personId: '89bb825c-171e-4bcc-9cf7-43448d6fb280',
person: {
id: '89bb825c-171e-4bcc-9cf7-43448d6fb280',
createdAt: '2023-04-26T10:12:42.33625+00:00',
updatedAt: '2023-04-26T10:23:42.33625+00:00',
city: 'City',
name: {
firstName: 'John',
lastName: 'Doe',
},
__typename: 'Person',
},
activityTargets: {
edges: [
{
node: {
id: '89bb825c-171e-4bcc-9cf7-43448d6fb300',
createdAt: '2023-04-26T10:12:42.33625+00:00',
updatedAt: '2023-04-26T10:23:42.33625+00:00',
personId: null,
companyId: '89bb825c-171e-4bcc-9cf7-43448d6fb280',
company: {
id: '89bb825c-171e-4bcc-9cf7-43448d6fb280',
name: 'Airbnb',
domainName: {
primaryLinkUrl: 'https://www.airbnb.com',
primaryLinkLabel: '',
secondaryLinks: null,
},
},
person: null,
activityId: '89bb825c-171e-4bcc-9cf7-43448d6fb230',
activity: {
id: '89bb825c-171e-4bcc-9cf7-43448d6fb230',
createdAt: '2023-04-26T10:12:42.33625+00:00',
updatedAt: '2023-04-26T10:23:42.33625+00:00',
},
__typename: 'ActivityTarget',
},
__typename: 'ActivityTargetEdge',
},
],
__typename: 'ActivityTargetConnection',
taskId: '89bb825c-171e-4bcc-9cf7-43448d6fb230',
task: {
id: '89bb825c-171e-4bcc-9cf7-43448d6fb230',
createdAt: '2023-04-26T10:12:42.33625+00:00',
updatedAt: '2023-04-26T10:23:42.33625+00:00',
dueAt: null,
body: '{}',
title: 'Task title',
assigneeId: null,
__typename: 'Task',
},
__typename: 'Activity' as const,
__typename: 'TaskTarget',
};
cache.writeFragment({
fragment: gql`
fragment CreateOneActivityInCache on Activity {
id
createdAt
fragment TaskTargetFragment on TaskTarget {
__typename
updatedAt
reminderAt
title
body
dueAt
completedAt
author
assignee
assigneeId
authorId
activityTargets {
edges {
node {
id
createdAt
updatedAt
personId
companyId
company {
id
name
domainName {
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
}
person
activityId
activity {
id
createdAt
updatedAt
}
__typename
}
createdAt
personId
taskId
companyId
id
task {
__typename
createdAt
title
updatedAt
body
dueAt
id
assigneeId
}
person {
__typename
id
createdAt
updatedAt
city
name {
firstName
lastName
}
}
__typename
company {
__typename
id
createdAt
updatedAt
}
}
`,
id: activityNode.id,
data: activityNode,
id: `TaskTarget:${taskTarget.id}`,
data: taskTarget,
});
const task = {
id: '89bb825c-171e-4bcc-9cf7-43448d6fb230',
createdAt: '2023-04-26T10:12:42.33625+00:00',
updatedAt: '2023-04-26T10:23:42.33625+00:00',
title: 'Task title',
body: null,
assigneeId: null,
status: null,
dueAt: '2023-04-26T10:12:42.33625+00:00',
assignee: null,
__typename: 'Task' as any,
taskTargets: [taskTarget],
};
const Wrapper = ({ children }: { children: ReactNode }) => (
<RecoilRoot>
<MockedProvider cache={cache}>
@ -142,7 +129,8 @@ describe('useActivityTargetObjectRecords', () => {
);
const { activityTargetObjectRecords } = useActivityTargetObjectRecords(
getRecordFromRecordNode({ recordNode: activityNode as any }),
task,
CoreObjectNameSingular.Task,
);
return {
@ -158,18 +146,17 @@ describe('useActivityTargetObjectRecords', () => {
result.current.setCurrentWorkspaceMember(mockWorkspaceMembers[0]);
result.current.setObjectMetadataItems(mockObjectMetadataItems);
});
const activityTargetObjectRecords =
result.current.activityTargetObjectRecords;
expect(activityTargetObjectRecords).toHaveLength(1);
expect(activityTargetObjectRecords[0].activityTarget).toEqual(
activityNode.activityTargets.edges[0].node,
);
expect(activityTargetObjectRecords[0].activityTarget).toEqual(taskTarget);
expect(activityTargetObjectRecords[0].targetObject).toEqual(
activityNode.activityTargets.edges[0].node.company,
taskTarget.person,
);
expect(
activityTargetObjectRecords[0].targetObjectMetadataItem.nameSingular,
).toEqual('company');
).toEqual('person');
});
});

View File

@ -1,131 +0,0 @@
import { MockedProvider, MockedResponse } from '@apollo/client/testing';
import { act, renderHook, waitFor } from '@testing-library/react';
import gql from 'graphql-tag';
import { ReactNode } from 'react';
import { RecoilRoot, useSetRecoilState } from 'recoil';
import { useActivityTargetsForTargetableObject } from '@/activities/hooks/useActivityTargetsForTargetableObject';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope';
import { mockWorkspaceMembers } from '~/testing/mock-data/workspace-members';
const mockActivityTarget = {
__typename: 'ActivityTarget',
updatedAt: '2021-08-03T19:20:06.000Z',
createdAt: '2021-08-03T19:20:06.000Z',
personId: '1',
activityId: '234',
companyId: '1',
id: '123',
};
const defaultResponseData = {
pageInfo: {
hasNextPage: false,
startCursor: '',
endCursor: '',
},
totalCount: 1,
};
const mocks: MockedResponse[] = [
{
request: {
query: gql`
query FindManyActivityTargets(
$filter: ActivityTargetFilterInput
$orderBy: [ActivityTargetOrderByInput]
$lastCursor: String
$limit: Int
) {
activityTargets(
filter: $filter
orderBy: $orderBy
first: $limit
after: $lastCursor
) {
edges {
node {
__typename
updatedAt
createdAt
personId
activityId
companyId
id
}
cursor
}
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
totalCount
}
}
`,
variables: {
filter: { personId: { eq: '1234' } },
orderBy: undefined,
lastCursor: undefined,
limit: undefined,
},
},
result: jest.fn(() => ({
data: {
activityTargets: {
...defaultResponseData,
edges: [
{
node: mockActivityTarget,
cursor: '1',
},
],
},
},
})),
},
];
const Wrapper = ({ children }: { children: ReactNode }) => (
<RecoilRoot>
<MockedProvider mocks={mocks} addTypename={false}>
<SnackBarProviderScope snackBarManagerScopeId="snack-bar-manager">
{children}
</SnackBarProviderScope>
</MockedProvider>
</RecoilRoot>
);
describe('useActivityTargetsForTargetableObject', () => {
it('works as expected', async () => {
const { result } = renderHook(
() => {
const setCurrentWorkspaceMember = useSetRecoilState(
currentWorkspaceMemberState,
);
const res = useActivityTargetsForTargetableObject({
targetableObject: {
id: '1234',
targetObjectNameSingular: 'person',
},
});
return { ...res, setCurrentWorkspaceMember };
},
{ wrapper: Wrapper },
);
act(() => {
result.current.setCurrentWorkspaceMember(mockWorkspaceMembers[0]);
});
expect(result.current.loadingActivityTargets).toBe(true);
await waitFor(() => !result.current.loadingActivityTargets);
expect(result.current.activityTargets).toEqual([mockActivityTarget]);
});
});

View File

@ -1,106 +0,0 @@
import { ReactNode } from 'react';
import { MockedProvider, MockedResponse } from '@apollo/client/testing';
import { act, renderHook } from '@testing-library/react';
import gql from 'graphql-tag';
import { RecoilRoot, useSetRecoilState } from 'recoil';
import { useCreateActivityInCache } from '@/activities/hooks/useCreateActivityInCache';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { getObjectMetadataItemsMock } from '@/object-metadata/utils/getObjectMetadataItemsMock';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope';
import { mockWorkspaceMembers } from '~/testing/mock-data/workspace-members';
const mocks: MockedResponse[] = [
{
request: {
query: gql`
query FindOneWorkspaceMember($objectRecordId: ID!) {
workspaceMember(filter: { id: { eq: $objectRecordId } }) {
__typename
colorScheme
name {
firstName
lastName
}
locale
userId
avatarUrl
createdAt
updatedAt
id
}
}
`,
variables: { objectRecordId: '20202020-1553-45c6-a028-5a9064cce07f' },
},
result: jest.fn(() => ({
data: {
workspaceMember: mockWorkspaceMembers[0],
},
})),
},
];
const Wrapper = ({ children }: { children: ReactNode }) => (
<RecoilRoot>
<MockedProvider mocks={mocks} addTypename={false}>
<SnackBarProviderScope snackBarManagerScopeId="snack-bar-manager">
{children}
</SnackBarProviderScope>
</MockedProvider>
</RecoilRoot>
);
const mockObjectMetadataItems = getObjectMetadataItemsMock();
describe('useCreateActivityInCache', () => {
it('Should create activity in cache', async () => {
const { result } = renderHook(
() => {
const setCurrentWorkspaceMember = useSetRecoilState(
currentWorkspaceMemberState,
);
const setObjectMetadataItems = useSetRecoilState(
objectMetadataItemsState,
);
const setRecordStore = useSetRecoilState(
recordStoreFamilyState('1234'),
);
const res = useCreateActivityInCache();
return {
...res,
setCurrentWorkspaceMember,
setObjectMetadataItems,
setRecordStore,
};
},
{ wrapper: Wrapper },
);
act(() => {
result.current.setRecordStore({
id: '1234',
__typename: 'Person',
});
result.current.setCurrentWorkspaceMember(mockWorkspaceMembers[0]);
result.current.setObjectMetadataItems(mockObjectMetadataItems);
});
act(() => {
const res = result.current.createActivityInCache({
type: 'Note',
targetObject: {
targetObjectNameSingular: 'person',
id: '1234',
},
});
expect(res.createdActivityInCache).toHaveProperty('id');
expect(res.createdActivityInCache).toHaveProperty('__typename');
expect(res.createdActivityInCache).toHaveProperty('activityTargets');
});
});
});

View File

@ -6,22 +6,16 @@ import { ReactNode } from 'react';
import { RecoilRoot } from 'recoil';
import { useCreateActivityInDB } from '@/activities/hooks/useCreateActivityInDB';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope';
import { mockedActivities } from '~/testing/mock-data/activities';
import { mockedTasks } from '~/testing/mock-data/tasks';
const mockedDate = '2024-03-15T12:00:00.000Z';
const toISOStringMock = jest.fn(() => mockedDate);
global.Date.prototype.toISOString = toISOStringMock;
const mockedActivity = {
...pick(mockedActivities[0], [
'id',
'title',
'body',
'type',
'completedAt',
'dueAt',
]),
...pick(mockedTasks[0], ['id', 'title', 'body', 'type', 'status', 'dueAt']),
updatedAt: mockedDate,
};
@ -36,7 +30,7 @@ const mocks: MockedResponse[] = [
reminderAt
authorId
title
completedAt
status
updatedAt
body
dueAt
@ -77,14 +71,19 @@ const Wrapper = ({ children }: { children: ReactNode }) => (
describe('useCreateActivityInDB', () => {
it('Should create activity in DB', async () => {
const { result } = renderHook(() => useCreateActivityInDB(), {
wrapper: Wrapper,
});
const { result } = renderHook(
() =>
useCreateActivityInDB({
activityObjectNameSingular: CoreObjectNameSingular.Task,
}),
{
wrapper: Wrapper,
},
);
await act(async () => {
await result.current.createActivityInDB({
...mockedActivity,
__typename: 'Activity',
});
});

View File

@ -1,10 +1,10 @@
import { ReactNode } from 'react';
import { MockedProvider } from '@apollo/client/testing';
import { act, renderHook } from '@testing-library/react';
import { ReactNode } from 'react';
import { RecoilRoot, useRecoilValue } from 'recoil';
import { useOpenActivityRightDrawer } from '@/activities/hooks/useOpenActivityRightDrawer';
import { activityIdInDrawerState } from '@/activities/states/activityIdInDrawerState';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState';
const Wrapper = ({ children }: { children: ReactNode }) => (
@ -17,12 +17,12 @@ describe('useOpenActivityRightDrawer', () => {
it('works as expected', () => {
const { result } = renderHook(
() => {
const openActivityRightDrawer = useOpenActivityRightDrawer();
const openActivityRightDrawer = useOpenActivityRightDrawer({
objectNameSingular: CoreObjectNameSingular.Task,
});
const viewableRecordId = useRecoilValue(viewableRecordIdState);
const activityIdInDrawer = useRecoilValue(activityIdInDrawerState);
return {
openActivityRightDrawer,
activityIdInDrawer,
viewableRecordId,
};
},
@ -31,12 +31,10 @@ describe('useOpenActivityRightDrawer', () => {
},
);
expect(result.current.activityIdInDrawer).toBeNull();
expect(result.current.viewableRecordId).toBeNull();
act(() => {
result.current.openActivityRightDrawer('123');
});
expect(result.current.activityIdInDrawer).toBe('123');
expect(result.current.viewableRecordId).toBe('123');
});
});

View File

@ -1,23 +1,71 @@
import { ReactNode } from 'react';
import { MockedProvider } from '@apollo/client/testing';
import { MockedProvider, MockedResponse } from '@apollo/client/testing';
import { act, renderHook } from '@testing-library/react';
import { ReactNode } from 'react';
import { RecoilRoot, useRecoilValue, useSetRecoilState } from 'recoil';
import { useOpenCreateActivityDrawer } from '@/activities/hooks/useOpenCreateActivityDrawer';
import { activityIdInDrawerState } from '@/activities/states/activityIdInDrawerState';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { getObjectMetadataItemsMock } from '@/object-metadata/utils/getObjectMetadataItemsMock';
import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState';
import gql from 'graphql-tag';
import pick from 'lodash.pick';
import { mockedTasks } from '~/testing/mock-data/tasks';
const mockUUID = '37873e04-2f83-4468-9ab7-3f87da6cafad';
const mockedDate = '2024-03-15T12:00:00.000Z';
const toISOStringMock = jest.fn(() => mockedDate);
global.Date.prototype.toISOString = toISOStringMock;
jest.mock('uuid', () => ({
v4: () => mockUUID,
}));
const mockedActivity = {
...pick(mockedTasks[0], ['id', 'title', 'body', 'type', 'status', 'dueAt']),
updatedAt: mockedDate,
};
const mocks: MockedResponse[] = [
{
request: {
query: gql`
mutation CreateOneActivity($input: ActivityCreateInput!) {
createActivity(data: $input) {
__typename
createdAt
reminderAt
authorId
title
status
updatedAt
body
dueAt
type
id
assigneeId
}
}
`,
variables: {
input: mockedActivity,
},
},
result: jest.fn(() => ({
data: {
createActivity: {
...mockedActivity,
__typename: 'Activity',
assigneeId: '',
authorId: '1',
reminderAt: null,
createdAt: mockedDate,
},
},
})),
},
];
const Wrapper = ({ children }: { children: ReactNode }) => (
<RecoilRoot>
<MockedProvider addTypename={false}>{children}</MockedProvider>
<MockedProvider addTypename={false} mocks={mocks}>
{children}
</MockedProvider>
</RecoilRoot>
);
@ -27,15 +75,15 @@ describe('useOpenCreateActivityDrawer', () => {
it('works as expected', async () => {
const { result } = renderHook(
() => {
const openActivityRightDrawer = useOpenCreateActivityDrawer();
const openActivityRightDrawer = useOpenCreateActivityDrawer({
activityObjectNameSingular: CoreObjectNameSingular.Note,
});
const viewableRecordId = useRecoilValue(viewableRecordIdState);
const activityIdInDrawer = useRecoilValue(activityIdInDrawerState);
const setObjectMetadataItems = useSetRecoilState(
objectMetadataItemsState,
);
return {
openActivityRightDrawer,
activityIdInDrawer,
viewableRecordId,
setObjectMetadataItems,
};
@ -49,15 +97,11 @@ describe('useOpenCreateActivityDrawer', () => {
result.current.setObjectMetadataItems(mockObjectMetadataItems);
});
expect(result.current.activityIdInDrawer).toBeNull();
expect(result.current.viewableRecordId).toBeNull();
await act(async () => {
result.current.openActivityRightDrawer({
type: 'Note',
targetableObjects: [],
});
});
expect(result.current.activityIdInDrawer).toBe(mockUUID);
expect(result.current.viewableRecordId).toBe(mockUUID);
});
});

View File

@ -3,21 +3,25 @@ import { useRecoilCallback } from 'recoil';
import { findActivitiesOperationSignatureFactory } from '@/activities/graphql/operation-signatures/factories/findActivitiesOperationSignatureFactory';
import { useActivityTargetsForTargetableObjects } from '@/activities/hooks/useActivityTargetsForTargetableObjects';
import { Activity } from '@/activities/types/Activity';
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
import { Note } from '@/activities/types/Note';
import { Task } from '@/activities/types/Task';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { RecordGqlOperationFilter } from '@/object-record/graphql/types/RecordGqlOperationFilter';
import { RecordGqlOperationOrderBy } from '@/object-record/graphql/types/RecordGqlOperationOrderBy';
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { sortByAscString } from '~/utils/array/sortByAscString';
export const useActivities = ({
export const useActivities = <T extends Task | Note>({
objectNameSingular,
targetableObjects,
activitiesFilters,
activitiesOrderByVariables,
skip,
}: {
objectNameSingular: CoreObjectNameSingular;
targetableObjects: ActivityTargetableObject[];
activitiesFilters: RecordGqlOperationFilter;
activitiesOrderByVariables: RecordGqlOperationOrderBy;
@ -27,6 +31,7 @@ export const useActivities = ({
const { activityTargets, loadingActivityTargets } =
useActivityTargetsForTargetableObjects({
objectNameSingular,
targetableObjects,
skip: skip,
});
@ -36,7 +41,10 @@ export const useActivities = ({
activityTargets
? [
...activityTargets
.map((activityTarget) => activityTarget.activityId)
.map(
(activityTarget) =>
activityTarget.taskId ?? activityTarget.noteId,
)
.filter(isNonEmptyString),
].sort(sortByAscString)
: [],
@ -54,10 +62,13 @@ export const useActivities = ({
};
const FIND_ACTIVITIES_OPERATION_SIGNATURE =
findActivitiesOperationSignatureFactory({ objectMetadataItems });
findActivitiesOperationSignatureFactory({
objectMetadataItems,
objectNameSingular,
});
const { records: activities, loading: loadingActivities } =
useFindManyRecords<Activity>({
useFindManyRecords<Task | Note>({
skip: skip || loadingActivityTargets,
objectNameSingular:
FIND_ACTIVITIES_OPERATION_SIGNATURE.objectNameSingular,
@ -76,7 +87,7 @@ export const useActivities = ({
});
return {
activities,
activities: activities as T[],
loading: loadingActivities || loadingActivityTargets,
};
};

View File

@ -2,31 +2,41 @@ import { useApolloClient } from '@apollo/client';
import { useRecoilValue } from 'recoil';
import { Nullable } from 'twenty-ui';
import { Activity } from '@/activities/types/Activity';
import { ActivityTarget } from '@/activities/types/ActivityTarget';
import { ActivityTargetWithTargetRecord } from '@/activities/types/ActivityTargetObject';
import { Note } from '@/activities/types/Note';
import { NoteTarget } from '@/activities/types/NoteTarget';
import { Task } from '@/activities/types/Task';
import { TaskTarget } from '@/activities/types/TaskTarget';
import { getJoinObjectNameSingular } from '@/activities/utils/getJoinObjectNameSingular';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordFromCache';
import { isDefined } from '~/utils/isDefined';
export const useActivityTargetObjectRecords = (activity: Activity) => {
export const useActivityTargetObjectRecords = (
activity: Task | Note,
objectNameSingular: CoreObjectNameSingular,
) => {
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
const activityTargets = activity.activityTargets ?? [];
const activityTargets =
'noteTargets' in activity && activity.noteTargets
? activity.noteTargets
: 'taskTargets' in activity && activity.taskTargets
? activity.taskTargets
: [];
const getRecordFromCache = useGetRecordFromCache({
objectNameSingular: CoreObjectNameSingular.ActivityTarget,
objectNameSingular: getJoinObjectNameSingular(objectNameSingular),
});
const apolloClient = useApolloClient();
const activityTargetObjectRecords = activityTargets
.map<Nullable<ActivityTargetWithTargetRecord>>((activityTarget) => {
const activityTargetFromCache = getRecordFromCache<ActivityTarget>(
activityTarget.id,
apolloClient.cache,
);
const activityTargetFromCache = getRecordFromCache<
NoteTarget | TaskTarget
>(activityTarget.id, apolloClient.cache);
if (!isDefined(activityTargetFromCache)) {
throw new Error(
@ -37,7 +47,9 @@ export const useActivityTargetObjectRecords = (activity: Activity) => {
const correspondingObjectMetadataItem = objectMetadataItems.find(
(objectMetadataItem) =>
isDefined(activityTargetFromCache[objectMetadataItem.nameSingular]) &&
!objectMetadataItem.isSystem,
![CoreObjectNameSingular.Note, CoreObjectNameSingular.Task].includes(
objectMetadataItem.nameSingular as CoreObjectNameSingular,
),
);
if (!correspondingObjectMetadataItem) {

View File

@ -1,40 +0,0 @@
import { isNonEmptyString } from '@sniptt/guards';
import { ActivityTarget } from '@/activities/types/ActivityTarget';
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
import { getActivityTargetObjectFieldIdName } from '@/activities/utils/getActivityTargetObjectFieldIdName';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
export const useActivityTargetsForTargetableObject = ({
targetableObject,
}: {
targetableObject: ActivityTargetableObject;
}) => {
const targetObjectFieldName = getActivityTargetObjectFieldIdName({
nameSingular: targetableObject.targetObjectNameSingular,
});
const targetableObjectId = targetableObject.id;
const skipRequest = !isNonEmptyString(targetableObjectId);
// TODO: We want to optimistically remove from this request
// If we are on a show page and we remove the current show page object corresponding activity target
// See also if we need to update useTimelineActivities
const { records: activityTargets, loading: loadingActivityTargets } =
useFindManyRecords<ActivityTarget>({
objectNameSingular: CoreObjectNameSingular.ActivityTarget,
skip: skipRequest,
filter: {
[targetObjectFieldName]: {
eq: targetableObject.id,
},
},
});
return {
activityTargets,
loadingActivityTargets,
};
};

View File

@ -1,23 +1,27 @@
import { useRecoilValue } from 'recoil';
import { findActivityTargetsOperationSignatureFactory } from '@/activities/graphql/operation-signatures/factories/findActivityTargetsOperationSignatureFactory';
import { ActivityTarget } from '@/activities/types/ActivityTarget';
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
import { NoteTarget } from '@/activities/types/NoteTarget';
import { TaskTarget } from '@/activities/types/TaskTarget';
import { getActivityTargetsFilter } from '@/activities/utils/getActivityTargetsFilter';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
export const useActivityTargetsForTargetableObjects = ({
objectNameSingular,
targetableObjects,
skip,
onCompleted,
}: {
objectNameSingular: CoreObjectNameSingular;
targetableObjects: Pick<
ActivityTargetableObject,
'id' | 'targetObjectNameSingular'
>[];
skip?: boolean;
onCompleted?: (activityTargets: ActivityTarget[]) => void;
onCompleted?: (activityTargets: (TaskTarget | NoteTarget)[]) => void;
}) => {
const activityTargetsFilter = getActivityTargetsFilter({
targetableObjects: targetableObjects,
@ -26,13 +30,16 @@ export const useActivityTargetsForTargetableObjects = ({
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
const FIND_ACTIVITY_TARGETS_OPERATION_SIGNATURE =
findActivityTargetsOperationSignatureFactory({ objectMetadataItems });
findActivityTargetsOperationSignatureFactory({
objectNameSingular,
objectMetadataItems,
});
// TODO: We want to optimistically remove from this request
// If we are on a show page and we remove the current show page object corresponding activity target
// See also if we need to update useTimelineActivities
const { records: activityTargets, loading: loadingActivityTargets } =
useFindManyRecords<ActivityTarget>({
useFindManyRecords<TaskTarget | NoteTarget>({
skip,
objectNameSingular:
FIND_ACTIVITY_TARGETS_OPERATION_SIGNATURE.objectNameSingular,

View File

@ -1,188 +0,0 @@
import { Reference, useApolloClient } from '@apollo/client';
import { useRecoilCallback, useRecoilValue } from 'recoil';
import { v4 } from 'uuid';
import { Activity, ActivityType } from '@/activities/types/Activity';
import { ActivityTarget } from '@/activities/types/ActivityTarget';
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
import { makeActivityTargetsToCreateFromTargetableObjects } from '@/activities/utils/getActivityTargetsToCreateFromTargetableObjects';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useCreateManyRecordsInCache } from '@/object-record/cache/hooks/useCreateManyRecordsInCache';
import { useCreateOneRecordInCache } from '@/object-record/cache/hooks/useCreateOneRecordInCache';
import { getRecordConnectionFromRecords } from '@/object-record/cache/utils/getRecordConnectionFromRecords';
import { modifyRecordFromCache } from '@/object-record/cache/utils/modifyRecordFromCache';
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { WorkspaceMember } from '@/workspace-member/types/WorkspaceMember';
import { isDefined } from '~/utils/isDefined';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
export const useCreateActivityInCache = () => {
const { createManyRecordsInCache: createManyActivityTargetsInCache } =
useCreateManyRecordsInCache<ActivityTarget>({
objectNameSingular: CoreObjectNameSingular.ActivityTarget,
});
const cache = useApolloClient().cache;
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
const { record: currentWorkspaceMemberRecord } = useFindOneRecord({
objectNameSingular: CoreObjectNameSingular.WorkspaceMember,
objectRecordId: currentWorkspaceMember?.id,
});
const { objectMetadataItem: objectMetadataItemActivity } =
useObjectMetadataItem({
objectNameSingular: CoreObjectNameSingular.Activity,
});
const { objectMetadataItem: objectMetadataItemActivityTarget } =
useObjectMetadataItem({
objectNameSingular: CoreObjectNameSingular.ActivityTarget,
});
const createOneActivityInCache = useCreateOneRecordInCache<Activity>({
objectMetadataItem: objectMetadataItemActivity,
});
const createActivityInCache = useRecoilCallback(
({ snapshot, set }) =>
({
type,
targetObject,
customAssignee,
}: {
type: ActivityType;
targetObject?: ActivityTargetableObject;
customAssignee?: WorkspaceMember;
}) => {
const activityId = v4();
const createdActivityInCache = createOneActivityInCache({
id: activityId,
__typename: 'Activity',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
author: currentWorkspaceMemberRecord,
authorId: currentWorkspaceMemberRecord?.id,
assignee: customAssignee ?? currentWorkspaceMemberRecord,
assigneeId: customAssignee?.id ?? currentWorkspaceMemberRecord?.id,
type,
});
if (isUndefinedOrNull(createdActivityInCache)) {
throw new Error('Failed to create activity in cache');
}
if (isUndefinedOrNull(targetObject)) {
set(recordStoreFamilyState(activityId), {
...createdActivityInCache,
activityTargets: [],
comments: [],
});
return {
createdActivityInCache: {
...createdActivityInCache,
activityTargets: [],
},
};
}
const targetObjectRecord = snapshot
.getLoadable(recordStoreFamilyState(targetObject.id))
.getValue();
if (isUndefinedOrNull(targetObjectRecord)) {
throw new Error('Failed to find target object record');
}
const activityTargetsToCreate =
makeActivityTargetsToCreateFromTargetableObjects({
activity: createdActivityInCache,
targetableObjects: [targetObject],
targetObjectRecords: [targetObjectRecord],
});
const createdActivityTargetsInCache = createManyActivityTargetsInCache(
activityTargetsToCreate,
);
const activityTargetsConnection = getRecordConnectionFromRecords({
objectMetadataItems: objectMetadataItems,
objectMetadataItem: objectMetadataItemActivityTarget,
records: createdActivityTargetsInCache,
withPageInfo: false,
computeReferences: true,
isRootLevel: false,
});
modifyRecordFromCache({
recordId: createdActivityInCache.id,
cache,
fieldModifiers: {
activityTargets: () => activityTargetsConnection,
},
objectMetadataItem: objectMetadataItemActivity,
});
const targetObjectMetadataItem = objectMetadataItems.find(
(item) => item.nameSingular === targetObject.targetObjectNameSingular,
);
if (isDefined(targetObjectMetadataItem)) {
modifyRecordFromCache({
cache,
objectMetadataItem: targetObjectMetadataItem,
recordId: targetObject.id,
fieldModifiers: {
activityTargets: (activityTargetsRef, { readField }) => {
const edges = readField<{ node: Reference }[]>(
'edges',
activityTargetsRef,
);
if (!edges) return activityTargetsRef;
return {
...activityTargetsRef,
edges: [...edges, ...activityTargetsConnection.edges],
};
},
},
});
}
set(recordStoreFamilyState(activityId), {
...createdActivityInCache,
activityTargets: createdActivityTargetsInCache,
comments: [],
});
return {
createdActivityInCache: {
...createdActivityInCache,
activityTargets: createdActivityTargetsInCache,
},
};
},
[
createOneActivityInCache,
currentWorkspaceMemberRecord,
createManyActivityTargetsInCache,
objectMetadataItems,
objectMetadataItemActivityTarget,
cache,
objectMetadataItemActivity,
],
);
return {
createActivityInCache,
};
};

View File

@ -1,8 +1,6 @@
import { isNonEmptyArray } from '@sniptt/guards';
import { CREATE_ONE_ACTIVITY_OPERATION_SIGNATURE } from '@/activities/graphql/operation-signatures/CreateOneActivityOperationSignature';
import { ActivityForEditor } from '@/activities/types/ActivityForEditor';
import { ActivityTarget } from '@/activities/types/ActivityTarget';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
@ -13,33 +11,48 @@ import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { useApolloClient } from '@apollo/client';
import { createOneActivityOperationSignatureFactory } from '@/activities/graphql/operation-signatures/factories/createOneActivityOperationSignatureFactory';
import { NoteTarget } from '@/activities/types/NoteTarget';
import { TaskTarget } from '@/activities/types/TaskTarget';
import { getJoinObjectNameSingular } from '@/activities/utils/getJoinObjectNameSingular';
import { useRecoilCallback } from 'recoil';
import { capitalize } from '~/utils/string/capitalize';
export const useCreateActivityInDB = () => {
export const useCreateActivityInDB = ({
activityObjectNameSingular,
}: {
activityObjectNameSingular:
| CoreObjectNameSingular.Task
| CoreObjectNameSingular.Note;
}) => {
const createOneActivityOperationSignature =
createOneActivityOperationSignatureFactory({
objectNameSingular: activityObjectNameSingular,
});
const { createOneRecord: createOneActivity } = useCreateOneRecord({
objectNameSingular:
CREATE_ONE_ACTIVITY_OPERATION_SIGNATURE.objectNameSingular,
recordGqlFields: CREATE_ONE_ACTIVITY_OPERATION_SIGNATURE.fields,
objectNameSingular: activityObjectNameSingular,
recordGqlFields: createOneActivityOperationSignature.fields,
shouldMatchRootQueryFilter: true,
});
const { createManyRecords: createManyActivityTargets } =
useCreateManyRecords<ActivityTarget>({
objectNameSingular: CoreObjectNameSingular.ActivityTarget,
shouldMatchRootQueryFilter: true,
});
const { createManyRecords: createManyActivityTargets } = useCreateManyRecords<
TaskTarget | NoteTarget
>({
objectNameSingular: getJoinObjectNameSingular(activityObjectNameSingular),
shouldMatchRootQueryFilter: true,
});
const { objectMetadataItems } = useObjectMetadataItems();
const { objectMetadataItem: objectMetadataItemActivityTarget } =
useObjectMetadataItem({
objectNameSingular: CoreObjectNameSingular.ActivityTarget,
objectNameSingular: getJoinObjectNameSingular(activityObjectNameSingular),
});
const { objectMetadataItem: objectMetadataItemActivity } =
useObjectMetadataItem({
objectNameSingular: CoreObjectNameSingular.Activity,
objectNameSingular: activityObjectNameSingular,
});
const cache = useApolloClient().cache;
@ -52,7 +65,8 @@ export const useCreateActivityInDB = () => {
updatedAt: new Date().toISOString(),
});
const activityTargetsToCreate = activityToCreate.activityTargets ?? [];
const activityTargetsToCreate =
activityToCreate.noteTargets ?? activityToCreate.taskTargets ?? [];
if (isNonEmptyArray(activityTargetsToCreate)) {
await createManyActivityTargets(activityTargetsToCreate);

View File

@ -1,25 +1,34 @@
import { useRecoilState, useSetRecoilState } from 'recoil';
import { activityIdInDrawerState } from '@/activities/states/activityIdInDrawerState';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState';
import { viewableRecordNameSingularState } from '@/object-record/record-right-drawer/states/viewableRecordNameSingularState';
import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
import { RightDrawerHotkeyScope } from '@/ui/layout/right-drawer/types/RightDrawerHotkeyScope';
import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages';
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
export const useOpenActivityRightDrawer = () => {
export const useOpenActivityRightDrawer = ({
objectNameSingular,
}: {
objectNameSingular: CoreObjectNameSingular;
}) => {
const { openRightDrawer, isRightDrawerOpen, rightDrawerPage } =
useRightDrawer();
const [viewableRecordId, setViewableRecordId] = useRecoilState(
viewableRecordIdState,
);
const setActivityIdInDrawer = useSetRecoilState(activityIdInDrawerState);
const setViewableRecordNameSingular = useSetRecoilState(
viewableRecordNameSingularState,
);
const setHotkeyScope = useSetHotkeyScope();
return (activityId: string) => {
if (
isRightDrawerOpen &&
rightDrawerPage === RightDrawerPages.EditActivity &&
rightDrawerPage === RightDrawerPages.ViewRecord &&
viewableRecordId === activityId
) {
return;
@ -27,7 +36,7 @@ export const useOpenActivityRightDrawer = () => {
setHotkeyScope(RightDrawerHotkeyScope.RightDrawer, { goto: false });
setViewableRecordId(activityId);
setActivityIdInDrawer(activityId);
openRightDrawer(RightDrawerPages.EditActivity);
setViewableRecordNameSingular(objectNameSingular);
openRightDrawer(RightDrawerPages.ViewRecord);
};
};

View File

@ -1,12 +1,7 @@
import { useRecoilState, useSetRecoilState } from 'recoil';
import { useSetRecoilState } from 'recoil';
import { useCreateActivityInCache } from '@/activities/hooks/useCreateActivityInCache';
import { activityIdInDrawerState } from '@/activities/states/activityIdInDrawerState';
import { activityTargetableEntityArrayState } from '@/activities/states/activityTargetableEntityArrayState';
import { isActivityInCreateModeState } from '@/activities/states/isActivityInCreateModeState';
import { isUpsertingActivityInDBState } from '@/activities/states/isCreatingActivityInDBState';
import { temporaryActivityForEditorState } from '@/activities/states/temporaryActivityForEditorState';
import { ActivityType } from '@/activities/types/Activity';
import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState';
import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
import { RightDrawerHotkeyScope } from '@/ui/layout/right-drawer/types/RightDrawerHotkeyScope';
@ -14,54 +9,84 @@ import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPage
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
import { WorkspaceMember } from '@/workspace-member/types/WorkspaceMember';
import { Note } from '@/activities/types/Note';
import { NoteTarget } from '@/activities/types/NoteTarget';
import { Task } from '@/activities/types/Task';
import { TaskTarget } from '@/activities/types/TaskTarget';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
import { viewableRecordNameSingularState } from '@/object-record/record-right-drawer/states/viewableRecordNameSingularState';
import { ActivityTargetableObject } from '../types/ActivityTargetableEntity';
export const useOpenCreateActivityDrawer = () => {
export const useOpenCreateActivityDrawer = ({
activityObjectNameSingular,
}: {
activityObjectNameSingular:
| CoreObjectNameSingular.Note
| CoreObjectNameSingular.Task;
}) => {
const { openRightDrawer } = useRightDrawer();
const setHotkeyScope = useSetHotkeyScope();
const { createActivityInCache } = useCreateActivityInCache();
const { createOneRecord: createOneActivity } = useCreateOneRecord<
Task | Note
>({
objectNameSingular: activityObjectNameSingular,
});
const { createOneRecord: createOneActivityTarget } = useCreateOneRecord<
TaskTarget | NoteTarget
>({
objectNameSingular:
activityObjectNameSingular === CoreObjectNameSingular.Task
? CoreObjectNameSingular.TaskTarget
: CoreObjectNameSingular.NoteTarget,
});
const setActivityTargetableEntityArray = useSetRecoilState(
activityTargetableEntityArrayState,
);
const setViewableRecordId = useSetRecoilState(viewableRecordIdState);
const setIsCreatingActivity = useSetRecoilState(isActivityInCreateModeState);
const setTemporaryActivityForEditor = useSetRecoilState(
temporaryActivityForEditorState,
const setViewableRecordNameSingular = useSetRecoilState(
viewableRecordNameSingularState,
);
const setActivityIdInDrawer = useSetRecoilState(activityIdInDrawerState);
const [, setIsUpsertingActivityInDB] = useRecoilState(
const setIsUpsertingActivityInDB = useSetRecoilState(
isUpsertingActivityInDBState,
);
const openCreateActivityDrawer = async ({
type,
targetableObjects,
customAssignee,
}: {
type: ActivityType;
targetableObjects: ActivityTargetableObject[];
customAssignee?: WorkspaceMember;
}) => {
const { createdActivityInCache } = createActivityInCache({
type,
targetObject: targetableObjects[0],
customAssignee,
const activity = await createOneActivity({
assigneeId: customAssignee?.id,
});
const targetableObjectRelationIdName = `${targetableObjects[0].targetObjectNameSingular}Id`;
await createOneActivityTarget({
taskId:
activityObjectNameSingular === CoreObjectNameSingular.Task
? activity.id
: undefined,
noteId:
activityObjectNameSingular === CoreObjectNameSingular.Note
? activity.id
: undefined,
[targetableObjectRelationIdName]: targetableObjects[0].id,
});
setActivityIdInDrawer(createdActivityInCache.id);
setTemporaryActivityForEditor(createdActivityInCache);
setIsCreatingActivity(true);
setHotkeyScope(RightDrawerHotkeyScope.RightDrawer, { goto: false });
setViewableRecordId(createdActivityInCache.id);
setViewableRecordId(activity.id);
setViewableRecordNameSingular(activityObjectNameSingular);
setActivityTargetableEntityArray(targetableObjects ?? []);
openRightDrawer(RightDrawerPages.CreateActivity);
openRightDrawer(RightDrawerPages.ViewRecord);
setIsUpsertingActivityInDB(false);
};

View File

@ -1,9 +1,11 @@
import { useApolloClient } from '@apollo/client';
import { findActivitiesOperationSignatureFactory } from '@/activities/graphql/operation-signatures/factories/findActivitiesOperationSignatureFactory';
import { Activity } from '@/activities/types/Activity';
import { ActivityTarget } from '@/activities/types/ActivityTarget';
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
import { Note } from '@/activities/types/Note';
import { NoteTarget } from '@/activities/types/NoteTarget';
import { Task } from '@/activities/types/Task';
import { TaskTarget } from '@/activities/types/TaskTarget';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
@ -14,14 +16,18 @@ import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { sortByAscString } from '~/utils/array/sortByAscString';
import { isDefined } from '~/utils/isDefined';
export const usePrepareFindManyActivitiesQuery = () => {
export const usePrepareFindManyActivitiesQuery = ({
activityObjectNameSingular,
}: {
activityObjectNameSingular: CoreObjectNameSingular;
}) => {
const { objectMetadataItem: objectMetadataItemActivity } =
useObjectMetadataItem({
objectNameSingular: CoreObjectNameSingular.Activity,
objectNameSingular: activityObjectNameSingular,
});
const getActivityFromCache = useGetRecordFromCache({
objectNameSingular: CoreObjectNameSingular.Activity,
objectNameSingular: activityObjectNameSingular,
});
const cache = useApolloClient().cache;
@ -39,7 +45,7 @@ export const usePrepareFindManyActivitiesQuery = () => {
}: {
additionalFilter?: Record<string, unknown>;
targetableObject: ActivityTargetableObject;
shouldActivityBeExcluded?: (activityTarget: Activity) => boolean;
shouldActivityBeExcluded?: (activityTarget: Task | Note) => boolean;
}) => {
const targetableObjectMetadataItem = objectMetadataItems.find(
(objectMetadataItem) =>
@ -60,8 +66,10 @@ export const usePrepareFindManyActivitiesQuery = () => {
cache,
});
const activityTargets: ActivityTarget[] =
targetableObjectRecord?.activityTargets ?? [];
const activityTargets: (TaskTarget | NoteTarget)[] =
targetableObjectRecord?.taskTargets ??
targetableObjectRecord?.noteTargets ??
[];
const activityTargetIds = [
...new Set(
@ -71,7 +79,7 @@ export const usePrepareFindManyActivitiesQuery = () => {
),
];
const activities: Activity[] = activityTargetIds
const activities: (Task | Note)[] = activityTargetIds
.map((activityTargetId) => {
const activityTarget = activityTargets.find(
(activityTarget) => activityTarget.id === activityTargetId,
@ -81,7 +89,7 @@ export const usePrepareFindManyActivitiesQuery = () => {
return undefined;
}
return getActivityFromCache<Activity>(activityTarget.activityId);
return getActivityFromCache<Task | Note>(activityTarget.activityId);
})
.filter(isDefined);
@ -103,7 +111,10 @@ export const usePrepareFindManyActivitiesQuery = () => {
});
const FIND_ACTIVITIES_OPERATION_SIGNATURE =
findActivitiesOperationSignatureFactory({ objectMetadataItems });
findActivitiesOperationSignatureFactory({
objectNameSingular: activityObjectNameSingular,
objectMetadataItems,
});
upsertFindManyActivitiesInCache({
objectRecordsToOverwrite: filteredActivities,

View File

@ -1,20 +1,25 @@
import { useRecoilValue } from 'recoil';
import { usePrepareFindManyActivitiesQuery } from '@/activities/hooks/usePrepareFindManyActivitiesQuery';
import { objectShowPageTargetableObjectState } from '@/activities/timeline/states/objectShowPageTargetableObjectIdState';
import { Activity } from '@/activities/types/Activity';
import { objectShowPageTargetableObjectState } from '@/activities/timelineActivities/states/objectShowPageTargetableObjectIdState';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { isDefined } from '~/utils/isDefined';
// This hook should only be executed if the normalized cache is up-to-date
// It will take a targetableObject and prepare the queries for the activities
// based on the activityTargets of the targetableObject
export const useRefreshShowPageFindManyActivitiesQueries = () => {
export const useRefreshShowPageFindManyActivitiesQueries = ({
activityObjectNameSingular,
}: {
activityObjectNameSingular: CoreObjectNameSingular;
}) => {
const objectShowPageTargetableObject = useRecoilValue(
objectShowPageTargetableObjectState,
);
const { prepareFindManyActivitiesQuery } =
usePrepareFindManyActivitiesQuery();
const { prepareFindManyActivitiesQuery } = usePrepareFindManyActivitiesQuery({
activityObjectNameSingular,
});
const refreshShowPageFindManyActivitiesQueries = () => {
if (isDefined(objectShowPageTargetableObject)) {
@ -24,21 +29,12 @@ export const useRefreshShowPageFindManyActivitiesQueries = () => {
prepareFindManyActivitiesQuery({
targetableObject: objectShowPageTargetableObject,
additionalFilter: {
completedAt: { is: 'NULL' },
type: { eq: 'Task' },
},
shouldActivityBeExcluded: (activity: Activity) => {
return activity.type !== 'Task';
status: { eq: 'TODO' },
},
});
prepareFindManyActivitiesQuery({
targetableObject: objectShowPageTargetableObject,
additionalFilter: {
type: { eq: 'Note' },
},
shouldActivityBeExcluded: (activity: Activity) => {
return activity.type !== 'Note';
},
additionalFilter: {},
});
}
};

View File

@ -1,50 +1,58 @@
import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil';
import { useRecoilState, useRecoilValue } from 'recoil';
import { useCreateActivityInDB } from '@/activities/hooks/useCreateActivityInDB';
import { useRefreshShowPageFindManyActivitiesQueries } from '@/activities/hooks/useRefreshShowPageFindManyActivitiesQueries';
import { activityIdInDrawerState } from '@/activities/states/activityIdInDrawerState';
import { isActivityInCreateModeState } from '@/activities/states/isActivityInCreateModeState';
import { isUpsertingActivityInDBState } from '@/activities/states/isCreatingActivityInDBState';
import { objectShowPageTargetableObjectState } from '@/activities/timeline/states/objectShowPageTargetableObjectIdState';
import { Activity } from '@/activities/types/Activity';
import { objectShowPageTargetableObjectState } from '@/activities/timelineActivities/states/objectShowPageTargetableObjectIdState';
import { Note } from '@/activities/types/Note';
import { Task } from '@/activities/types/Task';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
import { isDefined } from '~/utils/isDefined';
export const useUpsertActivity = () => {
const [isActivityInCreateMode, setIsActivityInCreateMode] = useRecoilState(
isActivityInCreateModeState,
);
export const useUpsertActivity = ({
activityObjectNameSingular,
}: {
activityObjectNameSingular:
| CoreObjectNameSingular.Task
| CoreObjectNameSingular.Note;
}) => {
const [isActivityInCreateMode] = useRecoilState(isActivityInCreateModeState);
const { updateOneRecord: updateOneActivity } = useUpdateOneRecord<Activity>({
objectNameSingular: CoreObjectNameSingular.Activity,
const { updateOneRecord: updateOneActivity } = useUpdateOneRecord<
Task | Note
>({
objectNameSingular: activityObjectNameSingular,
});
const { createActivityInDB } = useCreateActivityInDB();
const { createActivityInDB } = useCreateActivityInDB({
activityObjectNameSingular,
});
const [, setIsUpsertingActivityInDB] = useRecoilState(
isUpsertingActivityInDBState,
);
const setActivityIdInDrawer = useSetRecoilState(activityIdInDrawerState);
const objectShowPageTargetableObject = useRecoilValue(
objectShowPageTargetableObjectState,
);
const { refreshShowPageFindManyActivitiesQueries } =
useRefreshShowPageFindManyActivitiesQueries();
useRefreshShowPageFindManyActivitiesQueries({
activityObjectNameSingular,
});
const upsertActivity = async ({
activity,
input,
}: {
activity: Activity;
input: Partial<Activity>;
activity: Task | Note;
input: Partial<Task | Note>;
}) => {
setIsUpsertingActivityInDB(true);
if (isActivityInCreateMode) {
const activityToCreate: Activity = {
const activityToCreate: Partial<Task | Note> = {
...activity,
...input,
};
@ -54,9 +62,6 @@ export const useUpsertActivity = () => {
}
await createActivityInDB(activityToCreate);
setActivityIdInDrawer(activityToCreate.id);
setIsActivityInCreateMode(false);
} else {
await updateOneActivity?.({
idToUpdate: activity.id,