4900 multi select field front implement expanded cells (#5151)
Add expanded cell https://github.com/twentyhq/twenty/assets/29927851/363f2b44-7b3c-4771-a651-dfc4014da6ac 
This commit is contained in:
@ -1,14 +1,15 @@
|
||||
import { useRef } from 'react';
|
||||
import { useRef, useState } from 'react';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
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';
|
||||
import { ExpandableList } from '@/ui/layout/expandable-list/components/ExpandableList';
|
||||
import { isRightDrawerAnimationCompletedState } from '@/ui/layout/right-drawer/states/isRightDrawerAnimationCompleted';
|
||||
|
||||
const StyledInlineCellBaseContainer = styled.div`
|
||||
align-items: center;
|
||||
@ -55,6 +56,9 @@ const StyledLabelContainer = styled.div<{ width?: number }>`
|
||||
font-size: ${({ theme }) => theme.font.size.sm};
|
||||
width: ${({ width }) => width}px;
|
||||
`;
|
||||
const StyledDiv = styled.div`
|
||||
max-width: 70%;
|
||||
`;
|
||||
|
||||
export const CalendarEventParticipantsResponseStatusField = ({
|
||||
responseStatus,
|
||||
@ -64,6 +68,9 @@ export const CalendarEventParticipantsResponseStatusField = ({
|
||||
participants: CalendarEventParticipant[];
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const isRightDrawerAnimationCompleted = useRecoilValue(
|
||||
isRightDrawerAnimationCompletedState,
|
||||
);
|
||||
|
||||
const Icon = {
|
||||
Yes: <IconCheck stroke={theme.icon.stroke.sm} />,
|
||||
@ -81,9 +88,9 @@ export const CalendarEventParticipantsResponseStatusField = ({
|
||||
];
|
||||
|
||||
const participantsContainerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const StyledChips = orderedParticipants.map((participant) => (
|
||||
<ParticipantChip participant={participant} />
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
const styledChips = orderedParticipants.map((participant, index) => (
|
||||
<ParticipantChip key={index} participant={participant} />
|
||||
));
|
||||
|
||||
return (
|
||||
@ -96,12 +103,21 @@ export const CalendarEventParticipantsResponseStatusField = ({
|
||||
<EllipsisDisplay>{responseStatus}</EllipsisDisplay>
|
||||
</StyledLabelContainer>
|
||||
</StyledLabelAndIconContainer>
|
||||
|
||||
<ExpandableList
|
||||
listItems={StyledChips}
|
||||
id={v4()}
|
||||
rootRef={participantsContainerRef}
|
||||
/>
|
||||
<StyledDiv
|
||||
ref={participantsContainerRef}
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
>
|
||||
{isRightDrawerAnimationCompleted && (
|
||||
<ExpandableList
|
||||
isHovered={isHovered}
|
||||
reference={participantsContainerRef.current || undefined}
|
||||
forceDisplayHiddenCount
|
||||
>
|
||||
{styledChips}
|
||||
</ExpandableList>
|
||||
)}
|
||||
</StyledDiv>
|
||||
</StyledInlineCellBaseContainer>
|
||||
</StyledPropertyBox>
|
||||
);
|
||||
|
||||
@ -14,6 +14,7 @@ import {
|
||||
import { RecordInlineCell } from '@/object-record/record-inline-cell/components/RecordInlineCell';
|
||||
import { PropertyBox } from '@/object-record/record-inline-cell/property-box/components/PropertyBox';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { isRightDrawerAnimationCompletedState } from '@/ui/layout/right-drawer/states/isRightDrawerAnimationCompleted';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
const StyledPropertyBox = styled(PropertyBox)`
|
||||
@ -27,6 +28,10 @@ export const ActivityEditorFields = ({
|
||||
}) => {
|
||||
const { upsertActivity } = useUpsertActivity();
|
||||
|
||||
const isRightDrawerAnimationCompleted = useRecoilValue(
|
||||
isRightDrawerAnimationCompletedState,
|
||||
);
|
||||
|
||||
const getRecordFromCache = useGetRecordFromCache({
|
||||
objectNameSingular: CoreObjectNameSingular.Activity,
|
||||
});
|
||||
@ -93,11 +98,16 @@ export const ActivityEditorFields = ({
|
||||
</AssigneeFieldContextProvider>
|
||||
</>
|
||||
)}
|
||||
{ActivityTargetsContextProvider && isDefined(activityFromCache) && (
|
||||
<ActivityTargetsContextProvider>
|
||||
<ActivityTargetsInlineCell activity={activityFromCache} />
|
||||
</ActivityTargetsContextProvider>
|
||||
)}
|
||||
{ActivityTargetsContextProvider &&
|
||||
isDefined(activityFromCache) &&
|
||||
isRightDrawerAnimationCompleted && (
|
||||
<ActivityTargetsContextProvider>
|
||||
<ActivityTargetsInlineCell
|
||||
activity={activityFromCache}
|
||||
maxWidth={340}
|
||||
/>
|
||||
</ActivityTargetsContextProvider>
|
||||
)}
|
||||
</StyledPropertyBox>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,91 +1,47 @@
|
||||
import { useMemo } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
import { Chip, ChipVariant } from 'twenty-ui';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { ActivityTargetWithTargetRecord } from '@/activities/types/ActivityTargetObject';
|
||||
import { RecordChip } from '@/object-record/components/RecordChip';
|
||||
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
||||
import {
|
||||
ExpandableList,
|
||||
ExpandableListProps,
|
||||
} from '@/ui/layout/expandable-list/components/ExpandableList';
|
||||
|
||||
const MAX_RECORD_CHIPS_DISPLAY = 2;
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
const StyledContainer = styled.div<{ maxWidth?: number }>`
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
max-width: ${({ maxWidth }) => `${maxWidth}px` || 'none'};
|
||||
`;
|
||||
|
||||
const StyledRelationsListContainer = styled(StyledContainer)`
|
||||
padding: ${({ theme }) => theme.spacing(2)};
|
||||
border-radius: ${({ theme }) => theme.spacing(1)};
|
||||
background-color: ${({ theme }) => theme.background.secondary};
|
||||
box-shadow: '0px 2px 4px ${({ theme }) =>
|
||||
theme.boxShadow.light}, 2px 4px 16px ${({ theme }) =>
|
||||
theme.boxShadow.strong}';
|
||||
backdrop-filter: ${({ theme }) => theme.blur.strong};
|
||||
`;
|
||||
|
||||
const showMoreRelationsHandler = (event?: React.MouseEvent<HTMLDivElement>) => {
|
||||
event?.preventDefault();
|
||||
event?.stopPropagation();
|
||||
};
|
||||
|
||||
export const ActivityTargetChips = ({
|
||||
activityTargetObjectRecords,
|
||||
isHovered,
|
||||
reference,
|
||||
maxWidth,
|
||||
}: {
|
||||
activityTargetObjectRecords: ActivityTargetWithTargetRecord[];
|
||||
}) => {
|
||||
const dropdownId = useMemo(() => `multiple-relations-dropdown-${v4()}`, []);
|
||||
|
||||
maxWidth?: number;
|
||||
} & ExpandableListProps) => {
|
||||
return (
|
||||
<StyledContainer>
|
||||
{activityTargetObjectRecords
|
||||
?.slice(0, MAX_RECORD_CHIPS_DISPLAY)
|
||||
.map((activityTargetObjectRecord) => (
|
||||
<RecordChip
|
||||
key={activityTargetObjectRecord.targetObject.id}
|
||||
record={activityTargetObjectRecord.targetObject}
|
||||
objectNameSingular={
|
||||
activityTargetObjectRecord.targetObjectMetadataItem.nameSingular
|
||||
}
|
||||
/>
|
||||
))}
|
||||
|
||||
{activityTargetObjectRecords.length > MAX_RECORD_CHIPS_DISPLAY && (
|
||||
<div onClick={showMoreRelationsHandler}>
|
||||
<Dropdown
|
||||
dropdownId={dropdownId}
|
||||
dropdownHotkeyScope={{
|
||||
scope: dropdownId,
|
||||
}}
|
||||
clickableComponent={
|
||||
<Chip
|
||||
label={`+${
|
||||
activityTargetObjectRecords.length - MAX_RECORD_CHIPS_DISPLAY
|
||||
}`}
|
||||
variant={ChipVariant.Highlighted}
|
||||
/>
|
||||
}
|
||||
dropdownOffset={{ x: 0, y: -20 }}
|
||||
dropdownComponents={
|
||||
<StyledRelationsListContainer>
|
||||
{activityTargetObjectRecords.map(
|
||||
(activityTargetObjectRecord) => (
|
||||
<RecordChip
|
||||
key={activityTargetObjectRecord.targetObject.id}
|
||||
record={activityTargetObjectRecord.targetObject}
|
||||
objectNameSingular={
|
||||
activityTargetObjectRecord.targetObjectMetadataItem
|
||||
.nameSingular
|
||||
}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
</StyledRelationsListContainer>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<StyledContainer maxWidth={maxWidth}>
|
||||
<ExpandableList
|
||||
isHovered={isHovered}
|
||||
reference={reference}
|
||||
forceDisplayHiddenCount
|
||||
>
|
||||
{activityTargetObjectRecords.map(
|
||||
(activityTargetObjectRecord, index) => (
|
||||
<RecordChip
|
||||
key={index}
|
||||
record={activityTargetObjectRecord.targetObject}
|
||||
objectNameSingular={
|
||||
activityTargetObjectRecord.targetObjectMetadataItem.nameSingular
|
||||
}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
</ExpandableList>
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
|
||||
@ -39,6 +39,7 @@ const StyledChip = styled.div`
|
||||
padding: ${({ theme }) => theme.spacing(1)};
|
||||
height: 20px;
|
||||
box-sizing: border-box;
|
||||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
type ParticipantChipVariant = 'default' | 'bold';
|
||||
|
||||
@ -13,10 +13,16 @@ import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||
|
||||
type ActivityTargetsInlineCellProps = {
|
||||
activity: Activity;
|
||||
showLabel?: boolean;
|
||||
maxWidth?: number;
|
||||
readonly?: boolean;
|
||||
};
|
||||
|
||||
export const ActivityTargetsInlineCell = ({
|
||||
activity,
|
||||
showLabel = true,
|
||||
maxWidth,
|
||||
readonly,
|
||||
}: ActivityTargetsInlineCellProps) => {
|
||||
const { activityTargetObjectRecords } =
|
||||
useActivityTargetObjectRecords(activity);
|
||||
@ -37,8 +43,9 @@ export const ActivityTargetsInlineCell = ({
|
||||
customEditHotkeyScope={{
|
||||
scope: ActivityEditorHotkeyScope.ActivityTargets,
|
||||
}}
|
||||
IconLabel={IconArrowUpRight}
|
||||
showLabel={true}
|
||||
IconLabel={showLabel ? IconArrowUpRight : undefined}
|
||||
showLabel={showLabel}
|
||||
readonly={readonly}
|
||||
editModeContent={
|
||||
<ActivityTargetInlineCellEditMode
|
||||
activity={activity}
|
||||
@ -49,6 +56,7 @@ export const ActivityTargetsInlineCell = ({
|
||||
displayModeContent={
|
||||
<ActivityTargetChips
|
||||
activityTargetObjectRecords={activityTargetObjectRecords}
|
||||
maxWidth={maxWidth}
|
||||
/>
|
||||
}
|
||||
isDisplayModeContentEmpty={activityTargetObjectRecords.length === 0}
|
||||
|
||||
@ -100,7 +100,7 @@ export const NoteCard = ({
|
||||
<StyledCardContent>{body}</StyledCardContent>
|
||||
</StyledCardDetailsContainer>
|
||||
<StyledFooter>
|
||||
<ActivityTargetsInlineCell activity={note} />
|
||||
<ActivityTargetsInlineCell activity={note} readonly />
|
||||
{note.comments && note.comments.length > 0 && (
|
||||
<StyledCommentIcon>
|
||||
<IconComment size={theme.icon.size.md} />
|
||||
|
||||
@ -6,9 +6,8 @@ import {
|
||||
OverflowingTextWithTooltip,
|
||||
} from 'twenty-ui';
|
||||
|
||||
import { ActivityTargetChips } from '@/activities/components/ActivityTargetChips';
|
||||
import { useActivityTargetObjectRecords } from '@/activities/hooks/useActivityTargetObjectRecords';
|
||||
import { useOpenActivityRightDrawer } from '@/activities/hooks/useOpenActivityRightDrawer';
|
||||
import { ActivityTargetsInlineCell } from '@/activities/inline-cell/components/ActivityTargetsInlineCell';
|
||||
import { Activity } from '@/activities/types/Activity';
|
||||
import { getActivitySummary } from '@/activities/utils/getActivitySummary';
|
||||
import { Checkbox, CheckboxShape } from '@/ui/input/components/Checkbox';
|
||||
@ -18,7 +17,7 @@ import { useCompleteTask } from '../hooks/useCompleteTask';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
align-items: center;
|
||||
align-self: stretch;
|
||||
justify-content: space-between;
|
||||
border-bottom: 1px solid ${({ theme }) => theme.border.color.light};
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
@ -34,9 +33,6 @@ const StyledContainer = styled.div`
|
||||
const StyledTaskBody = styled.div`
|
||||
color: ${({ theme }) => theme.font.color.tertiary};
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-grow: 1;
|
||||
width: 1px;
|
||||
`;
|
||||
|
||||
const StyledTaskTitle = styled.div<{
|
||||
@ -64,9 +60,10 @@ const StyledDueDate = styled.div<{
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
padding-left: ${({ theme }) => theme.spacing(2)};
|
||||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
const StyledFieldsContainer = styled.div`
|
||||
const StyledRightSideContainer = styled.div`
|
||||
display: flex;
|
||||
`;
|
||||
|
||||
@ -74,6 +71,14 @@ const StyledPlaceholder = styled.div`
|
||||
color: ${({ theme }) => theme.font.color.light};
|
||||
`;
|
||||
|
||||
const StyledLeftSideContainer = styled.div`
|
||||
display: flex;
|
||||
`;
|
||||
|
||||
const StyledCheckboxContainer = styled.div`
|
||||
display: flex;
|
||||
`;
|
||||
|
||||
export const TaskRow = ({ task }: { task: Activity }) => {
|
||||
const theme = useTheme();
|
||||
const openActivityRightDrawer = useOpenActivityRightDrawer();
|
||||
@ -81,39 +86,42 @@ export const TaskRow = ({ task }: { task: Activity }) => {
|
||||
const body = getActivitySummary(task.body);
|
||||
const { completeTask } = useCompleteTask(task);
|
||||
|
||||
const { activityTargetObjectRecords } = useActivityTargetObjectRecords(task);
|
||||
|
||||
return (
|
||||
<StyledContainer
|
||||
onClick={() => {
|
||||
openActivityRightDrawer(task.id);
|
||||
}}
|
||||
>
|
||||
<div
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<Checkbox
|
||||
checked={!!task.completedAt}
|
||||
shape={CheckboxShape.Rounded}
|
||||
onCheckedChange={completeTask}
|
||||
/>
|
||||
</div>
|
||||
<StyledTaskTitle completed={task.completedAt !== null}>
|
||||
{task.title || <StyledPlaceholder>Task title</StyledPlaceholder>}
|
||||
</StyledTaskTitle>
|
||||
<StyledTaskBody>
|
||||
<OverflowingTextWithTooltip text={body} />
|
||||
{task.comments && task.comments.length > 0 && (
|
||||
<StyledCommentIcon>
|
||||
<IconComment size={theme.icon.size.md} />
|
||||
</StyledCommentIcon>
|
||||
)}
|
||||
</StyledTaskBody>
|
||||
<StyledFieldsContainer>
|
||||
<ActivityTargetChips
|
||||
activityTargetObjectRecords={activityTargetObjectRecords}
|
||||
<StyledLeftSideContainer>
|
||||
<StyledCheckboxContainer
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<Checkbox
|
||||
checked={!!task.completedAt}
|
||||
shape={CheckboxShape.Rounded}
|
||||
onCheckedChange={completeTask}
|
||||
/>
|
||||
</StyledCheckboxContainer>
|
||||
<StyledTaskTitle completed={task.completedAt !== null}>
|
||||
{task.title || <StyledPlaceholder>Task title</StyledPlaceholder>}
|
||||
</StyledTaskTitle>
|
||||
<StyledTaskBody>
|
||||
<OverflowingTextWithTooltip text={body} />
|
||||
{task.comments && task.comments.length > 0 && (
|
||||
<StyledCommentIcon>
|
||||
<IconComment size={theme.icon.size.md} />
|
||||
</StyledCommentIcon>
|
||||
)}
|
||||
</StyledTaskBody>
|
||||
</StyledLeftSideContainer>
|
||||
<StyledRightSideContainer>
|
||||
<ActivityTargetsInlineCell
|
||||
activity={task}
|
||||
showLabel={false}
|
||||
maxWidth={200}
|
||||
readonly
|
||||
/>
|
||||
<StyledDueDate
|
||||
isPast={
|
||||
@ -123,7 +131,7 @@ export const TaskRow = ({ task }: { task: Activity }) => {
|
||||
<IconCalendar size={theme.icon.size.md} />
|
||||
{task.dueAt && beautifyExactDate(task.dueAt)}
|
||||
</StyledDueDate>
|
||||
</StyledFieldsContainer>
|
||||
</StyledRightSideContainer>
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
|
||||
@ -70,7 +70,7 @@ export const EventsGroup = ({
|
||||
{group.items.map((event, index) => (
|
||||
<EventRow
|
||||
mainObjectMetadataItem={mainObjectMetadataItem}
|
||||
key={event.id}
|
||||
key={index}
|
||||
event={event}
|
||||
isLastEvent={index === group.items.length - 1}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user