A few polish on tasks (#1023)

A few polishing on tasks
This commit is contained in:
Charles Bochet 2023-07-31 18:15:08 -07:00 committed by GitHub
parent 22ca00bb67
commit 8b8e4ac4a5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 141 additions and 65 deletions

View File

@ -1,13 +1,11 @@
import { useCallback } from 'react';
import { Tooltip } from 'react-tooltip'; import { Tooltip } from 'react-tooltip';
import { getOperationName } from '@apollo/client/utilities';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { useOpenActivityRightDrawer } from '@/activities/hooks/useOpenActivityRightDrawer'; import { useOpenActivityRightDrawer } from '@/activities/hooks/useOpenActivityRightDrawer';
import { GET_ACTIVITIES_BY_TARGETS } from '@/activities/queries'; import { useCompleteTask } from '@/tasks/hooks/useCompleteTask';
import { IconNotes } from '@/ui/icon'; import { IconNotes } from '@/ui/icon';
import { OverflowingTextWithTooltip } from '@/ui/tooltip/OverflowingTextWithTooltip'; import { OverflowingTextWithTooltip } from '@/ui/tooltip/OverflowingTextWithTooltip';
import { Activity, User, useUpdateActivityMutation } from '~/generated/graphql'; import { Activity, User } from '~/generated/graphql';
import { import {
beautifyExactDate, beautifyExactDate,
beautifyPastDateRelativeToNow, beautifyPastDateRelativeToNow,
@ -87,7 +85,7 @@ const StyledCardContent = styled.div`
align-self: stretch; align-self: stretch;
color: ${({ theme }) => theme.font.color.secondary}; color: ${({ theme }) => theme.font.color.secondary};
margin-top: ${({ theme }) => theme.spacing(2)}; margin-top: ${({ theme }) => theme.spacing(2)};
width: 100%; width: calc(100% - ${({ theme }) => theme.spacing(4)});
`; `;
const StyledTooltip = styled(Tooltip)` const StyledTooltip = styled(Tooltip)`
@ -132,22 +130,7 @@ export function TimelineActivity({ activity }: OwnProps) {
const body = JSON.parse(activity.body ?? '{}')[0]?.content[0]?.text; const body = JSON.parse(activity.body ?? '{}')[0]?.content[0]?.text;
const openActivityRightDrawer = useOpenActivityRightDrawer(); const openActivityRightDrawer = useOpenActivityRightDrawer();
const [updateActivityMutation] = useUpdateActivityMutation(); const { completeTask } = useCompleteTask(activity);
const handleActivityCompletionChange = useCallback(
(value: boolean) => {
updateActivityMutation({
variables: {
where: { id: activity.id },
data: {
completedAt: value ? new Date().toISOString() : null,
},
},
refetchQueries: [getOperationName(GET_ACTIVITIES_BY_TARGETS) ?? ''],
});
},
[activity, updateActivityMutation],
);
return ( return (
<> <>
@ -180,7 +163,7 @@ export function TimelineActivity({ activity }: OwnProps) {
title={activity.title ?? ''} title={activity.title ?? ''}
completed={!!activity.completedAt} completed={!!activity.completedAt}
type={activity.type} type={activity.type}
onCompletionChange={handleActivityCompletionChange} onCompletionChange={completeTask}
/> />
<StyledCardContent> <StyledCardContent>
<OverflowingTextWithTooltip <OverflowingTextWithTooltip

View File

@ -3,11 +3,11 @@ import { useTasks } from '../hooks/useTasks';
import { TaskList } from './TaskList'; import { TaskList } from './TaskList';
export function TaskGroups() { export function TaskGroups() {
const { todayTasks, otherTasks } = useTasks(); const { todayOrPreviousTasks, upcomingTasks } = useTasks();
return ( return (
<> <>
<TaskList title="Today" tasks={todayTasks ?? []} /> <TaskList title="Today" tasks={todayOrPreviousTasks ?? []} />
<TaskList title="Others" tasks={otherTasks ?? []} /> <TaskList title="Upcoming" tasks={upcomingTasks ?? []} />
</> </>
); );
} }

View File

@ -8,9 +8,11 @@ import {
Checkbox, Checkbox,
CheckboxShape, CheckboxShape,
} from '@/ui/input/checkbox/components/Checkbox'; } from '@/ui/input/checkbox/components/Checkbox';
import { OverflowingTextWithTooltip } from '@/ui/tooltip/OverflowingTextWithTooltip';
import { useGetCompaniesQuery, useGetPeopleQuery } from '~/generated/graphql'; import { useGetCompaniesQuery, useGetPeopleQuery } from '~/generated/graphql';
import { beautifyExactDate } from '~/utils/date-utils'; import { beautifyExactDate } from '~/utils/date-utils';
import { useCompleteTask } from '../hooks/useCompleteTask';
import { TaskForList } from '../types/TaskForList'; import { TaskForList } from '../types/TaskForList';
const StyledContainer = styled.div` const StyledContainer = styled.div`
@ -18,29 +20,44 @@ const StyledContainer = styled.div`
align-self: stretch; align-self: stretch;
border-bottom: 1px solid ${({ theme }) => theme.border.color.light}; border-bottom: 1px solid ${({ theme }) => theme.border.color.light};
cursor: pointer; cursor: pointer;
display: flex; display: inline-flex;
height: ${({ theme }) => theme.spacing(12)}; height: ${({ theme }) => theme.spacing(12)};
min-width: calc(100% - ${({ theme }) => theme.spacing(8)});
padding: 0 ${({ theme }) => theme.spacing(4)}; padding: 0 ${({ theme }) => theme.spacing(4)};
`; `;
const StyledSeparator = styled.div` const StyledTaskBody = styled.div`
flex: 1; color: ${({ theme }) => theme.font.color.tertiary};
display: flex;
flex-direction: row;
flex-grow: 1;
width: 1px;
`; `;
const StyledTaskTitle = styled.div` const StyledTaskTitle = styled.div`
font-weight: ${({ theme }) => theme.font.weight.medium};
padding: 0 ${({ theme }) => theme.spacing(2)}; padding: 0 ${({ theme }) => theme.spacing(2)};
`; `;
const StyledCommentIcon = styled.div` const StyledCommentIcon = styled.div`
align-items: center;
color: ${({ theme }) => theme.font.color.light}; color: ${({ theme }) => theme.font.color.light};
display: flex;
margin-left: ${({ theme }) => theme.spacing(2)};
`; `;
const StyledDueDate = styled.div` const StyledDueDate = styled.div`
align-items: center;
color: ${({ theme }) => theme.font.color.secondary}; color: ${({ theme }) => theme.font.color.secondary};
display: flex;
gap: ${({ theme }) => theme.spacing(1)}; gap: ${({ theme }) => theme.spacing(1)};
padding-left: ${({ theme }) => theme.spacing(2)}; padding-left: ${({ theme }) => theme.spacing(2)};
`; `;
const StyledFieldsContainer = styled.div`
display: flex;
`;
export function TaskRow({ task }: { task: TaskForList }) { export function TaskRow({ task }: { task: TaskForList }) {
const theme = useTheme(); const theme = useTheme();
const openActivityRightDrawer = useOpenActivityRightDrawer(); const openActivityRightDrawer = useOpenActivityRightDrawer();
@ -71,6 +88,8 @@ export function TaskRow({ task }: { task: TaskForList }) {
}, },
}, },
}); });
const body = JSON.parse(task.body ?? '{}')[0]?.content[0]?.text;
const { completeTask } = useCompleteTask(task);
return ( return (
<StyledContainer <StyledContainer
@ -83,23 +102,31 @@ export function TaskRow({ task }: { task: TaskForList }) {
e.stopPropagation(); e.stopPropagation();
}} }}
> >
<Checkbox checked={false} shape={CheckboxShape.Rounded} /> <Checkbox
checked={!!task.completedAt}
shape={CheckboxShape.Rounded}
onChange={completeTask}
/>
</div> </div>
<StyledTaskTitle>{task.title}</StyledTaskTitle> <StyledTaskTitle>{task.title ?? '(No title)'}</StyledTaskTitle>
{task.comments && task.comments.length > 0 && ( <StyledTaskBody>
<StyledCommentIcon> <OverflowingTextWithTooltip text={body} />
<IconComment size={theme.icon.size.md} /> {task.comments && task.comments.length > 0 && (
</StyledCommentIcon> <StyledCommentIcon>
)} <IconComment size={theme.icon.size.md} />
<StyledSeparator /> </StyledCommentIcon>
<ActivityTargetChips )}
targetCompanies={targetCompanies} </StyledTaskBody>
targetPeople={targetPeople} <StyledFieldsContainer>
/> <ActivityTargetChips
<StyledDueDate> targetCompanies={targetCompanies}
<IconCalendar size={theme.icon.size.md} /> targetPeople={targetPeople}
{task.dueAt && beautifyExactDate(task.dueAt)} />
</StyledDueDate> <StyledDueDate>
<IconCalendar size={theme.icon.size.md} />
{task.dueAt && beautifyExactDate(task.dueAt)}
</StyledDueDate>
</StyledFieldsContainer>
</StyledContainer> </StyledContainer>
); );
} }

View File

@ -0,0 +1,35 @@
import { useCallback } from 'react';
import { getOperationName } from '@apollo/client/utilities';
import {
GET_ACTIVITIES,
GET_ACTIVITIES_BY_TARGETS,
} from '@/activities/queries';
import { Activity, useUpdateActivityMutation } from '~/generated/graphql';
type Task = Pick<Activity, 'id'>;
export function useCompleteTask(task: Task) {
const [updateActivityMutation] = useUpdateActivityMutation();
const completeTask = useCallback(
(value: boolean) => {
updateActivityMutation({
variables: {
where: { id: task.id },
data: {
completedAt: value ? new Date().toISOString() : null,
},
},
refetchQueries: [
getOperationName(GET_ACTIVITIES_BY_TARGETS) ?? '',
getOperationName(GET_ACTIVITIES) ?? '',
],
});
},
[task, updateActivityMutation],
);
return {
completeTask,
};
}

View File

@ -1,3 +1,5 @@
import { DateTime } from 'luxon';
import { activeTabIdScopedState } from '@/ui/tab/states/activeTabIdScopedState'; import { activeTabIdScopedState } from '@/ui/tab/states/activeTabIdScopedState';
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState'; import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
import { ActivityType, useGetActivitiesQuery } from '~/generated/graphql'; import { ActivityType, useGetActivitiesQuery } from '~/generated/graphql';
@ -11,7 +13,16 @@ export function useTasks() {
TasksContext, TasksContext,
); );
const { data, loading } = useGetActivitiesQuery({ const { data: completeTasksData } = useGetActivitiesQuery({
variables: {
where: {
type: { equals: ActivityType.Task },
completedAt: { not: { equals: null } },
},
},
});
const { data: incompleteTaskData } = useGetActivitiesQuery({
variables: { variables: {
where: { where: {
type: { equals: ActivityType.Task }, type: { equals: ActivityType.Task },
@ -21,27 +32,28 @@ export function useTasks() {
}, },
}); });
const todayTasks = data?.findManyActivities.filter((task) => { const data = activeTabId === 'done' ? completeTasksData : incompleteTaskData;
const todayOrPreviousTasks = data?.findManyActivities.filter((task) => {
if (!task.dueAt) { if (!task.dueAt) {
return false; return false;
} }
const dueDate = parseDate(task.dueAt).toJSDate(); const dueDate = parseDate(task.dueAt).toJSDate();
const today = new Date(); const today = DateTime.now().endOf('day').toJSDate();
return dueDate.getDate() === today.getDate(); return dueDate <= today;
}); });
const otherTasks = data?.findManyActivities.filter((task) => { const upcomingTasks = data?.findManyActivities.filter((task) => {
if (!task.dueAt) { if (!task.dueAt) {
return false; return false;
} }
const dueDate = parseDate(task.dueAt).toJSDate(); const dueDate = parseDate(task.dueAt).toJSDate();
const today = new Date(); const today = DateTime.now().endOf('day').toJSDate();
return dueDate.getDate() !== today.getDate(); return dueDate > today;
}); });
return { return {
todayTasks, todayOrPreviousTasks,
otherTasks, upcomingTasks,
loading,
}; };
} }

View File

@ -16,9 +16,10 @@ const StyledTab = styled.div<{ active?: boolean }>`
active ? theme.border.color.inverted : 'transparent'}; active ? theme.border.color.inverted : 'transparent'};
color: ${({ theme, active }) => color: ${({ theme, active }) =>
active ? theme.font.color.primary : theme.font.color.secondary}; active ? theme.font.color.primary : theme.font.color.secondary};
cursor: pointer; cursor: pointer;
display: flex; display: flex;
gap: ${({ theme }) => theme.spacing(1)};
justify-content: center; justify-content: center;
padding: ${({ theme }) => theme.spacing(2) + ' ' + theme.spacing(4)}; padding: ${({ theme }) => theme.spacing(2) + ' ' + theme.spacing(4)};
@ -38,7 +39,8 @@ export function Tab({
}: OwnProps) { }: OwnProps) {
return ( return (
<StyledTab onClick={onClick} active={active} className={className}> <StyledTab onClick={onClick} active={active} className={className}>
{icon} {title} {icon}
{title}
</StyledTab> </StyledTab>
); );
} }

View File

@ -63,6 +63,7 @@ export function TableHeader<SortField>({
{viewName} {viewName}
</> </>
} }
displayBottomBorder={false}
rightComponents={[ rightComponents={[
<FilterDropdownButton <FilterDropdownButton
context={TableContext} context={TableContext}

View File

@ -32,6 +32,7 @@ const common = {
}, },
}, },
spacing: (multiplicator: number) => `${multiplicator * 4}px`, spacing: (multiplicator: number) => `${multiplicator * 4}px`,
betweenSiblingsGap: `2px`,
table: { table: {
horizontalCellMargin: '8px', horizontalCellMargin: '8px',
checkboxColumnWidth: '32px', checkboxColumnWidth: '32px',

View File

@ -11,12 +11,12 @@ const StyledOverflowingText = styled.div<{ cursorPointer: boolean }>`
font-size: inherit; font-size: inherit;
font-weight: inherit; font-weight: inherit;
max-width: 100%;
overflow: hidden; overflow: hidden;
text-decoration: inherit; text-decoration: inherit;
text-overflow: ellipsis;
text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
width: 100%;
`; `;
export function OverflowingTextWithTooltip({ export function OverflowingTextWithTooltip({

View File

@ -5,6 +5,7 @@ type OwnProps = {
leftComponent?: ReactNode; leftComponent?: ReactNode;
rightComponents?: ReactNode[]; rightComponents?: ReactNode[];
bottomComponent?: ReactNode; bottomComponent?: ReactNode;
displayBottomBorder?: boolean;
}; };
const StyledContainer = styled.div` const StyledContainer = styled.div`
@ -12,13 +13,16 @@ const StyledContainer = styled.div`
flex-direction: column; flex-direction: column;
`; `;
const StyledTableHeader = styled.div` const StyledTopBar = styled.div<{ displayBottomBorder: boolean }>`
align-items: center; align-items: center;
border-bottom: ${({ displayBottomBorder, theme }) =>
displayBottomBorder ? `1px solid ${theme.border.color.light}` : 'none'};
box-sizing: border-box;
color: ${({ theme }) => theme.font.color.secondary}; color: ${({ theme }) => theme.font.color.secondary};
display: flex; display: flex;
flex-direction: row; flex-direction: row;
font-weight: ${({ theme }) => theme.font.weight.medium}; font-weight: ${({ theme }) => theme.font.weight.medium};
height: 40px; height: 39px;
justify-content: space-between; justify-content: space-between;
padding-left: ${({ theme }) => theme.spacing(3)}; padding-left: ${({ theme }) => theme.spacing(3)};
padding-right: ${({ theme }) => theme.spacing(2)}; padding-right: ${({ theme }) => theme.spacing(2)};
@ -31,20 +35,21 @@ const StyledLeftSection = styled.div`
const StyledRightSection = styled.div` const StyledRightSection = styled.div`
display: flex; display: flex;
font-weight: ${({ theme }) => theme.font.weight.regular}; font-weight: ${({ theme }) => theme.font.weight.regular};
gap: 2px; gap: ${({ theme }) => theme.betweenSiblingsGap};
`; `;
export function TopBar({ export function TopBar({
leftComponent, leftComponent,
rightComponents, rightComponents,
bottomComponent, bottomComponent,
displayBottomBorder = true,
}: OwnProps) { }: OwnProps) {
return ( return (
<StyledContainer> <StyledContainer>
<StyledTableHeader> <StyledTopBar displayBottomBorder={displayBottomBorder}>
<StyledLeftSection>{leftComponent}</StyledLeftSection> <StyledLeftSection>{leftComponent}</StyledLeftSection>
<StyledRightSection>{rightComponents}</StyledRightSection> <StyledRightSection>{rightComponents}</StyledRightSection>
</StyledTableHeader> </StyledTopBar>
{bottomComponent} {bottomComponent}
</StyledContainer> </StyledContainer>
); );

View File

@ -17,6 +17,12 @@ const StyledTasksContainer = styled.div`
overflow: auto; overflow: auto;
`; `;
const StyledTabListContainer = styled.div`
align-items: end;
display: flex;
height: 40px;
`;
export function Tasks() { export function Tasks() {
const theme = useTheme(); const theme = useTheme();
@ -41,7 +47,11 @@ export function Tasks() {
<StyledTasksContainer> <StyledTasksContainer>
<RecoilScope SpecificContext={TasksContext}> <RecoilScope SpecificContext={TasksContext}>
<TopBar <TopBar
leftComponent={<TabList context={TasksContext} tabs={TASK_TABS} />} leftComponent={
<StyledTabListContainer>
<TabList context={TasksContext} tabs={TASK_TABS} />
</StyledTabListContainer>
}
/> />
<TaskGroups /> <TaskGroups />
</RecoilScope> </RecoilScope>

View File

@ -62,7 +62,7 @@ export class EnvironmentService {
getFrontAuthCallbackUrl(): string { getFrontAuthCallbackUrl(): string {
return ( return (
this.configService.get<string>('FRONT_AUTH_CALLBACK_URL') ?? this.configService.get<string>('FRONT_AUTH_CALLBACK_URL') ??
this.getFrontBaseUrl() + '/auth/callback' this.getFrontBaseUrl() + '/verify'
); );
} }