Migrate to a monorepo structure (#2909)
This commit is contained in:
@ -0,0 +1,105 @@
|
||||
import React from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { ActivityCreateButton } from '@/activities/components/ActivityCreateButton';
|
||||
import { useOpenCreateActivityDrawer } from '@/activities/hooks/useOpenCreateActivityDrawer';
|
||||
import { Activity } from '@/activities/types/Activity';
|
||||
import { ActivityTargetableEntity } from '@/activities/types/ActivityTargetableEntity';
|
||||
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
||||
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
||||
|
||||
import { TimelineItemsContainer } from './TimelineItemsContainer';
|
||||
|
||||
const StyledMainContainer = styled.div`
|
||||
align-items: flex-start;
|
||||
align-self: stretch;
|
||||
border-top: ${({ theme }) =>
|
||||
useIsMobile() ? `1px solid ${theme.border.color.medium}` : 'none'};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
const StyledTimelineEmptyContainer = styled.div`
|
||||
align-items: center;
|
||||
align-self: stretch;
|
||||
display: flex;
|
||||
flex: 1 0 0;
|
||||
flex-direction: column;
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
const StyledEmptyTimelineTitle = styled.div`
|
||||
color: ${({ theme }) => theme.font.color.secondary};
|
||||
font-size: ${({ theme }) => theme.font.size.xxl};
|
||||
font-weight: ${({ theme }) => theme.font.weight.semiBold};
|
||||
line-height: ${({ theme }) => theme.text.lineHeight.md};
|
||||
`;
|
||||
|
||||
const StyledEmptyTimelineSubTitle = styled.div`
|
||||
color: ${({ theme }) => theme.font.color.extraLight};
|
||||
font-size: ${({ theme }) => theme.font.size.xxl};
|
||||
font-weight: ${({ theme }) => theme.font.weight.semiBold};
|
||||
line-height: ${({ theme }) => theme.text.lineHeight.md};
|
||||
margin-bottom: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
export const Timeline = ({ entity }: { entity: ActivityTargetableEntity }) => {
|
||||
const { records: activityTargets, loading } = useFindManyRecords({
|
||||
objectNameSingular: 'activityTarget',
|
||||
filter: {
|
||||
[entity.type === 'Company' ? 'companyId' : 'personId']: { eq: entity.id },
|
||||
},
|
||||
});
|
||||
|
||||
const { records: activities } = useFindManyRecords({
|
||||
skip: !activityTargets?.length,
|
||||
objectNameSingular: 'activity',
|
||||
filter: {
|
||||
id: {
|
||||
in: activityTargets?.map((activityTarget) => activityTarget.activityId),
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'AscNullsFirst',
|
||||
},
|
||||
});
|
||||
|
||||
const openCreateActivity = useOpenCreateActivityDrawer();
|
||||
|
||||
if (loading || entity.type === 'Custom') {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
if (!activities.length) {
|
||||
return (
|
||||
<StyledTimelineEmptyContainer>
|
||||
<StyledEmptyTimelineTitle>No activity yet</StyledEmptyTimelineTitle>
|
||||
<StyledEmptyTimelineSubTitle>Create one:</StyledEmptyTimelineSubTitle>
|
||||
<ActivityCreateButton
|
||||
onNoteClick={() =>
|
||||
openCreateActivity({
|
||||
type: 'Note',
|
||||
targetableEntities: [entity],
|
||||
})
|
||||
}
|
||||
onTaskClick={() =>
|
||||
openCreateActivity({
|
||||
type: 'Task',
|
||||
targetableEntities: [entity],
|
||||
})
|
||||
}
|
||||
/>
|
||||
</StyledTimelineEmptyContainer>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledMainContainer>
|
||||
<TimelineItemsContainer activities={activities as Activity[]} />
|
||||
</StyledMainContainer>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,224 @@
|
||||
import { Tooltip } from 'react-tooltip';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { useOpenActivityRightDrawer } from '@/activities/hooks/useOpenActivityRightDrawer';
|
||||
import { Activity } from '@/activities/types/Activity';
|
||||
import { IconCheckbox, IconNotes } from '@/ui/display/icon';
|
||||
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
||||
import { Avatar } from '@/users/components/Avatar';
|
||||
import { WorkspaceMember } from '@/workspace-member/types/WorkspaceMember';
|
||||
import {
|
||||
beautifyExactDateTime,
|
||||
beautifyPastDateRelativeToNow,
|
||||
} from '~/utils/date-utils';
|
||||
|
||||
const StyledAvatarContainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
height: 26px;
|
||||
justify-content: center;
|
||||
user-select: none;
|
||||
width: 26px;
|
||||
z-index: 2;
|
||||
`;
|
||||
|
||||
const StyledIconContainer = styled.div`
|
||||
align-items: center;
|
||||
color: ${({ theme }) => theme.font.color.tertiary};
|
||||
display: flex;
|
||||
height: 16px;
|
||||
justify-content: center;
|
||||
text-decoration-line: underline;
|
||||
width: 16px;
|
||||
`;
|
||||
|
||||
const StyledActivityTitle = styled.div`
|
||||
color: ${({ theme }) => theme.font.color.secondary};
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
font-weight: ${({ theme }) => theme.font.weight.regular};
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
const StyledActivityLink = styled.div`
|
||||
color: ${({ theme }) => theme.font.color.secondary};
|
||||
font-weight: ${({ theme }) => theme.font.weight.regular};
|
||||
overflow: hidden;
|
||||
text-decoration-line: underline;
|
||||
text-overflow: ellipsis;
|
||||
`;
|
||||
|
||||
const StyledItemContainer = styled.div`
|
||||
align-content: center;
|
||||
align-items: center;
|
||||
color: ${({ theme }) => theme.font.color.tertiary};
|
||||
display: flex;
|
||||
flex: 1;
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
span {
|
||||
color: ${({ theme }) => theme.font.color.secondary};
|
||||
}
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
const StyledItemTitleContainer = styled.div`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-flow: row ${() => (useIsMobile() ? 'wrap' : 'nowrap')};
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
const StyledItemAuthorText = styled.div`
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
`;
|
||||
|
||||
const StyledItemTitle = styled.div`
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
const StyledItemTitleDate = styled.div`
|
||||
align-items: center;
|
||||
color: ${({ theme }) => theme.font.color.tertiary};
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
justify-content: flex-end;
|
||||
margin-left: auto;
|
||||
`;
|
||||
|
||||
const StyledVerticalLineContainer = styled.div`
|
||||
align-items: center;
|
||||
align-self: stretch;
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
justify-content: center;
|
||||
width: 26px;
|
||||
z-index: 2;
|
||||
`;
|
||||
|
||||
const StyledVerticalLine = styled.div`
|
||||
align-self: stretch;
|
||||
background: ${({ theme }) => theme.border.color.light};
|
||||
flex-shrink: 0;
|
||||
width: 2px;
|
||||
`;
|
||||
|
||||
const StyledTooltip = styled(Tooltip)`
|
||||
background-color: ${({ theme }) => theme.background.primary};
|
||||
|
||||
box-shadow: 0px 2px 4px 3px
|
||||
${({ theme }) => theme.background.transparent.light};
|
||||
|
||||
box-shadow: 2px 4px 16px 6px
|
||||
${({ theme }) => theme.background.transparent.light};
|
||||
|
||||
color: ${({ theme }) => theme.font.color.primary};
|
||||
|
||||
opacity: 1;
|
||||
padding: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
const StyledTimelineItemContainer = styled.div<{ isGap?: boolean }>`
|
||||
align-items: center;
|
||||
align-self: stretch;
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(4)};
|
||||
height: ${({ isGap, theme }) =>
|
||||
isGap ? (useIsMobile() ? theme.spacing(6) : theme.spacing(3)) : 'auto'};
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
type TimelineActivityProps = {
|
||||
activity: Pick<
|
||||
Activity,
|
||||
| 'id'
|
||||
| 'title'
|
||||
| 'body'
|
||||
| 'createdAt'
|
||||
| 'completedAt'
|
||||
| 'type'
|
||||
| 'comments'
|
||||
| 'dueAt'
|
||||
> & { author: Pick<WorkspaceMember, 'name' | 'avatarUrl'> } & {
|
||||
assignee?: Pick<WorkspaceMember, 'id' | 'name' | 'avatarUrl'> | null;
|
||||
};
|
||||
isLastActivity?: boolean;
|
||||
};
|
||||
|
||||
export const TimelineActivity = ({
|
||||
activity,
|
||||
isLastActivity,
|
||||
}: TimelineActivityProps) => {
|
||||
const beautifiedCreatedAt = beautifyPastDateRelativeToNow(activity.createdAt);
|
||||
const exactCreatedAt = beautifyExactDateTime(activity.createdAt);
|
||||
const openActivityRightDrawer = useOpenActivityRightDrawer();
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledTimelineItemContainer>
|
||||
<StyledAvatarContainer>
|
||||
<Avatar
|
||||
avatarUrl={activity.author.avatarUrl}
|
||||
placeholder={activity.author.name.firstName ?? ''}
|
||||
size="sm"
|
||||
type="rounded"
|
||||
/>
|
||||
</StyledAvatarContainer>
|
||||
<StyledItemContainer>
|
||||
<StyledItemTitleContainer>
|
||||
<StyledItemAuthorText>
|
||||
<span>
|
||||
{activity.author.name.firstName} {activity.author.name.lastName}
|
||||
</span>
|
||||
created a {activity.type.toLowerCase()}
|
||||
</StyledItemAuthorText>
|
||||
<StyledItemTitle>
|
||||
<StyledIconContainer>
|
||||
{activity.type === 'Note' && (
|
||||
<IconNotes size={theme.icon.size.sm} />
|
||||
)}
|
||||
{activity.type === 'Task' && (
|
||||
<IconCheckbox size={theme.icon.size.sm} />
|
||||
)}
|
||||
</StyledIconContainer>
|
||||
{(activity.type === 'Note' || activity.type === 'Task') && (
|
||||
<StyledActivityTitle
|
||||
onClick={() => openActivityRightDrawer(activity.id)}
|
||||
>
|
||||
“
|
||||
<StyledActivityLink title={activity.title ?? '(No Title)'}>
|
||||
{activity.title ?? '(No Title)'}
|
||||
</StyledActivityLink>
|
||||
“
|
||||
</StyledActivityTitle>
|
||||
)}
|
||||
</StyledItemTitle>
|
||||
</StyledItemTitleContainer>
|
||||
<StyledItemTitleDate id={`id-${activity.id}`}>
|
||||
{beautifiedCreatedAt}
|
||||
</StyledItemTitleDate>
|
||||
<StyledTooltip
|
||||
anchorSelect={`#id-${activity.id}`}
|
||||
content={exactCreatedAt}
|
||||
clickable
|
||||
noArrow
|
||||
/>
|
||||
</StyledItemContainer>
|
||||
</StyledTimelineItemContainer>
|
||||
{!isLastActivity && (
|
||||
<StyledTimelineItemContainer isGap>
|
||||
<StyledVerticalLineContainer>
|
||||
<StyledVerticalLine></StyledVerticalLine>
|
||||
</StyledVerticalLineContainer>
|
||||
</StyledTimelineItemContainer>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,71 @@
|
||||
import { isNonEmptyArray } from '@apollo/client/utilities';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import CommentCounter from '@/activities/comment/CommentCounter';
|
||||
import { Activity } from '@/activities/types/Activity';
|
||||
import { UserChip } from '@/users/components/UserChip';
|
||||
import { WorkspaceMember } from '@/workspace-member/types/WorkspaceMember';
|
||||
import { beautifyExactDate } from '~/utils/date-utils';
|
||||
|
||||
type TimelineActivityCardFooterProps = {
|
||||
activity: Pick<Activity, 'id' | 'dueAt' | 'comments'> & {
|
||||
assignee?: Pick<WorkspaceMember, 'id' | 'name' | 'avatarUrl'> | null;
|
||||
};
|
||||
};
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
align-items: center;
|
||||
border-top: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||
color: ${({ theme }) => theme.font.color.primary};
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
padding: ${({ theme }) => theme.spacing(2)};
|
||||
width: calc(100% - ${({ theme }) => theme.spacing(4)});
|
||||
`;
|
||||
|
||||
const StyledVerticalSeparator = styled.div`
|
||||
border-left: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||
height: 24px;
|
||||
`;
|
||||
|
||||
const StyledComment = styled.div`
|
||||
margin-left: auto;
|
||||
`;
|
||||
export const TimelineActivityCardFooter = ({
|
||||
activity,
|
||||
}: TimelineActivityCardFooterProps) => {
|
||||
const hasComments = isNonEmptyArray(activity.comments || []);
|
||||
|
||||
return (
|
||||
<>
|
||||
{(activity.assignee || activity.dueAt || hasComments) && (
|
||||
<StyledContainer>
|
||||
{activity.assignee && (
|
||||
<UserChip
|
||||
id={activity.assignee.id}
|
||||
name={
|
||||
activity.assignee.name.firstName +
|
||||
' ' +
|
||||
activity.assignee.name.lastName ?? ''
|
||||
}
|
||||
avatarUrl={activity.assignee.avatarUrl ?? ''}
|
||||
/>
|
||||
)}
|
||||
|
||||
{activity.dueAt && (
|
||||
<>
|
||||
{activity.assignee && <StyledVerticalSeparator />}
|
||||
{beautifyExactDate(activity.dueAt)}
|
||||
</>
|
||||
)}
|
||||
<StyledComment>
|
||||
{hasComments && (
|
||||
<CommentCounter commentCount={activity.comments?.length || 0} />
|
||||
)}
|
||||
</StyledComment>
|
||||
</StyledContainer>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,62 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { ActivityType } from '@/activities/types/Activity';
|
||||
import { OverflowingTextWithTooltip } from '@/ui/display/tooltip/OverflowingTextWithTooltip';
|
||||
import { Checkbox, CheckboxShape } from '@/ui/input/components/Checkbox';
|
||||
|
||||
const StyledTitleContainer = styled.div`
|
||||
color: ${({ theme }) => theme.font.color.primary};
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
font-weight: ${({ theme }) => theme.font.weight.medium};
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
line-height: ${({ theme }) => theme.text.lineHeight.lg};
|
||||
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledTitleText = styled.div<{
|
||||
completed?: boolean;
|
||||
hasCheckbox?: boolean;
|
||||
}>`
|
||||
text-decoration: ${({ completed }) => (completed ? 'line-through' : 'none')};
|
||||
width: ${({ hasCheckbox, theme }) =>
|
||||
!hasCheckbox ? '100%;' : `calc(100% - ${theme.spacing(5)});`};
|
||||
`;
|
||||
|
||||
const StyledCheckboxContainer = styled.div<{ hasCheckbox?: boolean }>`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
type TimelineActivityTitleProps = {
|
||||
title: string;
|
||||
completed?: boolean;
|
||||
type: ActivityType;
|
||||
onCompletionChange?: (value: boolean) => void;
|
||||
};
|
||||
|
||||
export const TimelineActivityTitle = ({
|
||||
title,
|
||||
completed,
|
||||
type,
|
||||
onCompletionChange,
|
||||
}: TimelineActivityTitleProps) => (
|
||||
<StyledTitleContainer>
|
||||
{type === 'Task' && (
|
||||
<StyledCheckboxContainer
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
onCompletionChange?.(!completed);
|
||||
}}
|
||||
>
|
||||
<Checkbox checked={completed ?? false} shape={CheckboxShape.Rounded} />
|
||||
</StyledCheckboxContainer>
|
||||
)}
|
||||
<StyledTitleText completed={completed} hasCheckbox={type === 'Task'}>
|
||||
<OverflowingTextWithTooltip text={title ? title : 'Task title'} />
|
||||
</StyledTitleText>
|
||||
</StyledTitleContainer>
|
||||
);
|
||||
@ -0,0 +1,56 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { ActivityForDrawer } from '@/activities/types/ActivityForDrawer';
|
||||
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
|
||||
|
||||
import { groupActivitiesByMonth } from '../utils/groupActivitiesByMonth';
|
||||
|
||||
import { TimelineActivityGroup } from './TimelingeActivityGroup';
|
||||
|
||||
const StyledTimelineContainer = styled.div`
|
||||
align-items: center;
|
||||
align-self: stretch;
|
||||
|
||||
display: flex;
|
||||
flex: 1 0 0;
|
||||
flex-direction: column;
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
justify-content: flex-start;
|
||||
|
||||
padding: ${({ theme }) => theme.spacing(4)};
|
||||
width: calc(100% - ${({ theme }) => theme.spacing(8)});
|
||||
`;
|
||||
|
||||
const StyledScrollWrapper = styled(ScrollWrapper)``;
|
||||
|
||||
export type TimelineItemsContainerProps = {
|
||||
activities: ActivityForDrawer[];
|
||||
};
|
||||
|
||||
export const TimelineItemsContainer = ({
|
||||
activities,
|
||||
}: TimelineItemsContainerProps) => {
|
||||
const groupedActivities = groupActivitiesByMonth(activities);
|
||||
|
||||
return (
|
||||
<StyledScrollWrapper>
|
||||
<StyledTimelineContainer>
|
||||
{groupedActivities.map((group, index) => (
|
||||
<TimelineActivityGroup
|
||||
key={group.year.toString() + group.month}
|
||||
group={group}
|
||||
month={new Date(group.items[0].createdAt).toLocaleString(
|
||||
'default',
|
||||
{ month: 'long' },
|
||||
)}
|
||||
year={
|
||||
index === 0 || group.year !== groupedActivities[index - 1].year
|
||||
? group.year
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</StyledTimelineContainer>
|
||||
</StyledScrollWrapper>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,78 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { ActivityGroup } from '../utils/groupActivitiesByMonth';
|
||||
|
||||
import { TimelineActivity } from './TimelineActivity';
|
||||
|
||||
type TimelineActivityGroupProps = {
|
||||
group: ActivityGroup;
|
||||
month: string;
|
||||
year?: number;
|
||||
};
|
||||
|
||||
const StyledActivityGroup = styled.div`
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
gap: ${({ theme }) => theme.spacing(4)};
|
||||
margin-bottom: ${({ theme }) => theme.spacing(4)};
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledActivityGroupContainer = styled.div`
|
||||
padding-bottom: ${({ theme }) => theme.spacing(2)};
|
||||
padding-top: ${({ theme }) => theme.spacing(2)};
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
const StyledActivityGroupBar = styled.div`
|
||||
align-items: center;
|
||||
background: ${({ theme }) => theme.background.secondary};
|
||||
border: 1px solid ${({ theme }) => theme.border.color.light};
|
||||
border-radius: ${({ theme }) => theme.border.radius.xl};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
justify-content: center;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 24px;
|
||||
`;
|
||||
|
||||
const StyledMonthSeperator = styled.div`
|
||||
align-items: center;
|
||||
align-self: stretch;
|
||||
color: ${({ theme }) => theme.font.color.light};
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(4)};
|
||||
`;
|
||||
const StyledMonthSeperatorLine = styled.div`
|
||||
background: ${({ theme }) => theme.border.color.light};
|
||||
border-radius: 50px;
|
||||
flex: 1 0 0;
|
||||
height: 1px;
|
||||
`;
|
||||
|
||||
export const TimelineActivityGroup = ({
|
||||
group,
|
||||
month,
|
||||
year,
|
||||
}: TimelineActivityGroupProps) => {
|
||||
return (
|
||||
<StyledActivityGroup>
|
||||
<StyledMonthSeperator>
|
||||
{month} {year}
|
||||
<StyledMonthSeperatorLine />
|
||||
</StyledMonthSeperator>
|
||||
<StyledActivityGroupContainer>
|
||||
<StyledActivityGroupBar />
|
||||
{group.items.map((activity, index) => (
|
||||
<TimelineActivity
|
||||
key={activity.id}
|
||||
activity={activity}
|
||||
isLastActivity={index === group.items.length - 1}
|
||||
/>
|
||||
))}
|
||||
</StyledActivityGroupContainer>
|
||||
</StyledActivityGroup>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user