Skip to content

Commit

Permalink
feat: open event details drawer on event row click
Browse files Browse the repository at this point in the history
Closes #4294
  • Loading branch information
thaisguigon committed Mar 13, 2024
1 parent a02e11f commit aa75801
Show file tree
Hide file tree
Showing 28 changed files with 519 additions and 213 deletions.
1 change: 1 addition & 0 deletions packages/twenty-front/.eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ module.exports = {
],
},
],
'no-extra-boolean-cast': 'off',

'@nx/workspace-effect-components': 'error',
'@nx/workspace-no-hardcoded-colors': 'error',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ import { format, getYear } from 'date-fns';
import { CalendarMonthCard } from '@/activities/calendar/components/CalendarMonthCard';
import { CalendarContext } from '@/activities/calendar/contexts/CalendarContext';
import { useCalendarEvents } from '@/activities/calendar/hooks/useCalendarEvents';
import { sortCalendarEventsDesc } from '@/activities/calendar/utils/sortCalendarEvents';
import { CalendarEvent } from '@/activities/calendar/types/CalendarEvent';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
import { H3Title } from '@/ui/display/typography/components/H3Title';
import { Section } from '@/ui/layout/section/components/Section';
import { mockedCalendarEvents } from '~/testing/mock-data/calendar';

const StyledContainer = styled.div`
box-sizing: border-box;
Expand All @@ -23,9 +24,11 @@ const StyledYear = styled.span`
`;

export const Calendar = () => {
const sortedCalendarEvents = [...mockedCalendarEvents].sort(
sortCalendarEventsDesc,
);
const { records: calendarEvents } = useFindManyRecords<CalendarEvent>({
objectNameSingular: CoreObjectNameSingular.CalendarEvent,
orderBy: { startsAt: 'DescNullsLast', endsAt: 'DescNullsLast' },
useRecordsWithoutConnection: true,
});

const {
calendarEventsByDayTime,
Expand All @@ -35,7 +38,13 @@ export const Calendar = () => {
monthTimes,
monthTimesByYear,
updateCurrentCalendarEvent,
} = useCalendarEvents(sortedCalendarEvents);
} = useCalendarEvents(
calendarEvents.map((calendarEvent) => ({
...calendarEvent,
// TODO: retrieve CalendarChannel visibility from backend
visibility: 'SHARE_EVERYTHING',
})),
);

return (
<CalendarContext.Provider
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { AnimatePresence, motion } from 'framer-motion';
import { CalendarContext } from '@/activities/calendar/contexts/CalendarContext';
import { CalendarEvent } from '@/activities/calendar/types/CalendarEvent';
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';

Expand Down Expand Up @@ -52,12 +53,16 @@ export const CalendarCurrentEventCursor = ({
} = useContext(CalendarContext);

const nextCalendarEvent = getNextCalendarEvent(calendarEvent);
const nextCalendarEventStartsAt = nextCalendarEvent
? getCalendarEventStartDate(nextCalendarEvent)
: undefined;
const isNextEventThisMonth =
!!nextCalendarEvent && isThisMonth(nextCalendarEvent.startsAt);
!!nextCalendarEventStartsAt && isThisMonth(nextCalendarEventStartsAt);

const calendarEventStartsAt = getCalendarEventStartDate(calendarEvent);
const calendarEventEndsAt = getCalendarEventEndDate(calendarEvent);

const isCurrent = currentCalendarEvent.id === calendarEvent.id;
const isCurrent = currentCalendarEvent?.id === calendarEvent.id;
const [hasStarted, setHasStarted] = useState(
hasCalendarEventStarted(calendarEvent),
);
Expand All @@ -66,7 +71,7 @@ export const CalendarCurrentEventCursor = ({
);
const [isWaiting, setIsWaiting] = useState(hasEnded && !isNextEventThisMonth);

const dayTime = startOfDay(calendarEvent.startsAt).getTime();
const dayTime = startOfDay(calendarEventStartsAt).getTime();
const dayEvents = calendarEventsByDayTime[dayTime];
const isFirstEventOfDay = dayEvents?.slice(-1)[0] === calendarEvent;
const isLastEventOfDay = dayEvents?.[0] === calendarEvent;
Expand All @@ -81,7 +86,7 @@ export const CalendarCurrentEventCursor = ({
transition: {
delay: Math.max(
0,
differenceInSeconds(calendarEvent.startsAt, new Date()),
differenceInSeconds(calendarEventStartsAt, new Date()),
),
},
},
Expand All @@ -99,9 +104,9 @@ export const CalendarCurrentEventCursor = ({
top: `-${topOffset}px`,
transition: {
delay:
isWaiting && nextCalendarEvent
isWaiting && nextCalendarEventStartsAt
? differenceInSeconds(
startOfMonth(nextCalendarEvent.startsAt),
startOfMonth(nextCalendarEventStartsAt),
new Date(),
)
: 0,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { differenceInSeconds, endOfDay, format } from 'date-fns';

import { CalendarEventRow } from '@/activities/calendar/components/CalendarEventRow';
import { CalendarEvent } from '@/activities/calendar/types/CalendarEvent';
import { getCalendarEventStartDate } from '@/activities/calendar/utils/getCalendarEventStartDate';
import { CardContent } from '@/ui/layout/card/components/CardContent';

type CalendarDayCardContentProps = {
Expand Down Expand Up @@ -53,8 +54,8 @@ export const CalendarDayCardContent = ({
}: CalendarDayCardContentProps) => {
const theme = useTheme();

const endOfDayDate = endOfDay(calendarEvents[0].startsAt);
const endsIn = differenceInSeconds(endOfDayDate, Date.now());
const endOfDayDate = endOfDay(getCalendarEventStartDate(calendarEvents[0]));
const dayEndsIn = differenceInSeconds(endOfDayDate, Date.now());

const weekDayLabel = format(endOfDayDate, 'EE');
const monthDayLabel = format(endOfDayDate, 'dd');
Expand All @@ -71,7 +72,7 @@ export const CalendarDayCardContent = ({
animate="ended"
variants={upcomingDayCardContentVariants}
transition={{
delay: Math.max(0, endsIn),
delay: Math.max(0, dayEndsIn),
duration: theme.animation.duration.fast,
}}
>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import { css, useTheme } from '@emotion/react';
import styled from '@emotion/styled';

import { CalendarEvent } from '@/activities/calendar/types/CalendarEvent';
import { useObjectMetadataItemOnly } from '@/object-metadata/hooks/useObjectMetadataItemOnly';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { formatFieldMetadataItemAsFieldDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsFieldDefinition';
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
import {
FieldContext,
RecordUpdateHook,
RecordUpdateHookParams,
} from '@/object-record/record-field/contexts/FieldContext';
import { FieldDefinition } from '@/object-record/record-field/types/FieldDefinition';
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
import { RecordInlineCell } from '@/object-record/record-inline-cell/components/RecordInlineCell';
import { PropertyBox } from '@/object-record/record-inline-cell/property-box/components/PropertyBox';
import { InlineCellHotkeyScope } from '@/object-record/record-inline-cell/types/InlineCellHotkeyScope';
import {
Chip,
ChipAccent,
ChipSize,
ChipVariant,
} from '@/ui/display/chip/components/Chip';
import { IconCalendarEvent } from '@/ui/display/icon';
import { mapArrayToObject } from '~/utils/array/mapArrayToObject';
import { beautifyPastDateRelativeToNow } from '~/utils/date-utils';

type CalendarEventDetailsProps = {
calendarEvent: CalendarEvent;
};

const StyledContainer = styled.div`
background: ${({ theme }) => theme.background.secondary};
align-items: flex-start;
border-bottom: 1px solid ${({ theme }) => theme.border.color.medium};
display: flex;
flex-direction: column;
gap: ${({ theme }) => theme.spacing(6)};
padding: ${({ theme }) => theme.spacing(6)};
`;

const StyledEventChip = styled(Chip)`
gap: ${({ theme }) => theme.spacing(2)};
padding-left: ${({ theme }) => theme.spacing(2)};
padding-right: ${({ theme }) => theme.spacing(2)};
`;

const StyledHeader = styled.header``;

const StyledTitle = styled.h2<{ canceled?: boolean }>`
color: ${({ theme }) => theme.font.color.primary};
font-weight: ${({ theme }) => theme.font.weight.semiBold};
margin: ${({ theme }) => theme.spacing(0, 0, 2)};
${({ canceled }) =>
canceled &&
css`
text-decoration: line-through;
`}
`;

const StyledCreatedAt = styled.div`
color: ${({ theme }) => theme.font.color.tertiary};
`;

const StyledFields = styled.div`
display: flex;
flex-direction: column;
gap: ${({ theme }) => theme.spacing(3)};
`;

const StyledPropertyBox = styled(PropertyBox)`
height: ${({ theme }) => theme.spacing(6)};
padding: 0;
`;

export const CalendarEventDetails = ({
calendarEvent,
}: CalendarEventDetailsProps) => {
const theme = useTheme();
const { objectMetadataItem } = useObjectMetadataItemOnly({
objectNameSingular: CoreObjectNameSingular.CalendarEvent,
});
const { updateOneRecord: updateOneCalendarEvent } =
useUpdateOneRecord<CalendarEvent>({
objectNameSingular: CoreObjectNameSingular.CalendarEvent,
});

const useUpdateOneCalendarEventMutation: RecordUpdateHook = () => {
const updateEntity = ({ variables }: RecordUpdateHookParams) =>
updateOneCalendarEvent({
idToUpdate: variables.where.id as string,
updateOneRecordInput: variables.updateOneRecordInput,
});

return [updateEntity, { loading: false }];
};

const fieldsToDisplay: Partial<
Record<
keyof CalendarEvent,
Partial<Pick<FieldDefinition<FieldMetadata>, 'label'>>
>
> = {
startsAt: { label: 'Start Date' },
endsAt: { label: 'End Date' },
conferenceUri: { label: 'Meet link' },
location: {},
description: {},
};
const fieldsByName = mapArrayToObject(
objectMetadataItem.fields,
({ name }) => name,
);

return (
<StyledContainer>
<StyledEventChip
accent={ChipAccent.TextSecondary}
size={ChipSize.Large}
variant={ChipVariant.Highlighted}
clickable={false}
leftComponent={<IconCalendarEvent size={theme.icon.size.md} />}
label="Event"
/>
<StyledHeader>
<StyledTitle canceled={calendarEvent.isCanceled}>
{calendarEvent.title}
</StyledTitle>
<StyledCreatedAt>
Created{' '}
{beautifyPastDateRelativeToNow(
new Date(calendarEvent.externalCreatedAt),
)}
</StyledCreatedAt>
</StyledHeader>
<StyledFields>
{Object.entries(fieldsToDisplay).map(([fieldName, fieldOverride]) => (
<StyledPropertyBox key={fieldName}>
<FieldContext.Provider
value={{
entityId: calendarEvent.id,
recoilScopeId: `${calendarEvent.id}-${fieldName}`,
isLabelIdentifier: false,
fieldDefinition: formatFieldMetadataItemAsFieldDefinition({
field: {
...fieldsByName[fieldName],
...fieldOverride,
},
objectMetadataItem,
showLabel: true,
labelWidth: 72,
}),
useUpdateRecord: useUpdateOneCalendarEventMutation,
hotkeyScope: InlineCellHotkeyScope.InlineCell,
}}
>
<RecordInlineCell />
</FieldContext.Provider>
</StyledPropertyBox>
))}
</StyledFields>
</StyledContainer>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ import { useRecoilValue } from 'recoil';

import { CalendarCurrentEventCursor } from '@/activities/calendar/components/CalendarCurrentEventCursor';
import { CalendarContext } from '@/activities/calendar/contexts/CalendarContext';
import { useOpenCalendarEventRightDrawer } from '@/activities/calendar/right-drawer/hooks/useOpenCalendarEventRightDrawer';
import { CalendarEvent } from '@/activities/calendar/types/CalendarEvent';
import { getCalendarEventEndDate } from '@/activities/calendar/utils/getCalendarEventEndDate';
import { getCalendarEventStartDate } from '@/activities/calendar/utils/getCalendarEventStartDate';
import { hasCalendarEventEnded } from '@/activities/calendar/utils/hasCalendarEventEnded';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { IconArrowRight, IconLock } from '@/ui/display/icon';
Expand All @@ -22,12 +24,13 @@ type CalendarEventRowProps = {
className?: string;
};

const StyledContainer = styled.div`
const StyledContainer = styled.div<{ showTitle?: boolean }>`
align-items: center;
display: inline-flex;
gap: ${({ theme }) => theme.spacing(3)};
height: ${({ theme }) => theme.spacing(6)};
position: relative;
cursor: ${({ showTitle }) => (showTitle ? 'pointer' : 'not-allowed')};
`;

const StyledAttendanceIndicator = styled.div<{ active?: boolean }>`
Expand Down Expand Up @@ -101,21 +104,32 @@ export const CalendarEventRow = ({
const theme = useTheme();
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState());
const { displayCurrentEventCursor = false } = useContext(CalendarContext);
const { openCalendarEventRightDrawer } = useOpenCalendarEventRightDrawer();

const startsAt = getCalendarEventStartDate(calendarEvent);
const endsAt = getCalendarEventEndDate(calendarEvent);
const hasEnded = hasCalendarEventEnded(calendarEvent);

const startTimeLabel = calendarEvent.isFullDay
? 'All day'
: format(calendarEvent.startsAt, 'HH:mm');
: format(startsAt, 'HH:mm');
const endTimeLabel = calendarEvent.isFullDay ? '' : format(endsAt, 'HH:mm');

const isCurrentWorkspaceMemberAttending = !!calendarEvent.attendees?.find(
const isCurrentWorkspaceMemberAttending = calendarEvent.attendees?.some(
({ workspaceMemberId }) => workspaceMemberId === currentWorkspaceMember?.id,
);
const showTitle = calendarEvent.visibility === 'SHARE_EVERYTHING';

return (
<StyledContainer className={className}>
<StyledContainer
className={className}
showTitle={showTitle}
onClick={
showTitle
? () => openCalendarEventRightDrawer(calendarEvent.id)
: undefined
}
>
<StyledAttendanceIndicator active={isCurrentWorkspaceMemberAttending} />
<StyledLabels>
<StyledTime>
Expand All @@ -127,17 +141,17 @@ export const CalendarEventRow = ({
</>
)}
</StyledTime>
{calendarEvent.visibility === 'METADATA' ? (
{showTitle ? (
<StyledTitle active={!hasEnded} canceled={!!calendarEvent.isCanceled}>
{calendarEvent.title}
</StyledTitle>
) : (
<StyledVisibilityCard active={!hasEnded}>
<StyledVisibilityCardContent>
<IconLock size={theme.icon.size.sm} />
Not shared
</StyledVisibilityCardContent>
</StyledVisibilityCard>
) : (
<StyledTitle active={!hasEnded} canceled={!!calendarEvent.isCanceled}>
{calendarEvent.title}
</StyledTitle>
)}
</StyledLabels>
{!!calendarEvent.attendees?.length && (
Expand Down
Loading

0 comments on commit aa75801

Please sign in to comment.