Migrate to a monorepo structure (#2909)

This commit is contained in:
Charles Bochet
2023-12-10 18:10:54 +01:00
committed by GitHub
parent a70a9281eb
commit 5bdca9de6c
2304 changed files with 37152 additions and 25869 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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