From 4e7a7ce8939ed6890b75b5fdce895bea6b4ec87c Mon Sep 17 00:00:00 2001 From: Thomas Trompette Date: Thu, 22 May 2025 13:35:30 +0200 Subject: [PATCH] Remove calendar cursor (#12200) Fixes https://github.com/twentyhq/twenty/issues/12125 The root cause of the infinite loop was the calendar cursor. In some cases, it was not properly displayed and was causing the loop because of its animation that was always restarting. We agreed with @FelixMalfait and @Bonapara that given the current importance of the feature and the amount of issues associated, we remove the cursor for now. --- .../calendar/components/Calendar.tsx | 11 +- .../components/CalendarCurrentEventCursor.tsx | 180 ------------------ .../components/CalendarDayCardContent.tsx | 2 +- .../calendar/components/CalendarEventRow.tsx | 7 - .../calendar/contexts/CalendarContext.ts | 8 - .../__tests__/useCalendarEvents.test.tsx | 21 +- .../calendar/hooks/useCalendarEvents.ts | 48 +---- .../findUpcomingCalendarEvent.test.ts | 80 -------- .../utils/findUpcomingCalendarEvent.ts | 12 -- ...ettingsAccountsCalendarChannelsGeneral.tsx | 7 +- 10 files changed, 13 insertions(+), 363 deletions(-) delete mode 100644 packages/twenty-front/src/modules/activities/calendar/components/CalendarCurrentEventCursor.tsx delete mode 100644 packages/twenty-front/src/modules/activities/calendar/utils/__tests__/findUpcomingCalendarEvent.test.ts delete mode 100644 packages/twenty-front/src/modules/activities/calendar/utils/findUpcomingCalendarEvent.ts diff --git a/packages/twenty-front/src/modules/activities/calendar/components/Calendar.tsx b/packages/twenty-front/src/modules/activities/calendar/components/Calendar.tsx index 2c89215d5..ae35cb7a5 100644 --- a/packages/twenty-front/src/modules/activities/calendar/components/Calendar.tsx +++ b/packages/twenty-front/src/modules/activities/calendar/components/Calendar.tsx @@ -12,7 +12,7 @@ import { SkeletonLoader } from '@/activities/components/SkeletonLoader'; import { useCustomResolver } from '@/activities/hooks/useCustomResolver'; import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; -import { TimelineCalendarEventsWithTotal } from '~/generated/graphql'; +import { H3Title } from 'twenty-ui/display'; import { AnimatedPlaceholder, AnimatedPlaceholderEmptyContainer, @@ -22,7 +22,7 @@ import { EMPTY_PLACEHOLDER_TRANSITION_PROPS, Section, } from 'twenty-ui/layout'; -import { H3Title } from 'twenty-ui/display'; +import { TimelineCalendarEventsWithTotal } from '~/generated/graphql'; const StyledContainer = styled.div` box-sizing: border-box; @@ -82,12 +82,9 @@ export const Calendar = ({ const { calendarEventsByDayTime, - currentCalendarEvent, daysByMonthTime, - getNextCalendarEvent, monthTimes, monthTimesByYear, - updateCurrentCalendarEvent, } = useCalendarEvents(timelineCalendarEvents || []); if (firstQueryLoading) { @@ -119,10 +116,6 @@ export const Calendar = ({ diff --git a/packages/twenty-front/src/modules/activities/calendar/components/CalendarCurrentEventCursor.tsx b/packages/twenty-front/src/modules/activities/calendar/components/CalendarCurrentEventCursor.tsx deleted file mode 100644 index 3284431e9..000000000 --- a/packages/twenty-front/src/modules/activities/calendar/components/CalendarCurrentEventCursor.tsx +++ /dev/null @@ -1,180 +0,0 @@ -import { useTheme } from '@emotion/react'; -import styled from '@emotion/styled'; -import { - differenceInSeconds, - isThisMonth, - startOfDay, - startOfMonth, -} from 'date-fns'; -import { AnimatePresence, motion } from 'framer-motion'; -import { useContext, useMemo, useState } from 'react'; - -import { CalendarContext } from '@/activities/calendar/contexts/CalendarContext'; -import { getCalendarEventEndDate } from '@/activities/calendar/utils/getCalendarEventEndDate'; -import { getCalendarEventStartDate } from '@/activities/calendar/utils/getCalendarEventStartDate'; -import { hasCalendarEventEnded } from '@/activities/calendar/utils/hasCalendarEventEnded'; -import { hasCalendarEventStarted } from '@/activities/calendar/utils/hasCalendarEventStarted'; -import { TimelineCalendarEvent } from '~/generated/graphql'; - -type CalendarCurrentEventCursorProps = { - calendarEvent: TimelineCalendarEvent; -}; - -const StyledDot = styled(motion.div)` - background-color: ${({ theme }) => theme.font.color.danger}; - border-radius: 1px; - height: ${({ theme }) => theme.spacing(1)}; - width: ${({ theme }) => theme.spacing(1)}; -`; - -const StyledCurrentEventCursor = styled(motion.div)` - align-items: center; - background-color: ${({ theme }) => theme.font.color.danger}; - display: inline-flex; - height: 1.5px; - left: 0; - position: absolute; - right: 0; - border-radius: ${({ theme }) => theme.border.radius.sm}; - transform: translateY(-50%); -`; - -export const CalendarCurrentEventCursor = ({ - calendarEvent, -}: CalendarCurrentEventCursorProps) => { - const theme = useTheme(); - const { - calendarEventsByDayTime, - currentCalendarEvent, - getNextCalendarEvent, - updateCurrentCalendarEvent, - } = useContext(CalendarContext); - - const nextCalendarEvent = getNextCalendarEvent(calendarEvent); - const nextCalendarEventStartsAt = nextCalendarEvent - ? getCalendarEventStartDate(nextCalendarEvent) - : undefined; - const isNextEventThisMonth = - !!nextCalendarEventStartsAt && isThisMonth(nextCalendarEventStartsAt); - - const calendarEventStartsAt = getCalendarEventStartDate(calendarEvent); - const calendarEventEndsAt = getCalendarEventEndDate(calendarEvent); - - const isCurrent = currentCalendarEvent?.id === calendarEvent.id; - const [hasStarted, setHasStarted] = useState( - hasCalendarEventStarted(calendarEvent), - ); - const [hasEnded, setHasEnded] = useState( - hasCalendarEventEnded(calendarEvent), - ); - const [isWaiting, setIsWaiting] = useState(hasEnded && !isNextEventThisMonth); - - const dayTime = startOfDay(calendarEventStartsAt).getTime(); - const dayEvents = calendarEventsByDayTime[dayTime]; - const isFirstEventOfDay = dayEvents?.slice(-1)[0] === calendarEvent; - const isLastEventOfDay = dayEvents?.[0] === calendarEvent; - - const topOffset = isLastEventOfDay ? 9 : 6; - const bottomOffset = isFirstEventOfDay ? 9 : 6; - - const currentEventCursorVariants = { - beforeEvent: { top: `calc(100% + ${bottomOffset}px)` }, - eventStart: { - top: 'calc(100% + 3px)', - transition: { - delay: Math.max( - 0, - differenceInSeconds(calendarEventStartsAt, new Date()), - ), - }, - }, - eventEnd: { - top: `-${topOffset}px`, - transition: { - delay: Math.max( - 0, - differenceInSeconds(calendarEventEndsAt, new Date()) + 1, - ), - }, - }, - fadeAway: { - opacity: 0, - top: `-${topOffset}px`, - transition: { - delay: - isWaiting && nextCalendarEventStartsAt - ? differenceInSeconds( - startOfMonth(nextCalendarEventStartsAt), - new Date(), - ) - : 0, - }, - }, - }; - - const animationSequence = useMemo(() => { - if (!hasStarted) return { initial: 'beforeEvent', animate: 'eventStart' }; - - if (!hasEnded) { - return { initial: 'eventStart', animate: 'eventEnd' }; - } - - if (!isWaiting) { - return { initial: undefined, animate: 'eventEnd' }; - } - - return { initial: 'eventEnd', animate: 'fadeAway' }; - }, [hasEnded, hasStarted, isWaiting]); - - return ( - - {isCurrent && ( - { - if (stage === 'eventStart') { - setHasStarted(true); - } - - if (stage === 'eventEnd') { - setHasEnded(true); - - if (isNextEventThisMonth) { - updateCurrentCalendarEvent(); - } - // If the next event is not the same month as the current event, - // we don't want the cursor to jump to the next month until the next month starts. - // => Wait for the upcoming event's month to start before moving the cursor. - // Example: we're in March. The previous event is February 15th, and the next event is April 10th. - // We want the cursor to stay in February until April starts. - else { - setIsWaiting(true); - } - } - - if (isWaiting && stage === 'fadeAway') { - setIsWaiting(false); - updateCurrentCalendarEvent(); - } - }} - transition={{ duration: theme.animation.duration.normal }} - > - - - - - )} - - ); -}; diff --git a/packages/twenty-front/src/modules/activities/calendar/components/CalendarDayCardContent.tsx b/packages/twenty-front/src/modules/activities/calendar/components/CalendarDayCardContent.tsx index 48839ed8b..0f46422f1 100644 --- a/packages/twenty-front/src/modules/activities/calendar/components/CalendarDayCardContent.tsx +++ b/packages/twenty-front/src/modules/activities/calendar/components/CalendarDayCardContent.tsx @@ -4,8 +4,8 @@ import { differenceInSeconds, endOfDay, format } from 'date-fns'; import { CalendarEventRow } from '@/activities/calendar/components/CalendarEventRow'; import { getCalendarEventStartDate } from '@/activities/calendar/utils/getCalendarEventStartDate'; -import { TimelineCalendarEvent } from '~/generated/graphql'; import { CardContent } from 'twenty-ui/layout'; +import { TimelineCalendarEvent } from '~/generated/graphql'; type CalendarDayCardContentProps = { calendarEvents: TimelineCalendarEvent[]; diff --git a/packages/twenty-front/src/modules/activities/calendar/components/CalendarEventRow.tsx b/packages/twenty-front/src/modules/activities/calendar/components/CalendarEventRow.tsx index e0a484dae..e8ce9f753 100644 --- a/packages/twenty-front/src/modules/activities/calendar/components/CalendarEventRow.tsx +++ b/packages/twenty-front/src/modules/activities/calendar/components/CalendarEventRow.tsx @@ -1,13 +1,10 @@ import { css, useTheme } from '@emotion/react'; import styled from '@emotion/styled'; import { format } from 'date-fns'; -import { useContext } from 'react'; import { useRecoilValue } from 'recoil'; -import { CalendarCurrentEventCursor } from '@/activities/calendar/components/CalendarCurrentEventCursor'; import { CalendarEventNotSharedContent } from '@/activities/calendar/components/CalendarEventNotSharedContent'; import { CalendarEventParticipantsAvatarGroup } from '@/activities/calendar/components/CalendarEventParticipantsAvatarGroup'; -import { CalendarContext } from '@/activities/calendar/contexts/CalendarContext'; import { getCalendarEventEndDate } from '@/activities/calendar/utils/getCalendarEventEndDate'; import { getCalendarEventStartDate } from '@/activities/calendar/utils/getCalendarEventStartDate'; import { hasCalendarEventEnded } from '@/activities/calendar/utils/hasCalendarEventEnded'; @@ -88,7 +85,6 @@ export const CalendarEventRow = ({ }: CalendarEventRowProps) => { const theme = useTheme(); const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState); - const { displayCurrentEventCursor = false } = useContext(CalendarContext); const { openCalendarEventInCommandMenu } = useOpenCalendarEventInCommandMenu(); @@ -143,9 +139,6 @@ export const CalendarEventRow = ({ participants={calendarEvent.participants} /> )} - {displayCurrentEventCursor && ( - - )} ); }; diff --git a/packages/twenty-front/src/modules/activities/calendar/contexts/CalendarContext.ts b/packages/twenty-front/src/modules/activities/calendar/contexts/CalendarContext.ts index e012d90f2..38b23229c 100644 --- a/packages/twenty-front/src/modules/activities/calendar/contexts/CalendarContext.ts +++ b/packages/twenty-front/src/modules/activities/calendar/contexts/CalendarContext.ts @@ -4,16 +4,8 @@ import { TimelineCalendarEvent } from '~/generated/graphql'; type CalendarContextValue = { calendarEventsByDayTime: Record; - currentCalendarEvent?: TimelineCalendarEvent; - displayCurrentEventCursor?: boolean; - getNextCalendarEvent: ( - calendarEvent: TimelineCalendarEvent, - ) => TimelineCalendarEvent | undefined; - updateCurrentCalendarEvent: () => void; }; export const CalendarContext = createContext({ calendarEventsByDayTime: {}, - getNextCalendarEvent: () => undefined, - updateCurrentCalendarEvent: () => {}, }); diff --git a/packages/twenty-front/src/modules/activities/calendar/hooks/__tests__/useCalendarEvents.test.tsx b/packages/twenty-front/src/modules/activities/calendar/hooks/__tests__/useCalendarEvents.test.tsx index 1c62ae6fe..2d4f58640 100644 --- a/packages/twenty-front/src/modules/activities/calendar/hooks/__tests__/useCalendarEvents.test.tsx +++ b/packages/twenty-front/src/modules/activities/calendar/hooks/__tests__/useCalendarEvents.test.tsx @@ -1,4 +1,4 @@ -import { act, renderHook } from '@testing-library/react'; +import { renderHook } from '@testing-library/react'; import { useCalendarEvents } from '@/activities/calendar/hooks/useCalendarEvents'; import { @@ -85,20 +85,13 @@ const calendarEvents: TimelineCalendarEvent[] = [ }, ]; -describe('useCalendar', () => { - it('returns calendar events', () => { +describe('useCalendarEvents', () => { + it('returns calendar events grouped by day time', () => { const { result } = renderHook(() => useCalendarEvents(calendarEvents)); - expect(result.current.currentCalendarEvent).toBe(calendarEvents[0]); - - expect(result.current.getNextCalendarEvent(calendarEvents[1])).toBe( - calendarEvents[0], - ); - - act(() => { - result.current.updateCurrentCalendarEvent(); - }); - - expect(result.current.currentCalendarEvent).toBe(calendarEvents[0]); + expect(result.current.calendarEventsByDayTime).toBeDefined(); + expect(result.current.daysByMonthTime).toBeDefined(); + expect(result.current.monthTimes).toBeDefined(); + expect(result.current.monthTimesByYear).toBeDefined(); }); }); diff --git a/packages/twenty-front/src/modules/activities/calendar/hooks/useCalendarEvents.ts b/packages/twenty-front/src/modules/activities/calendar/hooks/useCalendarEvents.ts index 329bda154..8283633fc 100644 --- a/packages/twenty-front/src/modules/activities/calendar/hooks/useCalendarEvents.ts +++ b/packages/twenty-front/src/modules/activities/calendar/hooks/useCalendarEvents.ts @@ -1,12 +1,9 @@ -import { getYear, isThisMonth, startOfDay, startOfMonth } from 'date-fns'; -import { useMemo, useState } from 'react'; +import { getYear, startOfDay, startOfMonth } from 'date-fns'; -import { findUpcomingCalendarEvent } from '@/activities/calendar/utils/findUpcomingCalendarEvent'; import { getCalendarEventStartDate } from '@/activities/calendar/utils/getCalendarEventStartDate'; import { TimelineCalendarEvent } from '~/generated/graphql'; import { groupArrayItemsBy } from '~/utils/array/groupArrayItemsBy'; import { sortDesc } from '~/utils/sort'; -import { isDefined } from 'twenty-shared/utils'; export const useCalendarEvents = (calendarEvents: TimelineCalendarEvent[]) => { const calendarEventsByDayTime = groupArrayItemsBy( @@ -29,53 +26,10 @@ export const useCalendarEvents = (calendarEvents: TimelineCalendarEvent[]) => { const monthTimesByYear = groupArrayItemsBy(sortedMonthTimes, getYear); - const getPreviousCalendarEvent = (calendarEvent: TimelineCalendarEvent) => { - const calendarEventIndex = calendarEvents.indexOf(calendarEvent); - return calendarEventIndex < calendarEvents.length - 1 - ? calendarEvents[calendarEventIndex + 1] - : undefined; - }; - - const getNextCalendarEvent = (calendarEvent: TimelineCalendarEvent) => { - const calendarEventIndex = calendarEvents.indexOf(calendarEvent); - return calendarEventIndex > 0 - ? calendarEvents[calendarEventIndex - 1] - : undefined; - }; - - const initialUpcomingCalendarEvent = useMemo( - () => findUpcomingCalendarEvent(calendarEvents), - [calendarEvents], - ); - const lastEventInCalendar = calendarEvents.length - ? calendarEvents[0] - : undefined; - - const [currentCalendarEvent, setCurrentCalendarEvent] = useState( - (initialUpcomingCalendarEvent && - (isThisMonth(getCalendarEventStartDate(initialUpcomingCalendarEvent)) - ? initialUpcomingCalendarEvent - : getPreviousCalendarEvent(initialUpcomingCalendarEvent))) || - lastEventInCalendar, - ); - - const updateCurrentCalendarEvent = () => { - if (!currentCalendarEvent) return; - - const nextCurrentCalendarEvent = getNextCalendarEvent(currentCalendarEvent); - - if (isDefined(nextCurrentCalendarEvent)) { - setCurrentCalendarEvent(nextCurrentCalendarEvent); - } - }; - return { calendarEventsByDayTime, - currentCalendarEvent, daysByMonthTime, - getNextCalendarEvent, monthTimes: sortedMonthTimes, monthTimesByYear, - updateCurrentCalendarEvent, }; }; diff --git a/packages/twenty-front/src/modules/activities/calendar/utils/__tests__/findUpcomingCalendarEvent.test.ts b/packages/twenty-front/src/modules/activities/calendar/utils/__tests__/findUpcomingCalendarEvent.test.ts deleted file mode 100644 index dce86cc71..000000000 --- a/packages/twenty-front/src/modules/activities/calendar/utils/__tests__/findUpcomingCalendarEvent.test.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { addDays, addHours, startOfDay, subDays, subHours } from 'date-fns'; - -import { CalendarEvent } from '@/activities/calendar/types/CalendarEvent'; - -import { findUpcomingCalendarEvent } from '../findUpcomingCalendarEvent'; - -const pastEvent: Pick = { - startsAt: subHours(new Date(), 2).toISOString(), - endsAt: subHours(new Date(), 1).toISOString(), - isFullDay: false, -}; -const fullDayPastEvent: Pick = { - startsAt: subDays(new Date(), 1).toISOString(), - isFullDay: true, -}; - -const currentEvent: Pick = { - startsAt: addHours(new Date(), 1).toISOString(), - endsAt: addHours(new Date(), 2).toISOString(), - isFullDay: false, -}; -const currentFullDayEvent: Pick = { - startsAt: startOfDay(new Date()).toISOString(), - isFullDay: true, -}; - -const futureEvent: Pick = { - startsAt: addDays(new Date(), 1).toISOString(), - endsAt: addDays(new Date(), 2).toISOString(), - isFullDay: false, -}; -const fullDayFutureEvent: Pick = { - startsAt: addDays(new Date(), 2).toISOString(), - isFullDay: false, -}; - -describe('findUpcomingCalendarEvent', () => { - it('returns the first current event by chronological order', () => { - // Given - const calendarEvents = [ - futureEvent, - currentFullDayEvent, - pastEvent, - currentEvent, - ]; - - // When - const result = findUpcomingCalendarEvent(calendarEvents); - - // Then - expect(result).toEqual(currentFullDayEvent); - }); - - it('returns the next future event by chronological order', () => { - // Given - const calendarEvents = [ - fullDayPastEvent, - fullDayFutureEvent, - futureEvent, - pastEvent, - ]; - - // When - const result = findUpcomingCalendarEvent(calendarEvents); - - // Then - expect(result).toEqual(futureEvent); - }); - - it('returns undefined if all events are in the past', () => { - // Given - const calendarEvents = [pastEvent, fullDayPastEvent]; - - // When - const result = findUpcomingCalendarEvent(calendarEvents); - - // Then - expect(result).toBeUndefined(); - }); -}); diff --git a/packages/twenty-front/src/modules/activities/calendar/utils/findUpcomingCalendarEvent.ts b/packages/twenty-front/src/modules/activities/calendar/utils/findUpcomingCalendarEvent.ts deleted file mode 100644 index 250a45112..000000000 --- a/packages/twenty-front/src/modules/activities/calendar/utils/findUpcomingCalendarEvent.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { CalendarEvent } from '@/activities/calendar/types/CalendarEvent'; -import { hasCalendarEventEnded } from '@/activities/calendar/utils/hasCalendarEventEnded'; -import { sortCalendarEventsAsc } from '@/activities/calendar/utils/sortCalendarEvents'; - -export const findUpcomingCalendarEvent = < - T extends Pick, ->( - calendarEvents: T[], -) => - [...calendarEvents] - .sort(sortCalendarEventsAsc) - .find((calendarEvent) => !hasCalendarEventEnded(calendarEvent)); diff --git a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsCalendarChannelsGeneral.tsx b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsCalendarChannelsGeneral.tsx index 14898148c..4ab66f3fb 100644 --- a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsCalendarChannelsGeneral.tsx +++ b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsCalendarChannelsGeneral.tsx @@ -3,15 +3,15 @@ import { CalendarContext } from '@/activities/calendar/contexts/CalendarContext' import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; import { SettingsAccountsCalendarDisplaySettings } from '@/settings/accounts/components/SettingsAccountsCalendarDisplaySettings'; import styled from '@emotion/styled'; +import { t } from '@lingui/core/macro'; import { Section } from '@react-email/components'; import { addMinutes, endOfDay, min, startOfDay } from 'date-fns'; import { useRecoilValue } from 'recoil'; +import { H2Title } from 'twenty-ui/display'; import { CalendarChannelVisibility, TimelineCalendarEvent, } from '~/generated/graphql'; -import { t } from '@lingui/core/macro'; -import { H2Title } from 'twenty-ui/display'; const StyledGeneralContainer = styled.div` display: flex; @@ -78,12 +78,9 @@ export const SettingsAccountsCalendarChannelsGeneral = () => { /> undefined, - updateCurrentCalendarEvent: () => {}, }} >