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
@@ -1,16 +1,25 @@
import { ReactNode } from 'react';
import { MockedProvider, MockedResponse } from '@apollo/client/testing';
import { act, renderHook } from '@testing-library/react';
import gql from 'graphql-tag';
import { ReactNode } from 'react';
import { RecoilRoot } from 'recoil';
import { useCompleteTask } from '@/activities/tasks/hooks/useCompleteTask';
import { Task } from '@/activities/types/Task';
const task = { id: '123', completedAt: '2024-03-15T07:33:14.212Z' };
const mockedDate = task.completedAt;
const toISOStringMock = jest.fn(() => mockedDate);
global.Date.prototype.toISOString = toISOStringMock;
const task: Task = {
id: '123',
status: null,
title: 'Test',
body: 'Test',
dueAt: '2024-03-15T07:33:14.212Z',
createdAt: '2024-03-15T07:33:14.212Z',
updatedAt: '2024-03-15T07:33:14.212Z',
assignee: null,
assigneeId: null,
taskTargets: [],
__typename: 'Task',
};
const mocks: MockedResponse[] = [
{
@@ -26,7 +35,7 @@ const mocks: MockedResponse[] = [
reminderAt
authorId
title
completedAt
status
updatedAt
body
dueAt
@@ -38,7 +47,7 @@ const mocks: MockedResponse[] = [
`,
variables: {
idToUpdate: task.id,
input: { completedAt: task.completedAt },
input: { status: task.status },
},
},
result: jest.fn(() => ({
@@ -49,11 +58,11 @@ const mocks: MockedResponse[] = [
reminderAt: null,
authorId: '123',
title: 'Test',
completedAt: '2024-03-15T07:33:14.212Z',
status: 'DONE',
updatedAt: '2024-03-15T07:33:14.212Z',
body: 'Test',
dueAt: '2024-03-15T07:33:14.212Z',
type: 'Task',
type: 'TASK',
id: '123',
assigneeId: '123',
},
@@ -1,31 +0,0 @@
import { ReactNode } from 'react';
import { renderHook } from '@testing-library/react';
import { RecoilRoot } from 'recoil';
import { useCurrentUserTaskCount } from '@/activities/tasks/hooks/useCurrentUserDueTaskCount';
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
import { mockedActivities } from '~/testing/mock-data/activities';
const useFindManyRecordsMock = jest.fn(() => ({
records: [...mockedActivities, { id: '2' }],
}));
jest.mock('@/object-record/hooks/useFindManyRecords', () => ({
useFindManyRecords: jest.fn(),
}));
(useFindManyRecords as jest.Mock).mockImplementation(useFindManyRecordsMock);
const Wrapper = ({ children }: { children: ReactNode }) => (
<RecoilRoot>{children}</RecoilRoot>
);
describe('useCurrentUserTaskCount', () => {
it('should return the current user task count', async () => {
const { result } = renderHook(() => useCurrentUserTaskCount(), {
wrapper: Wrapper,
});
expect(result.current.currentUserDueTaskCount).toBe(1);
});
});
@@ -1,33 +1,27 @@
import { ReactNode } from 'react';
import { renderHook } from '@testing-library/react';
import { ReactNode } from 'react';
import { RecoilRoot } from 'recoil';
import { useActivities } from '@/activities/hooks/useActivities';
import { useTasks } from '@/activities/tasks/hooks/useTasks';
import { ObjectFilterDropdownScope } from '@/object-record/object-filter-dropdown/scopes/ObjectFilterDropdownScope';
const completedTasks = [
const tasks = [
{
id: '1',
completedAt: '2024-03-15T07:33:14.212Z',
status: 'DONE',
},
{
id: '2',
completedAt: '2024-03-15T07:33:14.212Z',
status: 'DONE',
},
{
id: '3',
completedAt: '2024-03-15T07:33:14.212Z',
status: 'DONE',
},
];
const unscheduledTasks = [
{
id: '4',
},
];
const todayOrPreviousTasks = [
{
id: '5',
dueAt: '2024-03-15T07:33:14.212Z',
@@ -38,20 +32,11 @@ const todayOrPreviousTasks = [
},
];
const useActivitiesMock = jest.fn(
({
activitiesFilters,
}: {
activitiesFilters: { completedAt: { is: 'NULL' | 'NOT_NULL' } };
}) => {
const isCompletedFilter = activitiesFilters.completedAt.is === 'NOT_NULL';
return {
activities: isCompletedFilter
? completedTasks
: [...todayOrPreviousTasks, ...unscheduledTasks],
};
},
);
const useActivitiesMock = jest.fn(() => {
return {
activities: tasks,
};
});
jest.mock('@/activities/hooks/useActivities', () => ({
useActivities: jest.fn(),
@@ -74,10 +59,7 @@ describe('useTasks', () => {
});
expect(result.current).toEqual({
todayOrPreviousTasks,
upcomingTasks: [],
unscheduledTasks,
completedTasks,
tasks: tasks,
});
});
});
@@ -1,23 +1,21 @@
import { useCallback } from 'react';
import { Activity } from '@/activities/types/Activity';
import { Task } from '@/activities/types/Task';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
type Task = Pick<Activity, 'id' | 'completedAt'>;
export const useCompleteTask = (task: Task) => {
const { updateOneRecord: updateOneActivity } = useUpdateOneRecord<Activity>({
objectNameSingular: CoreObjectNameSingular.Activity,
const { updateOneRecord: updateOneActivity } = useUpdateOneRecord<Task>({
objectNameSingular: CoreObjectNameSingular.Task,
});
const completeTask = useCallback(
async (value: boolean) => {
const completedAt = value ? new Date().toISOString() : null;
const status = value ? 'DONE' : 'TODO';
await updateOneActivity?.({
idToUpdate: task.id,
updateOneRecordInput: {
completedAt,
status,
},
});
},
@@ -1,33 +0,0 @@
import { DateTime } from 'luxon';
import { useRecoilValue } from 'recoil';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
import { parseDate } from '~/utils/date-utils';
export const useCurrentUserTaskCount = () => {
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
const { records: tasks } = useFindManyRecords({
objectNameSingular: CoreObjectNameSingular.Activity,
filter: {
type: { eq: 'Task' },
completedAt: { is: 'NULL' },
assigneeId: { eq: currentWorkspaceMember?.id },
},
});
const currentUserDueTaskCount = tasks.filter((task) => {
if (!task.dueAt) {
return false;
}
const dueDate = parseDate(task.dueAt).toJSDate();
const today = DateTime.now().endOf('day').toJSDate();
return dueDate <= today;
}).length;
return {
currentUserDueTaskCount,
};
};
@@ -1,156 +1,23 @@
import { useEffect, useMemo } from 'react';
import { DateTime } from 'luxon';
import { useRecoilState, useRecoilValue } from 'recoil';
import { useActivities } from '@/activities/hooks/useActivities';
import { currentCompletedTaskQueryVariablesState } from '@/activities/tasks/states/currentCompletedTaskQueryVariablesState';
import { currentIncompleteTaskQueryVariablesState } from '@/activities/tasks/states/currentIncompleteTaskQueryVariablesState';
import { FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY } from '@/activities/timeline/constants/FindManyTimelineActivitiesOrderBy';
import { Activity } from '@/activities/types/Activity';
import { FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY } from '@/activities/timelineActivities/constants/FindManyTimelineActivitiesOrderBy';
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
import { RecordGqlOperationVariables } from '@/object-record/graphql/types/RecordGqlOperationVariables';
import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown';
import { parseDate } from '~/utils/date-utils';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
import { Task } from '@/activities/types/Task';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
type UseTasksProps = {
filterDropdownId?: string;
targetableObjects: ActivityTargetableObject[];
};
export const useTasks = ({
targetableObjects,
filterDropdownId,
}: UseTasksProps) => {
const { selectedFilterState } = useFilterDropdown({
filterDropdownId,
export const useTasks = ({ targetableObjects }: UseTasksProps) => {
const { activities: tasks, loading: tasksLoading } = useActivities<Task>({
objectNameSingular: CoreObjectNameSingular.Task,
targetableObjects,
activitiesFilters: {},
activitiesOrderByVariables: FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY,
});
const selectedFilter = useRecoilValue(selectedFilterState);
const assigneeIdFilter = useMemo(
() =>
selectedFilter
? {
assigneeId: {
in: JSON.parse(selectedFilter.value),
},
}
: undefined,
[selectedFilter],
);
const completedQueryVariables = useMemo(
() =>
({
filter: {
completedAt: { is: 'NOT_NULL' },
type: { eq: 'Task' },
...assigneeIdFilter,
},
orderBy: FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY,
}) as RecordGqlOperationVariables,
[assigneeIdFilter],
);
const incompleteQueryVariables = useMemo(
() =>
({
filter: {
completedAt: { is: 'NULL' },
type: { eq: 'Task' },
...assigneeIdFilter,
},
orderBy: FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY,
}) as RecordGqlOperationVariables,
[assigneeIdFilter],
);
const [
currentCompletedTaskQueryVariables,
setCurrentCompletedTaskQueryVariables,
] = useRecoilState(currentCompletedTaskQueryVariablesState);
const [
currentIncompleteTaskQueryVariables,
setCurrentIncompleteTaskQueryVariables,
] = useRecoilState(currentIncompleteTaskQueryVariablesState);
// TODO: fix useEffect, remove with better pattern
useEffect(() => {
if (
!isDeeplyEqual(
completedQueryVariables,
currentCompletedTaskQueryVariables,
)
) {
setCurrentCompletedTaskQueryVariables(completedQueryVariables);
}
}, [
completedQueryVariables,
currentCompletedTaskQueryVariables,
setCurrentCompletedTaskQueryVariables,
]);
useEffect(() => {
if (
!isDeeplyEqual(
incompleteQueryVariables,
currentIncompleteTaskQueryVariables,
)
) {
setCurrentIncompleteTaskQueryVariables(incompleteQueryVariables);
}
}, [
incompleteQueryVariables,
currentIncompleteTaskQueryVariables,
setCurrentIncompleteTaskQueryVariables,
]);
const { activities: completeTasksData, loading: completeTasksLoading } =
useActivities({
targetableObjects,
activitiesFilters: completedQueryVariables.filter ?? {},
activitiesOrderByVariables: completedQueryVariables.orderBy ?? [{}],
});
const { activities: incompleteTaskData, loading: incompleteTasksLoading } =
useActivities({
targetableObjects,
activitiesFilters: incompleteQueryVariables.filter ?? {},
activitiesOrderByVariables: incompleteQueryVariables.orderBy ?? [{}],
});
const todayOrPreviousTasks = incompleteTaskData?.filter((task) => {
if (!task.dueAt) {
return false;
}
const dueDate = parseDate(task.dueAt).toJSDate();
const today = DateTime.now().endOf('day').toJSDate();
return dueDate <= today;
});
const upcomingTasks = incompleteTaskData?.filter((task) => {
if (!task.dueAt) {
return false;
}
const dueDate = parseDate(task.dueAt).toJSDate();
const today = DateTime.now().endOf('day').toJSDate();
return dueDate > today;
});
const unscheduledTasks = incompleteTaskData?.filter((task) => {
return !task.dueAt;
});
const completedTasks = completeTasksData;
return {
todayOrPreviousTasks: (todayOrPreviousTasks ?? []) as Activity[],
upcomingTasks: (upcomingTasks ?? []) as Activity[],
unscheduledTasks: (unscheduledTasks ?? []) as Activity[],
completedTasks: (completedTasks ?? []) as Activity[],
completeTasksLoading,
incompleteTasksLoading,
tasks: (tasks ?? []) as Task[],
tasksLoading,
};
};