martmull
2024-05-03 15:03:06 +02:00
committed by GitHub
parent 1351a95754
commit 87994c26ff
51 changed files with 687 additions and 405 deletions

View File

@ -11,11 +11,11 @@ import { PageTitle } from '@/ui/utilities/page-title/PageTitle';
import { CommandMenuEffect } from '~/effect-components/CommandMenuEffect'; import { CommandMenuEffect } from '~/effect-components/CommandMenuEffect';
import { GotoHotkeysEffect } from '~/effect-components/GotoHotkeysEffect'; import { GotoHotkeysEffect } from '~/effect-components/GotoHotkeysEffect';
import { Authorize } from '~/pages/auth/Authorize'; import { Authorize } from '~/pages/auth/Authorize';
import { ChooseYourPlan } from '~/pages/auth/ChooseYourPlan.tsx'; import { ChooseYourPlan } from '~/pages/auth/ChooseYourPlan';
import { CreateProfile } from '~/pages/auth/CreateProfile'; import { CreateProfile } from '~/pages/auth/CreateProfile';
import { CreateWorkspace } from '~/pages/auth/CreateWorkspace'; import { CreateWorkspace } from '~/pages/auth/CreateWorkspace';
import { PasswordReset } from '~/pages/auth/PasswordReset'; import { PasswordReset } from '~/pages/auth/PasswordReset';
import { PaymentSuccess } from '~/pages/auth/PaymentSuccess.tsx'; import { PaymentSuccess } from '~/pages/auth/PaymentSuccess';
import { SignInUp } from '~/pages/auth/SignInUp'; import { SignInUp } from '~/pages/auth/SignInUp';
import { DefaultHomePage } from '~/pages/DefaultHomePage'; import { DefaultHomePage } from '~/pages/DefaultHomePage';
import { ImpersonateEffect } from '~/pages/impersonate/ImpersonateEffect'; import { ImpersonateEffect } from '~/pages/impersonate/ImpersonateEffect';
@ -46,7 +46,7 @@ import { SettingsIntegrationNewDatabaseConnection } from '~/pages/settings/integ
import { SettingsIntegrations } from '~/pages/settings/integrations/SettingsIntegrations'; import { SettingsIntegrations } from '~/pages/settings/integrations/SettingsIntegrations';
import { SettingsIntegrationShowDatabaseConnection } from '~/pages/settings/integrations/SettingsIntegrationShowDatabaseConnection'; import { SettingsIntegrationShowDatabaseConnection } from '~/pages/settings/integrations/SettingsIntegrationShowDatabaseConnection';
import { SettingsAppearance } from '~/pages/settings/SettingsAppearance'; import { SettingsAppearance } from '~/pages/settings/SettingsAppearance';
import { SettingsBilling } from '~/pages/settings/SettingsBilling.tsx'; import { SettingsBilling } from '~/pages/settings/SettingsBilling';
import { SettingsProfile } from '~/pages/settings/SettingsProfile'; import { SettingsProfile } from '~/pages/settings/SettingsProfile';
import { SettingsWorkspace } from '~/pages/settings/SettingsWorkspace'; import { SettingsWorkspace } from '~/pages/settings/SettingsWorkspace';
import { SettingsWorkspaceMembers } from '~/pages/settings/SettingsWorkspaceMembers'; import { SettingsWorkspaceMembers } from '~/pages/settings/SettingsWorkspaceMembers';

View File

@ -1,14 +1,15 @@
import { useRef } from 'react'; import { useRef, useState } from 'react';
import { useTheme } from '@emotion/react'; import { useTheme } from '@emotion/react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { useRecoilValue } from 'recoil';
import { IconCheck, IconQuestionMark, IconX } from 'twenty-ui'; import { IconCheck, IconQuestionMark, IconX } from 'twenty-ui';
import { v4 } from 'uuid';
import { CalendarEventParticipant } from '@/activities/calendar/types/CalendarEventParticipant'; import { CalendarEventParticipant } from '@/activities/calendar/types/CalendarEventParticipant';
import { ParticipantChip } from '@/activities/components/ParticipantChip'; import { ParticipantChip } from '@/activities/components/ParticipantChip';
import { PropertyBox } from '@/object-record/record-inline-cell/property-box/components/PropertyBox'; 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 { 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` const StyledInlineCellBaseContainer = styled.div`
align-items: center; align-items: center;
@ -55,6 +56,9 @@ const StyledLabelContainer = styled.div<{ width?: number }>`
font-size: ${({ theme }) => theme.font.size.sm}; font-size: ${({ theme }) => theme.font.size.sm};
width: ${({ width }) => width}px; width: ${({ width }) => width}px;
`; `;
const StyledDiv = styled.div`
max-width: 70%;
`;
export const CalendarEventParticipantsResponseStatusField = ({ export const CalendarEventParticipantsResponseStatusField = ({
responseStatus, responseStatus,
@ -64,6 +68,9 @@ export const CalendarEventParticipantsResponseStatusField = ({
participants: CalendarEventParticipant[]; participants: CalendarEventParticipant[];
}) => { }) => {
const theme = useTheme(); const theme = useTheme();
const isRightDrawerAnimationCompleted = useRecoilValue(
isRightDrawerAnimationCompletedState,
);
const Icon = { const Icon = {
Yes: <IconCheck stroke={theme.icon.stroke.sm} />, Yes: <IconCheck stroke={theme.icon.stroke.sm} />,
@ -81,9 +88,9 @@ export const CalendarEventParticipantsResponseStatusField = ({
]; ];
const participantsContainerRef = useRef<HTMLDivElement>(null); const participantsContainerRef = useRef<HTMLDivElement>(null);
const [isHovered, setIsHovered] = useState(false);
const StyledChips = orderedParticipants.map((participant) => ( const styledChips = orderedParticipants.map((participant, index) => (
<ParticipantChip participant={participant} /> <ParticipantChip key={index} participant={participant} />
)); ));
return ( return (
@ -96,12 +103,21 @@ export const CalendarEventParticipantsResponseStatusField = ({
<EllipsisDisplay>{responseStatus}</EllipsisDisplay> <EllipsisDisplay>{responseStatus}</EllipsisDisplay>
</StyledLabelContainer> </StyledLabelContainer>
</StyledLabelAndIconContainer> </StyledLabelAndIconContainer>
<StyledDiv
<ExpandableList ref={participantsContainerRef}
listItems={StyledChips} onMouseEnter={() => setIsHovered(true)}
id={v4()} onMouseLeave={() => setIsHovered(false)}
rootRef={participantsContainerRef} >
/> {isRightDrawerAnimationCompleted && (
<ExpandableList
isHovered={isHovered}
reference={participantsContainerRef.current || undefined}
forceDisplayHiddenCount
>
{styledChips}
</ExpandableList>
)}
</StyledDiv>
</StyledInlineCellBaseContainer> </StyledInlineCellBaseContainer>
</StyledPropertyBox> </StyledPropertyBox>
); );

View File

@ -14,6 +14,7 @@ import {
import { RecordInlineCell } from '@/object-record/record-inline-cell/components/RecordInlineCell'; import { RecordInlineCell } from '@/object-record/record-inline-cell/components/RecordInlineCell';
import { PropertyBox } from '@/object-record/record-inline-cell/property-box/components/PropertyBox'; import { PropertyBox } from '@/object-record/record-inline-cell/property-box/components/PropertyBox';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { isRightDrawerAnimationCompletedState } from '@/ui/layout/right-drawer/states/isRightDrawerAnimationCompleted';
import { isDefined } from '~/utils/isDefined'; import { isDefined } from '~/utils/isDefined';
const StyledPropertyBox = styled(PropertyBox)` const StyledPropertyBox = styled(PropertyBox)`
@ -27,6 +28,10 @@ export const ActivityEditorFields = ({
}) => { }) => {
const { upsertActivity } = useUpsertActivity(); const { upsertActivity } = useUpsertActivity();
const isRightDrawerAnimationCompleted = useRecoilValue(
isRightDrawerAnimationCompletedState,
);
const getRecordFromCache = useGetRecordFromCache({ const getRecordFromCache = useGetRecordFromCache({
objectNameSingular: CoreObjectNameSingular.Activity, objectNameSingular: CoreObjectNameSingular.Activity,
}); });
@ -93,11 +98,16 @@ export const ActivityEditorFields = ({
</AssigneeFieldContextProvider> </AssigneeFieldContextProvider>
</> </>
)} )}
{ActivityTargetsContextProvider && isDefined(activityFromCache) && ( {ActivityTargetsContextProvider &&
<ActivityTargetsContextProvider> isDefined(activityFromCache) &&
<ActivityTargetsInlineCell activity={activityFromCache} /> isRightDrawerAnimationCompleted && (
</ActivityTargetsContextProvider> <ActivityTargetsContextProvider>
)} <ActivityTargetsInlineCell
activity={activityFromCache}
maxWidth={340}
/>
</ActivityTargetsContextProvider>
)}
</StyledPropertyBox> </StyledPropertyBox>
); );
}; };

View File

@ -1,91 +1,47 @@
import { useMemo } from 'react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { Chip, ChipVariant } from 'twenty-ui';
import { v4 } from 'uuid';
import { ActivityTargetWithTargetRecord } from '@/activities/types/ActivityTargetObject'; import { ActivityTargetWithTargetRecord } from '@/activities/types/ActivityTargetObject';
import { RecordChip } from '@/object-record/components/RecordChip'; 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<{ maxWidth?: number }>`
const StyledContainer = styled.div`
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: ${({ theme }) => theme.spacing(1)}; 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 = ({ export const ActivityTargetChips = ({
activityTargetObjectRecords, activityTargetObjectRecords,
isHovered,
reference,
maxWidth,
}: { }: {
activityTargetObjectRecords: ActivityTargetWithTargetRecord[]; activityTargetObjectRecords: ActivityTargetWithTargetRecord[];
}) => { maxWidth?: number;
const dropdownId = useMemo(() => `multiple-relations-dropdown-${v4()}`, []); } & ExpandableListProps) => {
return ( return (
<StyledContainer> <StyledContainer maxWidth={maxWidth}>
{activityTargetObjectRecords <ExpandableList
?.slice(0, MAX_RECORD_CHIPS_DISPLAY) isHovered={isHovered}
.map((activityTargetObjectRecord) => ( reference={reference}
<RecordChip forceDisplayHiddenCount
key={activityTargetObjectRecord.targetObject.id} >
record={activityTargetObjectRecord.targetObject} {activityTargetObjectRecords.map(
objectNameSingular={ (activityTargetObjectRecord, index) => (
activityTargetObjectRecord.targetObjectMetadataItem.nameSingular <RecordChip
} key={index}
/> record={activityTargetObjectRecord.targetObject}
))} objectNameSingular={
activityTargetObjectRecord.targetObjectMetadataItem.nameSingular
{activityTargetObjectRecords.length > MAX_RECORD_CHIPS_DISPLAY && ( }
<div onClick={showMoreRelationsHandler}> />
<Dropdown ),
dropdownId={dropdownId} )}
dropdownHotkeyScope={{ </ExpandableList>
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> </StyledContainer>
); );
}; };

View File

@ -39,6 +39,7 @@ const StyledChip = styled.div`
padding: ${({ theme }) => theme.spacing(1)}; padding: ${({ theme }) => theme.spacing(1)};
height: 20px; height: 20px;
box-sizing: border-box; box-sizing: border-box;
white-space: nowrap;
`; `;
type ParticipantChipVariant = 'default' | 'bold'; type ParticipantChipVariant = 'default' | 'bold';

View File

@ -13,10 +13,16 @@ import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
type ActivityTargetsInlineCellProps = { type ActivityTargetsInlineCellProps = {
activity: Activity; activity: Activity;
showLabel?: boolean;
maxWidth?: number;
readonly?: boolean;
}; };
export const ActivityTargetsInlineCell = ({ export const ActivityTargetsInlineCell = ({
activity, activity,
showLabel = true,
maxWidth,
readonly,
}: ActivityTargetsInlineCellProps) => { }: ActivityTargetsInlineCellProps) => {
const { activityTargetObjectRecords } = const { activityTargetObjectRecords } =
useActivityTargetObjectRecords(activity); useActivityTargetObjectRecords(activity);
@ -37,8 +43,9 @@ export const ActivityTargetsInlineCell = ({
customEditHotkeyScope={{ customEditHotkeyScope={{
scope: ActivityEditorHotkeyScope.ActivityTargets, scope: ActivityEditorHotkeyScope.ActivityTargets,
}} }}
IconLabel={IconArrowUpRight} IconLabel={showLabel ? IconArrowUpRight : undefined}
showLabel={true} showLabel={showLabel}
readonly={readonly}
editModeContent={ editModeContent={
<ActivityTargetInlineCellEditMode <ActivityTargetInlineCellEditMode
activity={activity} activity={activity}
@ -49,6 +56,7 @@ export const ActivityTargetsInlineCell = ({
displayModeContent={ displayModeContent={
<ActivityTargetChips <ActivityTargetChips
activityTargetObjectRecords={activityTargetObjectRecords} activityTargetObjectRecords={activityTargetObjectRecords}
maxWidth={maxWidth}
/> />
} }
isDisplayModeContentEmpty={activityTargetObjectRecords.length === 0} isDisplayModeContentEmpty={activityTargetObjectRecords.length === 0}

View File

@ -100,7 +100,7 @@ export const NoteCard = ({
<StyledCardContent>{body}</StyledCardContent> <StyledCardContent>{body}</StyledCardContent>
</StyledCardDetailsContainer> </StyledCardDetailsContainer>
<StyledFooter> <StyledFooter>
<ActivityTargetsInlineCell activity={note} /> <ActivityTargetsInlineCell activity={note} readonly />
{note.comments && note.comments.length > 0 && ( {note.comments && note.comments.length > 0 && (
<StyledCommentIcon> <StyledCommentIcon>
<IconComment size={theme.icon.size.md} /> <IconComment size={theme.icon.size.md} />

View File

@ -6,9 +6,8 @@ import {
OverflowingTextWithTooltip, OverflowingTextWithTooltip,
} from 'twenty-ui'; } from 'twenty-ui';
import { ActivityTargetChips } from '@/activities/components/ActivityTargetChips';
import { useActivityTargetObjectRecords } from '@/activities/hooks/useActivityTargetObjectRecords';
import { useOpenActivityRightDrawer } from '@/activities/hooks/useOpenActivityRightDrawer'; import { useOpenActivityRightDrawer } from '@/activities/hooks/useOpenActivityRightDrawer';
import { ActivityTargetsInlineCell } from '@/activities/inline-cell/components/ActivityTargetsInlineCell';
import { Activity } from '@/activities/types/Activity'; import { Activity } from '@/activities/types/Activity';
import { getActivitySummary } from '@/activities/utils/getActivitySummary'; import { getActivitySummary } from '@/activities/utils/getActivitySummary';
import { Checkbox, CheckboxShape } from '@/ui/input/components/Checkbox'; import { Checkbox, CheckboxShape } from '@/ui/input/components/Checkbox';
@ -18,7 +17,7 @@ import { useCompleteTask } from '../hooks/useCompleteTask';
const StyledContainer = styled.div` const StyledContainer = styled.div`
align-items: center; align-items: center;
align-self: stretch; justify-content: space-between;
border-bottom: 1px solid ${({ theme }) => theme.border.color.light}; border-bottom: 1px solid ${({ theme }) => theme.border.color.light};
cursor: pointer; cursor: pointer;
display: inline-flex; display: inline-flex;
@ -34,9 +33,6 @@ const StyledContainer = styled.div`
const StyledTaskBody = styled.div` const StyledTaskBody = styled.div`
color: ${({ theme }) => theme.font.color.tertiary}; color: ${({ theme }) => theme.font.color.tertiary};
display: flex; display: flex;
flex-direction: row;
flex-grow: 1;
width: 1px;
`; `;
const StyledTaskTitle = styled.div<{ const StyledTaskTitle = styled.div<{
@ -64,9 +60,10 @@ const StyledDueDate = styled.div<{
display: flex; display: flex;
gap: ${({ theme }) => theme.spacing(1)}; gap: ${({ theme }) => theme.spacing(1)};
padding-left: ${({ theme }) => theme.spacing(2)}; padding-left: ${({ theme }) => theme.spacing(2)};
white-space: nowrap;
`; `;
const StyledFieldsContainer = styled.div` const StyledRightSideContainer = styled.div`
display: flex; display: flex;
`; `;
@ -74,6 +71,14 @@ const StyledPlaceholder = styled.div`
color: ${({ theme }) => theme.font.color.light}; color: ${({ theme }) => theme.font.color.light};
`; `;
const StyledLeftSideContainer = styled.div`
display: flex;
`;
const StyledCheckboxContainer = styled.div`
display: flex;
`;
export const TaskRow = ({ task }: { task: Activity }) => { export const TaskRow = ({ task }: { task: Activity }) => {
const theme = useTheme(); const theme = useTheme();
const openActivityRightDrawer = useOpenActivityRightDrawer(); const openActivityRightDrawer = useOpenActivityRightDrawer();
@ -81,39 +86,42 @@ export const TaskRow = ({ task }: { task: Activity }) => {
const body = getActivitySummary(task.body); const body = getActivitySummary(task.body);
const { completeTask } = useCompleteTask(task); const { completeTask } = useCompleteTask(task);
const { activityTargetObjectRecords } = useActivityTargetObjectRecords(task);
return ( return (
<StyledContainer <StyledContainer
onClick={() => { onClick={() => {
openActivityRightDrawer(task.id); openActivityRightDrawer(task.id);
}} }}
> >
<div <StyledLeftSideContainer>
onClick={(e) => { <StyledCheckboxContainer
e.stopPropagation(); onClick={(e) => {
}} e.stopPropagation();
> }}
<Checkbox >
checked={!!task.completedAt} <Checkbox
shape={CheckboxShape.Rounded} checked={!!task.completedAt}
onCheckedChange={completeTask} shape={CheckboxShape.Rounded}
/> onCheckedChange={completeTask}
</div> />
<StyledTaskTitle completed={task.completedAt !== null}> </StyledCheckboxContainer>
{task.title || <StyledPlaceholder>Task title</StyledPlaceholder>} <StyledTaskTitle completed={task.completedAt !== null}>
</StyledTaskTitle> {task.title || <StyledPlaceholder>Task title</StyledPlaceholder>}
<StyledTaskBody> </StyledTaskTitle>
<OverflowingTextWithTooltip text={body} /> <StyledTaskBody>
{task.comments && task.comments.length > 0 && ( <OverflowingTextWithTooltip text={body} />
<StyledCommentIcon> {task.comments && task.comments.length > 0 && (
<IconComment size={theme.icon.size.md} /> <StyledCommentIcon>
</StyledCommentIcon> <IconComment size={theme.icon.size.md} />
)} </StyledCommentIcon>
</StyledTaskBody> )}
<StyledFieldsContainer> </StyledTaskBody>
<ActivityTargetChips </StyledLeftSideContainer>
activityTargetObjectRecords={activityTargetObjectRecords} <StyledRightSideContainer>
<ActivityTargetsInlineCell
activity={task}
showLabel={false}
maxWidth={200}
readonly
/> />
<StyledDueDate <StyledDueDate
isPast={ isPast={
@ -123,7 +131,7 @@ export const TaskRow = ({ task }: { task: Activity }) => {
<IconCalendar size={theme.icon.size.md} /> <IconCalendar size={theme.icon.size.md} />
{task.dueAt && beautifyExactDate(task.dueAt)} {task.dueAt && beautifyExactDate(task.dueAt)}
</StyledDueDate> </StyledDueDate>
</StyledFieldsContainer> </StyledRightSideContainer>
</StyledContainer> </StyledContainer>
); );
}; };

View File

@ -70,7 +70,7 @@ export const EventsGroup = ({
{group.items.map((event, index) => ( {group.items.map((event, index) => (
<EventRow <EventRow
mainObjectMetadataItem={mainObjectMetadataItem} mainObjectMetadataItem={mainObjectMetadataItem}
key={event.id} key={index}
event={event} event={event}
isLastEvent={index === group.items.length - 1} isLastEvent={index === group.items.length - 1}
/> />

View File

@ -17,7 +17,7 @@ import { captchaProviderState } from '@/client-config/states/captchaProviderStat
import { Loader } from '@/ui/feedback/loader/components/Loader'; import { Loader } from '@/ui/feedback/loader/components/Loader';
import { MainButton } from '@/ui/input/button/components/MainButton'; import { MainButton } from '@/ui/input/button/components/MainButton';
import { TextInput } from '@/ui/input/components/TextInput'; 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 { AnimatedEaseIn } from '@/ui/utilities/animation/components/AnimatedEaseIn';
import { isDefined } from '~/utils/isDefined'; import { isDefined } from '~/utils/isDefined';

View File

@ -1,7 +1,7 @@
import { useCallback } from 'react'; import { useCallback } from 'react';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { useEmailPasswordResetLinkMutation } from '~/generated/graphql.tsx'; import { useEmailPasswordResetLinkMutation } from '~/generated/graphql';
export const useHandleResetPassword = () => { export const useHandleResetPassword = () => {
const { enqueueSnackBar } = useSnackBar(); const { enqueueSnackBar } = useSnackBar();

View File

@ -6,7 +6,7 @@ import { CurrentWorkspace } from '@/auth/states/currentWorkspaceState';
import { previousUrlState } from '@/auth/states/previousUrlState'; import { previousUrlState } from '@/auth/states/previousUrlState';
import { billingState } from '@/client-config/states/billingState'; import { billingState } from '@/client-config/states/billingState';
import { AppPath } from '@/types/AppPath'; import { AppPath } from '@/types/AppPath';
import { WorkspaceMember } from '~/generated/graphql.tsx'; import { WorkspaceMember } from '~/generated/graphql';
export const useNavigateAfterSignInUp = () => { export const useNavigateAfterSignInUp = () => {
const navigate = useNavigate(); const navigate = useNavigate();

View File

@ -1,6 +1,6 @@
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import { useAuth } from '@/auth/hooks/useAuth.ts'; import { useAuth } from '@/auth/hooks/useAuth';
export const useSignInWithMicrosoft = () => { export const useSignInWithMicrosoft = () => {
const workspaceInviteHash = useParams().workspaceInviteHash; const workspaceInviteHash = useParams().workspaceInviteHash;

View File

@ -1,6 +1,6 @@
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import { useGetWorkspaceFromInviteHashQuery } from '~/generated/graphql.tsx'; import { useGetWorkspaceFromInviteHashQuery } from '~/generated/graphql';
export const useWorkspaceFromInviteHash = () => { export const useWorkspaceFromInviteHash = () => {
const workspaceInviteHash = useParams().workspaceInviteHash; const workspaceInviteHash = useParams().workspaceInviteHash;

View File

@ -1,6 +1,6 @@
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { SubscriptionCardPrice } from '@/billing/components/SubscriptionCardPrice.tsx'; import { SubscriptionCardPrice } from '@/billing/components/SubscriptionCardPrice';
import { capitalize } from '~/utils/string/capitalize'; import { capitalize } from '~/utils/string/capitalize';
type SubscriptionCardProps = { type SubscriptionCardProps = {

View File

@ -2,6 +2,7 @@ import { useContext } from 'react';
import { LinksFieldDisplay } from '@/object-record/record-field/meta-types/display/components/LinksFieldDisplay'; import { LinksFieldDisplay } from '@/object-record/record-field/meta-types/display/components/LinksFieldDisplay';
import { isFieldLinks } from '@/object-record/record-field/types/guards/isFieldLinks'; 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 { FieldContext } from '../contexts/FieldContext';
import { AddressFieldDisplay } from '../meta-types/display/components/AddressFieldDisplay'; 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 { FullNameFieldDisplay } from '../meta-types/display/components/FullNameFieldDisplay';
import { JsonFieldDisplay } from '../meta-types/display/components/JsonFieldDisplay'; import { JsonFieldDisplay } from '../meta-types/display/components/JsonFieldDisplay';
import { LinkFieldDisplay } from '../meta-types/display/components/LinkFieldDisplay'; 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 { NumberFieldDisplay } from '../meta-types/display/components/NumberFieldDisplay';
import { PhoneFieldDisplay } from '../meta-types/display/components/PhoneFieldDisplay'; import { PhoneFieldDisplay } from '../meta-types/display/components/PhoneFieldDisplay';
import { RelationFieldDisplay } from '../meta-types/display/components/RelationFieldDisplay'; 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 { isFieldText } from '../types/guards/isFieldText';
import { isFieldUuid } from '../types/guards/isFieldUuid'; 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 { fieldDefinition, isLabelIdentifier } = useContext(FieldContext);
const isChipDisplay = const isChipDisplay =
@ -74,7 +81,11 @@ export const FieldDisplay = () => {
) : isFieldSelect(fieldDefinition) ? ( ) : isFieldSelect(fieldDefinition) ? (
<SelectFieldDisplay /> <SelectFieldDisplay />
) : isFieldMultiSelect(fieldDefinition) ? ( ) : isFieldMultiSelect(fieldDefinition) ? (
<MultiSelectFieldDisplay /> <MultiSelectFieldDisplay
isHovered={isHovered}
reference={reference}
withDropDownBorder={fromTableCell}
/>
) : isFieldAddress(fieldDefinition) ? ( ) : isFieldAddress(fieldDefinition) ? (
<AddressFieldDisplay /> <AddressFieldDisplay />
) : isFieldRawJson(fieldDefinition) ? ( ) : isFieldRawJson(fieldDefinition) ? (

View File

@ -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 { DateFieldInput } from '@/object-record/record-field/meta-types/input/components/DateFieldInput';
import { FullNameFieldInput } from '@/object-record/record-field/meta-types/input/components/FullNameFieldInput'; 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 { 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 { RawJsonFieldInput } from '@/object-record/record-field/meta-types/input/components/RawJsonFieldInput';
import { SelectFieldInput } from '@/object-record/record-field/meta-types/input/components/SelectFieldInput'; import { SelectFieldInput } from '@/object-record/record-field/meta-types/input/components/SelectFieldInput';
import { RecordFieldInputScope } from '@/object-record/record-field/scopes/RecordFieldInputScope'; import { RecordFieldInputScope } from '@/object-record/record-field/scopes/RecordFieldInputScope';

View File

@ -1,6 +1,7 @@
import { useContext } from 'react'; import { useContext } from 'react';
import { IconComponent, IconPencil } from 'twenty-ui'; 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 { isFieldRelation } from '@/object-record/record-field/types/guards/isFieldRelation';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull'; import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
@ -17,7 +18,8 @@ export const useGetButtonIcon = (): IconComponent | undefined => {
if ( if (
isFieldLink(fieldDefinition) || isFieldLink(fieldDefinition) ||
isFieldEmail(fieldDefinition) || isFieldEmail(fieldDefinition) ||
isFieldPhone(fieldDefinition) isFieldPhone(fieldDefinition) ||
isFieldMultiSelect(fieldDefinition)
) { ) {
return IconPencil; return IconPencil;
} }

View File

@ -10,7 +10,7 @@ import { isFieldFullNameValue } from '@/object-record/record-field/types/guards/
import { isFieldLinks } from '@/object-record/record-field/types/guards/isFieldLinks'; import { isFieldLinks } from '@/object-record/record-field/types/guards/isFieldLinks';
import { isFieldLinksValue } from '@/object-record/record-field/types/guards/isFieldLinksValue'; import { isFieldLinksValue } from '@/object-record/record-field/types/guards/isFieldLinksValue';
import { isFieldMultiSelect } from '@/object-record/record-field/types/guards/isFieldMultiSelect'; 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 { isFieldRawJson } from '@/object-record/record-field/types/guards/isFieldRawJson';
import { isFieldRawJsonValue } from '@/object-record/record-field/types/guards/isFieldRawJsonValue'; import { isFieldRawJsonValue } from '@/object-record/record-field/types/guards/isFieldRawJsonValue';
import { isFieldSelect } from '@/object-record/record-field/types/guards/isFieldSelect'; import { isFieldSelect } from '@/object-record/record-field/types/guards/isFieldSelect';

View File

@ -1,13 +1,16 @@
import styled from '@emotion/styled';
import { useMultiSelectField } from '@/object-record/record-field/meta-types/hooks/useMultiSelectField'; import { useMultiSelectField } from '@/object-record/record-field/meta-types/hooks/useMultiSelectField';
import { Tag } from '@/ui/display/tag/components/Tag'; import { Tag } from '@/ui/display/tag/components/Tag';
import {
ExpandableList,
ExpandableListProps,
} from '@/ui/layout/expandable-list/components/ExpandableList';
const StyledTagContainer = styled.div` type MultiSelectFieldDisplayProps = ExpandableListProps;
display: flex; export const MultiSelectFieldDisplay = ({
gap: ${({ theme }) => theme.spacing(1)}; isHovered,
`; reference,
export const MultiSelectFieldDisplay = () => { withDropDownBorder,
}: MultiSelectFieldDisplayProps) => {
const { fieldValues, fieldDefinition } = useMultiSelectField(); const { fieldValues, fieldDefinition } = useMultiSelectField();
const selectedOptions = fieldValues const selectedOptions = fieldValues
@ -17,7 +20,11 @@ export const MultiSelectFieldDisplay = () => {
: []; : [];
return selectedOptions ? ( return selectedOptions ? (
<StyledTagContainer> <ExpandableList
isHovered={isHovered}
reference={reference}
withDropDownBorder={withDropDownBorder}
>
{selectedOptions.map((selectedOption, index) => ( {selectedOptions.map((selectedOption, index) => (
<Tag <Tag
key={index} key={index}
@ -25,7 +32,7 @@ export const MultiSelectFieldDisplay = () => {
text={selectedOption.label} text={selectedOption.label}
/> />
))} ))}
</StyledTagContainer> </ExpandableList>
) : ( ) : (
<></> <></>
); );

View File

@ -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 { isFieldMultiSelect } from '@/object-record/record-field/types/guards/isFieldMultiSelect';
import { isFieldMultiSelectValue } from '@/object-record/record-field/types/guards/isFieldMultiSelectValue'; import { isFieldMultiSelectValue } from '@/object-record/record-field/types/guards/isFieldMultiSelectValue';
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector'; import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
import { FieldMetadataType } from '~/generated/graphql.tsx'; import { FieldMetadataType } from '~/generated/graphql';
export const useMultiSelectField = () => { export const useMultiSelectField = () => {
const { entityId, fieldDefinition, hotkeyScope } = useContext(FieldContext); const { entityId, fieldDefinition, hotkeyScope } = useContext(FieldContext);

View File

@ -7,7 +7,7 @@ import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput'; import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator'; 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 { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
import { isDefined } from '~/utils/isDefined'; import { isDefined } from '~/utils/isDefined';

View File

@ -1,12 +1,12 @@
import { useContext, useState } from 'react'; import React, { useContext, useEffect, useRef, useState } from 'react';
import { Tooltip } from 'react-tooltip'; import { Tooltip } from 'react-tooltip';
import { css, useTheme } from '@emotion/react'; import { css, useTheme } from '@emotion/react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { motion } from 'framer-motion';
import { IconComponent } from 'twenty-ui'; import { IconComponent } from 'twenty-ui';
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext'; import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import { EllipsisDisplay } from '@/ui/field/display/components/EllipsisDisplay'; 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 { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
import { useInlineCell } from '../hooks/useInlineCell'; import { useInlineCell } from '../hooks/useInlineCell';
@ -48,11 +48,6 @@ const StyledLabelContainer = styled.div<{ width?: number }>`
width: ${({ width }) => width}px; width: ${({ width }) => width}px;
`; `;
const StyledEditButtonContainer = styled(motion.div)`
align-items: center;
display: flex;
`;
const StyledClickableContainer = styled.div<{ readonly?: boolean }>` const StyledClickableContainer = styled.div<{ readonly?: boolean }>`
display: flex; display: flex;
gap: ${({ theme }) => theme.spacing(1)}; gap: ${({ theme }) => theme.spacing(1)};
@ -119,18 +114,24 @@ export const RecordInlineCellContainer = ({
disableHoverEffect, disableHoverEffect,
}: RecordInlineCellContainerProps) => { }: RecordInlineCellContainerProps) => {
const { entityId, fieldDefinition } = useContext(FieldContext); const { entityId, fieldDefinition } = useContext(FieldContext);
const reference = useRef<HTMLDivElement>(null);
const [isHovered, setIsHovered] = useState(false); const [isHovered, setIsHovered] = useState(false);
const [isHoveredForDisplayMode, setIsHoveredForDisplayMode] = useState(false);
const [newDisplayModeContent, setNewDisplayModeContent] =
useState<React.ReactNode>(displayModeContent);
const handleContainerMouseEnter = () => { const handleContainerMouseEnter = () => {
if (!readonly) { if (!readonly) {
setIsHovered(true); setIsHovered(true);
} }
setIsHoveredForDisplayMode(true);
}; };
const handleContainerMouseLeave = () => { const handleContainerMouseLeave = () => {
if (!readonly) { if (!readonly) {
setIsHovered(false); setIsHovered(false);
} }
setIsHoveredForDisplayMode(false);
}; };
const { isInlineCellInEditMode, openInlineCell } = useInlineCell(); const { isInlineCellInEditMode, openInlineCell } = useInlineCell();
@ -151,6 +152,17 @@ export const RecordInlineCellContainer = ({
const theme = useTheme(); const theme = useTheme();
const labelId = `label-${entityId}-${fieldDefinition?.metadata?.fieldName}`; const labelId = `label-${entityId}-${fieldDefinition?.metadata?.fieldName}`;
useEffect(() => {
if (React.isValidElement<ExpandableListProps>(displayModeContent)) {
setNewDisplayModeContent(
React.cloneElement(displayModeContent, {
isHovered: isHoveredForDisplayMode,
reference: reference.current || undefined,
}),
);
}
}, [isHoveredForDisplayMode, displayModeContent, reference]);
return ( return (
<StyledInlineCellBaseContainer <StyledInlineCellBaseContainer
onMouseEnter={handleContainerMouseEnter} onMouseEnter={handleContainerMouseEnter}
@ -181,7 +193,7 @@ export const RecordInlineCellContainer = ({
)} )}
</StyledLabelAndIconContainer> </StyledLabelAndIconContainer>
)} )}
<StyledValueContainer> <StyledValueContainer ref={reference}>
{!readonly && isInlineCellInEditMode ? ( {!readonly && isInlineCellInEditMode ? (
<RecordInlineCellEditMode>{editModeContent}</RecordInlineCellEditMode> <RecordInlineCellEditMode>{editModeContent}</RecordInlineCellEditMode>
) : editModeContentOnly ? ( ) : editModeContentOnly ? (
@ -208,18 +220,9 @@ export const RecordInlineCellContainer = ({
isHovered={isHovered} isHovered={isHovered}
emptyPlaceholder={showLabel ? 'Empty' : label} emptyPlaceholder={showLabel ? 'Empty' : label}
> >
{displayModeContent} {newDisplayModeContent}
</RecordInlineCellDisplayMode> </RecordInlineCellDisplayMode>
{showEditButton && ( {showEditButton && <RecordInlineCellButton Icon={buttonIcon} />}
<StyledEditButtonContainer
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.1 }}
whileHover={{ scale: 1.04 }}
>
<RecordInlineCellButton Icon={buttonIcon} />
</StyledEditButtonContainer>
)}
</StyledClickableContainer> </StyledClickableContainer>
)} )}
</StyledValueContainer> </StyledValueContainer>

View File

@ -1,13 +1,23 @@
import styled from '@emotion/styled';
import { IconComponent } from 'twenty-ui'; import { IconComponent } from 'twenty-ui';
import { AnimatedContainer } from '@/object-record/record-table/components/AnimatedContainer';
import { FloatingIconButton } from '@/ui/input/button/components/FloatingIconButton'; import { FloatingIconButton } from '@/ui/input/button/components/FloatingIconButton';
const StyledInlineCellButtonContainer = styled.div`
align-items: center;
display: flex;
`;
export const RecordInlineCellButton = ({ Icon }: { Icon: IconComponent }) => { export const RecordInlineCellButton = ({ Icon }: { Icon: IconComponent }) => {
return ( return (
<FloatingIconButton <AnimatedContainer>
size="small" <StyledInlineCellButtonContainer>
Icon={Icon} <FloatingIconButton
data-testid="inline-cell-edit-mode-container" size="small"
/> Icon={Icon}
data-testid="inline-cell-edit-mode-container"
/>
</StyledInlineCellButtonContainer>
</AnimatedContainer>
); );
}; };

View File

@ -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;
}) => (
<StyledAnimatedChipContainer
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.1 }}
whileHover={{ scale: 1.04 }}
>
{children}
</StyledAnimatedChipContainer>
);

View File

@ -102,7 +102,7 @@ export const RecordTableCell = ({
isReadOnly={isReadOnly} isReadOnly={isReadOnly}
/> />
} }
nonEditModeContent={<FieldDisplay />} nonEditModeContent={<FieldDisplay fromTableCell />}
/> />
); );
}; };

View File

@ -1,12 +1,11 @@
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { motion } from 'framer-motion';
import { IconComponent } from 'twenty-ui'; import { IconComponent } from 'twenty-ui';
import { AnimatedContainer } from '@/object-record/record-table/components/AnimatedContainer';
import { FloatingIconButton } from '@/ui/input/button/components/FloatingIconButton'; import { FloatingIconButton } from '@/ui/input/button/components/FloatingIconButton';
const StyledEditButtonContainer = styled(motion.div)` const StyledButtonContainer = styled.div`
position: absolute; margin: ${({ theme }) => theme.spacing(1)};
right: 5px;
`; `;
type RecordTableCellButtonProps = { type RecordTableCellButtonProps = {
@ -18,12 +17,9 @@ export const RecordTableCellButton = ({
onClick, onClick,
Icon, Icon,
}: RecordTableCellButtonProps) => ( }: RecordTableCellButtonProps) => (
<StyledEditButtonContainer <AnimatedContainer>
initial={{ opacity: 0 }} <StyledButtonContainer>
animate={{ opacity: 1 }} <FloatingIconButton size="small" onClick={onClick} Icon={Icon} />
transition={{ duration: 0.1 }} </StyledButtonContainer>
whileHover={{ scale: 1.04 }} </AnimatedContainer>
>
<FloatingIconButton size="small" onClick={onClick} Icon={Icon} />
</StyledEditButtonContainer>
); );

View File

@ -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 styled from '@emotion/styled';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import { IconArrowUpRight } from 'twenty-ui'; 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 { RecordTableScopeInternalContext } from '@/object-record/record-table/scopes/scope-internal-context/RecordTableScopeInternalContext';
import { isSoftFocusOnTableCellComponentFamilyState } from '@/object-record/record-table/states/isSoftFocusOnTableCellComponentFamilyState'; import { isSoftFocusOnTableCellComponentFamilyState } from '@/object-record/record-table/states/isSoftFocusOnTableCellComponentFamilyState';
import { isTableCellInEditModeComponentFamilyState } from '@/object-record/record-table/states/isTableCellInEditModeComponentFamilyState'; 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 { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId'; import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId';
import { getScopeIdOrUndefinedFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdOrUndefinedFromComponentId'; 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')}; z-index: ${({ isInEditMode }) => (isInEditMode ? '4 !important' : '3')};
`; `;
const StyledCellBaseContainer = styled.div` const StyledCellBaseContainer = styled.div<{ softFocus: boolean }>`
align-items: center; align-items: center;
box-sizing: border-box; box-sizing: border-box;
cursor: pointer; cursor: pointer;
@ -41,6 +48,12 @@ const StyledCellBaseContainer = styled.div`
height: 32px; height: 32px;
position: relative; position: relative;
user-select: none; 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 = { export type RecordTableCellContainerProps = {
@ -63,6 +76,10 @@ export const RecordTableCellContainer = ({
editHotkeyScope, editHotkeyScope,
}: RecordTableCellContainerProps) => { }: RecordTableCellContainerProps) => {
const { columnIndex } = useContext(RecordTableCellContext); const { columnIndex } = useContext(RecordTableCellContext);
const reference = useRef<HTMLTableCellElement>(null);
const [isHovered, setIsHovered] = useState(false);
const [newNonEditModeContent, setNewNonEditModeContent] =
useState<ReactElement>(nonEditModeContent);
const { isReadOnly, isSelected, recordId } = useContext( const { isReadOnly, isSelected, recordId } = useContext(
RecordTableRowContext, RecordTableRowContext,
); );
@ -71,8 +88,6 @@ export const RecordTableCellContainer = ({
const cellPosition = useCurrentTableCellPosition(); const cellPosition = useCurrentTableCellPosition();
const [isHovered, setIsHovered] = useState(false);
const { openTableCell } = useOpenRecordTableCellFromCell(); const { openTableCell } = useOpenRecordTableCellFromCell();
const tableScopeId = useAvailableScopeIdOrThrow( const tableScopeId = useAvailableScopeIdOrThrow(
@ -135,8 +150,20 @@ export const RecordTableCellContainer = ({
(!isFirstColumn || !isEmpty) && (!isFirstColumn || !isEmpty) &&
!isReadOnly; !isReadOnly;
useEffect(() => {
if (React.isValidElement<ExpandableListProps>(nonEditModeContent)) {
setNewNonEditModeContent(
React.cloneElement(nonEditModeContent, {
isHovered: showButton,
reference: reference.current || undefined,
}),
);
}
}, [nonEditModeContent, showButton, reference]);
return ( return (
<StyledTd <StyledTd
ref={reference}
isSelected={isSelected} isSelected={isSelected}
onContextMenu={handleContextMenu} onContextMenu={handleContextMenu}
isInEditMode={isCurrentTableCellInEditMode} isInEditMode={isCurrentTableCellInEditMode}
@ -147,34 +174,37 @@ export const RecordTableCellContainer = ({
<StyledCellBaseContainer <StyledCellBaseContainer
onMouseEnter={handleContainerMouseEnter} onMouseEnter={handleContainerMouseEnter}
onMouseLeave={handleContainerMouseLeave} onMouseLeave={handleContainerMouseLeave}
softFocus={hasSoftFocus}
> >
{isCurrentTableCellInEditMode ? ( {isCurrentTableCellInEditMode ? (
<RecordTableCellEditMode>{editModeContent}</RecordTableCellEditMode> <RecordTableCellEditMode>{editModeContent}</RecordTableCellEditMode>
) : hasSoftFocus ? ( ) : hasSoftFocus ? (
<> <>
<RecordTableCellSoftFocusMode>
{editModeContentOnly ? editModeContent : newNonEditModeContent}
</RecordTableCellSoftFocusMode>
{showButton && ( {showButton && (
<RecordTableCellButton <RecordTableCellButton
onClick={handleButtonClick} onClick={handleButtonClick}
Icon={buttonIcon} Icon={buttonIcon}
/> />
)} )}
<RecordTableCellSoftFocusMode>
{editModeContentOnly ? editModeContent : nonEditModeContent}
</RecordTableCellSoftFocusMode>
</> </>
) : ( ) : (
<> <>
{!isEmpty && (
<RecordTableCellDisplayMode>
{editModeContentOnly
? editModeContent
: newNonEditModeContent}
</RecordTableCellDisplayMode>
)}
{showButton && ( {showButton && (
<RecordTableCellButton <RecordTableCellButton
onClick={handleButtonClick} onClick={handleButtonClick}
Icon={buttonIcon} Icon={buttonIcon}
/> />
)} )}
{!isEmpty && (
<RecordTableCellDisplayMode>
{editModeContentOnly ? editModeContent : nonEditModeContent}
</RecordTableCellDisplayMode>
)}
</> </>
)} )}
</StyledCellBaseContainer> </StyledCellBaseContainer>

View File

@ -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) => (
<StyledEditButtonContainer
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.1 }}
whileHover={{ scale: 1.04 }}
>
<FloatingIconButton size="small" onClick={onClick} Icon={Icon} />
</StyledEditButtonContainer>
);

View File

@ -101,7 +101,6 @@ export const RecordTableCellSoftFocusMode = ({
return ( return (
<RecordTableCellDisplayContainer <RecordTableCellDisplayContainer
onClick={handleClick} onClick={handleClick}
softFocus
scrollRef={scrollRef} scrollRef={scrollRef}
> >
{children} {children}

View File

@ -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<HTMLElement>;
id: string;
}) => {
const [listItemsInView, setListItemsInView] = useState(new Set<number>());
const firstListItem = listItems[0];
const dropdownId = `expandable-list-dropdown-${id}`;
const containerRef = useRef<HTMLDivElement>(null);
const divRef = useRef<HTMLDivElement>(null);
return (
<StyledContainer ref={containerRef}>
{firstListItem}
{listItems.slice(1).map((listItem, index) => (
<React.Fragment key={index}>
<IntersectionObserverWrapper
set={setListItemsInView}
id={index}
rootRef={rootRef}
>
{listItem}
</IntersectionObserverWrapper>
{index === listItemsInView.size - 1 &&
listItems.length - listItemsInView.size - 1 !== 0 && (
<Dropdown
dropdownId={dropdownId}
dropdownHotkeyScope={{
scope: dropdownId,
}}
clickableComponent={
<Chip
label={`+${listItems.length - listItemsInView.size - 1}`}
variant={ChipVariant.Highlighted}
/>
}
dropdownComponents={
<>
{divRef.current &&
createPortal(
<StyledExpendableCell>
{listItems}
</StyledExpendableCell>,
divRef.current as HTMLElement,
)}
</>
}
/>
)}
</React.Fragment>
))}
<div
ref={divRef}
style={{
position: 'absolute',
top: '100%',
zIndex: 1,
boxSizing: 'border-box',
}}
></div>
</StyledContainer>
);
};

View File

@ -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<React.SetStateAction<Set<number>>>;
id: number;
rootRef?: React.RefObject<HTMLElement>;
children: React.ReactNode;
}) => {
const { ref, inView } = useInView({
threshold: 1,
onChange: (inView) => {
if (inView) {
set((prev: Set<number>) => {
const newSet = new Set(prev);
newSet.add(id);
return newSet;
});
}
if (!inView) {
set((prev: Set<number>) => {
const newSet = new Set(prev);
newSet.delete(id);
return newSet;
});
}
},
root: rootRef?.current,
rootMargin: '0px 0px -50px 0px',
});
return (
<StyledDiv ref={ref} inView={inView}>
{children}
</StyledDiv>
);
};

View File

@ -3,7 +3,7 @@ import { css, useTheme } from '@emotion/react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { IconInfoCircle } from 'twenty-ui'; 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 InfoAccent = 'blue' | 'danger';
export type InfoProps = { export type InfoProps = {

View File

@ -1,7 +1,8 @@
import React from 'react'; import React from 'react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { Radio } from '@/ui/input/components/Radio.tsx'; import { Radio } from '@/ui/input/components/Radio';
const StyledSubscriptionCardContainer = styled.button` const StyledSubscriptionCardContainer = styled.button`
background-color: ${({ theme }) => theme.background.secondary}; background-color: ${({ theme }) => theme.background.secondary};
border: 1px solid ${({ theme }) => theme.border.color.medium}; border: 1px solid ${({ theme }) => theme.border.color.medium};

View File

@ -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<number, ChildrenProperty>;
setChildrenWidths: Dispatch<SetStateAction<Record<number, number>>>;
isFocusedMode: boolean;
}) => {
return (
<StyledChildrenContainer>
{children.map((child, index) => {
return (
<StyledChildContainer
ref={(el) => {
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}
</StyledChildContainer>
);
})}
</StyledChildrenContainer>
);
};

View File

@ -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<Record<number, number>>(
{},
);
// 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 (
<StyledContainer
ref={(el) => {
if (!el) return;
setContainerWidth(el.getBoundingClientRect().width);
}}
>
<ChildrenContainer
childrenProperties={childrenProperties}
setChildrenWidths={setChildrenWidths}
isFocusedMode={isFocusedMode}
>
{children}
</ChildrenContainer>
{displayHiddenCountChip && (
<AnimatedContainer>
<Chip
label={`+${hiddenChildrenCount}`}
variant={ChipVariant.Highlighted}
onClick={openDropdownMenu}
/>
</AnimatedContainer>
)}
{isDropdownMenuOpen && (
<DropdownMenu
ref={refs.setFloating}
style={floatingStyles}
width={
reference
? Math.max(220, reference.getBoundingClientRect().width)
: undefined
}
>
<StyledRelationsListContainer withDropDownBorder={withDropDownBorder}>
{children}
</StyledRelationsListContainer>
</DropdownMenu>
)}
</StyledContainer>
);
};

View File

@ -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<HTMLDivElement>(null);
return (
<StyledContainer
ref={reference}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
<ExpandableList
reference={reference.current || undefined}
forceDisplayHiddenCount={args.forceDisplayHiddenCount}
withDropDownBorder={args.withDropDownBorder}
isHovered={isHovered}
>
{args.children}
</ExpandableList>
</StyledContainer>
);
};
const meta: Meta<typeof ExpandableList> = {
title: 'UI/Layout/ExpandableList/ExpandableList',
component: ExpandableList,
decorators: [ComponentDecorator],
args: {
children: [
<Tag key={1} text={'Option 1'} color={MAIN_COLOR_NAMES[0]} />,
<Tag key={2} text={'Option 2'} color={MAIN_COLOR_NAMES[1]} />,
<Tag key={3} text={'Option 3'} color={MAIN_COLOR_NAMES[2]} />,
<Tag key={4} text={'Option 4'} color={MAIN_COLOR_NAMES[3]} />,
<Tag key={5} text={'Option 5'} color={MAIN_COLOR_NAMES[4]} />,
<Tag key={6} text={'Option 6'} color={MAIN_COLOR_NAMES[5]} />,
<Tag key={7} text={'Option 7'} color={MAIN_COLOR_NAMES[6]} />,
],
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<typeof ExpandableList>;
export const Default: Story = {};

View File

@ -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 },
});
});
});

View File

@ -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);
});
});

View File

@ -0,0 +1,30 @@
import {
ChildrenProperty,
GAP_WIDTH,
} from '@/ui/layout/expandable-list/components/ExpandableList';
export const getChildrenProperties = (
isFocusedMode: boolean,
availableWidth: number,
childrenWidths: Record<number, number>,
) => {
if (!isFocusedMode) {
return {};
}
let cumulatedChildrenWidth = 0;
const result: Record<number, ChildrenProperty> = {};
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;
};

View File

@ -0,0 +1,6 @@
export const getChipContentWidth = (numberOfChildren: number) => {
if (numberOfChildren <= 1) {
return 0;
}
return 17 + 8 * Math.trunc(Math.log10(numberOfChildren));
};

View File

@ -6,6 +6,7 @@ import { useRecoilCallback, useRecoilState, useRecoilValue } from 'recoil';
import { Key } from 'ts-key-enum'; import { Key } from 'ts-key-enum';
import { RIGHT_DRAWER_CLICK_OUTSIDE_LISTENER_ID } from '@/ui/layout/right-drawer/constants/RightDrawerClickOutsideListener'; 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 { rightDrawerCloseEventState } from '@/ui/layout/right-drawer/states/rightDrawerCloseEventsState';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { useClickOutsideListener } from '@/ui/utilities/pointer-event/hooks/useClickOutsideListener'; import { useClickOutsideListener } from '@/ui/utilities/pointer-event/hooks/useClickOutsideListener';
@ -46,6 +47,9 @@ export const RightDrawer = () => {
); );
const isRightDrawerExpanded = useRecoilValue(isRightDrawerExpandedState); const isRightDrawerExpanded = useRecoilValue(isRightDrawerExpandedState);
const [, setIsRightDrawerAnimationCompleted] = useRecoilState(
isRightDrawerAnimationCompletedState,
);
const rightDrawerPage = useRecoilValue(rightDrawerPageState); const rightDrawerPage = useRecoilValue(rightDrawerPageState);
@ -112,6 +116,9 @@ export const RightDrawer = () => {
x: '100%', x: '100%',
}, },
}; };
const handleAnimationComplete = () => {
setIsRightDrawerAnimationCompleted(isRightDrawerOpen);
};
return ( return (
<StyledContainer <StyledContainer
@ -121,6 +128,7 @@ export const RightDrawer = () => {
transition={{ transition={{
duration: theme.animation.duration.normal, duration: theme.animation.duration.normal,
}} }}
onAnimationComplete={handleAnimationComplete}
> >
<StyledRightDrawer ref={rightDrawerRef}> <StyledRightDrawer ref={rightDrawerRef}>
{isRightDrawerOpen && <RightDrawerRouter />} {isRightDrawerOpen && <RightDrawerRouter />}

View File

@ -0,0 +1,6 @@
import { createState } from 'twenty-ui';
export const isRightDrawerAnimationCompletedState = createState<boolean>({
key: 'isRightDrawerAnimationCompletedState',
defaultValue: false,
});

View File

@ -1,7 +1,7 @@
import { useTheme } from '@emotion/react'; import { useTheme } from '@emotion/react';
import { IconBrandGithub } from 'twenty-ui'; 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 packageJson from '../../../../../../package.json';
import { GITHUB_LINK } from '../constants/GithubLink'; import { GITHUB_LINK } from '../constants/GithubLink';

View File

@ -1,7 +1,7 @@
import { Meta, StoryObj } from '@storybook/react'; import { Meta, StoryObj } from '@storybook/react';
import { ComponentDecorator } from 'twenty-ui'; 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<typeof ActionLink> = { const meta: Meta<typeof ActionLink> = {
title: 'UI/navigation/link/ActionLink', title: 'UI/navigation/link/ActionLink',

View File

@ -3,21 +3,21 @@ import styled from '@emotion/styled';
import { isNonEmptyString, isNumber } from '@sniptt/guards'; import { isNonEmptyString, isNumber } from '@sniptt/guards';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import { SubTitle } from '@/auth/components/SubTitle.tsx'; import { SubTitle } from '@/auth/components/SubTitle';
import { Title } from '@/auth/components/Title.tsx'; import { Title } from '@/auth/components/Title';
import { SubscriptionBenefit } from '@/billing/components/SubscriptionBenefit.tsx'; import { SubscriptionBenefit } from '@/billing/components/SubscriptionBenefit';
import { SubscriptionCard } from '@/billing/components/SubscriptionCard.tsx'; import { SubscriptionCard } from '@/billing/components/SubscriptionCard';
import { billingState } from '@/client-config/states/billingState'; import { billingState } from '@/client-config/states/billingState';
import { AppPath } from '@/types/AppPath'; 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 { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { MainButton } from '@/ui/input/button/components/MainButton.tsx'; import { MainButton } from '@/ui/input/button/components/MainButton';
import { CardPicker } from '@/ui/input/components/CardPicker.tsx'; import { CardPicker } from '@/ui/input/components/CardPicker';
import { import {
ProductPriceEntity, ProductPriceEntity,
useCheckoutSessionMutation, useCheckoutSessionMutation,
useGetProductPricesQuery, useGetProductPricesQuery,
} from '~/generated/graphql.tsx'; } from '~/generated/graphql';
import { isDefined } from '~/utils/isDefined'; import { isDefined } from '~/utils/isDefined';
const StyledChoosePlanContainer = styled.div` const StyledChoosePlanContainer = styled.div`

View File

@ -17,7 +17,7 @@ import { useApolloMetadataClient } from '@/object-metadata/hooks/useApolloMetada
import { WorkspaceLogoUploader } from '@/settings/workspace/components/WorkspaceLogoUploader'; import { WorkspaceLogoUploader } from '@/settings/workspace/components/WorkspaceLogoUploader';
import { AppPath } from '@/types/AppPath'; import { AppPath } from '@/types/AppPath';
import { H2Title } from '@/ui/display/typography/components/H2Title'; 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 { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { MainButton } from '@/ui/input/button/components/MainButton'; import { MainButton } from '@/ui/input/button/components/MainButton';
import { TextInputV2 } from '@/ui/input/components/TextInputV2'; import { TextInputV2 } from '@/ui/input/components/TextInputV2';

View File

@ -4,12 +4,12 @@ import { useTheme } from '@emotion/react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { IconCheck } from 'twenty-ui'; import { IconCheck } from 'twenty-ui';
import { SubTitle } from '@/auth/components/SubTitle.tsx'; import { SubTitle } from '@/auth/components/SubTitle';
import { Title } from '@/auth/components/Title.tsx'; import { Title } from '@/auth/components/Title';
import { AppPath } from '@/types/AppPath'; 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 { 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` const StyledCheckContainer = styled.div`
align-items: center; align-items: center;

View File

@ -9,7 +9,7 @@
"esModuleInterop": true, "esModuleInterop": true,
"moduleResolution": "bundler", "moduleResolution": "bundler",
"allowSyntheticDefaultImports": true, "allowSyntheticDefaultImports": true,
"allowImportingTsExtensions": true, "allowImportingTsExtensions": false,
"resolveJsonModule": true, "resolveJsonModule": true,
"isolatedModules": true, "isolatedModules": true,
"noEmit": true, "noEmit": true,

View File

@ -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'; 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/FloatingIconButton'
export * from './src/modules/ui/input/button/components/FloatingIconButtonGroup' export * from './src/modules/ui/input/button/components/FloatingIconButtonGroup'
export * from './src/modules/ui/input/button/components/LightButton' 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/LightIconButton'
export * from './src/modules/ui/input/button/components/MainButton' export * from './src/modules/ui/input/button/components/MainButton'
export * from './src/modules/ui/input/button/components/RoundedIconButton' export * from './src/modules/ui/input/button/components/RoundedIconButton'

View File

@ -51,6 +51,8 @@ const StyledContainer = styled.div<
cursor: ${({ clickable, disabled }) => cursor: ${({ clickable, disabled }) =>
clickable ? 'pointer' : disabled ? 'not-allowed' : 'inherit'}; clickable ? 'pointer' : disabled ? 'not-allowed' : 'inherit'};
display: inline-flex; display: inline-flex;
flex-direction: row-reverse;
justify-content: center;
gap: ${({ theme }) => theme.spacing(1)}; gap: ${({ theme }) => theme.spacing(1)};
height: ${({ theme }) => theme.spacing(3)}; height: ${({ theme }) => theme.spacing(3)};
max-width: ${({ maxWidth }) => max-width: ${({ maxWidth }) =>