Files
twenty/packages/twenty-front/src/modules/navigation-menu-item/components/WorkspaceNavigationMenuItemsFolder.tsx
T
Charles Bochet d81f006979 Rename Jotai state hooks and utilities to consistent Atom-based naming (#18209)
## Summary

Rename all Jotai-based state management hooks and utilities to a
consistent naming convention that:
1. Removes legacy Recoil naming (`useRecoilXxxV2`, `createXxxV2`)
2. Always includes `Atom` in the name to distinguish from jotai native
hooks
3. Follows a consistent ordering: `Atom` → `Component` → `Family` →
`Type` (`State`/`Selector`)
4. Includes the type qualifier (`State` or `Selector`) in all
value-reading hooks to avoid naming conflicts with jotai's native
`useAtomValue`

## Naming Convention

**Hooks**:
`use[Set]Atom[Component][Family][State|Selector][Value|State|CallbackState]`
**Utils**: `createAtom[Component][Family][State|Selector]`

## Changes

### Hooks renamed (definition files + all usages across ~1,500 files):

| Old Name (Recoil) | Intermediate (Atom) | Final Name |
|---|---|---|
| `useRecoilValueV2` | `useAtomValue` | **`useAtomStateValue`** |
| `useRecoilStateV2` | `useAtomState` | `useAtomState` |
| `useSetRecoilStateV2` | `useSetAtomState` | `useSetAtomState` |
| `useFamilyRecoilValueV2` | `useFamilyAtomValue` |
**`useAtomFamilyStateValue`** |
| `useSetFamilyRecoilStateV2` | `useSetFamilyAtomState` |
**`useSetAtomFamilyState`** |
| `useFamilySelectorValueV2` | `useFamilySelectorValue` |
**`useAtomFamilySelectorValue`** |
| `useFamilySelectorStateV2` | `useFamilySelectorState` |
**`useAtomFamilySelectorState`** |
| `useRecoilComponentValueV2` | `useAtomComponentValue` |
**`useAtomComponentStateValue`** |
| `useRecoilComponentStateV2` | `useAtomComponentState` |
`useAtomComponentState` |
| `useSetRecoilComponentStateV2` | `useSetAtomComponentState` |
`useSetAtomComponentState` |
| `useRecoilComponentFamilyValueV2` | `useAtomComponentFamilyValue` |
**`useAtomComponentFamilyStateValue`** |
| `useRecoilComponentFamilyStateV2` | `useAtomComponentFamilyState` |
`useAtomComponentFamilyState` |
| `useSetRecoilComponentFamilyStateV2` |
`useSetAtomComponentFamilyState` | `useSetAtomComponentFamilyState` |
| `useRecoilComponentSelectorValueV2` | `useAtomComponentSelectorValue`
| `useAtomComponentSelectorValue` |
| `useRecoilComponentFamilySelectorValueV2` |
`useAtomComponentFamilySelectorValue` |
`useAtomComponentFamilySelectorValue` |
| `useRecoilComponentStateCallbackStateV2` |
`useAtomComponentStateCallbackState` |
`useAtomComponentStateCallbackState` |
| `useRecoilComponentSelectorCallbackStateV2` |
`useAtomComponentSelectorCallbackState` |
`useAtomComponentSelectorCallbackState` |
| `useRecoilComponentFamilyStateCallbackStateV2` |
`useAtomComponentFamilyStateCallbackState` |
`useAtomComponentFamilyStateCallbackState` |
| `useRecoilComponentFamilySelectorCallbackStateV2` |
`useAtomComponentFamilySelectorCallbackState` |
`useAtomComponentFamilySelectorCallbackState` |

### Utilities renamed:

| Old Name (Recoil) | Final Name |
|---|---|
| `createStateV2` | **`createAtomState`** |
| `createFamilyStateV2` | **`createAtomFamilyState`** |
| `createSelectorV2` | **`createAtomSelector`** |
| `createFamilySelectorV2` | **`createAtomFamilySelector`** |
| `createWritableSelectorV2` | **`createAtomWritableSelector`** |
| `createWritableFamilySelectorV2` |
**`createAtomWritableFamilySelector`** |
| `createComponentStateV2` | **`createAtomComponentState`** |
| `createComponentFamilyStateV2` | **`createAtomComponentFamilyState`**
|
| `createComponentSelectorV2` | **`createAtomComponentSelector`** |
| `createComponentFamilySelectorV2` |
**`createAtomComponentFamilySelector`** |

## Details

- All definition files renamed to match new convention
- All import paths and usages updated across the entire `twenty-front`
package
- Jotai's native `useAtomValue` is aliased as `useJotaiAtomValue` in the
wrapper hook to avoid collision
- Legacy Recoil files in `state/utils/` and `component-state/utils/`
left untouched (separate naming scope)
- Typecheck passes cleanly
2026-02-25 01:55:46 +01:00

311 lines
14 KiB
TypeScript

import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { Droppable } from '@hello-pangea/dnd';
import { isNonEmptyString } from '@sniptt/guards';
import { useContext } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { useIsDropDisabledForSection } from '@/navigation-menu-item/hooks/useIsDropDisabledForSection';
import { useLocation, useNavigate } from 'react-router-dom';
import { IconChevronDown, IconChevronRight, useIcons } from 'twenty-ui/display';
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 { 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 { openNavigationMenuItemFolderIdsStateV2 } from '@/navigation-menu-item/states/openNavigationMenuItemFolderIdsStateV2';
import { getNavigationMenuItemIconColors } from '@/navigation-menu-item/utils/getNavigationMenuItemIconColors';
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 { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
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 { currentNavigationMenuItemFolderIdStateV2 } from '@/ui/navigation/navigation-drawer/states/currentNavigationMenuItemFolderIdStateV2';
import { getNavigationSubItemLeftAdornment } from '@/ui/navigation/navigation-drawer/utils/getNavigationSubItemLeftAdornment';
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
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';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { FeatureFlagKey } from '~/generated-metadata/graphql';
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;
gap: ${({ theme }) => theme.betweenSiblingsGap};
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;
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,
navigationMenuItems,
isGroup,
isEditMode = false,
isSelectedInEditMode = false,
onEditModeClick,
onNavigationMenuItemClick,
selectedNavigationMenuItemId = null,
isDragging = false,
}: WorkspaceNavigationMenuItemsFolderProps) => {
const theme = useTheme();
const { getIcon } = useIcons();
const isNavigationMenuItemEditingEnabled = useIsFeatureEnabled(
FeatureFlagKey.IS_NAVIGATION_MENU_ITEM_EDITING_ENABLED,
);
const iconColors = getNavigationMenuItemIconColors(theme);
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 [openNavigationMenuItemFolderIds, setOpenNavigationMenuItemFolderIds] =
useAtomState(openNavigationMenuItemFolderIdsStateV2);
const setCurrentFolderId = useSetAtomState(
currentNavigationMenuItemFolderIdStateV2,
);
const isOpen = openNavigationMenuItemFolderIds.includes(folderId);
const handleToggle = () => {
if (isMobile) {
setCurrentFolderId((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}
iconBackgroundColor={
isNavigationMenuItemEditingEnabled ? iconColors.folder : undefined
}
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:
navigationMenuItemFolderContentLength,
selectedIndex: selectedNavigationMenuItemIndex,
})}
isDragging={isContextDragging}
triggerEvent="CLICK"
/>
}
/>
);
})}
{provided.placeholder}
</StyledFolderDroppableContent>
)}
</Droppable>
</AnimatedExpandableContainer>
</StyledFolderExpandableWrapper>
</NavigationDrawerItemsCollapsableContainer>
</StyledFolderContainer>
);
};