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:
Félix Malfait
2025-05-28 12:32:49 +02:00
committed by GitHub
parent 97d4ec96af
commit d4fac6793a
29 changed files with 203 additions and 60 deletions

View File

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

View File

@ -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 (

View File

@ -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,

View File

@ -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"
/>
}
/>

View File

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

View File

@ -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}
/>

View File

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

View File

@ -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: [],

View File

@ -68,6 +68,7 @@ export const RecordBoardCardBody = ({
},
useUpdateRecord: useUpdateOneRecordHook,
isDisplayModeFixHeight: true,
triggerEvent: 'CLICK',
}}
>
<RecordFieldComponentInstanceContext.Provider

View File

@ -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>

View File

@ -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>(

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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>

View File

@ -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> = {

View File

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

View File

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