Files
twenty/packages/twenty-front/src/modules/navigation-menu-item/components/WorkspaceNavigationMenuItemsFolder.tsx
T
Abdul Rahman 9fe2a07c55 Navbar customization v2 (#18026)
Adds color support for navigation menu items.

---------

Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
Co-authored-by: Devessier <baptiste@devessier.fr>
Co-authored-by: Charles Bochet <charles@twenty.com>
2026-02-28 11:48:28 +01:00

344 lines
14 KiB
TypeScript

import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { Droppable } from '@hello-pangea/dnd';
import { useLingui } from '@lingui/react/macro';
import { isNonEmptyString } from '@sniptt/guards';
import { useContext } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import { isDefined } from 'twenty-shared/utils';
import {
IconChevronDown,
IconChevronRight,
IconPlus,
useIcons,
} from 'twenty-ui/display';
import { useIsDropDisabledForSection } from '@/navigation-menu-item/hooks/useIsDropDisabledForSection';
import { useOpenAddItemToFolderPage } from '@/navigation-menu-item/hooks/useOpenAddItemToFolderPage';
import { AnimatedExpandableContainer } from 'twenty-ui/layout';
import { useIsMobile } from 'twenty-ui/utilities';
import { NavigationMenuItemDroppable } from '@/navigation-menu-item/components/NavigationMenuItemDroppable';
import { NavigationMenuItemIcon } from '@/navigation-menu-item/components/NavigationMenuItemIcon';
import { WorkspaceNavigationMenuItemFolderDragClone } from '@/navigation-menu-item/components/WorkspaceNavigationMenuItemFolderDragClone';
import { FOLDER_ICON_DEFAULT } from '@/navigation-menu-item/constants/FolderIconDefault';
import { DEFAULT_NAVIGATION_MENU_ITEM_COLOR_FOLDER } from '@/navigation-menu-item/constants/NavigationMenuItemDefaultColorFolder';
import { NavigationMenuItemDroppableIds } from '@/navigation-menu-item/constants/NavigationMenuItemDroppableIds';
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
import { NavigationMenuItemDragContext } from '@/navigation-menu-item/contexts/NavigationMenuItemDragContext';
import { type NavigationMenuItemClickParams } from '@/navigation-menu-item/hooks/useWorkspaceSectionItems';
import { openNavigationMenuItemFolderIdsState } from '@/navigation-menu-item/states/openNavigationMenuItemFolderIdsState';
import { getNavigationMenuItemSecondaryLabel } from '@/navigation-menu-item/utils/getNavigationMenuItemSecondaryLabel';
import { getObjectMetadataForNavigationMenuItem } from '@/navigation-menu-item/utils/getObjectMetadataForNavigationMenuItem';
import { isLocationMatchingNavigationMenuItem } from '@/navigation-menu-item/utils/isLocationMatchingNavigationMenuItem';
import { type ProcessedNavigationMenuItem } from '@/navigation-menu-item/utils/sortNavigationMenuItems';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { DraggableItem } from '@/ui/layout/draggable-list/components/DraggableItem';
import { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem';
import { NavigationDrawerItemsCollapsableContainer } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItemsCollapsableContainer';
import { NavigationDrawerSubItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSubItem';
import { currentNavigationMenuItemFolderIdState } from '@/ui/navigation/navigation-drawer/states/currentNavigationMenuItemFolderIdState';
import { getNavigationSubItemLeftAdornment } from '@/ui/navigation/navigation-drawer/utils/getNavigationSubItemLeftAdornment';
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
import { coreViewsState } from '@/views/states/coreViewState';
import { ViewKey } from '@/views/types/ViewKey';
import { convertCoreViewToView } from '@/views/utils/convertCoreViewToView';
const StyledFolderContainer = styled.div<{ $isSelectedInEditMode: boolean }>`
border: ${({ theme, $isSelectedInEditMode }) =>
$isSelectedInEditMode
? `1px solid ${theme.color.blue}`
: '1px solid transparent'};
border-radius: ${({ theme }) => theme.border.radius.sm};
`;
const StyledFolderDroppableContent = styled.div<{
$compact: boolean;
}>`
display: flex;
flex-direction: column;
padding-bottom: ${({ theme, $compact }) => ($compact ? 0 : theme.spacing(2))};
`;
const StyledFolderExpandableWrapper = styled.div`
& > div {
overflow: visible !important;
}
`;
type WorkspaceNavigationMenuItemsFolderProps = {
folderId: string;
folderName: string;
folderIconKey?: string | null;
folderColor?: string | null;
navigationMenuItems: ProcessedNavigationMenuItem[];
isGroup: boolean;
isEditMode?: boolean;
isSelectedInEditMode?: boolean;
onEditModeClick?: () => void;
onNavigationMenuItemClick?: (params: NavigationMenuItemClickParams) => void;
selectedNavigationMenuItemId?: string | null;
isDragging?: boolean;
};
export const WorkspaceNavigationMenuItemsFolder = ({
folderId,
folderName,
folderIconKey,
folderColor,
navigationMenuItems,
isGroup,
isEditMode = false,
isSelectedInEditMode = false,
onEditModeClick,
onNavigationMenuItemClick,
selectedNavigationMenuItemId = null,
isDragging = false,
}: WorkspaceNavigationMenuItemsFolderProps) => {
const theme = useTheme();
const { getIcon } = useIcons();
const FolderIcon = getIcon(folderIconKey ?? FOLDER_ICON_DEFAULT);
const objectMetadataItems = useAtomStateValue(objectMetadataItemsState);
const coreViews = useAtomStateValue(coreViewsState);
const views = coreViews.map(convertCoreViewToView);
const location = useLocation();
const navigate = useNavigate();
const currentPath = location.pathname;
const currentViewPath = location.pathname + location.search;
const isMobile = useIsMobile();
const { t } = useLingui();
const [openNavigationMenuItemFolderIds, setOpenNavigationMenuItemFolderIds] =
useAtomState(openNavigationMenuItemFolderIdsState);
const setCurrentNavigationMenuItemFolderId = useSetAtomState(
currentNavigationMenuItemFolderIdState,
);
const { openAddItemToFolderPage } = useOpenAddItemToFolderPage();
const isOpen = openNavigationMenuItemFolderIds.includes(folderId);
const folderContentLengthForTree =
isEditMode && isSelectedInEditMode
? navigationMenuItems.length + 1
: navigationMenuItems.length;
const handleAddMenuItemToFolder = () => {
openAddItemToFolderPage({
targetFolderId: folderId,
targetIndex: navigationMenuItems.length,
resetNavigationStack: true,
});
};
const handleToggle = () => {
if (isMobile) {
setCurrentNavigationMenuItemFolderId((prev) =>
prev === folderId ? null : folderId,
);
} else {
setOpenNavigationMenuItemFolderIds((current) =>
isOpen
? current.filter((id) => id !== folderId)
: [...current, folderId],
);
}
if (!isOpen) {
const firstNonLinkItem = navigationMenuItems.find(
(item) =>
item.itemType !== NavigationMenuItemType.LINK &&
isNonEmptyString(item.link),
);
if (isDefined(firstNonLinkItem?.link)) {
navigate(firstNonLinkItem.link);
}
}
};
const shouldUseEditModeClick = isEditMode && isDefined(onEditModeClick);
const handleClick = shouldUseEditModeClick ? onEditModeClick : handleToggle;
const selectedNavigationMenuItemIndex = navigationMenuItems.findIndex(
(item) =>
isLocationMatchingNavigationMenuItem(currentPath, currentViewPath, item),
);
const navigationMenuItemFolderContentLength = navigationMenuItems.length;
const { isDragging: isContextDragging } = useContext(
NavigationMenuItemDragContext,
);
const folderContentDropDisabled = useIsDropDisabledForSection(true);
return (
<StyledFolderContainer $isSelectedInEditMode={isSelectedInEditMode}>
<NavigationDrawerItemsCollapsableContainer isGroup={isGroup}>
<NavigationMenuItemDroppable
droppableId={`${NavigationMenuItemDroppableIds.WORKSPACE_FOLDER_HEADER_PREFIX}${folderId}`}
isWorkspaceSection={true}
>
<NavigationDrawerItem
label={folderName}
Icon={FolderIcon}
iconColor={
isDefined(folderColor)
? folderColor
: DEFAULT_NAVIGATION_MENU_ITEM_COLOR_FOLDER
}
onClick={handleClick}
className="navigation-drawer-item"
triggerEvent="CLICK"
preventCollapseOnMobile={isMobile}
isDragging={isDragging}
rightOptions={
isOpen ? (
<IconChevronDown
size={theme.icon.size.sm}
stroke={theme.icon.stroke.sm}
color={theme.font.color.tertiary}
/>
) : (
<IconChevronRight
size={theme.icon.size.sm}
stroke={theme.icon.stroke.sm}
color={theme.font.color.tertiary}
/>
)
}
/>
</NavigationMenuItemDroppable>
<StyledFolderExpandableWrapper>
<AnimatedExpandableContainer
isExpanded={isOpen}
dimension="height"
mode="fit-content"
containAnimation
>
<Droppable
droppableId={`${NavigationMenuItemDroppableIds.WORKSPACE_FOLDER_PREFIX}${folderId}`}
isDropDisabled={folderContentDropDisabled}
ignoreContainerClipping
renderClone={(provided, snapshot, rubric) => (
<WorkspaceNavigationMenuItemFolderDragClone
draggableProvided={provided}
draggableSnapshot={snapshot}
rubric={rubric}
navigationMenuItems={navigationMenuItems}
navigationMenuItemFolderContentLength={
navigationMenuItemFolderContentLength
}
selectedNavigationMenuItemIndex={
selectedNavigationMenuItemIndex
}
/>
)}
getContainerForClone={() => document.body}
>
{(provided) => (
<StyledFolderDroppableContent
ref={provided.innerRef}
$compact={isEditMode || navigationMenuItems.length === 0}
// eslint-disable-next-line react/jsx-props-no-spreading
{...provided.droppableProps}
>
{navigationMenuItems.map((navigationMenuItem, index) => {
const objectMetadataItem =
navigationMenuItem.itemType ===
NavigationMenuItemType.VIEW ||
navigationMenuItem.itemType ===
NavigationMenuItemType.RECORD
? getObjectMetadataForNavigationMenuItem(
navigationMenuItem,
objectMetadataItems,
views,
)
: null;
const handleEditModeClick =
isEditMode &&
isDefined(onNavigationMenuItemClick) &&
(navigationMenuItem.itemType ===
NavigationMenuItemType.LINK ||
isDefined(objectMetadataItem))
? () =>
onNavigationMenuItemClick({
item: navigationMenuItem,
objectMetadataItem:
objectMetadataItem ?? undefined,
})
: undefined;
return (
<DraggableItem
key={navigationMenuItem.id}
draggableId={navigationMenuItem.id}
index={index}
isInsideScrollableContainer
isDragDisabled={!isEditMode}
disableInteractiveElementBlocking={isEditMode}
itemComponent={
<NavigationDrawerSubItem
secondaryLabel={
navigationMenuItem.viewKey === ViewKey.Index
? undefined
: getNavigationMenuItemSecondaryLabel({
objectMetadataItems,
navigationMenuItemObjectNameSingular:
navigationMenuItem.objectNameSingular,
})
}
label={navigationMenuItem.labelIdentifier}
Icon={() => (
<NavigationMenuItemIcon
navigationMenuItem={navigationMenuItem}
/>
)}
to={
isContextDragging || handleEditModeClick
? undefined
: navigationMenuItem.link
}
onClick={handleEditModeClick}
active={index === selectedNavigationMenuItemIndex}
isSelectedInEditMode={
selectedNavigationMenuItemId ===
navigationMenuItem.id
}
subItemState={getNavigationSubItemLeftAdornment({
index,
arrayLength: folderContentLengthForTree,
selectedIndex: selectedNavigationMenuItemIndex,
})}
isDragging={isContextDragging}
triggerEvent="CLICK"
/>
}
/>
);
})}
{provided.placeholder}
</StyledFolderDroppableContent>
)}
</Droppable>
{isEditMode && isSelectedInEditMode && (
<NavigationDrawerSubItem
label={t`Add menu item`}
Icon={IconPlus}
onClick={handleAddMenuItemToFolder}
triggerEvent="CLICK"
subItemState={getNavigationSubItemLeftAdornment({
index: navigationMenuItems.length,
arrayLength: folderContentLengthForTree,
selectedIndex: selectedNavigationMenuItemIndex,
})}
/>
)}
</AnimatedExpandableContainer>
</StyledFolderExpandableWrapper>
</NavigationDrawerItemsCollapsableContainer>
</StyledFolderContainer>
);
};