Enforce front project structure through ESLINT (#7863)
Fixes: https://github.com/twentyhq/twenty/issues/7329
This commit is contained in:
@ -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>
|
||||
);
|
||||
};
|
||||
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -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 = {};
|
||||
Reference in New Issue
Block a user