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

@ -1,6 +1,5 @@
import { Meta, StoryObj } from '@storybook/react';
import { TasksRecoilScopeContext } from '@/activities/states/recoil-scope-contexts/TasksRecoilScopeContext';
import { TaskGroups } from '@/activities/tasks/components/TaskGroups';
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
import { ObjectFilterDropdownScope } from '@/object-record/object-filter-dropdown/scopes/ObjectFilterDropdownScope';
@ -9,7 +8,7 @@ import { ComponentWithRouterDecorator } from '~/testing/decorators/ComponentWith
import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator';
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
import { graphqlMocks } from '~/testing/graphqlMocks';
import { mockedTasks } from '~/testing/mock-data/activities';
import { mockedTasks } from '~/testing/mock-data/tasks';
const meta: Meta<typeof TaskGroups> = {
title: 'Modules/Activity/TaskGroups',
@ -25,9 +24,6 @@ const meta: Meta<typeof TaskGroups> = {
ObjectMetadataItemsDecorator,
SnackBarDecorator,
],
parameters: {
customRecoilScopeContext: TasksRecoilScopeContext,
},
};
export default meta;
@ -39,7 +35,7 @@ export const WithTasks: Story = {
args: {
targetableObjects: [
{
id: mockedTasks[0].authorId,
id: mockedTasks[0].taskTargets?.[0].personId,
targetObjectNameSingular: 'person',
},
] as ActivityTargetableObject[],

View File

@ -2,168 +2,10 @@ import { Meta, StoryObj } from '@storybook/react';
import { ComponentDecorator } from 'twenty-ui';
import { TaskList } from '@/activities/tasks/components/TaskList';
import { Activity } from '@/activities/types/Activity';
import { WorkspaceMember } from '@/workspace-member/types/WorkspaceMember';
import { MemoryRouterDecorator } from '~/testing/decorators/MemoryRouterDecorator';
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
import { graphqlMocks } from '~/testing/graphqlMocks';
const workspaceMember: WorkspaceMember = {
__typename: 'WorkspaceMember',
id: '374fe3a5-df1e-4119-afe0-2a62a2ba481e',
name: {
firstName: 'Charles',
lastName: 'Test',
},
avatarUrl: '',
locale: 'en',
createdAt: '2023-04-26T10:23:42.33625+00:00',
updatedAt: '2023-04-26T10:23:42.33625+00:00',
userId: 'e2409670-1088-46b4-858e-f20a598d9d0f',
userEmail: 'charles@test.com',
colorScheme: 'Light',
};
const mockedActivities: Array<Activity> = [
{
id: '3ecaa1be-aac7-463a-a38e-64078dd451d5',
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: workspaceMember,
assignee: workspaceMember,
assigneeId: workspaceMember.id,
authorId: workspaceMember.id,
comments: [],
activityTargets: [
{
id: '89bb825c-171e-4bcc-9cf7-43448d6fb300',
createdAt: '2023-04-26T10:12:42.33625+00:00',
updatedAt: '2023-04-26T10:23:42.33625+00:00',
targetObjectNameSingular: 'company',
personId: null,
companyId: '89bb825c-171e-4bcc-9cf7-43448d6fb280',
company: {
__typename: 'Company',
id: '89bb825c-171e-4bcc-9cf7-43448d6fb280',
name: 'Airbnb',
domainName: 'airbnb.com',
},
person: null,
activityId: '89bb825c-171e-4bcc-9cf7-43448d6fb230',
activity: {
__typename: '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',
},
{
id: '89bb825c-171e-4bcc-9cf7-43448d6fb301',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
targetObjectNameSingular: 'company',
personId: null,
companyId: 'b396e6b9-dc5c-4643-bcff-61b6cf7523ae',
company: {
__typename: 'Company',
id: '89bb825c-171e-4bcc-9cf7-43448d6fb278',
name: 'Aircall',
domainName: 'aircall.io',
},
person: null,
activityId: 'b396e6b9-dc5c-4643-bcff-61b6cf7523ae',
activity: {
__typename: 'Activity',
id: '89bb825c-171e-4bcc-9cf7-43448d6fb231',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
},
__typename: 'ActivityTarget',
},
],
__typename: 'Activity',
},
{
id: '89bb825c-171e-4bcc-9cf7-43448d6fb278a',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
reminderAt: null,
title: 'Another note',
body: '',
type: 'Note',
completedAt: null,
dueAt: '2029-08-26T10:12:42.33625+00:00',
author: {
...workspaceMember,
},
assignee: { ...workspaceMember },
assigneeId: workspaceMember.id,
authorId: workspaceMember.id,
comments: [],
activityTargets: [
{
id: '89bb825c-171e-4bcc-9cf7-43448d6fb278t',
createdAt: '2023-04-26T10:12:42.33625+00:00',
updatedAt: '2023-04-26T10:23:42.33625+00:00',
targetObjectNameSingular: 'person',
personId: '7dfbc3f7-6e5e-4128-957e-8d86808cdf6b', // Alexandre
person: {
__typename: 'Person',
id: '7dfbc3f7-6e5e-4128-957e-8d86808cdf6b',
name: {
firstName: 'Alexandre',
lastName: 'Test',
},
avatarUrl: '',
},
company: null,
companyId: null,
activityId: '89bb825c-171e-4bcc-9cf7-43448d6fb278a',
activity: {
__typename: 'Activity',
id: '89bb825c-171e-4bcc-9cf7-43448d6fb278a',
createdAt: '2023-04-26T10:12:42.33625+00:00',
updatedAt: '2023-04-26T10:23:42.33625+00:00',
},
__typename: 'ActivityTarget',
},
{
id: '89bb825c-171e-4bcc-9cf7-43448d6fb279t',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
personId: '7dfbc3f7-6e5e-4128-957e-8d86808cdf6d', // Jean d'Eau
companyId: null,
targetObjectNameSingular: 'person',
company: null,
person: {
__typename: 'Person',
id: '7dfbc3f7-6e5e-4128-957e-8d86808cdf6d',
name: {
firstName: 'Jean',
lastName: "d'Eau",
},
avatarUrl: '',
},
activityId: '89bb825c-171e-4bcc-9cf7-43448d6fb278a',
activity: {
__typename: 'Activity',
id: '89bb825c-171e-4bcc-9cf7-43448d6fb278a',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
},
__typename: 'ActivityTarget',
},
],
__typename: 'Activity',
},
];
import { mockedTasks } from '~/testing/mock-data/tasks';
const meta: Meta<typeof TaskList> = {
title: 'Modules/Activity/TaskList',
@ -171,7 +13,7 @@ const meta: Meta<typeof TaskList> = {
decorators: [MemoryRouterDecorator, ComponentDecorator, SnackBarDecorator],
args: {
title: 'Tasks',
tasks: mockedActivities,
tasks: mockedTasks,
},
parameters: {
msw: graphqlMocks,
@ -184,6 +26,6 @@ type Story = StoryObj<typeof TaskList>;
export const Default: Story = {
args: {
title: 'Tasks',
tasks: mockedActivities,
tasks: mockedTasks,
},
};

View File

@ -3,6 +3,7 @@ import { IconPlus } from 'twenty-ui';
import { useOpenCreateActivityDrawer } from '@/activities/hooks/useOpenCreateActivityDrawer';
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { Button } from '@/ui/input/button/components/Button';
export const AddTaskButton = ({
@ -10,7 +11,9 @@ export const AddTaskButton = ({
}: {
activityTargetableObjects?: ActivityTargetableObject[];
}) => {
const openCreateActivity = useOpenCreateActivityDrawer();
const openCreateActivity = useOpenCreateActivityDrawer({
activityObjectNameSingular: CoreObjectNameSingular.Task,
});
if (!isNonEmptyArray(activityTargetableObjects)) {
return <></>;
@ -24,7 +27,6 @@ export const AddTaskButton = ({
title="Add task"
onClick={() =>
openCreateActivity({
type: 'Task',
targetableObjects: activityTargetableObjects,
})
}

View File

@ -1,48 +0,0 @@
import { useEffect } from 'react';
import { DateTime } from 'luxon';
import { useRecoilState, useRecoilValue } from 'recoil';
import { currentUserDueTaskCountState } from '@/activities/tasks/states/currentUserTaskCountState';
import { Activity } from '@/activities/types/Activity';
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 CurrentUserDueTaskCountEffect = () => {
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
const [currentUserDueTaskCount, setCurrentUserDueTaskCount] = useRecoilState(
currentUserDueTaskCountState,
);
const { records: tasks } = useFindManyRecords<Activity>({
objectNameSingular: CoreObjectNameSingular.Activity,
filter: {
type: { eq: 'Task' },
completedAt: { is: 'NULL' },
assigneeId: { eq: currentWorkspaceMember?.id },
},
});
const computedCurrentUserDueTaskCount = 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;
useEffect(() => {
if (currentUserDueTaskCount !== computedCurrentUserDueTaskCount) {
setCurrentUserDueTaskCount(computedCurrentUserDueTaskCount);
}
}, [
computedCurrentUserDueTaskCount,
currentUserDueTaskCount,
setCurrentUserDueTaskCount,
]);
return <></>;
};

View File

@ -1,10 +1,8 @@
import styled from '@emotion/styled';
import { TasksRecoilScopeContext } from '@/activities/states/recoil-scope-contexts/TasksRecoilScopeContext';
import { TaskGroups } from '@/activities/tasks/components/TaskGroups';
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
import { ObjectFilterDropdownScope } from '@/object-record/object-filter-dropdown/scopes/ObjectFilterDropdownScope';
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
const StyledContainer = styled.div`
display: flex;
@ -21,11 +19,9 @@ export const ObjectTasks = ({
}) => {
return (
<StyledContainer>
<RecoilScope CustomRecoilScopeContext={TasksRecoilScopeContext}>
<ObjectFilterDropdownScope filterScopeId="entity-tasks-filter-scope">
<TaskGroups targetableObjects={[targetableObject]} showAddButton />
</ObjectFilterDropdownScope>
</RecoilScope>
<ObjectFilterDropdownScope filterScopeId="entity-tasks-filter-scope">
<TaskGroups targetableObjects={[targetableObject]} showAddButton />
</ObjectFilterDropdownScope>
</StyledContainer>
);
};

View File

@ -1,14 +1,16 @@
import { useOpenCreateActivityDrawer } from '@/activities/hooks/useOpenCreateActivityDrawer';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { PageAddButton } from '@/ui/layout/page/PageAddButton';
export const PageAddTaskButton = () => {
const openCreateActivity = useOpenCreateActivityDrawer();
const openCreateActivity = useOpenCreateActivityDrawer({
activityObjectNameSingular: CoreObjectNameSingular.Task,
});
// TODO: fetch workspace member from filter here
const handleClick = () => {
openCreateActivity({
type: 'Task',
targetableObjects: [],
});
};

View File

@ -18,6 +18,9 @@ import {
} from '@/ui/layout/animated-placeholder/components/EmptyPlaceholderStyled';
import { useTabList } from '@/ui/layout/tab/hooks/useTabList';
import { Task } from '@/activities/types/Task';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import groupBy from 'lodash.groupby';
import { AddTaskButton } from './AddTaskButton';
import { TaskList } from './TaskList';
@ -33,37 +36,27 @@ type TaskGroupsProps = {
};
export const TaskGroups = ({
filterDropdownId,
targetableObjects,
showAddButton,
}: TaskGroupsProps) => {
const {
todayOrPreviousTasks,
upcomingTasks,
unscheduledTasks,
completedTasks,
incompleteTasksLoading,
completeTasksLoading,
} = useTasks({
filterDropdownId: filterDropdownId,
const { tasks, tasksLoading } = useTasks({
targetableObjects: targetableObjects ?? [],
});
const openCreateActivity = useOpenCreateActivityDrawer();
const openCreateActivity = useOpenCreateActivityDrawer({
activityObjectNameSingular: CoreObjectNameSingular.Task,
});
const { activeTabIdState } = useTabList(TASKS_TAB_LIST_COMPONENT_ID);
const activeTabId = useRecoilValue(activeTabIdState);
const isLoading =
(activeTabId !== 'done' && incompleteTasksLoading) ||
(activeTabId === 'done' && completeTasksLoading);
(activeTabId !== 'done' && tasksLoading) ||
(activeTabId === 'done' && tasksLoading);
const isTasksEmpty =
(activeTabId !== 'done' &&
todayOrPreviousTasks?.length === 0 &&
upcomingTasks?.length === 0 &&
unscheduledTasks?.length === 0) ||
(activeTabId === 'done' && completedTasks?.length === 0);
(activeTabId !== 'done' && tasks?.length === 0) ||
(activeTabId === 'done' && tasks?.length === 0);
if (isLoading && isTasksEmpty) {
return <SkeletonLoader />;
@ -90,7 +83,6 @@ export const TaskGroups = ({
variant={'secondary'}
onClick={() =>
openCreateActivity({
type: 'Task',
targetableObjects: targetableObjects ?? [],
})
}
@ -101,48 +93,19 @@ export const TaskGroups = ({
return (
<StyledContainer>
{activeTabId === 'done' ? (
<TaskList
tasks={completedTasks ?? []}
button={
showAddButton && (
<AddTaskButton activityTargetableObjects={targetableObjects} />
)
}
/>
) : (
<>
{Object.entries(groupBy(tasks, ({ status }) => status)).map(
([status, tasksByStatus]: [string, Task[]]) => (
<TaskList
title="Today"
tasks={todayOrPreviousTasks ?? []}
key={status}
title={status}
tasks={tasksByStatus}
button={
showAddButton && (
<AddTaskButton activityTargetableObjects={targetableObjects} />
)
}
/>
<TaskList
title="Upcoming"
tasks={upcomingTasks ?? []}
button={
showAddButton &&
!todayOrPreviousTasks?.length && (
<AddTaskButton activityTargetableObjects={targetableObjects} />
)
}
/>
<TaskList
title="Unscheduled"
tasks={unscheduledTasks ?? []}
button={
showAddButton &&
!todayOrPreviousTasks?.length &&
!upcomingTasks?.length && (
<AddTaskButton activityTargetableObjects={targetableObjects} />
)
}
/>
</>
),
)}
</StyledContainer>
);

View File

@ -1,13 +1,12 @@
import { ReactElement } from 'react';
import styled from '@emotion/styled';
import { ReactElement } from 'react';
import { Activity } from '@/activities/types/Activity';
import { Task } from '@/activities/types/Task';
import { TaskRow } from './TaskRow';
type TaskListProps = {
title?: string;
tasks: Activity[];
tasks: Task[];
button?: ReactElement | false;
};

View File

@ -1,18 +1,16 @@
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import {
IconCalendar,
IconComment,
OverflowingTextWithTooltip,
} from 'twenty-ui';
import { IconCalendar, OverflowingTextWithTooltip } from 'twenty-ui';
import { useOpenActivityRightDrawer } from '@/activities/hooks/useOpenActivityRightDrawer';
import { ActivityTargetsInlineCell } from '@/activities/inline-cell/components/ActivityTargetsInlineCell';
import { Activity } from '@/activities/types/Activity';
import { getActivitySummary } from '@/activities/utils/getActivitySummary';
import { Checkbox, CheckboxShape } from '@/ui/input/components/Checkbox';
import { beautifyExactDate, hasDatePassed } from '~/utils/date-utils';
import { Task } from '@/activities/types/Task';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useFieldContext } from '@/object-record/hooks/useFieldContext';
import { useCompleteTask } from '../hooks/useCompleteTask';
const StyledContainer = styled.div`
@ -52,13 +50,6 @@ const StyledTaskTitle = styled.div<{
text-overflow: ellipsis;
`;
const StyledCommentIcon = styled.div`
align-items: center;
color: ${({ theme }) => theme.font.color.light};
display: flex;
margin-left: ${({ theme }) => theme.spacing(2)};
`;
const StyledDueDate = styled.div<{
isPast: boolean;
}>`
@ -89,13 +80,22 @@ const StyledCheckboxContainer = styled.div`
display: flex;
`;
export const TaskRow = ({ task }: { task: Activity }) => {
export const TaskRow = ({ task }: { task: Task }) => {
const theme = useTheme();
const openActivityRightDrawer = useOpenActivityRightDrawer();
const openActivityRightDrawer = useOpenActivityRightDrawer({
objectNameSingular: CoreObjectNameSingular.Task,
});
const body = getActivitySummary(task.body);
const { completeTask } = useCompleteTask(task);
const { FieldContextProvider: TaskTargetsContextProvider } = useFieldContext({
objectNameSingular: CoreObjectNameSingular.Task,
objectRecordId: task.id,
fieldMetadataName: 'taskTargets',
fieldPosition: 0,
});
return (
<StyledContainer
onClick={() => {
@ -109,33 +109,33 @@ export const TaskRow = ({ task }: { task: Activity }) => {
}}
>
<Checkbox
checked={!!task.completedAt}
checked={task.status === 'DONE'}
shape={CheckboxShape.Rounded}
onCheckedChange={completeTask}
/>
</StyledCheckboxContainer>
<StyledTaskTitle completed={task.completedAt !== null}>
<StyledTaskTitle completed={task.status === 'DONE'}>
{task.title || <StyledPlaceholder>Task title</StyledPlaceholder>}
</StyledTaskTitle>
<StyledTaskBody>
<OverflowingTextWithTooltip text={body} />
{task.comments && task.comments.length > 0 && (
<StyledCommentIcon>
<IconComment size={theme.icon.size.md} />
</StyledCommentIcon>
)}
</StyledTaskBody>
</StyledLeftSideContainer>
<StyledRightSideContainer>
<ActivityTargetsInlineCell
activity={task}
showLabel={false}
maxWidth={200}
readonly
/>
{TaskTargetsContextProvider && (
<TaskTargetsContextProvider>
<ActivityTargetsInlineCell
activityObjectNameSingular={CoreObjectNameSingular.Task}
activity={task}
showLabel={false}
maxWidth={200}
readonly
/>
</TaskTargetsContextProvider>
)}
<StyledDueDate
isPast={
!!task.dueAt && hasDatePassed(task.dueAt) && !task.completedAt
!!task.dueAt && hasDatePassed(task.dueAt) && task.status === 'TODO'
}
>
<IconCalendar size={theme.icon.size.md} />

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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