refactor(twenty-front): unify Favorites and Workspace navigation menu item code (#18697)
## Summary - Consolidate duplicated Favorites and Workspace navigation menu item frontend code into a single unified codebase within `navigation-menu-item/` - Move all DnD-related code from `navigation/` module into `navigation-menu-item/display/dnd/`, renaming `workspaceDndKit*` files to `navigationMenuItemDndKit*` - Unify duplicated components (DnD providers, DnD hooks, folder components, orphan items, section shell) using a `NavigationSections` enum to parameterize section-specific behavior - Rename residual workspace-prefixed symbols (`WorkspaceDndKitSortableItem`, `WorkspaceDndKitDroppableSlot`, `useWorkspaceSectionItems`, etc.) to `navigationMenuItem`-prefixed equivalents - Clean `navigation/` module to only contain app-level concerns (drawer layout, settings, routing) ### Key changes | Before (duplicated) | After (unified) | |---|---| | `FavoritesDndKitProvider` + `WorkspaceDndKitProvider` | `NavigationMenuItemDndKitProvider` with `section` prop | | `useFavoritesDndKit` + `useWorkspaceDndKit` | `useNavigationMenuItemDndKit(section)` | | `FavoritesFolderItem` + `WorkspaceNavigationMenuItemsFolder` | `NavigationMenuItemFolder` with `section` prop | | `FavoritesOrphanItems` | `NavigationMenuItemOrphanItems` with `section` prop | | Separate section shells | Shared `NavigationMenuItemSection` with thin wrappers | | `WorkspaceDndKitSortableItem` | `NavigationMenuItemSortableItem` | | `WorkspaceDndKitDroppableSlot` | `NavigationMenuItemDroppableSlot` | | `useWorkspaceSectionItems` | `useNavigationMenuItemSectionItems` | | `useWorkspaceFolderOpenState` | `useNavigationMenuItemFolderOpenState` | Net result: **~1650 lines deleted** across 51 files. ## Test plan - [x] `npx nx typecheck twenty-front` passes - [x] `npx nx lint twenty-front` passes (0 errors) - [x] `npx nx test twenty-front` passes (763 suites, 4467 tests) - [ ] Smoke test: Favorites section renders, DnD reorder works, folder create/rename/delete works - [ ] Smoke test: Workspace section renders, edit mode works, DnD reorder works - [ ] Smoke test: Add-to-navigation from side panel works for both sections Made with [Cursor](https://cursor.com)
This commit is contained in:
+6
-1
@@ -1,5 +1,10 @@
|
||||
export enum NavigationMenuItemDroppableIds {
|
||||
ORPHAN_NAVIGATION_MENU_ITEMS = 'orphan-navigation-menu-items',
|
||||
FAVORITE_DROPPABLE_PREFIX = 'favorite-',
|
||||
FAVORITE_ORPHAN_NAVIGATION_MENU_ITEMS = 'favorite-orphan-navigation-menu-items',
|
||||
FAVORITE_FOLDER_PREFIX = 'favorite-folder-',
|
||||
FAVORITE_FOLDER_HEADER_PREFIX = 'favorite-folder-header-',
|
||||
|
||||
WORKSPACE_DROPPABLE_PREFIX = 'workspace-',
|
||||
WORKSPACE_ORPHAN_NAVIGATION_MENU_ITEMS = 'workspace-orphan-navigation-menu-items',
|
||||
WORKSPACE_FOLDER_PREFIX = 'workspace-folder-',
|
||||
WORKSPACE_FOLDER_HEADER_PREFIX = 'workspace-folder-header-',
|
||||
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
import { NavigationMenuItemDroppableIds } from '@/navigation-menu-item/common/constants/NavigationMenuItemDroppableIds';
|
||||
import type { NavigationMenuItemSection } from '@/navigation-menu-item/common/types/NavigationMenuItemSection';
|
||||
|
||||
type SectionDroppableConfig = {
|
||||
orphanDroppableId: string;
|
||||
folderHeaderPrefix: string;
|
||||
folderPrefix: string;
|
||||
};
|
||||
|
||||
export const NAVIGATION_MENU_ITEM_SECTION_DROPPABLE_CONFIG: Record<
|
||||
NavigationMenuItemSection,
|
||||
SectionDroppableConfig
|
||||
> = {
|
||||
favorite: {
|
||||
orphanDroppableId:
|
||||
NavigationMenuItemDroppableIds.FAVORITE_ORPHAN_NAVIGATION_MENU_ITEMS,
|
||||
folderHeaderPrefix:
|
||||
NavigationMenuItemDroppableIds.FAVORITE_FOLDER_HEADER_PREFIX,
|
||||
folderPrefix: NavigationMenuItemDroppableIds.FAVORITE_FOLDER_PREFIX,
|
||||
},
|
||||
workspace: {
|
||||
orphanDroppableId:
|
||||
NavigationMenuItemDroppableIds.WORKSPACE_ORPHAN_NAVIGATION_MENU_ITEMS,
|
||||
folderHeaderPrefix:
|
||||
NavigationMenuItemDroppableIds.WORKSPACE_FOLDER_HEADER_PREFIX,
|
||||
folderPrefix: NavigationMenuItemDroppableIds.WORKSPACE_FOLDER_PREFIX,
|
||||
},
|
||||
};
|
||||
+1
@@ -10,6 +10,7 @@ export const useUpdateNavigationMenuItem = () => {
|
||||
UpdateNavigationMenuItemDocument,
|
||||
{
|
||||
refetchQueries: ['FindManyNavigationMenuItems'],
|
||||
awaitRefetchQueries: false,
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
+1
@@ -0,0 +1 @@
|
||||
export type NavigationMenuItemSection = 'workspace' | 'favorite';
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import type { DropDestination } from '@/navigation/types/workspaceDndKitDropDestination';
|
||||
import type { DropDestination } from '@/navigation-menu-item/common/types/navigationMenuItemDndKitDropDestination';
|
||||
|
||||
export type SortableTargetDestination = {
|
||||
destination: DropDestination;
|
||||
+97
-25
@@ -1,37 +1,109 @@
|
||||
import { getDropTargetIdFromDestination } from '@/navigation-menu-item/common/utils/getDropTargetIdFromDestination';
|
||||
|
||||
describe('getDropTargetIdFromDestination', () => {
|
||||
it('should return null when destination is null or droppableId is not workspace', () => {
|
||||
expect(getDropTargetIdFromDestination(null)).toBe(null);
|
||||
expect(
|
||||
getDropTargetIdFromDestination({
|
||||
droppableId: 'favorites-orphan',
|
||||
index: 0,
|
||||
}),
|
||||
).toBe(null);
|
||||
describe('workspace section', () => {
|
||||
it('should return null when destination is null or droppableId is not workspace', () => {
|
||||
expect(
|
||||
getDropTargetIdFromDestination({
|
||||
navigationMenuItemSection: 'workspace',
|
||||
destination: null,
|
||||
}),
|
||||
).toBe(null);
|
||||
expect(
|
||||
getDropTargetIdFromDestination({
|
||||
navigationMenuItemSection: 'workspace',
|
||||
destination: { droppableId: 'favorite-orphan', index: 0 },
|
||||
}),
|
||||
).toBe(null);
|
||||
});
|
||||
|
||||
it('should return workspace-orphan-index for workspace orphan droppable', () => {
|
||||
expect(
|
||||
getDropTargetIdFromDestination({
|
||||
navigationMenuItemSection: 'workspace',
|
||||
destination: {
|
||||
droppableId: 'workspace-orphan-navigation-menu-items',
|
||||
index: 2,
|
||||
},
|
||||
}),
|
||||
).toBe('workspace-orphan-2');
|
||||
});
|
||||
|
||||
it('should return workspace-folderId-index for workspace folder droppable', () => {
|
||||
expect(
|
||||
getDropTargetIdFromDestination({
|
||||
navigationMenuItemSection: 'workspace',
|
||||
destination: {
|
||||
droppableId: 'workspace-folder-folder-123',
|
||||
index: 1,
|
||||
},
|
||||
}),
|
||||
).toBe('workspace-folder-123-1');
|
||||
});
|
||||
|
||||
it('should return workspace-folderId-index for workspace folder header droppable', () => {
|
||||
expect(
|
||||
getDropTargetIdFromDestination({
|
||||
navigationMenuItemSection: 'workspace',
|
||||
destination: {
|
||||
droppableId: 'workspace-folder-header-folder-456',
|
||||
index: 0,
|
||||
},
|
||||
}),
|
||||
).toBe('workspace-folder-456-0');
|
||||
});
|
||||
});
|
||||
|
||||
it('should return workspace-orphan-index for workspace orphan droppable', () => {
|
||||
const result = getDropTargetIdFromDestination({
|
||||
droppableId: 'workspace-orphan-navigation-menu-items',
|
||||
index: 2,
|
||||
describe('favorite section', () => {
|
||||
it('should return null when destination is null or droppableId is not favorite', () => {
|
||||
expect(
|
||||
getDropTargetIdFromDestination({
|
||||
navigationMenuItemSection: 'favorite',
|
||||
destination: null,
|
||||
}),
|
||||
).toBe(null);
|
||||
expect(
|
||||
getDropTargetIdFromDestination({
|
||||
navigationMenuItemSection: 'favorite',
|
||||
destination: { droppableId: 'workspace-orphan', index: 0 },
|
||||
}),
|
||||
).toBe(null);
|
||||
});
|
||||
expect(result).toBe('workspace-orphan-2');
|
||||
});
|
||||
|
||||
it('should return workspace-folderId-index for workspace folder droppable', () => {
|
||||
const result = getDropTargetIdFromDestination({
|
||||
droppableId: 'workspace-folder-folder-123',
|
||||
index: 1,
|
||||
it('should return favorite-orphan-index for favorite orphan droppable', () => {
|
||||
expect(
|
||||
getDropTargetIdFromDestination({
|
||||
navigationMenuItemSection: 'favorite',
|
||||
destination: {
|
||||
droppableId: 'favorite-orphan-navigation-menu-items',
|
||||
index: 3,
|
||||
},
|
||||
}),
|
||||
).toBe('favorite-orphan-3');
|
||||
});
|
||||
expect(result).toBe('workspace-folder-123-1');
|
||||
});
|
||||
|
||||
it('should return workspace-folderId-index for workspace folder header droppable', () => {
|
||||
const result = getDropTargetIdFromDestination({
|
||||
droppableId: 'workspace-folder-header-folder-456',
|
||||
index: 0,
|
||||
it('should return favorite-folderId-index for favorite folder droppable', () => {
|
||||
expect(
|
||||
getDropTargetIdFromDestination({
|
||||
navigationMenuItemSection: 'favorite',
|
||||
destination: {
|
||||
droppableId: 'favorite-folder-abc-123',
|
||||
index: 1,
|
||||
},
|
||||
}),
|
||||
).toBe('favorite-abc-123-1');
|
||||
});
|
||||
|
||||
it('should return favorite-folderId-index for favorite folder header droppable', () => {
|
||||
expect(
|
||||
getDropTargetIdFromDestination({
|
||||
navigationMenuItemSection: 'favorite',
|
||||
destination: {
|
||||
droppableId: 'favorite-folder-header-def-456',
|
||||
index: 0,
|
||||
},
|
||||
}),
|
||||
).toBe('favorite-def-456-0');
|
||||
});
|
||||
expect(result).toBe('workspace-folder-456-0');
|
||||
});
|
||||
});
|
||||
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { NavigationMenuItemDroppableIds } from '@/navigation-menu-item/common/constants/NavigationMenuItemDroppableIds';
|
||||
import type { NavigationMenuItemSection } from '@/navigation-menu-item/common/types/NavigationMenuItemSection';
|
||||
|
||||
export const canNavigationMenuItemBeDroppedIn = ({
|
||||
navigationMenuItemSection,
|
||||
droppableId,
|
||||
}: {
|
||||
navigationMenuItemSection: NavigationMenuItemSection;
|
||||
droppableId: string | null | undefined;
|
||||
}): boolean => {
|
||||
if (!isDefined(droppableId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const prefix =
|
||||
navigationMenuItemSection === 'workspace'
|
||||
? NavigationMenuItemDroppableIds.WORKSPACE_DROPPABLE_PREFIX
|
||||
: NavigationMenuItemDroppableIds.FAVORITE_DROPPABLE_PREFIX;
|
||||
|
||||
return droppableId.startsWith(prefix);
|
||||
};
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
import { NAVIGATION_MENU_ITEM_SECTION_DROPPABLE_CONFIG } from '@/navigation-menu-item/common/constants/NavigationMenuItemSectionDroppableConfig';
|
||||
import type { NavigationMenuItemSection } from '@/navigation-menu-item/common/types/NavigationMenuItemSection';
|
||||
|
||||
export const extractFolderIdFromDroppableId = (
|
||||
droppableId: string,
|
||||
navigationMenuItemSection: NavigationMenuItemSection,
|
||||
): string | null => {
|
||||
const config =
|
||||
NAVIGATION_MENU_ITEM_SECTION_DROPPABLE_CONFIG[navigationMenuItemSection];
|
||||
|
||||
if (droppableId === config.orphanDroppableId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (droppableId.startsWith(config.folderHeaderPrefix)) {
|
||||
return droppableId.slice(config.folderHeaderPrefix.length) || null;
|
||||
}
|
||||
|
||||
if (droppableId.startsWith(config.folderPrefix)) {
|
||||
return droppableId.slice(config.folderPrefix.length) || null;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
+37
-10
@@ -1,15 +1,42 @@
|
||||
import { NavigationSections } from '@/navigation-menu-item/common/constants/NavigationSections.constants';
|
||||
import { isWorkspaceDroppableId } from '@/navigation-menu-item/common/utils/isWorkspaceDroppableId';
|
||||
import { validateAndExtractWorkspaceFolderId } from '@/navigation-menu-item/common/utils/validateAndExtractWorkspaceFolderId';
|
||||
import { NAVIGATION_MENU_ITEM_SECTION_DROPPABLE_CONFIG } from '@/navigation-menu-item/common/constants/NavigationMenuItemSectionDroppableConfig';
|
||||
import type { NavigationMenuItemSection } from '@/navigation-menu-item/common/types/NavigationMenuItemSection';
|
||||
import { canNavigationMenuItemBeDroppedIn } from '@/navigation-menu-item/common/utils/canNavigationMenuItemBeDroppedIn';
|
||||
import type { DropResult } from '@hello-pangea/dnd';
|
||||
|
||||
export const getDropTargetIdFromDestination = (
|
||||
destination: DropResult['destination'],
|
||||
): string | null => {
|
||||
if (!destination || !isWorkspaceDroppableId(destination.droppableId)) {
|
||||
export const getDropTargetIdFromDestination = ({
|
||||
navigationMenuItemSection,
|
||||
destination,
|
||||
}: {
|
||||
navigationMenuItemSection: NavigationMenuItemSection;
|
||||
destination: DropResult['destination'];
|
||||
}): string | null => {
|
||||
if (
|
||||
!destination ||
|
||||
!canNavigationMenuItemBeDroppedIn({
|
||||
navigationMenuItemSection,
|
||||
droppableId: destination.droppableId,
|
||||
})
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
const folderId = validateAndExtractWorkspaceFolderId(destination.droppableId);
|
||||
const folderSegment = folderId ?? 'orphan';
|
||||
return `${NavigationSections.WORKSPACE}-${folderSegment}-${destination.index}`;
|
||||
|
||||
const config =
|
||||
NAVIGATION_MENU_ITEM_SECTION_DROPPABLE_CONFIG[navigationMenuItemSection];
|
||||
const { droppableId, index } = destination;
|
||||
|
||||
if (droppableId === config.orphanDroppableId) {
|
||||
return `${navigationMenuItemSection}-orphan-${index}`;
|
||||
}
|
||||
|
||||
if (droppableId.startsWith(config.folderHeaderPrefix)) {
|
||||
const folderId = droppableId.slice(config.folderHeaderPrefix.length);
|
||||
return `${navigationMenuItemSection}-${folderId}-${index}`;
|
||||
}
|
||||
|
||||
if (droppableId.startsWith(config.folderPrefix)) {
|
||||
const folderId = droppableId.slice(config.folderPrefix.length);
|
||||
return `${navigationMenuItemSection}-${folderId}-${index}`;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
-22
@@ -1,22 +0,0 @@
|
||||
import { NavigationMenuItemDroppableIds } from '@/navigation-menu-item/common/constants/NavigationMenuItemDroppableIds';
|
||||
import { NavigationSections } from '@/navigation-menu-item/common/constants/NavigationSections.constants';
|
||||
import type { DropResult } from '@hello-pangea/dnd';
|
||||
|
||||
const FOLDER_PREFIX = 'folder-';
|
||||
|
||||
export const getFavoritesDropTargetIdFromDestination = (
|
||||
destination: DropResult['destination'],
|
||||
): string | null => {
|
||||
if (!destination) return null;
|
||||
const { droppableId, index } = destination;
|
||||
if (
|
||||
droppableId === NavigationMenuItemDroppableIds.ORPHAN_NAVIGATION_MENU_ITEMS
|
||||
) {
|
||||
return `${NavigationSections.FAVORITES}-orphan-${index}`;
|
||||
}
|
||||
if (droppableId.startsWith(FOLDER_PREFIX)) {
|
||||
const folderId = droppableId.slice(FOLDER_PREFIX.length);
|
||||
return `${NavigationSections.FAVORITES}-${folderId}-${index}`;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
-21
@@ -1,21 +0,0 @@
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { NavigationMenuItemDroppableIds } from '@/navigation-menu-item/common/constants/NavigationMenuItemDroppableIds';
|
||||
|
||||
export const isWorkspaceDroppableId = (
|
||||
droppableId: string | null | undefined,
|
||||
): boolean => {
|
||||
if (!isDefined(droppableId)) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
droppableId ===
|
||||
NavigationMenuItemDroppableIds.WORKSPACE_ORPHAN_NAVIGATION_MENU_ITEMS ||
|
||||
droppableId.startsWith(
|
||||
NavigationMenuItemDroppableIds.WORKSPACE_FOLDER_PREFIX,
|
||||
) ||
|
||||
droppableId.startsWith(
|
||||
NavigationMenuItemDroppableIds.WORKSPACE_FOLDER_HEADER_PREFIX,
|
||||
)
|
||||
);
|
||||
};
|
||||
+11
-4
@@ -1,17 +1,19 @@
|
||||
import { NavigationMenuItemType } from 'twenty-shared/types';
|
||||
import { NavigationMenuItemFolderDisplay } from '@/navigation-menu-item/display/folder/components/NavigationMenuItemFolderDisplay';
|
||||
import { NavigationMenuItemFolder } from '@/navigation-menu-item/display/folder/components/NavigationMenuItemFolder';
|
||||
import { NavigationMenuItemLinkDisplay } from '@/navigation-menu-item/display/link/components/NavigationMenuItemLinkDisplay';
|
||||
import { NavigationMenuItemObjectDisplay } from '@/navigation-menu-item/display/object/components/NavigationMenuItemObjectDisplay';
|
||||
import type { WorkspaceSectionItemContentProps } from '@/navigation-menu-item/display/sections/types/WorkspaceSectionItemContentProps';
|
||||
import type { NavigationMenuItemSectionContentProps } from '@/navigation-menu-item/display/sections/types/NavigationMenuItemSectionContentProps';
|
||||
|
||||
type NavigationMenuItemDisplayProps = WorkspaceSectionItemContentProps;
|
||||
type NavigationMenuItemDisplayProps = NavigationMenuItemSectionContentProps;
|
||||
|
||||
export const NavigationMenuItemDisplay = ({
|
||||
item,
|
||||
isEditInPlace,
|
||||
editModeProps,
|
||||
isDragging,
|
||||
folderChildrenById,
|
||||
folderCount,
|
||||
rightOptions,
|
||||
selectedNavigationMenuItemId,
|
||||
onNavigationMenuItemClick,
|
||||
onActiveObjectMetadataItemClick,
|
||||
@@ -20,8 +22,9 @@ export const NavigationMenuItemDisplay = ({
|
||||
switch (item.type) {
|
||||
case NavigationMenuItemType.FOLDER:
|
||||
return (
|
||||
<NavigationMenuItemFolderDisplay
|
||||
<NavigationMenuItemFolder
|
||||
item={item}
|
||||
isEditInPlace={isEditInPlace}
|
||||
editModeProps={editModeProps}
|
||||
isDragging={isDragging}
|
||||
folderChildrenById={folderChildrenById}
|
||||
@@ -35,10 +38,12 @@ export const NavigationMenuItemDisplay = ({
|
||||
return (
|
||||
<NavigationMenuItemLinkDisplay
|
||||
item={item}
|
||||
isEditInPlace={isEditInPlace}
|
||||
editModeProps={editModeProps}
|
||||
isDragging={isDragging}
|
||||
folderChildrenById={folderChildrenById}
|
||||
folderCount={folderCount}
|
||||
rightOptions={rightOptions}
|
||||
selectedNavigationMenuItemId={selectedNavigationMenuItemId}
|
||||
onNavigationMenuItemClick={onNavigationMenuItemClick}
|
||||
onActiveObjectMetadataItemClick={onActiveObjectMetadataItemClick}
|
||||
@@ -49,10 +54,12 @@ export const NavigationMenuItemDisplay = ({
|
||||
return (
|
||||
<NavigationMenuItemObjectDisplay
|
||||
item={item}
|
||||
isEditInPlace={isEditInPlace}
|
||||
editModeProps={editModeProps}
|
||||
isDragging={isDragging}
|
||||
folderChildrenById={folderChildrenById}
|
||||
folderCount={folderCount}
|
||||
rightOptions={rightOptions}
|
||||
selectedNavigationMenuItemId={selectedNavigationMenuItemId}
|
||||
onNavigationMenuItemClick={onNavigationMenuItemClick}
|
||||
onActiveObjectMetadataItemClick={onActiveObjectMetadataItemClick}
|
||||
|
||||
+4
-4
@@ -4,7 +4,7 @@ import { type ReactNode } from 'react';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
import { getDndKitDropTargetId } from '@/navigation-menu-item/common/utils/getDndKitDropTargetId';
|
||||
import type { DroppableData } from '@/navigation/types/workspaceDndKitDroppableData';
|
||||
import type { DroppableData } from '@/navigation-menu-item/common/types/navigationMenuItemDndKitDroppableData';
|
||||
|
||||
const StyledSlotWrapper = styled.div<{ $empty: boolean }>`
|
||||
min-height: 0;
|
||||
@@ -16,7 +16,7 @@ const SLOT_COLLISION_PRIORITY = 1;
|
||||
|
||||
export const FOLDER_HEADER_SLOT_COLLISION_PRIORITY = 2;
|
||||
|
||||
type WorkspaceDndKitDroppableSlotProps = {
|
||||
type NavigationMenuItemDroppableSlotProps = {
|
||||
droppableId: string;
|
||||
index: number;
|
||||
children?: ReactNode;
|
||||
@@ -24,13 +24,13 @@ type WorkspaceDndKitDroppableSlotProps = {
|
||||
collisionPriority?: number;
|
||||
};
|
||||
|
||||
export const WorkspaceDndKitDroppableSlot = ({
|
||||
export const NavigationMenuItemDroppableSlot = ({
|
||||
droppableId,
|
||||
index,
|
||||
children,
|
||||
disabled = false,
|
||||
collisionPriority = SLOT_COLLISION_PRIORITY,
|
||||
}: WorkspaceDndKitDroppableSlotProps) => {
|
||||
}: NavigationMenuItemDroppableSlotProps) => {
|
||||
const id = getDndKitDropTargetId(droppableId, index);
|
||||
const data: DroppableData = { droppableId, index };
|
||||
const { ref } = useDroppable({
|
||||
+3
-3
@@ -14,7 +14,7 @@ const StyledSortableRoot = styled.div`
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
type WorkspaceDndKitSortableItemProps = {
|
||||
type NavigationMenuItemSortableItemProps = {
|
||||
children: ReactNode;
|
||||
disabled?: boolean;
|
||||
group: string;
|
||||
@@ -22,13 +22,13 @@ type WorkspaceDndKitSortableItemProps = {
|
||||
index: number;
|
||||
};
|
||||
|
||||
export const WorkspaceDndKitSortableItem = ({
|
||||
export const NavigationMenuItemSortableItem = ({
|
||||
id,
|
||||
index,
|
||||
group,
|
||||
disabled = false,
|
||||
children,
|
||||
}: WorkspaceDndKitSortableItemProps) => {
|
||||
}: NavigationMenuItemSortableItemProps) => {
|
||||
const { handleRef, ref, targetRef } = useSortable({
|
||||
id,
|
||||
index,
|
||||
+5
-2
@@ -19,7 +19,7 @@ import { navigationMenuItemsDraftState } from '@/navigation-menu-item/common/sta
|
||||
import { openNavigationMenuItemFolderIdsState } from '@/navigation-menu-item/common/states/openNavigationMenuItemFolderIdsState';
|
||||
import { getObjectMetadataIdsInDraft } from '@/navigation-menu-item/common/utils/getObjectMetadataIdsInDraft';
|
||||
import { getStandardObjectIconColor } from '@/navigation-menu-item/common/utils/getStandardObjectIconColor';
|
||||
import { isWorkspaceDroppableId } from '@/navigation-menu-item/common/utils/isWorkspaceDroppableId';
|
||||
import { canNavigationMenuItemBeDroppedIn } from '@/navigation-menu-item/common/utils/canNavigationMenuItemBeDroppedIn';
|
||||
import { validateAndExtractWorkspaceFolderId } from '@/navigation-menu-item/common/utils/validateAndExtractWorkspaceFolderId';
|
||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
@@ -54,7 +54,10 @@ export const useHandleAddToNavigationDrop = () => {
|
||||
if (
|
||||
source.droppableId !== ADD_TO_NAV_SOURCE_DROPPABLE_ID ||
|
||||
!destination ||
|
||||
!isWorkspaceDroppableId(destination.droppableId)
|
||||
!canNavigationMenuItemBeDroppedIn({
|
||||
navigationMenuItemSection: 'workspace',
|
||||
droppableId: destination.droppableId,
|
||||
})
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
+17
-15
@@ -1,14 +1,13 @@
|
||||
import { type OnDragEndResponder } from '@hello-pangea/dnd';
|
||||
|
||||
import { NavigationMenuItemDroppableIds } from '@/navigation-menu-item/common/constants/NavigationMenuItemDroppableIds';
|
||||
import { isWorkspaceDroppableId } from '@/navigation-menu-item/common/utils/isWorkspaceDroppableId';
|
||||
import { canNavigationMenuItemBeDroppedIn } from '@/navigation-menu-item/common/utils/canNavigationMenuItemBeDroppedIn';
|
||||
import { useSortedNavigationMenuItems } from '@/navigation-menu-item/display/hooks/useSortedNavigationMenuItems';
|
||||
import { useUpdateNavigationMenuItem } from '@/navigation-menu-item/common/hooks/useUpdateNavigationMenuItem';
|
||||
import { openNavigationMenuItemFolderIdsState } from '@/navigation-menu-item/common/states/openNavigationMenuItemFolderIdsState';
|
||||
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
|
||||
import { calculateNewPosition } from '@/ui/layout/draggable-list/utils/calculateNewPosition';
|
||||
import { FOLDER_DROPPABLE_IDS } from '@/ui/layout/draggable-list/utils/folderDroppableIds';
|
||||
import { validateAndExtractFolderId } from '@/ui/layout/draggable-list/utils/validateAndExtractFolderId';
|
||||
import { extractFolderIdFromDroppableId } from '@/navigation-menu-item/common/utils/extractFolderIdFromDroppableId';
|
||||
|
||||
import { useNavigationMenuItemsData } from '@/navigation-menu-item/display/hooks/useNavigationMenuItemsData';
|
||||
|
||||
@@ -49,7 +48,12 @@ export const useHandleNavigationMenuItemDragAndDrop = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isWorkspaceDroppableId(destination.droppableId)) {
|
||||
if (
|
||||
canNavigationMenuItemBeDroppedIn({
|
||||
navigationMenuItemSection: 'workspace',
|
||||
droppableId: destination.droppableId,
|
||||
})
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -60,20 +64,18 @@ export const useHandleNavigationMenuItemDragAndDrop = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const destinationFolderId = validateAndExtractFolderId({
|
||||
droppableId: destination.droppableId,
|
||||
orphanDroppableId:
|
||||
NavigationMenuItemDroppableIds.ORPHAN_NAVIGATION_MENU_ITEMS,
|
||||
});
|
||||
const sourceFolderId = validateAndExtractFolderId({
|
||||
droppableId: source.droppableId,
|
||||
orphanDroppableId:
|
||||
NavigationMenuItemDroppableIds.ORPHAN_NAVIGATION_MENU_ITEMS,
|
||||
});
|
||||
const destinationFolderId = extractFolderIdFromDroppableId(
|
||||
destination.droppableId,
|
||||
'favorite',
|
||||
);
|
||||
const sourceFolderId = extractFolderIdFromDroppableId(
|
||||
source.droppableId,
|
||||
'favorite',
|
||||
);
|
||||
|
||||
if (
|
||||
destination.droppableId.startsWith(
|
||||
FOLDER_DROPPABLE_IDS.FOLDER_HEADER_PREFIX,
|
||||
NavigationMenuItemDroppableIds.FAVORITE_FOLDER_HEADER_PREFIX,
|
||||
)
|
||||
) {
|
||||
if (destinationFolderId === null)
|
||||
|
||||
+6
-2
@@ -1,6 +1,6 @@
|
||||
import { ADD_TO_NAV_SOURCE_DROPPABLE_ID } from '@/navigation-menu-item/common/constants/AddToNavSourceDroppableId';
|
||||
import { NavigationDragSourceContext } from '@/navigation-menu-item/common/contexts/NavigationDragSourceContext';
|
||||
import { isWorkspaceDroppableId } from '@/navigation-menu-item/common/utils/isWorkspaceDroppableId';
|
||||
import { canNavigationMenuItemBeDroppedIn } from '@/navigation-menu-item/common/utils/canNavigationMenuItemBeDroppedIn';
|
||||
import { useContext } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
@@ -12,5 +12,9 @@ export const useIsDropDisabledForSection = (isWorkspaceSection: boolean) => {
|
||||
if (sourceDroppableId === ADD_TO_NAV_SOURCE_DROPPABLE_ID) {
|
||||
return !isWorkspaceSection;
|
||||
}
|
||||
return isWorkspaceDroppableId(sourceDroppableId) !== isWorkspaceSection;
|
||||
const sourceIsWorkspace = canNavigationMenuItemBeDroppedIn({
|
||||
navigationMenuItemSection: 'workspace',
|
||||
droppableId: sourceDroppableId,
|
||||
});
|
||||
return sourceIsWorkspace !== isWorkspaceSection;
|
||||
};
|
||||
|
||||
+426
@@ -0,0 +1,426 @@
|
||||
import { type DragDropProvider } from '@dnd-kit/react';
|
||||
import { isSortable } from '@dnd-kit/react/sortable';
|
||||
import type { ResponderProvided } from '@hello-pangea/dnd';
|
||||
import { useStore } from 'jotai';
|
||||
import { type ComponentProps, useCallback, useState } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { ADD_TO_NAV_SOURCE_DROPPABLE_ID } from '@/navigation-menu-item/common/constants/AddToNavSourceDroppableId';
|
||||
import { NavigationSections } from '@/navigation-menu-item/common/constants/NavigationSections.constants';
|
||||
import { addToNavPayloadRegistryState } from '@/navigation-menu-item/common/states/addToNavPayloadRegistryState';
|
||||
import type { DraggableData } from '@/navigation-menu-item/common/types/navigationMenuItemDndKitDraggableData';
|
||||
import type { DropDestination } from '@/navigation-menu-item/common/types/navigationMenuItemDndKitDropDestination';
|
||||
import type { NavigationMenuItemSection } from '@/navigation-menu-item/common/types/NavigationMenuItemSection';
|
||||
import { canNavigationMenuItemBeDroppedIn } from '@/navigation-menu-item/common/utils/canNavigationMenuItemBeDroppedIn';
|
||||
import { extractFolderIdFromDroppableId } from '@/navigation-menu-item/common/utils/extractFolderIdFromDroppableId';
|
||||
import { getDndKitDropTargetId } from '@/navigation-menu-item/common/utils/getDndKitDropTargetId';
|
||||
import { isNavigationMenuItemFolder } from '@/navigation-menu-item/common/utils/isNavigationMenuItemFolder';
|
||||
import { DROP_RESULT_OPTIONS } from '@/navigation-menu-item/display/dnd/constants/navigationMenuItemDndKitDropResultOptions';
|
||||
import { useHandleAddToNavigationDrop } from '@/navigation-menu-item/display/dnd/hooks/useHandleAddToNavigationDrop';
|
||||
import { useHandleNavigationMenuItemDragAndDrop } from '@/navigation-menu-item/display/dnd/hooks/useHandleNavigationMenuItemDragAndDrop';
|
||||
import { useHandleWorkspaceNavigationMenuItemDragAndDrop } from '@/navigation-menu-item/display/dnd/hooks/useHandleWorkspaceNavigationMenuItemDragAndDrop';
|
||||
import { resolveDropTarget } from '@/navigation-menu-item/display/dnd/utils/navigationMenuItemDndKitResolveDropTarget';
|
||||
import { toDropResult } from '@/navigation-menu-item/display/dnd/utils/navigationMenuItemDndKitToDropResult';
|
||||
import { useNavigationMenuItemsData } from '@/navigation-menu-item/display/hooks/useNavigationMenuItemsData';
|
||||
import { useSortedNavigationMenuItems } from '@/navigation-menu-item/display/hooks/useSortedNavigationMenuItems';
|
||||
import { useNavigationMenuItemsDraftState } from '@/navigation-menu-item/edit/hooks/useNavigationMenuItemsDraftState';
|
||||
|
||||
import { NAVIGATION_MENU_ITEM_SECTION_DROPPABLE_CONFIG } from '@/navigation-menu-item/common/constants/NavigationMenuItemSectionDroppableConfig';
|
||||
import type { SortableTargetDestination } from '@/navigation-menu-item/common/types/navigationMenuItemDndKitSortableTargetDestination';
|
||||
|
||||
type DragStartPayload = Parameters<
|
||||
NonNullable<
|
||||
ComponentProps<typeof DragDropProvider<DraggableData>>['onDragStart']
|
||||
>
|
||||
>[0];
|
||||
type DragOverPayload = Parameters<
|
||||
NonNullable<
|
||||
ComponentProps<typeof DragDropProvider<DraggableData>>['onDragOver']
|
||||
>
|
||||
>[0];
|
||||
type DragEndPayload = Parameters<
|
||||
NonNullable<
|
||||
ComponentProps<typeof DragDropProvider<DraggableData>>['onDragEnd']
|
||||
>
|
||||
>[0];
|
||||
|
||||
export type NavigationMenuItemDndKitContextValues = {
|
||||
dragSource: { sourceDroppableId: string | null };
|
||||
drag: { isDragging: boolean };
|
||||
dropTarget: {
|
||||
activeDropTargetId: string | null;
|
||||
setActiveDropTargetId: (id: string | null) => void;
|
||||
forbiddenDropTargetId: string | null;
|
||||
setForbiddenDropTargetId: (id: string | null) => void;
|
||||
addToNavigationFallbackDestination: DropDestination | null;
|
||||
};
|
||||
};
|
||||
|
||||
export const useNavigationMenuItemDndKit = (
|
||||
section: NavigationSections,
|
||||
): {
|
||||
contextValues: NavigationMenuItemDndKitContextValues;
|
||||
handlers: {
|
||||
onDragStart: (event: DragStartPayload) => void;
|
||||
onDragOver: (event: DragOverPayload) => void;
|
||||
onDragEnd: (event: DragEndPayload) => void;
|
||||
};
|
||||
} => {
|
||||
const sectionType: NavigationMenuItemSection =
|
||||
section === NavigationSections.FAVORITES ? 'favorite' : 'workspace';
|
||||
const isWorkspaceSection = sectionType === 'workspace';
|
||||
|
||||
const store = useStore();
|
||||
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [sourceDroppableId, setSourceDroppableId] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
const [activeDropTargetId, setActiveDropTargetId] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
const [forbiddenDropTargetId, setForbiddenDropTargetId] = useState<
|
||||
string | null
|
||||
>(null);
|
||||
const [
|
||||
addToNavigationFallbackDestination,
|
||||
setAddToNavigationFallbackDestination,
|
||||
] = useState<DropDestination | null>(null);
|
||||
|
||||
const { navigationMenuItems } = useNavigationMenuItemsData();
|
||||
const { navigationMenuItemsSorted } = useSortedNavigationMenuItems();
|
||||
const { workspaceNavigationMenuItems } = useNavigationMenuItemsDraftState();
|
||||
const { handleAddToNavigationDrop } = useHandleAddToNavigationDrop();
|
||||
const { handleNavigationMenuItemDragAndDrop } =
|
||||
useHandleNavigationMenuItemDragAndDrop();
|
||||
const { handleWorkspaceNavigationMenuItemDragAndDrop } =
|
||||
useHandleWorkspaceNavigationMenuItemDragAndDrop();
|
||||
|
||||
const items = isWorkspaceSection
|
||||
? workspaceNavigationMenuItems
|
||||
: navigationMenuItems;
|
||||
|
||||
const orphanItems = isWorkspaceSection
|
||||
? workspaceNavigationMenuItems
|
||||
: navigationMenuItemsSorted;
|
||||
|
||||
const orphanItemCount = orphanItems.filter(
|
||||
(item: { folderId?: string | null }) => !isDefined(item.folderId),
|
||||
).length;
|
||||
|
||||
const { orphanDroppableId: defaultOrphanDroppableId } =
|
||||
NAVIGATION_MENU_ITEM_SECTION_DROPPABLE_CONFIG[sectionType];
|
||||
|
||||
const getNavItemById = useCallback(
|
||||
(id: string | undefined) =>
|
||||
id ? items.find((item) => item.id === id) : undefined,
|
||||
[items],
|
||||
);
|
||||
|
||||
const getAddToNavPayload = useCallback(
|
||||
(sourceId: unknown) =>
|
||||
store.get(addToNavPayloadRegistryState.atom).get(String(sourceId)) ??
|
||||
null,
|
||||
[store],
|
||||
);
|
||||
|
||||
const isSourceFolderDrag = useCallback(
|
||||
(source: { id?: unknown; data?: unknown } | null): boolean => {
|
||||
const sourceItem = getNavItemById(
|
||||
source?.id != null ? String(source.id) : undefined,
|
||||
);
|
||||
if (isDefined(sourceItem) && isNavigationMenuItemFolder(sourceItem)) {
|
||||
return true;
|
||||
}
|
||||
const payload = getAddToNavPayload(source?.id);
|
||||
return payload?.type === 'FOLDER';
|
||||
},
|
||||
[getNavItemById, getAddToNavPayload],
|
||||
);
|
||||
|
||||
const computeForbiddenTargetId = useCallback(
|
||||
(
|
||||
source: { id?: unknown; data?: unknown } | null,
|
||||
resolved: SortableTargetDestination,
|
||||
isAddToNavDrag: boolean,
|
||||
): string | null => {
|
||||
const sourceIsFolder = isAddToNavDrag
|
||||
? getAddToNavPayload(source?.id)?.type === 'FOLDER'
|
||||
: isSourceFolderDrag(source);
|
||||
|
||||
if (!sourceIsFolder) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (resolved.isTargetFolder) {
|
||||
return resolved.effectiveDropTargetId;
|
||||
}
|
||||
|
||||
const destFolderId = extractFolderIdFromDroppableId(
|
||||
resolved.destination.droppableId,
|
||||
sectionType,
|
||||
);
|
||||
if (isDefined(destFolderId)) {
|
||||
return resolved.dropTargetId;
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
[sectionType, getAddToNavPayload, isSourceFolderDrag],
|
||||
);
|
||||
|
||||
const applyWorkspaceReorderIfAllowed = useCallback(
|
||||
(
|
||||
id: string,
|
||||
source: DropDestination,
|
||||
destination: DropDestination,
|
||||
insertBeforeItemId?: string | null,
|
||||
) => {
|
||||
const draggedItem = getNavItemById(id);
|
||||
const destFolderId = extractFolderIdFromDroppableId(
|
||||
destination.droppableId,
|
||||
'workspace',
|
||||
);
|
||||
if (
|
||||
isDefined(destFolderId) &&
|
||||
isDefined(draggedItem) &&
|
||||
isNavigationMenuItemFolder(draggedItem)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const result = toDropResult(
|
||||
id,
|
||||
{
|
||||
sourceDroppableId: source.droppableId,
|
||||
sourceIndex: source.index,
|
||||
},
|
||||
destination,
|
||||
);
|
||||
const provided: ResponderProvided = { announce: () => {} };
|
||||
handleWorkspaceNavigationMenuItemDragAndDrop(
|
||||
{
|
||||
...result,
|
||||
...DROP_RESULT_OPTIONS,
|
||||
...(insertBeforeItemId != null && { insertBeforeItemId }),
|
||||
},
|
||||
provided,
|
||||
);
|
||||
},
|
||||
[getNavItemById, handleWorkspaceNavigationMenuItemDragAndDrop],
|
||||
);
|
||||
|
||||
const handleDragStart = (event: DragStartPayload) => {
|
||||
const { operation } = event;
|
||||
setIsDragging(true);
|
||||
const source = operation.source;
|
||||
const sourceId = source?.data?.sourceDroppableId ?? null;
|
||||
setSourceDroppableId(sourceId);
|
||||
|
||||
if (sourceId === ADD_TO_NAV_SOURCE_DROPPABLE_ID) {
|
||||
const defaultDestination: DropDestination = {
|
||||
droppableId: defaultOrphanDroppableId,
|
||||
index: orphanItemCount,
|
||||
};
|
||||
setAddToNavigationFallbackDestination(defaultDestination);
|
||||
setActiveDropTargetId(
|
||||
getDndKitDropTargetId(
|
||||
defaultDestination.droppableId,
|
||||
defaultDestination.index,
|
||||
),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDragOver = useCallback(
|
||||
(event: DragOverPayload) => {
|
||||
const { operation } = event;
|
||||
const source = operation.source;
|
||||
const target = operation.target;
|
||||
const isAddToNavDrag =
|
||||
sourceDroppableId === ADD_TO_NAV_SOURCE_DROPPABLE_ID;
|
||||
const sourceIsSortable = source !== null && isSortable(source);
|
||||
const resolved = resolveDropTarget(target, getNavItemById, sectionType);
|
||||
|
||||
// Branch 1: sortable-to-sortable
|
||||
if (
|
||||
resolved !== null &&
|
||||
source !== null &&
|
||||
target !== null &&
|
||||
isSortable(source) &&
|
||||
isSortable(target)
|
||||
) {
|
||||
setActiveDropTargetId(resolved.effectiveDropTargetId);
|
||||
setForbiddenDropTargetId(
|
||||
isAddToNavDrag
|
||||
? computeForbiddenTargetId(source, resolved, true)
|
||||
: computeForbiddenTargetId(source, resolved, false),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Branch 2: sortable-to-droppable-slot
|
||||
if (resolved !== null && sourceIsSortable) {
|
||||
setActiveDropTargetId(resolved.effectiveDropTargetId);
|
||||
setAddToNavigationFallbackDestination(resolved.destination);
|
||||
setForbiddenDropTargetId(
|
||||
computeForbiddenTargetId(source, resolved, isAddToNavDrag),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isAddToNavDrag) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Branch 3: add-to-nav drag
|
||||
if (resolved !== null) {
|
||||
setAddToNavigationFallbackDestination(resolved.destination);
|
||||
setActiveDropTargetId(resolved.effectiveDropTargetId);
|
||||
setForbiddenDropTargetId(
|
||||
computeForbiddenTargetId(source, resolved, true),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const fallback = addToNavigationFallbackDestination;
|
||||
setActiveDropTargetId(
|
||||
fallback
|
||||
? getDndKitDropTargetId(fallback.droppableId, fallback.index)
|
||||
: null,
|
||||
);
|
||||
setForbiddenDropTargetId(null);
|
||||
},
|
||||
[
|
||||
sourceDroppableId,
|
||||
addToNavigationFallbackDestination,
|
||||
getNavItemById,
|
||||
sectionType,
|
||||
computeForbiddenTargetId,
|
||||
],
|
||||
);
|
||||
|
||||
const handleDragEnd = (event: DragEndPayload) => {
|
||||
const { operation } = event;
|
||||
const source = operation.source;
|
||||
const target = operation.target;
|
||||
const draggableId = String(source?.id);
|
||||
const data = source?.data;
|
||||
const sourceId = data?.sourceDroppableId ?? null;
|
||||
const fallback = addToNavigationFallbackDestination;
|
||||
|
||||
setIsDragging(false);
|
||||
setSourceDroppableId(null);
|
||||
setActiveDropTargetId(null);
|
||||
setForbiddenDropTargetId(null);
|
||||
setAddToNavigationFallbackDestination(null);
|
||||
|
||||
const sourceIsSortable = source !== null && isSortable(source);
|
||||
const targetIsSortable = target !== null && isSortable(target);
|
||||
const resolved = resolveDropTarget(target, getNavItemById, sectionType);
|
||||
|
||||
// Workspace fast path: sortable-to-sortable within workspace
|
||||
if (
|
||||
isWorkspaceSection &&
|
||||
sourceIsSortable &&
|
||||
targetIsSortable &&
|
||||
isDefined(source) &&
|
||||
isDefined(target) &&
|
||||
resolved !== null
|
||||
) {
|
||||
const sourceDraggable = 'initialGroup' in source ? source : null;
|
||||
const initialGroup = String(sourceDraggable?.initialGroup ?? '');
|
||||
const initialIndex = sourceDraggable?.initialIndex ?? 0;
|
||||
const destGroup = String(target.group ?? '');
|
||||
const bothWorkspace =
|
||||
canNavigationMenuItemBeDroppedIn({
|
||||
navigationMenuItemSection: 'workspace',
|
||||
droppableId: initialGroup,
|
||||
}) &&
|
||||
canNavigationMenuItemBeDroppedIn({
|
||||
navigationMenuItemSection: 'workspace',
|
||||
droppableId: destGroup,
|
||||
});
|
||||
if (bothWorkspace) {
|
||||
const insertBeforeItemId = resolved.isTargetFolder
|
||||
? null
|
||||
: String(target?.id ?? '');
|
||||
applyWorkspaceReorderIfAllowed(
|
||||
draggableId,
|
||||
{ droppableId: initialGroup, index: initialIndex },
|
||||
resolved.destination,
|
||||
insertBeforeItemId || undefined,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let destination: DropDestination | null = resolved?.destination ?? null;
|
||||
if (
|
||||
destination == null &&
|
||||
isDefined(fallback) &&
|
||||
canNavigationMenuItemBeDroppedIn({
|
||||
navigationMenuItemSection: sectionType,
|
||||
droppableId: fallback.droppableId,
|
||||
})
|
||||
) {
|
||||
destination = fallback;
|
||||
}
|
||||
|
||||
const result = toDropResult(draggableId, data, destination);
|
||||
const provided: ResponderProvided = { announce: () => {} };
|
||||
const dropResult = { ...result, ...DROP_RESULT_OPTIONS };
|
||||
|
||||
if (sourceId === ADD_TO_NAV_SOURCE_DROPPABLE_ID) {
|
||||
handleAddToNavigationDrop(dropResult, provided);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isWorkspaceSection) {
|
||||
if (
|
||||
isDefined(sourceId) &&
|
||||
canNavigationMenuItemBeDroppedIn({
|
||||
navigationMenuItemSection: 'workspace',
|
||||
droppableId: sourceId,
|
||||
}) &&
|
||||
isDefined(destination) &&
|
||||
canNavigationMenuItemBeDroppedIn({
|
||||
navigationMenuItemSection: 'workspace',
|
||||
droppableId: destination.droppableId,
|
||||
})
|
||||
) {
|
||||
applyWorkspaceReorderIfAllowed(
|
||||
draggableId,
|
||||
{
|
||||
droppableId: data?.sourceDroppableId ?? '',
|
||||
index: data?.sourceIndex ?? 0,
|
||||
},
|
||||
destination,
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
handleNavigationMenuItemDragAndDrop(dropResult, provided);
|
||||
};
|
||||
|
||||
const contextValues: NavigationMenuItemDndKitContextValues = {
|
||||
dragSource: { sourceDroppableId },
|
||||
drag: { isDragging },
|
||||
dropTarget: {
|
||||
activeDropTargetId,
|
||||
setActiveDropTargetId,
|
||||
forbiddenDropTargetId,
|
||||
setForbiddenDropTargetId,
|
||||
addToNavigationFallbackDestination,
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
contextValues,
|
||||
handlers: {
|
||||
onDragStart: handleDragStart,
|
||||
onDragOver: handleDragOver,
|
||||
onDragEnd: handleDragEnd,
|
||||
},
|
||||
};
|
||||
};
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
let preloadScheduled = false;
|
||||
|
||||
const preload = () => {
|
||||
void import(
|
||||
'@/navigation-menu-item/display/dnd/providers/NavigationMenuItemDndKitProvider'
|
||||
);
|
||||
void import(
|
||||
'@/navigation-menu-item/display/sections/workspace/components/WorkspaceSectionListDndKit'
|
||||
);
|
||||
};
|
||||
|
||||
export const preloadNavigationMenuItemDndKit = (): void => {
|
||||
if (preloadScheduled) {
|
||||
return;
|
||||
}
|
||||
preloadScheduled = true;
|
||||
preload();
|
||||
};
|
||||
+11
-9
@@ -6,14 +6,14 @@ import {
|
||||
} from '@dnd-kit/react';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
import type { NavigationSections } from '@/navigation-menu-item/common/constants/NavigationSections.constants';
|
||||
import { NavigationDragSourceContext } from '@/navigation-menu-item/common/contexts/NavigationDragSourceContext';
|
||||
import { NavigationDropTargetContext } from '@/navigation-menu-item/common/contexts/NavigationDropTargetContext';
|
||||
import { NavigationMenuItemDragContext } from '@/navigation-menu-item/common/contexts/NavigationMenuItemDragContext';
|
||||
import type { DraggableData } from '@/navigation/types/workspaceDndKitDraggableData';
|
||||
import type { DraggableData } from '@/navigation-menu-item/common/types/navigationMenuItemDndKitDraggableData';
|
||||
import { useNavigationMenuItemDndKit } from '@/navigation-menu-item/display/dnd/hooks/useNavigationMenuItemDndKit';
|
||||
|
||||
import { useFavoritesDndKit } from '@/navigation/hooks/useFavoritesDndKit';
|
||||
|
||||
const FAVORITES_DND_SENSORS = [
|
||||
const NAVIGATION_MENU_ITEM_DND_SENSORS = [
|
||||
PointerSensor.configure({
|
||||
activationConstraints: [
|
||||
new PointerActivationConstraints.Distance({ value: 8 }),
|
||||
@@ -22,21 +22,23 @@ const FAVORITES_DND_SENSORS = [
|
||||
KeyboardSensor,
|
||||
];
|
||||
|
||||
type FavoritesDndKitProviderProps = {
|
||||
type NavigationMenuItemDndKitProviderProps = {
|
||||
section: NavigationSections;
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export const FavoritesDndKitProvider = ({
|
||||
export const NavigationMenuItemDndKitProvider = ({
|
||||
section,
|
||||
children,
|
||||
}: FavoritesDndKitProviderProps) => {
|
||||
const { contextValues, handlers } = useFavoritesDndKit();
|
||||
}: NavigationMenuItemDndKitProviderProps) => {
|
||||
const { contextValues, handlers } = useNavigationMenuItemDndKit(section);
|
||||
|
||||
return (
|
||||
<NavigationDragSourceContext.Provider value={contextValues.dragSource}>
|
||||
<NavigationMenuItemDragContext.Provider value={contextValues.drag}>
|
||||
<NavigationDropTargetContext.Provider value={contextValues.dropTarget}>
|
||||
<DragDropProvider<DraggableData>
|
||||
sensors={FAVORITES_DND_SENSORS}
|
||||
sensors={NAVIGATION_MENU_ITEM_DND_SENSORS}
|
||||
onDragStart={handlers.onDragStart}
|
||||
onDragOver={handlers.onDragOver}
|
||||
onDragEnd={handlers.onDragEnd}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
import { lazy, Suspense, useState, type ReactNode } from 'react';
|
||||
|
||||
import { NavigationSections } from '@/navigation-menu-item/common/constants/NavigationSections.constants';
|
||||
import { PageDragDropProviderMountEffect } from '@/navigation-menu-item/display/dnd/providers/PageDragDropProviderMountEffect';
|
||||
|
||||
const LazyNavigationMenuItemDndKitProvider = lazy(() =>
|
||||
import(
|
||||
'@/navigation-menu-item/display/dnd/providers/NavigationMenuItemDndKitProvider'
|
||||
).then((m) => ({
|
||||
default: m.NavigationMenuItemDndKitProvider,
|
||||
})),
|
||||
);
|
||||
|
||||
type PageDragDropProviderProps = {
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export const PageDragDropProvider = ({
|
||||
children,
|
||||
}: PageDragDropProviderProps) => {
|
||||
const [hasProviderMounted, setHasProviderMounted] = useState(false);
|
||||
|
||||
if (!hasProviderMounted) {
|
||||
return (
|
||||
<>
|
||||
<PageDragDropProviderMountEffect
|
||||
onEnterEditMode={() => setHasProviderMounted(true)}
|
||||
/>
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Suspense fallback={<>{children}</>}>
|
||||
<LazyNavigationMenuItemDndKitProvider
|
||||
section={NavigationSections.WORKSPACE}
|
||||
>
|
||||
{children}
|
||||
</LazyNavigationMenuItemDndKitProvider>
|
||||
</Suspense>
|
||||
);
|
||||
};
|
||||
+11
-9
@@ -1,13 +1,13 @@
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import type { NavigationMenuItem } from '~/generated-metadata/graphql';
|
||||
|
||||
import { NavigationMenuItemDroppableIds } from '@/navigation-menu-item/common/constants/NavigationMenuItemDroppableIds';
|
||||
import { NAVIGATION_MENU_ITEM_SECTION_DROPPABLE_CONFIG } from '@/navigation-menu-item/common/constants/NavigationMenuItemSectionDroppableConfig';
|
||||
import type { NavigationMenuItemSection } from '@/navigation-menu-item/common/types/NavigationMenuItemSection';
|
||||
import type { DropDestination } from '@/navigation-menu-item/common/types/navigationMenuItemDndKitDropDestination';
|
||||
import type { SortableTargetDestination } from '@/navigation-menu-item/common/types/navigationMenuItemDndKitSortableTargetDestination';
|
||||
import { getDndKitDropTargetId } from '@/navigation-menu-item/common/utils/getDndKitDropTargetId';
|
||||
import { isNavigationMenuItemFolder } from '@/navigation-menu-item/common/utils/isNavigationMenuItemFolder';
|
||||
|
||||
import type { DropDestination } from '@/navigation/types/workspaceDndKitDropDestination';
|
||||
import type { SortableTargetDestination } from '@/navigation/types/workspaceDndKitSortableTargetDestination';
|
||||
|
||||
type GetNavItemById = (
|
||||
id: string | undefined,
|
||||
) => NavigationMenuItem | undefined;
|
||||
@@ -15,6 +15,7 @@ type GetNavItemById = (
|
||||
export const getDestinationFromSortableTarget = (
|
||||
target: { id: unknown; group?: unknown; index?: unknown },
|
||||
getNavItemById: GetNavItemById,
|
||||
navigationMenuItemSection: NavigationMenuItemSection,
|
||||
): SortableTargetDestination | null => {
|
||||
const group = target.group;
|
||||
const rawIndex = target.index;
|
||||
@@ -32,15 +33,16 @@ export const getDestinationFromSortableTarget = (
|
||||
const isTargetFolder =
|
||||
isDefined(targetItem) && isNavigationMenuItemFolder(targetItem);
|
||||
const dropTargetId = getDndKitDropTargetId(destDroppableId, index);
|
||||
|
||||
const { folderHeaderPrefix } =
|
||||
NAVIGATION_MENU_ITEM_SECTION_DROPPABLE_CONFIG[navigationMenuItemSection];
|
||||
|
||||
const effectiveDropTargetId = isTargetFolder
|
||||
? getDndKitDropTargetId(
|
||||
`${NavigationMenuItemDroppableIds.WORKSPACE_FOLDER_HEADER_PREFIX}${target.id}`,
|
||||
0,
|
||||
)
|
||||
? getDndKitDropTargetId(`${folderHeaderPrefix}${target.id}`, 0)
|
||||
: dropTargetId;
|
||||
const destination: DropDestination = {
|
||||
droppableId: isTargetFolder
|
||||
? `${NavigationMenuItemDroppableIds.WORKSPACE_FOLDER_HEADER_PREFIX}${target.id}`
|
||||
? `${folderHeaderPrefix}${target.id}`
|
||||
: destDroppableId,
|
||||
index: isTargetFolder ? 0 : index,
|
||||
};
|
||||
+13
-6
@@ -1,11 +1,11 @@
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import type { NavigationMenuItem } from '~/generated-metadata/graphql';
|
||||
|
||||
import { isWorkspaceDroppableId } from '@/navigation-menu-item/common/utils/isWorkspaceDroppableId';
|
||||
|
||||
import type { DroppableData } from '@/navigation/types/workspaceDndKitDroppableData';
|
||||
import type { SortableTargetDestination } from '@/navigation/types/workspaceDndKitSortableTargetDestination';
|
||||
import { getDestinationFromSortableTarget } from '@/navigation/utils/workspaceDndKitGetDestinationFromSortableTarget';
|
||||
import type { NavigationMenuItemSection } from '@/navigation-menu-item/common/types/NavigationMenuItemSection';
|
||||
import type { DroppableData } from '@/navigation-menu-item/common/types/navigationMenuItemDndKitDroppableData';
|
||||
import type { SortableTargetDestination } from '@/navigation-menu-item/common/types/navigationMenuItemDndKitSortableTargetDestination';
|
||||
import { canNavigationMenuItemBeDroppedIn } from '@/navigation-menu-item/common/utils/canNavigationMenuItemBeDroppedIn';
|
||||
import { getDestinationFromSortableTarget } from '@/navigation-menu-item/display/dnd/utils/navigationMenuItemDndKitGetDestinationFromSortableTarget';
|
||||
|
||||
type GetNavItemById = (
|
||||
id: string | undefined,
|
||||
@@ -25,6 +25,7 @@ export const resolveDropTarget = (
|
||||
data?: unknown;
|
||||
} | null,
|
||||
getNavItemById: GetNavItemById,
|
||||
navigationMenuItemSection: NavigationMenuItemSection,
|
||||
): SortableTargetDestination | null => {
|
||||
if (target === null || target === undefined) {
|
||||
return null;
|
||||
@@ -33,11 +34,17 @@ export const resolveDropTarget = (
|
||||
return getDestinationFromSortableTarget(
|
||||
{ id: target.id, group: target.group, index: target.index },
|
||||
getNavItemById,
|
||||
navigationMenuItemSection,
|
||||
);
|
||||
}
|
||||
if (isDroppableData(target.data)) {
|
||||
const { droppableId, index } = target.data;
|
||||
if (isWorkspaceDroppableId(droppableId)) {
|
||||
if (
|
||||
canNavigationMenuItemBeDroppedIn({
|
||||
navigationMenuItemSection,
|
||||
droppableId,
|
||||
})
|
||||
) {
|
||||
return {
|
||||
destination: { droppableId, index },
|
||||
effectiveDropTargetId: String(target.id),
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
import type { DropDestination } from '@/navigation/types/workspaceDndKitDropDestination';
|
||||
import type { DraggableData } from '@/navigation/types/workspaceDndKitDraggableData';
|
||||
import type { DropDestination } from '@/navigation-menu-item/common/types/navigationMenuItemDndKitDropDestination';
|
||||
import type { DraggableData } from '@/navigation-menu-item/common/types/navigationMenuItemDndKitDraggableData';
|
||||
|
||||
export const toDropResult = (
|
||||
draggableId: string,
|
||||
+169
@@ -0,0 +1,169 @@
|
||||
import { Suspense, lazy } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { IconChevronDown, IconChevronRight, useIcons } from 'twenty-ui/display';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
import { useIsMobile } from 'twenty-ui/utilities';
|
||||
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
|
||||
|
||||
import { FOLDER_ICON_DEFAULT } from '@/navigation-menu-item/common/constants/FolderIconDefault';
|
||||
import { DEFAULT_NAVIGATION_MENU_ITEM_COLOR_FOLDER } from '@/navigation-menu-item/common/constants/NavigationMenuItemDefaultColorFolder';
|
||||
import { NavigationMenuItemFolderLayout } from '@/navigation-menu-item/display/folder/components/NavigationMenuItemFolderLayout';
|
||||
import { NavigationMenuItemFolderSubItem } from '@/navigation-menu-item/display/folder/components/NavigationMenuItemFolderSubItem';
|
||||
import { useNavigationMenuItemFolderOpenState } from '@/navigation-menu-item/display/folder/hooks/useNavigationMenuItemFolderOpenState';
|
||||
import type { NavigationMenuItemSectionContentProps } from '@/navigation-menu-item/display/sections/types/NavigationMenuItemSectionContentProps';
|
||||
import { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem';
|
||||
|
||||
const LazyNavigationMenuItemFolderDnd = lazy(() =>
|
||||
import(
|
||||
'@/navigation-menu-item/display/folder/components/NavigationMenuItemFolderDnd'
|
||||
).then((module) => ({ default: module.NavigationMenuItemFolderDnd })),
|
||||
);
|
||||
|
||||
type NavigationMenuItemFolderProps = Pick<
|
||||
NavigationMenuItemSectionContentProps,
|
||||
| 'item'
|
||||
| 'isEditInPlace'
|
||||
| 'editModeProps'
|
||||
| 'isDragging'
|
||||
| 'folderChildrenById'
|
||||
| 'folderCount'
|
||||
| 'selectedNavigationMenuItemId'
|
||||
| 'onNavigationMenuItemClick'
|
||||
| 'readOnly'
|
||||
>;
|
||||
|
||||
export const NavigationMenuItemFolder = ({
|
||||
item,
|
||||
isEditInPlace = false,
|
||||
editModeProps,
|
||||
isDragging,
|
||||
folderChildrenById,
|
||||
folderCount,
|
||||
selectedNavigationMenuItemId,
|
||||
onNavigationMenuItemClick,
|
||||
readOnly = false,
|
||||
}: NavigationMenuItemFolderProps) => {
|
||||
const folderId = item.id;
|
||||
const folderName = item.name ?? 'Folder';
|
||||
const folderIconKey = item.icon;
|
||||
const folderColor = 'color' in item ? (item.color as string | null) : null;
|
||||
const navigationMenuItems = folderChildrenById.get(folderId) ?? [];
|
||||
const isGroup = folderCount > 1;
|
||||
|
||||
if (readOnly) {
|
||||
return (
|
||||
<NavigationMenuItemFolderReadOnlyContent
|
||||
folderId={folderId}
|
||||
folderName={folderName}
|
||||
folderIconKey={folderIconKey}
|
||||
folderColor={folderColor}
|
||||
navigationMenuItems={navigationMenuItems}
|
||||
isGroup={isGroup}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Suspense
|
||||
fallback={
|
||||
<NavigationMenuItemFolderReadOnlyContent
|
||||
folderId={folderId}
|
||||
folderName={folderName}
|
||||
folderIconKey={folderIconKey}
|
||||
folderColor={folderColor}
|
||||
navigationMenuItems={navigationMenuItems}
|
||||
isGroup={isGroup}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<LazyNavigationMenuItemFolderDnd
|
||||
folderId={folderId}
|
||||
folderName={folderName}
|
||||
folderIconKey={folderIconKey}
|
||||
folderColor={folderColor}
|
||||
navigationMenuItems={navigationMenuItems}
|
||||
isGroup={isGroup}
|
||||
isEditInPlace={isEditInPlace}
|
||||
editModeProps={editModeProps}
|
||||
isDragging={isDragging}
|
||||
selectedNavigationMenuItemId={selectedNavigationMenuItemId}
|
||||
onNavigationMenuItemClick={onNavigationMenuItemClick}
|
||||
/>
|
||||
</Suspense>
|
||||
);
|
||||
};
|
||||
|
||||
type NavigationMenuItemFolderReadOnlyContentProps = {
|
||||
folderId: string;
|
||||
folderName: string;
|
||||
folderIconKey?: string | null;
|
||||
folderColor?: string | null;
|
||||
navigationMenuItems: NavigationMenuItem[];
|
||||
isGroup: boolean;
|
||||
};
|
||||
|
||||
const NavigationMenuItemFolderReadOnlyContent = ({
|
||||
folderId,
|
||||
folderName,
|
||||
folderIconKey,
|
||||
folderColor,
|
||||
navigationMenuItems,
|
||||
isGroup,
|
||||
}: NavigationMenuItemFolderReadOnlyContentProps) => {
|
||||
const { getIcon } = useIcons();
|
||||
const isMobile = useIsMobile();
|
||||
const FolderIcon = getIcon(folderIconKey ?? FOLDER_ICON_DEFAULT);
|
||||
|
||||
const { isOpen, handleToggle, selectedNavigationMenuItemIndex } =
|
||||
useNavigationMenuItemFolderOpenState({ folderId, navigationMenuItems });
|
||||
|
||||
return (
|
||||
<NavigationMenuItemFolderLayout
|
||||
header={
|
||||
<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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
}
|
||||
isOpen={isOpen}
|
||||
isGroup={isGroup}
|
||||
>
|
||||
{navigationMenuItems.map((navigationMenuItem, index) => (
|
||||
<NavigationMenuItemFolderSubItem
|
||||
key={navigationMenuItem.id}
|
||||
navigationMenuItem={navigationMenuItem}
|
||||
index={index}
|
||||
arrayLength={navigationMenuItems.length}
|
||||
selectedNavigationMenuItemIndex={selectedNavigationMenuItemIndex}
|
||||
isDragging={false}
|
||||
/>
|
||||
))}
|
||||
</NavigationMenuItemFolderLayout>
|
||||
);
|
||||
};
|
||||
+5
-4
@@ -2,9 +2,10 @@ import { IconHeartOff } from 'twenty-ui/display';
|
||||
import { LightIconButton } from 'twenty-ui/input';
|
||||
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
|
||||
|
||||
import { NavigationMenuItemDroppableIds } from '@/navigation-menu-item/common/constants/NavigationMenuItemDroppableIds';
|
||||
import { NavigationMenuItemBackButton } from '@/navigation-menu-item/edit/components/NavigationMenuItemBackButton';
|
||||
import { NavigationMenuItemIcon } from '@/navigation-menu-item/display/components/NavigationMenuItemIcon';
|
||||
import { WorkspaceDndKitSortableItem } from '@/navigation-menu-item/display/dnd/components/WorkspaceDndKitSortableItem';
|
||||
import { NavigationMenuItemSortableItem } from '@/navigation-menu-item/display/dnd/components/NavigationMenuItemSortableItem';
|
||||
import { useDeleteNavigationMenuItem } from '@/navigation-menu-item/common/hooks/useDeleteNavigationMenuItem';
|
||||
import { getNavigationMenuItemComputedLink } from '@/navigation-menu-item/display/utils/getNavigationMenuItemComputedLink';
|
||||
import { getNavigationMenuItemLabel } from '@/navigation-menu-item/display/utils/getNavigationMenuItemLabel';
|
||||
@@ -30,7 +31,7 @@ export const NavigationMenuItemFolderContent = ({
|
||||
const views = useAtomStateValue(viewsSelector);
|
||||
const { deleteNavigationMenuItem } = useDeleteNavigationMenuItem();
|
||||
|
||||
const folderDroppableId = `folder-${folderId}`;
|
||||
const folderDroppableId = `${NavigationMenuItemDroppableIds.FAVORITE_FOLDER_PREFIX}${folderId}`;
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -53,7 +54,7 @@ export const NavigationMenuItemFolderContent = ({
|
||||
);
|
||||
|
||||
return (
|
||||
<WorkspaceDndKitSortableItem
|
||||
<NavigationMenuItemSortableItem
|
||||
key={navigationMenuItem.id}
|
||||
id={navigationMenuItem.id}
|
||||
index={index}
|
||||
@@ -83,7 +84,7 @@ export const NavigationMenuItemFolderContent = ({
|
||||
triggerEvent="CLICK"
|
||||
to={computedLink}
|
||||
/>
|
||||
</WorkspaceDndKitSortableItem>
|
||||
</NavigationMenuItemSortableItem>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
|
||||
-71
@@ -1,71 +0,0 @@
|
||||
import { lazy, Suspense } from 'react';
|
||||
|
||||
import { NavigationMenuItemFolderReadOnly } from '@/navigation-menu-item/display/folder/components/NavigationMenuItemFolderReadOnly';
|
||||
import type { WorkspaceSectionItemContentProps } from '@/navigation-menu-item/display/sections/types/WorkspaceSectionItemContentProps';
|
||||
|
||||
const LazyWorkspaceNavigationMenuItemsFolder = lazy(() =>
|
||||
import(
|
||||
'@/navigation-menu-item/display/folder/components/WorkspaceNavigationMenuItemsFolder'
|
||||
).then((m) => ({ default: m.WorkspaceNavigationMenuItemsFolder })),
|
||||
);
|
||||
|
||||
type NavigationMenuItemFolderDisplayProps = WorkspaceSectionItemContentProps;
|
||||
|
||||
export const NavigationMenuItemFolderDisplay = ({
|
||||
item,
|
||||
editModeProps,
|
||||
isDragging,
|
||||
folderChildrenById,
|
||||
folderCount,
|
||||
selectedNavigationMenuItemId,
|
||||
onNavigationMenuItemClick,
|
||||
readOnly = false,
|
||||
}: NavigationMenuItemFolderDisplayProps) => {
|
||||
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 (
|
||||
<NavigationMenuItemFolderReadOnly
|
||||
folderId={folderId}
|
||||
folderName={folderName}
|
||||
folderIconKey={folderIconKey}
|
||||
folderColor={folderColor}
|
||||
navigationMenuItems={navigationMenuItems}
|
||||
isGroup={isGroup}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Suspense
|
||||
fallback={
|
||||
<NavigationMenuItemFolderReadOnly
|
||||
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>
|
||||
);
|
||||
};
|
||||
+404
@@ -0,0 +1,404 @@
|
||||
import { styled } from '@linaria/react';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import React, { Fragment, useCallback, useContext } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { SidePanelPages } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
IconChevronDown,
|
||||
IconChevronRight,
|
||||
IconFolder,
|
||||
IconHeartOff,
|
||||
IconPlus,
|
||||
useIcons,
|
||||
} from 'twenty-ui/display';
|
||||
import { LightIconButton } from 'twenty-ui/input';
|
||||
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
import { useIsMobile } from 'twenty-ui/utilities';
|
||||
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
|
||||
|
||||
import { FOLDER_ICON_DEFAULT } from '@/navigation-menu-item/common/constants/FolderIconDefault';
|
||||
import { DEFAULT_NAVIGATION_MENU_ITEM_COLOR_FOLDER } from '@/navigation-menu-item/common/constants/NavigationMenuItemDefaultColorFolder';
|
||||
import { NAVIGATION_MENU_ITEM_SECTION_DROPPABLE_CONFIG } from '@/navigation-menu-item/common/constants/NavigationMenuItemSectionDroppableConfig';
|
||||
import { NavigationSections } from '@/navigation-menu-item/common/constants/NavigationSections.constants';
|
||||
import { NavigationDropTargetContext } from '@/navigation-menu-item/common/contexts/NavigationDropTargetContext';
|
||||
import { NavigationMenuItemDragContext } from '@/navigation-menu-item/common/contexts/NavigationMenuItemDragContext';
|
||||
import { SortableDropTargetRefContext } from '@/navigation-menu-item/common/contexts/SortableDropTargetRefContext';
|
||||
import { useDeleteNavigationMenuItem } from '@/navigation-menu-item/common/hooks/useDeleteNavigationMenuItem';
|
||||
import { addMenuItemInsertionContextState } from '@/navigation-menu-item/common/states/addMenuItemInsertionContextState';
|
||||
import { isLayoutCustomizationModeEnabledState } from '@/layout-customization/states/isLayoutCustomizationModeEnabledState';
|
||||
import type { NavigationMenuItemSection } from '@/navigation-menu-item/common/types/NavigationMenuItemSection';
|
||||
import { getDndKitDropTargetId } from '@/navigation-menu-item/common/utils/getDndKitDropTargetId';
|
||||
import { NavigationItemDropTarget } from '@/navigation-menu-item/display/dnd/components/NavigationItemDropTarget';
|
||||
import {
|
||||
FOLDER_HEADER_SLOT_COLLISION_PRIORITY,
|
||||
NavigationMenuItemDroppableSlot,
|
||||
} from '@/navigation-menu-item/display/dnd/components/NavigationMenuItemDroppableSlot';
|
||||
import { NavigationMenuItemSortableItem } from '@/navigation-menu-item/display/dnd/components/NavigationMenuItemSortableItem';
|
||||
import { useIsDropDisabledForSection } from '@/navigation-menu-item/display/dnd/hooks/useIsDropDisabledForSection';
|
||||
import { NavigationMenuItemFolderLayout } from '@/navigation-menu-item/display/folder/components/NavigationMenuItemFolderLayout';
|
||||
import { NavigationMenuItemFolderNavigationDrawerItemDropdown } from '@/navigation-menu-item/display/folder/components/NavigationMenuItemFolderNavigationDrawerItemDropdown';
|
||||
import { NavigationMenuItemFolderSubItem } from '@/navigation-menu-item/display/folder/components/NavigationMenuItemFolderSubItem';
|
||||
import { useNavigationMenuItemFolderOpenState } from '@/navigation-menu-item/display/folder/hooks/useNavigationMenuItemFolderOpenState';
|
||||
import type { NavigationMenuItemClickParams } from '@/navigation-menu-item/display/hooks/useNavigationMenuItemSectionItems';
|
||||
import { useOpenAddItemToFolderPage } from '@/navigation-menu-item/edit/hooks/useOpenAddItemToFolderPage';
|
||||
import { useFavoritesFolderEdit } from '@/navigation-menu-item/edit/folder/hooks/useFavoritesFolderEdit';
|
||||
import type { EditModeProps } from '@/object-metadata/components/EditModeProps';
|
||||
import { sidePanelPageState } from '@/side-panel/states/sidePanelPageState';
|
||||
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
|
||||
import { NavigationDrawerInput } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerInput';
|
||||
import { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem';
|
||||
import { NavigationDrawerSubItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSubItem';
|
||||
import { getNavigationSubItemLeftAdornment } from '@/ui/navigation/navigation-drawer/utils/getNavigationSubItemLeftAdornment';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
|
||||
const StyledFolderContainer = styled.div<{
|
||||
$isSelectedInEditMode: boolean;
|
||||
}>`
|
||||
border: ${({ $isSelectedInEditMode }) =>
|
||||
$isSelectedInEditMode
|
||||
? `1px solid ${themeCssVariables.color.blue}`
|
||||
: '1px solid transparent'};
|
||||
border-radius: ${themeCssVariables.border.radius.sm};
|
||||
transition: background-color 150ms ease-in-out;
|
||||
|
||||
&[data-drag-over-header='true'] {
|
||||
background-color: ${themeCssVariables.background.transparent.blue};
|
||||
}
|
||||
|
||||
&[data-forbidden-drop-target='true'] {
|
||||
background-color: ${themeCssVariables.background.transparent.danger};
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledFolderDroppableContent = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
`;
|
||||
|
||||
type NavigationMenuItemFolderDndProps = {
|
||||
folderId: string;
|
||||
folderName: string;
|
||||
folderIconKey?: string | null;
|
||||
folderColor?: string | null;
|
||||
navigationMenuItems: NavigationMenuItem[];
|
||||
isGroup: boolean;
|
||||
isEditInPlace: boolean;
|
||||
editModeProps?: EditModeProps;
|
||||
isDragging: boolean;
|
||||
selectedNavigationMenuItemId?: string | null;
|
||||
onNavigationMenuItemClick?: (params: NavigationMenuItemClickParams) => void;
|
||||
};
|
||||
|
||||
export const NavigationMenuItemFolderDnd = ({
|
||||
folderId,
|
||||
folderName,
|
||||
folderIconKey,
|
||||
folderColor,
|
||||
navigationMenuItems,
|
||||
isGroup,
|
||||
isEditInPlace,
|
||||
editModeProps,
|
||||
isDragging: isDraggingProp,
|
||||
selectedNavigationMenuItemId,
|
||||
onNavigationMenuItemClick,
|
||||
}: NavigationMenuItemFolderDndProps) => {
|
||||
const { t } = useLingui();
|
||||
const { theme } = useContext(ThemeContext);
|
||||
const { getIcon } = useIcons();
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
const section: NavigationMenuItemSection = isEditInPlace
|
||||
? 'favorite'
|
||||
: 'workspace';
|
||||
const isWorkspace = !isEditInPlace;
|
||||
const sectionId = isEditInPlace
|
||||
? NavigationSections.FAVORITES
|
||||
: NavigationSections.WORKSPACE;
|
||||
|
||||
const { isOpen, handleToggle, selectedNavigationMenuItemIndex } =
|
||||
useNavigationMenuItemFolderOpenState({ folderId, navigationMenuItems });
|
||||
|
||||
const { isDragging: isContextDragging } = useContext(
|
||||
NavigationMenuItemDragContext,
|
||||
);
|
||||
const isDragging = isDraggingProp || isContextDragging;
|
||||
|
||||
const setSortableDropTargetRef = useContext(SortableDropTargetRefContext);
|
||||
const dropDisabled = useIsDropDisabledForSection(isWorkspace);
|
||||
const { activeDropTargetId, forbiddenDropTargetId } = useContext(
|
||||
NavigationDropTargetContext,
|
||||
);
|
||||
|
||||
const { deleteNavigationMenuItem } = useDeleteNavigationMenuItem();
|
||||
const favoritesEdit = useFavoritesFolderEdit({
|
||||
folderId,
|
||||
folderName,
|
||||
navigationMenuItems,
|
||||
});
|
||||
|
||||
const isLayoutCustomizationModeEnabled = useAtomStateValue(
|
||||
isLayoutCustomizationModeEnabledState,
|
||||
);
|
||||
const { openAddItemToFolderPage } = useOpenAddItemToFolderPage();
|
||||
const sidePanelPage = useAtomStateValue(sidePanelPageState);
|
||||
const addMenuItemInsertionContext = useAtomStateValue(
|
||||
addMenuItemInsertionContextState,
|
||||
);
|
||||
|
||||
const config = NAVIGATION_MENU_ITEM_SECTION_DROPPABLE_CONFIG[section];
|
||||
const folderHeaderDroppableId = `${config.folderHeaderPrefix}${folderId}`;
|
||||
const folderContentDroppableId = `${config.folderPrefix}${folderId}`;
|
||||
const folderHeaderSlotId = getDndKitDropTargetId(folderHeaderDroppableId, 0);
|
||||
|
||||
const isForbiddenDropTarget =
|
||||
isDefined(forbiddenDropTargetId) &&
|
||||
(forbiddenDropTargetId.startsWith(`${folderContentDroppableId}::`) ||
|
||||
forbiddenDropTargetId.startsWith(`${folderHeaderDroppableId}::`));
|
||||
const isDragOverFolderHeader =
|
||||
!isForbiddenDropTarget && activeDropTargetId === folderHeaderSlotId;
|
||||
|
||||
const FolderIcon = getIcon(folderIconKey ?? FOLDER_ICON_DEFAULT);
|
||||
const iconColor = isDefined(folderColor)
|
||||
? folderColor
|
||||
: DEFAULT_NAVIGATION_MENU_ITEM_COLOR_FOLDER;
|
||||
|
||||
const isSelectedInEditMode = editModeProps?.isSelectedInEditMode ?? false;
|
||||
const shouldUseEditModeClick =
|
||||
isWorkspace &&
|
||||
isLayoutCustomizationModeEnabled &&
|
||||
isDefined(editModeProps?.onEditModeClick);
|
||||
|
||||
const handleHeaderClick = shouldUseEditModeClick
|
||||
? (event?: React.MouseEvent) => {
|
||||
event?.stopPropagation();
|
||||
if (isSelectedInEditMode) {
|
||||
handleToggle();
|
||||
} else {
|
||||
editModeProps?.onEditModeClick?.();
|
||||
}
|
||||
}
|
||||
: handleToggle;
|
||||
|
||||
const headerRightOptions = isEditInPlace ? (
|
||||
<NavigationMenuItemFolderNavigationDrawerItemDropdown
|
||||
folderId={folderId}
|
||||
onRename={() => favoritesEdit.setIsRenaming(true)}
|
||||
onDelete={favoritesEdit.handleFolderDelete}
|
||||
closeDropdown={favoritesEdit.closeDropdown}
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
handleToggle();
|
||||
}}
|
||||
>
|
||||
{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}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
const headerOverride =
|
||||
isEditInPlace && favoritesEdit.isRenaming ? (
|
||||
<NavigationDrawerInput
|
||||
Icon={IconFolder}
|
||||
value={favoritesEdit.folderNameValue}
|
||||
onChange={favoritesEdit.setFolderNameValue}
|
||||
onSubmit={favoritesEdit.handleSubmitRename}
|
||||
onCancel={favoritesEdit.handleCancelRename}
|
||||
onClickOutside={favoritesEdit.handleClickOutsideRename}
|
||||
/>
|
||||
) : undefined;
|
||||
|
||||
const header = headerOverride ?? (
|
||||
<NavigationDrawerItem
|
||||
label={folderName}
|
||||
Icon={FolderIcon}
|
||||
iconColor={iconColor}
|
||||
active={
|
||||
(!isOpen && selectedNavigationMenuItemIndex >= 0) ||
|
||||
(isWorkspace && isSelectedInEditMode && !isOpen)
|
||||
}
|
||||
onClick={handleHeaderClick}
|
||||
rightOptions={headerRightOptions}
|
||||
className="navigation-drawer-item"
|
||||
isRightOptionsDropdownOpen={
|
||||
isEditInPlace ? favoritesEdit.isDropdownOpen : undefined
|
||||
}
|
||||
triggerEvent="CLICK"
|
||||
preventCollapseOnMobile={isMobile}
|
||||
isDragging={isDragging}
|
||||
alwaysShowRightOptions
|
||||
/>
|
||||
);
|
||||
|
||||
const wrappedHeader = (
|
||||
<div ref={setSortableDropTargetRef ?? undefined}>
|
||||
<NavigationMenuItemDroppableSlot
|
||||
droppableId={folderHeaderDroppableId}
|
||||
index={0}
|
||||
disabled={dropDisabled}
|
||||
collisionPriority={FOLDER_HEADER_SLOT_COLLISION_PRIORITY}
|
||||
>
|
||||
{header}
|
||||
</NavigationMenuItemDroppableSlot>
|
||||
</div>
|
||||
);
|
||||
|
||||
const isCompact = isWorkspace
|
||||
? isLayoutCustomizationModeEnabled || navigationMenuItems.length === 0
|
||||
: true;
|
||||
|
||||
const folderContentLength =
|
||||
isWorkspace && isLayoutCustomizationModeEnabled
|
||||
? navigationMenuItems.length + 1
|
||||
: navigationMenuItems.length;
|
||||
|
||||
const handleAddMenuItemToFolder = useCallback(() => {
|
||||
openAddItemToFolderPage({
|
||||
targetFolderId: folderId,
|
||||
targetIndex: navigationMenuItems.length,
|
||||
resetNavigationStack: true,
|
||||
});
|
||||
}, [folderId, navigationMenuItems.length, openAddItemToFolderPage]);
|
||||
|
||||
const deleteModal =
|
||||
isEditInPlace && favoritesEdit.isModalOpened
|
||||
? createPortal(
|
||||
<ConfirmationModal
|
||||
modalInstanceId={favoritesEdit.modalId}
|
||||
title={
|
||||
favoritesEdit.navigationMenuItemCount > 1
|
||||
? t`Remove ${favoritesEdit.navigationMenuItemCount} navigation menu items?`
|
||||
: t`Remove ${favoritesEdit.navigationMenuItemCount} navigation menu item?`
|
||||
}
|
||||
subtitle={
|
||||
favoritesEdit.navigationMenuItemCount > 1
|
||||
? t`This action will delete this folder and all ${favoritesEdit.navigationMenuItemCount} navigation menu items inside. Do you want to continue?`
|
||||
: t`This action will delete this folder and the navigation menu item inside. Do you want to continue?`
|
||||
}
|
||||
onConfirmClick={favoritesEdit.handleConfirmDelete}
|
||||
confirmButtonText={t`Delete Folder`}
|
||||
/>,
|
||||
document.body,
|
||||
)
|
||||
: null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledFolderContainer
|
||||
$isSelectedInEditMode={isWorkspace && isSelectedInEditMode}
|
||||
data-drag-over-header={isDragOverFolderHeader ? 'true' : undefined}
|
||||
data-forbidden-drop-target={isForbiddenDropTarget ? 'true' : undefined}
|
||||
>
|
||||
<NavigationMenuItemFolderLayout
|
||||
header={wrappedHeader}
|
||||
isOpen={isOpen}
|
||||
isGroup={isGroup}
|
||||
>
|
||||
<StyledFolderDroppableContent>
|
||||
{navigationMenuItems.map((navigationMenuItem, index) => (
|
||||
<Fragment key={navigationMenuItem.id}>
|
||||
<NavigationItemDropTarget
|
||||
folderId={folderId}
|
||||
index={index}
|
||||
sectionId={sectionId}
|
||||
compact={isCompact}
|
||||
dropTargetIdOverride={getDndKitDropTargetId(
|
||||
folderContentDroppableId,
|
||||
index,
|
||||
)}
|
||||
/>
|
||||
<NavigationMenuItemSortableItem
|
||||
id={navigationMenuItem.id}
|
||||
index={index}
|
||||
group={folderContentDroppableId}
|
||||
disabled={
|
||||
isWorkspace
|
||||
? !isLayoutCustomizationModeEnabled || dropDisabled
|
||||
: dropDisabled
|
||||
}
|
||||
>
|
||||
<NavigationMenuItemFolderSubItem
|
||||
navigationMenuItem={navigationMenuItem}
|
||||
index={index}
|
||||
arrayLength={folderContentLength}
|
||||
selectedNavigationMenuItemIndex={
|
||||
selectedNavigationMenuItemIndex
|
||||
}
|
||||
isDragging={isDragging}
|
||||
rightOptions={
|
||||
isEditInPlace ? (
|
||||
<LightIconButton
|
||||
Icon={IconHeartOff}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
deleteNavigationMenuItem(navigationMenuItem.id);
|
||||
}}
|
||||
accent="tertiary"
|
||||
/>
|
||||
) : undefined
|
||||
}
|
||||
onNavigationMenuItemClick={
|
||||
isWorkspace ? onNavigationMenuItemClick : undefined
|
||||
}
|
||||
selectedNavigationMenuItemId={
|
||||
isWorkspace ? selectedNavigationMenuItemId : undefined
|
||||
}
|
||||
/>
|
||||
</NavigationMenuItemSortableItem>
|
||||
</Fragment>
|
||||
))}
|
||||
<NavigationMenuItemDroppableSlot
|
||||
droppableId={folderContentDroppableId}
|
||||
index={navigationMenuItems.length}
|
||||
disabled={dropDisabled}
|
||||
>
|
||||
<NavigationItemDropTarget
|
||||
folderId={folderId}
|
||||
index={navigationMenuItems.length}
|
||||
sectionId={sectionId}
|
||||
compact={!(isWorkspace && isLayoutCustomizationModeEnabled)}
|
||||
dropTargetIdOverride={getDndKitDropTargetId(
|
||||
folderContentDroppableId,
|
||||
navigationMenuItems.length,
|
||||
)}
|
||||
/>
|
||||
{isWorkspace && isLayoutCustomizationModeEnabled && (
|
||||
<NavigationDrawerSubItem
|
||||
label={t`Add menu item`}
|
||||
Icon={IconPlus}
|
||||
onClick={handleAddMenuItemToFolder}
|
||||
triggerEvent="CLICK"
|
||||
variant="tertiary"
|
||||
isSelectedInEditMode={
|
||||
sidePanelPage === SidePanelPages.NavigationMenuAddItem &&
|
||||
addMenuItemInsertionContext?.targetFolderId === folderId
|
||||
}
|
||||
subItemState={getNavigationSubItemLeftAdornment({
|
||||
index: navigationMenuItems.length,
|
||||
arrayLength: folderContentLength,
|
||||
selectedIndex: selectedNavigationMenuItemIndex,
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
</NavigationMenuItemDroppableSlot>
|
||||
</StyledFolderDroppableContent>
|
||||
</NavigationMenuItemFolderLayout>
|
||||
</StyledFolderContainer>
|
||||
{deleteModal}
|
||||
</>
|
||||
);
|
||||
};
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
import { styled } from '@linaria/react';
|
||||
import { useState, type ReactNode } from 'react';
|
||||
import { AnimatedExpandableContainer } from 'twenty-ui/layout';
|
||||
|
||||
import { NavigationDrawerItemsCollapsableContainer } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItemsCollapsableContainer';
|
||||
|
||||
const StyledFolderExpandableWrapper = styled.div`
|
||||
& > div {
|
||||
overflow: visible !important;
|
||||
}
|
||||
`;
|
||||
|
||||
type NavigationMenuItemFolderLayoutProps = {
|
||||
header: ReactNode;
|
||||
isOpen: boolean;
|
||||
isGroup: boolean;
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export const NavigationMenuItemFolderLayout = ({
|
||||
header,
|
||||
isOpen,
|
||||
isGroup,
|
||||
children,
|
||||
}: NavigationMenuItemFolderLayoutProps) => {
|
||||
const [skipInitialExpandAnimation] = useState(() => isOpen);
|
||||
|
||||
return (
|
||||
<NavigationDrawerItemsCollapsableContainer isGroup={isGroup}>
|
||||
{header}
|
||||
<StyledFolderExpandableWrapper>
|
||||
<AnimatedExpandableContainer
|
||||
isExpanded={isOpen}
|
||||
dimension="height"
|
||||
mode="fit-content"
|
||||
containAnimation
|
||||
initial={!skipInitialExpandAnimation}
|
||||
>
|
||||
{children}
|
||||
</AnimatedExpandableContainer>
|
||||
</StyledFolderExpandableWrapper>
|
||||
</NavigationDrawerItemsCollapsableContainer>
|
||||
);
|
||||
};
|
||||
-120
@@ -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/display/folder/components/WorkspaceNavigationMenuItemFolderSubItem';
|
||||
import { FOLDER_ICON_DEFAULT } from '@/navigation-menu-item/common/constants/FolderIconDefault';
|
||||
import { DEFAULT_NAVIGATION_MENU_ITEM_COLOR_FOLDER } from '@/navigation-menu-item/common/constants/NavigationMenuItemDefaultColorFolder';
|
||||
import { useWorkspaceFolderOpenState } from '@/navigation-menu-item/display/folder/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 NavigationMenuItemFolderReadOnlyProps = {
|
||||
folderId: string;
|
||||
folderName: string;
|
||||
folderIconKey?: string | null;
|
||||
folderColor?: string | null;
|
||||
navigationMenuItems: NavigationMenuItem[];
|
||||
isGroup: boolean;
|
||||
};
|
||||
|
||||
export const NavigationMenuItemFolderReadOnly = ({
|
||||
folderId,
|
||||
folderName,
|
||||
folderIconKey,
|
||||
folderColor,
|
||||
navigationMenuItems,
|
||||
isGroup,
|
||||
}: NavigationMenuItemFolderReadOnlyProps) => {
|
||||
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>
|
||||
);
|
||||
};
|
||||
+52
-45
@@ -1,15 +1,16 @@
|
||||
import { type ReactNode } from 'react';
|
||||
import { NavigationMenuItemType } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
|
||||
|
||||
import { isLayoutCustomizationModeEnabledState } from '@/layout-customization/states/isLayoutCustomizationModeEnabledState';
|
||||
import { getEffectiveNavigationMenuItemColor } from '@/navigation-menu-item/common/utils/getEffectiveNavigationMenuItemColor';
|
||||
import { NavigationMenuItemIcon } from '@/navigation-menu-item/display/components/NavigationMenuItemIcon';
|
||||
import { type NavigationMenuItemClickParams } from '@/navigation-menu-item/display/hooks/useWorkspaceSectionItems';
|
||||
import { getNavigationMenuItemComputedLink } from '@/navigation-menu-item/display/utils/getNavigationMenuItemComputedLink';
|
||||
import { getNavigationMenuItemLabel } from '@/navigation-menu-item/display/utils/getNavigationMenuItemLabel';
|
||||
import type { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { getObjectMetadataForNavigationMenuItem } from '@/navigation-menu-item/display/object/utils/getObjectMetadataForNavigationMenuItem';
|
||||
import { getNavigationMenuItemObjectNameSingular } from '@/navigation-menu-item/display/object/utils/getNavigationMenuItemObjectNameSingular';
|
||||
import { getObjectNavigationMenuItemSecondaryLabel } from '@/navigation-menu-item/display/object/utils/getObjectNavigationMenuItemSecondaryLabel';
|
||||
import { getObjectMetadataForNavigationMenuItem } from '@/navigation-menu-item/display/object/utils/getObjectMetadataForNavigationMenuItem';
|
||||
import { getNavigationMenuItemComputedLink } from '@/navigation-menu-item/display/utils/getNavigationMenuItemComputedLink';
|
||||
import { getNavigationMenuItemLabel } from '@/navigation-menu-item/display/utils/getNavigationMenuItemLabel';
|
||||
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
|
||||
import { NavigationDrawerSubItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSubItem';
|
||||
import { getNavigationSubItemLeftAdornment } from '@/ui/navigation/navigation-drawer/utils/getNavigationSubItemLeftAdornment';
|
||||
@@ -17,57 +18,35 @@ import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomState
|
||||
import { viewsSelector } from '@/views/states/selectors/viewsSelector';
|
||||
import { ViewKey } from '@/views/types/ViewKey';
|
||||
|
||||
type WorkspaceNavigationMenuItemFolderSubItemProps = {
|
||||
type NavigationMenuItemFolderSubItemProps = {
|
||||
navigationMenuItem: NavigationMenuItem;
|
||||
index: number;
|
||||
arrayLength: number;
|
||||
selectedNavigationMenuItemIndex: number;
|
||||
onNavigationMenuItemClick?: (params: NavigationMenuItemClickParams) => void;
|
||||
selectedNavigationMenuItemId: string | null;
|
||||
isContextDragging: boolean;
|
||||
isDragging: boolean;
|
||||
rightOptions?: ReactNode;
|
||||
onClick?: () => void;
|
||||
onNavigationMenuItemClick?: (params: {
|
||||
item: NavigationMenuItem;
|
||||
objectMetadataItem?: ObjectMetadataItem;
|
||||
}) => void;
|
||||
selectedNavigationMenuItemId?: string | null;
|
||||
};
|
||||
|
||||
export const WorkspaceNavigationMenuItemFolderSubItem = ({
|
||||
export const NavigationMenuItemFolderSubItem = ({
|
||||
navigationMenuItem,
|
||||
index,
|
||||
arrayLength,
|
||||
selectedNavigationMenuItemIndex,
|
||||
isDragging,
|
||||
rightOptions,
|
||||
onClick,
|
||||
onNavigationMenuItemClick,
|
||||
selectedNavigationMenuItemId,
|
||||
isContextDragging,
|
||||
}: WorkspaceNavigationMenuItemFolderSubItemProps) => {
|
||||
const isLayoutCustomizationModeEnabled = useAtomStateValue(
|
||||
isLayoutCustomizationModeEnabledState,
|
||||
);
|
||||
}: NavigationMenuItemFolderSubItemProps) => {
|
||||
const objectMetadataItems = useAtomStateValue(objectMetadataItemsSelector);
|
||||
const views = useAtomStateValue(viewsSelector);
|
||||
|
||||
const objectMetadataItem =
|
||||
navigationMenuItem.type === NavigationMenuItemType.OBJECT ||
|
||||
navigationMenuItem.type === NavigationMenuItemType.VIEW ||
|
||||
navigationMenuItem.type === NavigationMenuItemType.RECORD
|
||||
? getObjectMetadataForNavigationMenuItem(
|
||||
navigationMenuItem,
|
||||
objectMetadataItems,
|
||||
views,
|
||||
)
|
||||
: null;
|
||||
|
||||
const isEditableInEditMode =
|
||||
isLayoutCustomizationModeEnabled &&
|
||||
isDefined(onNavigationMenuItemClick) &&
|
||||
(navigationMenuItem.type === NavigationMenuItemType.LINK ||
|
||||
isDefined(objectMetadataItem));
|
||||
|
||||
const handleEditModeClick =
|
||||
isEditableInEditMode && isDefined(onNavigationMenuItemClick)
|
||||
? () =>
|
||||
onNavigationMenuItemClick({
|
||||
item: navigationMenuItem,
|
||||
objectMetadataItem: objectMetadataItem ?? undefined,
|
||||
})
|
||||
: undefined;
|
||||
|
||||
const label = getNavigationMenuItemLabel(
|
||||
navigationMenuItem,
|
||||
objectMetadataItems,
|
||||
@@ -85,10 +64,36 @@ export const WorkspaceNavigationMenuItemFolderSubItem = ({
|
||||
);
|
||||
|
||||
const view = isDefined(navigationMenuItem.viewId)
|
||||
? views.find((view) => view.id === navigationMenuItem.viewId)
|
||||
? views.find((viewItem) => viewItem.id === navigationMenuItem.viewId)
|
||||
: undefined;
|
||||
const isIndexView = view?.key === ViewKey.INDEX;
|
||||
|
||||
const objectMetadataItem =
|
||||
navigationMenuItem.type === NavigationMenuItemType.OBJECT ||
|
||||
navigationMenuItem.type === NavigationMenuItemType.VIEW ||
|
||||
navigationMenuItem.type === NavigationMenuItemType.RECORD
|
||||
? getObjectMetadataForNavigationMenuItem(
|
||||
navigationMenuItem,
|
||||
objectMetadataItems,
|
||||
views,
|
||||
)
|
||||
: null;
|
||||
|
||||
const isEditable =
|
||||
isDefined(onNavigationMenuItemClick) &&
|
||||
(navigationMenuItem.type === NavigationMenuItemType.LINK ||
|
||||
isDefined(objectMetadataItem));
|
||||
|
||||
const handleClick =
|
||||
onClick ??
|
||||
(isEditable
|
||||
? () =>
|
||||
onNavigationMenuItemClick({
|
||||
item: navigationMenuItem,
|
||||
objectMetadataItem: objectMetadataItem ?? undefined,
|
||||
})
|
||||
: undefined);
|
||||
|
||||
return (
|
||||
<NavigationDrawerSubItem
|
||||
secondaryLabel={
|
||||
@@ -103,8 +108,9 @@ export const WorkspaceNavigationMenuItemFolderSubItem = ({
|
||||
Icon={() => (
|
||||
<NavigationMenuItemIcon navigationMenuItem={navigationMenuItem} />
|
||||
)}
|
||||
to={isContextDragging || handleEditModeClick ? undefined : computedLink}
|
||||
onClick={handleEditModeClick}
|
||||
iconColor={getEffectiveNavigationMenuItemColor(navigationMenuItem)}
|
||||
to={isDragging || handleClick ? undefined : computedLink}
|
||||
onClick={handleClick}
|
||||
active={index === selectedNavigationMenuItemIndex}
|
||||
isSelectedInEditMode={
|
||||
selectedNavigationMenuItemId === navigationMenuItem.id
|
||||
@@ -114,7 +120,8 @@ export const WorkspaceNavigationMenuItemFolderSubItem = ({
|
||||
arrayLength,
|
||||
selectedIndex: selectedNavigationMenuItemIndex,
|
||||
})}
|
||||
isDragging={isContextDragging}
|
||||
rightOptions={rightOptions}
|
||||
isDragging={isDragging}
|
||||
triggerEvent="CLICK"
|
||||
/>
|
||||
);
|
||||
-81
@@ -1,81 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
import { IconFolder } from 'twenty-ui/display';
|
||||
|
||||
import { FavoritesFolderItem } from '@/navigation-menu-item/display/sections/favorites/components/FavoritesFolderItem';
|
||||
import { useCreateNavigationMenuItemFolder } from '@/navigation-menu-item/display/folder/hooks/useCreateNavigationMenuItemFolder';
|
||||
import { useNavigationMenuItemsByFolder } from '@/navigation-menu-item/display/folder/hooks/useNavigationMenuItemsByFolder';
|
||||
import { isNavigationMenuItemFolderCreatingState } from '@/navigation-menu-item/common/states/isNavigationMenuItemFolderCreatingState';
|
||||
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
|
||||
import { NavigationDrawerAnimatedCollapseWrapper } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerAnimatedCollapseWrapper';
|
||||
import { NavigationDrawerInput } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerInput';
|
||||
|
||||
export const NavigationMenuItemFolders = () => {
|
||||
const [newFolderName, setNewFolderName] = useState('');
|
||||
|
||||
const { userNavigationMenuItemsByFolder } = useNavigationMenuItemsByFolder();
|
||||
const { createNewNavigationMenuItemFolder } =
|
||||
useCreateNavigationMenuItemFolder();
|
||||
|
||||
const [
|
||||
isNavigationMenuItemFolderCreating,
|
||||
setIsNavigationMenuItemFolderCreating,
|
||||
] = useAtomState(isNavigationMenuItemFolderCreatingState);
|
||||
|
||||
const handleNavigationMenuItemFolderNameChange = (value: string) => {
|
||||
setNewFolderName(value);
|
||||
};
|
||||
|
||||
const handleSubmitNavigationMenuItemFolderCreation = async (
|
||||
value: string,
|
||||
) => {
|
||||
if (value === '') return;
|
||||
|
||||
setIsNavigationMenuItemFolderCreating(false);
|
||||
setNewFolderName('');
|
||||
await createNewNavigationMenuItemFolder(value);
|
||||
return true;
|
||||
};
|
||||
|
||||
const handleClickOutside = async (
|
||||
_event: MouseEvent | TouchEvent,
|
||||
value: string,
|
||||
) => {
|
||||
if (!value) {
|
||||
setIsNavigationMenuItemFolderCreating(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsNavigationMenuItemFolderCreating(false);
|
||||
setNewFolderName('');
|
||||
await createNewNavigationMenuItemFolder(value);
|
||||
};
|
||||
|
||||
const handleCancelNavigationMenuItemFolderCreation = () => {
|
||||
setNewFolderName('');
|
||||
setIsNavigationMenuItemFolderCreating(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{isNavigationMenuItemFolderCreating && (
|
||||
<NavigationDrawerAnimatedCollapseWrapper>
|
||||
<NavigationDrawerInput
|
||||
Icon={IconFolder}
|
||||
value={newFolderName}
|
||||
onChange={handleNavigationMenuItemFolderNameChange}
|
||||
onSubmit={handleSubmitNavigationMenuItemFolderCreation}
|
||||
onCancel={handleCancelNavigationMenuItemFolderCreation}
|
||||
onClickOutside={handleClickOutside}
|
||||
/>
|
||||
</NavigationDrawerAnimatedCollapseWrapper>
|
||||
)}
|
||||
{userNavigationMenuItemsByFolder.map((folder) => (
|
||||
<FavoritesFolderItem
|
||||
key={folder.id}
|
||||
folder={folder}
|
||||
isGroup={userNavigationMenuItemsByFolder.length > 1}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
-309
@@ -1,309 +0,0 @@
|
||||
import { styled } from '@linaria/react';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import React, { useContext, useState } from 'react';
|
||||
import { SidePanelPages } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
IconChevronDown,
|
||||
IconChevronRight,
|
||||
IconPlus,
|
||||
useIcons,
|
||||
} from 'twenty-ui/display';
|
||||
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
import { useIsDropDisabledForSection } from '@/navigation-menu-item/display/dnd/hooks/useIsDropDisabledForSection';
|
||||
import { useOpenAddItemToFolderPage } from '@/navigation-menu-item/edit/hooks/useOpenAddItemToFolderPage';
|
||||
import { useWorkspaceFolderOpenState } from '@/navigation-menu-item/display/folder/hooks/useWorkspaceFolderOpenState';
|
||||
import { addMenuItemInsertionContextState } from '@/navigation-menu-item/common/states/addMenuItemInsertionContextState';
|
||||
import { sidePanelPageState } from '@/side-panel/states/sidePanelPageState';
|
||||
import { AnimatedExpandableContainer } from 'twenty-ui/layout';
|
||||
import { useIsMobile } from 'twenty-ui/utilities';
|
||||
|
||||
import { NavigationItemDropTarget } from '@/navigation-menu-item/display/dnd/components/NavigationItemDropTarget';
|
||||
import {
|
||||
FOLDER_HEADER_SLOT_COLLISION_PRIORITY,
|
||||
WorkspaceDndKitDroppableSlot,
|
||||
} from '@/navigation-menu-item/display/dnd/components/WorkspaceDndKitDroppableSlot';
|
||||
import { WorkspaceDndKitSortableItem } from '@/navigation-menu-item/display/dnd/components/WorkspaceDndKitSortableItem';
|
||||
import { WorkspaceNavigationMenuItemFolderSubItem } from '@/navigation-menu-item/display/folder/components/WorkspaceNavigationMenuItemFolderSubItem';
|
||||
import { FOLDER_ICON_DEFAULT } from '@/navigation-menu-item/common/constants/FolderIconDefault';
|
||||
import { DEFAULT_NAVIGATION_MENU_ITEM_COLOR_FOLDER } from '@/navigation-menu-item/common/constants/NavigationMenuItemDefaultColorFolder';
|
||||
import { NavigationMenuItemDroppableIds } from '@/navigation-menu-item/common/constants/NavigationMenuItemDroppableIds';
|
||||
import { NavigationSections } from '@/navigation-menu-item/common/constants/NavigationSections.constants';
|
||||
import { NavigationDropTargetContext } from '@/navigation-menu-item/common/contexts/NavigationDropTargetContext';
|
||||
import { NavigationMenuItemDragContext } from '@/navigation-menu-item/common/contexts/NavigationMenuItemDragContext';
|
||||
import { SortableDropTargetRefContext } from '@/navigation-menu-item/common/contexts/SortableDropTargetRefContext';
|
||||
import { type NavigationMenuItemClickParams } from '@/navigation-menu-item/display/hooks/useWorkspaceSectionItems';
|
||||
import { isLayoutCustomizationModeEnabledState } from '@/layout-customization/states/isLayoutCustomizationModeEnabledState';
|
||||
import { getDndKitDropTargetId } from '@/navigation-menu-item/common/utils/getDndKitDropTargetId';
|
||||
import { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem';
|
||||
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
|
||||
import { NavigationDrawerItemsCollapsableContainer } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItemsCollapsableContainer';
|
||||
import { NavigationDrawerSubItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSubItem';
|
||||
import { getNavigationSubItemLeftAdornment } from '@/ui/navigation/navigation-drawer/utils/getNavigationSubItemLeftAdornment';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
|
||||
const StyledFolderContainer = styled.div<{ $isSelectedInEditMode: boolean }>`
|
||||
border: ${({ $isSelectedInEditMode }) =>
|
||||
$isSelectedInEditMode
|
||||
? `1px solid ${themeCssVariables.color.blue}`
|
||||
: '1px solid transparent'};
|
||||
border-radius: ${themeCssVariables.border.radius.sm};
|
||||
transition: background-color 150ms ease-in-out;
|
||||
|
||||
&[data-drag-over-header='true'] {
|
||||
background-color: ${themeCssVariables.background.transparent.blue};
|
||||
}
|
||||
|
||||
&[data-forbidden-drop-target='true'] {
|
||||
background-color: ${themeCssVariables.background.transparent.danger};
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledFolderDroppableContent = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
`;
|
||||
|
||||
const StyledFolderExpandableWrapper = styled.div`
|
||||
& > div {
|
||||
overflow: visible !important;
|
||||
}
|
||||
`;
|
||||
|
||||
type WorkspaceNavigationMenuItemsFolderProps = {
|
||||
folderId: string;
|
||||
folderName: string;
|
||||
folderIconKey?: string | null;
|
||||
folderColor?: string | null;
|
||||
navigationMenuItems: NavigationMenuItem[];
|
||||
isGroup: boolean;
|
||||
isSelectedInEditMode?: boolean;
|
||||
onEditModeClick?: () => void;
|
||||
onNavigationMenuItemClick?: (params: NavigationMenuItemClickParams) => void;
|
||||
selectedNavigationMenuItemId?: string | null;
|
||||
isDragging?: boolean;
|
||||
};
|
||||
|
||||
export const WorkspaceNavigationMenuItemsFolder = ({
|
||||
folderId,
|
||||
folderName,
|
||||
folderIconKey,
|
||||
folderColor,
|
||||
navigationMenuItems,
|
||||
isGroup,
|
||||
isSelectedInEditMode = false,
|
||||
onEditModeClick,
|
||||
onNavigationMenuItemClick,
|
||||
selectedNavigationMenuItemId = null,
|
||||
isDragging = false,
|
||||
}: WorkspaceNavigationMenuItemsFolderProps) => {
|
||||
const isLayoutCustomizationModeEnabled = useAtomStateValue(
|
||||
isLayoutCustomizationModeEnabledState,
|
||||
);
|
||||
const { theme } = useContext(ThemeContext);
|
||||
const { getIcon } = useIcons();
|
||||
const FolderIcon = getIcon(folderIconKey ?? FOLDER_ICON_DEFAULT);
|
||||
const isMobile = useIsMobile();
|
||||
const { t } = useLingui();
|
||||
const { isOpen, handleToggle, selectedNavigationMenuItemIndex } =
|
||||
useWorkspaceFolderOpenState({ folderId, navigationMenuItems });
|
||||
const { openAddItemToFolderPage } = useOpenAddItemToFolderPage();
|
||||
const sidePanelPage = useAtomStateValue(sidePanelPageState);
|
||||
const addMenuItemInsertionContext = useAtomStateValue(
|
||||
addMenuItemInsertionContextState,
|
||||
);
|
||||
|
||||
const folderContentLengthForTree = isLayoutCustomizationModeEnabled
|
||||
? navigationMenuItems.length + 1
|
||||
: navigationMenuItems.length;
|
||||
|
||||
const handleAddMenuItemToFolder = () => {
|
||||
openAddItemToFolderPage({
|
||||
targetFolderId: folderId,
|
||||
targetIndex: navigationMenuItems.length,
|
||||
resetNavigationStack: true,
|
||||
});
|
||||
};
|
||||
|
||||
const shouldUseEditModeClick =
|
||||
isLayoutCustomizationModeEnabled && isDefined(onEditModeClick);
|
||||
const handleClick = shouldUseEditModeClick
|
||||
? (e?: React.MouseEvent) => {
|
||||
e?.stopPropagation();
|
||||
if (isSelectedInEditMode) {
|
||||
handleToggle();
|
||||
} else {
|
||||
onEditModeClick?.();
|
||||
}
|
||||
}
|
||||
: handleToggle;
|
||||
|
||||
const [skipInitialExpandAnimation] = useState(() => isOpen);
|
||||
|
||||
const { isDragging: isContextDragging } = useContext(
|
||||
NavigationMenuItemDragContext,
|
||||
);
|
||||
const setSortableDropTargetRef = useContext(SortableDropTargetRefContext);
|
||||
const folderContentDropDisabled = useIsDropDisabledForSection(true);
|
||||
|
||||
const { activeDropTargetId, forbiddenDropTargetId } = useContext(
|
||||
NavigationDropTargetContext,
|
||||
);
|
||||
const folderHeaderDroppableId = `${NavigationMenuItemDroppableIds.WORKSPACE_FOLDER_HEADER_PREFIX}${folderId}`;
|
||||
const folderContentDroppableId = `${NavigationMenuItemDroppableIds.WORKSPACE_FOLDER_PREFIX}${folderId}`;
|
||||
const folderHeaderSlotId = getDndKitDropTargetId(folderHeaderDroppableId, 0);
|
||||
const isForbiddenDropTarget =
|
||||
isDefined(forbiddenDropTargetId) &&
|
||||
(forbiddenDropTargetId.startsWith(`${folderContentDroppableId}::`) ||
|
||||
forbiddenDropTargetId.startsWith(`${folderHeaderDroppableId}::`));
|
||||
const isDragOverFolderHeader =
|
||||
!isForbiddenDropTarget && activeDropTargetId === folderHeaderSlotId;
|
||||
const isCompact =
|
||||
isLayoutCustomizationModeEnabled || navigationMenuItems.length === 0;
|
||||
|
||||
const headerItem = (
|
||||
<NavigationDrawerItem
|
||||
label={folderName}
|
||||
Icon={FolderIcon}
|
||||
iconColor={
|
||||
isDefined(folderColor)
|
||||
? folderColor
|
||||
: DEFAULT_NAVIGATION_MENU_ITEM_COLOR_FOLDER
|
||||
}
|
||||
active={!isOpen && selectedNavigationMenuItemIndex >= 0}
|
||||
onClick={handleClick}
|
||||
className="navigation-drawer-item"
|
||||
triggerEvent="CLICK"
|
||||
preventCollapseOnMobile={isMobile}
|
||||
isDragging={isDragging}
|
||||
alwaysShowRightOptions
|
||||
rightOptions={
|
||||
<div
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleToggle();
|
||||
}}
|
||||
>
|
||||
{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}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledFolderContainer
|
||||
$isSelectedInEditMode={isSelectedInEditMode}
|
||||
data-drag-over-header={isDragOverFolderHeader ? 'true' : undefined}
|
||||
data-forbidden-drop-target={isForbiddenDropTarget ? 'true' : undefined}
|
||||
>
|
||||
<NavigationDrawerItemsCollapsableContainer isGroup={isGroup}>
|
||||
<div ref={setSortableDropTargetRef ?? undefined}>
|
||||
<WorkspaceDndKitDroppableSlot
|
||||
droppableId={folderHeaderDroppableId}
|
||||
index={0}
|
||||
disabled={folderContentDropDisabled}
|
||||
collisionPriority={FOLDER_HEADER_SLOT_COLLISION_PRIORITY}
|
||||
>
|
||||
{headerItem}
|
||||
</WorkspaceDndKitDroppableSlot>
|
||||
</div>
|
||||
<StyledFolderExpandableWrapper>
|
||||
<AnimatedExpandableContainer
|
||||
isExpanded={isOpen}
|
||||
dimension="height"
|
||||
mode="fit-content"
|
||||
containAnimation
|
||||
initial={!skipInitialExpandAnimation}
|
||||
>
|
||||
<StyledFolderDroppableContent>
|
||||
{navigationMenuItems.map((navigationMenuItem, index) => (
|
||||
<React.Fragment key={navigationMenuItem.id}>
|
||||
<NavigationItemDropTarget
|
||||
folderId={folderId}
|
||||
index={index}
|
||||
sectionId={NavigationSections.WORKSPACE}
|
||||
compact={isCompact}
|
||||
dropTargetIdOverride={getDndKitDropTargetId(
|
||||
folderContentDroppableId,
|
||||
index,
|
||||
)}
|
||||
/>
|
||||
<WorkspaceDndKitSortableItem
|
||||
id={navigationMenuItem.id}
|
||||
index={index}
|
||||
group={folderContentDroppableId}
|
||||
disabled={
|
||||
!isLayoutCustomizationModeEnabled ||
|
||||
folderContentDropDisabled
|
||||
}
|
||||
>
|
||||
<WorkspaceNavigationMenuItemFolderSubItem
|
||||
navigationMenuItem={navigationMenuItem}
|
||||
index={index}
|
||||
arrayLength={folderContentLengthForTree}
|
||||
selectedNavigationMenuItemIndex={
|
||||
selectedNavigationMenuItemIndex
|
||||
}
|
||||
onNavigationMenuItemClick={onNavigationMenuItemClick}
|
||||
selectedNavigationMenuItemId={
|
||||
selectedNavigationMenuItemId ?? null
|
||||
}
|
||||
isContextDragging={isContextDragging}
|
||||
/>
|
||||
</WorkspaceDndKitSortableItem>
|
||||
</React.Fragment>
|
||||
))}
|
||||
<WorkspaceDndKitDroppableSlot
|
||||
droppableId={folderContentDroppableId}
|
||||
index={navigationMenuItems.length}
|
||||
disabled={folderContentDropDisabled}
|
||||
>
|
||||
<NavigationItemDropTarget
|
||||
folderId={folderId}
|
||||
index={navigationMenuItems.length}
|
||||
sectionId={NavigationSections.WORKSPACE}
|
||||
compact
|
||||
dropTargetIdOverride={getDndKitDropTargetId(
|
||||
folderContentDroppableId,
|
||||
navigationMenuItems.length,
|
||||
)}
|
||||
/>
|
||||
{isLayoutCustomizationModeEnabled && (
|
||||
<NavigationDrawerSubItem
|
||||
label={t`Add menu item`}
|
||||
Icon={IconPlus}
|
||||
onClick={handleAddMenuItemToFolder}
|
||||
triggerEvent="CLICK"
|
||||
variant="tertiary"
|
||||
isSelectedInEditMode={
|
||||
sidePanelPage === SidePanelPages.NavigationMenuAddItem &&
|
||||
addMenuItemInsertionContext?.targetFolderId === folderId
|
||||
}
|
||||
subItemState={getNavigationSubItemLeftAdornment({
|
||||
index: navigationMenuItems.length,
|
||||
arrayLength: folderContentLengthForTree,
|
||||
selectedIndex: selectedNavigationMenuItemIndex,
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
</WorkspaceDndKitDroppableSlot>
|
||||
</StyledFolderDroppableContent>
|
||||
</AnimatedExpandableContainer>
|
||||
</StyledFolderExpandableWrapper>
|
||||
</NavigationDrawerItemsCollapsableContainer>
|
||||
</StyledFolderContainer>
|
||||
);
|
||||
};
|
||||
+3
-3
@@ -15,15 +15,15 @@ import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomState
|
||||
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
|
||||
import { viewsSelector } from '@/views/states/selectors/viewsSelector';
|
||||
|
||||
type UseWorkspaceFolderOpenStateParams = {
|
||||
type UseNavigationMenuItemFolderOpenStateParams = {
|
||||
folderId: string;
|
||||
navigationMenuItems: NavigationMenuItem[];
|
||||
};
|
||||
|
||||
export const useWorkspaceFolderOpenState = ({
|
||||
export const useNavigationMenuItemFolderOpenState = ({
|
||||
folderId,
|
||||
navigationMenuItems,
|
||||
}: UseWorkspaceFolderOpenStateParams) => {
|
||||
}: UseNavigationMenuItemFolderOpenStateParams) => {
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const currentPath = location.pathname;
|
||||
+1
-1
@@ -21,7 +21,7 @@ export type NavigationMenuItemClickParams = {
|
||||
objectMetadataItem?: ObjectMetadataItem | null;
|
||||
};
|
||||
|
||||
export const useWorkspaceSectionItems = (): NavigationMenuItem[] => {
|
||||
export const useNavigationMenuItemSectionItems = (): NavigationMenuItem[] => {
|
||||
const { workspaceNavigationMenuItems } = useNavigationMenuItemsData();
|
||||
const { workspaceNavigationMenuItemsSorted } = useSortedNavigationMenuItems();
|
||||
const { workspaceNavigationMenuItemsByFolder } =
|
||||
+17
-15
@@ -1,19 +1,21 @@
|
||||
import { IconArrowUpRight } from 'twenty-ui/display';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
import { isLayoutCustomizationModeEnabledState } from '@/layout-customization/states/isLayoutCustomizationModeEnabledState';
|
||||
import { NavigationMenuItemIcon } from '@/navigation-menu-item/display/components/NavigationMenuItemIcon';
|
||||
import { getLinkNavigationMenuItemComputedLink } from '@/navigation-menu-item/display/link/utils/getLinkNavigationMenuItemComputedLink';
|
||||
import { getLinkNavigationMenuItemLabel } from '@/navigation-menu-item/display/link/utils/getLinkNavigationMenuItemLabel';
|
||||
import type { WorkspaceSectionItemContentProps } from '@/navigation-menu-item/display/sections/types/WorkspaceSectionItemContentProps';
|
||||
import type { NavigationMenuItemSectionContentProps } from '@/navigation-menu-item/display/sections/types/NavigationMenuItemSectionContentProps';
|
||||
import { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { IconArrowUpRight } from 'twenty-ui/display';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
type NavigationMenuItemLinkDisplayProps = WorkspaceSectionItemContentProps;
|
||||
type NavigationMenuItemLinkDisplayProps = NavigationMenuItemSectionContentProps;
|
||||
|
||||
export const NavigationMenuItemLinkDisplay = ({
|
||||
item,
|
||||
editModeProps,
|
||||
isDragging,
|
||||
rightOptions,
|
||||
}: NavigationMenuItemLinkDisplayProps) => {
|
||||
const isLayoutCustomizationModeEnabled = useAtomStateValue(
|
||||
isLayoutCustomizationModeEnabledState,
|
||||
@@ -22,6 +24,14 @@ export const NavigationMenuItemLinkDisplay = ({
|
||||
const label = getLinkNavigationMenuItemLabel(item);
|
||||
const computedLink = getLinkNavigationMenuItemComputedLink(item);
|
||||
|
||||
const defaultRightOptions = !isLayoutCustomizationModeEnabled && (
|
||||
<IconArrowUpRight
|
||||
size={themeCssVariables.icon.size.sm}
|
||||
stroke={themeCssVariables.icon.stroke.md}
|
||||
color={themeCssVariables.font.color.light}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<NavigationDrawerItem
|
||||
label={label}
|
||||
@@ -32,23 +42,15 @@ export const NavigationMenuItemLinkDisplay = ({
|
||||
}
|
||||
onClick={
|
||||
isLayoutCustomizationModeEnabled
|
||||
? editModeProps.onEditModeClick
|
||||
? editModeProps?.onEditModeClick
|
||||
: undefined
|
||||
}
|
||||
Icon={() => <NavigationMenuItemIcon navigationMenuItem={item} />}
|
||||
active={false}
|
||||
isSelectedInEditMode={editModeProps.isSelectedInEditMode}
|
||||
isSelectedInEditMode={editModeProps?.isSelectedInEditMode}
|
||||
isDragging={isDragging}
|
||||
triggerEvent="CLICK"
|
||||
rightOptions={
|
||||
!isLayoutCustomizationModeEnabled && (
|
||||
<IconArrowUpRight
|
||||
size={themeCssVariables.icon.size.sm}
|
||||
stroke={themeCssVariables.icon.stroke.md}
|
||||
color={themeCssVariables.font.color.light}
|
||||
/>
|
||||
)
|
||||
}
|
||||
rightOptions={rightOptions ?? defaultRightOptions}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
+5
@@ -1,3 +1,5 @@
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
import { isLayoutCustomizationModeEnabledState } from '@/layout-customization/states/isLayoutCustomizationModeEnabledState';
|
||||
import { ObjectIconWithViewOverlay } from '@/navigation-menu-item/display/view/components/ObjectIconWithViewOverlay';
|
||||
import { useObjectNavItemColor } from '@/navigation-menu-item/common/hooks/useObjectNavItemColor';
|
||||
@@ -28,6 +30,7 @@ export type NavigationDrawerItemForObjectMetadataItemProps = {
|
||||
onEditModeClick?: () => void;
|
||||
onActiveItemClickWhenNotInEditMode?: () => void;
|
||||
isDragging?: boolean;
|
||||
rightOptions?: ReactNode;
|
||||
};
|
||||
|
||||
export const NavigationDrawerItemForObjectMetadataItem = ({
|
||||
@@ -37,6 +40,7 @@ export const NavigationDrawerItemForObjectMetadataItem = ({
|
||||
onEditModeClick,
|
||||
onActiveItemClickWhenNotInEditMode: _onActiveItemClickWhenNotInEditMode,
|
||||
isDragging = false,
|
||||
rightOptions,
|
||||
}: NavigationDrawerItemForObjectMetadataItemProps) => {
|
||||
const isLayoutCustomizationModeEnabled = useAtomStateValue(
|
||||
isLayoutCustomizationModeEnabledState,
|
||||
@@ -171,6 +175,7 @@ export const NavigationDrawerItemForObjectMetadataItem = ({
|
||||
isSelectedInEditMode={isSelectedInEditMode}
|
||||
isDragging={isDragging}
|
||||
triggerEvent={isLayoutCustomizationModeEnabled ? 'CLICK' : undefined}
|
||||
rightOptions={rightOptions}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
+7
-4
@@ -1,16 +1,18 @@
|
||||
import { getObjectMetadataForNavigationMenuItem } from '@/navigation-menu-item/display/object/utils/getObjectMetadataForNavigationMenuItem';
|
||||
import { NavigationDrawerItemForObjectMetadataItem } from '@/navigation-menu-item/display/object/components/NavigationDrawerItemForObjectMetadataItem';
|
||||
import type { WorkspaceSectionItemContentProps } from '@/navigation-menu-item/display/sections/types/WorkspaceSectionItemContentProps';
|
||||
import type { NavigationMenuItemSectionContentProps } from '@/navigation-menu-item/display/sections/types/NavigationMenuItemSectionContentProps';
|
||||
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { viewsSelector } from '@/views/states/selectors/viewsSelector';
|
||||
|
||||
type NavigationMenuItemObjectDisplayProps = WorkspaceSectionItemContentProps;
|
||||
type NavigationMenuItemObjectDisplayProps =
|
||||
NavigationMenuItemSectionContentProps;
|
||||
|
||||
export const NavigationMenuItemObjectDisplay = ({
|
||||
item,
|
||||
editModeProps,
|
||||
isDragging,
|
||||
rightOptions,
|
||||
onActiveObjectMetadataItemClick,
|
||||
}: NavigationMenuItemObjectDisplayProps) => {
|
||||
const objectMetadataItems = useAtomStateValue(objectMetadataItemsSelector);
|
||||
@@ -27,9 +29,10 @@ export const NavigationMenuItemObjectDisplay = ({
|
||||
<NavigationDrawerItemForObjectMetadataItem
|
||||
objectMetadataItem={objectMetadataItem}
|
||||
navigationMenuItem={item}
|
||||
isSelectedInEditMode={editModeProps.isSelectedInEditMode}
|
||||
onEditModeClick={editModeProps.onEditModeClick}
|
||||
isSelectedInEditMode={editModeProps?.isSelectedInEditMode}
|
||||
onEditModeClick={editModeProps?.onEditModeClick}
|
||||
isDragging={isDragging}
|
||||
rightOptions={rightOptions}
|
||||
onActiveItemClickWhenNotInEditMode={
|
||||
onActiveObjectMetadataItemClick
|
||||
? () => onActiveObjectMetadataItemClick(objectMetadataItem, item.id)
|
||||
|
||||
+11
-10
@@ -1,30 +1,31 @@
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
import { NavigationItemDropTarget } from '@/navigation-menu-item/display/dnd/components/NavigationItemDropTarget';
|
||||
import { NavigationSections } from '@/navigation-menu-item/common/constants/NavigationSections.constants';
|
||||
import { NavigationMenuItemDroppableIds } from '@/navigation-menu-item/common/constants/NavigationMenuItemDroppableIds';
|
||||
import { NavigationSections } from '@/navigation-menu-item/common/constants/NavigationSections.constants';
|
||||
import { getDndKitDropTargetId } from '@/navigation-menu-item/common/utils/getDndKitDropTargetId';
|
||||
import { NavigationItemDropTarget } from '@/navigation-menu-item/display/dnd/components/NavigationItemDropTarget';
|
||||
|
||||
type WorkspaceOrphanDropTargetProps = {
|
||||
type NavigationMenuItemOrphanDropTargetProps = {
|
||||
index: number;
|
||||
compact?: boolean;
|
||||
children?: ReactNode;
|
||||
sectionId?: NavigationSections;
|
||||
droppableId?: string;
|
||||
};
|
||||
|
||||
export const WorkspaceOrphanDropTarget = ({
|
||||
export const NavigationMenuItemOrphanDropTarget = ({
|
||||
index,
|
||||
compact = false,
|
||||
children,
|
||||
}: WorkspaceOrphanDropTargetProps) => (
|
||||
sectionId = NavigationSections.WORKSPACE,
|
||||
droppableId = NavigationMenuItemDroppableIds.WORKSPACE_ORPHAN_NAVIGATION_MENU_ITEMS,
|
||||
}: NavigationMenuItemOrphanDropTargetProps) => (
|
||||
<NavigationItemDropTarget
|
||||
folderId={null}
|
||||
index={index}
|
||||
sectionId={NavigationSections.WORKSPACE}
|
||||
sectionId={sectionId}
|
||||
compact={compact}
|
||||
dropTargetIdOverride={getDndKitDropTargetId(
|
||||
NavigationMenuItemDroppableIds.WORKSPACE_ORPHAN_NAVIGATION_MENU_ITEMS,
|
||||
index,
|
||||
)}
|
||||
dropTargetIdOverride={getDndKitDropTargetId(droppableId, index)}
|
||||
>
|
||||
{children}
|
||||
</NavigationItemDropTarget>
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { AnimatedExpandableContainer } from 'twenty-ui/layout';
|
||||
|
||||
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';
|
||||
|
||||
type NavigationMenuItemSectionProps = {
|
||||
title: string;
|
||||
isOpen: boolean;
|
||||
onToggle: () => void;
|
||||
rightIcon?: ReactNode;
|
||||
alwaysShowRightIcon?: boolean;
|
||||
forceExpanded?: boolean;
|
||||
children: ReactNode;
|
||||
contentWrapper?: (children: ReactNode) => ReactNode;
|
||||
};
|
||||
|
||||
export const NavigationMenuItemSection = ({
|
||||
title,
|
||||
isOpen,
|
||||
onToggle,
|
||||
rightIcon,
|
||||
alwaysShowRightIcon,
|
||||
forceExpanded = false,
|
||||
children,
|
||||
contentWrapper,
|
||||
}: NavigationMenuItemSectionProps) => {
|
||||
const content = (
|
||||
<AnimatedExpandableContainer
|
||||
isExpanded={isOpen || forceExpanded}
|
||||
dimension="height"
|
||||
mode="fit-content"
|
||||
containAnimation
|
||||
initial={false}
|
||||
>
|
||||
{children}
|
||||
</AnimatedExpandableContainer>
|
||||
);
|
||||
|
||||
return (
|
||||
<NavigationDrawerSection>
|
||||
<NavigationDrawerAnimatedCollapseWrapper>
|
||||
<NavigationDrawerSectionTitle
|
||||
label={title}
|
||||
onClick={onToggle}
|
||||
rightIcon={rightIcon}
|
||||
alwaysShowRightIcon={alwaysShowRightIcon}
|
||||
isOpen={isOpen}
|
||||
/>
|
||||
</NavigationDrawerAnimatedCollapseWrapper>
|
||||
{contentWrapper ? contentWrapper(content) : content}
|
||||
</NavigationDrawerSection>
|
||||
);
|
||||
};
|
||||
-372
@@ -1,372 +0,0 @@
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { useContext, useState } from 'react';
|
||||
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
|
||||
|
||||
import { NavigationSections } from '@/navigation-menu-item/common/constants/NavigationSections.constants';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import { NavigationMenuItemType } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { IconFolder, IconFolderOpen, IconHeartOff } from 'twenty-ui/display';
|
||||
|
||||
import { LightIconButton } from 'twenty-ui/input';
|
||||
import { AnimatedExpandableContainer } from 'twenty-ui/layout';
|
||||
import { useIsMobile } from 'twenty-ui/utilities';
|
||||
|
||||
import { NavigationItemDropTarget } from '@/navigation-menu-item/display/dnd/components/NavigationItemDropTarget';
|
||||
import { WorkspaceDndKitSortableItem } from '@/navigation-menu-item/display/dnd/components/WorkspaceDndKitSortableItem';
|
||||
import { NavigationMenuItemFolderNavigationDrawerItemDropdown } from '@/navigation-menu-item/display/folder/components/NavigationMenuItemFolderNavigationDrawerItemDropdown';
|
||||
import { NavigationMenuItemIcon } from '@/navigation-menu-item/display/components/NavigationMenuItemIcon';
|
||||
import { DEFAULT_NAVIGATION_MENU_ITEM_COLOR_FOLDER } from '@/navigation-menu-item/common/constants/NavigationMenuItemDefaultColorFolder';
|
||||
import { NAVIGATION_MENU_ITEM_FOLDER_DELETE_MODAL_ID } from '@/navigation-menu-item/common/constants/NavigationMenuItemFolderDeleteModalId';
|
||||
import { NavigationMenuItemDragContext } from '@/navigation-menu-item/common/contexts/NavigationMenuItemDragContext';
|
||||
import { useDeleteNavigationMenuItem } from '@/navigation-menu-item/common/hooks/useDeleteNavigationMenuItem';
|
||||
import { useDeleteNavigationMenuItemFolder } from '@/navigation-menu-item/edit/folder/hooks/useDeleteNavigationMenuItemFolder';
|
||||
import { useRenameNavigationMenuItemFolder } from '@/navigation-menu-item/edit/folder/hooks/useRenameNavigationMenuItemFolder';
|
||||
import { openNavigationMenuItemFolderIdsState } from '@/navigation-menu-item/common/states/openNavigationMenuItemFolderIdsState';
|
||||
import { getEffectiveNavigationMenuItemColor } from '@/navigation-menu-item/common/utils/getEffectiveNavigationMenuItemColor';
|
||||
import { getNavigationMenuItemComputedLink } from '@/navigation-menu-item/display/utils/getNavigationMenuItemComputedLink';
|
||||
import { getNavigationMenuItemLabel } from '@/navigation-menu-item/display/utils/getNavigationMenuItemLabel';
|
||||
import { getNavigationMenuItemObjectNameSingular } from '@/navigation-menu-item/display/object/utils/getNavigationMenuItemObjectNameSingular';
|
||||
import { getObjectNavigationMenuItemSecondaryLabel } from '@/navigation-menu-item/display/object/utils/getObjectNavigationMenuItemSecondaryLabel';
|
||||
import { isLocationMatchingNavigationMenuItem } from '@/navigation-menu-item/common/utils/isLocationMatchingNavigationMenuItem';
|
||||
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
|
||||
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
|
||||
import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState';
|
||||
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
|
||||
import { useModal } from '@/ui/layout/modal/hooks/useModal';
|
||||
import { isModalOpenedComponentState } from '@/ui/layout/modal/states/isModalOpenedComponentState';
|
||||
import { NavigationDrawerInput } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerInput';
|
||||
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 '@/navigation-menu-item/common/states/currentNavigationMenuItemFolderIdState';
|
||||
import { getNavigationSubItemLeftAdornment } from '@/ui/navigation/navigation-drawer/utils/getNavigationSubItemLeftAdornment';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
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 { viewsSelector } from '@/views/states/selectors/viewsSelector';
|
||||
import { ViewKey } from '@/views/types/ViewKey';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
|
||||
type FavoritesFolderItemProps = {
|
||||
folder: {
|
||||
id: string;
|
||||
folderName: string;
|
||||
navigationMenuItems: NavigationMenuItem[];
|
||||
};
|
||||
isGroup: boolean;
|
||||
isWorkspaceFolder?: boolean;
|
||||
};
|
||||
|
||||
export const FavoritesFolderItem = ({
|
||||
folder,
|
||||
isGroup,
|
||||
isWorkspaceFolder = false,
|
||||
}: FavoritesFolderItemProps) => {
|
||||
const { t } = useLingui();
|
||||
const objectMetadataItems = useAtomStateValue(objectMetadataItemsSelector);
|
||||
const views = useAtomStateValue(viewsSelector);
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const currentPath = location.pathname;
|
||||
const currentViewPath = location.pathname + location.search;
|
||||
const { isDragging } = useContext(NavigationMenuItemDragContext);
|
||||
const [
|
||||
isNavigationMenuItemFolderRenaming,
|
||||
setIsNavigationMenuItemFolderRenaming,
|
||||
] = useState(false);
|
||||
const [navigationMenuItemFolderName, setNavigationMenuItemFolderName] =
|
||||
useState(folder.folderName);
|
||||
const { openModal } = useModal();
|
||||
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
const [openNavigationMenuItemFolderIds, setOpenNavigationMenuItemFolderIds] =
|
||||
useAtomState(openNavigationMenuItemFolderIdsState);
|
||||
|
||||
const setCurrentNavigationMenuItemFolderId = useSetAtomState(
|
||||
currentNavigationMenuItemFolderIdState,
|
||||
);
|
||||
|
||||
const isOpen = openNavigationMenuItemFolderIds.includes(folder.id);
|
||||
|
||||
const handleToggle = () => {
|
||||
if (isMobile) {
|
||||
setCurrentNavigationMenuItemFolderId((prev) =>
|
||||
prev === folder.id ? null : folder.id,
|
||||
);
|
||||
} else {
|
||||
setOpenNavigationMenuItemFolderIds((currentOpenFolders) => {
|
||||
if (isOpen) {
|
||||
return currentOpenFolders.filter((id) => id !== folder.id);
|
||||
} else {
|
||||
return [...currentOpenFolders, folder.id];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!isOpen) {
|
||||
const firstNonLinkItem = folder.navigationMenuItems.find((item) => {
|
||||
if (item.type === NavigationMenuItemType.LINK) {
|
||||
return false;
|
||||
}
|
||||
const computedLink = getNavigationMenuItemComputedLink(
|
||||
item,
|
||||
objectMetadataItems,
|
||||
views,
|
||||
);
|
||||
return isNonEmptyString(computedLink);
|
||||
});
|
||||
if (isDefined(firstNonLinkItem)) {
|
||||
const link = getNavigationMenuItemComputedLink(
|
||||
firstNonLinkItem,
|
||||
objectMetadataItems,
|
||||
views,
|
||||
);
|
||||
if (isNonEmptyString(link)) {
|
||||
navigate(link);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const { renameNavigationMenuItemFolder } =
|
||||
useRenameNavigationMenuItemFolder();
|
||||
const { deleteNavigationMenuItemFolder } =
|
||||
useDeleteNavigationMenuItemFolder();
|
||||
|
||||
const dropdownId = `navigation-menu-item-folder-edit-${folder.id}`;
|
||||
|
||||
const isDropdownOpen = useAtomComponentStateValue(
|
||||
isDropdownOpenComponentState,
|
||||
dropdownId,
|
||||
);
|
||||
|
||||
const { closeDropdown } = useCloseDropdown();
|
||||
|
||||
const selectedNavigationMenuItemIndex = folder.navigationMenuItems.findIndex(
|
||||
(item) => {
|
||||
const computedLink = getNavigationMenuItemComputedLink(
|
||||
item,
|
||||
objectMetadataItems,
|
||||
views,
|
||||
);
|
||||
return isLocationMatchingNavigationMenuItem(
|
||||
currentPath,
|
||||
currentViewPath,
|
||||
item.type,
|
||||
computedLink,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
const { deleteNavigationMenuItem } = useDeleteNavigationMenuItem();
|
||||
|
||||
const navigationMenuItemFolderContentLength =
|
||||
folder.navigationMenuItems.length;
|
||||
|
||||
const handleSubmitRename = async (value: string) => {
|
||||
if (value === '') return;
|
||||
await renameNavigationMenuItemFolder(folder.id, value);
|
||||
setIsNavigationMenuItemFolderRenaming(false);
|
||||
return true;
|
||||
};
|
||||
|
||||
const handleCancelRename = () => {
|
||||
setNavigationMenuItemFolderName(folder.folderName);
|
||||
setIsNavigationMenuItemFolderRenaming(false);
|
||||
};
|
||||
|
||||
const handleClickOutside = async (
|
||||
_event: MouseEvent | TouchEvent,
|
||||
value: string,
|
||||
) => {
|
||||
if (!value) {
|
||||
setIsNavigationMenuItemFolderRenaming(false);
|
||||
return;
|
||||
}
|
||||
|
||||
await renameNavigationMenuItemFolder(folder.id, value);
|
||||
setIsNavigationMenuItemFolderRenaming(false);
|
||||
};
|
||||
|
||||
const modalId = `${NAVIGATION_MENU_ITEM_FOLDER_DELETE_MODAL_ID}-${folder.id}`;
|
||||
|
||||
const handleNavigationMenuItemFolderDelete = async () => {
|
||||
if (folder.navigationMenuItems.length > 0) {
|
||||
openModal(modalId);
|
||||
closeDropdown(dropdownId);
|
||||
} else {
|
||||
await deleteNavigationMenuItemFolder(folder.id);
|
||||
closeDropdown(dropdownId);
|
||||
}
|
||||
};
|
||||
|
||||
const handleConfirmDelete = async () => {
|
||||
await deleteNavigationMenuItemFolder(folder.id);
|
||||
};
|
||||
|
||||
const rightOptions = isWorkspaceFolder ? undefined : (
|
||||
<NavigationMenuItemFolderNavigationDrawerItemDropdown
|
||||
folderId={folder.id}
|
||||
onRename={() => setIsNavigationMenuItemFolderRenaming(true)}
|
||||
onDelete={handleNavigationMenuItemFolderDelete}
|
||||
closeDropdown={() => {
|
||||
closeDropdown(dropdownId);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
const isModalOpened = useAtomComponentStateValue(
|
||||
isModalOpenedComponentState,
|
||||
modalId,
|
||||
);
|
||||
|
||||
const navigationMenuItemCount = folder.navigationMenuItems.length;
|
||||
|
||||
const folderDroppableId = `folder-${folder.id}`;
|
||||
|
||||
return (
|
||||
<>
|
||||
<NavigationDrawerItemsCollapsableContainer
|
||||
key={folder.id}
|
||||
isGroup={isGroup}
|
||||
>
|
||||
{isNavigationMenuItemFolderRenaming ? (
|
||||
<NavigationDrawerInput
|
||||
Icon={IconFolder}
|
||||
value={navigationMenuItemFolderName}
|
||||
onChange={setNavigationMenuItemFolderName}
|
||||
onSubmit={handleSubmitRename}
|
||||
onCancel={handleCancelRename}
|
||||
onClickOutside={handleClickOutside}
|
||||
/>
|
||||
) : (
|
||||
<NavigationItemDropTarget
|
||||
folderId={folder.id}
|
||||
index={0}
|
||||
sectionId={
|
||||
isWorkspaceFolder
|
||||
? NavigationSections.WORKSPACE
|
||||
: NavigationSections.FAVORITES
|
||||
}
|
||||
>
|
||||
<NavigationDrawerItem
|
||||
label={folder.folderName}
|
||||
Icon={isOpen ? IconFolderOpen : IconFolder}
|
||||
iconColor={DEFAULT_NAVIGATION_MENU_ITEM_COLOR_FOLDER}
|
||||
onClick={handleToggle}
|
||||
rightOptions={rightOptions}
|
||||
className="navigation-drawer-item"
|
||||
isRightOptionsDropdownOpen={isDropdownOpen}
|
||||
triggerEvent="CLICK"
|
||||
preventCollapseOnMobile={isMobile}
|
||||
/>
|
||||
</NavigationItemDropTarget>
|
||||
)}
|
||||
|
||||
<AnimatedExpandableContainer
|
||||
isExpanded={isOpen}
|
||||
dimension="height"
|
||||
mode="fit-content"
|
||||
containAnimation
|
||||
>
|
||||
{folder.navigationMenuItems.map((navigationMenuItem, index) => {
|
||||
const label = getNavigationMenuItemLabel(
|
||||
navigationMenuItem,
|
||||
objectMetadataItems,
|
||||
views,
|
||||
);
|
||||
const computedLink = getNavigationMenuItemComputedLink(
|
||||
navigationMenuItem,
|
||||
objectMetadataItems,
|
||||
views,
|
||||
);
|
||||
const objectNameSingular = getNavigationMenuItemObjectNameSingular(
|
||||
navigationMenuItem,
|
||||
objectMetadataItems,
|
||||
views,
|
||||
);
|
||||
const view = isDefined(navigationMenuItem.viewId)
|
||||
? views.find((view) => view.id === navigationMenuItem.viewId)
|
||||
: undefined;
|
||||
const isIndexView = view?.key === ViewKey.INDEX;
|
||||
|
||||
return (
|
||||
<WorkspaceDndKitSortableItem
|
||||
key={navigationMenuItem.id}
|
||||
id={navigationMenuItem.id}
|
||||
index={index}
|
||||
group={folderDroppableId}
|
||||
>
|
||||
<NavigationDrawerSubItem
|
||||
secondaryLabel={
|
||||
isIndexView
|
||||
? undefined
|
||||
: getObjectNavigationMenuItemSecondaryLabel({
|
||||
objectMetadataItems,
|
||||
navigationMenuItemObjectNameSingular:
|
||||
objectNameSingular ?? '',
|
||||
})
|
||||
}
|
||||
label={label}
|
||||
Icon={() => (
|
||||
<NavigationMenuItemIcon
|
||||
navigationMenuItem={navigationMenuItem}
|
||||
/>
|
||||
)}
|
||||
to={isDragging ? undefined : computedLink}
|
||||
active={index === selectedNavigationMenuItemIndex}
|
||||
subItemState={getNavigationSubItemLeftAdornment({
|
||||
index,
|
||||
arrayLength: navigationMenuItemFolderContentLength,
|
||||
selectedIndex: selectedNavigationMenuItemIndex,
|
||||
})}
|
||||
rightOptions={
|
||||
isWorkspaceFolder ? undefined : (
|
||||
<LightIconButton
|
||||
Icon={IconHeartOff}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
deleteNavigationMenuItem(navigationMenuItem.id);
|
||||
}}
|
||||
accent="tertiary"
|
||||
/>
|
||||
)
|
||||
}
|
||||
isDragging={isDragging}
|
||||
triggerEvent="CLICK"
|
||||
iconColor={getEffectiveNavigationMenuItemColor(
|
||||
navigationMenuItem,
|
||||
)}
|
||||
/>
|
||||
</WorkspaceDndKitSortableItem>
|
||||
);
|
||||
})}
|
||||
</AnimatedExpandableContainer>
|
||||
</NavigationDrawerItemsCollapsableContainer>
|
||||
|
||||
{isModalOpened &&
|
||||
createPortal(
|
||||
<ConfirmationModal
|
||||
modalInstanceId={modalId}
|
||||
title={
|
||||
folder.navigationMenuItems.length > 1
|
||||
? t`Remove ${navigationMenuItemCount} navigation menu items?`
|
||||
: t`Remove ${navigationMenuItemCount} navigation menu item?`
|
||||
}
|
||||
subtitle={
|
||||
folder.navigationMenuItems.length > 1
|
||||
? t`This action will delete this folder and all ${navigationMenuItemCount} navigation menu items inside. Do you want to continue?`
|
||||
: t`This action will delete this folder and the navigation menu item inside. Do you want to continue?`
|
||||
}
|
||||
onConfirmClick={handleConfirmDelete}
|
||||
confirmButtonText={t`Delete Folder`}
|
||||
/>,
|
||||
document.body,
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
-116
@@ -1,116 +0,0 @@
|
||||
import { styled } from '@linaria/react';
|
||||
import { useContext } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { IconHeartOff } from 'twenty-ui/display';
|
||||
import { LightIconButton } from 'twenty-ui/input';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
import { WorkspaceDndKitSortableItem } from '@/navigation-menu-item/display/dnd/components/WorkspaceDndKitSortableItem';
|
||||
import { NavigationMenuItemIcon } from '@/navigation-menu-item/display/components/NavigationMenuItemIcon';
|
||||
import { NavigationMenuItemDroppableIds } from '@/navigation-menu-item/common/constants/NavigationMenuItemDroppableIds';
|
||||
import { NavigationMenuItemDragContext } from '@/navigation-menu-item/common/contexts/NavigationMenuItemDragContext';
|
||||
import { useDeleteNavigationMenuItem } from '@/navigation-menu-item/common/hooks/useDeleteNavigationMenuItem';
|
||||
import { useSortedNavigationMenuItems } from '@/navigation-menu-item/display/hooks/useSortedNavigationMenuItems';
|
||||
import { getEffectiveNavigationMenuItemColor } from '@/navigation-menu-item/common/utils/getEffectiveNavigationMenuItemColor';
|
||||
import { getNavigationMenuItemComputedLink } from '@/navigation-menu-item/display/utils/getNavigationMenuItemComputedLink';
|
||||
import { getNavigationMenuItemLabel } from '@/navigation-menu-item/display/utils/getNavigationMenuItemLabel';
|
||||
import { getNavigationMenuItemObjectNameSingular } from '@/navigation-menu-item/display/object/utils/getNavigationMenuItemObjectNameSingular';
|
||||
import { getObjectNavigationMenuItemSecondaryLabel } from '@/navigation-menu-item/display/object/utils/getObjectNavigationMenuItemSecondaryLabel';
|
||||
import { isLocationMatchingNavigationMenuItem } from '@/navigation-menu-item/common/utils/isLocationMatchingNavigationMenuItem';
|
||||
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { viewsSelector } from '@/views/states/selectors/viewsSelector';
|
||||
import { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem';
|
||||
|
||||
const StyledEmptyContainer = styled.div`
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledOrphanNavigationMenuItemsContainer = styled.div`
|
||||
margin-bottom: ${themeCssVariables.betweenSiblingsGap};
|
||||
`;
|
||||
|
||||
export const FavoritesOrphanItems = () => {
|
||||
const objectMetadataItems = useAtomStateValue(objectMetadataItemsSelector);
|
||||
const views = useAtomStateValue(viewsSelector);
|
||||
const { navigationMenuItemsSorted } = useSortedNavigationMenuItems();
|
||||
const { deleteNavigationMenuItem } = useDeleteNavigationMenuItem();
|
||||
const currentPath = useLocation().pathname;
|
||||
const currentViewPath = useLocation().pathname + useLocation().search;
|
||||
const { isDragging } = useContext(NavigationMenuItemDragContext);
|
||||
|
||||
const orphanNavigationMenuItems = navigationMenuItemsSorted.filter(
|
||||
(item) => !item.folderId,
|
||||
);
|
||||
|
||||
return orphanNavigationMenuItems.length > 0 ? (
|
||||
<>
|
||||
{orphanNavigationMenuItems.map((navigationMenuItem, index) => {
|
||||
const label = getNavigationMenuItemLabel(
|
||||
navigationMenuItem,
|
||||
objectMetadataItems,
|
||||
views,
|
||||
);
|
||||
const computedLink = getNavigationMenuItemComputedLink(
|
||||
navigationMenuItem,
|
||||
objectMetadataItems,
|
||||
views,
|
||||
);
|
||||
const objectNameSingular = getNavigationMenuItemObjectNameSingular(
|
||||
navigationMenuItem,
|
||||
objectMetadataItems,
|
||||
views,
|
||||
);
|
||||
|
||||
return (
|
||||
<WorkspaceDndKitSortableItem
|
||||
key={navigationMenuItem.id}
|
||||
id={navigationMenuItem.id}
|
||||
index={index}
|
||||
group={NavigationMenuItemDroppableIds.ORPHAN_NAVIGATION_MENU_ITEMS}
|
||||
>
|
||||
<StyledOrphanNavigationMenuItemsContainer>
|
||||
<NavigationDrawerItem
|
||||
secondaryLabel={getObjectNavigationMenuItemSecondaryLabel({
|
||||
objectMetadataItems,
|
||||
navigationMenuItemObjectNameSingular:
|
||||
objectNameSingular ?? '',
|
||||
})}
|
||||
label={label}
|
||||
Icon={() => (
|
||||
<NavigationMenuItemIcon
|
||||
navigationMenuItem={navigationMenuItem}
|
||||
/>
|
||||
)}
|
||||
iconColor={getEffectiveNavigationMenuItemColor(
|
||||
navigationMenuItem,
|
||||
)}
|
||||
active={isLocationMatchingNavigationMenuItem(
|
||||
currentPath,
|
||||
currentViewPath,
|
||||
navigationMenuItem.type,
|
||||
computedLink,
|
||||
)}
|
||||
to={isDragging ? undefined : computedLink}
|
||||
rightOptions={
|
||||
<LightIconButton
|
||||
Icon={IconHeartOff}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
deleteNavigationMenuItem(navigationMenuItem.id);
|
||||
}}
|
||||
accent="tertiary"
|
||||
/>
|
||||
}
|
||||
isDragging={isDragging}
|
||||
triggerEvent="CLICK"
|
||||
/>
|
||||
</StyledOrphanNavigationMenuItemsContainer>
|
||||
</WorkspaceDndKitSortableItem>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
) : (
|
||||
<StyledEmptyContainer style={{ height: isDragging ? '24px' : '1px' }} />
|
||||
);
|
||||
};
|
||||
+193
-34
@@ -1,23 +1,63 @@
|
||||
import { styled } from '@linaria/react';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { IconFolderPlus } from 'twenty-ui/display';
|
||||
import { useCallback, useContext, useMemo, useState } from 'react';
|
||||
import { IconFolder, IconFolderPlus, IconHeartOff } from 'twenty-ui/display';
|
||||
import { LightIconButton } from 'twenty-ui/input';
|
||||
import { AnimatedExpandableContainer } from 'twenty-ui/layout';
|
||||
import { FavoritesOrphanItems } from '@/navigation-menu-item/display/sections/favorites/components/FavoritesOrphanItems';
|
||||
import { NavigationMenuItemFolders } from '@/navigation-menu-item/display/folder/components/NavigationMenuItemFolders';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
|
||||
|
||||
import { NavigationMenuItemDroppableIds } from '@/navigation-menu-item/common/constants/NavigationMenuItemDroppableIds';
|
||||
import { NavigationSections } from '@/navigation-menu-item/common/constants/NavigationSections.constants';
|
||||
import { NavigationMenuItemDragContext } from '@/navigation-menu-item/common/contexts/NavigationMenuItemDragContext';
|
||||
import { useDeleteNavigationMenuItem } from '@/navigation-menu-item/common/hooks/useDeleteNavigationMenuItem';
|
||||
import { isNavigationMenuItemFolderCreatingState } from '@/navigation-menu-item/common/states/isNavigationMenuItemFolderCreatingState';
|
||||
import { isNavigationMenuItemFolder } from '@/navigation-menu-item/common/utils/isNavigationMenuItemFolder';
|
||||
import { NavigationMenuItemDisplay } from '@/navigation-menu-item/display/components/NavigationMenuItemDisplay';
|
||||
import { NavigationMenuItemDroppableSlot } from '@/navigation-menu-item/display/dnd/components/NavigationMenuItemDroppableSlot';
|
||||
import { NavigationMenuItemSortableItem } from '@/navigation-menu-item/display/dnd/components/NavigationMenuItemSortableItem';
|
||||
import { useIsDropDisabledForSection } from '@/navigation-menu-item/display/dnd/hooks/useIsDropDisabledForSection';
|
||||
import { useCreateNavigationMenuItemFolder } from '@/navigation-menu-item/display/folder/hooks/useCreateNavigationMenuItemFolder';
|
||||
import { useNavigationMenuItemsByFolder } from '@/navigation-menu-item/display/folder/hooks/useNavigationMenuItemsByFolder';
|
||||
import { useSortedNavigationMenuItems } from '@/navigation-menu-item/display/hooks/useSortedNavigationMenuItems';
|
||||
import { isNavigationMenuItemFolderCreatingState } from '@/navigation-menu-item/common/states/isNavigationMenuItemFolderCreatingState';
|
||||
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
|
||||
import { NavigationMenuItemOrphanDropTarget } from '@/navigation-menu-item/display/sections/components/NavigationMenuItemOrphanDropTarget';
|
||||
import { NavigationMenuItemSection } from '@/navigation-menu-item/display/sections/components/NavigationMenuItemSection';
|
||||
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 { NavigationDrawerInput } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerInput';
|
||||
import { useNavigationSection } from '@/ui/navigation/navigation-drawer/hooks/useNavigationSection';
|
||||
import { isNavigationSectionOpenFamilyState } from '@/ui/navigation/navigation-drawer/states/isNavigationSectionOpenFamilyState';
|
||||
import { useAtomFamilyStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilyStateValue';
|
||||
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
|
||||
|
||||
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;
|
||||
`;
|
||||
|
||||
const StyledEmptyContainer = styled.div`
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const ORPHAN_DROPPABLE_ID =
|
||||
NavigationMenuItemDroppableIds.FAVORITE_ORPHAN_NAVIGATION_MENU_ITEMS;
|
||||
|
||||
export const FavoritesSection = () => {
|
||||
const { navigationMenuItemsSorted } = useSortedNavigationMenuItems();
|
||||
const { userNavigationMenuItemsByFolder } = useNavigationMenuItemsByFolder();
|
||||
const { deleteNavigationMenuItem } = useDeleteNavigationMenuItem();
|
||||
const { isDragging } = useContext(NavigationMenuItemDragContext);
|
||||
const favoritesDropDisabled = useIsDropDisabledForSection(false);
|
||||
|
||||
const [newFolderName, setNewFolderName] = useState('');
|
||||
const { createNewNavigationMenuItemFolder } =
|
||||
useCreateNavigationMenuItemFolder();
|
||||
|
||||
const [
|
||||
isNavigationMenuItemFolderCreating,
|
||||
@@ -33,13 +73,71 @@ export const FavoritesSection = () => {
|
||||
'Favorites',
|
||||
);
|
||||
|
||||
const topLevelItems = useMemo(
|
||||
() => navigationMenuItemsSorted.filter((item) => !item.folderId),
|
||||
[navigationMenuItemsSorted],
|
||||
);
|
||||
|
||||
const folderChildrenById = useMemo(() => {
|
||||
const map = new Map<string, NavigationMenuItem[]>();
|
||||
for (const folder of userNavigationMenuItemsByFolder) {
|
||||
map.set(folder.id, folder.navigationMenuItems);
|
||||
}
|
||||
return map;
|
||||
}, [userNavigationMenuItemsByFolder]);
|
||||
|
||||
const folderCount = useMemo(
|
||||
() => topLevelItems.filter(isNavigationMenuItemFolder).length,
|
||||
[topLevelItems],
|
||||
);
|
||||
|
||||
const toggleNewFolder = () => {
|
||||
openNavigationSection();
|
||||
setIsNavigationMenuItemFolderCreating((current) => !current);
|
||||
};
|
||||
|
||||
const handleSubmitFolderCreation = async (value: string) => {
|
||||
if (value === '') return;
|
||||
setIsNavigationMenuItemFolderCreating(false);
|
||||
setNewFolderName('');
|
||||
await createNewNavigationMenuItemFolder(value);
|
||||
return true;
|
||||
};
|
||||
|
||||
const handleClickOutside = async (
|
||||
_event: MouseEvent | TouchEvent,
|
||||
value: string,
|
||||
) => {
|
||||
if (!value) {
|
||||
setIsNavigationMenuItemFolderCreating(false);
|
||||
return;
|
||||
}
|
||||
setIsNavigationMenuItemFolderCreating(false);
|
||||
setNewFolderName('');
|
||||
await createNewNavigationMenuItemFolder(value);
|
||||
};
|
||||
|
||||
const handleCancelFolderCreation = () => {
|
||||
setNewFolderName('');
|
||||
setIsNavigationMenuItemFolderCreating(false);
|
||||
};
|
||||
|
||||
const makeRightOptions = useCallback(
|
||||
(item: NavigationMenuItem) => (
|
||||
<LightIconButton
|
||||
Icon={IconHeartOff}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
deleteNavigationMenuItem(item.id);
|
||||
}}
|
||||
accent="tertiary"
|
||||
/>
|
||||
),
|
||||
[deleteNavigationMenuItem],
|
||||
);
|
||||
|
||||
if (
|
||||
navigationMenuItemsSorted.length === 0 &&
|
||||
topLevelItems.length === 0 &&
|
||||
!isNavigationMenuItemFolderCreating &&
|
||||
userNavigationMenuItemsByFolder.length === 0
|
||||
) {
|
||||
@@ -47,31 +145,92 @@ export const FavoritesSection = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<NavigationDrawerSection>
|
||||
<NavigationDrawerAnimatedCollapseWrapper>
|
||||
<NavigationDrawerSectionTitle
|
||||
label={t`Favorites`}
|
||||
onClick={toggleNavigationSection}
|
||||
rightIcon={
|
||||
<LightIconButton
|
||||
Icon={IconFolderPlus}
|
||||
onClick={toggleNewFolder}
|
||||
accent="tertiary"
|
||||
/>
|
||||
}
|
||||
isOpen={isNavigationSectionOpen}
|
||||
<NavigationMenuItemSection
|
||||
title={t`Favorites`}
|
||||
isOpen={isNavigationSectionOpen}
|
||||
onToggle={toggleNavigationSection}
|
||||
rightIcon={
|
||||
<LightIconButton
|
||||
Icon={IconFolderPlus}
|
||||
onClick={toggleNewFolder}
|
||||
accent="tertiary"
|
||||
/>
|
||||
</NavigationDrawerAnimatedCollapseWrapper>
|
||||
<AnimatedExpandableContainer
|
||||
isExpanded={isNavigationSectionOpen}
|
||||
dimension="height"
|
||||
mode="fit-content"
|
||||
containAnimation
|
||||
initial={false}
|
||||
>
|
||||
<NavigationMenuItemFolders />
|
||||
<FavoritesOrphanItems />
|
||||
</AnimatedExpandableContainer>
|
||||
</NavigationDrawerSection>
|
||||
}
|
||||
>
|
||||
{isNavigationMenuItemFolderCreating && (
|
||||
<NavigationDrawerAnimatedCollapseWrapper>
|
||||
<NavigationDrawerInput
|
||||
Icon={IconFolder}
|
||||
value={newFolderName}
|
||||
onChange={setNewFolderName}
|
||||
onSubmit={handleSubmitFolderCreation}
|
||||
onCancel={handleCancelFolderCreation}
|
||||
onClickOutside={handleClickOutside}
|
||||
/>
|
||||
</NavigationDrawerAnimatedCollapseWrapper>
|
||||
)}
|
||||
{topLevelItems.length > 0 ? (
|
||||
<StyledList>
|
||||
{topLevelItems.map((item, index) => (
|
||||
<StyledListItemRow key={item.id}>
|
||||
{index === 0 ? (
|
||||
<NavigationMenuItemDroppableSlot
|
||||
droppableId={ORPHAN_DROPPABLE_ID}
|
||||
index={0}
|
||||
disabled={favoritesDropDisabled}
|
||||
>
|
||||
<NavigationMenuItemOrphanDropTarget
|
||||
index={0}
|
||||
compact
|
||||
sectionId={NavigationSections.FAVORITES}
|
||||
droppableId={ORPHAN_DROPPABLE_ID}
|
||||
/>
|
||||
</NavigationMenuItemDroppableSlot>
|
||||
) : (
|
||||
<NavigationMenuItemOrphanDropTarget
|
||||
index={index}
|
||||
compact
|
||||
sectionId={NavigationSections.FAVORITES}
|
||||
droppableId={ORPHAN_DROPPABLE_ID}
|
||||
/>
|
||||
)}
|
||||
<NavigationMenuItemSortableItem
|
||||
id={item.id}
|
||||
index={index}
|
||||
group={ORPHAN_DROPPABLE_ID}
|
||||
disabled={favoritesDropDisabled}
|
||||
>
|
||||
<NavigationMenuItemDisplay
|
||||
item={item}
|
||||
isEditInPlace={isNavigationMenuItemFolder(item)}
|
||||
isDragging={isDragging}
|
||||
folderChildrenById={folderChildrenById}
|
||||
folderCount={folderCount}
|
||||
rightOptions={
|
||||
isNavigationMenuItemFolder(item)
|
||||
? undefined
|
||||
: makeRightOptions(item)
|
||||
}
|
||||
/>
|
||||
</NavigationMenuItemSortableItem>
|
||||
</StyledListItemRow>
|
||||
))}
|
||||
<NavigationMenuItemDroppableSlot
|
||||
droppableId={ORPHAN_DROPPABLE_ID}
|
||||
index={topLevelItems.length}
|
||||
disabled={favoritesDropDisabled}
|
||||
>
|
||||
<NavigationMenuItemOrphanDropTarget
|
||||
index={topLevelItems.length}
|
||||
compact
|
||||
sectionId={NavigationSections.FAVORITES}
|
||||
droppableId={ORPHAN_DROPPABLE_ID}
|
||||
/>
|
||||
</NavigationMenuItemDroppableSlot>
|
||||
</StyledList>
|
||||
) : (
|
||||
<StyledEmptyContainer style={{ height: isDragging ? '24px' : '1px' }} />
|
||||
)}
|
||||
</NavigationMenuItemSection>
|
||||
);
|
||||
};
|
||||
|
||||
+4
-3
@@ -1,6 +1,7 @@
|
||||
import { isLayoutCustomizationModeEnabledState } from '@/layout-customization/states/isLayoutCustomizationModeEnabledState';
|
||||
import { NavigationSections } from '@/navigation-menu-item/common/constants/NavigationSections.constants';
|
||||
import { NavigationMenuItemDndKitProvider } from '@/navigation-menu-item/display/dnd/providers/NavigationMenuItemDndKitProvider';
|
||||
import { FavoritesSection } from '@/navigation-menu-item/display/sections/favorites/components/FavoritesSection';
|
||||
import { FavoritesDndKitProvider } from '@/navigation/components/FavoritesDndKitProvider';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { AnimatedEaseInOut } from 'twenty-ui/utilities';
|
||||
|
||||
@@ -11,9 +12,9 @@ export const FavoritesSectionDispatcher = () => {
|
||||
|
||||
return (
|
||||
<AnimatedEaseInOut isOpen={!isLayoutCustomizationModeEnabled} initial>
|
||||
<FavoritesDndKitProvider>
|
||||
<NavigationMenuItemDndKitProvider section={NavigationSections.FAVORITES}>
|
||||
<FavoritesSection />
|
||||
</FavoritesDndKitProvider>
|
||||
</NavigationMenuItemDndKitProvider>
|
||||
</AnimatedEaseInOut>
|
||||
);
|
||||
};
|
||||
|
||||
+8
-4
@@ -1,16 +1,20 @@
|
||||
import type { NavigationMenuItemClickParams } from '@/navigation-menu-item/display/hooks/useWorkspaceSectionItems';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
import type { NavigationMenuItemClickParams } from '@/navigation-menu-item/display/hooks/useNavigationMenuItemSectionItems';
|
||||
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 = {
|
||||
export type NavigationMenuItemSectionContentProps = {
|
||||
item: NavigationMenuItem;
|
||||
editModeProps: EditModeProps;
|
||||
isEditInPlace?: boolean;
|
||||
editModeProps?: EditModeProps;
|
||||
isDragging: boolean;
|
||||
folderChildrenById: Map<string, NavigationMenuItem[]>;
|
||||
folderCount: number;
|
||||
selectedNavigationMenuItemId: string | null;
|
||||
rightOptions?: ReactNode;
|
||||
selectedNavigationMenuItemId?: string | null;
|
||||
onNavigationMenuItemClick?: (params: NavigationMenuItemClickParams) => void;
|
||||
onActiveObjectMetadataItemClick?: (
|
||||
objectMetadataItem: ObjectMetadataItem,
|
||||
+2
-2
@@ -1,10 +1,10 @@
|
||||
import type { NavigationMenuItemClickParams } from '@/navigation-menu-item/display/hooks/useWorkspaceSectionItems';
|
||||
import type { NavigationMenuItemClickParams } from '@/navigation-menu-item/display/hooks/useNavigationMenuItemSectionItems';
|
||||
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 = {
|
||||
export type NavigationMenuItemSectionListDndKitProps = {
|
||||
filteredItems: NavigationMenuItem[];
|
||||
getEditModeProps: (item: NavigationMenuItem) => EditModeProps;
|
||||
folderChildrenById: Map<string, NavigationMenuItem[]>;
|
||||
-5
@@ -1,5 +0,0 @@
|
||||
import { WorkspaceNavigationMenuItems } from '@/navigation-menu-item/display/sections/workspace/components/WorkspaceNavigationMenuItems';
|
||||
|
||||
export const WorkspaceNavigationMenuItemsDispatcher = () => {
|
||||
return <WorkspaceNavigationMenuItems />;
|
||||
};
|
||||
+8
-8
@@ -20,14 +20,14 @@ import { useOpenNavigationMenuItemInSidePanel } from '@/navigation-menu-item/edi
|
||||
import { useSortedNavigationMenuItems } from '@/navigation-menu-item/display/hooks/useSortedNavigationMenuItems';
|
||||
import {
|
||||
type NavigationMenuItemClickParams,
|
||||
useWorkspaceSectionItems,
|
||||
} from '@/navigation-menu-item/display/hooks/useWorkspaceSectionItems';
|
||||
useNavigationMenuItemSectionItems,
|
||||
} from '@/navigation-menu-item/display/hooks/useNavigationMenuItemSectionItems';
|
||||
import { openNavigationMenuItemFolderIdsState } from '@/navigation-menu-item/common/states/openNavigationMenuItemFolderIdsState';
|
||||
import { selectedNavigationMenuItemInEditModeState } from '@/navigation-menu-item/common/states/selectedNavigationMenuItemInEditModeState';
|
||||
import { getNavigationMenuItemComputedLink } from '@/navigation-menu-item/display/utils/getNavigationMenuItemComputedLink';
|
||||
import { getNavigationMenuItemLabel } from '@/navigation-menu-item/display/utils/getNavigationMenuItemLabel';
|
||||
import { preloadWorkspaceDndKit } from '@/navigation/preloadWorkspaceDndKit';
|
||||
import { NavigationDrawerSectionForWorkspaceItems } from '@/navigation-menu-item/display/sections/components/NavigationDrawerSectionForWorkspaceItems';
|
||||
import { preloadNavigationMenuItemDndKit } from '@/navigation-menu-item/display/dnd/preloadNavigationMenuItemDndKit';
|
||||
import { WorkspaceSectionContainer } from '@/navigation-menu-item/display/sections/workspace/components/WorkspaceSectionContainer';
|
||||
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { useNavigateSidePanel } from '@/side-panel/hooks/useNavigateSidePanel';
|
||||
@@ -42,8 +42,8 @@ const StyledRightIconsContainer = styled.div`
|
||||
gap: ${themeCssVariables.spacing[1]};
|
||||
`;
|
||||
|
||||
export const WorkspaceNavigationMenuItems = () => {
|
||||
const items = useWorkspaceSectionItems();
|
||||
export const WorkspaceSection = () => {
|
||||
const items = useNavigationMenuItemSectionItems();
|
||||
const { workspaceNavigationMenuItemsSorted } = useSortedNavigationMenuItems();
|
||||
const objectMetadataItems = useAtomStateValue(objectMetadataItemsSelector);
|
||||
const views = useAtomStateValue(viewsSelector);
|
||||
@@ -173,7 +173,7 @@ export const WorkspaceNavigationMenuItems = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<NavigationDrawerSectionForWorkspaceItems
|
||||
<WorkspaceSectionContainer
|
||||
sectionTitle={t`Workspace`}
|
||||
items={items}
|
||||
rightIcon={
|
||||
@@ -186,7 +186,7 @@ export const WorkspaceNavigationMenuItems = () => {
|
||||
onClick={handleAddMenuItem}
|
||||
/>
|
||||
) : (
|
||||
<div onMouseEnter={preloadWorkspaceDndKit}>
|
||||
<div onMouseEnter={preloadNavigationMenuItemDndKit}>
|
||||
<LightIconButton
|
||||
Icon={IconTool}
|
||||
accent="tertiary"
|
||||
+46
-62
@@ -1,33 +1,30 @@
|
||||
import { NavigationDropTargetContext } from '@/navigation-menu-item/common/contexts/NavigationDropTargetContext';
|
||||
import { styled } from '@linaria/react';
|
||||
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 { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
|
||||
|
||||
import { isLayoutCustomizationModeEnabledState } from '@/layout-customization/states/isLayoutCustomizationModeEnabledState';
|
||||
import { NavigationMenuItemDroppableIds } from '@/navigation-menu-item/common/constants/NavigationMenuItemDroppableIds';
|
||||
import { type NavigationMenuItemClickParams } from '@/navigation-menu-item/display/hooks/useWorkspaceSectionItems';
|
||||
import { NavigationDropTargetContext } from '@/navigation-menu-item/common/contexts/NavigationDropTargetContext';
|
||||
import { type NavigationMenuItemClickParams } from '@/navigation-menu-item/display/hooks/useNavigationMenuItemSectionItems';
|
||||
import { getObjectMetadataForNavigationMenuItem } from '@/navigation-menu-item/display/object/utils/getObjectMetadataForNavigationMenuItem';
|
||||
import { WorkspaceSectionListReadOnly } from '@/navigation-menu-item/display/sections/workspace/components/WorkspaceSectionListReadOnly';
|
||||
import { NavigationMenuItemSection } from '@/navigation-menu-item/display/sections/components/NavigationMenuItemSection';
|
||||
import type { EditModeProps } from '@/object-metadata/components/EditModeProps';
|
||||
import { NavigationDrawerSectionForWorkspaceItemsListReadOnly } from '@/navigation-menu-item/display/sections/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(
|
||||
'@/navigation-menu-item/display/sections/components/NavigationDrawerSectionForWorkspaceItemsListDndKit'
|
||||
'@/navigation-menu-item/display/sections/workspace/components/WorkspaceSectionListDndKit'
|
||||
).then((m) => ({ default: m.WorkspaceSectionListDndKit })),
|
||||
);
|
||||
|
||||
@@ -35,7 +32,7 @@ const StyledWorkspaceSectionContentGapOffset = styled.div`
|
||||
margin-top: calc(-1 * ${themeCssVariables.betweenSiblingsGap});
|
||||
`;
|
||||
|
||||
type NavigationDrawerSectionForWorkspaceItemsProps = {
|
||||
type WorkspaceSectionContainerProps = {
|
||||
sectionTitle: string;
|
||||
items: NavigationMenuItem[];
|
||||
rightIcon?: React.ReactNode;
|
||||
@@ -47,14 +44,14 @@ type NavigationDrawerSectionForWorkspaceItemsProps = {
|
||||
) => void;
|
||||
};
|
||||
|
||||
export const NavigationDrawerSectionForWorkspaceItems = ({
|
||||
export const WorkspaceSectionContainer = ({
|
||||
sectionTitle,
|
||||
items,
|
||||
rightIcon,
|
||||
selectedNavigationMenuItemId = null,
|
||||
onNavigationMenuItemClick,
|
||||
onActiveObjectMetadataItemClick,
|
||||
}: NavigationDrawerSectionForWorkspaceItemsProps) => {
|
||||
}: WorkspaceSectionContainerProps) => {
|
||||
const isLayoutCustomizationModeEnabled = useAtomStateValue(
|
||||
isLayoutCustomizationModeEnabledState,
|
||||
);
|
||||
@@ -145,58 +142,45 @@ export const NavigationDrawerSectionForWorkspaceItems = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<NavigationDrawerSection>
|
||||
<NavigationDrawerAnimatedCollapseWrapper>
|
||||
<NavigationDrawerSectionTitle
|
||||
label={sectionTitle}
|
||||
onClick={() => toggleNavigationSection()}
|
||||
rightIcon={rightIcon}
|
||||
alwaysShowRightIcon={isLayoutCustomizationModeEnabled}
|
||||
isOpen={isNavigationSectionOpen}
|
||||
/>
|
||||
</NavigationDrawerAnimatedCollapseWrapper>
|
||||
<StyledWorkspaceSectionContentGapOffset>
|
||||
<AnimatedExpandableContainer
|
||||
isExpanded={
|
||||
isNavigationSectionOpen || isAddToNavigationDropTargetVisible
|
||||
}
|
||||
dimension="height"
|
||||
mode="fit-content"
|
||||
containAnimation
|
||||
initial={false}
|
||||
>
|
||||
{isLayoutCustomizationModeEnabled ? (
|
||||
<Suspense
|
||||
fallback={
|
||||
<WorkspaceSectionListEditModeFallback
|
||||
filteredItems={filteredItems}
|
||||
folderChildrenById={folderChildrenById}
|
||||
onActiveObjectMetadataItemClick={
|
||||
onActiveObjectMetadataItemClick
|
||||
}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<LazyWorkspaceSectionListDndKit
|
||||
filteredItems={filteredItems}
|
||||
getEditModeProps={getEditModeProps}
|
||||
folderChildrenById={folderChildrenById}
|
||||
selectedNavigationMenuItemId={selectedNavigationMenuItemId}
|
||||
onNavigationMenuItemClick={onNavigationMenuItemClick}
|
||||
onActiveObjectMetadataItemClick={
|
||||
onActiveObjectMetadataItemClick
|
||||
}
|
||||
/>
|
||||
</Suspense>
|
||||
) : (
|
||||
<NavigationDrawerSectionForWorkspaceItemsListReadOnly
|
||||
<NavigationMenuItemSection
|
||||
title={sectionTitle}
|
||||
isOpen={isNavigationSectionOpen}
|
||||
onToggle={() => toggleNavigationSection()}
|
||||
rightIcon={rightIcon}
|
||||
alwaysShowRightIcon={isLayoutCustomizationModeEnabled}
|
||||
forceExpanded={isAddToNavigationDropTargetVisible}
|
||||
contentWrapper={(children) => (
|
||||
<StyledWorkspaceSectionContentGapOffset>
|
||||
{children}
|
||||
</StyledWorkspaceSectionContentGapOffset>
|
||||
)}
|
||||
>
|
||||
{isLayoutCustomizationModeEnabled ? (
|
||||
<Suspense
|
||||
fallback={
|
||||
<WorkspaceSectionListEditModeFallback
|
||||
filteredItems={filteredItems}
|
||||
folderChildrenById={folderChildrenById}
|
||||
onActiveObjectMetadataItemClick={onActiveObjectMetadataItemClick}
|
||||
/>
|
||||
)}
|
||||
</AnimatedExpandableContainer>
|
||||
</StyledWorkspaceSectionContentGapOffset>
|
||||
</NavigationDrawerSection>
|
||||
}
|
||||
>
|
||||
<LazyWorkspaceSectionListDndKit
|
||||
filteredItems={filteredItems}
|
||||
getEditModeProps={getEditModeProps}
|
||||
folderChildrenById={folderChildrenById}
|
||||
selectedNavigationMenuItemId={selectedNavigationMenuItemId}
|
||||
onNavigationMenuItemClick={onNavigationMenuItemClick}
|
||||
onActiveObjectMetadataItemClick={onActiveObjectMetadataItemClick}
|
||||
/>
|
||||
</Suspense>
|
||||
) : (
|
||||
<WorkspaceSectionListReadOnly
|
||||
filteredItems={filteredItems}
|
||||
folderChildrenById={folderChildrenById}
|
||||
onActiveObjectMetadataItemClick={onActiveObjectMetadataItemClick}
|
||||
/>
|
||||
)}
|
||||
</NavigationMenuItemSection>
|
||||
);
|
||||
};
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
import { WorkspaceSection } from '@/navigation-menu-item/display/sections/workspace/components/WorkspaceSection';
|
||||
|
||||
export const WorkspaceSectionDispatcher = () => {
|
||||
return <WorkspaceSection />;
|
||||
};
|
||||
+16
-14
@@ -1,6 +1,6 @@
|
||||
import { NavigationMenuItemDroppableSlot } from '@/navigation-menu-item/display/dnd/components/NavigationMenuItemDroppableSlot';
|
||||
import { NavigationMenuItemSortableItem } from '@/navigation-menu-item/display/dnd/components/NavigationMenuItemSortableItem';
|
||||
import { isLayoutCustomizationModeEnabledState } from '@/layout-customization/states/isLayoutCustomizationModeEnabledState';
|
||||
import { WorkspaceDndKitDroppableSlot } from '@/navigation-menu-item/display/dnd/components/WorkspaceDndKitDroppableSlot';
|
||||
import { WorkspaceDndKitSortableItem } from '@/navigation-menu-item/display/dnd/components/WorkspaceDndKitSortableItem';
|
||||
import { NavigationMenuItemDroppableIds } from '@/navigation-menu-item/common/constants/NavigationMenuItemDroppableIds';
|
||||
import { NavigationMenuItemType } from 'twenty-shared/types';
|
||||
import { NavigationDropTargetContext } from '@/navigation-menu-item/common/contexts/NavigationDropTargetContext';
|
||||
@@ -12,9 +12,11 @@ import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
import { NavigationMenuItemDragContext } from '@/navigation-menu-item/common/contexts/NavigationMenuItemDragContext';
|
||||
import { NavigationMenuItemDisplay } from '@/navigation-menu-item/display/components/NavigationMenuItemDisplay';
|
||||
import { WorkspaceOrphanDropTarget } from '@/navigation-menu-item/display/sections/components/WorkspaceOrphanDropTarget';
|
||||
import { NavigationMenuItemOrphanDropTarget } from '@/navigation-menu-item/display/sections/components/NavigationMenuItemOrphanDropTarget';
|
||||
import { WorkspaceSectionAddMenuItemButton } from '@/navigation-menu-item/edit/components/WorkspaceSectionAddMenuItemButton';
|
||||
import type { WorkspaceSectionListDndKitProps } from '@/navigation-menu-item/display/sections/types/WorkspaceSectionListDndKitProps';
|
||||
import type { NavigationMenuItemSectionListDndKitProps } from '@/navigation-menu-item/display/sections/types/NavigationMenuItemSectionListDndKitProps';
|
||||
|
||||
type WorkspaceSectionListDndKitProps = NavigationMenuItemSectionListDndKitProps;
|
||||
|
||||
const StyledList = styled.div`
|
||||
display: flex;
|
||||
@@ -53,8 +55,8 @@ export const WorkspaceSectionListDndKit = ({
|
||||
<StyledList>
|
||||
{filteredItems.map((item, index) => (
|
||||
<StyledListItemRow key={item.id}>
|
||||
<WorkspaceOrphanDropTarget index={index} compact />
|
||||
<WorkspaceDndKitSortableItem
|
||||
<NavigationMenuItemOrphanDropTarget index={index} compact />
|
||||
<NavigationMenuItemSortableItem
|
||||
id={item.id}
|
||||
index={index}
|
||||
group={
|
||||
@@ -74,38 +76,38 @@ export const WorkspaceSectionListDndKit = ({
|
||||
onNavigationMenuItemClick={onNavigationMenuItemClick}
|
||||
onActiveObjectMetadataItemClick={onActiveObjectMetadataItemClick}
|
||||
/>
|
||||
</WorkspaceDndKitSortableItem>
|
||||
</NavigationMenuItemSortableItem>
|
||||
</StyledListItemRow>
|
||||
))}
|
||||
<WorkspaceDndKitDroppableSlot
|
||||
<NavigationMenuItemDroppableSlot
|
||||
droppableId={
|
||||
NavigationMenuItemDroppableIds.WORKSPACE_ORPHAN_NAVIGATION_MENU_ITEMS
|
||||
}
|
||||
index={filteredItems.length}
|
||||
disabled={workspaceDropDisabled}
|
||||
>
|
||||
<WorkspaceOrphanDropTarget
|
||||
<NavigationMenuItemOrphanDropTarget
|
||||
index={filteredItems.length}
|
||||
compact={!isAddMenuItemButtonVisible}
|
||||
>
|
||||
{isAddMenuItemButtonVisible && <WorkspaceSectionAddMenuItemButton />}
|
||||
</WorkspaceOrphanDropTarget>
|
||||
</WorkspaceDndKitDroppableSlot>
|
||||
</NavigationMenuItemOrphanDropTarget>
|
||||
</NavigationMenuItemDroppableSlot>
|
||||
{addToNavigationFallbackDestination?.droppableId ===
|
||||
NavigationMenuItemDroppableIds.WORKSPACE_ORPHAN_NAVIGATION_MENU_ITEMS &&
|
||||
addToNavigationFallbackDestination.index > filteredItems.length && (
|
||||
<WorkspaceDndKitDroppableSlot
|
||||
<NavigationMenuItemDroppableSlot
|
||||
droppableId={
|
||||
NavigationMenuItemDroppableIds.WORKSPACE_ORPHAN_NAVIGATION_MENU_ITEMS
|
||||
}
|
||||
index={addToNavigationFallbackDestination.index}
|
||||
disabled={workspaceDropDisabled}
|
||||
>
|
||||
<WorkspaceOrphanDropTarget
|
||||
<NavigationMenuItemOrphanDropTarget
|
||||
index={addToNavigationFallbackDestination.index}
|
||||
compact
|
||||
/>
|
||||
</WorkspaceDndKitDroppableSlot>
|
||||
</NavigationMenuItemDroppableSlot>
|
||||
)}
|
||||
</StyledList>
|
||||
);
|
||||
+5
-5
@@ -3,7 +3,7 @@ import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
import type { NavigationMenuItem } from '~/generated-metadata/graphql';
|
||||
import { NavigationMenuItemType } from 'twenty-shared/types';
|
||||
import type { WorkspaceSectionListDndKitProps } from '@/navigation-menu-item/display/sections/types/WorkspaceSectionListDndKitProps';
|
||||
import type { NavigationMenuItemSectionListDndKitProps } from '@/navigation-menu-item/display/sections/types/NavigationMenuItemSectionListDndKitProps';
|
||||
import { NavigationMenuItemDisplay } from '@/navigation-menu-item/display/components/NavigationMenuItemDisplay';
|
||||
|
||||
const StyledList = styled.div`
|
||||
@@ -13,8 +13,8 @@ const StyledList = styled.div`
|
||||
padding-top: ${themeCssVariables.betweenSiblingsGap};
|
||||
`;
|
||||
|
||||
type NavigationDrawerSectionForWorkspaceItemsListReadOnlyProps = Pick<
|
||||
WorkspaceSectionListDndKitProps,
|
||||
type WorkspaceSectionListReadOnlyProps = Pick<
|
||||
NavigationMenuItemSectionListDndKitProps,
|
||||
'filteredItems' | 'folderChildrenById' | 'onActiveObjectMetadataItemClick'
|
||||
>;
|
||||
|
||||
@@ -23,11 +23,11 @@ const READ_ONLY_EDIT_MODE_PROPS = {
|
||||
onEditModeClick: undefined,
|
||||
} as const;
|
||||
|
||||
export const NavigationDrawerSectionForWorkspaceItemsListReadOnly = ({
|
||||
export const WorkspaceSectionListReadOnly = ({
|
||||
filteredItems,
|
||||
folderChildrenById,
|
||||
onActiveObjectMetadataItemClick,
|
||||
}: NavigationDrawerSectionForWorkspaceItemsListReadOnlyProps) => {
|
||||
}: WorkspaceSectionListReadOnlyProps) => {
|
||||
const folderCount = filteredItems.filter(
|
||||
(item) => item.type === NavigationMenuItemType.FOLDER,
|
||||
).length;
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
import { useState } from 'react';
|
||||
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
|
||||
|
||||
import { NAVIGATION_MENU_ITEM_FOLDER_DELETE_MODAL_ID } from '@/navigation-menu-item/common/constants/NavigationMenuItemFolderDeleteModalId';
|
||||
import { useDeleteNavigationMenuItemFolder } from '@/navigation-menu-item/edit/folder/hooks/useDeleteNavigationMenuItemFolder';
|
||||
import { useRenameNavigationMenuItemFolder } from '@/navigation-menu-item/edit/folder/hooks/useRenameNavigationMenuItemFolder';
|
||||
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
|
||||
import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState';
|
||||
import { useModal } from '@/ui/layout/modal/hooks/useModal';
|
||||
import { isModalOpenedComponentState } from '@/ui/layout/modal/states/isModalOpenedComponentState';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
|
||||
type UseFavoritesFolderEditParams = {
|
||||
folderId: string;
|
||||
folderName: string;
|
||||
navigationMenuItems: NavigationMenuItem[];
|
||||
};
|
||||
|
||||
export const useFavoritesFolderEdit = ({
|
||||
folderId,
|
||||
folderName: initialFolderName,
|
||||
navigationMenuItems,
|
||||
}: UseFavoritesFolderEditParams) => {
|
||||
const [isRenaming, setIsRenaming] = useState(false);
|
||||
const [folderNameValue, setFolderNameValue] = useState(initialFolderName);
|
||||
|
||||
const { renameNavigationMenuItemFolder } =
|
||||
useRenameNavigationMenuItemFolder();
|
||||
const { deleteNavigationMenuItemFolder } =
|
||||
useDeleteNavigationMenuItemFolder();
|
||||
const { openModal } = useModal();
|
||||
|
||||
const dropdownId = `navigation-menu-item-folder-edit-${folderId}`;
|
||||
const isDropdownOpen = useAtomComponentStateValue(
|
||||
isDropdownOpenComponentState,
|
||||
dropdownId,
|
||||
);
|
||||
const { closeDropdown } = useCloseDropdown();
|
||||
|
||||
const modalId = `${NAVIGATION_MENU_ITEM_FOLDER_DELETE_MODAL_ID}-${folderId}`;
|
||||
const isModalOpened = useAtomComponentStateValue(
|
||||
isModalOpenedComponentState,
|
||||
modalId,
|
||||
);
|
||||
|
||||
const handleSubmitRename = async (value: string) => {
|
||||
if (value === '') return;
|
||||
await renameNavigationMenuItemFolder(folderId, value);
|
||||
setIsRenaming(false);
|
||||
return true;
|
||||
};
|
||||
|
||||
const handleCancelRename = () => {
|
||||
setFolderNameValue(initialFolderName);
|
||||
setIsRenaming(false);
|
||||
};
|
||||
|
||||
const handleClickOutsideRename = async (
|
||||
_event: MouseEvent | TouchEvent,
|
||||
value: string,
|
||||
) => {
|
||||
if (!value) {
|
||||
setIsRenaming(false);
|
||||
return;
|
||||
}
|
||||
await renameNavigationMenuItemFolder(folderId, value);
|
||||
setIsRenaming(false);
|
||||
};
|
||||
|
||||
const handleFolderDelete = async () => {
|
||||
if (navigationMenuItems.length > 0) {
|
||||
openModal(modalId);
|
||||
closeDropdown(dropdownId);
|
||||
} else {
|
||||
await deleteNavigationMenuItemFolder(folderId);
|
||||
closeDropdown(dropdownId);
|
||||
}
|
||||
};
|
||||
|
||||
const handleConfirmDelete = async () => {
|
||||
await deleteNavigationMenuItemFolder(folderId);
|
||||
};
|
||||
|
||||
return {
|
||||
isRenaming,
|
||||
setIsRenaming,
|
||||
folderNameValue,
|
||||
setFolderNameValue,
|
||||
handleSubmitRename,
|
||||
handleCancelRename,
|
||||
handleClickOutsideRename,
|
||||
handleFolderDelete,
|
||||
handleConfirmDelete,
|
||||
isDropdownOpen,
|
||||
dropdownId,
|
||||
closeDropdown: () => closeDropdown(dropdownId),
|
||||
modalId,
|
||||
isModalOpened,
|
||||
navigationMenuItemCount: navigationMenuItems.length,
|
||||
};
|
||||
};
|
||||
+2
-2
@@ -5,13 +5,13 @@ import { getPositionBetween } from '@/navigation-menu-item/common/utils/getPosit
|
||||
import { isNavigationMenuItemFolder } from '@/navigation-menu-item/common/utils/isNavigationMenuItemFolder';
|
||||
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
|
||||
|
||||
import { useWorkspaceSectionItems } from '@/navigation-menu-item/display/hooks/useWorkspaceSectionItems';
|
||||
import { useNavigationMenuItemSectionItems } from '@/navigation-menu-item/display/hooks/useNavigationMenuItemSectionItems';
|
||||
|
||||
export const useNavigationMenuItemMoveRemove = () => {
|
||||
const setNavigationMenuItemsDraft = useSetAtomState(
|
||||
navigationMenuItemsDraftState,
|
||||
);
|
||||
const items = useWorkspaceSectionItems();
|
||||
const items = useNavigationMenuItemSectionItems();
|
||||
const visibleItemIds = new Set(items.map((item) => item.id));
|
||||
|
||||
const moveUp = (navigationMenuItemId: string) => {
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
import { useWorkspaceSectionItems } from '@/navigation-menu-item/display/hooks/useWorkspaceSectionItems';
|
||||
import { useNavigationMenuItemSectionItems } from '@/navigation-menu-item/display/hooks/useNavigationMenuItemSectionItems';
|
||||
import { selectedNavigationMenuItemInEditModeState } from '@/navigation-menu-item/common/states/selectedNavigationMenuItemInEditModeState';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
|
||||
@@ -6,7 +6,7 @@ export const useSelectedNavigationMenuItemEditItem = () => {
|
||||
const selectedNavigationMenuItemInEditMode = useAtomStateValue(
|
||||
selectedNavigationMenuItemInEditModeState,
|
||||
);
|
||||
const items = useWorkspaceSectionItems();
|
||||
const items = useNavigationMenuItemSectionItems();
|
||||
|
||||
const selectedItem = selectedNavigationMenuItemInEditMode
|
||||
? items.find((item) => item.id === selectedNavigationMenuItemInEditMode)
|
||||
|
||||
+2
-2
@@ -7,7 +7,7 @@ import { useNavigateSidePanel } from '@/side-panel/hooks/useNavigateSidePanel';
|
||||
import { type OrganizeActionsProps } from '@/navigation-menu-item/edit/side-panel/components/SidePanelEditOrganizeActions';
|
||||
import { useNavigationMenuItemMoveRemove } from '@/navigation-menu-item/edit/hooks/useNavigationMenuItemMoveRemove';
|
||||
import { useNavigationMenuItemsDraftState } from '@/navigation-menu-item/edit/hooks/useNavigationMenuItemsDraftState';
|
||||
import { useWorkspaceSectionItems } from '@/navigation-menu-item/display/hooks/useWorkspaceSectionItems';
|
||||
import { useNavigationMenuItemSectionItems } from '@/navigation-menu-item/display/hooks/useNavigationMenuItemSectionItems';
|
||||
import { addMenuItemInsertionContextState } from '@/navigation-menu-item/common/states/addMenuItemInsertionContextState';
|
||||
import { selectedNavigationMenuItemInEditModeState } from '@/navigation-menu-item/common/states/selectedNavigationMenuItemInEditModeState';
|
||||
import { type AddMenuItemInsertionContext } from '@/navigation-menu-item/common/types/AddMenuItemInsertionContext';
|
||||
@@ -60,7 +60,7 @@ export const useNavigationMenuItemEditOrganizeActions =
|
||||
addMenuItemInsertionContextState,
|
||||
);
|
||||
const { workspaceNavigationMenuItems } = useNavigationMenuItemsDraftState();
|
||||
const items = useWorkspaceSectionItems();
|
||||
const items = useNavigationMenuItemSectionItems();
|
||||
const { moveUp, moveDown, remove } = useNavigationMenuItemMoveRemove();
|
||||
|
||||
const selectedItem = selectedNavigationMenuItemInEditMode
|
||||
|
||||
+4
-4
@@ -16,11 +16,11 @@ const FavoritesSectionDispatcher = lazy(() =>
|
||||
})),
|
||||
);
|
||||
|
||||
const WorkspaceNavigationMenuItemsDispatcher = lazy(() =>
|
||||
const WorkspaceSectionDispatcher = lazy(() =>
|
||||
import(
|
||||
'@/navigation-menu-item/display/sections/workspace/components/WorkspaceNavigationMenuItemsDispatcher'
|
||||
'@/navigation-menu-item/display/sections/workspace/components/WorkspaceSectionDispatcher'
|
||||
).then((module) => ({
|
||||
default: module.WorkspaceNavigationMenuItemsDispatcher,
|
||||
default: module.WorkspaceSectionDispatcher,
|
||||
})),
|
||||
);
|
||||
|
||||
@@ -36,7 +36,7 @@ export const MainNavigationDrawerScrollableItems = () => {
|
||||
<NavigationDrawerOpenedSection />
|
||||
<Suspense fallback={<NavigationDrawerWorkspaceSectionSkeletonLoader />}>
|
||||
<FavoritesSectionDispatcher />
|
||||
<WorkspaceNavigationMenuItemsDispatcher />
|
||||
<WorkspaceSectionDispatcher />
|
||||
</Suspense>
|
||||
<RemoteNavigationDrawerSection />
|
||||
<NavigationDrawerOtherSection />
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
import { lazy, Suspense, useState, type ReactNode } from 'react';
|
||||
|
||||
import { PageDragDropProviderMountEffect } from '@/navigation/components/PageDragDropProviderMountEffect';
|
||||
|
||||
const LazyWorkspaceDndKitProvider = lazy(() =>
|
||||
import('@/navigation/components/WorkspaceDndKitProvider').then((m) => ({
|
||||
default: m.WorkspaceDndKitProvider,
|
||||
})),
|
||||
);
|
||||
|
||||
type PageDragDropProviderProps = {
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export const PageDragDropProvider = ({
|
||||
children,
|
||||
}: PageDragDropProviderProps) => {
|
||||
const [hasProviderMounted, setHasProviderMounted] = useState(false);
|
||||
|
||||
if (!hasProviderMounted) {
|
||||
return (
|
||||
<>
|
||||
<PageDragDropProviderMountEffect
|
||||
onEnterEditMode={() => setHasProviderMounted(true)}
|
||||
/>
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Suspense fallback={<>{children}</>}>
|
||||
<LazyWorkspaceDndKitProvider>{children}</LazyWorkspaceDndKitProvider>
|
||||
</Suspense>
|
||||
);
|
||||
};
|
||||
@@ -1,50 +0,0 @@
|
||||
import { PointerActivationConstraints } from '@dnd-kit/dom';
|
||||
import {
|
||||
DragDropProvider,
|
||||
KeyboardSensor,
|
||||
PointerSensor,
|
||||
} from '@dnd-kit/react';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
import { NavigationDragSourceContext } from '@/navigation-menu-item/common/contexts/NavigationDragSourceContext';
|
||||
import { NavigationDropTargetContext } from '@/navigation-menu-item/common/contexts/NavigationDropTargetContext';
|
||||
import { NavigationMenuItemDragContext } from '@/navigation-menu-item/common/contexts/NavigationMenuItemDragContext';
|
||||
import type { DraggableData } from '@/navigation/types/workspaceDndKitDraggableData';
|
||||
|
||||
import { useWorkspaceDndKit } from '@/navigation/hooks/useWorkspaceDndKit';
|
||||
|
||||
const WORKSPACE_DND_SENSORS = [
|
||||
PointerSensor.configure({
|
||||
activationConstraints: [
|
||||
new PointerActivationConstraints.Distance({ value: 8 }),
|
||||
],
|
||||
}),
|
||||
KeyboardSensor,
|
||||
];
|
||||
|
||||
type WorkspaceDndKitProviderProps = {
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export const WorkspaceDndKitProvider = ({
|
||||
children,
|
||||
}: WorkspaceDndKitProviderProps) => {
|
||||
const { contextValues, handlers } = useWorkspaceDndKit();
|
||||
|
||||
return (
|
||||
<NavigationDragSourceContext.Provider value={contextValues.dragSource}>
|
||||
<NavigationMenuItemDragContext.Provider value={contextValues.drag}>
|
||||
<NavigationDropTargetContext.Provider value={contextValues.dropTarget}>
|
||||
<DragDropProvider<DraggableData>
|
||||
sensors={WORKSPACE_DND_SENSORS}
|
||||
onDragStart={handlers.onDragStart}
|
||||
onDragOver={handlers.onDragOver}
|
||||
onDragEnd={handlers.onDragEnd}
|
||||
>
|
||||
{children}
|
||||
</DragDropProvider>
|
||||
</NavigationDropTargetContext.Provider>
|
||||
</NavigationMenuItemDragContext.Provider>
|
||||
</NavigationDragSourceContext.Provider>
|
||||
);
|
||||
};
|
||||
@@ -1,267 +0,0 @@
|
||||
import { type DragDropProvider } from '@dnd-kit/react';
|
||||
import { isSortable } from '@dnd-kit/react/sortable';
|
||||
import type { ResponderProvided } from '@hello-pangea/dnd';
|
||||
import { type ComponentProps, useCallback, useState } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
|
||||
|
||||
import { ADD_TO_NAV_SOURCE_DROPPABLE_ID } from '@/navigation-menu-item/common/constants/AddToNavSourceDroppableId';
|
||||
import { NavigationMenuItemDroppableIds } from '@/navigation-menu-item/common/constants/NavigationMenuItemDroppableIds';
|
||||
import { useHandleAddToNavigationDrop } from '@/navigation-menu-item/display/dnd/hooks/useHandleAddToNavigationDrop';
|
||||
import { useHandleNavigationMenuItemDragAndDrop } from '@/navigation-menu-item/display/dnd/hooks/useHandleNavigationMenuItemDragAndDrop';
|
||||
import { getDndKitDropTargetId } from '@/navigation-menu-item/common/utils/getDndKitDropTargetId';
|
||||
import { isNavigationMenuItemFolder } from '@/navigation-menu-item/common/utils/isNavigationMenuItemFolder';
|
||||
import { useNavigationMenuItemsData } from '@/navigation-menu-item/display/hooks/useNavigationMenuItemsData';
|
||||
import { useSortedNavigationMenuItems } from '@/navigation-menu-item/display/hooks/useSortedNavigationMenuItems';
|
||||
|
||||
import { DROP_RESULT_OPTIONS } from '@/navigation/constants/workspaceDndKitDropResultOptions';
|
||||
import type { DraggableData } from '@/navigation/types/workspaceDndKitDraggableData';
|
||||
import type { DropDestination } from '@/navigation/types/workspaceDndKitDropDestination';
|
||||
import { toDropResult } from '@/navigation/utils/workspaceDndKitToDropResult';
|
||||
|
||||
type DragStartPayload = Parameters<
|
||||
NonNullable<
|
||||
ComponentProps<typeof DragDropProvider<DraggableData>>['onDragStart']
|
||||
>
|
||||
>[0];
|
||||
type DragOverPayload = Parameters<
|
||||
NonNullable<
|
||||
ComponentProps<typeof DragDropProvider<DraggableData>>['onDragOver']
|
||||
>
|
||||
>[0];
|
||||
type DragEndPayload = Parameters<
|
||||
NonNullable<
|
||||
ComponentProps<typeof DragDropProvider<DraggableData>>['onDragEnd']
|
||||
>
|
||||
>[0];
|
||||
|
||||
export type FavoritesDndKitContextValues = {
|
||||
dragSource: { sourceDroppableId: string | null };
|
||||
drag: { isDragging: boolean };
|
||||
dropTarget: {
|
||||
activeDropTargetId: string | null;
|
||||
setActiveDropTargetId: (id: string | null) => void;
|
||||
forbiddenDropTargetId: string | null;
|
||||
setForbiddenDropTargetId: (id: string | null) => void;
|
||||
addToNavigationFallbackDestination: DropDestination | null;
|
||||
};
|
||||
};
|
||||
|
||||
const resolveFavoritesDropTarget = (
|
||||
target: {
|
||||
id?: unknown;
|
||||
group?: unknown;
|
||||
index?: unknown;
|
||||
data?: unknown;
|
||||
} | null,
|
||||
getNavItemById: (id: string | undefined) => NavigationMenuItem | undefined,
|
||||
): {
|
||||
destination: DropDestination;
|
||||
effectiveDropTargetId: string;
|
||||
isTargetFolder: boolean;
|
||||
} | null => {
|
||||
if (target === null || target === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isDefined(target.group) && isDefined(target.index)) {
|
||||
const group = String(target.group);
|
||||
const index = Number(target.index);
|
||||
if (!Number.isInteger(index) || index < 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const targetItem = getNavItemById(
|
||||
target.id != null ? String(target.id) : undefined,
|
||||
);
|
||||
const isTargetFolder =
|
||||
isDefined(targetItem) && isNavigationMenuItemFolder(targetItem);
|
||||
|
||||
const destination: DropDestination = isTargetFolder
|
||||
? { droppableId: `folder-header-${target.id}`, index: 0 }
|
||||
: { droppableId: group, index };
|
||||
|
||||
const effectiveDropTargetId = isTargetFolder
|
||||
? getDndKitDropTargetId(`folder-header-${target.id}`, 0)
|
||||
: getDndKitDropTargetId(group, index);
|
||||
|
||||
return { destination, effectiveDropTargetId, isTargetFolder };
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const useFavoritesDndKit = (): {
|
||||
contextValues: FavoritesDndKitContextValues;
|
||||
handlers: {
|
||||
onDragStart: (event: DragStartPayload) => void;
|
||||
onDragOver: (event: DragOverPayload) => void;
|
||||
onDragEnd: (event: DragEndPayload) => void;
|
||||
};
|
||||
} => {
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [sourceDroppableId, setSourceDroppableId] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
const [activeDropTargetId, setActiveDropTargetId] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
const [forbiddenDropTargetId, setForbiddenDropTargetId] = useState<
|
||||
string | null
|
||||
>(null);
|
||||
const [
|
||||
addToNavigationFallbackDestination,
|
||||
setAddToNavigationFallbackDestination,
|
||||
] = useState<DropDestination | null>(null);
|
||||
|
||||
const { navigationMenuItems } = useNavigationMenuItemsData();
|
||||
const { navigationMenuItemsSorted } = useSortedNavigationMenuItems();
|
||||
const { handleAddToNavigationDrop } = useHandleAddToNavigationDrop();
|
||||
const { handleNavigationMenuItemDragAndDrop } =
|
||||
useHandleNavigationMenuItemDragAndDrop();
|
||||
|
||||
const orphanItemCount = navigationMenuItemsSorted.filter(
|
||||
(item: { folderId?: string | null }) => !isDefined(item.folderId),
|
||||
).length;
|
||||
|
||||
const getNavItemById = useCallback(
|
||||
(id: string | undefined) =>
|
||||
id ? navigationMenuItems.find((item) => item.id === id) : undefined,
|
||||
[navigationMenuItems],
|
||||
);
|
||||
|
||||
const handleDragStart = (event: DragStartPayload) => {
|
||||
const { operation } = event;
|
||||
setIsDragging(true);
|
||||
const source = operation.source;
|
||||
const sourceId = source?.data?.sourceDroppableId ?? null;
|
||||
setSourceDroppableId(sourceId);
|
||||
|
||||
if (sourceId === ADD_TO_NAV_SOURCE_DROPPABLE_ID) {
|
||||
const defaultDestination: DropDestination = {
|
||||
droppableId:
|
||||
NavigationMenuItemDroppableIds.ORPHAN_NAVIGATION_MENU_ITEMS,
|
||||
index: orphanItemCount,
|
||||
};
|
||||
setAddToNavigationFallbackDestination(defaultDestination);
|
||||
setActiveDropTargetId(
|
||||
getDndKitDropTargetId(
|
||||
defaultDestination.droppableId,
|
||||
defaultDestination.index,
|
||||
),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDragOver = useCallback(
|
||||
(event: DragOverPayload) => {
|
||||
const { operation } = event;
|
||||
const source = operation.source;
|
||||
const target = operation.target;
|
||||
const isAddToNavDrag =
|
||||
sourceDroppableId === ADD_TO_NAV_SOURCE_DROPPABLE_ID;
|
||||
const resolved = resolveFavoritesDropTarget(target, getNavItemById);
|
||||
|
||||
if (
|
||||
resolved !== null &&
|
||||
source !== null &&
|
||||
target !== null &&
|
||||
isSortable(source) &&
|
||||
isSortable(target)
|
||||
) {
|
||||
setActiveDropTargetId(resolved.effectiveDropTargetId);
|
||||
setForbiddenDropTargetId(null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (resolved !== null && source !== null && isSortable(source)) {
|
||||
setActiveDropTargetId(resolved.effectiveDropTargetId);
|
||||
setAddToNavigationFallbackDestination(resolved.destination);
|
||||
setForbiddenDropTargetId(null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isAddToNavDrag) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (resolved !== null) {
|
||||
setAddToNavigationFallbackDestination(resolved.destination);
|
||||
setActiveDropTargetId(resolved.effectiveDropTargetId);
|
||||
setForbiddenDropTargetId(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const fallback = addToNavigationFallbackDestination;
|
||||
setActiveDropTargetId(
|
||||
fallback
|
||||
? getDndKitDropTargetId(fallback.droppableId, fallback.index)
|
||||
: null,
|
||||
);
|
||||
setForbiddenDropTargetId(null);
|
||||
},
|
||||
[
|
||||
sourceDroppableId,
|
||||
addToNavigationFallbackDestination,
|
||||
getNavItemById,
|
||||
setActiveDropTargetId,
|
||||
setForbiddenDropTargetId,
|
||||
setAddToNavigationFallbackDestination,
|
||||
],
|
||||
);
|
||||
|
||||
const handleDragEnd = (event: DragEndPayload) => {
|
||||
const { operation } = event;
|
||||
const source = operation.source;
|
||||
const target = operation.target;
|
||||
const draggableId = String(source?.id);
|
||||
const data = source?.data;
|
||||
const sourceId = data?.sourceDroppableId ?? null;
|
||||
const fallback = addToNavigationFallbackDestination;
|
||||
|
||||
setIsDragging(false);
|
||||
setSourceDroppableId(null);
|
||||
setActiveDropTargetId(null);
|
||||
setForbiddenDropTargetId(null);
|
||||
setAddToNavigationFallbackDestination(null);
|
||||
|
||||
const resolved = resolveFavoritesDropTarget(target, getNavItemById);
|
||||
let destination: DropDestination | null = resolved?.destination ?? null;
|
||||
|
||||
if (destination == null && isDefined(fallback)) {
|
||||
destination = fallback;
|
||||
}
|
||||
|
||||
const result = toDropResult(draggableId, data, destination);
|
||||
const dropResult = { ...result, ...DROP_RESULT_OPTIONS };
|
||||
const provided: ResponderProvided = { announce: () => {} };
|
||||
|
||||
if (sourceId === ADD_TO_NAV_SOURCE_DROPPABLE_ID) {
|
||||
handleAddToNavigationDrop(dropResult, provided);
|
||||
return;
|
||||
}
|
||||
|
||||
handleNavigationMenuItemDragAndDrop(dropResult, provided);
|
||||
};
|
||||
|
||||
const contextValues: FavoritesDndKitContextValues = {
|
||||
dragSource: { sourceDroppableId },
|
||||
drag: { isDragging },
|
||||
dropTarget: {
|
||||
activeDropTargetId,
|
||||
setActiveDropTargetId,
|
||||
forbiddenDropTargetId,
|
||||
setForbiddenDropTargetId,
|
||||
addToNavigationFallbackDestination,
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
contextValues,
|
||||
handlers: {
|
||||
onDragStart: handleDragStart,
|
||||
onDragOver: handleDragOver,
|
||||
onDragEnd: handleDragEnd,
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -1,360 +0,0 @@
|
||||
import { type DragDropProvider } from '@dnd-kit/react';
|
||||
import { isSortable } from '@dnd-kit/react/sortable';
|
||||
import type { ResponderProvided } from '@hello-pangea/dnd';
|
||||
import { useStore } from 'jotai';
|
||||
import { type ComponentProps, useCallback, useState } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { ADD_TO_NAV_SOURCE_DROPPABLE_ID } from '@/navigation-menu-item/common/constants/AddToNavSourceDroppableId';
|
||||
import { NavigationMenuItemDroppableIds } from '@/navigation-menu-item/common/constants/NavigationMenuItemDroppableIds';
|
||||
import { useHandleAddToNavigationDrop } from '@/navigation-menu-item/display/dnd/hooks/useHandleAddToNavigationDrop';
|
||||
import { useHandleWorkspaceNavigationMenuItemDragAndDrop } from '@/navigation-menu-item/display/dnd/hooks/useHandleWorkspaceNavigationMenuItemDragAndDrop';
|
||||
import { useNavigationMenuItemsDraftState } from '@/navigation-menu-item/edit/hooks/useNavigationMenuItemsDraftState';
|
||||
import { addToNavPayloadRegistryState } from '@/navigation-menu-item/common/states/addToNavPayloadRegistryState';
|
||||
import { getDndKitDropTargetId } from '@/navigation-menu-item/common/utils/getDndKitDropTargetId';
|
||||
import { isNavigationMenuItemFolder } from '@/navigation-menu-item/common/utils/isNavigationMenuItemFolder';
|
||||
import { isWorkspaceDroppableId } from '@/navigation-menu-item/common/utils/isWorkspaceDroppableId';
|
||||
import { validateAndExtractWorkspaceFolderId } from '@/navigation-menu-item/common/utils/validateAndExtractWorkspaceFolderId';
|
||||
|
||||
import { DROP_RESULT_OPTIONS } from '@/navigation/constants/workspaceDndKitDropResultOptions';
|
||||
import type { DraggableData } from '@/navigation/types/workspaceDndKitDraggableData';
|
||||
import type { DropDestination } from '@/navigation/types/workspaceDndKitDropDestination';
|
||||
import { isFolderDrag } from '@/navigation/utils/workspaceDndKitIsFolderDrag';
|
||||
import { resolveDropTarget } from '@/navigation/utils/workspaceDndKitResolveDropTarget';
|
||||
import { toDropResult } from '@/navigation/utils/workspaceDndKitToDropResult';
|
||||
|
||||
type DragStartPayload = Parameters<
|
||||
NonNullable<
|
||||
ComponentProps<typeof DragDropProvider<DraggableData>>['onDragStart']
|
||||
>
|
||||
>[0];
|
||||
type DragOverPayload = Parameters<
|
||||
NonNullable<
|
||||
ComponentProps<typeof DragDropProvider<DraggableData>>['onDragOver']
|
||||
>
|
||||
>[0];
|
||||
type DragEndPayload = Parameters<
|
||||
NonNullable<
|
||||
ComponentProps<typeof DragDropProvider<DraggableData>>['onDragEnd']
|
||||
>
|
||||
>[0];
|
||||
|
||||
export type WorkspaceDndKitContextValues = {
|
||||
dragSource: { sourceDroppableId: string | null };
|
||||
drag: { isDragging: boolean };
|
||||
dropTarget: {
|
||||
activeDropTargetId: string | null;
|
||||
setActiveDropTargetId: (id: string | null) => void;
|
||||
forbiddenDropTargetId: string | null;
|
||||
setForbiddenDropTargetId: (id: string | null) => void;
|
||||
addToNavigationFallbackDestination: DropDestination | null;
|
||||
};
|
||||
};
|
||||
|
||||
export const useWorkspaceDndKit = (): {
|
||||
contextValues: WorkspaceDndKitContextValues;
|
||||
handlers: {
|
||||
onDragStart: (event: DragStartPayload) => void;
|
||||
onDragOver: (event: DragOverPayload) => void;
|
||||
onDragEnd: (event: DragEndPayload) => void;
|
||||
};
|
||||
} => {
|
||||
const store = useStore();
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [sourceDroppableId, setSourceDroppableId] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
const [activeDropTargetId, setActiveDropTargetId] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
const [forbiddenDropTargetId, setForbiddenDropTargetId] = useState<
|
||||
string | null
|
||||
>(null);
|
||||
const [
|
||||
addToNavigationFallbackDestination,
|
||||
setAddToNavigationFallbackDestination,
|
||||
] = useState<DropDestination | null>(null);
|
||||
|
||||
const { workspaceNavigationMenuItems } = useNavigationMenuItemsDraftState();
|
||||
const { handleAddToNavigationDrop } = useHandleAddToNavigationDrop();
|
||||
const { handleWorkspaceNavigationMenuItemDragAndDrop } =
|
||||
useHandleWorkspaceNavigationMenuItemDragAndDrop();
|
||||
|
||||
const orphanItemCount = workspaceNavigationMenuItems.filter(
|
||||
(item: { folderId?: string | null }) => !isDefined(item.folderId),
|
||||
).length;
|
||||
|
||||
const getNavItemById = useCallback(
|
||||
(id: string | undefined) =>
|
||||
id
|
||||
? workspaceNavigationMenuItems.find((item) => item.id === id)
|
||||
: undefined,
|
||||
[workspaceNavigationMenuItems],
|
||||
);
|
||||
|
||||
const applyWorkspaceReorderIfAllowed = (
|
||||
id: string,
|
||||
source: DropDestination,
|
||||
destination: DropDestination,
|
||||
insertBeforeItemId?: string | null,
|
||||
) => {
|
||||
const draggedItem = getNavItemById(id);
|
||||
const destFolderId = validateAndExtractWorkspaceFolderId(
|
||||
destination.droppableId,
|
||||
);
|
||||
if (
|
||||
isDefined(destFolderId) &&
|
||||
isDefined(draggedItem) &&
|
||||
isNavigationMenuItemFolder(draggedItem)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const result = toDropResult(
|
||||
id,
|
||||
{
|
||||
sourceDroppableId: source.droppableId,
|
||||
sourceIndex: source.index,
|
||||
},
|
||||
destination,
|
||||
);
|
||||
const provided: ResponderProvided = { announce: () => {} };
|
||||
handleWorkspaceNavigationMenuItemDragAndDrop(
|
||||
{
|
||||
...result,
|
||||
...DROP_RESULT_OPTIONS,
|
||||
...(insertBeforeItemId != null && { insertBeforeItemId }),
|
||||
},
|
||||
provided,
|
||||
);
|
||||
};
|
||||
|
||||
const handleDragStart = (event: DragStartPayload) => {
|
||||
const { operation } = event;
|
||||
setIsDragging(true);
|
||||
const source = operation.source;
|
||||
const sourceId = source?.data?.sourceDroppableId ?? null;
|
||||
setSourceDroppableId(sourceId);
|
||||
|
||||
if (sourceId === ADD_TO_NAV_SOURCE_DROPPABLE_ID) {
|
||||
const defaultDestination: DropDestination = {
|
||||
droppableId:
|
||||
NavigationMenuItemDroppableIds.WORKSPACE_ORPHAN_NAVIGATION_MENU_ITEMS,
|
||||
index: orphanItemCount,
|
||||
};
|
||||
setAddToNavigationFallbackDestination(defaultDestination);
|
||||
setActiveDropTargetId(
|
||||
getDndKitDropTargetId(
|
||||
defaultDestination.droppableId,
|
||||
defaultDestination.index,
|
||||
),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDragOver = useCallback(
|
||||
(event: DragOverPayload) => {
|
||||
const { operation } = event;
|
||||
const source = operation.source;
|
||||
const target = operation.target;
|
||||
const isAddToNavDrag =
|
||||
sourceDroppableId === ADD_TO_NAV_SOURCE_DROPPABLE_ID;
|
||||
const sourceIsSortable = source !== null && isSortable(source);
|
||||
const resolved = resolveDropTarget(target, getNavItemById);
|
||||
|
||||
const getPayload = () =>
|
||||
store.get(addToNavPayloadRegistryState.atom).get(String(source?.id)) ??
|
||||
null;
|
||||
const getSourceItem = () =>
|
||||
getNavItemById(source?.id != null ? String(source.id) : undefined);
|
||||
|
||||
if (
|
||||
resolved !== null &&
|
||||
source !== null &&
|
||||
target !== null &&
|
||||
isSortable(source) &&
|
||||
isSortable(target)
|
||||
) {
|
||||
setActiveDropTargetId(resolved.effectiveDropTargetId);
|
||||
if (isAddToNavDrag) {
|
||||
setForbiddenDropTargetId(null);
|
||||
} else {
|
||||
const destFolderId =
|
||||
'group' in target
|
||||
? validateAndExtractWorkspaceFolderId(String(target.group))
|
||||
: validateAndExtractWorkspaceFolderId(
|
||||
resolved.destination.droppableId,
|
||||
);
|
||||
const folderDrag = isFolderDrag(getPayload(), getSourceItem());
|
||||
const isFolderOverFolder = resolved.isTargetFolder && folderDrag;
|
||||
const isFolderOverFolderInList =
|
||||
!resolved.isTargetFolder && isDefined(destFolderId) && folderDrag;
|
||||
setForbiddenDropTargetId(
|
||||
isFolderOverFolder
|
||||
? resolved.effectiveDropTargetId
|
||||
: isFolderOverFolderInList
|
||||
? resolved.dropTargetId
|
||||
: null,
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (resolved !== null && sourceIsSortable) {
|
||||
setActiveDropTargetId(resolved.effectiveDropTargetId);
|
||||
setAddToNavigationFallbackDestination(resolved.destination);
|
||||
if (!isAddToNavDrag) {
|
||||
const destFolderId = validateAndExtractWorkspaceFolderId(
|
||||
resolved.destination.droppableId,
|
||||
);
|
||||
const folderDrag = isFolderDrag(null, getSourceItem());
|
||||
setForbiddenDropTargetId(
|
||||
isDefined(destFolderId) && folderDrag
|
||||
? resolved.effectiveDropTargetId
|
||||
: null,
|
||||
);
|
||||
} else {
|
||||
setForbiddenDropTargetId(null);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isAddToNavDrag) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (resolved !== null) {
|
||||
setAddToNavigationFallbackDestination(resolved.destination);
|
||||
setActiveDropTargetId(resolved.effectiveDropTargetId);
|
||||
const folderId = validateAndExtractWorkspaceFolderId(
|
||||
resolved.destination.droppableId,
|
||||
);
|
||||
const folderDrag =
|
||||
getPayload()?.type === 'FOLDER' && isDefined(folderId);
|
||||
setForbiddenDropTargetId(
|
||||
folderDrag ? resolved.effectiveDropTargetId : null,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const fallback = addToNavigationFallbackDestination;
|
||||
setActiveDropTargetId(
|
||||
fallback
|
||||
? getDndKitDropTargetId(fallback.droppableId, fallback.index)
|
||||
: null,
|
||||
);
|
||||
setForbiddenDropTargetId(null);
|
||||
},
|
||||
[
|
||||
sourceDroppableId,
|
||||
addToNavigationFallbackDestination,
|
||||
getNavItemById,
|
||||
setActiveDropTargetId,
|
||||
setForbiddenDropTargetId,
|
||||
setAddToNavigationFallbackDestination,
|
||||
store,
|
||||
],
|
||||
);
|
||||
|
||||
const handleDragEnd = (event: DragEndPayload) => {
|
||||
const { operation } = event;
|
||||
const source = operation.source;
|
||||
const target = operation.target;
|
||||
const draggableId = String(source?.id);
|
||||
const data = source?.data;
|
||||
const sourceId = data?.sourceDroppableId ?? null;
|
||||
const fallback = addToNavigationFallbackDestination;
|
||||
|
||||
setIsDragging(false);
|
||||
setSourceDroppableId(null);
|
||||
setActiveDropTargetId(null);
|
||||
setForbiddenDropTargetId(null);
|
||||
setAddToNavigationFallbackDestination(null);
|
||||
|
||||
const sourceIsSortable = source !== null && isSortable(source);
|
||||
const targetIsSortable = target !== null && isSortable(target);
|
||||
const sortableToSortable =
|
||||
sourceIsSortable &&
|
||||
targetIsSortable &&
|
||||
isDefined(source) &&
|
||||
isDefined(target);
|
||||
const resolved = resolveDropTarget(target, getNavItemById);
|
||||
|
||||
if (sortableToSortable && resolved !== null) {
|
||||
const sourceDraggable = 'initialGroup' in source ? source : null;
|
||||
const initialGroup = sourceDraggable?.initialGroup ?? '';
|
||||
const initialIndex = sourceDraggable?.initialIndex ?? 0;
|
||||
const initialGroupStr = String(initialGroup);
|
||||
const destGroup = String(target.group ?? '');
|
||||
const bothWorkspace =
|
||||
isWorkspaceDroppableId(initialGroupStr) &&
|
||||
isWorkspaceDroppableId(destGroup);
|
||||
if (bothWorkspace) {
|
||||
const insertBeforeItemId = resolved.isTargetFolder
|
||||
? null
|
||||
: String(target?.id ?? '');
|
||||
applyWorkspaceReorderIfAllowed(
|
||||
draggableId,
|
||||
{ droppableId: initialGroupStr, index: initialIndex },
|
||||
resolved.destination,
|
||||
insertBeforeItemId || undefined,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let destination: DropDestination | null = resolved?.destination ?? null;
|
||||
if (
|
||||
destination == null &&
|
||||
isDefined(fallback) &&
|
||||
isWorkspaceDroppableId(fallback.droppableId)
|
||||
) {
|
||||
destination = fallback;
|
||||
}
|
||||
|
||||
const result = toDropResult(draggableId, data, destination);
|
||||
const provided: ResponderProvided = { announce: () => {} };
|
||||
const dropResult = { ...result, ...DROP_RESULT_OPTIONS };
|
||||
|
||||
if (sourceId === ADD_TO_NAV_SOURCE_DROPPABLE_ID) {
|
||||
handleAddToNavigationDrop(dropResult, provided);
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
isDefined(sourceId) &&
|
||||
isWorkspaceDroppableId(sourceId) &&
|
||||
isDefined(destination) &&
|
||||
isWorkspaceDroppableId(destination.droppableId)
|
||||
) {
|
||||
applyWorkspaceReorderIfAllowed(
|
||||
draggableId,
|
||||
{
|
||||
droppableId: data?.sourceDroppableId ?? '',
|
||||
index: data?.sourceIndex ?? 0,
|
||||
},
|
||||
destination,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const contextValues: WorkspaceDndKitContextValues = {
|
||||
dragSource: { sourceDroppableId },
|
||||
drag: { isDragging },
|
||||
dropTarget: {
|
||||
activeDropTargetId,
|
||||
setActiveDropTargetId,
|
||||
forbiddenDropTargetId,
|
||||
setForbiddenDropTargetId,
|
||||
addToNavigationFallbackDestination,
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
contextValues,
|
||||
handlers: {
|
||||
onDragStart: handleDragStart,
|
||||
onDragOver: handleDragOver,
|
||||
onDragEnd: handleDragEnd,
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -1,16 +0,0 @@
|
||||
let preloadScheduled = false;
|
||||
|
||||
const preload = () => {
|
||||
void import('@/navigation/components/WorkspaceDndKitProvider');
|
||||
void import(
|
||||
'@/navigation-menu-item/display/sections/components/NavigationDrawerSectionForWorkspaceItemsListDndKit'
|
||||
);
|
||||
};
|
||||
|
||||
export const preloadWorkspaceDndKit = (): void => {
|
||||
if (preloadScheduled) {
|
||||
return;
|
||||
}
|
||||
preloadScheduled = true;
|
||||
preload();
|
||||
};
|
||||
@@ -1,13 +0,0 @@
|
||||
import type { NavigationMenuItem } from '~/generated-metadata/graphql';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { isNavigationMenuItemFolder } from '@/navigation-menu-item/common/utils/isNavigationMenuItemFolder';
|
||||
|
||||
type AddToNavPayload = { type?: string } | null;
|
||||
|
||||
export const isFolderDrag = (
|
||||
payload: AddToNavPayload,
|
||||
sourceItem: NavigationMenuItem | undefined,
|
||||
): boolean =>
|
||||
payload?.type === 'FOLDER' ||
|
||||
(isDefined(sourceItem) && isNavigationMenuItemFolder(sourceItem));
|
||||
+4
-4
@@ -1,8 +1,8 @@
|
||||
import { styled } from '@linaria/react';
|
||||
|
||||
import { NavigationDrawerSectionForWorkspaceItemsListReadOnly } from '@/navigation-menu-item/display/sections/components/NavigationDrawerSectionForWorkspaceItemsListReadOnly';
|
||||
import { WorkspaceSectionListReadOnly } from '@/navigation-menu-item/display/sections/workspace/components/WorkspaceSectionListReadOnly';
|
||||
import { WorkspaceSectionAddMenuItemButton } from '@/navigation-menu-item/edit/components/WorkspaceSectionAddMenuItemButton';
|
||||
import type { WorkspaceSectionListDndKitProps } from '@/navigation-menu-item/display/sections/types/WorkspaceSectionListDndKitProps';
|
||||
import type { NavigationMenuItemSectionListDndKitProps } from '@/navigation-menu-item/display/sections/types/NavigationMenuItemSectionListDndKitProps';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
const StyledFallback = styled.div`
|
||||
@@ -12,7 +12,7 @@ const StyledFallback = styled.div`
|
||||
`;
|
||||
|
||||
type WorkspaceSectionListEditModeFallbackProps = Pick<
|
||||
WorkspaceSectionListDndKitProps,
|
||||
NavigationMenuItemSectionListDndKitProps,
|
||||
'filteredItems' | 'folderChildrenById' | 'onActiveObjectMetadataItemClick'
|
||||
>;
|
||||
|
||||
@@ -22,7 +22,7 @@ export const WorkspaceSectionListEditModeFallback = ({
|
||||
onActiveObjectMetadataItemClick,
|
||||
}: WorkspaceSectionListEditModeFallbackProps) => (
|
||||
<StyledFallback>
|
||||
<NavigationDrawerSectionForWorkspaceItemsListReadOnly
|
||||
<WorkspaceSectionListReadOnly
|
||||
filteredItems={filteredItems}
|
||||
folderChildrenById={folderChildrenById}
|
||||
onActiveObjectMetadataItemClick={onActiveObjectMetadataItemClick}
|
||||
|
||||
@@ -9,7 +9,7 @@ import { NavigationMenuItemStyleIcon } from '@/navigation-menu-item/display/comp
|
||||
import { FOLDER_ICON_DEFAULT } from '@/navigation-menu-item/common/constants/FolderIconDefault';
|
||||
import { NavigationMenuItemType } from 'twenty-shared/types';
|
||||
import { useUpdateFolderInDraft } from '@/navigation-menu-item/edit/folder/hooks/useUpdateFolderInDraft';
|
||||
import { useWorkspaceSectionItems } from '@/navigation-menu-item/display/hooks/useWorkspaceSectionItems';
|
||||
import { useNavigationMenuItemSectionItems } from '@/navigation-menu-item/display/hooks/useNavigationMenuItemSectionItems';
|
||||
import { selectedNavigationMenuItemInEditModeState } from '@/navigation-menu-item/common/states/selectedNavigationMenuItemInEditModeState';
|
||||
import { IconPicker } from '@/ui/input/components/IconPicker';
|
||||
import { TitleInput } from '@/ui/input/components/TitleInput';
|
||||
@@ -32,7 +32,7 @@ export const SidePanelFolderInfo = () => {
|
||||
const selectedNavigationMenuItemInEditMode = useAtomStateValue(
|
||||
selectedNavigationMenuItemInEditModeState,
|
||||
);
|
||||
const items = useWorkspaceSectionItems();
|
||||
const items = useNavigationMenuItemSectionItems();
|
||||
const { updateFolderInDraft } = useUpdateFolderInDraft();
|
||||
|
||||
const defaultLabel = t`New folder`;
|
||||
|
||||
@@ -4,7 +4,7 @@ import { IconLink, IconWorld } from 'twenty-ui/display';
|
||||
import { LinkIconWithLinkOverlay } from '@/navigation-menu-item/display/link/components/LinkIconWithLinkOverlay';
|
||||
import { NavigationMenuItemType } from 'twenty-shared/types';
|
||||
import { useUpdateLinkInDraft } from '@/navigation-menu-item/edit/link/hooks/useUpdateLinkInDraft';
|
||||
import { useWorkspaceSectionItems } from '@/navigation-menu-item/display/hooks/useWorkspaceSectionItems';
|
||||
import { useNavigationMenuItemSectionItems } from '@/navigation-menu-item/display/hooks/useNavigationMenuItemSectionItems';
|
||||
import { selectedNavigationMenuItemInEditModeState } from '@/navigation-menu-item/common/states/selectedNavigationMenuItemInEditModeState';
|
||||
import { SidePanelPageInfoLayout } from '@/side-panel/components/SidePanelPageInfoLayout';
|
||||
import { sidePanelPageInfoState } from '@/side-panel/states/sidePanelPageInfoState';
|
||||
@@ -24,7 +24,7 @@ export const SidePanelLinkInfo = () => {
|
||||
const selectedNavigationMenuItemInEditMode = useAtomStateValue(
|
||||
selectedNavigationMenuItemInEditModeState,
|
||||
);
|
||||
const items = useWorkspaceSectionItems();
|
||||
const items = useNavigationMenuItemSectionItems();
|
||||
const { updateLinkInDraft } = useUpdateLinkInDraft();
|
||||
|
||||
const defaultLabel = t`Link label`;
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
} from 'twenty-ui/display';
|
||||
|
||||
import { NavigationMenuItemType, SidePanelPages } from 'twenty-shared/types';
|
||||
import { useWorkspaceSectionItems } from '@/navigation-menu-item/display/hooks/useWorkspaceSectionItems';
|
||||
import { useNavigationMenuItemSectionItems } from '@/navigation-menu-item/display/hooks/useNavigationMenuItemSectionItems';
|
||||
import { selectedNavigationMenuItemInEditModeState } from '@/navigation-menu-item/common/states/selectedNavigationMenuItemInEditModeState';
|
||||
import { SidePanelAskAIInfo } from '@/side-panel/components/SidePanelAskAIInfo';
|
||||
import { SidePanelFolderInfo } from '@/side-panel/components/SidePanelFolderInfo';
|
||||
@@ -37,7 +37,7 @@ export const SidePanelPageInfo = ({ pageChip }: SidePanelPageInfoProps) => {
|
||||
const selectedNavigationMenuItemInEditMode = useAtomStateValue(
|
||||
selectedNavigationMenuItemInEditModeState,
|
||||
);
|
||||
const items = useWorkspaceSectionItems();
|
||||
const items = useNavigationMenuItemSectionItems();
|
||||
|
||||
if (!isDefined(pageChip)) {
|
||||
return null;
|
||||
|
||||
+18
-8
@@ -66,11 +66,10 @@ describe('calculateNewPosition', () => {
|
||||
items,
|
||||
});
|
||||
|
||||
expect(result).toBe(Math.round(30 + (20 - 30) / 2));
|
||||
expect(result).toBe(25);
|
||||
});
|
||||
|
||||
it('should round fractional midpoints', () => {
|
||||
it('should return fractional midpoints for adjacent positions', () => {
|
||||
const items = createItems([1, 2, 5]);
|
||||
|
||||
const result = calculateNewPosition({
|
||||
@@ -79,8 +78,7 @@ describe('calculateNewPosition', () => {
|
||||
items,
|
||||
});
|
||||
|
||||
expect(result).toBe(Math.round(5 + (2 - 5) / 2));
|
||||
expect(result).toBe(4);
|
||||
expect(result).toBe(3.5);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -94,11 +92,10 @@ describe('calculateNewPosition', () => {
|
||||
items,
|
||||
});
|
||||
|
||||
expect(result).toBe(Math.round(20 - (20 - 10) / 2));
|
||||
expect(result).toBe(15);
|
||||
});
|
||||
|
||||
it('should round fractional midpoints', () => {
|
||||
it('should return fractional midpoints for adjacent positions', () => {
|
||||
const items = createItems([1, 4, 10]);
|
||||
|
||||
const result = calculateNewPosition({
|
||||
@@ -107,8 +104,21 @@ describe('calculateNewPosition', () => {
|
||||
items,
|
||||
});
|
||||
|
||||
expect(result).toBe(Math.round(4 - (4 - 1) / 2));
|
||||
expect(result).toBe(3);
|
||||
expect(result).toBe(2.5);
|
||||
});
|
||||
|
||||
it('should produce unique position for sequential integers', () => {
|
||||
const items = createItems([1, 2]);
|
||||
|
||||
const result = calculateNewPosition({
|
||||
destinationIndex: 1,
|
||||
sourceIndex: 2,
|
||||
items,
|
||||
});
|
||||
|
||||
expect(result).toBe(1.5);
|
||||
expect(result).not.toBe(items[0].position);
|
||||
expect(result).not.toBe(items[1].position);
|
||||
});
|
||||
|
||||
it('should handle destinationIndex equal to sourceIndex', () => {
|
||||
|
||||
+7
-8
@@ -18,18 +18,17 @@ export const calculateNewPosition = ({
|
||||
}
|
||||
|
||||
if (destinationIndex > sourceIndex) {
|
||||
return Math.round(
|
||||
return (
|
||||
items[destinationIndex].position +
|
||||
(items[destinationIndex - 1].position -
|
||||
items[destinationIndex].position) /
|
||||
2,
|
||||
(items[destinationIndex - 1].position -
|
||||
items[destinationIndex].position) /
|
||||
2
|
||||
);
|
||||
}
|
||||
|
||||
return Math.round(
|
||||
return (
|
||||
items[destinationIndex].position -
|
||||
(items[destinationIndex].position -
|
||||
items[destinationIndex - 1].position) /
|
||||
2,
|
||||
(items[destinationIndex].position - items[destinationIndex - 1].position) /
|
||||
2
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
export const FOLDER_DROPPABLE_IDS = {
|
||||
FOLDER_PREFIX: 'folder-',
|
||||
FOLDER_HEADER_PREFIX: 'folder-header-',
|
||||
} as const;
|
||||
-48
@@ -1,48 +0,0 @@
|
||||
import { CustomError } from 'twenty-shared/utils';
|
||||
import { FOLDER_DROPPABLE_IDS } from './folderDroppableIds';
|
||||
|
||||
type ValidateAndExtractFolderIdParams = {
|
||||
droppableId: string;
|
||||
// TODO: Remove orphanDroppableId prop when deleting all favorites code
|
||||
orphanDroppableId: string;
|
||||
};
|
||||
|
||||
export const validateAndExtractFolderId = ({
|
||||
droppableId,
|
||||
orphanDroppableId,
|
||||
}: ValidateAndExtractFolderIdParams): string | null => {
|
||||
if (droppableId === orphanDroppableId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (droppableId.startsWith(FOLDER_DROPPABLE_IDS.FOLDER_HEADER_PREFIX)) {
|
||||
const folderId = droppableId.replace(
|
||||
FOLDER_DROPPABLE_IDS.FOLDER_HEADER_PREFIX,
|
||||
'',
|
||||
);
|
||||
if (!folderId)
|
||||
throw new CustomError(
|
||||
`Invalid folder header ID: ${droppableId}`,
|
||||
'INVALID_FOLDER_HEADER_ID',
|
||||
);
|
||||
return folderId;
|
||||
}
|
||||
|
||||
if (droppableId.startsWith(FOLDER_DROPPABLE_IDS.FOLDER_PREFIX)) {
|
||||
const folderId = droppableId.replace(
|
||||
FOLDER_DROPPABLE_IDS.FOLDER_PREFIX,
|
||||
'',
|
||||
);
|
||||
if (!folderId)
|
||||
throw new CustomError(
|
||||
`Invalid folder ID: ${droppableId}`,
|
||||
'INVALID_FOLDER_ID',
|
||||
);
|
||||
return folderId;
|
||||
}
|
||||
|
||||
throw new CustomError(
|
||||
`Invalid droppable ID format: ${droppableId}`,
|
||||
'INVALID_DROPPABLE_ID_FORMAT',
|
||||
);
|
||||
};
|
||||
@@ -8,7 +8,7 @@ import { KeyboardShortcutMenu } from '@/keyboard-shortcut-menu/components/Keyboa
|
||||
import { LayoutCustomizationBar } from '@/layout-customization/components/LayoutCustomizationBar';
|
||||
import { AppNavigationDrawer } from '@/navigation/components/AppNavigationDrawer';
|
||||
import { MobileNavigationBar } from '@/navigation/components/MobileNavigationBar';
|
||||
import { PageDragDropProvider } from '@/navigation/components/PageDragDropProvider';
|
||||
import { PageDragDropProvider } from '@/navigation-menu-item/display/dnd/providers/PageDragDropProvider';
|
||||
import { useIsSettingsPage } from '@/navigation/hooks/useIsSettingsPage';
|
||||
import { OBJECT_SETTINGS_WIDTH } from '@/settings/data-model/constants/ObjectSettings';
|
||||
import { SignInAppNavigationDrawerMock } from '@/sign-in-background-mock/components/SignInAppNavigationDrawerMock';
|
||||
|
||||
Reference in New Issue
Block a user