refactor(twenty-front): reorganize NavigationMenuItem module into common/display/edit subfolders (#18691)

## Summary

- Reorganizes the `navigation-menu-item` frontend module from a flat
structure into `common/`, `display/`, and `edit/` subfolders with
type-specific subdirectories (`link/`, `folder/`, `object/`, `view/`,
`record/`)
- Every file is now in a leaf folder describing its type: `components/`,
`hooks/`, `utils/`, `types/`, or `constants/`
- Moves 14 NavigationMenuItem-related components out of
`object-metadata/` and `side-panel/pages/` into the
`navigation-menu-item` module where they belong
- Creates type-specific display utility functions (e.g.,
`getLinkNavigationMenuItemLabel`,
`getObjectNavigationMenuItemComputedLink`) to replace generic
switch-based functions
- Unifies the Favorites section drag-and-drop from `@hello-pangea/dnd`
to `@dnd-kit/react`, matching the Workspace section's DnD library
- Renames Favorites section components from `CurrentWorkspaceMember*` to
`Favorites*` for clarity
- Deletes unused `FavoritesDragDropProviderContent` and
`NavbarDragProvider`

## Test plan

- [x] `npx nx typecheck twenty-front` passes
- [x] `npx nx lint:diff-with-main twenty-front` passes (0 warnings, 0
errors)
- [x] `npx nx test twenty-front` passes (763 suites, 4467 tests)
- [ ] Verify favorites drag-and-drop still works in the UI (reorder
items, move between folders)
- [ ] Verify workspace edit mode drag-and-drop still works
- [ ] Verify "add to navigation" drag from command menu/side panel still
works
This commit is contained in:
Charles Bochet
2026-03-17 06:46:50 +01:00
committed by GitHub
parent 1be87eb97b
commit f1dfcfd163
251 changed files with 1361 additions and 1194 deletions
@@ -1,174 +0,0 @@
import { ObjectIconWithViewOverlay } from '@/navigation-menu-item/components/ObjectIconWithViewOverlay';
import { useObjectNavItemColor } from '@/navigation-menu-item/hooks/useObjectNavItemColor';
import { isNavigationMenuInEditModeState } from '@/navigation-menu-item/states/isNavigationMenuInEditModeState';
import { getNavigationMenuItemComputedLink } from '@/navigation-menu-item/utils/getNavigationMenuItemComputedLink';
import { getNavigationMenuItemLabel } from '@/navigation-menu-item/utils/getNavigationMenuItemLabel';
import { recordIdentifierToObjectRecordIdentifier } from '@/navigation-menu-item/utils/recordIdentifierToObjectRecordIdentifier';
import { lastVisitedViewPerObjectMetadataItemState } from '@/navigation/states/lastVisitedViewPerObjectMetadataItemState';
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { viewsSelector } from '@/views/states/selectors/viewsSelector';
import { ViewKey } from '@/views/types/ViewKey';
import { useLocation } from 'react-router-dom';
import {
AppPath,
CoreObjectNameSingular,
NavigationMenuItemType,
} from 'twenty-shared/types';
import { getAppPath, isDefined } from 'twenty-shared/utils';
import { Avatar, useIcons } from 'twenty-ui/display';
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
export type NavigationDrawerItemForObjectMetadataItemProps = {
objectMetadataItem: ObjectMetadataItem;
navigationMenuItem?: NavigationMenuItem;
isSelectedInEditMode?: boolean;
onEditModeClick?: () => void;
onActiveItemClickWhenNotInEditMode?: () => void;
isDragging?: boolean;
};
export const NavigationDrawerItemForObjectMetadataItem = ({
objectMetadataItem,
navigationMenuItem,
isSelectedInEditMode = false,
onEditModeClick,
onActiveItemClickWhenNotInEditMode: _onActiveItemClickWhenNotInEditMode,
isDragging = false,
}: NavigationDrawerItemForObjectMetadataItemProps) => {
const isNavigationMenuInEditMode = useAtomStateValue(
isNavigationMenuInEditModeState,
);
const lastVisitedViewPerObjectMetadataItem = useAtomStateValue(
lastVisitedViewPerObjectMetadataItemState,
);
const objectMetadataItems = useAtomStateValue(objectMetadataItemsSelector);
const views = useAtomStateValue(viewsSelector);
const lastVisitedViewId =
lastVisitedViewPerObjectMetadataItem?.[objectMetadataItem.id];
const { getIcon } = useIcons();
const objectNavItemColor = useObjectNavItemColor(
objectMetadataItem.nameSingular,
);
const location = useLocation();
const currentPath = location.pathname;
const currentPathWithSearch = `${location.pathname}${location.search}`;
const isRecord = navigationMenuItem?.type === NavigationMenuItemType.RECORD;
const isView = navigationMenuItem?.type === NavigationMenuItemType.VIEW;
const isObject = navigationMenuItem?.type === NavigationMenuItemType.OBJECT;
const hasCustomLink = isRecord || isView || isObject;
const navigationPath = hasCustomLink
? getNavigationMenuItemComputedLink(
navigationMenuItem!,
objectMetadataItems,
views,
)
: getAppPath(
AppPath.RecordIndexPage,
{ objectNamePlural: objectMetadataItem.namePlural },
lastVisitedViewId ? { viewId: lastVisitedViewId } : undefined,
);
const computedLink = hasCustomLink ? navigationPath : '';
const isActive = hasCustomLink
? (isView || isObject ? currentPathWithSearch : currentPath) ===
computedLink
: currentPath ===
getAppPath(AppPath.RecordIndexPage, {
objectNamePlural: objectMetadataItem.namePlural,
}) ||
currentPath.includes(
getAppPath(AppPath.RecordShowPage, {
objectNameSingular: objectMetadataItem.nameSingular,
objectRecordId: '',
}) + '/',
);
const handleClick = isNavigationMenuInEditMode ? onEditModeClick : undefined;
const shouldNavigate = !isNavigationMenuInEditMode;
const view = isDefined(navigationMenuItem?.viewId)
? views.find((view) => view.id === navigationMenuItem!.viewId)
: undefined;
const viewKey = view?.key ?? null;
const isViewWithCustomName =
isView && viewKey !== ViewKey.INDEX && isDefined(view);
const itemLabel = isDefined(navigationMenuItem)
? getNavigationMenuItemLabel(navigationMenuItem, objectMetadataItems, views)
: objectMetadataItem.labelPlural;
const label = isRecord
? itemLabel
: isViewWithCustomName
? itemLabel
: objectMetadataItem.labelPlural;
const recordIdentifier =
isRecord && isDefined(navigationMenuItem?.targetRecordIdentifier)
? recordIdentifierToObjectRecordIdentifier({
recordIdentifier: navigationMenuItem!.targetRecordIdentifier!,
objectMetadataItem,
})
: null;
const Icon = isRecord
? () => (
<Avatar
type={
objectMetadataItem.nameSingular === CoreObjectNameSingular.Company
? 'squared'
: 'rounded'
}
avatarUrl={recordIdentifier?.avatarUrl}
placeholderColorSeed={navigationMenuItem!.targetRecordId ?? undefined}
placeholder={itemLabel}
/>
)
: isViewWithCustomName && isDefined(view?.icon)
? () => (
<ObjectIconWithViewOverlay
ObjectIcon={getIcon(objectMetadataItem.icon)}
ViewIcon={getIcon(view!.icon)}
objectColor={objectNavItemColor}
/>
)
: getIcon(objectMetadataItem.icon);
const iconThemeColor = !isRecord ? objectNavItemColor : undefined;
const secondaryLabel =
isRecord || isViewWithCustomName
? objectMetadataItem.labelSingular
: undefined;
return (
<NavigationDrawerItem
label={label}
secondaryLabel={secondaryLabel}
to={
isNavigationMenuInEditMode || isDragging
? undefined
: shouldNavigate
? navigationPath
: undefined
}
onClick={handleClick}
Icon={Icon}
iconColor={iconThemeColor}
active={isActive}
isSelectedInEditMode={isSelectedInEditMode}
isDragging={isDragging}
triggerEvent={isNavigationMenuInEditMode ? 'CLICK' : undefined}
/>
);
};
@@ -1,63 +0,0 @@
import { useParams } from 'react-router-dom';
import { useWorkspaceNavigationMenuItems } from '@/navigation-menu-item/hooks/useWorkspaceNavigationMenuItems';
import { NavigationDrawerSectionForObjectMetadataItems } from '@/object-metadata/components/NavigationDrawerSectionForObjectMetadataItems';
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
import { CoreObjectNameSingular } from 'twenty-shared/types';
import { useLingui } from '@lingui/react/macro';
const WORKFLOW_OBJECTS_IN_SIDEBAR = [
CoreObjectNameSingular.Workflow,
CoreObjectNameSingular.WorkflowRun,
CoreObjectNameSingular.WorkflowVersion,
];
export const NavigationDrawerOpenedSection = () => {
const { t } = useLingui();
const { activeObjectMetadataItems } = useFilteredObjectMetadataItems();
const filteredActiveNonSystemObjectMetadataItems =
activeObjectMetadataItems.filter((item) => !item.isRemote);
const { workspaceNavigationMenuItemsObjectMetadataItems } =
useWorkspaceNavigationMenuItems();
const {
objectNamePlural: currentObjectNamePlural,
objectNameSingular: currentObjectNameSingular,
} = useParams();
if (!currentObjectNamePlural && !currentObjectNameSingular) {
return;
}
const objectMetadataItem = filteredActiveNonSystemObjectMetadataItems.find(
(item) =>
item.namePlural === currentObjectNamePlural ||
item.nameSingular === currentObjectNameSingular,
);
if (!objectMetadataItem) {
return;
}
const isWorkflowObjectInSidebar = WORKFLOW_OBJECTS_IN_SIDEBAR.includes(
objectMetadataItem.nameSingular as CoreObjectNameSingular,
);
const shouldDisplayObjectInOpenedSection =
!isWorkflowObjectInSidebar &&
!workspaceNavigationMenuItemsObjectMetadataItems
.map((item) => item.id)
.includes(objectMetadataItem.id);
return (
shouldDisplayObjectInOpenedSection && (
<NavigationDrawerSectionForObjectMetadataItems
sectionTitle={t`Opened`}
objectMetadataItems={[objectMetadataItem]}
isRemote={false}
/>
)
);
};
@@ -1,4 +1,4 @@
import { NavigationDrawerItemForObjectMetadataItem } from '@/object-metadata/components/NavigationDrawerItemForObjectMetadataItem';
import { NavigationDrawerItemForObjectMetadataItem } from '@/navigation-menu-item/display/object/components/NavigationDrawerItemForObjectMetadataItem';
import { CoreObjectNameSingular } from 'twenty-shared/types';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { getObjectPermissionsForObject } from '@/object-metadata/utils/getObjectPermissionsForObject';
@@ -1,64 +0,0 @@
import { NavigationMenuItemType } from 'twenty-shared/types';
import { NavigationDrawerSectionForWorkspaceItemFolderContent } from '@/object-metadata/components/NavigationDrawerSectionForWorkspaceItemFolderContent';
import { NavigationDrawerSectionForWorkspaceItemLinkContent } from '@/object-metadata/components/NavigationDrawerSectionForWorkspaceItemLinkContent';
import { NavigationDrawerSectionForWorkspaceItemObjectContent } from '@/object-metadata/components/NavigationDrawerSectionForWorkspaceItemObjectContent';
import type { WorkspaceSectionItemContentProps } from '@/object-metadata/components/WorkspaceSectionItemContentProps';
type NavigationDrawerSectionForWorkspaceItemContentProps =
WorkspaceSectionItemContentProps;
export const NavigationDrawerSectionForWorkspaceItemContent = ({
item,
editModeProps,
isDragging,
folderChildrenById,
folderCount,
selectedNavigationMenuItemId,
onNavigationMenuItemClick,
onActiveObjectMetadataItemClick,
readOnly,
}: NavigationDrawerSectionForWorkspaceItemContentProps) => {
switch (item.type) {
case NavigationMenuItemType.FOLDER:
return (
<NavigationDrawerSectionForWorkspaceItemFolderContent
item={item}
editModeProps={editModeProps}
isDragging={isDragging}
folderChildrenById={folderChildrenById}
folderCount={folderCount}
selectedNavigationMenuItemId={selectedNavigationMenuItemId}
onNavigationMenuItemClick={onNavigationMenuItemClick}
readOnly={readOnly}
/>
);
case NavigationMenuItemType.LINK:
return (
<NavigationDrawerSectionForWorkspaceItemLinkContent
item={item}
editModeProps={editModeProps}
isDragging={isDragging}
folderChildrenById={folderChildrenById}
folderCount={folderCount}
selectedNavigationMenuItemId={selectedNavigationMenuItemId}
onNavigationMenuItemClick={onNavigationMenuItemClick}
onActiveObjectMetadataItemClick={onActiveObjectMetadataItemClick}
readOnly={readOnly}
/>
);
default:
return (
<NavigationDrawerSectionForWorkspaceItemObjectContent
item={item}
editModeProps={editModeProps}
isDragging={isDragging}
folderChildrenById={folderChildrenById}
folderCount={folderCount}
selectedNavigationMenuItemId={selectedNavigationMenuItemId}
onNavigationMenuItemClick={onNavigationMenuItemClick}
onActiveObjectMetadataItemClick={onActiveObjectMetadataItemClick}
readOnly={readOnly}
/>
);
}
};
@@ -1,72 +0,0 @@
import { lazy, Suspense } from 'react';
import { WorkspaceFolderReadOnly } from '@/object-metadata/components/WorkspaceFolderReadOnly';
import type { WorkspaceSectionItemContentProps } from '@/object-metadata/components/WorkspaceSectionItemContentProps';
const LazyWorkspaceNavigationMenuItemsFolder = lazy(() =>
import(
'@/navigation-menu-item/components/WorkspaceNavigationMenuItemsFolder'
).then((m) => ({ default: m.WorkspaceNavigationMenuItemsFolder })),
);
type NavigationDrawerSectionForWorkspaceItemFolderContentProps =
WorkspaceSectionItemContentProps;
export const NavigationDrawerSectionForWorkspaceItemFolderContent = ({
item,
editModeProps,
isDragging,
folderChildrenById,
folderCount,
selectedNavigationMenuItemId,
onNavigationMenuItemClick,
readOnly = false,
}: NavigationDrawerSectionForWorkspaceItemFolderContentProps) => {
const folderId = item.id;
const folderName = item.name ?? 'Folder';
const folderIconKey = item.icon;
const folderColor = 'color' in item ? item.color : undefined;
const navigationMenuItems = folderChildrenById.get(item.id) ?? [];
const isGroup = folderCount > 1;
if (readOnly) {
return (
<WorkspaceFolderReadOnly
folderId={folderId}
folderName={folderName}
folderIconKey={folderIconKey}
folderColor={folderColor}
navigationMenuItems={navigationMenuItems}
isGroup={isGroup}
/>
);
}
return (
<Suspense
fallback={
<WorkspaceFolderReadOnly
folderId={folderId}
folderName={folderName}
folderIconKey={folderIconKey}
folderColor={folderColor}
navigationMenuItems={navigationMenuItems}
isGroup={isGroup}
/>
}
>
<LazyWorkspaceNavigationMenuItemsFolder
folderId={folderId}
folderName={folderName}
folderIconKey={folderIconKey}
folderColor={folderColor}
navigationMenuItems={navigationMenuItems}
isGroup={isGroup}
isSelectedInEditMode={editModeProps.isSelectedInEditMode}
onEditModeClick={editModeProps.onEditModeClick}
onNavigationMenuItemClick={onNavigationMenuItemClick}
selectedNavigationMenuItemId={selectedNavigationMenuItemId}
isDragging={isDragging}
/>
</Suspense>
);
};
@@ -1,57 +0,0 @@
import { NavigationMenuItemIcon } from '@/navigation-menu-item/components/NavigationMenuItemIcon';
import { isNavigationMenuInEditModeState } from '@/navigation-menu-item/states/isNavigationMenuInEditModeState';
import { getNavigationMenuItemComputedLink } from '@/navigation-menu-item/utils/getNavigationMenuItemComputedLink';
import { getNavigationMenuItemLabel } from '@/navigation-menu-item/utils/getNavigationMenuItemLabel';
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
import type { WorkspaceSectionItemContentProps } from '@/object-metadata/components/WorkspaceSectionItemContentProps';
import { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { viewsSelector } from '@/views/states/selectors/viewsSelector';
import { IconArrowUpRight } from 'twenty-ui/display';
import { themeCssVariables } from 'twenty-ui/theme-constants';
type NavigationDrawerSectionForWorkspaceItemLinkContentProps =
WorkspaceSectionItemContentProps;
export const NavigationDrawerSectionForWorkspaceItemLinkContent = ({
item,
editModeProps,
isDragging,
}: NavigationDrawerSectionForWorkspaceItemLinkContentProps) => {
const isNavigationMenuInEditMode = useAtomStateValue(
isNavigationMenuInEditModeState,
);
const objectMetadataItems = useAtomStateValue(objectMetadataItemsSelector);
const views = useAtomStateValue(viewsSelector);
const label = getNavigationMenuItemLabel(item, objectMetadataItems, views);
const computedLink = getNavigationMenuItemComputedLink(
item,
objectMetadataItems,
views,
);
return (
<NavigationDrawerItem
label={label}
to={isNavigationMenuInEditMode || isDragging ? undefined : computedLink}
onClick={
isNavigationMenuInEditMode ? editModeProps.onEditModeClick : undefined
}
Icon={() => <NavigationMenuItemIcon navigationMenuItem={item} />}
active={false}
isSelectedInEditMode={editModeProps.isSelectedInEditMode}
isDragging={isDragging}
triggerEvent="CLICK"
rightOptions={
!isNavigationMenuInEditMode && (
<IconArrowUpRight
size={themeCssVariables.icon.size.sm}
stroke={themeCssVariables.icon.stroke.md}
color={themeCssVariables.font.color.light}
/>
)
}
/>
);
};
@@ -1,41 +0,0 @@
import { getObjectMetadataForNavigationMenuItem } from '@/navigation-menu-item/utils/getObjectMetadataForNavigationMenuItem';
import { NavigationDrawerItemForObjectMetadataItem } from '@/object-metadata/components/NavigationDrawerItemForObjectMetadataItem';
import type { WorkspaceSectionItemContentProps } from '@/object-metadata/components/WorkspaceSectionItemContentProps';
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { viewsSelector } from '@/views/states/selectors/viewsSelector';
type NavigationDrawerSectionForWorkspaceItemObjectContentProps =
WorkspaceSectionItemContentProps;
export const NavigationDrawerSectionForWorkspaceItemObjectContent = ({
item,
editModeProps,
isDragging,
onActiveObjectMetadataItemClick,
}: NavigationDrawerSectionForWorkspaceItemObjectContentProps) => {
const objectMetadataItems = useAtomStateValue(objectMetadataItemsSelector);
const views = useAtomStateValue(viewsSelector);
const objectMetadataItem = getObjectMetadataForNavigationMenuItem(
item,
objectMetadataItems,
views,
);
if (!objectMetadataItem) {
return null;
}
return (
<NavigationDrawerItemForObjectMetadataItem
objectMetadataItem={objectMetadataItem}
navigationMenuItem={item}
isSelectedInEditMode={editModeProps.isSelectedInEditMode}
onEditModeClick={editModeProps.onEditModeClick}
isDragging={isDragging}
onActiveItemClickWhenNotInEditMode={
onActiveObjectMetadataItemClick
? () => onActiveObjectMetadataItemClick(objectMetadataItem, item.id)
: undefined
}
/>
);
};
@@ -1,202 +0,0 @@
import { NavigationDropTargetContext } from '@/navigation-menu-item/contexts/NavigationDropTargetContext';
import React, { lazy, Suspense, useContext } from 'react';
import { NavigationMenuItemType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { AnimatedExpandableContainer } from 'twenty-ui/layout';
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
import { NavigationMenuItemDroppableIds } from '@/navigation-menu-item/constants/NavigationMenuItemDroppableIds';
import { type NavigationMenuItemClickParams } from '@/navigation-menu-item/hooks/useWorkspaceSectionItems';
import { isNavigationMenuInEditModeState } from '@/navigation-menu-item/states/isNavigationMenuInEditModeState';
import { getObjectMetadataForNavigationMenuItem } from '@/navigation-menu-item/utils/getObjectMetadataForNavigationMenuItem';
import type { EditModeProps } from '@/object-metadata/components/EditModeProps';
import { NavigationDrawerSectionForWorkspaceItemsListReadOnly } from '@/object-metadata/components/NavigationDrawerSectionForWorkspaceItemsListReadOnly';
import { WorkspaceSectionListEditModeFallback } from '@/object-metadata/components/WorkspaceSectionListEditModeFallback';
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { getObjectPermissionsForObject } from '@/object-metadata/utils/getObjectPermissionsForObject';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { NavigationDrawerAnimatedCollapseWrapper } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerAnimatedCollapseWrapper';
import { NavigationDrawerSection } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSection';
import { NavigationDrawerSectionTitle } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSectionTitle';
import { useNavigationSection } from '@/ui/navigation/navigation-drawer/hooks/useNavigationSection';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { viewsSelector } from '@/views/states/selectors/viewsSelector';
import { styled } from '@linaria/react';
import { themeCssVariables } from 'twenty-ui/theme-constants';
const LazyWorkspaceSectionListDndKit = lazy(() =>
import(
'@/object-metadata/components/NavigationDrawerSectionForWorkspaceItemsListDndKit'
).then((m) => ({ default: m.WorkspaceSectionListDndKit })),
);
const StyledWorkspaceSectionContentGapOffset = styled.div`
margin-top: calc(-1 * ${themeCssVariables.betweenSiblingsGap});
`;
type NavigationDrawerSectionForWorkspaceItemsProps = {
sectionTitle: string;
items: NavigationMenuItem[];
rightIcon?: React.ReactNode;
selectedNavigationMenuItemId?: string | null;
onNavigationMenuItemClick?: (params: NavigationMenuItemClickParams) => void;
onActiveObjectMetadataItemClick?: (
objectMetadataItem: ObjectMetadataItem,
navigationMenuItemId: string,
) => void;
};
export const NavigationDrawerSectionForWorkspaceItems = ({
sectionTitle,
items,
rightIcon,
selectedNavigationMenuItemId = null,
onNavigationMenuItemClick,
onActiveObjectMetadataItemClick,
}: NavigationDrawerSectionForWorkspaceItemsProps) => {
const isNavigationMenuInEditMode = useAtomStateValue(
isNavigationMenuInEditModeState,
);
const { toggleNavigationSection, isNavigationSectionOpen } =
useNavigationSection('Workspace');
const views = useAtomStateValue(viewsSelector);
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
const objectMetadataItems = useAtomStateValue(objectMetadataItemsSelector);
const { addToNavigationFallbackDestination } = useContext(
NavigationDropTargetContext,
);
const flatItems = items.filter((item) => !isDefined(item.folderId));
const isAddToNavigationDropTargetVisible =
addToNavigationFallbackDestination?.droppableId ===
NavigationMenuItemDroppableIds.WORKSPACE_ORPHAN_NAVIGATION_MENU_ITEMS;
const folderChildrenById = items.reduce<Map<string, NavigationMenuItem[]>>(
(acc, item) => {
const folderId = item.folderId;
if (isDefined(folderId)) {
const children = acc.get(folderId) ?? [];
children.push(item);
acc.set(folderId, children);
}
return acc;
},
new Map(),
);
const filteredItems = flatItems.filter((item) => {
const itemType = item.type;
if (
itemType === NavigationMenuItemType.FOLDER ||
itemType === NavigationMenuItemType.LINK
) {
return true;
}
if (
itemType === NavigationMenuItemType.OBJECT ||
itemType === NavigationMenuItemType.VIEW ||
itemType === NavigationMenuItemType.RECORD
) {
const objectMetadataItem = getObjectMetadataForNavigationMenuItem(
item,
objectMetadataItems,
views,
);
return (
isDefined(objectMetadataItem) &&
getObjectPermissionsForObject(
objectPermissionsByObjectMetadataId,
objectMetadataItem.id,
).canReadObjectRecords
);
}
return false;
});
const getEditModeProps = (item: NavigationMenuItem): EditModeProps => {
const itemId = item.id;
return {
isSelectedInEditMode: selectedNavigationMenuItemId === itemId,
onEditModeClick: onNavigationMenuItemClick
? () => {
const itemType = item.type;
const objectMetadataItem =
itemType === 'OBJECT' ||
itemType === 'VIEW' ||
itemType === 'RECORD'
? getObjectMetadataForNavigationMenuItem(
item,
objectMetadataItems,
views,
)
: null;
onNavigationMenuItemClick({
item,
objectMetadataItem: objectMetadataItem ?? undefined,
});
}
: undefined,
};
};
if (flatItems.length === 0 && !isAddToNavigationDropTargetVisible) {
return null;
}
return (
<NavigationDrawerSection>
<NavigationDrawerAnimatedCollapseWrapper>
<NavigationDrawerSectionTitle
label={sectionTitle}
onClick={() => toggleNavigationSection()}
rightIcon={rightIcon}
alwaysShowRightIcon={isNavigationMenuInEditMode}
isOpen={isNavigationSectionOpen}
/>
</NavigationDrawerAnimatedCollapseWrapper>
<StyledWorkspaceSectionContentGapOffset>
<AnimatedExpandableContainer
isExpanded={
isNavigationSectionOpen || isAddToNavigationDropTargetVisible
}
dimension="height"
mode="fit-content"
containAnimation
initial={false}
>
{isNavigationMenuInEditMode ? (
<Suspense
fallback={
<WorkspaceSectionListEditModeFallback
filteredItems={filteredItems}
folderChildrenById={folderChildrenById}
onActiveObjectMetadataItemClick={
onActiveObjectMetadataItemClick
}
/>
}
>
<LazyWorkspaceSectionListDndKit
filteredItems={filteredItems}
getEditModeProps={getEditModeProps}
folderChildrenById={folderChildrenById}
selectedNavigationMenuItemId={selectedNavigationMenuItemId}
onNavigationMenuItemClick={onNavigationMenuItemClick}
onActiveObjectMetadataItemClick={
onActiveObjectMetadataItemClick
}
/>
</Suspense>
) : (
<NavigationDrawerSectionForWorkspaceItemsListReadOnly
filteredItems={filteredItems}
folderChildrenById={folderChildrenById}
onActiveObjectMetadataItemClick={onActiveObjectMetadataItemClick}
/>
)}
</AnimatedExpandableContainer>
</StyledWorkspaceSectionContentGapOffset>
</NavigationDrawerSection>
);
};
@@ -1,110 +0,0 @@
import { WorkspaceDndKitDroppableSlot } from '@/navigation-menu-item/components/WorkspaceDndKitDroppableSlot';
import { WorkspaceDndKitSortableItem } from '@/navigation-menu-item/components/WorkspaceDndKitSortableItem';
import { NavigationMenuItemDroppableIds } from '@/navigation-menu-item/constants/NavigationMenuItemDroppableIds';
import { NavigationMenuItemType } from 'twenty-shared/types';
import { NavigationDropTargetContext } from '@/navigation-menu-item/contexts/NavigationDropTargetContext';
import { useIsDropDisabledForSection } from '@/navigation-menu-item/hooks/useIsDropDisabledForSection';
import { isNavigationMenuInEditModeState } from '@/navigation-menu-item/states/isNavigationMenuInEditModeState';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { styled } from '@linaria/react';
import { useContext } from 'react';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import { NavigationMenuItemDragContext } from '@/navigation-menu-item/contexts/NavigationMenuItemDragContext';
import { NavigationDrawerSectionForWorkspaceItemContent } from '@/object-metadata/components/NavigationDrawerSectionForWorkspaceItemContent';
import { WorkspaceOrphanDropTarget } from '@/object-metadata/components/WorkspaceOrphanDropTarget';
import { WorkspaceSectionAddMenuItemButton } from '@/object-metadata/components/WorkspaceSectionAddMenuItemButton';
import type { WorkspaceSectionListDndKitProps } from '@/object-metadata/components/WorkspaceSectionListDndKitProps';
const StyledList = styled.div`
display: flex;
flex-direction: column;
gap: ${themeCssVariables.betweenSiblingsGap};
padding-top: ${themeCssVariables.betweenSiblingsGap};
`;
const StyledListItemRow = styled.div`
display: flex;
flex-direction: column;
gap: 0;
`;
export const WorkspaceSectionListDndKit = ({
filteredItems,
getEditModeProps,
folderChildrenById,
selectedNavigationMenuItemId,
onNavigationMenuItemClick,
onActiveObjectMetadataItemClick,
}: WorkspaceSectionListDndKitProps) => {
const isNavigationMenuInEditMode = useAtomStateValue(
isNavigationMenuInEditModeState,
);
const workspaceDropDisabled = useIsDropDisabledForSection(true);
const { isDragging } = useContext(NavigationMenuItemDragContext);
const { addToNavigationFallbackDestination } = useContext(
NavigationDropTargetContext,
);
const folderCount = filteredItems.filter(
(item) => item.type === NavigationMenuItemType.FOLDER,
).length;
const isAddMenuItemButtonVisible = isNavigationMenuInEditMode;
return (
<StyledList>
{filteredItems.map((item, index) => (
<StyledListItemRow key={item.id}>
<WorkspaceOrphanDropTarget index={index} compact />
<WorkspaceDndKitSortableItem
id={item.id}
index={index}
group={
NavigationMenuItemDroppableIds.WORKSPACE_ORPHAN_NAVIGATION_MENU_ITEMS
}
disabled={!isNavigationMenuInEditMode || workspaceDropDisabled}
>
<NavigationDrawerSectionForWorkspaceItemContent
item={item}
editModeProps={getEditModeProps(item)}
isDragging={isDragging}
folderChildrenById={folderChildrenById}
folderCount={folderCount}
selectedNavigationMenuItemId={selectedNavigationMenuItemId}
onNavigationMenuItemClick={onNavigationMenuItemClick}
onActiveObjectMetadataItemClick={onActiveObjectMetadataItemClick}
/>
</WorkspaceDndKitSortableItem>
</StyledListItemRow>
))}
<WorkspaceDndKitDroppableSlot
droppableId={
NavigationMenuItemDroppableIds.WORKSPACE_ORPHAN_NAVIGATION_MENU_ITEMS
}
index={filteredItems.length}
disabled={workspaceDropDisabled}
>
<WorkspaceOrphanDropTarget
index={filteredItems.length}
compact={!isAddMenuItemButtonVisible}
>
{isAddMenuItemButtonVisible && <WorkspaceSectionAddMenuItemButton />}
</WorkspaceOrphanDropTarget>
</WorkspaceDndKitDroppableSlot>
{addToNavigationFallbackDestination?.droppableId ===
NavigationMenuItemDroppableIds.WORKSPACE_ORPHAN_NAVIGATION_MENU_ITEMS &&
addToNavigationFallbackDestination.index > filteredItems.length && (
<WorkspaceDndKitDroppableSlot
droppableId={
NavigationMenuItemDroppableIds.WORKSPACE_ORPHAN_NAVIGATION_MENU_ITEMS
}
index={addToNavigationFallbackDestination.index}
disabled={workspaceDropDisabled}
>
<WorkspaceOrphanDropTarget
index={addToNavigationFallbackDestination.index}
compact
/>
</WorkspaceDndKitDroppableSlot>
)}
</StyledList>
);
};
@@ -1,52 +0,0 @@
import { styled } from '@linaria/react';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import type { NavigationMenuItem } from '~/generated-metadata/graphql';
import { NavigationMenuItemType } from 'twenty-shared/types';
import type { WorkspaceSectionListDndKitProps } from '@/object-metadata/components/WorkspaceSectionListDndKitProps';
import { NavigationDrawerSectionForWorkspaceItemContent } from '@/object-metadata/components/NavigationDrawerSectionForWorkspaceItemContent';
const StyledList = styled.div`
display: flex;
flex-direction: column;
gap: ${themeCssVariables.betweenSiblingsGap};
padding-top: ${themeCssVariables.betweenSiblingsGap};
`;
type NavigationDrawerSectionForWorkspaceItemsListReadOnlyProps = Pick<
WorkspaceSectionListDndKitProps,
'filteredItems' | 'folderChildrenById' | 'onActiveObjectMetadataItemClick'
>;
const READ_ONLY_EDIT_MODE_PROPS = {
isSelectedInEditMode: false,
onEditModeClick: undefined,
} as const;
export const NavigationDrawerSectionForWorkspaceItemsListReadOnly = ({
filteredItems,
folderChildrenById,
onActiveObjectMetadataItemClick,
}: NavigationDrawerSectionForWorkspaceItemsListReadOnlyProps) => {
const folderCount = filteredItems.filter(
(item) => item.type === NavigationMenuItemType.FOLDER,
).length;
return (
<StyledList>
{filteredItems.map((item: NavigationMenuItem) => (
<NavigationDrawerSectionForWorkspaceItemContent
key={item.id}
item={item}
editModeProps={READ_ONLY_EDIT_MODE_PROPS}
isDragging={false}
folderChildrenById={folderChildrenById}
folderCount={folderCount}
selectedNavigationMenuItemId={null}
onActiveObjectMetadataItemClick={onActiveObjectMetadataItemClick}
readOnly
/>
))}
</StyledList>
);
};
@@ -1,120 +0,0 @@
import { styled } from '@linaria/react';
import { IconChevronDown, IconChevronRight, useIcons } from 'twenty-ui/display';
import { AnimatedExpandableContainer } from 'twenty-ui/layout';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import { useIsMobile } from 'twenty-ui/utilities';
import { useState } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
import { WorkspaceNavigationMenuItemFolderSubItem } from '@/navigation-menu-item/components/WorkspaceNavigationMenuItemFolderSubItem';
import { FOLDER_ICON_DEFAULT } from '@/navigation-menu-item/constants/FolderIconDefault';
import { DEFAULT_NAVIGATION_MENU_ITEM_COLOR_FOLDER } from '@/navigation-menu-item/constants/NavigationMenuItemDefaultColorFolder';
import { useWorkspaceFolderOpenState } from '@/navigation-menu-item/hooks/useWorkspaceFolderOpenState';
import { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem';
import { NavigationDrawerItemsCollapsableContainer } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItemsCollapsableContainer';
const StyledFolderContainer = styled.div`
border: 1px solid transparent;
border-radius: ${themeCssVariables.border.radius.sm};
`;
const StyledFolderContent = styled.div`
display: flex;
flex-direction: column;
`;
const StyledFolderExpandableWrapper = styled.div`
& > div {
overflow: visible !important;
}
`;
type WorkspaceFolderReadOnlyProps = {
folderId: string;
folderName: string;
folderIconKey?: string | null;
folderColor?: string | null;
navigationMenuItems: NavigationMenuItem[];
isGroup: boolean;
};
export const WorkspaceFolderReadOnly = ({
folderId,
folderName,
folderIconKey,
folderColor,
navigationMenuItems,
isGroup,
}: WorkspaceFolderReadOnlyProps) => {
const { getIcon } = useIcons();
const FolderIcon = getIcon(folderIconKey ?? FOLDER_ICON_DEFAULT);
const isMobile = useIsMobile();
const { isOpen, handleToggle, selectedNavigationMenuItemIndex } =
useWorkspaceFolderOpenState({ folderId, navigationMenuItems });
const [skipInitialExpandAnimation] = useState(() => isOpen);
return (
<StyledFolderContainer>
<NavigationDrawerItemsCollapsableContainer isGroup={isGroup}>
<NavigationDrawerItem
label={folderName}
Icon={FolderIcon}
iconColor={
isDefined(folderColor)
? folderColor
: DEFAULT_NAVIGATION_MENU_ITEM_COLOR_FOLDER
}
active={!isOpen && selectedNavigationMenuItemIndex >= 0}
onClick={handleToggle}
className="navigation-drawer-item"
triggerEvent="CLICK"
preventCollapseOnMobile={isMobile}
alwaysShowRightOptions
rightOptions={
isOpen ? (
<IconChevronDown
size={themeCssVariables.icon.size.sm}
stroke={themeCssVariables.icon.stroke.sm}
color={themeCssVariables.font.color.tertiary}
/>
) : (
<IconChevronRight
size={themeCssVariables.icon.size.sm}
stroke={themeCssVariables.icon.stroke.sm}
color={themeCssVariables.font.color.tertiary}
/>
)
}
/>
<StyledFolderExpandableWrapper>
<AnimatedExpandableContainer
isExpanded={isOpen}
dimension="height"
mode="fit-content"
containAnimation
initial={!skipInitialExpandAnimation}
>
<StyledFolderContent>
{navigationMenuItems.map((navigationMenuItem, index) => (
<WorkspaceNavigationMenuItemFolderSubItem
key={navigationMenuItem.id}
navigationMenuItem={navigationMenuItem}
index={index}
arrayLength={navigationMenuItems.length}
selectedNavigationMenuItemIndex={
selectedNavigationMenuItemIndex
}
onNavigationMenuItemClick={undefined}
selectedNavigationMenuItemId={null}
isContextDragging={false}
/>
))}
</StyledFolderContent>
</AnimatedExpandableContainer>
</StyledFolderExpandableWrapper>
</NavigationDrawerItemsCollapsableContainer>
</StyledFolderContainer>
);
};
@@ -1,31 +0,0 @@
import type { ReactNode } from 'react';
import { NavigationItemDropTarget } from '@/navigation-menu-item/components/NavigationItemDropTarget';
import { NavigationSections } from '@/navigation-menu-item/constants/NavigationSections.constants';
import { NavigationMenuItemDroppableIds } from '@/navigation-menu-item/constants/NavigationMenuItemDroppableIds';
import { getDndKitDropTargetId } from '@/navigation-menu-item/utils/getDndKitDropTargetId';
type WorkspaceOrphanDropTargetProps = {
index: number;
compact?: boolean;
children?: ReactNode;
};
export const WorkspaceOrphanDropTarget = ({
index,
compact = false,
children,
}: WorkspaceOrphanDropTargetProps) => (
<NavigationItemDropTarget
folderId={null}
index={index}
sectionId={NavigationSections.WORKSPACE}
compact={compact}
dropTargetIdOverride={getDndKitDropTargetId(
NavigationMenuItemDroppableIds.WORKSPACE_ORPHAN_NAVIGATION_MENU_ITEMS,
index,
)}
>
{children}
</NavigationItemDropTarget>
);
@@ -1,55 +0,0 @@
import { useLingui } from '@lingui/react/macro';
import React from 'react';
import { SidePanelPages } from 'twenty-shared/types';
import { IconColumnInsertRight, IconPlus } from 'twenty-ui/display';
import { addMenuItemInsertionContextState } from '@/navigation-menu-item/states/addMenuItemInsertionContextState';
import { selectedNavigationMenuItemInEditModeState } from '@/navigation-menu-item/states/selectedNavigationMenuItemInEditModeState';
import { useNavigateSidePanel } from '@/side-panel/hooks/useNavigateSidePanel';
import { sidePanelPageState } from '@/side-panel/states/sidePanelPageState';
import { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
export const WorkspaceSectionAddMenuItemButton = () => {
const { t } = useLingui();
const { navigateSidePanel } = useNavigateSidePanel();
const sidePanelPage = useAtomStateValue(sidePanelPageState);
const addMenuItemInsertionContext = useAtomStateValue(
addMenuItemInsertionContextState,
);
const setAddMenuItemInsertionContext = useSetAtomState(
addMenuItemInsertionContextState,
);
const setSelectedNavigationMenuItemInEditMode = useSetAtomState(
selectedNavigationMenuItemInEditModeState,
);
const handleClick = (event?: React.MouseEvent) => {
event?.stopPropagation();
setAddMenuItemInsertionContext(null);
setSelectedNavigationMenuItemInEditMode(null);
navigateSidePanel({
page: SidePanelPages.NavigationMenuAddItem,
pageTitle: t`New sidebar item`,
pageIcon: IconColumnInsertRight,
resetNavigationStack: true,
});
};
const isSelected =
sidePanelPage === SidePanelPages.NavigationMenuAddItem &&
addMenuItemInsertionContext === null;
return (
<NavigationDrawerItem
Icon={IconPlus}
label={t`Add menu item`}
onClick={handleClick}
triggerEvent="CLICK"
variant="tertiary"
isSelectedInEditMode={isSelected}
/>
);
};
@@ -1,20 +0,0 @@
import type { NavigationMenuItemClickParams } from '@/navigation-menu-item/hooks/useWorkspaceSectionItems';
import type { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import type { NavigationMenuItem } from '~/generated-metadata/graphql';
import type { EditModeProps } from '@/object-metadata/components/EditModeProps';
export type WorkspaceSectionItemContentProps = {
item: NavigationMenuItem;
editModeProps: EditModeProps;
isDragging: boolean;
folderChildrenById: Map<string, NavigationMenuItem[]>;
folderCount: number;
selectedNavigationMenuItemId: string | null;
onNavigationMenuItemClick?: (params: NavigationMenuItemClickParams) => void;
onActiveObjectMetadataItemClick?: (
objectMetadataItem: ObjectMetadataItem,
navigationMenuItemId: string,
) => void;
readOnly?: boolean;
};
@@ -1,17 +0,0 @@
import type { NavigationMenuItemClickParams } from '@/navigation-menu-item/hooks/useWorkspaceSectionItems';
import type { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import type { NavigationMenuItem } from '~/generated-metadata/graphql';
import type { EditModeProps } from '@/object-metadata/components/EditModeProps';
export type WorkspaceSectionListDndKitProps = {
filteredItems: NavigationMenuItem[];
getEditModeProps: (item: NavigationMenuItem) => EditModeProps;
folderChildrenById: Map<string, NavigationMenuItem[]>;
selectedNavigationMenuItemId: string | null;
onNavigationMenuItemClick?: (params: NavigationMenuItemClickParams) => void;
onActiveObjectMetadataItemClick?: (
objectMetadataItem: ObjectMetadataItem,
navigationMenuItemId: string,
) => void;
};
@@ -1,8 +1,8 @@
import { styled } from '@linaria/react';
import { NavigationDrawerSectionForWorkspaceItemsListReadOnly } from '@/object-metadata/components/NavigationDrawerSectionForWorkspaceItemsListReadOnly';
import { WorkspaceSectionAddMenuItemButton } from '@/object-metadata/components/WorkspaceSectionAddMenuItemButton';
import type { WorkspaceSectionListDndKitProps } from '@/object-metadata/components/WorkspaceSectionListDndKitProps';
import { NavigationDrawerSectionForWorkspaceItemsListReadOnly } from '@/navigation-menu-item/display/sections/components/NavigationDrawerSectionForWorkspaceItemsListReadOnly';
import { WorkspaceSectionAddMenuItemButton } from '@/navigation-menu-item/edit/components/WorkspaceSectionAddMenuItemButton';
import type { WorkspaceSectionListDndKitProps } from '@/navigation-menu-item/display/sections/types/WorkspaceSectionListDndKitProps';
import { themeCssVariables } from 'twenty-ui/theme-constants';
const StyledFallback = styled.div`