diff --git a/packages/twenty-front/src/modules/activities/files/components/AttachmentRow.tsx b/packages/twenty-front/src/modules/activities/files/components/AttachmentRow.tsx index d1ba431d5..e9de906b3 100644 --- a/packages/twenty-front/src/modules/activities/files/components/AttachmentRow.tsx +++ b/packages/twenty-front/src/modules/activities/files/components/AttachmentRow.tsx @@ -18,7 +18,7 @@ import { isDefined } from 'twenty-shared/utils'; import { PREVIEWABLE_EXTENSIONS } from '@/activities/files/const/previewable-extensions.const'; import { IconCalendar, OverflowingTextWithTooltip } from 'twenty-ui/display'; -import { isModifiedEvent } from 'twenty-ui/utilities'; +import { isNavigationModifierPressed } from 'twenty-ui/utilities'; import { formatToHumanReadableDate } from '~/utils/date-utils'; import { getFileNameAndExtension } from '~/utils/file/getFileNameAndExtension'; @@ -141,7 +141,7 @@ export const AttachmentRow = ({ const handleOpenDocument = (e: React.MouseEvent) => { // Cmd/Ctrl+click opens new tab, right click opens context menu - if (isModifiedEvent(e) || e.button === 2) { + if (isNavigationModifierPressed(e) === true) { return; } diff --git a/packages/twenty-front/src/modules/command-menu/components/CommandMenuOpenContainer.tsx b/packages/twenty-front/src/modules/command-menu/components/CommandMenuOpenContainer.tsx index 073f87c71..89edd727c 100644 --- a/packages/twenty-front/src/modules/command-menu/components/CommandMenuOpenContainer.tsx +++ b/packages/twenty-front/src/modules/command-menu/components/CommandMenuOpenContainer.tsx @@ -13,6 +13,7 @@ import styled from '@emotion/styled'; import { motion } from 'framer-motion'; import { useRef } from 'react'; import { useRecoilCallback } from 'recoil'; +import { LINK_CHIP_CLICK_OUTSIDE_ID } from 'twenty-ui/components'; import { useIsMobile } from 'twenty-ui/utilities'; const StyledCommandMenu = styled(motion.div)` @@ -64,7 +65,10 @@ export const CommandMenuOpenContainer = ({ refs: [commandMenuRef], callback: handleClickOutside, listenerId: 'COMMAND_MENU_LISTENER_ID', - excludedClickOutsideIds: [PAGE_HEADER_COMMAND_MENU_BUTTON_CLICK_OUTSIDE_ID], + excludedClickOutsideIds: [ + PAGE_HEADER_COMMAND_MENU_BUTTON_CLICK_OUTSIDE_ID, + LINK_CHIP_CLICK_OUTSIDE_ID, + ], }); return ( diff --git a/packages/twenty-front/src/modules/error-handler/components/SentryInitEffect.tsx b/packages/twenty-front/src/modules/error-handler/components/SentryInitEffect.tsx index 9c895bab2..c5ee54a1d 100644 --- a/packages/twenty-front/src/modules/error-handler/components/SentryInitEffect.tsx +++ b/packages/twenty-front/src/modules/error-handler/components/SentryInitEffect.tsx @@ -23,9 +23,15 @@ export const SentryInitEffect = () => { const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState); const [isSentryInitialized, setIsSentryInitialized] = useState(false); + const [isSentryInitializing, setIsSentryInitializing] = useState(false); useEffect(() => { - if (isNonEmptyString(sentryConfig?.dsn) && !isSentryInitialized) { + if ( + !isSentryInitializing && + isNonEmptyString(sentryConfig?.dsn) && + !isSentryInitialized + ) { + setIsSentryInitializing(true); init({ environment: sentryConfig?.environment ?? undefined, release: sentryConfig?.release ?? undefined, @@ -38,6 +44,7 @@ export const SentryInitEffect = () => { }); setIsSentryInitialized(true); + setIsSentryInitializing(false); } if (isDefined(currentUser)) { @@ -53,6 +60,7 @@ export const SentryInitEffect = () => { }, [ sentryConfig, isSentryInitialized, + isSentryInitializing, currentUser, currentWorkspace, currentWorkspaceMember, diff --git a/packages/twenty-front/src/modules/favorites/components/CurrentWorkspaceMemberFavorites.tsx b/packages/twenty-front/src/modules/favorites/components/CurrentWorkspaceMemberFavorites.tsx index 273d23141..88cf7b46e 100644 --- a/packages/twenty-front/src/modules/favorites/components/CurrentWorkspaceMemberFavorites.tsx +++ b/packages/twenty-front/src/modules/favorites/components/CurrentWorkspaceMemberFavorites.tsx @@ -160,6 +160,7 @@ export const CurrentWorkspaceMemberFavorites = ({ rightOptions={rightOptions} className="navigation-drawer-item" isRightOptionsDropdownOpen={isFavoriteFolderEditDropdownOpen} + triggerEvent="CLICK" /> )} @@ -190,7 +191,7 @@ export const CurrentWorkspaceMemberFavorites = ({ label={favorite.labelIdentifier} objectName={favorite.objectNameSingular} Icon={() => } - to={favorite.link} + to={isDragging ? undefined : favorite.link} active={index === selectedFavoriteIndex} subItemState={getNavigationSubItemLeftAdornment({ index, @@ -205,6 +206,7 @@ export const CurrentWorkspaceMemberFavorites = ({ /> } isDragging={isDragging} + triggerEvent="CLICK" /> } /> diff --git a/packages/twenty-front/src/modules/favorites/components/CurrentWorkspaceMemberOrphanFavorites.tsx b/packages/twenty-front/src/modules/favorites/components/CurrentWorkspaceMemberOrphanFavorites.tsx index 3205b066c..b2bc671c7 100644 --- a/packages/twenty-front/src/modules/favorites/components/CurrentWorkspaceMemberOrphanFavorites.tsx +++ b/packages/twenty-front/src/modules/favorites/components/CurrentWorkspaceMemberOrphanFavorites.tsx @@ -50,7 +50,7 @@ export const CurrentWorkspaceMemberOrphanFavorites = () => { currentViewPath, favorite, )} - to={favorite.link} + to={isDragging ? undefined : favorite.link} rightOptions={ { } objectName={favorite.objectNameSingular} isDragging={isDragging} + triggerEvent="CLICK" /> } diff --git a/packages/twenty-front/src/modules/navigation/components/MainNavigationDrawerFixedItems.tsx b/packages/twenty-front/src/modules/navigation/components/MainNavigationDrawerFixedItems.tsx index 56a976f5f..b588f0287 100644 --- a/packages/twenty-front/src/modules/navigation/components/MainNavigationDrawerFixedItems.tsx +++ b/packages/twenty-front/src/modules/navigation/components/MainNavigationDrawerFixedItems.tsx @@ -5,7 +5,7 @@ import { isNavigationDrawerExpandedState } from '@/ui/navigation/states/isNaviga import { navigationDrawerExpandedMemorizedState } from '@/ui/navigation/states/navigationDrawerExpandedMemorizedState'; import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState'; import { useLingui } from '@lingui/react/macro'; -import { useLocation } from 'react-router-dom'; +import { useLocation, useNavigate } from 'react-router-dom'; import { useRecoilState, useSetRecoilState } from 'recoil'; import { IconSearch, IconSettings } from 'twenty-ui/display'; import { useIsMobile } from 'twenty-ui/utilities'; @@ -24,6 +24,8 @@ export const MainNavigationDrawerFixedItems = () => { navigationDrawerExpandedMemorizedState, ); + const navigate = useNavigate(); + const { t } = useLingui(); const { openRecordsSearchPage } = useOpenRecordsSearchPageInCommandMenu(); @@ -43,6 +45,7 @@ export const MainNavigationDrawerFixedItems = () => { setNavigationDrawerExpandedMemorized(isNavigationDrawerExpanded); setIsNavigationDrawerExpanded(true); setNavigationMemorizedUrl(location.pathname + location.search); + navigate(getSettingsPath(SettingsPath.ProfilePage)); }} Icon={IconSettings} /> diff --git a/packages/twenty-front/src/modules/object-record/components/RecordChip.tsx b/packages/twenty-front/src/modules/object-record/components/RecordChip.tsx index 3090dfaa4..9509e9ccb 100644 --- a/packages/twenty-front/src/modules/object-record/components/RecordChip.tsx +++ b/packages/twenty-front/src/modules/object-record/components/RecordChip.tsx @@ -5,6 +5,7 @@ import { useRecordChipData } from '@/object-record/hooks/useRecordChipData'; import { recordIndexOpenRecordInState } from '@/object-record/record-index/states/recordIndexOpenRecordInState'; import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { ViewOpenRecordInType } from '@/views/types/ViewOpenRecordInType'; +import { MouseEvent } from 'react'; import { useRecoilValue } from 'recoil'; import { AvatarChip, @@ -13,7 +14,7 @@ import { ChipVariant, LinkAvatarChip, } from 'twenty-ui/components'; -import { isModifiedEvent } from 'twenty-ui/utilities'; +import { TriggerEventType } from 'twenty-ui/utilities'; export type RecordChipProps = { objectNameSingular: string; @@ -25,6 +26,7 @@ export type RecordChipProps = { to?: string | undefined; size?: ChipSize; isLabelHidden?: boolean; + triggerEvent?: TriggerEventType; }; export const RecordChip = ({ @@ -37,6 +39,7 @@ export const RecordChip = ({ size, forceDisableClick = false, isLabelHidden = false, + triggerEvent = 'MOUSE_DOWN', }: RecordChipProps) => { const { recordChipData } = useRecordChipData({ objectNameSingular, @@ -47,6 +50,18 @@ export const RecordChip = ({ const recordIndexOpenRecordIn = useRecoilValue(recordIndexOpenRecordInState); + const isSidePanelViewOpenRecordInType = + recordIndexOpenRecordIn === ViewOpenRecordInType.SIDE_PANEL; + + const handleCustomClick = isSidePanelViewOpenRecordInType + ? (_event: MouseEvent) => { + openRecordInCommandMenu({ + recordId: record.id, + objectNameSingular, + }); + } + : undefined; + // TODO temporary until we create a record show page for Workspaces members if ( @@ -67,17 +82,6 @@ export const RecordChip = ({ ); } - const isSidePanelViewOpenRecordInType = - recordIndexOpenRecordIn === ViewOpenRecordInType.SIDE_PANEL; - - const onClick = isSidePanelViewOpenRecordInType - ? () => - openRecordInCommandMenu({ - recordId: record.id, - objectNameSingular, - }) - : undefined; - return ( { - // TODO refactor wrapper event listener to avoid colliding events - clickEvent.stopPropagation(); - - const isModifiedEventResult = isModifiedEvent(clickEvent); - if (isSidePanelViewOpenRecordInType && !isModifiedEventResult) { - clickEvent.preventDefault(); - onClick?.(); - } - }} + onClick={handleCustomClick} + triggerEvent={triggerEvent} /> ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoard.tsx b/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoard.tsx index cbc4446ed..ac189d251 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoard.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoard.tsx @@ -40,6 +40,7 @@ import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component- import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue'; import { ViewType } from '@/views/types/ViewType'; +import { LINK_CHIP_CLICK_OUTSIDE_ID } from 'twenty-ui/components'; import { getIndexNeighboursElementsFromArray } from '~/utils/array/getIndexNeighboursElementsFromArray'; const StyledContainer = styled.div` @@ -121,6 +122,7 @@ export const RecordBoard = () => { MODAL_BACKDROP_CLICK_OUTSIDE_ID, PAGE_ACTION_CONTAINER_CLICK_OUTSIDE_ID, RECORD_BOARD_CARD_CLICK_OUTSIDE_ID, + LINK_CHIP_CLICK_OUTSIDE_ID, ], listenerId: RECORD_BOARD_CLICK_OUTSIDE_LISTENER_ID, refs: [], diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-card/components/RecordBoardCardBody.tsx b/packages/twenty-front/src/modules/object-record/record-board/record-board-card/components/RecordBoardCardBody.tsx index b951bbd83..8c477eeeb 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-card/components/RecordBoardCardBody.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-card/components/RecordBoardCardBody.tsx @@ -68,6 +68,7 @@ export const RecordBoardCardBody = ({ }, useUpdateRecord: useUpdateOneRecordHook, isDisplayModeFixHeight: true, + triggerEvent: 'CLICK', }} > )} diff --git a/packages/twenty-front/src/modules/object-record/record-field/contexts/FieldContext.ts b/packages/twenty-front/src/modules/object-record/record-field/contexts/FieldContext.ts index 4ba05777a..e80712687 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/contexts/FieldContext.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/contexts/FieldContext.ts @@ -1,5 +1,6 @@ import { createContext } from 'react'; +import { TriggerEventType } from 'twenty-ui/utilities'; import { FieldDefinition } from '../types/FieldDefinition'; import { FieldMetadata } from '../types/FieldMetadata'; @@ -36,6 +37,7 @@ export type GenericFieldContextType = { disableChipClick?: boolean; onOpenEditMode?: () => void; onCloseEditMode?: () => void; + triggerEvent?: TriggerEventType; }; export const FieldContext = createContext( diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/ChipFieldDisplay.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/ChipFieldDisplay.tsx index d8981b3e2..84b4d0db4 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/ChipFieldDisplay.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/ChipFieldDisplay.tsx @@ -11,6 +11,7 @@ export const ChipFieldDisplay = () => { isLabelIdentifierCompact, disableChipClick, maxWidth, + triggerEvent, } = useChipFieldDisplay(); if (!isDefined(recordValue)) { @@ -26,6 +27,7 @@ export const ChipFieldDisplay = () => { to={labelIdentifierLink} isLabelHidden={isLabelIdentifierCompact} forceDisableClick={disableChipClick} + triggerEvent={triggerEvent} /> ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/RelationFromManyFieldDisplay.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/RelationFromManyFieldDisplay.tsx index eec4a25ca..0eebdfff4 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/RelationFromManyFieldDisplay.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/RelationFromManyFieldDisplay.tsx @@ -15,7 +15,7 @@ export const RelationFromManyFieldDisplay = () => { const { fieldValue, fieldDefinition, generateRecordChipData } = useRelationFromManyFieldDisplay(); const { isFocused } = useFieldFocus(); - const { disableChipClick } = useContext(FieldContext); + const { disableChipClick, triggerEvent } = useContext(FieldContext); const { fieldName, objectMetadataNameSingular } = fieldDefinition.metadata; @@ -94,6 +94,7 @@ export const RelationFromManyFieldDisplay = () => { objectNameSingular={recordChipData.objectNameSingular} record={record} forceDisableClick={disableChipClick} + triggerEvent={triggerEvent} /> ); })} diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/RelationToOneFieldDisplay.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/RelationToOneFieldDisplay.tsx index 9ba8033ac..3b039f863 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/RelationToOneFieldDisplay.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/RelationToOneFieldDisplay.tsx @@ -9,7 +9,7 @@ export const RelationToOneFieldDisplay = () => { const { fieldValue, fieldDefinition, generateRecordChipData } = useRelationToOneFieldDisplay(); - const { disableChipClick } = useContext(FieldContext); + const { disableChipClick, triggerEvent } = useContext(FieldContext); if ( !isDefined(fieldValue) || @@ -31,6 +31,7 @@ export const RelationToOneFieldDisplay = () => { forceDisableClick={ isWorkspaceMemberFieldMetadataRelation || disableChipClick } + triggerEvent={triggerEvent} /> ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useChipFieldDisplay.ts b/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useChipFieldDisplay.ts index d6a8df62b..5ff0b7149 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useChipFieldDisplay.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useChipFieldDisplay.ts @@ -20,6 +20,7 @@ export const useChipFieldDisplay = () => { isLabelIdentifierCompact, disableChipClick, maxWidth, + triggerEvent, } = useContext(FieldContext); const { chipGeneratorPerObjectPerField } = useContext( @@ -52,5 +53,6 @@ export const useChipFieldDisplay = () => { isLabelIdentifierCompact, disableChipClick, maxWidth, + triggerEvent, }; }; diff --git a/packages/twenty-front/src/modules/ui/layout/tab/components/Tab.tsx b/packages/twenty-front/src/modules/ui/layout/tab/components/Tab.tsx index 3cdece3f2..575114275 100644 --- a/packages/twenty-front/src/modules/ui/layout/tab/components/Tab.tsx +++ b/packages/twenty-front/src/modules/ui/layout/tab/components/Tab.tsx @@ -6,6 +6,7 @@ import { ReactElement } from 'react'; import { Link } from 'react-router-dom'; import { Pill } from 'twenty-ui/components'; import { Avatar, IconComponent } from 'twenty-ui/display'; +import { useMouseDownNavigation } from 'twenty-ui/utilities'; type TabProps = { id: string; @@ -87,6 +88,13 @@ export const Tab = ({ logo, }: TabProps) => { const theme = useTheme(); + const { onClick: handleClick, onMouseDown: handleMouseDown } = + useMouseDownNavigation({ + to, + onClick, + disabled, + }); + const iconColor = active ? theme.font.color.primary : disabled @@ -95,7 +103,8 @@ export const Tab = ({ return ( { - if (!behaveAsLinks) { - setActiveTabId(tab.id); - } - }} + onClick={ + behaveAsLinks + ? undefined + : () => { + setActiveTabId(tab.id); + } + } /> ))} diff --git a/packages/twenty-front/src/modules/ui/layout/tab/components/__stories__/Tab.stories.tsx b/packages/twenty-front/src/modules/ui/layout/tab/components/__stories__/Tab.stories.tsx index a13bd91ef..dcad6f4e0 100644 --- a/packages/twenty-front/src/modules/ui/layout/tab/components/__stories__/Tab.stories.tsx +++ b/packages/twenty-front/src/modules/ui/layout/tab/components/__stories__/Tab.stories.tsx @@ -1,16 +1,17 @@ import { Meta, StoryObj } from '@storybook/react'; -import { Tab } from '../Tab'; +import { IconCheckbox } from 'twenty-ui/display'; import { CatalogDecorator, CatalogStory, - ComponentDecorator, + ComponentWithRouterDecorator, } from 'twenty-ui/testing'; -import { IconCheckbox } from 'twenty-ui/display'; +import { Tab } from '../Tab'; const meta: Meta = { title: 'UI/Layout/Tab/Tab', component: Tab, + decorators: [ComponentWithRouterDecorator], }; export default meta; @@ -23,8 +24,7 @@ export const Default: Story = { Icon: IconCheckbox, disabled: false, }, - - decorators: [ComponentDecorator], + decorators: [ComponentWithRouterDecorator], }; export const Catalog: CatalogStory = { diff --git a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerItem.tsx b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerItem.tsx index fe10b0194..0fb25c3a4 100644 --- a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerItem.tsx +++ b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerItem.tsx @@ -11,10 +11,11 @@ import styled from '@emotion/styled'; import { ReactNode } from 'react'; import { Link } from 'react-router-dom'; import { useRecoilState } from 'recoil'; -import { capitalize, isDefined } from 'twenty-shared/utils'; +import { capitalize } from 'twenty-shared/utils'; +import { Pill } from 'twenty-ui/components'; import { IconComponent, Label, TablerIconsProps } from 'twenty-ui/display'; import { MOBILE_VIEWPORT } from 'twenty-ui/theme'; -import { Pill } from 'twenty-ui/components'; +import { TriggerEventType, useMouseDownNavigation } from 'twenty-ui/utilities'; const DEFAULT_INDENTATION_LEVEL = 1; @@ -37,6 +38,7 @@ export type NavigationDrawerItemProps = { rightOptions?: ReactNode; isDragging?: boolean; isRightOptionsDropdownOpen?: boolean; + triggerEvent?: TriggerEventType; }; type StyledItemProps = Pick< @@ -250,6 +252,7 @@ export const NavigationDrawerItem = ({ rightOptions, isDragging, isRightOptionsDropdownOpen, + triggerEvent, }: NavigationDrawerItemProps) => { const theme = useTheme(); const isMobile = useIsMobile(); @@ -259,22 +262,26 @@ export const NavigationDrawerItem = ({ const showBreadcrumb = indentationLevel === 2; const showStyledSpacer = !!soon || !!count || !!keyboard || !!rightOptions; - const handleItemClick = () => { + const handleMobileNavigation = () => { if (isMobile) { setIsNavigationDrawerExpanded(false); } - - if (isDefined(onClick)) { - onClick(); - return; - } }; + const { onClick: handleClick, onMouseDown: handleMouseDown } = + useMouseDownNavigation({ + to, + onClick, + onBeforeNavigation: handleMobileNavigation, + triggerEvent, + }); + return ( { return ( ); }; diff --git a/packages/twenty-ui/src/components/avatar-chip/LinkAvatarChip.tsx b/packages/twenty-ui/src/components/avatar-chip/LinkAvatarChip.tsx index 8ffeeb113..1c8b0fa10 100644 --- a/packages/twenty-ui/src/components/avatar-chip/LinkAvatarChip.tsx +++ b/packages/twenty-ui/src/components/avatar-chip/LinkAvatarChip.tsx @@ -3,6 +3,7 @@ import { AvatarChipsCommonProps } from '@ui/components/avatar-chip/types/AvatarC import { AvatarChipVariant } from '@ui/components/avatar-chip/types/AvatarChipsVariant.type'; import { ChipVariant } from '@ui/components/chip/Chip'; import { LinkChip, LinkChipProps } from '@ui/components/chip/LinkChip'; +import { TriggerEventType } from '@ui/utilities'; export type LinkAvatarChipProps = Omit< AvatarChipsCommonProps, @@ -10,8 +11,10 @@ export type LinkAvatarChipProps = Omit< > & { to: string; onClick?: LinkChipProps['onClick']; + onMouseDown?: LinkChipProps['onMouseDown']; variant?: AvatarChipVariant; isLabelHidden?: boolean; + triggerEvent?: TriggerEventType; }; export const LinkAvatarChip = ({ @@ -29,6 +32,7 @@ export const LinkAvatarChip = ({ size, variant, isLabelHidden, + triggerEvent, }: LinkAvatarChipProps) => ( ); diff --git a/packages/twenty-ui/src/components/chip/LinkChip.tsx b/packages/twenty-ui/src/components/chip/LinkChip.tsx index af9c05411..70046fc77 100644 --- a/packages/twenty-ui/src/components/chip/LinkChip.tsx +++ b/packages/twenty-ui/src/components/chip/LinkChip.tsx @@ -6,6 +6,8 @@ import { ChipSize, ChipVariant, } from '@ui/components/chip/Chip'; +import { LINK_CHIP_CLICK_OUTSIDE_ID } from '@ui/components/chip/constants/LinkChipClickOutsideId'; +import { TriggerEventType, useMouseDownNavigation } from '@ui/utilities'; import { MouseEvent } from 'react'; import { Link } from 'react-router-dom'; @@ -14,7 +16,9 @@ export type LinkChipProps = Omit< 'onClick' | 'disabled' | 'clickable' > & { to: string; - onClick?: (event: MouseEvent) => void; + onClick?: (event: MouseEvent) => void; + onMouseDown?: (event: MouseEvent) => void; + triggerEvent?: TriggerEventType; }; const StyledLink = styled(Link)` @@ -34,9 +38,25 @@ export const LinkChip = ({ className, maxWidth, onClick, + triggerEvent, }: LinkChipProps) => { + const { onClick: onClickHandler, onMouseDown: onMouseDownHandler } = + useMouseDownNavigation({ + to: to, + onClick: onClick, + triggerEvent, + }); + return ( - + { + event.stopPropagation(); + onClickHandler(event); + }} + onMouseDown={onMouseDownHandler} + data-click-outside-id={LINK_CHIP_CLICK_OUTSIDE_ID} + > theme.font.color.tertiary}; `; -const StyledEmptyState = styled.div` - color: ${({ theme }) => theme.font.color.tertiary}; -`; - const StyledJsonList = styled(JsonList)``.withComponent(motion.ul); export const JsonNestedNode = ({ diff --git a/packages/twenty-ui/src/utilities/index.ts b/packages/twenty-ui/src/utilities/index.ts index 6562033b8..4ad512a6d 100644 --- a/packages/twenty-ui/src/utilities/index.ts +++ b/packages/twenty-ui/src/utilities/index.ts @@ -20,7 +20,9 @@ export { getOsControlSymbol } from './device/getOsControlSymbol'; export { getOsShortcutSeparator } from './device/getOsShortcutSeparator'; export { getUserDevice } from './device/getUserDevice'; export { AutogrowWrapper } from './dimensions/components/AutogrowWrapper'; -export { isModifiedEvent } from './events/isModifiedEvent'; +export { useMouseDownNavigation } from './navigation/hooks/useMouseDownNavigation'; +export { isNavigationModifierPressed } from './navigation/isNavigationModifierPressed'; +export type { TriggerEventType } from './navigation/types/trigger-event.type'; export { useIsMobile } from './responsive/hooks/useIsMobile'; export { useScreenSize } from './screen-size/hooks/useScreenSize'; export { createState } from './state/utils/createState'; diff --git a/packages/twenty-ui/src/utilities/navigation/hooks/useMouseDownNavigation.ts b/packages/twenty-ui/src/utilities/navigation/hooks/useMouseDownNavigation.ts new file mode 100644 index 000000000..2e8360515 --- /dev/null +++ b/packages/twenty-ui/src/utilities/navigation/hooks/useMouseDownNavigation.ts @@ -0,0 +1,71 @@ +import { isNavigationModifierPressed } from '@ui/utilities/navigation/isNavigationModifierPressed'; +import { TriggerEventType } from '@ui/utilities/navigation/types/trigger-event.type'; +import { MouseEvent } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { isDefined } from 'twenty-shared/utils'; + +type UseMouseDownNavigationProps = { + to?: string; + onClick?: (event: MouseEvent) => void; + disabled?: boolean; + onBeforeNavigation?: () => void; + triggerEvent?: TriggerEventType; + stopPropagation?: boolean; +}; + +export const useMouseDownNavigation = ({ + to, + onClick, + disabled = false, + onBeforeNavigation, + triggerEvent = 'MOUSE_DOWN', +}: UseMouseDownNavigationProps) => { + const navigate = useNavigate(); + + const handleClick = (event: MouseEvent) => { + if (disabled) return; + + // For modifier keys, let the default browser behavior handle it + if (isNavigationModifierPressed(event)) { + onBeforeNavigation?.(); + if (isDefined(onClick) && !isDefined(to)) { + onClick(event); + } + // Don't prevent default for modifier keys to allow browser navigation + return; + } + + if (triggerEvent === 'CLICK') { + onBeforeNavigation?.(); + if (isDefined(onClick)) { + onClick(event); + } else if (isDefined(to)) { + navigate(to); + } + } + + // For regular clicks, prevent default to avoid double navigation + event.preventDefault(); + }; + + const handleMouseDown = (event: MouseEvent) => { + if (disabled || triggerEvent === 'CLICK') return; + + if (isNavigationModifierPressed(event)) { + return; + } + + onBeforeNavigation?.(); + + if (isDefined(onClick)) { + onClick(event); + } else if (isDefined(to)) { + navigate(to); + } + }; + + return { + onClick: handleClick, + onMouseDown: handleMouseDown, + }; +}; diff --git a/packages/twenty-ui/src/utilities/events/isModifiedEvent.ts b/packages/twenty-ui/src/utilities/navigation/isNavigationModifierPressed.ts similarity index 88% rename from packages/twenty-ui/src/utilities/events/isModifiedEvent.ts rename to packages/twenty-ui/src/utilities/navigation/isNavigationModifierPressed.ts index 18600b84e..92398ceb4 100644 --- a/packages/twenty-ui/src/utilities/events/isModifiedEvent.ts +++ b/packages/twenty-ui/src/utilities/navigation/isNavigationModifierPressed.ts @@ -3,7 +3,7 @@ type LimitedMouseEvent = Pick< 'button' | 'metaKey' | 'altKey' | 'ctrlKey' | 'shiftKey' >; -export const isModifiedEvent = ({ +export const isNavigationModifierPressed = ({ altKey, ctrlKey, shiftKey, diff --git a/packages/twenty-ui/src/utilities/navigation/types/trigger-event.type.ts b/packages/twenty-ui/src/utilities/navigation/types/trigger-event.type.ts new file mode 100644 index 000000000..ea2294ba9 --- /dev/null +++ b/packages/twenty-ui/src/utilities/navigation/types/trigger-event.type.ts @@ -0,0 +1 @@ +export type TriggerEventType = 'MOUSE_DOWN' | 'CLICK';