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:
@ -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>
|
||||
);
|
||||
};
|
||||
@ -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"
|
||||
/>
|
||||
))}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -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 && (
|
||||
|
||||
@ -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;
|
||||
};
|
||||
@ -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>
|
||||
|
||||
Reference in New Issue
Block a user