- {activityTargetObjectRecords
- ?.slice(0, MAX_RECORD_CHIPS_DISPLAY)
- .map((activityTargetObjectRecord) => (
-
- ))}
-
- {activityTargetObjectRecords.length > MAX_RECORD_CHIPS_DISPLAY && (
-
-
- }
- dropdownOffset={{ x: 0, y: -20 }}
- dropdownComponents={
-
- {activityTargetObjectRecords.map(
- (activityTargetObjectRecord) => (
-
- ),
- )}
-
- }
- />
-
- )}
+
+
+ {activityTargetObjectRecords.map(
+ (activityTargetObjectRecord, index) => (
+
+ ),
+ )}
+
);
};
diff --git a/packages/twenty-front/src/modules/activities/components/ParticipantChip.tsx b/packages/twenty-front/src/modules/activities/components/ParticipantChip.tsx
index 4ba26346fefc..f0242f67ea69 100644
--- a/packages/twenty-front/src/modules/activities/components/ParticipantChip.tsx
+++ b/packages/twenty-front/src/modules/activities/components/ParticipantChip.tsx
@@ -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';
diff --git a/packages/twenty-front/src/modules/activities/inline-cell/components/ActivityTargetsInlineCell.tsx b/packages/twenty-front/src/modules/activities/inline-cell/components/ActivityTargetsInlineCell.tsx
index 0ba763525629..21310d09aabc 100644
--- a/packages/twenty-front/src/modules/activities/inline-cell/components/ActivityTargetsInlineCell.tsx
+++ b/packages/twenty-front/src/modules/activities/inline-cell/components/ActivityTargetsInlineCell.tsx
@@ -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={
}
isDisplayModeContentEmpty={activityTargetObjectRecords.length === 0}
diff --git a/packages/twenty-front/src/modules/activities/notes/components/NoteCard.tsx b/packages/twenty-front/src/modules/activities/notes/components/NoteCard.tsx
index 3ae87c635959..caefb987a80d 100644
--- a/packages/twenty-front/src/modules/activities/notes/components/NoteCard.tsx
+++ b/packages/twenty-front/src/modules/activities/notes/components/NoteCard.tsx
@@ -100,7 +100,7 @@ export const NoteCard = ({
{body}
-
+
{note.comments && note.comments.length > 0 && (
diff --git a/packages/twenty-front/src/modules/activities/tasks/components/TaskRow.tsx b/packages/twenty-front/src/modules/activities/tasks/components/TaskRow.tsx
index a7ad9927ce82..7ba8edb00276 100644
--- a/packages/twenty-front/src/modules/activities/tasks/components/TaskRow.tsx
+++ b/packages/twenty-front/src/modules/activities/tasks/components/TaskRow.tsx
@@ -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 (
{
openActivityRightDrawer(task.id);
}}
>
- {
- e.stopPropagation();
- }}
- >
-
-
-
- {task.title || Task title}
-
-
-
- {task.comments && task.comments.length > 0 && (
-
-
-
- )}
-
-
-
+ {
+ e.stopPropagation();
+ }}
+ >
+
+
+
+ {task.title || Task title}
+
+
+
+ {task.comments && task.comments.length > 0 && (
+
+
+
+ )}
+
+
+
+
{
{task.dueAt && beautifyExactDate(task.dueAt)}
-
+
);
};
diff --git a/packages/twenty-front/src/modules/activities/timelineActivities/components/EventsGroup.tsx b/packages/twenty-front/src/modules/activities/timelineActivities/components/EventsGroup.tsx
index b441b7ab77dc..afb561724d23 100644
--- a/packages/twenty-front/src/modules/activities/timelineActivities/components/EventsGroup.tsx
+++ b/packages/twenty-front/src/modules/activities/timelineActivities/components/EventsGroup.tsx
@@ -70,7 +70,7 @@ export const EventsGroup = ({
{group.items.map((event, index) => (
diff --git a/packages/twenty-front/src/modules/auth/sign-in-up/components/SignInUpForm.tsx b/packages/twenty-front/src/modules/auth/sign-in-up/components/SignInUpForm.tsx
index a5823db43939..80e5d1310576 100644
--- a/packages/twenty-front/src/modules/auth/sign-in-up/components/SignInUpForm.tsx
+++ b/packages/twenty-front/src/modules/auth/sign-in-up/components/SignInUpForm.tsx
@@ -17,7 +17,7 @@ import { captchaProviderState } from '@/client-config/states/captchaProviderStat
import { Loader } from '@/ui/feedback/loader/components/Loader';
import { MainButton } from '@/ui/input/button/components/MainButton';
import { TextInput } from '@/ui/input/components/TextInput';
-import { ActionLink } from '@/ui/navigation/link/components/ActionLink.tsx';
+import { ActionLink } from '@/ui/navigation/link/components/ActionLink';
import { AnimatedEaseIn } from '@/ui/utilities/animation/components/AnimatedEaseIn';
import { isDefined } from '~/utils/isDefined';
diff --git a/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useHandleResetPassword.ts b/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useHandleResetPassword.ts
index 0a0feb5f6323..45cc945b3a3c 100644
--- a/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useHandleResetPassword.ts
+++ b/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useHandleResetPassword.ts
@@ -1,7 +1,7 @@
import { useCallback } from 'react';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
-import { useEmailPasswordResetLinkMutation } from '~/generated/graphql.tsx';
+import { useEmailPasswordResetLinkMutation } from '~/generated/graphql';
export const useHandleResetPassword = () => {
const { enqueueSnackBar } = useSnackBar();
diff --git a/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useNavigateAfterSignInUp.ts b/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useNavigateAfterSignInUp.ts
index 7d4336071b18..195a4e44daee 100644
--- a/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useNavigateAfterSignInUp.ts
+++ b/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useNavigateAfterSignInUp.ts
@@ -6,7 +6,7 @@ import { CurrentWorkspace } from '@/auth/states/currentWorkspaceState';
import { previousUrlState } from '@/auth/states/previousUrlState';
import { billingState } from '@/client-config/states/billingState';
import { AppPath } from '@/types/AppPath';
-import { WorkspaceMember } from '~/generated/graphql.tsx';
+import { WorkspaceMember } from '~/generated/graphql';
export const useNavigateAfterSignInUp = () => {
const navigate = useNavigate();
diff --git a/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useSignInWithMicrosoft.ts b/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useSignInWithMicrosoft.ts
index 06150ec0d821..444bff19d31f 100644
--- a/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useSignInWithMicrosoft.ts
+++ b/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useSignInWithMicrosoft.ts
@@ -1,6 +1,6 @@
import { useParams } from 'react-router-dom';
-import { useAuth } from '@/auth/hooks/useAuth.ts';
+import { useAuth } from '@/auth/hooks/useAuth';
export const useSignInWithMicrosoft = () => {
const workspaceInviteHash = useParams().workspaceInviteHash;
diff --git a/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useWorkspaceFromInviteHash.ts b/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useWorkspaceFromInviteHash.ts
index a821ecee4590..f5403e9b4a1d 100644
--- a/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useWorkspaceFromInviteHash.ts
+++ b/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useWorkspaceFromInviteHash.ts
@@ -1,6 +1,6 @@
import { useParams } from 'react-router-dom';
-import { useGetWorkspaceFromInviteHashQuery } from '~/generated/graphql.tsx';
+import { useGetWorkspaceFromInviteHashQuery } from '~/generated/graphql';
export const useWorkspaceFromInviteHash = () => {
const workspaceInviteHash = useParams().workspaceInviteHash;
diff --git a/packages/twenty-front/src/modules/billing/components/SubscriptionCard.tsx b/packages/twenty-front/src/modules/billing/components/SubscriptionCard.tsx
index 7af7e093bc90..d92da1d823ab 100644
--- a/packages/twenty-front/src/modules/billing/components/SubscriptionCard.tsx
+++ b/packages/twenty-front/src/modules/billing/components/SubscriptionCard.tsx
@@ -1,6 +1,6 @@
import styled from '@emotion/styled';
-import { SubscriptionCardPrice } from '@/billing/components/SubscriptionCardPrice.tsx';
+import { SubscriptionCardPrice } from '@/billing/components/SubscriptionCardPrice';
import { capitalize } from '~/utils/string/capitalize';
type SubscriptionCardProps = {
diff --git a/packages/twenty-front/src/modules/object-record/record-field/components/FieldDisplay.tsx b/packages/twenty-front/src/modules/object-record/record-field/components/FieldDisplay.tsx
index 3c8fee324ce7..57ca920cf8cd 100644
--- a/packages/twenty-front/src/modules/object-record/record-field/components/FieldDisplay.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-field/components/FieldDisplay.tsx
@@ -2,6 +2,7 @@ import { useContext } from 'react';
import { LinksFieldDisplay } from '@/object-record/record-field/meta-types/display/components/LinksFieldDisplay';
import { isFieldLinks } from '@/object-record/record-field/types/guards/isFieldLinks';
+import { ExpandableListProps } from '@/ui/layout/expandable-list/components/ExpandableList';
import { FieldContext } from '../contexts/FieldContext';
import { AddressFieldDisplay } from '../meta-types/display/components/AddressFieldDisplay';
@@ -13,7 +14,7 @@ import { EmailFieldDisplay } from '../meta-types/display/components/EmailFieldDi
import { FullNameFieldDisplay } from '../meta-types/display/components/FullNameFieldDisplay';
import { JsonFieldDisplay } from '../meta-types/display/components/JsonFieldDisplay';
import { LinkFieldDisplay } from '../meta-types/display/components/LinkFieldDisplay';
-import { MultiSelectFieldDisplay } from '../meta-types/display/components/MultiSelectFieldDisplay.tsx';
+import { MultiSelectFieldDisplay } from '../meta-types/display/components/MultiSelectFieldDisplay';
import { NumberFieldDisplay } from '../meta-types/display/components/NumberFieldDisplay';
import { PhoneFieldDisplay } from '../meta-types/display/components/PhoneFieldDisplay';
import { RelationFieldDisplay } from '../meta-types/display/components/RelationFieldDisplay';
@@ -36,7 +37,13 @@ import { isFieldSelect } from '../types/guards/isFieldSelect';
import { isFieldText } from '../types/guards/isFieldText';
import { isFieldUuid } from '../types/guards/isFieldUuid';
-export const FieldDisplay = () => {
+type FieldDisplayProps = ExpandableListProps;
+
+export const FieldDisplay = ({
+ isHovered,
+ reference,
+ fromTableCell,
+}: FieldDisplayProps & { fromTableCell?: boolean }) => {
const { fieldDefinition, isLabelIdentifier } = useContext(FieldContext);
const isChipDisplay =
@@ -74,7 +81,11 @@ export const FieldDisplay = () => {
) : isFieldSelect(fieldDefinition) ? (
) : isFieldMultiSelect(fieldDefinition) ? (
-
+
) : isFieldAddress(fieldDefinition) ? (
) : isFieldRawJson(fieldDefinition) ? (
diff --git a/packages/twenty-front/src/modules/object-record/record-field/components/FieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/components/FieldInput.tsx
index 54a59fd57d8b..f814ab66ce1c 100644
--- a/packages/twenty-front/src/modules/object-record/record-field/components/FieldInput.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-field/components/FieldInput.tsx
@@ -4,7 +4,7 @@ import { AddressFieldInput } from '@/object-record/record-field/meta-types/input
import { DateFieldInput } from '@/object-record/record-field/meta-types/input/components/DateFieldInput';
import { FullNameFieldInput } from '@/object-record/record-field/meta-types/input/components/FullNameFieldInput';
import { LinksFieldInput } from '@/object-record/record-field/meta-types/input/components/LinksFieldInput';
-import { MultiSelectFieldInput } from '@/object-record/record-field/meta-types/input/components/MultiSelectFieldInput.tsx';
+import { MultiSelectFieldInput } from '@/object-record/record-field/meta-types/input/components/MultiSelectFieldInput';
import { RawJsonFieldInput } from '@/object-record/record-field/meta-types/input/components/RawJsonFieldInput';
import { SelectFieldInput } from '@/object-record/record-field/meta-types/input/components/SelectFieldInput';
import { RecordFieldInputScope } from '@/object-record/record-field/scopes/RecordFieldInputScope';
diff --git a/packages/twenty-front/src/modules/object-record/record-field/hooks/useGetButtonIcon.ts b/packages/twenty-front/src/modules/object-record/record-field/hooks/useGetButtonIcon.ts
index ebec13c24947..035c24c3cbb0 100644
--- a/packages/twenty-front/src/modules/object-record/record-field/hooks/useGetButtonIcon.ts
+++ b/packages/twenty-front/src/modules/object-record/record-field/hooks/useGetButtonIcon.ts
@@ -1,6 +1,7 @@
import { useContext } from 'react';
import { IconComponent, IconPencil } from 'twenty-ui';
+import { isFieldMultiSelect } from '@/object-record/record-field/types/guards/isFieldMultiSelect';
import { isFieldRelation } from '@/object-record/record-field/types/guards/isFieldRelation';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
@@ -17,7 +18,8 @@ export const useGetButtonIcon = (): IconComponent | undefined => {
if (
isFieldLink(fieldDefinition) ||
isFieldEmail(fieldDefinition) ||
- isFieldPhone(fieldDefinition)
+ isFieldPhone(fieldDefinition) ||
+ isFieldMultiSelect(fieldDefinition)
) {
return IconPencil;
}
diff --git a/packages/twenty-front/src/modules/object-record/record-field/hooks/usePersistField.ts b/packages/twenty-front/src/modules/object-record/record-field/hooks/usePersistField.ts
index 10648a4bd935..9336da879ee5 100644
--- a/packages/twenty-front/src/modules/object-record/record-field/hooks/usePersistField.ts
+++ b/packages/twenty-front/src/modules/object-record/record-field/hooks/usePersistField.ts
@@ -10,7 +10,7 @@ import { isFieldFullNameValue } from '@/object-record/record-field/types/guards/
import { isFieldLinks } from '@/object-record/record-field/types/guards/isFieldLinks';
import { isFieldLinksValue } from '@/object-record/record-field/types/guards/isFieldLinksValue';
import { isFieldMultiSelect } from '@/object-record/record-field/types/guards/isFieldMultiSelect';
-import { isFieldMultiSelectValue } from '@/object-record/record-field/types/guards/isFieldMultiSelectValue.ts';
+import { isFieldMultiSelectValue } from '@/object-record/record-field/types/guards/isFieldMultiSelectValue';
import { isFieldRawJson } from '@/object-record/record-field/types/guards/isFieldRawJson';
import { isFieldRawJsonValue } from '@/object-record/record-field/types/guards/isFieldRawJsonValue';
import { isFieldSelect } from '@/object-record/record-field/types/guards/isFieldSelect';
diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/MultiSelectFieldDisplay.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/MultiSelectFieldDisplay.tsx
index 5cafe51565b0..bd8bb95a1cdb 100644
--- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/MultiSelectFieldDisplay.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/MultiSelectFieldDisplay.tsx
@@ -1,13 +1,16 @@
-import styled from '@emotion/styled';
-
import { useMultiSelectField } from '@/object-record/record-field/meta-types/hooks/useMultiSelectField';
import { Tag } from '@/ui/display/tag/components/Tag';
+import {
+ ExpandableList,
+ ExpandableListProps,
+} from '@/ui/layout/expandable-list/components/ExpandableList';
-const StyledTagContainer = styled.div`
- display: flex;
- gap: ${({ theme }) => theme.spacing(1)};
-`;
-export const MultiSelectFieldDisplay = () => {
+type MultiSelectFieldDisplayProps = ExpandableListProps;
+export const MultiSelectFieldDisplay = ({
+ isHovered,
+ reference,
+ withDropDownBorder,
+}: MultiSelectFieldDisplayProps) => {
const { fieldValues, fieldDefinition } = useMultiSelectField();
const selectedOptions = fieldValues
@@ -17,7 +20,11 @@ export const MultiSelectFieldDisplay = () => {
: [];
return selectedOptions ? (
-
+
{selectedOptions.map((selectedOption, index) => (
{
text={selectedOption.label}
/>
))}
-
+
) : (
<>>
);
diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useMultiSelectField.ts b/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useMultiSelectField.ts
index 9cd2bed9663f..39816a277e32 100644
--- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useMultiSelectField.ts
+++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useMultiSelectField.ts
@@ -9,7 +9,7 @@ import { assertFieldMetadata } from '@/object-record/record-field/types/guards/a
import { isFieldMultiSelect } from '@/object-record/record-field/types/guards/isFieldMultiSelect';
import { isFieldMultiSelectValue } from '@/object-record/record-field/types/guards/isFieldMultiSelectValue';
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
-import { FieldMetadataType } from '~/generated/graphql.tsx';
+import { FieldMetadataType } from '~/generated/graphql';
export const useMultiSelectField = () => {
const { entityId, fieldDefinition, hotkeyScope } = useContext(FieldContext);
diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/MultiSelectFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/MultiSelectFieldInput.tsx
index 0614ba33b866..38c3cca0c588 100644
--- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/MultiSelectFieldInput.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/MultiSelectFieldInput.tsx
@@ -7,7 +7,7 @@ import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
-import { MenuItemMultiSelectTag } from '@/ui/navigation/menu-item/components/MenuItemMultiSelectTag.tsx';
+import { MenuItemMultiSelectTag } from '@/ui/navigation/menu-item/components/MenuItemMultiSelectTag';
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
import { isDefined } from '~/utils/isDefined';
diff --git a/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellContainer.tsx b/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellContainer.tsx
index e3db9d7100f9..185a9924996c 100644
--- a/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellContainer.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellContainer.tsx
@@ -1,12 +1,12 @@
-import { useContext, useState } from 'react';
+import React, { useContext, useEffect, useRef, useState } from 'react';
import { Tooltip } from 'react-tooltip';
import { css, useTheme } from '@emotion/react';
import styled from '@emotion/styled';
-import { motion } from 'framer-motion';
import { IconComponent } from 'twenty-ui';
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import { EllipsisDisplay } from '@/ui/field/display/components/EllipsisDisplay';
+import { ExpandableListProps } from '@/ui/layout/expandable-list/components/ExpandableList';
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
import { useInlineCell } from '../hooks/useInlineCell';
@@ -48,11 +48,6 @@ const StyledLabelContainer = styled.div<{ width?: number }>`
width: ${({ width }) => width}px;
`;
-const StyledEditButtonContainer = styled(motion.div)`
- align-items: center;
- display: flex;
-`;
-
const StyledClickableContainer = styled.div<{ readonly?: boolean }>`
display: flex;
gap: ${({ theme }) => theme.spacing(1)};
@@ -119,18 +114,24 @@ export const RecordInlineCellContainer = ({
disableHoverEffect,
}: RecordInlineCellContainerProps) => {
const { entityId, fieldDefinition } = useContext(FieldContext);
+ const reference = useRef(null);
const [isHovered, setIsHovered] = useState(false);
+ const [isHoveredForDisplayMode, setIsHoveredForDisplayMode] = useState(false);
+ const [newDisplayModeContent, setNewDisplayModeContent] =
+ useState(displayModeContent);
const handleContainerMouseEnter = () => {
if (!readonly) {
setIsHovered(true);
}
+ setIsHoveredForDisplayMode(true);
};
const handleContainerMouseLeave = () => {
if (!readonly) {
setIsHovered(false);
}
+ setIsHoveredForDisplayMode(false);
};
const { isInlineCellInEditMode, openInlineCell } = useInlineCell();
@@ -151,6 +152,17 @@ export const RecordInlineCellContainer = ({
const theme = useTheme();
const labelId = `label-${entityId}-${fieldDefinition?.metadata?.fieldName}`;
+ useEffect(() => {
+ if (React.isValidElement(displayModeContent)) {
+ setNewDisplayModeContent(
+ React.cloneElement(displayModeContent, {
+ isHovered: isHoveredForDisplayMode,
+ reference: reference.current || undefined,
+ }),
+ );
+ }
+ }, [isHoveredForDisplayMode, displayModeContent, reference]);
+
return (
)}
-
+
{!readonly && isInlineCellInEditMode ? (
{editModeContent}
) : editModeContentOnly ? (
@@ -208,18 +220,9 @@ export const RecordInlineCellContainer = ({
isHovered={isHovered}
emptyPlaceholder={showLabel ? 'Empty' : label}
>
- {displayModeContent}
+ {newDisplayModeContent}
- {showEditButton && (
-
-
-
- )}
+ {showEditButton && }
)}
diff --git a/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellEditButton.tsx b/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellEditButton.tsx
index 3bd50946bce3..0475a925606a 100644
--- a/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellEditButton.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellEditButton.tsx
@@ -1,13 +1,23 @@
+import styled from '@emotion/styled';
import { IconComponent } from 'twenty-ui';
+import { AnimatedContainer } from '@/object-record/record-table/components/AnimatedContainer';
import { FloatingIconButton } from '@/ui/input/button/components/FloatingIconButton';
+const StyledInlineCellButtonContainer = styled.div`
+ align-items: center;
+ display: flex;
+`;
export const RecordInlineCellButton = ({ Icon }: { Icon: IconComponent }) => {
return (
-
+
+
+
+
+
);
};
diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/AnimatedContainer.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/AnimatedContainer.tsx
new file mode 100644
index 000000000000..c821abb3794f
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-table/components/AnimatedContainer.tsx
@@ -0,0 +1,20 @@
+import React from 'react';
+import styled from '@emotion/styled';
+import { motion } from 'framer-motion';
+
+const StyledAnimatedChipContainer = styled(motion.div)``;
+
+export const AnimatedContainer = ({
+ children,
+}: {
+ children: React.ReactNode;
+}) => (
+
+ {children}
+
+);
diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCell.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCell.tsx
index 201043957a81..9d89372d7cd8 100644
--- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCell.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCell.tsx
@@ -102,7 +102,7 @@ export const RecordTableCell = ({
isReadOnly={isReadOnly}
/>
}
- nonEditModeContent={}
+ nonEditModeContent={}
/>
);
};
diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellButton.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellButton.tsx
index 51202803b217..287df8331f07 100644
--- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellButton.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellButton.tsx
@@ -1,12 +1,11 @@
import styled from '@emotion/styled';
-import { motion } from 'framer-motion';
import { IconComponent } from 'twenty-ui';
+import { AnimatedContainer } from '@/object-record/record-table/components/AnimatedContainer';
import { FloatingIconButton } from '@/ui/input/button/components/FloatingIconButton';
-const StyledEditButtonContainer = styled(motion.div)`
- position: absolute;
- right: 5px;
+const StyledButtonContainer = styled.div`
+ margin: ${({ theme }) => theme.spacing(1)};
`;
type RecordTableCellButtonProps = {
@@ -18,12 +17,9 @@ export const RecordTableCellButton = ({
onClick,
Icon,
}: RecordTableCellButtonProps) => (
-
-
-
+
+
+
+
+
);
diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellContainer.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellContainer.tsx
index 044bf895513c..17cc3011af02 100644
--- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellContainer.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellContainer.tsx
@@ -1,4 +1,10 @@
-import React, { ReactElement, useContext, useState } from 'react';
+import React, {
+ ReactElement,
+ useContext,
+ useEffect,
+ useRef,
+ useState,
+} from 'react';
import styled from '@emotion/styled';
import { useRecoilValue } from 'recoil';
import { IconArrowUpRight } from 'twenty-ui';
@@ -14,6 +20,7 @@ import { useOpenRecordTableCellFromCell } from '@/object-record/record-table/rec
import { RecordTableScopeInternalContext } from '@/object-record/record-table/scopes/scope-internal-context/RecordTableScopeInternalContext';
import { isSoftFocusOnTableCellComponentFamilyState } from '@/object-record/record-table/states/isSoftFocusOnTableCellComponentFamilyState';
import { isTableCellInEditModeComponentFamilyState } from '@/object-record/record-table/states/isTableCellInEditModeComponentFamilyState';
+import { ExpandableListProps } from '@/ui/layout/expandable-list/components/ExpandableList';
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId';
import { getScopeIdOrUndefinedFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdOrUndefinedFromComponentId';
@@ -33,7 +40,7 @@ const StyledTd = styled.td<{ isSelected: boolean; isInEditMode: boolean }>`
z-index: ${({ isInEditMode }) => (isInEditMode ? '4 !important' : '3')};
`;
-const StyledCellBaseContainer = styled.div`
+const StyledCellBaseContainer = styled.div<{ softFocus: boolean }>`
align-items: center;
box-sizing: border-box;
cursor: pointer;
@@ -41,6 +48,12 @@ const StyledCellBaseContainer = styled.div`
height: 32px;
position: relative;
user-select: none;
+ ${(props) =>
+ props.softFocus
+ ? `background: ${props.theme.background.transparent.secondary};
+ border-radius: ${props.theme.border.radius.sm};
+ outline: 1px solid ${props.theme.font.color.extraLight};`
+ : ''}
`;
export type RecordTableCellContainerProps = {
@@ -63,6 +76,10 @@ export const RecordTableCellContainer = ({
editHotkeyScope,
}: RecordTableCellContainerProps) => {
const { columnIndex } = useContext(RecordTableCellContext);
+ const reference = useRef(null);
+ const [isHovered, setIsHovered] = useState(false);
+ const [newNonEditModeContent, setNewNonEditModeContent] =
+ useState(nonEditModeContent);
const { isReadOnly, isSelected, recordId } = useContext(
RecordTableRowContext,
);
@@ -71,8 +88,6 @@ export const RecordTableCellContainer = ({
const cellPosition = useCurrentTableCellPosition();
- const [isHovered, setIsHovered] = useState(false);
-
const { openTableCell } = useOpenRecordTableCellFromCell();
const tableScopeId = useAvailableScopeIdOrThrow(
@@ -135,8 +150,20 @@ export const RecordTableCellContainer = ({
(!isFirstColumn || !isEmpty) &&
!isReadOnly;
+ useEffect(() => {
+ if (React.isValidElement(nonEditModeContent)) {
+ setNewNonEditModeContent(
+ React.cloneElement(nonEditModeContent, {
+ isHovered: showButton,
+ reference: reference.current || undefined,
+ }),
+ );
+ }
+ }, [nonEditModeContent, showButton, reference]);
+
return (
{isCurrentTableCellInEditMode ? (
{editModeContent}
) : hasSoftFocus ? (
<>
+
+ {editModeContentOnly ? editModeContent : newNonEditModeContent}
+
{showButton && (
)}
-
- {editModeContentOnly ? editModeContent : nonEditModeContent}
-
>
) : (
<>
+ {!isEmpty && (
+
+ {editModeContentOnly
+ ? editModeContent
+ : newNonEditModeContent}
+
+ )}
{showButton && (
)}
- {!isEmpty && (
-
- {editModeContentOnly ? editModeContent : nonEditModeContent}
-
- )}
>
)}
diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellEditButton.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellEditButton.tsx
deleted file mode 100644
index 5aec39f9a1f7..000000000000
--- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellEditButton.tsx
+++ /dev/null
@@ -1,29 +0,0 @@
-import styled from '@emotion/styled';
-import { motion } from 'framer-motion';
-import { IconComponent } from 'twenty-ui';
-
-import { FloatingIconButton } from '@/ui/input/button/components/FloatingIconButton';
-
-const StyledEditButtonContainer = styled(motion.div)`
- position: absolute;
- right: 5px;
-`;
-
-type RecordTableCellEditButtonProps = {
- onClick?: () => void;
- Icon: IconComponent;
-};
-
-export const RecordTableCellEditButton = ({
- onClick,
- Icon,
-}: RecordTableCellEditButtonProps) => (
-
-
-
-);
diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellSoftFocusMode.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellSoftFocusMode.tsx
index ecec3005c8ef..3c016ddb2006 100644
--- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellSoftFocusMode.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellSoftFocusMode.tsx
@@ -101,7 +101,6 @@ export const RecordTableCellSoftFocusMode = ({
return (
{children}
diff --git a/packages/twenty-front/src/modules/ui/display/expandable-list/ExpandableList.tsx b/packages/twenty-front/src/modules/ui/display/expandable-list/ExpandableList.tsx
deleted file mode 100644
index 631dd788bb95..000000000000
--- a/packages/twenty-front/src/modules/ui/display/expandable-list/ExpandableList.tsx
+++ /dev/null
@@ -1,104 +0,0 @@
-import React, { ReactElement, useRef, useState } from 'react';
-import { createPortal } from 'react-dom';
-import styled from '@emotion/styled';
-import { Chip, ChipVariant } from 'twenty-ui';
-
-import { IntersectionObserverWrapper } from '@/ui/display/expandable-list/IntersectionObserverWrapper';
-import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
-
-const StyledContainer = styled.div`
- align-items: center;
- display: flex;
- flex: 1;
- gap: ${({ theme }) => theme.spacing(1)};
- box-sizing: border-box;
- white-space: nowrap;
- overflow-x: hidden;
-`;
-
-const StyledExpendableCell = styled.div`
- align-content: center;
- align-items: center;
- backdrop-filter: ${({ theme }) => theme.blur.strong};
- background: ${({ theme }) => theme.background.secondary};
- border: 1px solid ${({ theme }) => theme.border.color.medium};
- border-radius: ${({ theme }) => theme.border.radius.sm};
- box-shadow: ${({ theme }) => theme.boxShadow.light};
- box-sizing: border-box;
- display: flex;
- flex-wrap: wrap;
- gap: ${({ theme }) => theme.spacing(1)};
- padding: ${({ theme }) => theme.spacing(2)};
-`;
-
-export const ExpandableList = ({
- listItems,
- rootRef,
- id,
-}: {
- listItems: ReactElement[];
- rootRef: React.RefObject;
- id: string;
-}) => {
- const [listItemsInView, setListItemsInView] = useState(new Set());
-
- const firstListItem = listItems[0];
-
- const dropdownId = `expandable-list-dropdown-${id}`;
-
- const containerRef = useRef(null);
-
- const divRef = useRef(null);
-
- return (
-
- {firstListItem}
- {listItems.slice(1).map((listItem, index) => (
-
-
- {listItem}
-
- {index === listItemsInView.size - 1 &&
- listItems.length - listItemsInView.size - 1 !== 0 && (
-
- }
- dropdownComponents={
- <>
- {divRef.current &&
- createPortal(
-
- {listItems}
- ,
- divRef.current as HTMLElement,
- )}
- >
- }
- />
- )}
-
- ))}
-
-
- );
-};
diff --git a/packages/twenty-front/src/modules/ui/display/expandable-list/IntersectionObserverWrapper.tsx b/packages/twenty-front/src/modules/ui/display/expandable-list/IntersectionObserverWrapper.tsx
deleted file mode 100644
index d4a81d2e2974..000000000000
--- a/packages/twenty-front/src/modules/ui/display/expandable-list/IntersectionObserverWrapper.tsx
+++ /dev/null
@@ -1,47 +0,0 @@
-import React from 'react';
-import { useInView } from 'react-intersection-observer';
-import styled from '@emotion/styled';
-
-const StyledDiv = styled.div<{ inView?: boolean }>`
- opacity: ${({ inView }) => (inView === undefined || inView ? 1 : 0)};
-`;
-
-export const IntersectionObserverWrapper = ({
- set,
- id,
- rootRef,
- children,
-}: {
- set: React.Dispatch>>;
- id: number;
- rootRef?: React.RefObject;
- children: React.ReactNode;
-}) => {
- const { ref, inView } = useInView({
- threshold: 1,
- onChange: (inView) => {
- if (inView) {
- set((prev: Set) => {
- const newSet = new Set(prev);
- newSet.add(id);
- return newSet;
- });
- }
- if (!inView) {
- set((prev: Set) => {
- const newSet = new Set(prev);
- newSet.delete(id);
- return newSet;
- });
- }
- },
- root: rootRef?.current,
- rootMargin: '0px 0px -50px 0px',
- });
-
- return (
-
- {children}
-
- );
-};
diff --git a/packages/twenty-front/src/modules/ui/display/info/components/Info.tsx b/packages/twenty-front/src/modules/ui/display/info/components/Info.tsx
index 22ab79bf128f..822c193e8d15 100644
--- a/packages/twenty-front/src/modules/ui/display/info/components/Info.tsx
+++ b/packages/twenty-front/src/modules/ui/display/info/components/Info.tsx
@@ -3,7 +3,7 @@ import { css, useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { IconInfoCircle } from 'twenty-ui';
-import { Button } from '@/ui/input/button/components/Button.tsx';
+import { Button } from '@/ui/input/button/components/Button';
export type InfoAccent = 'blue' | 'danger';
export type InfoProps = {
diff --git a/packages/twenty-front/src/modules/ui/input/components/CardPicker.tsx b/packages/twenty-front/src/modules/ui/input/components/CardPicker.tsx
index b0d42ad010d3..e80b990fe1c5 100644
--- a/packages/twenty-front/src/modules/ui/input/components/CardPicker.tsx
+++ b/packages/twenty-front/src/modules/ui/input/components/CardPicker.tsx
@@ -1,7 +1,8 @@
import React from 'react';
import styled from '@emotion/styled';
-import { Radio } from '@/ui/input/components/Radio.tsx';
+import { Radio } from '@/ui/input/components/Radio';
+
const StyledSubscriptionCardContainer = styled.button`
background-color: ${({ theme }) => theme.background.secondary};
border: 1px solid ${({ theme }) => theme.border.color.medium};
diff --git a/packages/twenty-front/src/modules/ui/layout/expandable-list/components/ChildrenContainer.tsx b/packages/twenty-front/src/modules/ui/layout/expandable-list/components/ChildrenContainer.tsx
new file mode 100644
index 000000000000..a22cff682c57
--- /dev/null
+++ b/packages/twenty-front/src/modules/ui/layout/expandable-list/components/ChildrenContainer.tsx
@@ -0,0 +1,57 @@
+import { Dispatch, ReactElement, SetStateAction } from 'react';
+import styled from '@emotion/styled';
+
+import { ChildrenProperty } from '@/ui/layout/expandable-list/components/ExpandableList';
+
+const StyledChildContainer = styled.div<{
+ shrink?: number;
+ isVisible?: boolean;
+ displayHiddenCount?: boolean;
+}>`
+ display: ${({ isVisible = true }) => (isVisible ? 'flex' : 'none')};
+ flex-shrink: ${({ shrink = 1 }) => shrink};
+ overflow: ${({ displayHiddenCount }) =>
+ displayHiddenCount ? 'hidden' : 'none'};
+`;
+
+const StyledChildrenContainer = styled.div`
+ align-items: center;
+ display: flex;
+ gap: ${({ theme }) => theme.spacing(1)};
+ overflow: hidden;
+`;
+export const ChildrenContainer = ({
+ children,
+ childrenProperties,
+ setChildrenWidths,
+ isFocusedMode,
+}: {
+ children: ReactElement[];
+ childrenProperties: Record;
+ setChildrenWidths: Dispatch>>;
+ isFocusedMode: boolean;
+}) => {
+ return (
+
+ {children.map((child, index) => {
+ return (
+ {
+ if (!el || isFocusedMode) return;
+ setChildrenWidths((prevState) => {
+ prevState[index] = el.getBoundingClientRect().width;
+ return prevState;
+ });
+ }}
+ key={index}
+ displayHiddenCount={isFocusedMode}
+ isVisible={childrenProperties[index]?.isVisible}
+ shrink={childrenProperties[index]?.shrink}
+ >
+ {child}
+
+ );
+ })}
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/ui/layout/expandable-list/components/ExpandableList.tsx b/packages/twenty-front/src/modules/ui/layout/expandable-list/components/ExpandableList.tsx
new file mode 100644
index 000000000000..6fe0f6d3de53
--- /dev/null
+++ b/packages/twenty-front/src/modules/ui/layout/expandable-list/components/ExpandableList.tsx
@@ -0,0 +1,147 @@
+import { ReactElement, useEffect, useState } from 'react';
+import styled from '@emotion/styled';
+import { offset, useFloating } from '@floating-ui/react';
+import { Chip, ChipVariant } from 'twenty-ui';
+
+import { AnimatedContainer } from '@/object-record/record-table/components/AnimatedContainer';
+import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
+import { ChildrenContainer } from '@/ui/layout/expandable-list/components/ChildrenContainer';
+import { getChildrenProperties } from '@/ui/layout/expandable-list/utils/getChildProperties';
+import { getChipContentWidth } from '@/ui/layout/expandable-list/utils/getChipContentWidth';
+
+export const GAP_WIDTH = 4;
+
+const StyledContainer = styled.div`
+ align-items: center;
+ display: flex;
+ gap: ${({ theme }) => theme.spacing(1)};
+ justify-content: space-between;
+ width: 100%;
+`;
+
+const StyledRelationsListContainer = styled.div<{
+ withDropDownBorder?: boolean;
+}>`
+ backdrop-filter: ${({ theme }) => theme.blur.strong};
+ background-color: ${({ theme }) => theme.background.secondary};
+ border-radius: ${({ theme }) => theme.border.radius.sm};
+ box-shadow: '0px 2px 4px ${({ theme }) =>
+ theme.boxShadow.light}, 2px 4px 16px ${({ theme }) =>
+ theme.boxShadow.strong}';
+ display: flex;
+ flex-wrap: wrap;
+ gap: ${({ theme }) => theme.spacing(1)};
+ padding: ${({ theme }) => theme.spacing(2)};
+ outline: ${(props) =>
+ props.withDropDownBorder
+ ? `1px solid ${props.theme.font.color.extraLight}`
+ : 'none'};
+`;
+
+export type ExpandableListProps = {
+ isHovered?: boolean;
+ reference?: HTMLDivElement;
+ forceDisplayHiddenCount?: boolean;
+ withDropDownBorder?: boolean;
+};
+
+export type ChildrenProperty = {
+ shrink: number;
+ isVisible: boolean;
+};
+
+export const ExpandableList = ({
+ children,
+ isHovered,
+ reference,
+ forceDisplayHiddenCount = false,
+ withDropDownBorder = false,
+}: {
+ children: ReactElement[];
+} & ExpandableListProps) => {
+ const [containerWidth, setContainerWidth] = useState(0);
+ const [isDropdownMenuOpen, setIsDropdownMenuOpen] = useState(false);
+ const [childrenWidths, setChildrenWidths] = useState>(
+ {},
+ );
+
+ // Because Chip width depends on the number of hidden children which depends on the Chip width, we have a circular dependency
+ // To avoid it, we set the Chip width and make sure it can display its content (a number greater than 1)
+ const chipContentWidth = getChipContentWidth(children.length);
+ const chipContainerWidth = chipContentWidth + 2 * GAP_WIDTH; // Because Chip component has 4px padding-left and right
+ const availableWidth = containerWidth - (chipContainerWidth + GAP_WIDTH); // Because there is a 4px gap between ChildrenContainer and ChipContainer
+ const isFocusedMode =
+ (isHovered || forceDisplayHiddenCount) &&
+ Object.values(childrenWidths).length > 0;
+
+ const childrenProperties = getChildrenProperties(
+ isFocusedMode,
+ availableWidth,
+ childrenWidths,
+ );
+
+ const hiddenChildrenCount = Object.values(childrenProperties).filter(
+ (childProperties) => !childProperties.isVisible,
+ ).length;
+
+ const displayHiddenCountChip = isFocusedMode && hiddenChildrenCount > 0;
+
+ const { refs, floatingStyles } = useFloating({
+ // @ts-expect-error placement accepts 'start' as value even if the typing does not permit it
+ placement: 'start',
+ middleware: [offset({ mainAxis: -1, crossAxis: -1 })],
+ elements: { reference },
+ });
+
+ const openDropdownMenu = (event: React.MouseEvent) => {
+ event.stopPropagation();
+ setIsDropdownMenuOpen(true);
+ };
+
+ useEffect(() => {
+ if (!isHovered) {
+ setIsDropdownMenuOpen(false);
+ }
+ }, [isHovered]);
+
+ return (
+ {
+ if (!el) return;
+ setContainerWidth(el.getBoundingClientRect().width);
+ }}
+ >
+
+ {children}
+
+ {displayHiddenCountChip && (
+
+
+
+ )}
+ {isDropdownMenuOpen && (
+
+
+ {children}
+
+
+ )}
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/ui/layout/expandable-list/components/__stories__/ExpandableList.stories.tsx b/packages/twenty-front/src/modules/ui/layout/expandable-list/components/__stories__/ExpandableList.stories.tsx
new file mode 100644
index 000000000000..7cf3bcb38522
--- /dev/null
+++ b/packages/twenty-front/src/modules/ui/layout/expandable-list/components/__stories__/ExpandableList.stories.tsx
@@ -0,0 +1,74 @@
+import { ReactElement, useRef, useState } from 'react';
+import styled from '@emotion/styled';
+import { Meta, StoryObj } from '@storybook/react';
+import { ComponentDecorator } from 'packages/twenty-ui';
+
+import { Tag } from '@/ui/display/tag/components/Tag';
+import {
+ ExpandableList,
+ ExpandableListProps,
+} from '@/ui/layout/expandable-list/components/ExpandableList';
+import { MAIN_COLOR_NAMES } from '@/ui/theme/constants/MainColorNames';
+
+const StyledContainer = styled.div`
+ padding: ${({ theme }) => theme.spacing(1)};
+ width: 300px;
+`;
+
+type RenderProps = ExpandableListProps & {
+ children: ReactElement[];
+};
+
+const Render = (args: RenderProps) => {
+ const [isHovered, setIsHovered] = useState(false);
+ const reference = useRef(null);
+
+ return (
+ setIsHovered(true)}
+ onMouseLeave={() => setIsHovered(false)}
+ >
+
+ {args.children}
+
+
+ );
+};
+
+const meta: Meta = {
+ title: 'UI/Layout/ExpandableList/ExpandableList',
+ component: ExpandableList,
+ decorators: [ComponentDecorator],
+ args: {
+ children: [
+ ,
+ ,
+ ,
+ ,
+ ,
+ ,
+ ,
+ ],
+ isHovered: undefined,
+ reference: undefined,
+ forceDisplayHiddenCount: false,
+ withDropDownBorder: false,
+ },
+ argTypes: {
+ children: { control: false },
+ isHovered: { control: false },
+ reference: { control: false },
+ },
+ render: Render,
+};
+
+export default meta;
+type Story = StoryObj;
+
+export const Default: Story = {};
diff --git a/packages/twenty-front/src/modules/ui/layout/expandable-list/utils/__tests__/getChildProperties.test.ts b/packages/twenty-front/src/modules/ui/layout/expandable-list/utils/__tests__/getChildProperties.test.ts
new file mode 100644
index 000000000000..56e75c67d7ef
--- /dev/null
+++ b/packages/twenty-front/src/modules/ui/layout/expandable-list/utils/__tests__/getChildProperties.test.ts
@@ -0,0 +1,41 @@
+import { getChildrenProperties } from '@/ui/layout/expandable-list/utils/getChildProperties';
+
+describe('getChildrenProperties', () => {
+ it('should return default value when isFocused is False', () => {
+ const isFocused = false;
+ const availableWidth = 100;
+ expect(getChildrenProperties(isFocused, availableWidth, {})).toEqual({});
+
+ expect(
+ getChildrenProperties(isFocused, availableWidth, { 0: 40, 1: 40 }),
+ ).toEqual({});
+ });
+
+ it('should return proper value when isFocused is True', () => {
+ const isFocused = true;
+ const availableWidth = 100;
+ expect(getChildrenProperties(isFocused, availableWidth, {})).toEqual({});
+
+ expect(
+ getChildrenProperties(isFocused, availableWidth, { 0: 40, 1: 40 }),
+ ).toEqual({
+ 0: { shrink: 0, isVisible: true },
+ 1: { shrink: 0, isVisible: true },
+ });
+ expect(
+ getChildrenProperties(isFocused, availableWidth, {
+ 0: 40,
+ 1: 40,
+ 2: 40,
+ 3: 40,
+ 4: 40,
+ }),
+ ).toEqual({
+ 0: { shrink: 0, isVisible: true },
+ 1: { shrink: 0, isVisible: true },
+ 2: { shrink: 1, isVisible: true },
+ 3: { shrink: 1, isVisible: false },
+ 4: { shrink: 1, isVisible: false },
+ });
+ });
+});
diff --git a/packages/twenty-front/src/modules/ui/layout/expandable-list/utils/__tests__/getChipContentWidth.test.ts b/packages/twenty-front/src/modules/ui/layout/expandable-list/utils/__tests__/getChipContentWidth.test.ts
new file mode 100644
index 000000000000..61345210750e
--- /dev/null
+++ b/packages/twenty-front/src/modules/ui/layout/expandable-list/utils/__tests__/getChipContentWidth.test.ts
@@ -0,0 +1,13 @@
+import { getChipContentWidth } from '@/ui/layout/expandable-list/utils/getChipContentWidth';
+
+describe('getChipContentWidth', () => {
+ it('should return proper value', () => {
+ expect(getChipContentWidth(0)).toEqual(0);
+ expect(getChipContentWidth(1)).toEqual(0);
+ expect(getChipContentWidth(2)).toEqual(17);
+ expect(getChipContentWidth(20)).toEqual(25);
+ expect(getChipContentWidth(200)).toEqual(33);
+ expect(getChipContentWidth(2000)).toEqual(41);
+ expect(getChipContentWidth(20000)).toEqual(49);
+ });
+});
diff --git a/packages/twenty-front/src/modules/ui/layout/expandable-list/utils/getChildProperties.ts b/packages/twenty-front/src/modules/ui/layout/expandable-list/utils/getChildProperties.ts
new file mode 100644
index 000000000000..1528bedfce2c
--- /dev/null
+++ b/packages/twenty-front/src/modules/ui/layout/expandable-list/utils/getChildProperties.ts
@@ -0,0 +1,30 @@
+import {
+ ChildrenProperty,
+ GAP_WIDTH,
+} from '@/ui/layout/expandable-list/components/ExpandableList';
+
+export const getChildrenProperties = (
+ isFocusedMode: boolean,
+ availableWidth: number,
+ childrenWidths: Record,
+) => {
+ if (!isFocusedMode) {
+ return {};
+ }
+ let cumulatedChildrenWidth = 0;
+ const result: Record = {};
+ Object.values(childrenWidths).forEach((width, index) => {
+ // Because there is a 4px gap between children
+ const childWidth = width + GAP_WIDTH;
+ let shrink = 1;
+ let isVisible = true;
+ if (cumulatedChildrenWidth > availableWidth) {
+ isVisible = false;
+ } else if (cumulatedChildrenWidth + childWidth <= availableWidth) {
+ shrink = 0;
+ }
+ result[index] = { shrink, isVisible };
+ cumulatedChildrenWidth += childWidth;
+ });
+ return result;
+};
diff --git a/packages/twenty-front/src/modules/ui/layout/expandable-list/utils/getChipContentWidth.ts b/packages/twenty-front/src/modules/ui/layout/expandable-list/utils/getChipContentWidth.ts
new file mode 100644
index 000000000000..dcf50862f7bf
--- /dev/null
+++ b/packages/twenty-front/src/modules/ui/layout/expandable-list/utils/getChipContentWidth.ts
@@ -0,0 +1,6 @@
+export const getChipContentWidth = (numberOfChildren: number) => {
+ if (numberOfChildren <= 1) {
+ return 0;
+ }
+ return 17 + 8 * Math.trunc(Math.log10(numberOfChildren));
+};
diff --git a/packages/twenty-front/src/modules/ui/layout/right-drawer/components/RightDrawer.tsx b/packages/twenty-front/src/modules/ui/layout/right-drawer/components/RightDrawer.tsx
index b5de9a806336..907d16dda6bd 100644
--- a/packages/twenty-front/src/modules/ui/layout/right-drawer/components/RightDrawer.tsx
+++ b/packages/twenty-front/src/modules/ui/layout/right-drawer/components/RightDrawer.tsx
@@ -6,6 +6,7 @@ import { useRecoilCallback, useRecoilState, useRecoilValue } from 'recoil';
import { Key } from 'ts-key-enum';
import { RIGHT_DRAWER_CLICK_OUTSIDE_LISTENER_ID } from '@/ui/layout/right-drawer/constants/RightDrawerClickOutsideListener';
+import { isRightDrawerAnimationCompletedState } from '@/ui/layout/right-drawer/states/isRightDrawerAnimationCompleted';
import { rightDrawerCloseEventState } from '@/ui/layout/right-drawer/states/rightDrawerCloseEventsState';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { useClickOutsideListener } from '@/ui/utilities/pointer-event/hooks/useClickOutsideListener';
@@ -46,6 +47,9 @@ export const RightDrawer = () => {
);
const isRightDrawerExpanded = useRecoilValue(isRightDrawerExpandedState);
+ const [, setIsRightDrawerAnimationCompleted] = useRecoilState(
+ isRightDrawerAnimationCompletedState,
+ );
const rightDrawerPage = useRecoilValue(rightDrawerPageState);
@@ -112,6 +116,9 @@ export const RightDrawer = () => {
x: '100%',
},
};
+ const handleAnimationComplete = () => {
+ setIsRightDrawerAnimationCompleted(isRightDrawerOpen);
+ };
return (
{
transition={{
duration: theme.animation.duration.normal,
}}
+ onAnimationComplete={handleAnimationComplete}
>
{isRightDrawerOpen && }
diff --git a/packages/twenty-front/src/modules/ui/layout/right-drawer/states/isRightDrawerAnimationCompleted.ts b/packages/twenty-front/src/modules/ui/layout/right-drawer/states/isRightDrawerAnimationCompleted.ts
new file mode 100644
index 000000000000..3e1cb030a30c
--- /dev/null
+++ b/packages/twenty-front/src/modules/ui/layout/right-drawer/states/isRightDrawerAnimationCompleted.ts
@@ -0,0 +1,6 @@
+import { createState } from 'twenty-ui';
+
+export const isRightDrawerAnimationCompletedState = createState({
+ key: 'isRightDrawerAnimationCompletedState',
+ defaultValue: false,
+});
diff --git a/packages/twenty-front/src/modules/ui/navigation/link/components/GithubVersionLink.tsx b/packages/twenty-front/src/modules/ui/navigation/link/components/GithubVersionLink.tsx
index 57e86a6e67d4..0d2ed27331cc 100644
--- a/packages/twenty-front/src/modules/ui/navigation/link/components/GithubVersionLink.tsx
+++ b/packages/twenty-front/src/modules/ui/navigation/link/components/GithubVersionLink.tsx
@@ -1,7 +1,7 @@
import { useTheme } from '@emotion/react';
import { IconBrandGithub } from 'twenty-ui';
-import { ActionLink } from '@/ui/navigation/link/components/ActionLink.tsx';
+import { ActionLink } from '@/ui/navigation/link/components/ActionLink';
import packageJson from '../../../../../../package.json';
import { GITHUB_LINK } from '../constants/GithubLink';
diff --git a/packages/twenty-front/src/modules/ui/navigation/link/components/__stories__/ActionLink.stories.tsx b/packages/twenty-front/src/modules/ui/navigation/link/components/__stories__/ActionLink.stories.tsx
index 522c24c112bf..bb7dc0de6ad7 100644
--- a/packages/twenty-front/src/modules/ui/navigation/link/components/__stories__/ActionLink.stories.tsx
+++ b/packages/twenty-front/src/modules/ui/navigation/link/components/__stories__/ActionLink.stories.tsx
@@ -1,7 +1,7 @@
import { Meta, StoryObj } from '@storybook/react';
import { ComponentDecorator } from 'twenty-ui';
-import { ActionLink } from '@/ui/navigation/link/components/ActionLink.tsx';
+import { ActionLink } from '@/ui/navigation/link/components/ActionLink';
const meta: Meta = {
title: 'UI/navigation/link/ActionLink',
diff --git a/packages/twenty-front/src/pages/auth/ChooseYourPlan.tsx b/packages/twenty-front/src/pages/auth/ChooseYourPlan.tsx
index 9736eabb660c..882e39df9596 100644
--- a/packages/twenty-front/src/pages/auth/ChooseYourPlan.tsx
+++ b/packages/twenty-front/src/pages/auth/ChooseYourPlan.tsx
@@ -3,21 +3,21 @@ import styled from '@emotion/styled';
import { isNonEmptyString, isNumber } from '@sniptt/guards';
import { useRecoilValue } from 'recoil';
-import { SubTitle } from '@/auth/components/SubTitle.tsx';
-import { Title } from '@/auth/components/Title.tsx';
-import { SubscriptionBenefit } from '@/billing/components/SubscriptionBenefit.tsx';
-import { SubscriptionCard } from '@/billing/components/SubscriptionCard.tsx';
+import { SubTitle } from '@/auth/components/SubTitle';
+import { Title } from '@/auth/components/Title';
+import { SubscriptionBenefit } from '@/billing/components/SubscriptionBenefit';
+import { SubscriptionCard } from '@/billing/components/SubscriptionCard';
import { billingState } from '@/client-config/states/billingState';
import { AppPath } from '@/types/AppPath';
-import { Loader } from '@/ui/feedback/loader/components/Loader.tsx';
+import { Loader } from '@/ui/feedback/loader/components/Loader';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
-import { MainButton } from '@/ui/input/button/components/MainButton.tsx';
-import { CardPicker } from '@/ui/input/components/CardPicker.tsx';
+import { MainButton } from '@/ui/input/button/components/MainButton';
+import { CardPicker } from '@/ui/input/components/CardPicker';
import {
ProductPriceEntity,
useCheckoutSessionMutation,
useGetProductPricesQuery,
-} from '~/generated/graphql.tsx';
+} from '~/generated/graphql';
import { isDefined } from '~/utils/isDefined';
const StyledChoosePlanContainer = styled.div`
diff --git a/packages/twenty-front/src/pages/auth/CreateWorkspace.tsx b/packages/twenty-front/src/pages/auth/CreateWorkspace.tsx
index 81c93bcfee1c..5d598430b538 100644
--- a/packages/twenty-front/src/pages/auth/CreateWorkspace.tsx
+++ b/packages/twenty-front/src/pages/auth/CreateWorkspace.tsx
@@ -17,7 +17,7 @@ import { useApolloMetadataClient } from '@/object-metadata/hooks/useApolloMetada
import { WorkspaceLogoUploader } from '@/settings/workspace/components/WorkspaceLogoUploader';
import { AppPath } from '@/types/AppPath';
import { H2Title } from '@/ui/display/typography/components/H2Title';
-import { Loader } from '@/ui/feedback/loader/components/Loader.tsx';
+import { Loader } from '@/ui/feedback/loader/components/Loader';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { MainButton } from '@/ui/input/button/components/MainButton';
import { TextInputV2 } from '@/ui/input/components/TextInputV2';
diff --git a/packages/twenty-front/src/pages/auth/PaymentSuccess.tsx b/packages/twenty-front/src/pages/auth/PaymentSuccess.tsx
index 200a589b52d6..6be845d67338 100644
--- a/packages/twenty-front/src/pages/auth/PaymentSuccess.tsx
+++ b/packages/twenty-front/src/pages/auth/PaymentSuccess.tsx
@@ -4,12 +4,12 @@ import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { IconCheck } from 'twenty-ui';
-import { SubTitle } from '@/auth/components/SubTitle.tsx';
-import { Title } from '@/auth/components/Title.tsx';
+import { SubTitle } from '@/auth/components/SubTitle';
+import { Title } from '@/auth/components/Title';
import { AppPath } from '@/types/AppPath';
-import { MainButton } from '@/ui/input/button/components/MainButton.tsx';
+import { MainButton } from '@/ui/input/button/components/MainButton';
import { RGBA } from '@/ui/theme/constants/Rgba';
-import { AnimatedEaseIn } from '@/ui/utilities/animation/components/AnimatedEaseIn.tsx';
+import { AnimatedEaseIn } from '@/ui/utilities/animation/components/AnimatedEaseIn';
const StyledCheckContainer = styled.div`
align-items: center;
diff --git a/packages/twenty-front/tsconfig.json b/packages/twenty-front/tsconfig.json
index 9b8b10f5230c..e55b6b262faa 100644
--- a/packages/twenty-front/tsconfig.json
+++ b/packages/twenty-front/tsconfig.json
@@ -9,7 +9,7 @@
"esModuleInterop": true,
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true,
- "allowImportingTsExtensions": true,
+ "allowImportingTsExtensions": false,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
diff --git a/packages/twenty-front/tsup.ui.index.tsx b/packages/twenty-front/tsup.ui.index.tsx
index e16dafe3b1f0..ce31aa7a3c91 100644
--- a/packages/twenty-front/tsup.ui.index.tsx
+++ b/packages/twenty-front/tsup.ui.index.tsx
@@ -1,4 +1,4 @@
-import { ThemeType } from './src/modules/ui/theme/constants/ThemeLight';
+import { ThemeType } from '@/ui/theme/constants/ThemeLight';
export { ThemeProvider } from '@emotion/react';
@@ -21,7 +21,7 @@ export * from './src/modules/ui/input/button/components/FloatingButtonGroup'
export * from './src/modules/ui/input/button/components/FloatingIconButton'
export * from './src/modules/ui/input/button/components/FloatingIconButtonGroup'
export * from './src/modules/ui/input/button/components/LightButton'
-export * from './src/modules/ui/navigation/link/components/ActionLink.tsx'
+export * from './src/modules/ui/navigation/link/components/ActionLink'
export * from './src/modules/ui/input/button/components/LightIconButton'
export * from './src/modules/ui/input/button/components/MainButton'
export * from './src/modules/ui/input/button/components/RoundedIconButton'
diff --git a/packages/twenty-ui/src/display/chip/components/Chip.tsx b/packages/twenty-ui/src/display/chip/components/Chip.tsx
index 7f8130ae6a2d..4ddb950baad9 100644
--- a/packages/twenty-ui/src/display/chip/components/Chip.tsx
+++ b/packages/twenty-ui/src/display/chip/components/Chip.tsx
@@ -51,6 +51,8 @@ const StyledContainer = styled.div<
cursor: ${({ clickable, disabled }) =>
clickable ? 'pointer' : disabled ? 'not-allowed' : 'inherit'};
display: inline-flex;
+ flex-direction: row-reverse;
+ justify-content: center;
gap: ${({ theme }) => theme.spacing(1)};
height: ${({ theme }) => theme.spacing(3)};
max-width: ${({ maxWidth }) =>