Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

4810 display participants in the right drawer of the calendar event #4896

Merged
Show file tree
Hide file tree
Changes from 33 commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
17cd986
Create ParticipantChip
bosiraphael Apr 8, 2024
6cabfd8
create CalendarEventParticipantsResponseStatus component
bosiraphael Apr 8, 2024
983e60a
modify calendarEventParticipant
bosiraphael Apr 8, 2024
09e97af
augment depth in useFindOneRecord
bosiraphael Apr 8, 2024
6f20834
wip
bosiraphael Apr 8, 2024
b13aa18
add typing
bosiraphael Apr 8, 2024
1710c50
add labels and icons
bosiraphael Apr 8, 2024
9b8c06c
improve styling
bosiraphael Apr 8, 2024
c0f4105
add chip variant
bosiraphael Apr 8, 2024
472b0a8
order participants
bosiraphael Apr 8, 2024
3c255d2
add a way to count the participants in view
bosiraphael Apr 8, 2024
5afe8df
add right marginto the intersection observer
bosiraphael Apr 8, 2024
6eb1424
use react intersection observer instead
bosiraphael Apr 8, 2024
7aa3256
add root margin
bosiraphael Apr 8, 2024
dd9ab48
introduce IntersectionObserverWrapper
bosiraphael Apr 8, 2024
cb6fd97
create CalendarEventParticipantPlus
bosiraphael Apr 8, 2024
f7077db
improve plus button
bosiraphael Apr 8, 2024
5aa8423
update root
bosiraphael Apr 9, 2024
1e4b1c1
making the component generic
bosiraphael Apr 9, 2024
c609a91
improve component and fix bugs
bosiraphael Apr 9, 2024
d5fafa5
improve code
bosiraphael Apr 9, 2024
a329cac
show more on click
bosiraphael Apr 9, 2024
9b6a2e1
update style
bosiraphael Apr 9, 2024
78425ce
styling
bosiraphael Apr 9, 2024
3b21d90
use dropdown
bosiraphael Apr 9, 2024
f867c7e
create portal
bosiraphael Apr 9, 2024
1b8756c
add disableBorder prop to dropdown
bosiraphael Apr 9, 2024
a31c48e
make dropdown full width
bosiraphael Apr 9, 2024
c8b040e
add id
bosiraphael Apr 9, 2024
822dbce
move components
bosiraphael Apr 9, 2024
63510c3
reorder
bosiraphael Apr 9, 2024
0cb3e39
Merge branch 'main' into 4810-display-participants-in-the-right-drawe…
bosiraphael Apr 9, 2024
8331ba1
Merge branch 'main' into 4810-display-participants-in-the-right-drawe…
bosiraphael Apr 10, 2024
5750062
Merge branch 'main' into 4810-display-participants-in-the-right-drawe…
bosiraphael Apr 11, 2024
a44d9a3
use theme
bosiraphael Apr 11, 2024
c062a63
decompose spacing
bosiraphael Apr 11, 2024
02ad6f6
refactoring
bosiraphael Apr 11, 2024
46fc049
remove margin prop
bosiraphael Apr 11, 2024
20c3b37
add opacity logic to the intersection observer wrapper
bosiraphael Apr 11, 2024
681cfd8
refactoring
bosiraphael Apr 11, 2024
5cbb4f0
introduce ParticipantChipVariant
bosiraphael Apr 11, 2024
a5b33a8
remove portal and disableBorder
bosiraphael Apr 11, 2024
3bc2f80
rename eventParticipants in calendarEventParticipants
bosiraphael Apr 11, 2024
6b6f254
update style
bosiraphael Apr 11, 2024
6d95c48
fix styling issues
bosiraphael Apr 11, 2024
c000397
fix font size
bosiraphael Apr 11, 2024
3499072
move to ui
bosiraphael Apr 11, 2024
c5eeb94
use maxWidth
bosiraphael Apr 11, 2024
82d011d
fix lint
bosiraphael Apr 12, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -30,6 +32,7 @@ const StyledContainer = styled.div`
flex-direction: column;
gap: ${({ theme }) => theme.spacing(6)};
padding: ${({ theme }) => theme.spacing(6)};
max-width: 100%;
`;

const StyledEventChip = styled(Chip)`
Expand Down Expand Up @@ -88,6 +91,30 @@ 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 }],
}}
>
<RecordInlineCell readonly />
</FieldContext.Provider>
</StyledPropertyBox>
));

return (
<StyledContainer>
<StyledEventChip
Expand All @@ -110,27 +137,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>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -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] || []}
/>
))}
</>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
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 { ExpandableList } from '@/activities/components/ExpandableList/ExpandableList';
import { ParticipantChip } from '@/activities/components/ParticipantChip';
import { PropertyBox } from '@/object-record/record-inline-cell/property-box/components/PropertyBox';
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;
`;

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

const StyledParticipantChip = styled(ParticipantChip)<{ inView?: boolean }>`
opacity: ${({ inView }) => (inView === undefined || inView ? 1 : 0)};
`;

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) => (
<StyledParticipantChip participant={participant} />
));

return (
<StyledPropertyBox>
<StyledInlineCellBaseContainer>
<StyledLabelAndIconContainer>
<StyledIconContainer>{Icon}</StyledIconContainer>

<StyledLabelContainer width={72}>
<EllipsisDisplay>{responseStatus}</EllipsisDisplay>
</StyledLabelContainer>
</StyledLabelAndIconContainer>

<ExpandableList
components={StyledChips}
id={v4()}
rootRef={participantsContainerRef}
margin="0px -50px 0px 0px"
/>
</StyledInlineCellBaseContainer>
</StyledPropertyBox>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export const RightDrawerCalendarEvent = () => {
objectNameSingular: CoreObjectNameSingular.CalendarEvent,
objectRecordId: viewableCalendarEventId ?? '',
onCompleted: (record) => setRecords([record]),
depth: 2,
});

if (!calendarEvent) return null;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { CalendarEventParticipant } from '@/activities/calendar/types/CalendarEventParticipant';

// TODO: use backend CalendarEvent type when ready
export type CalendarEvent = {
conferenceLink?: {
Expand All @@ -14,8 +16,5 @@ export type CalendarEvent = {
startsAt: string;
title?: string;
visibility: 'METADATA' | 'SHARE_EVERYTHING';
participants?: {
displayName: string;
workspaceMemberId?: string;
}[];
calendarEventParticipants?: CalendarEventParticipant[];
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Person } from '@/people/types/Person';
import { WorkspaceMember } from '~/generated-metadata/graphql';

export type CalendarEventParticipant = {
id: string;
handle: string;
isOrganizer: boolean;
displayName: string;
person?: Person;
workspaceMember?: WorkspaceMember;
responseStatus: 'ACCEPTED' | 'DECLINED' | 'NEEDS_ACTION' | 'TENTATIVE';
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import React, { ReactElement, useState } from 'react';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

move it to ui or nobody will find it! We are missing a story on this component too :)

import ReactDOM from 'react-dom';
import styled from '@emotion/styled';

import { IntersectionObserverWrapper } from '@/activities/components/ExpandableList/IntersectionObserverWrapper';
import { Chip, ChipVariant } from '@/ui/display/chip/components/Chip';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';

const StyledContainer = styled.div`
align-items: center;
display: flex;
gap: ${({ theme }) => theme.spacing(1)};
position: relative;
box-sizing: border-box;
`;

const StyledDiv = styled.div`
align-items: center;
display: flex;
gap: ${({ theme }) => theme.spacing(1)};
position: relative;
white-space: nowrap;
max-width: 100%;
width: 100%;
box-sizing: border-box;
overflow: hidden;
`;

const StyledExpendableCell = styled.div`
display: flex;
align-items: center;
align-content: center;
flex-wrap: wrap;
gap: ${({ theme }) => theme.spacing(1)};
position: absolute;
top: ${({ theme }) => `-${theme.spacing(2.25)}`};
bosiraphael marked this conversation as resolved.
Show resolved Hide resolved
left: ${({ theme }) => `-${theme.spacing(2.25)}`};
width: 232px;
z-index: 1;
box-sizing: border-box;
background: ${({ theme }) => theme.background.secondary};
padding: ${({ theme }) => theme.spacing(2)};
box-shadow: ${({ theme }) => theme.boxShadow.light};
backdrop-filter: blur(20px);
border-radius: 4px;
bosiraphael marked this conversation as resolved.
Show resolved Hide resolved
border: 1px solid ${({ theme }) => theme.border.color.medium};
`;

export const ExpandableList = ({
components,
rootRef,
id,
margin,
}: {
components: ReactElement[];
bosiraphael marked this conversation as resolved.
Show resolved Hide resolved
rootRef: React.RefObject<HTMLElement>;
id: string;
margin?: string;
bosiraphael marked this conversation as resolved.
Show resolved Hide resolved
}) => {
const [componentsInView, setComponentsInView] = useState(new Set<number>());

const firstComponent = components[0];

const dropdownId = `expandable-list-dropdown-${id}`;

const containerRef = React.useRef<HTMLDivElement>(null);

return (
<StyledContainer ref={containerRef}>
<StyledDiv>
{firstComponent}
{components.slice(1).map((component, index) => (
<React.Fragment key={index}>
<IntersectionObserverWrapper
set={setComponentsInView}
id={index}
rootRef={rootRef}
margin={margin}
>
{component}
</IntersectionObserverWrapper>
{index === componentsInView.size - 1 &&
components.length - componentsInView.size - 1 !== 0 && (
<Dropdown
dropdownId={dropdownId}
dropdownHotkeyScope={{
scope: dropdownId,
}}
clickableComponent={
<Chip
label={`+${
components.length - componentsInView.size - 1
}`}
variant={ChipVariant.Highlighted}
bosiraphael marked this conversation as resolved.
Show resolved Hide resolved
/>
}
dropdownComponents={ReactDOM.createPortal(
<StyledExpendableCell>{components}</StyledExpendableCell>,
containerRef.current as HTMLDivElement,
)}
disableBorder
/>
)}
</React.Fragment>
))}
</StyledDiv>
</StyledContainer>
);
};
Loading
Loading