feat: add event rows to Show Page Calendar tab (#4319)
* feat: add event rows to Show Page Calendar tab Closes #4287 * refactor: use time as events group key instead of ISO string for easier sorting * feat: implement data model changes * refactor: improve sorting
This commit is contained in:
@ -0,0 +1,47 @@
|
|||||||
|
import styled from '@emotion/styled';
|
||||||
|
import { startOfMonth } from 'date-fns';
|
||||||
|
|
||||||
|
import { CalendarMonthCard } from '@/activities/calendar/components/CalendarMonthCard';
|
||||||
|
import { sortCalendarEventsDesc } from '@/activities/calendar/utils/sortCalendarEvents';
|
||||||
|
import { mockedCalendarEvents } from '~/testing/mock-data/calendar';
|
||||||
|
import { groupArrayItemsBy } from '~/utils/array/groupArrayItemsBy';
|
||||||
|
import { sortDesc } from '~/utils/sort';
|
||||||
|
|
||||||
|
const StyledContainer = styled.div`
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: ${({ theme }) => theme.spacing(8)};
|
||||||
|
padding: ${({ theme }) => theme.spacing(6)};
|
||||||
|
width: 100%;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const Calendar = () => {
|
||||||
|
const sortedCalendarEvents = [...mockedCalendarEvents].sort(
|
||||||
|
sortCalendarEventsDesc,
|
||||||
|
);
|
||||||
|
const calendarEventsByMonthTime = groupArrayItemsBy(
|
||||||
|
sortedCalendarEvents,
|
||||||
|
({ startsAt }) => startOfMonth(startsAt).getTime(),
|
||||||
|
);
|
||||||
|
const sortedMonthTimes = Object.keys(calendarEventsByMonthTime)
|
||||||
|
.map(Number)
|
||||||
|
.sort(sortDesc);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledContainer>
|
||||||
|
{sortedMonthTimes.map((monthTime) => {
|
||||||
|
const monthCalendarEvents = calendarEventsByMonthTime[monthTime];
|
||||||
|
|
||||||
|
return (
|
||||||
|
!!monthCalendarEvents?.length && (
|
||||||
|
<CalendarMonthCard
|
||||||
|
key={monthTime}
|
||||||
|
calendarEvents={monthCalendarEvents}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</StyledContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,79 @@
|
|||||||
|
import { css } from '@emotion/react';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
import { endOfDay, format, isPast } from 'date-fns';
|
||||||
|
|
||||||
|
import { CalendarEventRow } from '@/activities/calendar/components/CalendarEventRow';
|
||||||
|
import { CalendarEvent } from '@/activities/calendar/types/CalendarEvent';
|
||||||
|
import { CardContent } from '@/ui/layout/card/components/CardContent';
|
||||||
|
|
||||||
|
type CalendarDayCardContentProps = {
|
||||||
|
calendarEvents: CalendarEvent[];
|
||||||
|
divider?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const StyledCardContent = styled(CardContent)<{ active: boolean }>`
|
||||||
|
align-items: flex-start;
|
||||||
|
border-color: ${({ theme }) => theme.border.color.light};
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: ${({ theme }) => theme.spacing(3)};
|
||||||
|
padding: ${({ theme }) => theme.spacing(2, 3)};
|
||||||
|
|
||||||
|
${({ active }) =>
|
||||||
|
!active &&
|
||||||
|
css`
|
||||||
|
background-color: transparent;
|
||||||
|
`}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledDayContainer = styled.div`
|
||||||
|
text-align: center;
|
||||||
|
width: ${({ theme }) => theme.spacing(6)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledWeekDay = styled.div`
|
||||||
|
color: ${({ theme }) => theme.font.color.tertiary};
|
||||||
|
font-size: ${({ theme }) => theme.font.size.xxs};
|
||||||
|
font-weight: ${({ theme }) => theme.font.weight.semiBold};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledDateDay = styled.div`
|
||||||
|
font-weight: ${({ theme }) => theme.font.weight.medium};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledEvents = styled.div`
|
||||||
|
align-items: stretch;
|
||||||
|
display: flex;
|
||||||
|
flex: 1 0 auto;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: ${({ theme }) => theme.spacing(3)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledEventRow = styled(CalendarEventRow)`
|
||||||
|
flex: 1 0 auto;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const CalendarDayCardContent = ({
|
||||||
|
calendarEvents,
|
||||||
|
divider,
|
||||||
|
}: CalendarDayCardContentProps) => {
|
||||||
|
const endOfDayDate = endOfDay(calendarEvents[0].startsAt);
|
||||||
|
const isPastDay = isPast(endOfDayDate);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledCardContent active={!isPastDay} divider={divider}>
|
||||||
|
<StyledDayContainer>
|
||||||
|
<StyledWeekDay>{format(endOfDayDate, 'EE')}</StyledWeekDay>
|
||||||
|
<StyledDateDay>{format(endOfDayDate, 'dd')}</StyledDateDay>
|
||||||
|
</StyledDayContainer>
|
||||||
|
<StyledEvents>
|
||||||
|
{calendarEvents.map((calendarEvent) => (
|
||||||
|
<StyledEventRow
|
||||||
|
key={calendarEvent.id}
|
||||||
|
calendarEvent={calendarEvent}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</StyledEvents>
|
||||||
|
</StyledCardContent>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,131 @@
|
|||||||
|
import { css, useTheme } from '@emotion/react';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
import { endOfDay, format, isPast } from 'date-fns';
|
||||||
|
|
||||||
|
import { CalendarEvent } from '@/activities/calendar/types/CalendarEvent';
|
||||||
|
import { IconArrowRight, IconLock } from '@/ui/display/icon';
|
||||||
|
import { Card } from '@/ui/layout/card/components/Card';
|
||||||
|
import { CardContent } from '@/ui/layout/card/components/CardContent';
|
||||||
|
|
||||||
|
type CalendarEventRowProps = {
|
||||||
|
calendarEvent: CalendarEvent;
|
||||||
|
className?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const StyledContainer = styled.div`
|
||||||
|
align-items: center;
|
||||||
|
display: inline-flex;
|
||||||
|
gap: ${({ theme }) => theme.spacing(3)};
|
||||||
|
height: ${({ theme }) => theme.spacing(6)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledAttendanceIndicator = styled.div<{ active?: boolean }>`
|
||||||
|
background-color: ${({ theme }) => theme.tag.background.gray};
|
||||||
|
height: 100%;
|
||||||
|
width: ${({ theme }) => theme.spacing(1)};
|
||||||
|
|
||||||
|
${({ active, theme }) =>
|
||||||
|
active &&
|
||||||
|
css`
|
||||||
|
background-color: ${theme.tag.background.red};
|
||||||
|
`}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledLabels = styled.div`
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
color: ${({ theme }) => theme.font.color.tertiary};
|
||||||
|
gap: ${({ theme }) => theme.spacing(2)};
|
||||||
|
flex: 1 0 auto;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledTime = styled.div`
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
gap: ${({ theme }) => theme.spacing(1)};
|
||||||
|
width: ${({ theme }) => theme.spacing(26)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledTitle = styled.div<{ active: boolean; canceled: boolean }>`
|
||||||
|
flex: 1 0 auto;
|
||||||
|
|
||||||
|
${({ theme, active }) =>
|
||||||
|
active &&
|
||||||
|
css`
|
||||||
|
color: ${theme.font.color.primary};
|
||||||
|
font-weight: ${theme.font.weight.medium};
|
||||||
|
`}
|
||||||
|
|
||||||
|
${({ canceled }) =>
|
||||||
|
canceled &&
|
||||||
|
css`
|
||||||
|
text-decoration: line-through;
|
||||||
|
`}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledVisibilityCard = styled(Card)<{ active: boolean }>`
|
||||||
|
color: ${({ active, theme }) =>
|
||||||
|
active ? theme.font.color.primary : theme.font.color.light};
|
||||||
|
border-color: ${({ theme }) => theme.border.color.light};
|
||||||
|
flex: 1 0 auto;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledVisibilityCardContent = styled(CardContent)`
|
||||||
|
align-items: center;
|
||||||
|
font-size: ${({ theme }) => theme.font.size.sm};
|
||||||
|
font-weight: ${({ theme }) => theme.font.weight.medium};
|
||||||
|
display: flex;
|
||||||
|
gap: ${({ theme }) => theme.spacing(1)};
|
||||||
|
padding: ${({ theme }) => theme.spacing(0, 1)};
|
||||||
|
height: ${({ theme }) => theme.spacing(6)};
|
||||||
|
background-color: ${({ theme }) => theme.background.transparent.lighter};
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const CalendarEventRow = ({
|
||||||
|
calendarEvent,
|
||||||
|
className,
|
||||||
|
}: CalendarEventRowProps) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
const hasEventEnded = calendarEvent.endsAt
|
||||||
|
? isPast(calendarEvent.endsAt)
|
||||||
|
: calendarEvent.isFullDay && isPast(endOfDay(calendarEvent.startsAt));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledContainer className={className}>
|
||||||
|
<StyledAttendanceIndicator />
|
||||||
|
<StyledLabels>
|
||||||
|
<StyledTime>
|
||||||
|
{calendarEvent.isFullDay ? (
|
||||||
|
'All Day'
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{format(calendarEvent.startsAt, 'HH:mm')}
|
||||||
|
{!!calendarEvent.endsAt && (
|
||||||
|
<>
|
||||||
|
<IconArrowRight size={theme.icon.size.sm} />
|
||||||
|
{format(calendarEvent.endsAt, 'HH:mm')}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</StyledTime>
|
||||||
|
{calendarEvent.visibility === 'METADATA' ? (
|
||||||
|
<StyledVisibilityCard active={!hasEventEnded}>
|
||||||
|
<StyledVisibilityCardContent>
|
||||||
|
<IconLock size={theme.icon.size.sm} />
|
||||||
|
Not shared
|
||||||
|
</StyledVisibilityCardContent>
|
||||||
|
</StyledVisibilityCard>
|
||||||
|
) : (
|
||||||
|
<StyledTitle
|
||||||
|
active={!hasEventEnded}
|
||||||
|
canceled={!!calendarEvent.isCanceled}
|
||||||
|
>
|
||||||
|
{calendarEvent.title}
|
||||||
|
</StyledTitle>
|
||||||
|
)}
|
||||||
|
</StyledLabels>
|
||||||
|
</StyledContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,41 @@
|
|||||||
|
import { startOfDay } from 'date-fns';
|
||||||
|
|
||||||
|
import { CalendarDayCardContent } from '@/activities/calendar/components/CalendarDayCardContent';
|
||||||
|
import { CalendarEvent } from '@/activities/calendar/types/CalendarEvent';
|
||||||
|
import { Card } from '@/ui/layout/card/components/Card';
|
||||||
|
import { groupArrayItemsBy } from '~/utils/array/groupArrayItemsBy';
|
||||||
|
import { sortDesc } from '~/utils/sort';
|
||||||
|
|
||||||
|
type CalendarMonthCardProps = {
|
||||||
|
calendarEvents: CalendarEvent[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CalendarMonthCard = ({
|
||||||
|
calendarEvents,
|
||||||
|
}: CalendarMonthCardProps) => {
|
||||||
|
const calendarEventsByDayTime = groupArrayItemsBy(
|
||||||
|
calendarEvents,
|
||||||
|
({ startsAt }) => startOfDay(startsAt).getTime(),
|
||||||
|
);
|
||||||
|
const sortedDayTimes = Object.keys(calendarEventsByDayTime)
|
||||||
|
.map(Number)
|
||||||
|
.sort(sortDesc);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card fullWidth>
|
||||||
|
{sortedDayTimes.map((dayTime, index) => {
|
||||||
|
const dayCalendarEvents = calendarEventsByDayTime[dayTime];
|
||||||
|
|
||||||
|
return (
|
||||||
|
!!dayCalendarEvents?.length && (
|
||||||
|
<CalendarDayCardContent
|
||||||
|
key={dayTime}
|
||||||
|
calendarEvents={dayCalendarEvents}
|
||||||
|
divider={index < sortedDayTimes.length - 1}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,18 @@
|
|||||||
|
import { Meta, StoryObj } from '@storybook/react';
|
||||||
|
|
||||||
|
import { Calendar } from '@/activities/calendar/components/Calendar';
|
||||||
|
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
|
||||||
|
|
||||||
|
const meta: Meta<typeof Calendar> = {
|
||||||
|
title: 'Modules/Activities/Calendar/Calendar',
|
||||||
|
component: Calendar,
|
||||||
|
decorators: [ComponentDecorator],
|
||||||
|
parameters: {
|
||||||
|
container: { width: 728 },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj<typeof Calendar>;
|
||||||
|
|
||||||
|
export const Default: Story = {};
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
// TODO: use backend CalendarEvent type when ready
|
||||||
|
export type CalendarEvent = {
|
||||||
|
endsAt?: Date;
|
||||||
|
id: string;
|
||||||
|
isFullDay: boolean;
|
||||||
|
startsAt: Date;
|
||||||
|
isCanceled?: boolean;
|
||||||
|
title?: string;
|
||||||
|
visibility: 'METADATA' | 'SHARE_EVERYTHING';
|
||||||
|
};
|
||||||
@ -0,0 +1,285 @@
|
|||||||
|
import { addHours } from 'date-fns';
|
||||||
|
|
||||||
|
import {
|
||||||
|
sortCalendarEventsAsc,
|
||||||
|
sortCalendarEventsDesc,
|
||||||
|
} from '../sortCalendarEvents';
|
||||||
|
|
||||||
|
const someDate = new Date(2000, 1, 1);
|
||||||
|
const someDatePlusOneHour = addHours(someDate, 1);
|
||||||
|
const someDatePlusTwoHours = addHours(someDate, 2);
|
||||||
|
const someDatePlusThreeHours = addHours(someDate, 3);
|
||||||
|
|
||||||
|
describe('sortCalendarEventsAsc', () => {
|
||||||
|
it('sorts non-intersecting events by ascending order', () => {
|
||||||
|
// Given
|
||||||
|
const eventA = {
|
||||||
|
startsAt: someDate,
|
||||||
|
endsAt: someDatePlusOneHour,
|
||||||
|
};
|
||||||
|
const eventB = {
|
||||||
|
startsAt: someDatePlusTwoHours,
|
||||||
|
endsAt: someDatePlusThreeHours,
|
||||||
|
};
|
||||||
|
|
||||||
|
// When
|
||||||
|
const result = sortCalendarEventsAsc(eventA, eventB);
|
||||||
|
const invertedArgsResult = sortCalendarEventsAsc(eventB, eventA);
|
||||||
|
|
||||||
|
// Then
|
||||||
|
expect(result).toBe(-1);
|
||||||
|
expect(invertedArgsResult).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sorts intersecting events by start date ascending order', () => {
|
||||||
|
// Given
|
||||||
|
const eventA = {
|
||||||
|
startsAt: someDate,
|
||||||
|
endsAt: someDatePlusTwoHours,
|
||||||
|
};
|
||||||
|
const eventB = {
|
||||||
|
startsAt: someDatePlusOneHour,
|
||||||
|
endsAt: someDatePlusThreeHours,
|
||||||
|
};
|
||||||
|
|
||||||
|
// When
|
||||||
|
const result = sortCalendarEventsAsc(eventA, eventB);
|
||||||
|
const invertedArgsResult = sortCalendarEventsAsc(eventB, eventA);
|
||||||
|
|
||||||
|
// Then
|
||||||
|
expect(result).toBe(-1);
|
||||||
|
expect(invertedArgsResult).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sorts events with same start date by end date ascending order', () => {
|
||||||
|
// Given
|
||||||
|
const eventA = {
|
||||||
|
startsAt: someDate,
|
||||||
|
endsAt: someDatePlusTwoHours,
|
||||||
|
};
|
||||||
|
const eventB = {
|
||||||
|
startsAt: someDate,
|
||||||
|
endsAt: someDatePlusThreeHours,
|
||||||
|
};
|
||||||
|
|
||||||
|
// When
|
||||||
|
const result = sortCalendarEventsAsc(eventA, eventB);
|
||||||
|
const invertedArgsResult = sortCalendarEventsAsc(eventB, eventA);
|
||||||
|
|
||||||
|
// Then
|
||||||
|
expect(result).toBe(-1);
|
||||||
|
expect(invertedArgsResult).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sorts events with same end date by start date ascending order', () => {
|
||||||
|
// Given
|
||||||
|
const eventA = {
|
||||||
|
startsAt: someDate,
|
||||||
|
endsAt: someDatePlusThreeHours,
|
||||||
|
};
|
||||||
|
const eventB = {
|
||||||
|
startsAt: someDatePlusOneHour,
|
||||||
|
endsAt: someDatePlusThreeHours,
|
||||||
|
};
|
||||||
|
|
||||||
|
// When
|
||||||
|
const result = sortCalendarEventsAsc(eventA, eventB);
|
||||||
|
const invertedArgsResult = sortCalendarEventsAsc(eventB, eventA);
|
||||||
|
|
||||||
|
// Then
|
||||||
|
expect(result).toBe(-1);
|
||||||
|
expect(invertedArgsResult).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sorts events without end date by start date ascending order', () => {
|
||||||
|
// Given
|
||||||
|
const eventA = {
|
||||||
|
startsAt: someDate,
|
||||||
|
};
|
||||||
|
const eventB = {
|
||||||
|
startsAt: someDatePlusOneHour,
|
||||||
|
};
|
||||||
|
|
||||||
|
// When
|
||||||
|
const result = sortCalendarEventsAsc(eventA, eventB);
|
||||||
|
const invertedArgsResult = sortCalendarEventsAsc(eventB, eventA);
|
||||||
|
|
||||||
|
// Then
|
||||||
|
expect(result).toBe(-1);
|
||||||
|
expect(invertedArgsResult).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns 0 for events with same start date and no end date', () => {
|
||||||
|
// Given
|
||||||
|
const eventA = {
|
||||||
|
startsAt: someDate,
|
||||||
|
};
|
||||||
|
const eventB = {
|
||||||
|
startsAt: someDate,
|
||||||
|
};
|
||||||
|
|
||||||
|
// When
|
||||||
|
const result = sortCalendarEventsAsc(eventA, eventB);
|
||||||
|
const invertedArgsResult = sortCalendarEventsAsc(eventB, eventA);
|
||||||
|
|
||||||
|
// Then
|
||||||
|
expect(result).toBe(0);
|
||||||
|
expect(invertedArgsResult).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns 0 for events with same start date if one of them has no end date', () => {
|
||||||
|
// Given
|
||||||
|
const eventA = {
|
||||||
|
startsAt: someDate,
|
||||||
|
endsAt: someDatePlusOneHour,
|
||||||
|
};
|
||||||
|
const eventB = {
|
||||||
|
startsAt: someDate,
|
||||||
|
};
|
||||||
|
|
||||||
|
// When
|
||||||
|
const result = sortCalendarEventsAsc(eventA, eventB);
|
||||||
|
const invertedArgsResult = sortCalendarEventsAsc(eventB, eventA);
|
||||||
|
|
||||||
|
// Then
|
||||||
|
expect(result).toBe(0);
|
||||||
|
expect(invertedArgsResult).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('sortCalendarEventsDesc', () => {
|
||||||
|
it('sorts non-intersecting events by descending order', () => {
|
||||||
|
// Given
|
||||||
|
const eventA = {
|
||||||
|
startsAt: someDate,
|
||||||
|
endsAt: someDatePlusOneHour,
|
||||||
|
};
|
||||||
|
const eventB = {
|
||||||
|
startsAt: someDatePlusTwoHours,
|
||||||
|
endsAt: someDatePlusThreeHours,
|
||||||
|
};
|
||||||
|
|
||||||
|
// When
|
||||||
|
const result = sortCalendarEventsDesc(eventA, eventB);
|
||||||
|
const invertedArgsResult = sortCalendarEventsDesc(eventB, eventA);
|
||||||
|
|
||||||
|
// Then
|
||||||
|
expect(result).toBe(1);
|
||||||
|
expect(invertedArgsResult).toBe(-1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sorts intersecting events by start date descending order', () => {
|
||||||
|
// Given
|
||||||
|
const eventA = {
|
||||||
|
startsAt: someDate,
|
||||||
|
endsAt: someDatePlusTwoHours,
|
||||||
|
};
|
||||||
|
const eventB = {
|
||||||
|
startsAt: someDatePlusOneHour,
|
||||||
|
endsAt: someDatePlusThreeHours,
|
||||||
|
};
|
||||||
|
|
||||||
|
// When
|
||||||
|
const result = sortCalendarEventsDesc(eventA, eventB);
|
||||||
|
const invertedArgsResult = sortCalendarEventsDesc(eventB, eventA);
|
||||||
|
|
||||||
|
// Then
|
||||||
|
expect(result).toBe(1);
|
||||||
|
expect(invertedArgsResult).toBe(-1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sorts events with same start date by end date descending order', () => {
|
||||||
|
// Given
|
||||||
|
const eventA = {
|
||||||
|
startsAt: someDate,
|
||||||
|
endsAt: someDatePlusTwoHours,
|
||||||
|
};
|
||||||
|
const eventB = {
|
||||||
|
startsAt: someDate,
|
||||||
|
endsAt: someDatePlusThreeHours,
|
||||||
|
};
|
||||||
|
|
||||||
|
// When
|
||||||
|
const result = sortCalendarEventsDesc(eventA, eventB);
|
||||||
|
const invertedArgsResult = sortCalendarEventsDesc(eventB, eventA);
|
||||||
|
|
||||||
|
// Then
|
||||||
|
expect(result).toBe(1);
|
||||||
|
expect(invertedArgsResult).toBe(-1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sorts events with same end date by start date descending order', () => {
|
||||||
|
// Given
|
||||||
|
const eventA = {
|
||||||
|
startsAt: someDate,
|
||||||
|
endsAt: someDatePlusThreeHours,
|
||||||
|
};
|
||||||
|
const eventB = {
|
||||||
|
startsAt: someDatePlusOneHour,
|
||||||
|
endsAt: someDatePlusThreeHours,
|
||||||
|
};
|
||||||
|
|
||||||
|
// When
|
||||||
|
const result = sortCalendarEventsDesc(eventA, eventB);
|
||||||
|
const invertedArgsResult = sortCalendarEventsDesc(eventB, eventA);
|
||||||
|
|
||||||
|
// Then
|
||||||
|
expect(result).toBe(1);
|
||||||
|
expect(invertedArgsResult).toBe(-1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sorts events without end date by start date descending order', () => {
|
||||||
|
// Given
|
||||||
|
const eventA = {
|
||||||
|
startsAt: someDate,
|
||||||
|
};
|
||||||
|
const eventB = {
|
||||||
|
startsAt: someDatePlusOneHour,
|
||||||
|
};
|
||||||
|
|
||||||
|
// When
|
||||||
|
const result = sortCalendarEventsDesc(eventA, eventB);
|
||||||
|
const invertedArgsResult = sortCalendarEventsDesc(eventB, eventA);
|
||||||
|
|
||||||
|
// Then
|
||||||
|
expect(result).toBe(1);
|
||||||
|
expect(invertedArgsResult).toBe(-1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns 0 for events with same start date and no end date', () => {
|
||||||
|
// Given
|
||||||
|
const eventA = {
|
||||||
|
startsAt: someDate,
|
||||||
|
};
|
||||||
|
const eventB = {
|
||||||
|
startsAt: someDate,
|
||||||
|
};
|
||||||
|
|
||||||
|
// When
|
||||||
|
const result = sortCalendarEventsDesc(eventA, eventB);
|
||||||
|
const invertedArgsResult = sortCalendarEventsDesc(eventB, eventA);
|
||||||
|
|
||||||
|
// Then
|
||||||
|
expect(result === 0).toBe(true);
|
||||||
|
expect(invertedArgsResult === 0).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns 0 for events with same start date if one of them has no end date', () => {
|
||||||
|
// Given
|
||||||
|
const eventA = {
|
||||||
|
startsAt: someDate,
|
||||||
|
endsAt: someDatePlusOneHour,
|
||||||
|
};
|
||||||
|
const eventB = {
|
||||||
|
startsAt: someDate,
|
||||||
|
};
|
||||||
|
|
||||||
|
// When
|
||||||
|
const result = sortCalendarEventsDesc(eventA, eventB);
|
||||||
|
const invertedArgsResult = sortCalendarEventsDesc(eventB, eventA);
|
||||||
|
|
||||||
|
// Then
|
||||||
|
expect(result === 0).toBe(true);
|
||||||
|
expect(invertedArgsResult === 0).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,26 @@
|
|||||||
|
import { CalendarEvent } from '@/activities/calendar/types/CalendarEvent';
|
||||||
|
import { sortAsc } from '~/utils/sort';
|
||||||
|
|
||||||
|
export const sortCalendarEventsAsc = (
|
||||||
|
calendarEventA: Pick<CalendarEvent, 'startsAt' | 'endsAt'>,
|
||||||
|
calendarEventB: Pick<CalendarEvent, 'startsAt' | 'endsAt'>,
|
||||||
|
) => {
|
||||||
|
const startsAtSort = sortAsc(
|
||||||
|
calendarEventA.startsAt.getTime(),
|
||||||
|
calendarEventB.startsAt.getTime(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (startsAtSort === 0 && calendarEventA.endsAt && calendarEventB.endsAt) {
|
||||||
|
return sortAsc(
|
||||||
|
calendarEventA.endsAt.getTime(),
|
||||||
|
calendarEventB.endsAt.getTime(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return startsAtSort;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const sortCalendarEventsDesc = (
|
||||||
|
calendarEventA: Pick<CalendarEvent, 'startsAt' | 'endsAt'>,
|
||||||
|
calendarEventB: Pick<CalendarEvent, 'startsAt' | 'endsAt'>,
|
||||||
|
) => -sortCalendarEventsAsc(calendarEventA, calendarEventB);
|
||||||
@ -1,6 +1,7 @@
|
|||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
|
|
||||||
|
import { Calendar } from '@/activities/calendar/components/Calendar';
|
||||||
import { EmailThreads } from '@/activities/emails/components/EmailThreads';
|
import { EmailThreads } from '@/activities/emails/components/EmailThreads';
|
||||||
import { Attachments } from '@/activities/files/components/Attachments';
|
import { Attachments } from '@/activities/files/components/Attachments';
|
||||||
import { Notes } from '@/activities/notes/components/Notes';
|
import { Notes } from '@/activities/notes/components/Notes';
|
||||||
@ -136,6 +137,7 @@ export const ShowPageRightContainer = ({
|
|||||||
<Attachments targetableObject={targetableObject} />
|
<Attachments targetableObject={targetableObject} />
|
||||||
)}
|
)}
|
||||||
{activeTabId === 'emails' && <EmailThreads entity={targetableObject} />}
|
{activeTabId === 'emails' && <EmailThreads entity={targetableObject} />}
|
||||||
|
{activeTabId === 'calendar' && <Calendar />}
|
||||||
</StyledShowPageRightContainer>
|
</StyledShowPageRightContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
52
packages/twenty-front/src/testing/mock-data/calendar.ts
Normal file
52
packages/twenty-front/src/testing/mock-data/calendar.ts
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import { addDays, subMonths } from 'date-fns';
|
||||||
|
|
||||||
|
import { CalendarEvent } from '@/activities/calendar/types/CalendarEvent';
|
||||||
|
|
||||||
|
export const mockedCalendarEvents: CalendarEvent[] = [
|
||||||
|
{
|
||||||
|
id: '9a6b35f1-6078-415b-9540-f62671bb81d0',
|
||||||
|
endsAt: addDays(new Date().setHours(11, 30), 1),
|
||||||
|
isFullDay: false,
|
||||||
|
startsAt: addDays(new Date().setHours(10, 0), 1),
|
||||||
|
visibility: 'METADATA',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '19b32878-a950-4968-9e3b-ce5da514ea41',
|
||||||
|
endsAt: new Date(new Date().setHours(18, 40)),
|
||||||
|
isCanceled: true,
|
||||||
|
isFullDay: false,
|
||||||
|
startsAt: new Date(new Date().setHours(18, 0)),
|
||||||
|
title: 'Bug solving',
|
||||||
|
visibility: 'SHARE_EVERYTHING',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '6ad1cbcb-2ac4-409e-aff0-48165556fc0c',
|
||||||
|
endsAt: new Date(new Date().setHours(16, 30)),
|
||||||
|
isFullDay: false,
|
||||||
|
startsAt: new Date(new Date().setHours(15, 15)),
|
||||||
|
title: 'Onboarding Follow-Up Call',
|
||||||
|
visibility: 'SHARE_EVERYTHING',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '52cc83e3-f3dc-4c25-8a7d-5ff857612142',
|
||||||
|
endsAt: new Date(new Date().setHours(10, 30)),
|
||||||
|
isFullDay: false,
|
||||||
|
startsAt: new Date(new Date().setHours(10, 0)),
|
||||||
|
title: 'Onboarding Call',
|
||||||
|
visibility: 'SHARE_EVERYTHING',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '5a792d11-259a-4099-af51-59eb85e15d83',
|
||||||
|
isFullDay: true,
|
||||||
|
startsAt: subMonths(new Date().setHours(8, 0), 1),
|
||||||
|
visibility: 'METADATA',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '89e2a1c7-3d3f-4e79-a492-aa5de3785fc5',
|
||||||
|
endsAt: subMonths(new Date().setHours(14, 30), 3),
|
||||||
|
isFullDay: false,
|
||||||
|
startsAt: subMonths(new Date().setHours(14, 0), 3),
|
||||||
|
title: 'Alan x Garry',
|
||||||
|
visibility: 'SHARE_EVERYTHING',
|
||||||
|
},
|
||||||
|
];
|
||||||
@ -17,10 +17,7 @@
|
|||||||
* vegetable: [{ id: '2', type: 'vegetable' }],
|
* vegetable: [{ id: '2', type: 'vegetable' }],
|
||||||
* }
|
* }
|
||||||
*/
|
*/
|
||||||
export const groupArrayItemsBy = <
|
export const groupArrayItemsBy = <ArrayItem, Key extends string | number>(
|
||||||
ArrayItem extends Record<string, unknown>,
|
|
||||||
Key extends string,
|
|
||||||
>(
|
|
||||||
array: ArrayItem[],
|
array: ArrayItem[],
|
||||||
computeGroupKey: (item: ArrayItem) => Key,
|
computeGroupKey: (item: ArrayItem) => Key,
|
||||||
) =>
|
) =>
|
||||||
|
|||||||
Reference in New Issue
Block a user