Enforce front project structure through ESLINT (#7863)

Fixes: https://github.com/twentyhq/twenty/issues/7329
This commit is contained in:
Charles Bochet
2024-10-20 20:20:19 +02:00
committed by GitHub
parent f801f3aa9f
commit eccf0bf8ba
260 changed files with 500 additions and 290 deletions

View File

@ -0,0 +1,70 @@
import styled from '@emotion/styled';
import { ReactElement } from 'react';
import { EventsGroup } from '@/activities/timeline-activities/components/EventsGroup';
import { TimelineActivity } from '@/activities/timeline-activities/types/TimelineActivity';
import { filterOutInvalidTimelineActivities } from '@/activities/timeline-activities/utils/filterOutInvalidTimelineActivities';
import { groupEventsByMonth } from '@/activities/timeline-activities/utils/groupEventsByMonth';
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
type EventListProps = {
targetableObject: ActivityTargetableObject;
title: string;
events: TimelineActivity[];
button?: ReactElement | false;
};
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;
width: calc(100% - ${({ theme }) => theme.spacing(8)});
`;
export const EventList = ({ events, targetableObject }: EventListProps) => {
const mainObjectMetadataItem = useObjectMetadataItem({
objectNameSingular: targetableObject.targetObjectNameSingular,
}).objectMetadataItem;
const { objectMetadataItems } = useObjectMetadataItems();
const filteredEvents = filterOutInvalidTimelineActivities(
events,
targetableObject.targetObjectNameSingular,
objectMetadataItems,
);
const groupedEvents = groupEventsByMonth(filteredEvents);
return (
<ScrollWrapper contextProviderName="eventList">
<StyledTimelineContainer>
{groupedEvents.map((group, index) => (
<EventsGroup
mainObjectMetadataItem={mainObjectMetadataItem}
key={group.year.toString() + group.month}
group={group}
month={new Date(group.items[0].createdAt).toLocaleString(
'default',
{ month: 'long' },
)}
year={
index === 0 || group.year !== groupedEvents[index - 1].year
? group.year
: undefined
}
/>
))}
</StyledTimelineContainer>
</ScrollWrapper>
);
};

View File

@ -0,0 +1,154 @@
import styled from '@emotion/styled';
import { useContext } from 'react';
import { useRecoilValue } from 'recoil';
import { TimelineActivityContext } from '@/activities/timeline-activities/contexts/TimelineActivityContext';
import { useLinkedObjectObjectMetadataItem } from '@/activities/timeline-activities/hooks/useLinkedObjectObjectMetadataItem';
import { EventIconDynamicComponent } from '@/activities/timeline-activities/rows/components/EventIconDynamicComponent';
import { EventRowDynamicComponent } from '@/activities/timeline-activities/rows/components/EventRowDynamicComponent';
import { TimelineActivity } from '@/activities/timeline-activities/types/TimelineActivity';
import { getTimelineActivityAuthorFullName } from '@/activities/timeline-activities/utils/getTimelineActivityAuthorFullName';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { beautifyPastDateRelativeToNow } from '~/utils/date-utils';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
const StyledTimelineItemContainer = styled.div`
color: ${({ theme }) => theme.font.color.primary};
display: flex;
gap: ${({ theme }) => theme.spacing(4)};
height: 'auto';
justify-content: space-between;
overflow: hidden;
white-space: nowrap;
`;
const StyledLeftContainer = styled.div`
display: flex;
flex-direction: column;
`;
const StyledIconContainer = styled.div`
display: flex;
align-items: center;
justify-content: center;
color: ${({ theme }) => theme.font.color.tertiary};
height: 16px;
width: 16px;
margin: 5px;
user-select: none;
text-decoration-line: underline;
z-index: 2;
`;
const StyledVerticalLineContainer = styled.div`
display: flex;
flex-shrink: 0;
justify-content: center;
z-index: 2;
height: 100%;
`;
const StyledVerticalLine = styled.div`
background: ${({ theme }) => theme.border.color.light};
width: 2px;
height: 100%;
`;
const StyledSummary = styled.summary`
align-items: center;
display: flex;
flex: 1;
flex-direction: row;
gap: ${({ theme }) => theme.spacing(1)};
`;
const StyledItemContainer = styled.div<{ isMarginBottom?: boolean }>`
align-items: flex-start;
display: flex;
flex: 1;
flex-direction: column;
gap: ${({ theme }) => theme.spacing(1)};
overflow: hidden;
margin-bottom: ${({ isMarginBottom, theme }) =>
isMarginBottom ? theme.spacing(3) : 0};
min-height: 26px;
`;
const StyledItemTitleDate = styled.div`
align-items: flex-start;
padding-top: ${({ theme }) => theme.spacing(1)};
color: ${({ theme }) => theme.font.color.tertiary};
display: flex;
gap: ${({ theme }) => theme.spacing(1)};
justify-content: flex-end;
margin-left: auto;
`;
type EventRowProps = {
mainObjectMetadataItem: ObjectMetadataItem | null;
isLastEvent?: boolean;
event: TimelineActivity;
};
export const EventRow = ({
isLastEvent,
event,
mainObjectMetadataItem,
}: EventRowProps) => {
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
const { labelIdentifierValue } = useContext(TimelineActivityContext);
const beautifiedCreatedAt = beautifyPastDateRelativeToNow(event.createdAt);
const linkedObjectMetadataItem = useLinkedObjectObjectMetadataItem(
event.linkedObjectMetadataId,
);
if (isUndefinedOrNull(currentWorkspaceMember)) {
return null;
}
const authorFullName = getTimelineActivityAuthorFullName(
event,
currentWorkspaceMember,
);
if (isUndefinedOrNull(mainObjectMetadataItem)) {
throw new Error('mainObjectMetadataItem is required');
}
return (
<>
<StyledTimelineItemContainer>
<StyledLeftContainer>
<StyledIconContainer>
<EventIconDynamicComponent
event={event}
linkedObjectMetadataItem={linkedObjectMetadataItem}
/>
</StyledIconContainer>
{!isLastEvent && (
<StyledVerticalLineContainer>
<StyledVerticalLine />
</StyledVerticalLineContainer>
)}
</StyledLeftContainer>
<StyledItemContainer isMarginBottom={!isLastEvent}>
<StyledSummary>
<EventRowDynamicComponent
authorFullName={authorFullName}
labelIdentifierValue={labelIdentifierValue}
event={event}
mainObjectMetadataItem={mainObjectMetadataItem}
linkedObjectMetadataItem={linkedObjectMetadataItem}
/>
</StyledSummary>
</StyledItemContainer>
<StyledItemTitleDate id={`id-${event.id}`}>
{beautifiedCreatedAt}
</StyledItemTitleDate>
</StyledTimelineItemContainer>
</>
);
};

View File

@ -0,0 +1,83 @@
import styled from '@emotion/styled';
import { EventRow } from '@/activities/timeline-activities/components/EventRow';
import { EventGroup } from '@/activities/timeline-activities/utils/groupEventsByMonth';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
type EventsGroupProps = {
group: EventGroup;
month: string;
year?: number;
mainObjectMetadataItem: ObjectMetadataItem | null;
};
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`
margin-bottom: ${({ theme }) => theme.spacing(3)};
margin-top: ${({ theme }) => theme.spacing(3)};
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.md};
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)};
font-weight: ${({ theme }) => theme.font.weight.semiBold};
font-size: ${({ theme }) => theme.font.size.xs};
`;
const StyledMonthSeperatorLine = styled.div`
background: ${({ theme }) => theme.border.color.light};
border-radius: 50px;
flex: 1 0 0;
height: 1px;
`;
export const EventsGroup = ({
group,
month,
year,
mainObjectMetadataItem,
}: EventsGroupProps) => {
return (
<StyledActivityGroup>
<StyledMonthSeperator>
{month} {year}
<StyledMonthSeperatorLine />
</StyledMonthSeperator>
<StyledActivityGroupContainer>
<StyledActivityGroupBar />
{group.items.map((event, index) => (
<EventRow
mainObjectMetadataItem={mainObjectMetadataItem}
key={index}
event={event}
isLastEvent={index === group.items.length - 1}
/>
))}
</StyledActivityGroupContainer>
</StyledActivityGroup>
);
};

View File

@ -0,0 +1,86 @@
import styled from '@emotion/styled';
import { CustomResolverFetchMoreLoader } from '@/activities/components/CustomResolverFetchMoreLoader';
import { SkeletonLoader } from '@/activities/components/SkeletonLoader';
import { EventList } from '@/activities/timeline-activities/components/EventList';
import { TimelineCreateButtonGroup } from '@/activities/timeline-activities/components/TimelineCreateButtonGroup';
import { useTimelineActivities } from '@/activities/timeline-activities/hooks/useTimelineActivities';
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
import AnimatedPlaceholder from '@/ui/layout/animated-placeholder/components/AnimatedPlaceholder';
import {
AnimatedPlaceholderEmptyContainer,
AnimatedPlaceholderEmptySubTitle,
AnimatedPlaceholderEmptyTextContainer,
AnimatedPlaceholderEmptyTitle,
EMPTY_PLACEHOLDER_TRANSITION_PROPS,
} from '@/ui/layout/animated-placeholder/components/EmptyPlaceholderStyled';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
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%;
overflow: auto;
justify-content: center;
padding-top: ${({ theme }) => theme.spacing(6)};
padding-right: ${({ theme }) => theme.spacing(6)};
padding-left: ${({ theme }) => theme.spacing(6)};
gap: ${({ theme }) => theme.spacing(4)};
`;
export const TimelineActivities = ({
targetableObject,
isInRightDrawer = false,
}: {
targetableObject: ActivityTargetableObject;
isInRightDrawer?: boolean;
}) => {
const { timelineActivities, loading, fetchMoreRecords } =
useTimelineActivities(targetableObject);
const isTimelineActivitiesEmpty =
!timelineActivities || timelineActivities.length === 0;
if (loading === true) {
return <SkeletonLoader withSubSections />;
}
if (isTimelineActivitiesEmpty) {
return (
<AnimatedPlaceholderEmptyContainer
// eslint-disable-next-line react/jsx-props-no-spreading
{...EMPTY_PLACEHOLDER_TRANSITION_PROPS}
>
<AnimatedPlaceholder type="emptyTimeline" />
<AnimatedPlaceholderEmptyTextContainer>
<AnimatedPlaceholderEmptyTitle>
Add your first Activity
</AnimatedPlaceholderEmptyTitle>
<AnimatedPlaceholderEmptySubTitle>
There are no activities associated with this record.{' '}
</AnimatedPlaceholderEmptySubTitle>
</AnimatedPlaceholderEmptyTextContainer>
<TimelineCreateButtonGroup isInRightDrawer={isInRightDrawer} />
</AnimatedPlaceholderEmptyContainer>
);
}
return (
<StyledMainContainer>
<EventList
targetableObject={targetableObject}
title="All"
events={timelineActivities ?? []}
/>
<CustomResolverFetchMoreLoader
loading={loading}
onLastRowVisible={fetchMoreRecords}
/>
</StyledMainContainer>
);
};

View File

@ -0,0 +1,42 @@
import { useSetRecoilState } from 'recoil';
import { IconCheckbox, IconNotes, IconPaperclip } from 'twenty-ui';
import { Button } from '@/ui/input/button/components/Button';
import { ButtonGroup } from '@/ui/input/button/components/ButtonGroup';
import { TAB_LIST_COMPONENT_ID } from '@/ui/layout/show-page/components/ShowPageSubContainer';
import { useTabList } from '@/ui/layout/tab/hooks/useTabList';
export const TimelineCreateButtonGroup = ({
isInRightDrawer = false,
}: {
isInRightDrawer?: boolean;
}) => {
const { activeTabIdState } = useTabList(
`${TAB_LIST_COMPONENT_ID}-${isInRightDrawer}`,
);
const setActiveTabId = useSetRecoilState(activeTabIdState);
return (
<ButtonGroup variant={'secondary'}>
<Button
Icon={IconNotes}
title="Note"
onClick={() => {
setActiveTabId('notes');
}}
/>
<Button
Icon={IconCheckbox}
title="Task"
onClick={() => {
setActiveTabId('tasks');
}}
/>
<Button
Icon={IconPaperclip}
title="File"
onClick={() => setActiveTabId('files')}
/>
</ButtonGroup>
);
};

View File

@ -0,0 +1,80 @@
import { Meta, StoryObj } from '@storybook/react';
import { HttpResponse, graphql } from 'msw';
import { ComponentDecorator } from 'twenty-ui';
import { TimelineActivities } from '@/activities/timeline-activities/components/TimelineActivities';
import { TimelineActivityContext } from '@/activities/timeline-activities/contexts/TimelineActivityContext';
import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator';
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
import { mockedTimelineActivities } from '~/testing/mock-data/timeline-activities';
const meta: Meta<typeof TimelineActivities> = {
title: 'Modules/TimelineActivities/TimelineActivities',
component: TimelineActivities,
decorators: [
ComponentDecorator,
ObjectMetadataItemsDecorator,
SnackBarDecorator,
(Story) => {
return (
<TimelineActivityContext.Provider
value={{
labelIdentifierValue: 'Mock',
}}
>
<Story />
</TimelineActivityContext.Provider>
);
},
],
args: {
targetableObject: {
id: '1',
targetObjectNameSingular: 'company',
},
},
parameters: {
msw: {
handlers: [
graphql.query('FindManyActivities', () => {
return HttpResponse.json({
data: {
activities: {
edges: [],
pageInfo: {
hasNextPage: false,
hasPreviousPage: false,
startCursor: null,
endCursor: null,
},
},
},
});
}),
graphql.query('FindManyTimelineActivities', () => {
return HttpResponse.json({
data: {
timelineActivities: {
edges: mockedTimelineActivities.map((activity) => ({
node: activity,
cursor: activity.id,
})),
pageInfo: {
hasNextPage: false,
hasPreviousPage: false,
startCursor: null,
endCursor: null,
},
},
},
});
}),
],
},
},
};
export default meta;
type Story = StoryObj<typeof TimelineActivities>;
export const Default: Story = {};