Left menu and chip links (#12294)
Small optimization for faster loading (gaining ~80ms - average time of a click) It might seem a little over-engineered but there are a lot of edge cases and I couldn't find a simpler solution I also tried to tackle Link Chips but it's more complex so this will be for another PR
This commit is contained in:
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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 (
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -160,6 +160,7 @@ export const CurrentWorkspaceMemberFavorites = ({
|
||||
rightOptions={rightOptions}
|
||||
className="navigation-drawer-item"
|
||||
isRightOptionsDropdownOpen={isFavoriteFolderEditDropdownOpen}
|
||||
triggerEvent="CLICK"
|
||||
/>
|
||||
</FavoritesDroppable>
|
||||
)}
|
||||
@ -190,7 +191,7 @@ export const CurrentWorkspaceMemberFavorites = ({
|
||||
label={favorite.labelIdentifier}
|
||||
objectName={favorite.objectNameSingular}
|
||||
Icon={() => <FavoriteIcon favorite={favorite} />}
|
||||
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"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
@ -50,7 +50,7 @@ export const CurrentWorkspaceMemberOrphanFavorites = () => {
|
||||
currentViewPath,
|
||||
favorite,
|
||||
)}
|
||||
to={favorite.link}
|
||||
to={isDragging ? undefined : favorite.link}
|
||||
rightOptions={
|
||||
<LightIconButton
|
||||
Icon={IconHeartOff}
|
||||
@ -60,6 +60,7 @@ export const CurrentWorkspaceMemberOrphanFavorites = () => {
|
||||
}
|
||||
objectName={favorite.objectNameSingular}
|
||||
isDragging={isDragging}
|
||||
triggerEvent="CLICK"
|
||||
/>
|
||||
</StyledOrphanFavoritesContainer>
|
||||
}
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
|
||||
@ -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<HTMLElement>) => {
|
||||
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 (
|
||||
<LinkAvatarChip
|
||||
size={size}
|
||||
@ -95,16 +99,8 @@ export const RecordChip = ({
|
||||
: AvatarChipVariant.Transparent)
|
||||
}
|
||||
to={to ?? getLinkToShowPage(objectNameSingular, record)}
|
||||
onClick={(clickEvent) => {
|
||||
// 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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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: [],
|
||||
|
||||
@ -68,6 +68,7 @@ export const RecordBoardCardBody = ({
|
||||
},
|
||||
useUpdateRecord: useUpdateOneRecordHook,
|
||||
isDisplayModeFixHeight: true,
|
||||
triggerEvent: 'CLICK',
|
||||
}}
|
||||
>
|
||||
<RecordFieldComponentInstanceContext.Provider
|
||||
|
||||
@ -20,8 +20,8 @@ import { Dispatch, SetStateAction, useContext } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { AvatarChipVariant } from 'twenty-ui/components';
|
||||
import { Checkbox, CheckboxVariant, LightIconButton } from 'twenty-ui/input';
|
||||
import { IconEye, IconEyeOff } from 'twenty-ui/display';
|
||||
import { Checkbox, CheckboxVariant, LightIconButton } from 'twenty-ui/input';
|
||||
|
||||
const StyledCompactIconContainer = styled.div`
|
||||
align-items: center;
|
||||
@ -84,6 +84,7 @@ export const RecordBoardCardHeader = ({
|
||||
? indexIdentifierUrl(recordId)
|
||||
: undefined
|
||||
}
|
||||
triggerEvent="CLICK"
|
||||
/>
|
||||
)}
|
||||
</StopPropagationContainer>
|
||||
|
||||
@ -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<GenericFieldContextType>(
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
@ -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 (
|
||||
<StyledTab
|
||||
onClick={onClick}
|
||||
onClick={handleClick}
|
||||
onMouseDown={handleMouseDown}
|
||||
active={active}
|
||||
className={className}
|
||||
disabled={disabled}
|
||||
|
||||
@ -93,11 +93,13 @@ export const TabList = ({
|
||||
disabled={tab.disabled ?? loading}
|
||||
pill={tab.pill}
|
||||
to={behaveAsLinks ? `#${tab.id}` : undefined}
|
||||
onClick={() => {
|
||||
if (!behaveAsLinks) {
|
||||
setActiveTabId(tab.id);
|
||||
}
|
||||
}}
|
||||
onClick={
|
||||
behaveAsLinks
|
||||
? undefined
|
||||
: () => {
|
||||
setActiveTabId(tab.id);
|
||||
}
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</StyledContainer>
|
||||
|
||||
@ -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<typeof Tab> = {
|
||||
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<Story, typeof Tab> = {
|
||||
|
||||
@ -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 (
|
||||
<StyledNavigationDrawerItemContainer>
|
||||
<StyledItem
|
||||
className={`navigation-drawer-item ${className || ''}`}
|
||||
onClick={handleItemClick}
|
||||
onClick={handleClick}
|
||||
onMouseDown={handleMouseDown}
|
||||
active={active}
|
||||
aria-selected={active}
|
||||
danger={danger}
|
||||
|
||||
@ -20,6 +20,7 @@ export const NavigationDrawerSubItem = ({
|
||||
subItemState,
|
||||
rightOptions,
|
||||
isDragging,
|
||||
triggerEvent,
|
||||
}: NavigationDrawerSubItemProps) => {
|
||||
return (
|
||||
<NavigationDrawerItem
|
||||
@ -38,6 +39,7 @@ export const NavigationDrawerSubItem = ({
|
||||
keyboard={keyboard}
|
||||
rightOptions={rightOptions}
|
||||
isDragging={isDragging}
|
||||
triggerEvent={triggerEvent}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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) => (
|
||||
<LinkChip
|
||||
to={to}
|
||||
@ -55,5 +59,6 @@ export const LinkAvatarChip = ({
|
||||
}
|
||||
className={className}
|
||||
maxWidth={maxWidth}
|
||||
triggerEvent={triggerEvent}
|
||||
/>
|
||||
);
|
||||
|
||||
@ -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<HTMLAnchorElement>) => void;
|
||||
onClick?: (event: MouseEvent<HTMLElement>) => void;
|
||||
onMouseDown?: (event: MouseEvent<HTMLElement>) => 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 (
|
||||
<StyledLink to={to} onClick={onClick}>
|
||||
<StyledLink
|
||||
to={to}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
onClickHandler(event);
|
||||
}}
|
||||
onMouseDown={onMouseDownHandler}
|
||||
data-click-outside-id={LINK_CHIP_CLICK_OUTSIDE_ID}
|
||||
>
|
||||
<Chip
|
||||
size={size}
|
||||
label={label}
|
||||
|
||||
@ -0,0 +1 @@
|
||||
export const LINK_CHIP_CLICK_OUTSIDE_ID = 'link-chip-click-outside-id';
|
||||
@ -17,6 +17,7 @@ export type { AvatarChipsCommonProps } from './avatar-chip/types/AvatarChipsComm
|
||||
export { AvatarChipVariant } from './avatar-chip/types/AvatarChipsVariant.type';
|
||||
export type { ChipProps } from './chip/Chip';
|
||||
export { ChipSize, ChipAccent, ChipVariant, Chip } from './chip/Chip';
|
||||
export { LINK_CHIP_CLICK_OUTSIDE_ID } from './chip/constants/LinkChipClickOutsideId';
|
||||
export type { LinkChipProps } from './chip/LinkChip';
|
||||
export { LinkChip } from './chip/LinkChip';
|
||||
export { Pill } from './Pill/Pill';
|
||||
|
||||
@ -29,10 +29,6 @@ const StyledElementsCount = styled.span`
|
||||
color: ${({ theme }) => theme.font.color.tertiary};
|
||||
`;
|
||||
|
||||
const StyledEmptyState = styled.div`
|
||||
color: ${({ theme }) => theme.font.color.tertiary};
|
||||
`;
|
||||
|
||||
const StyledJsonList = styled(JsonList)``.withComponent(motion.ul);
|
||||
|
||||
export const JsonNestedNode = ({
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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<HTMLElement>) => 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<HTMLElement>) => {
|
||||
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<HTMLElement>) => {
|
||||
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,
|
||||
};
|
||||
};
|
||||
@ -3,7 +3,7 @@ type LimitedMouseEvent = Pick<
|
||||
'button' | 'metaKey' | 'altKey' | 'ctrlKey' | 'shiftKey'
|
||||
>;
|
||||
|
||||
export const isModifiedEvent = ({
|
||||
export const isNavigationModifierPressed = ({
|
||||
altKey,
|
||||
ctrlKey,
|
||||
shiftKey,
|
||||
@ -0,0 +1 @@
|
||||
export type TriggerEventType = 'MOUSE_DOWN' | 'CLICK';
|
||||
Reference in New Issue
Block a user