Fix calendar events & messages fetching + fix timeline design (#11840)

Preview : 

<img width="501" alt="Screenshot 2025-05-02 at 16 24 34"
src="https://github.com/user-attachments/assets/0c649df1-0e26-4ddc-8e13-ebd78af7ec09"
/>


Done : 
- Fix getCalendarEventsFromPersonIds and getCalendarEventsFromCompanyId
(include accountOwner check)
- Fix permission check on pre-hook - Pre-hook seems useless, calendar
events are always on METADATA or SHARE_EVERYTHING visibility, else post
hook always has the responsibility of returning the data user can
access. >> To delete or to keep in case other visibility options are
added ?
- Add post hook to secure finOne / findMany calendarEvents resolver
- Update design

To do :
- same on messages (PR to arrive)

closes : https://github.com/twentyhq/twenty/issues/9826
This commit is contained in:
Etienne
2025-05-05 13:12:16 +02:00
committed by GitHub
parent d0d872fdd0
commit 521e75981a
20 changed files with 832 additions and 97 deletions

View File

@ -0,0 +1,36 @@
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { IconLock } from 'twenty-ui/display';
import { Card, CardContent } from 'twenty-ui/layout';
const StyledVisibilityCard = styled(Card)`
border-color: ${({ theme }) => theme.border.color.light};
color: ${({ theme }) => theme.font.color.light};
flex: 1;
transition: color ${({ theme }) => theme.animation.duration.normal} ease;
width: 100%;
`;
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 CalendarEventNotSharedContent = () => {
const theme = useTheme();
return (
<StyledVisibilityCard>
<StyledVisibilityCardContent>
<IconLock size={theme.icon.size.sm} />
Not shared
</StyledVisibilityCardContent>
</StyledVisibilityCard>
);
};

View File

@ -0,0 +1,68 @@
import { CalendarEventParticipant } from '@/activities/calendar/types/CalendarEventParticipant';
import { isTimelineCalendarEventParticipant } from '@/activities/calendar/types/guards/IsTimelineCalendarEventParticipant';
import { isDefined } from 'twenty-shared/utils';
import { Avatar, AvatarGroup } from 'twenty-ui/display';
import { TimelineCalendarEventParticipant } from '~/generated-metadata/graphql';
type CalendarEventParticipantsAvatarGroupProps = {
participants: CalendarEventParticipant[] | TimelineCalendarEventParticipant[];
};
export const CalendarEventParticipantsAvatarGroup = ({
participants,
}: CalendarEventParticipantsAvatarGroupProps) => {
const timelineParticipants: TimelineCalendarEventParticipant[] =
participants.map((participant) => {
if (isTimelineCalendarEventParticipant(participant)) {
return participant;
} else {
return {
personId: participant.person?.id ?? null,
workspaceMemberId: participant.workspaceMember?.id ?? null,
firstName:
participant.person?.name?.firstName ||
participant.workspaceMember?.name.firstName ||
'',
lastName:
participant.person?.name?.lastName ||
participant.workspaceMember?.name.lastName ||
'',
displayName:
participant.person?.name?.firstName ||
participant.person?.name?.lastName ||
participant.workspaceMember?.name.firstName ||
participant.workspaceMember?.name.lastName ||
participant.displayName ||
participant.handle ||
'',
avatarUrl:
participant.person?.avatarUrl ||
participant.workspaceMember?.avatarUrl ||
'',
handle: participant.handle,
};
}
});
return (
<AvatarGroup
avatars={timelineParticipants.map((participant) => (
<Avatar
key={[participant.workspaceMemberId, participant.displayName]
.filter(isDefined)
.join('-')}
avatarUrl={participant.avatarUrl}
placeholder={
participant.firstName && participant.lastName
? `${participant.firstName} ${participant.lastName}`
: participant.displayName
}
placeholderColorSeed={
participant.workspaceMemberId || participant.personId
}
type="rounded"
/>
))}
/>
);
};

View File

@ -5,24 +5,19 @@ 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';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { useOpenCalendarEventInCommandMenu } from '@/command-menu/hooks/useOpenCalendarEventInCommandMenu';
import { IconArrowRight } from 'twenty-ui/display';
import {
CalendarChannelVisibility,
TimelineCalendarEvent,
} from '~/generated-metadata/graphql';
import { isDefined } from 'twenty-shared/utils';
import {
Avatar,
AvatarGroup,
IconArrowRight,
IconLock,
} from 'twenty-ui/display';
import { Card, CardContent } from 'twenty-ui/layout';
type CalendarEventRowProps = {
calendarEvent: TimelineCalendarEvent;
@ -87,25 +82,6 @@ const StyledTitle = styled.div<{ active: boolean; canceled: boolean }>`
`}
`;
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;
transition: color ${({ theme }) => theme.animation.duration.normal} ease;
`;
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,
@ -159,33 +135,12 @@ export const CalendarEventRow = ({
{calendarEvent.title}
</StyledTitle>
) : (
<StyledVisibilityCard active={!hasEnded}>
<StyledVisibilityCardContent>
<IconLock size={theme.icon.size.sm} />
Not shared
</StyledVisibilityCardContent>
</StyledVisibilityCard>
<CalendarEventNotSharedContent />
)}
</StyledLabels>
{!!calendarEvent.participants?.length && (
<AvatarGroup
avatars={calendarEvent.participants.map((participant) => (
<Avatar
key={[participant.workspaceMemberId, participant.displayName]
.filter(isDefined)
.join('-')}
avatarUrl={participant.avatarUrl}
placeholder={
participant.firstName && participant.lastName
? `${participant.firstName} ${participant.lastName}`
: participant.displayName
}
placeholderColorSeed={
participant.workspaceMemberId || participant.personId
}
type="rounded"
/>
))}
<CalendarEventParticipantsAvatarGroup
participants={calendarEvent.participants}
/>
)}
{displayCurrentEventCursor && (

View File

@ -0,0 +1,8 @@
import { CalendarEventParticipant } from '@/activities/calendar/types/CalendarEventParticipant';
import { TimelineCalendarEventParticipant } from '~/generated-metadata/graphql';
export const isTimelineCalendarEventParticipant = (
participant: CalendarEventParticipant | TimelineCalendarEventParticipant,
): participant is TimelineCalendarEventParticipant => {
return 'avatarUrl' in participant;
};

View File

@ -1,19 +1,22 @@
import styled from '@emotion/styled';
import { isUndefined } from '@sniptt/guards';
import { CalendarEventNotSharedContent } from '@/activities/calendar/components/CalendarEventNotSharedContent';
import { CalendarEventParticipantsAvatarGroup } from '@/activities/calendar/components/CalendarEventParticipantsAvatarGroup';
import { CalendarEvent } from '@/activities/calendar/types/CalendarEvent';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore';
import { UserContext } from '@/users/contexts/UserContext';
import { useContext } from 'react';
import { FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED } from 'twenty-shared/constants';
import { isDefined } from 'twenty-shared/utils';
import {
formatToHumanReadableDay,
formatToHumanReadableMonth,
formatToHumanReadableTime,
} from '~/utils/format/formatDate';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
import { isDefined } from 'twenty-shared/utils';
const StyledEventCardCalendarEventContainer = styled.div`
cursor: pointer;
@ -29,12 +32,14 @@ const StyledCalendarEventContent = styled.div`
gap: ${({ theme }) => theme.spacing(2)};
justify-content: center;
overflow: hidden;
width: 100%;
`;
const StyledCalendarEventTop = styled.div`
align-items: center;
align-self: stretch;
display: flex;
width: 100%;
gap: ${({ theme }) => theme.spacing(2)};
justify-content: space-between;
`;
@ -100,6 +105,12 @@ export const EventCardCalendarEvent = ({
title: true,
startsAt: true,
endsAt: true,
calendarEventParticipants: {
person: true,
workspaceMember: true,
handle: true,
displayName: true,
},
},
onCompleted: (data) => {
upsertRecords([data]);
@ -114,7 +125,7 @@ export const EventCardCalendarEvent = ({
);
if (shouldHideMessageContent) {
return <div>Calendar event not shared</div>;
return <CalendarEventNotSharedContent />;
}
const shouldHandleNotFound = error.graphQLErrors.some(
@ -160,10 +171,21 @@ export const EventCardCalendarEvent = ({
</StyledCalendarEventDateCard>
<StyledCalendarEventContent>
<StyledCalendarEventTop>
<StyledCalendarEventTitle>
{calendarEvent.title}
</StyledCalendarEventTitle>
{calendarEvent.title ===
FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED ? (
<CalendarEventNotSharedContent />
) : (
<StyledCalendarEventTitle>
{calendarEvent.title}
</StyledCalendarEventTitle>
)}
{!!calendarEvent.calendarEventParticipants?.length && (
<CalendarEventParticipantsAvatarGroup
participants={calendarEvent.calendarEventParticipants}
/>
)}
</StyledCalendarEventTop>
<StyledCalendarEventBody>
{startsAtHour} {endsAtHour && <> {endsAtHour}</>}
</StyledCalendarEventBody>