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;
};