4810 display participants in the right drawer of the calendar event (#4896)
Closes #4810 - Introduces a new component `ExpandableList` which uses intersection observers to display the maximum number of elements possible
This commit is contained in:
@ -1,7 +1,9 @@
|
||||
import React from 'react';
|
||||
import { css, useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { IconCalendarEvent } from 'twenty-ui';
|
||||
|
||||
import { CalendarEventParticipantsResponseStatus } from '@/activities/calendar/components/CalendarEventParticipantsResponseStatus';
|
||||
import { CalendarEvent } from '@/activities/calendar/types/CalendarEvent';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
@ -30,6 +32,8 @@ const StyledContainer = styled.div`
|
||||
flex-direction: column;
|
||||
gap: ${({ theme }) => theme.spacing(6)};
|
||||
padding: ${({ theme }) => theme.spacing(6)};
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
`;
|
||||
|
||||
const StyledEventChip = styled(Chip)`
|
||||
@ -60,11 +64,13 @@ const StyledFields = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${({ theme }) => theme.spacing(3)};
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledPropertyBox = styled(PropertyBox)`
|
||||
height: ${({ theme }) => theme.spacing(6)};
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export const CalendarEventDetails = ({
|
||||
@ -88,6 +94,31 @@ export const CalendarEventDetails = ({
|
||||
({ name }) => name,
|
||||
);
|
||||
|
||||
const { calendarEventParticipants } = calendarEvent;
|
||||
|
||||
const Fields = fieldsToDisplay.map((fieldName) => (
|
||||
<StyledPropertyBox key={fieldName}>
|
||||
<FieldContext.Provider
|
||||
value={{
|
||||
entityId: calendarEvent.id,
|
||||
hotkeyScope: 'calendar-event-details',
|
||||
recoilScopeId: `${calendarEvent.id}-${fieldName}`,
|
||||
isLabelIdentifier: false,
|
||||
fieldDefinition: formatFieldMetadataItemAsFieldDefinition({
|
||||
field: fieldsByName[fieldName],
|
||||
objectMetadataItem,
|
||||
showLabel: true,
|
||||
labelWidth: 72,
|
||||
}),
|
||||
useUpdateRecord: () => [() => undefined, { loading: false }],
|
||||
maxWidth: 300,
|
||||
}}
|
||||
>
|
||||
<RecordInlineCell readonly />
|
||||
</FieldContext.Provider>
|
||||
</StyledPropertyBox>
|
||||
));
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
<StyledEventChip
|
||||
@ -110,27 +141,13 @@ export const CalendarEventDetails = ({
|
||||
</StyledCreatedAt>
|
||||
</StyledHeader>
|
||||
<StyledFields>
|
||||
{fieldsToDisplay.map((fieldName) => (
|
||||
<StyledPropertyBox key={fieldName}>
|
||||
<FieldContext.Provider
|
||||
value={{
|
||||
entityId: calendarEvent.id,
|
||||
hotkeyScope: 'calendar-event-details',
|
||||
recoilScopeId: `${calendarEvent.id}-${fieldName}`,
|
||||
isLabelIdentifier: false,
|
||||
fieldDefinition: formatFieldMetadataItemAsFieldDefinition({
|
||||
field: fieldsByName[fieldName],
|
||||
objectMetadataItem,
|
||||
showLabel: true,
|
||||
labelWidth: 72,
|
||||
}),
|
||||
useUpdateRecord: () => [() => undefined, { loading: false }],
|
||||
}}
|
||||
>
|
||||
<RecordInlineCell readonly />
|
||||
</FieldContext.Provider>
|
||||
</StyledPropertyBox>
|
||||
))}
|
||||
{Fields.slice(0, 2)}
|
||||
{calendarEventParticipants && (
|
||||
<CalendarEventParticipantsResponseStatus
|
||||
participants={calendarEventParticipants}
|
||||
/>
|
||||
)}
|
||||
{Fields.slice(2)}
|
||||
</StyledFields>
|
||||
</StyledContainer>
|
||||
);
|
||||
|
||||
@ -0,0 +1,42 @@
|
||||
import groupBy from 'lodash.groupby';
|
||||
|
||||
import { CalendarEventParticipantsResponseStatusField } from '@/activities/calendar/components/CalendarEventParticipantsResponseStatusField';
|
||||
import { CalendarEventParticipant } from '@/activities/calendar/types/CalendarEventParticipant';
|
||||
|
||||
export const CalendarEventParticipantsResponseStatus = ({
|
||||
participants,
|
||||
}: {
|
||||
participants: CalendarEventParticipant[];
|
||||
}) => {
|
||||
const groupedParticipants = groupBy(participants, (participant) => {
|
||||
switch (participant.responseStatus) {
|
||||
case 'ACCEPTED':
|
||||
return 'Yes';
|
||||
case 'DECLINED':
|
||||
return 'No';
|
||||
case 'NEEDS_ACTION':
|
||||
case 'TENTATIVE':
|
||||
return 'Maybe';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
});
|
||||
|
||||
const responseStatusOrder: ('Yes' | 'Maybe' | 'No')[] = [
|
||||
'Yes',
|
||||
'Maybe',
|
||||
'No',
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
{responseStatusOrder.map((responseStatus) => (
|
||||
<CalendarEventParticipantsResponseStatusField
|
||||
key={responseStatus}
|
||||
responseStatus={responseStatus}
|
||||
participants={groupedParticipants[responseStatus] || []}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,108 @@
|
||||
import { useRef } from 'react';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { IconCheck, IconQuestionMark, IconX } from 'twenty-ui';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { CalendarEventParticipant } from '@/activities/calendar/types/CalendarEventParticipant';
|
||||
import { ParticipantChip } from '@/activities/components/ParticipantChip';
|
||||
import { PropertyBox } from '@/object-record/record-inline-cell/property-box/components/PropertyBox';
|
||||
import { ExpandableList } from '@/ui/display/expandable-list/ExpandableList';
|
||||
import { EllipsisDisplay } from '@/ui/field/display/components/EllipsisDisplay';
|
||||
|
||||
const StyledInlineCellBaseContainer = styled.div`
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
|
||||
position: relative;
|
||||
user-select: none;
|
||||
`;
|
||||
|
||||
const StyledPropertyBox = styled(PropertyBox)`
|
||||
height: ${({ theme }) => theme.spacing(6)};
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledIconContainer = styled.div`
|
||||
align-items: center;
|
||||
color: ${({ theme }) => theme.font.color.tertiary};
|
||||
display: flex;
|
||||
width: 16px;
|
||||
|
||||
svg {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
height: 16px;
|
||||
justify-content: center;
|
||||
width: 16px;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledLabelAndIconContainer = styled.div`
|
||||
align-items: center;
|
||||
color: ${({ theme }) => theme.font.color.tertiary};
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
`;
|
||||
|
||||
const StyledLabelContainer = styled.div<{ width?: number }>`
|
||||
color: ${({ theme }) => theme.font.color.tertiary};
|
||||
font-size: ${({ theme }) => theme.font.size.sm};
|
||||
width: ${({ width }) => width}px;
|
||||
`;
|
||||
|
||||
export const CalendarEventParticipantsResponseStatusField = ({
|
||||
responseStatus,
|
||||
participants,
|
||||
}: {
|
||||
responseStatus: 'Yes' | 'Maybe' | 'No';
|
||||
participants: CalendarEventParticipant[];
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const Icon = {
|
||||
Yes: <IconCheck stroke={theme.icon.stroke.sm} />,
|
||||
Maybe: <IconQuestionMark stroke={theme.icon.stroke.sm} />,
|
||||
No: <IconX stroke={theme.icon.stroke.sm} />,
|
||||
}[responseStatus];
|
||||
|
||||
// We want to display external participants first
|
||||
const orderedParticipants = [
|
||||
...participants.filter((participant) => participant.person),
|
||||
...participants.filter(
|
||||
(participant) => !participant.person && !participant.workspaceMember,
|
||||
),
|
||||
...participants.filter((participant) => participant.workspaceMember),
|
||||
];
|
||||
|
||||
const participantsContainerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const StyledChips = orderedParticipants.map((participant) => (
|
||||
<ParticipantChip participant={participant} />
|
||||
));
|
||||
|
||||
return (
|
||||
<StyledPropertyBox>
|
||||
<StyledInlineCellBaseContainer>
|
||||
<StyledLabelAndIconContainer>
|
||||
<StyledIconContainer>{Icon}</StyledIconContainer>
|
||||
|
||||
<StyledLabelContainer width={72}>
|
||||
<EllipsisDisplay>{responseStatus}</EllipsisDisplay>
|
||||
</StyledLabelContainer>
|
||||
</StyledLabelAndIconContainer>
|
||||
|
||||
<ExpandableList
|
||||
listItems={StyledChips}
|
||||
id={v4()}
|
||||
rootRef={participantsContainerRef}
|
||||
/>
|
||||
</StyledInlineCellBaseContainer>
|
||||
</StyledPropertyBox>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user