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:
@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
@ -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 <></>;
|
||||
};
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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: [],
|
||||
});
|
||||
};
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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;
|
||||
};
|
||||
|
||||
|
||||
@ -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} />
|
||||
|
||||
Reference in New Issue
Block a user